Keyan Mohsenin

Conway's Game of Life

Since December 2025, I’ve devoted most of my free time to the 20 Games Challenge, which describes itself as “a codified approach to deliberately learning the fundamentals of game development” in twenty games or less. Game development is a challenging, often demoralizing discipline, and it’s easy to lose time bouncing between tutorials and the sort of overambitious projects that invariably end in disappointment.

The 20 Games Challenge offers a middle way: each step along the path presents a goal—clone Pong, for example—and tasks the student with working out how to achieve it.

As of March 2026, I’ve completed five projects, but the most recent—a recreation of Conway’s Game of Life—is the first one that I’ve been brave enough to post on itch.io. This blog post will serve as a sort of post-mortem devlog for the project.

What is Conway’s Game of Life?

Conway’s Game of Life is a zero-player game devised by the mathematician John Conway. The game consists of a grid of square cells, each of which is either live or dead. Each cell references its four orthogonal and four diagonal neighbors to determine whether it will survive to the next generation:

  1. A live cell with one or fewer live neighbors dies from isolation.
  2. A live cell with two or three live neighbors survives to the next generation.
  3. A live cell with four or more live neighbors dies from overpopulation.
  4. A dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

The output of these simple rules is a surprisingly engaging simulation that has fascinated mathematicians, scientists, programmers, philosophers, musicians, and more for over half a century. You can read more about the many patterns discovered by Life enthusiasts over the years at conwaylife.com/wiki.

My implementation

I treated this project as an opportunity to practice using dictionaries and Godot’s _draw() function. Obviously, I can’t claim that this is the most performant solution; I don’t know enough about math or programming to even guess at a more elegant alternative.

Setting up the grid

This is pretty straightforward. The grid is defined by a dictionary comprising boolean values referenced by Vector2i keys. For example, referencing the cell Vector2i(8, 12) returns either true or false, which tells us whether the cell is live or dead.

func _ready() -> void:
	for x in grid_size.x:
			for y in grid_size.y:
				cell_dictionary.set(Vector2i(x, y), false)
				y += 1
			x += 1

In the _draw() function, I use the same grid_size variable to render the grid to the screen. I start by drawing a black square for every true cell, then draw the actual grid overtop of the squares so the lines aren’t occluded.

func _draw() -> void:
	for cell in cell_dictionary:
		if cell_dictionary[cell]:
			var cell_rect := Rect2(cell * cell_size, Vector2.ONE * cell_size)
			draw_rect(cell_rect, Color.BLACK, true)
	
	for x in grid_size.x + 1:
		draw_line(Vector2(x * cell_size, 0), Vector2(x * cell_size, grid_size.y * cell_size), Color.LIGHT_BLUE, 1.0)
	for y in grid_size.y + 1:
		draw_line(Vector2(0, y * cell_size), Vector2(grid_size.x * cell_size, y * cell_size), Color.LIGHT_BLUE, 1.0)

Starting a new generation

New generations are triggered by the timeout() signal of a Timer node set to 0.25 seconds. At the start of each new generation, I create a copy of cell_dictionary and iterate through every key-value pair to determine whether it should be true or false.

func _on_timer_timeout() -> void:
	var updated_dictionary := cell_dictionary.duplicate()
	for cell in cell_dictionary:
		var neighbor_array := _get_neighbors(cell)
		var living_neighbors := _count_living(neighbor_array)
		
		if cell_dictionary[cell]:
			if living_neighbors < 2 or living_neighbors > 3:
				updated_dictionary.set(cell, false)
			else:
				updated_dictionary.set(cell, true)
		
		if !cell_dictionary[cell]:
			if living_neighbors == 3:
				updated_dictionary.set(cell, true)
	
	cell_dictionary.assign(updated_dictionary)
	queue_redraw()

I then assign the updated dictionary to cell_dictionary and use queue_redraw() to call the _draw() function again. Timers in Godot automatically restart; after another 0.25 seconds the whole process will repeat until the player clicks Stop or Reset.

Possible improvements

My version is relatively spartan as far as options go: the player can’t change the grid size or time between generations, and there’s no way to step gradually from one generation to the next. These aren’t difficult problems to solve, but working out how to display those options without cluttering the screen would take longer than I’m willing to spend on this particular project.

There’s also the issue that the grid isn’t actually infinite: the cells along the edges have less than eight neighbors, so they behave differently than every other cell in the grid. For example, the standard glider stalls out and becomes a block rather than continuing offscreen. I haven’t worked out how to resolve this yet; a hacky solution would be to make the grid larger than the game’s viewport, but that’s not entirely satisfying.

What’s next?

I’ve circled back to the fourth game I created for the challenge—a clone of Asteroids—to do some refactoring/polish before I upload it to itch. Once that’s done, I’ll move on to the sixth project, which offers a choice between cloning Super Mario Bros., Pitfall, or VVVVVV.

The more confident I get working in Godot, though, the more tempted I am to deviate from the actual challenge. I imagine this is why they say twenty games or less.

#20-games-challenge #gamedev #godot