Editor

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.

game_of_life.py
"""Conway's Game of Life: cells on a grid live or die by four simple rules."""
import random
from collections import Counter
ROWS, COLS = 8, 10
ALIVE, DEAD = "O", "."
MAX_GENERATIONS = 30
GRIDS_PER_LINE = 3 # how many generations to print side by side
RANDOM_FILL = 0.3 # share of cells alive in a random start
RULES = [
"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.",
]
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
}
# --- Setting up the grid ---
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}
def random_cells():
"""Fill the grid at random."""
return {(r, c) for r in range(ROWS) for c in range(COLS)
if random.random() < RANDOM_FILL}
# --- The rules ---
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)}
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)}."
# --- Printing ---
def grid_lines(live):
"""Draw one generation as a list of text rows."""
return ["".join(ALIVE if (r, c) in live else DEAD for c in range(COLS))
for r in range(ROWS)]
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))
# --- Talking to the user ---
def ask_pattern():
"""Ask for a pattern name until it is one of the known ones."""
names = ", ".join(PATTERNS)
while True:
name = input(f"Pattern ({names}): ").strip().lower()
if name in PATTERNS:
return name
print("Please type one of the pattern names.")
def ask_generations():
"""Ask how many generations to show."""
while True:
answer = input(f"Generations (1-{MAX_GENERATIONS}): ").strip()
if answer.isdecimal() and 1 <= int(answer) <= MAX_GENERATIONS:
return int(answer)
print(f"Please type a whole number from 1 to {MAX_GENERATIONS}.")
def main():
"""Run patterns until the user wants to stop."""
print("Conway's Game of Life")
print("\n".join(RULES) + "\n")
while True:
name = ask_pattern()
if name == "random":
live = random_cells()
else:
live = place_pattern(PATTERNS[name])
generations, note = run(live, ask_generations())
show(generations)
print(note)
again = input("\nTry another pattern? (y/n) ").strip().lower()
while again not in ("y", "n"):
again = input("Please type y or n: ").strip().lower()
if again == "n":
print("Goodbye.")
break
if __name__ == "__main__":
main()

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