[Rod Stephens Books]
Index Books Python Examples About Rod Contact
[Mastodon] [Bluesky]
[Build Your Own Ray Tracer With Python]

[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

Title: Simulate Conway's game of life in Python

[The Gosper Glider Gun in Conway's game of life written Python]

The Game of Life is a cellular automaton created by British mathematician John Horton Conway in 1970. It starts with a set of active (alive) and inactive (dead) cells and then uses four very simple rules to run a simulation.

The program's window shows a big array (list of lists) of rectangles. They are all outlined in black and are filled with black or white to represent living and dead cells, respectively.

The program is pretty long so, as usual, I'm only going to describe the key pieces. The following sections describe the rules and three of the trickier parts of the program: performing updates, handling the world's edges, and loading known patterns.

Rules

  1. Underpopulation: If a live cell has fewer than two live neighbors, it dies from loneliness.
  2. Survival: If a live cell has two or three live neighbors, then it survives through the next round.
  3. Overpopulation: If a live cell has more than three live neighbors, it dies from overpopulation.
  4. Reproduction: If a dead cell has exactly three live neighbors, it becomes alive in the next turn by reproduction.

Updates

During each turn, the program must update every cell in the "world." The biggest trick here is realizing that you cannot update the world in situ. If you try to update the cells in place, changes to one space will affect the neighboring spaces.

To avoid that problem, the program uses the rules on the world to populate a temporary list of lists. It then copies the results back into the world lists.

Here's the code that performs one step of the game.

def step(self): '''Make a game step.''' # If we're no longer running, stop. if not self.running: return # Update the living squares in the temp lists. for r in range(self.num_rows): for c in range(self.num_cols): self.update_square(r, c) # Copy new values into the world lists and update square colors. for r in range(self.num_rows): for c in range(self.num_cols): self.set_square(r, c, self.temp[r][c]) # Update the turn label. self.num_steps += 1 self.status_label['text'] = f'Step {self.num_steps}' # Schedule the next step. fps = self.fps_scale.get() delay = 1000 // fps self.window.after(delay, self.step)

The code first checks that the game is still running. It then loops through the world's squares and calls update_square (shown shortly) for each.

After it updates every square, the code loops through the temporary temp lists and calls set_square (described shortly) to copy the results back into the main lists.

The method updates the display to show the current turn number, gets the desired number of frames per second, and uses after to make the program execute this method again after a suitable delay.

update_square

The following update_square method applies the rules to a square.

def update_square(self, r, c): '''Update this square.''' # Count the neighbors. num_neighbors = self.count_neighbors(r, c) # See if this square is currently alive. Save new values in the # temp lists and copy them into the world lists when we're done. if self.world[r][c]: # Alive. if num_neighbors < 2: # Underpopulation. self.temp[r][c] = False elif num_neighbors > 3: # Overpopulation. self.temp[r][c] = False else: # Status quo. self.temp[r][c] = True else: # Vacant. if num_neighbors == 3: # Reproduction. self.temp[r][c] = True else: # Status quo. self.temp[r][c] = False

This method applies the four rules to the square self.world[r][c]. Notice that it saves its results in the self.temp lists so self.world is unchanged.

set_square

After it calls update_square for each square, the program could just swap its world and temp lists. That would allow it to perform the simulation, but it wouldn't update the display.

Instead, the code loops through the squares and calls the following set_square method for each.

def set_square(self, r, c, is_alive): '''Set the world entry and the cell's color.''' # See if the value needs to be changed. if not self.world[r][c] == is_alive: # Update the cell and its square. self.world[r][c] = is_alive color = 'black' if is_alive else 'white' self.canvas.itemconfigure(self.squares[r][c], fill=color)

This code checks whether the self.world[r][c] value already has the correct value with True indicating a living cell and False indicating a dead cell. If the world list already holds the correct value, the method does nothing. Usually that's the case because the world is usually mostly full of dead cells.

If the world entry is incorrect, the code sets it to the right value. It then uses the Canvas widget's itemconfigure method to set the color for that cell's rectangle so it shows up correctly on the window.

Edges

The last issue I want to mention is the way the program handles the world's edges. Ideally the world would be infinitely big so it has no edges. That's not practical, so you need a way to deal with living cells approaching the world's edges.

One approach would be to enlarge the world as needed. That's not very practical because some initial patterns like Glider and Gosper Glider Gun launch packages of cells that travel indefinitely across the world. That means the world will grow indefinitely until your computer runs out of memory.

Another interesting approach is to make the world wrap around at the edges. In that case, a cell at the world's edge is a neighbor to the cell on the opposite edge. That approach creates some interesting effects but it has the result that traveling patterns like Gosper's Glider Gun wrap around and run into themselves messing up their pattern.

The approach I took in this game is to just ignore cells off of the world's edge. For example, a corner cell has only three neighboring cells because the others lie off of the world.

If you like, you could modify the program to let the user check a box to pick either a wrapping to a fixed-size world.

Patterns

You can click on the program's squares to toggle them from dead to alive. You can also load pre-defined patterns from the combo box. The following load_patterns method creates a list of pre-defined patterns.

def load_patterns(self): '''Load known patterns.''' self.patterns = { 'Clear': [], 'Block': ['11', '11'], 'Beehive': ['0110', '1001', '0110'], 'Loaf': ['0110', '1001', '0101', '0010'], 'Boat': ['110', '101', '010'], ... 'Pattern 3': ['111111110111110001110000001111111011111'], }

After loading the patterns, the program uses the following snippet to create its combo box.

self.pattern_combo = \ ttk.Combobox(top_frame, width=22, state='readonly', values=list(self.patterns.keys())) self.pattern_combo.bind('<>', self.load_pattern) self.pattern_combo.pack(side=tk.LEFT, padx=3, anchor=tk.S) self.pattern_combo.current(0)

This code creates the combo box, settings its values property to the patterns dictionary's key values so the pattern names (Clear, Block, etc.) appear in the combo box.

When you make a selection, the following load_pattern method executes.

def load_pattern(self, event): '''Stop and load the selected pattern.''' self.start_button.config(text='Start') self.running = False self.num_steps = 0 self.status_label['text'] = '' # Clear the world. self.clear_world() # Get the pattern. pattern = self.patterns[self.pattern_combo.get()] # Load the pattern. num_rows = len(pattern) if num_rows == 0: # If the pattern is Clear. return num_cols = len(pattern[0]) min_r = (len(self.world) - num_rows) // 2 min_c = (len(self.world[0]) - num_cols) // 2 for r in range(len(pattern)): for c in range(len(pattern[r])): if pattern[r][c] == '1': self.set_square(r + min_r, c + min_c, True)

This method stops the program if it's running and clears the world to reset every square to empty.

It then gets the selected pattern. It uses the number of rows and columns in the selected pattern to figure out where it needs to start applying the pattern to center it in the world. It then loops through the pattern's 0s and 1s to copy it onto the world. If a pattern entry is 1, the code calls the set_square method shown earlier to activate that cell.

Conclusion

Those are the most interesting pieces of the program. There are still a few details like how the program counts neighbors (which would be different if you provide edge wrapping), starts and stops, and use the mouse to toggle squares. Download the example to experiment with the program and to see those and other details.
© 2025 Rocky Mountain Computer Consulting, Inc. All rights reserved.