Conway's Game of Life in Python
Intermediate · Logic and puzzles · about 45 minutes to build · 136 lines
Choose a starting pattern and a number of generations, then watch the cells live, die and travel across the grid.
The program
Press Run and answer its questions in the box that appears under the code.
Open it in the full editor to save it, share it and keep building.
Sample run
What the program printed, and what was typed (in yellow).
Conway's Game of Life 1. A live cell with fewer than 2 live neighbours dies (too lonely). 2. A live cell with 2 or 3 live neighbours lives on. 3. A live cell with more than 3 live neighbours dies (too crowded). 4. An empty cell with exactly 3 live neighbours comes to life. Pattern (glider, blinker, toad, beacon, random): glider Generations (1-30): ten Please type a whole number from 1 to 30. Generations (1-30): 5 Gen 0 Gen 1 Gen 2 .......... .......... .......... .......... .......... .......... ....O..... .......... .......... .....O.... ...O.O.... .....O.... ...OOO.... ....OO.... ...O.O.... .......... ....O..... ....OO.... .......... .......... .......... .......... .......... .......... Gen 3 Gen 4 Gen 5 .......... .......... .......... .......... .......... .......... .......... .......... .......... ....O..... .....O.... .......... .....OO... ......O... ....O.O... ....OO.... ....OOO... .....OO... .......... .......... .....O.... .......... .......... .......... Live cells after 5 generations: 5. Try another pattern? (y/n) y Pattern (glider, blinker, toad, beacon, random): blinker Generations (1-30): 6 Gen 0 Gen 1 Gen 2 .......... .......... .......... .......... .......... .......... .......... ....O..... .......... ...OOO.... ....O..... ...OOO.... .......... ....O..... .......... .......... .......... .......... .......... .......... .......... .......... .......... .......... The pattern repeats every 2 generations. Try another pattern? (y/n) n Goodbye.
How it is built
1. Patterns as pictures
Each pattern is drawn with O for a live cell and . for an empty one, which is easier to read and change than a list of coordinates. random has no picture: it fills the grid by chance.
PATTERNS = { "glider": [".O.", "..O", "OOO"], # walks diagonally across the grid "blinker": ["OOO"], # flips between flat and upright "toad": [".OOO", "OOO."], # rocks back and forth "beacon": ["OO..", "OO..", "..OO", "..OO"], # two blocks that flash "random": None, # a random grid, different every time }
2. Placing a pattern
The picture becomes a set of (row, col) pairs for the live cells, moved to the middle of the grid. Only live cells are stored; every other cell is dead.
def place_pattern(picture): """Turn a picture like [".O.", "..O"] into live cells in the middle.""" top = (ROWS - len(picture)) // 2 left = (COLS - max(len(line) for line in picture)) // 2 return {(top + r, left + c) for r, line in enumerate(picture) for c, char in enumerate(line) if char == ALIVE}
3. One generation
For each live cell, each of its 8 neighbours gets 1 added in a Counter. The four rules then fit in one line: a cell is alive next time if it has exactly 3 live neighbours, or 2 and it is alive now. % ROWS and % COLS make the edges wrap, so a glider leaving one side comes back on the other.
def next_generation(live): """Apply the rules once to a set of live cells and return the new set.""" counts = Counter() # how many live neighbours each cell has for row, col in live: for dr in (-1, 0, 1): for dc in (-1, 0, 1): if dr or dc: # the edges wrap around, like a globe counts[((row + dr) % ROWS, (col + dc) % COLS)] += 1 # Born with exactly 3 neighbours; a live cell survives with 2 or 3. return {cell for cell, n in counts.items() if n == 3 or (n == 2 and cell in live)}
4. Spotting a repeat
Each generation is kept as a frozenset, a set that cannot change, so live in history can check whether this exact grid has been seen before. If it has, the pattern will repeat forever and the run stops.
def run(live, generations): """Return the generations, stopping early if they die out or repeat.""" history = [frozenset(live)] for number in range(1, generations + 1): live = frozenset(next_generation(live)) if not live: return history + [live], "Everything has died out." if live in history: period = number - history.index(live) note = f"The pattern repeats every {period} generations." return history + [live], note history.append(live) word = "generation" if generations == 1 else "generations" return history, f"Live cells after {generations} {word}: {len(live)}."
5. Printing side by side
Generations are printed three to a line: each is drawn as a list of rows, then row 0 of each grid is printed together, then row 1, and so on.
def show(generations): """Print the generations side by side, a few to a line.""" for start in range(0, len(generations), GRIDS_PER_LINE): group = generations[start:start + GRIDS_PER_LINE] labels = [f"Gen {start + i}".ljust(COLS) for i in range(len(group))] print("\n" + " ".join(labels).rstrip()) drawn = [grid_lines(live) for live in group] for row in range(ROWS): print(" ".join(lines[row] for lines in drawn))
Make it bigger
- Let the user draw a starting pattern by typing rows of O and dots.
- Add the Gosper glider gun on a bigger grid: it sends out a new glider every 30 generations.
- Turn off wrapping so cells on the edges have fewer neighbours, and see what happens to the glider.
- Spot spaceships: check whether a generation is an earlier one moved by some rows and columns.
What it practises
- sets of tuples
- collections.Counter
- % for wrapping around
- frozenset
- nested loops