Pixels2GenAI
Path i Foundations
M 01 · 1.2.2 · hands-on

1.2.2 Cellular Automata (Game of Life)

Generate dynamic, lifelike behaviour from four arithmetic rules. Implement Conway's Game of Life with NumPy and SciPy convolutions, then watch gliders walk, blinkers oscillate, and blocks sit still — none of which you ever programmed.

Duration20–25 min
Levelbeginner
Load4 core concepts
Prereqs1.1.1 (RGB arrays), basic Python

Overview

Five live cells on a 20×20 grid. Eight frames later, those five cells have walked diagonally across the canvas — without a single line of movement code. That illusion of motion comes from one of the most studied systems in twentieth-century mathematics: Conway’s Game of Life [1]. Four arithmetic rules, applied uniformly to every cell, produce structures that move, blink, and persist [2].

Learning objectives

  1. Implement Conway’s Game of Life as a synchronous update over a 2D NumPy grid.
  2. Count Moore-neighbourhood neighbours in parallel using a SciPy 2D convolution.
  3. Recognise the three pattern classes — still lifes, oscillators, spaceships — by their neighbour-count signatures.
  4. Reason about how boundary conditions (toroidal vs fixed) reshape a pattern’s evolution.

Quick start — see life in action

python · quick_start.py
import numpy as np
from PIL import Image
from scipy.ndimage import convolve
import imageio

def grid_to_image(grid, scale=8):
    """Convert binary grid to grayscale image."""
    return np.repeat(np.repeat(grid * 255, scale, axis=0), scale, axis=1).astype(np.uint8)

grid = np.zeros((20, 20), dtype=int)
grid[8:11, 8:11] = [[0, 1, 0], [0, 0, 1], [1, 1, 1]]  # Glider

# Moore neighbourhood kernel (counts 8 surrounding cells, excludes centre)
kernel = np.array([[1, 1, 1],
                   [1, 0, 1],
                   [1, 1, 1]])

frames = []
for step in range(8):
    frames.append(grid_to_image(grid))
    neighbor_count = convolve(grid, kernel, mode='wrap')
    # Apply B3/S23 rules
    birth = (neighbor_count == 3)
    survival = (grid == 1) & (neighbor_count == 2)
    grid = (birth | survival).astype(int)

imageio.mimsave('glider_animation.gif', frames, fps=2, duration=0.5)
Animated glider pattern walking diagonally across a 20x20 cellular automata grid
Fig. 1 The glider — five cells, no movement code, four generations per step diagonally.

Core concepts

Concept 1 — What a cellular automaton is

A cellular automaton is three things plus one constraint [2]:

  1. A grid — a regular array of cells, each in one of a small set of states (here: alive 1 or dead 0).
  2. A rule — a function that determines a cell’s next state from its current state and the states of its neighbours.
  3. Discrete time — every cell updates at once on each tick.
python
grid = np.zeros((height, width), dtype=int)   # 0 = dead, 1 = alive

for generation in range(num_steps):
    grid = apply_rules(grid)                   # all cells update together

This framework, despite its plainness, generates four qualitatively distinct long-run dynamics [2]: static equilibria, periodic cycles, traveling structures, and unbounded chaotic transients. Conway’s rules sit in the corner of rule space where all four can coexist on the same grid.

Animation showing all cells reading from the current generation and writing to the next, in one synchronous tick
Fig. 2 Synchronous update: every cell reads from the current state, then the grid swaps.

Conway’s rules (B3/S23)

The Game of Life uses four rules over each cell’s eight neighbours — the Moore neighbourhood [1]:

  • Birth — a dead cell with exactly 3 living neighbours becomes alive.
  • Survival — a living cell with 2 or 3 living neighbours stays alive.
  • Death by isolation — a living cell with fewer than 2 neighbours dies.
  • Death by overcrowding — a living cell with more than 3 neighbours dies.
python
birth     = (neighbor_count == 3) & (grid == 0)
survival  = (neighbor_count >= 2) & (neighbor_count <= 3) & (grid == 1)
new_grid  = (birth | survival).astype(int)

These rules sort almost any starting pattern into three pattern categories [3]: still lifes (unchanging), oscillators (periodic), and spaceships (traveling).

Animated B3/S23 rule evaluation: count alive neighbours, then apply birth, survival, or death rule
Fig. 3 B3/S23 in slow motion: count alive neighbours, then apply birth / survival / death.

Concept 2 — Pattern classification

Each pattern category has a recognisable signature once you know what to look for [3].

Still lifes — stable forever. The smallest is the block [9]: a 2×2 square where every living cell has exactly three living neighbours (counted including diagonals) and every adjacent dead cell has fewer than three. Nothing is born, nothing dies — perfect stability.

Generation 0:    Generation 1:    Generation 2:
. . . .          . . . .          . . . .
. ■ ■ .    →     . ■ ■ .    →     . ■ ■ .
. ■ ■ .          . ■ ■ .          . ■ ■ .
. . . .          . . . .          . . . .

Oscillators — rhythmic patterns. The blinker alternates between horizontal and vertical orientations every generation:

Generation 0:    Generation 1:    Generation 2:
. . . . .        . . ■ . .        . . . . .
. ■ ■ ■ .   →    . . ■ . .   →    . ■ ■ ■ .
. . . . .        . . ■ . .        . . . . .

The horizontal line becomes vertical, then horizontal again. This is a period-2 oscillator [9].

Blinker oscillator: horizontal three-cell line becoming vertical and back
Fig. 4 The blinker — always three living cells, but orientation flips every tick.

A more theatrical oscillator is the beacon, which alternates between six and eight living cells:

Generation 0:    Generation 1:    Generation 2:
■ ■ . .          ■ ■ . .          ■ ■ . .
■ . . .    →     ■ ■ . .    →     ■ . . .
. . . ■          . . ■ ■          . . . ■
. . ■ ■          . . ■ ■          . . ■ ■
Beacon oscillator alternating between six-cell and eight-cell configurations
Fig. 5 The beacon — two diagonal blocks 'reach' toward each other and pull back, period 2.

Spaceships — moving patterns. Where an oscillator transforms in place, a spaceship rebuilds itself one cell over each cycle. The glider moves diagonally:

Step 0:          Step 1:          Step 2:          Step 4:
. ■ . . .        . . . . .        . . . . .        . . . . .
. . ■ . .   →    ■ . ■ . .   →    . . ■ . .   →    . . . ■ .
■ ■ ■ . .        . ■ ■ . .        ■ . ■ . .        . . ■ ■ .
. . . . .        . ■ . . .        . ■ ■ . .        . . ■ . .

The glider reconstructs itself one cell down and one cell to the right every four generations [1] — the illusion of motion produced entirely by arithmetic.

Glider spaceship moving diagonally while maintaining its shape
Fig. 6 The glider walks down-and-right by reconstructing itself one cell over every four ticks.

Placing patterns in code

Every Game of Life pattern is a small 2D shape that you drop onto the grid via array slicing. Read the visual row by row, converting filled cells to 1 and empty cells to 0:

python
# Horizontal blinker (1 row × 3 columns):
# ■ ■ ■
grid[10, 5:8] = [1, 1, 1]

# Vertical blinker (3 rows × 1 column):
# ■
# ■
# ■
grid[10:13, 5] = [1, 1, 1]

# Glider (3 rows × 3 columns) — read row by row:
# . ■ .     →  [0, 1, 0]
# . . ■     →  [0, 0, 1]
# ■ ■ ■     →  [1, 1, 1]
grid[10:13, 5:8] = [[0, 1, 0], [0, 0, 1], [1, 1, 1]]

To rotate a pattern, rearrange the rows. A glider heading up-left instead of down-right:

python
# Glider rotated 180°:
# ■ ■ ■     →  [1, 1, 1]
# ■ . .     →  [1, 0, 0]
# . ■ .     →  [0, 1, 0]
grid[10:13, 5:8] = [[1, 1, 1], [1, 0, 0], [0, 1, 0]]

Concept 3 — Neighbour counting via convolution

The standard, blazingly fast approach to neighbour counting is a 2D convolution [8] with a kernel shaped like the Moore neighbourhood:

python
# Counts each cell's 8 neighbours (centre 0 excludes the cell itself)
kernel = np.array([[1, 1, 1],
                   [1, 0, 1],
                   [1, 1, 1]])

neighbor_count = convolve(grid, kernel, mode='wrap')

Convolution counts every cell’s neighbours in parallel, so the algorithm scales to large grids without nested Python loops. mode='wrap' produces toroidal boundary conditions [2] — opposite edges of the grid are connected.

Diagram showing eight cells surrounding a central cell, and the corresponding 3x3 convolution kernel that counts them
Fig. 7 The Moore neighbourhood: the eight cells touching a centre cell, and the 3×3 kernel that counts them.

Concept 4 — Boundary conditions reshape evolution

How the grid handles its edges changes pattern behaviour at the boundary [6]:

  • Wrap-around (torus)mode='wrap', patterns can move continuously across the edge.
  • Fixed dead boundarymode='constant', cells beyond the edge act as permanently dead — a wall.
  • Reflective — custom implementation, patterns bounce off the edge.
python
wrap_neighbors  = convolve(grid, kernel, mode='wrap')     # toroidal
fixed_neighbors = convolve(grid, kernel, mode='constant') # dead boundary

For art and exploration, wrap-around produces more interesting long-run behaviour [6] because patterns can interact across the entire space. For studying single patterns in isolation, a fixed boundary acts like a black hole at the edge.

Side-by-side animation comparing wrap-around boundary conditions (left) with fixed-dead-edge boundary conditions (right) using a glider pattern
Fig. 8 Wrap-around vs fixed-dead boundary, same glider, different fate.

Exercises

Three exercises in the standard Execute → Modify → Create progression [10, 11].

EXECUTE I.

The blinker

The blinker is the simplest oscillator — three cells, period 2. Run it and watch the line flip.

bash
python exercise1_execute.py
Expected output: a vertical line of three white cells against a black grid, the blinker's even-generation form
Fig. 9 After six (even) generations the blinker has returned to its horizontal form. Yours should match.

Reflection questions

  • Does the living-cell count change, or does it stay at exactly 3?
  • Can you trace, in your head, how the horizontal line becomes vertical, then horizontal again?
  • Which pattern category (still life / oscillator / spaceship) does the blinker belong to, and why?
MODIFY II.

Blinker variations

Open exercise2_modify.py. The script starts with a blinker at the centre of a 30×30 grid. Change the marked values to hit each goal below, then re-run.

bash
python exercise2_modify.py

Goals

  1. Move the blinker to the very top edge — change its position to row 0. Does it still oscillate?
  2. Switch boundary mode — keep the edge blinker and change mode='wrap' to mode='constant'. What happens?
  3. Multiple blinkers — add several blinkers at different positions and orientations.
Three reference panels: edge blinker oscillating on wrap-around, edge blinker dying on fixed boundary (empty grid), and a grid containing multiple coexisting blinkers
Fig. 10 Reference outputs for the three goals.
CREATE III.

A pattern garden

Open exercise3_create.py. The helper functions and grid setup are provided. Complete the three TODOs to place an oscillator and a spaceship on the same grid and let them coexist for twenty generations.

bash
python exercise3_create.py

Make it your own

After the TODOs work, try these by editing the script directly:

  • Add a single blinker: grid[row, col:col+3] = [1, 1, 1].
  • Add a block (still life): grid[row:row+2, col:col+2] = 1.
  • Place two gliders on a collision course. What happens when they meet?
  • Push the grid to 60×60 and scatter many patterns. Density changes the dynamics.

Downloads

exercise1_execute.py — blinker (Execute) exercise2_modify.py — blinker variations (Modify) exercise3_create.py — pattern garden (Create)

Summary

Common pitfalls to avoid

  • Updating cells during the sweep instead of after. The grid must be read entirely from the current generation before any cell flips. Use a separate new_grid or NumPy’s convolve (which is implicitly synchronous).
  • Forgetting mode='wrap' and then wondering why edge patterns die.
  • Confusing the kernel with the rule — the kernel of ones only counts neighbours, it does not encode B3/S23. The rule lives in the birth/survival comparison afterwards.

References

  1. [1] Gardner, M. (1970). Mathematical Games: The fantastic combinations of John Conway’s new solitaire game “life”. Scientific American, 223(4), 120–123.
  2. [2] Wolfram, S. (2002). A New Kind of Science. Wolfram Media. ISBN 978-1-57955-008-0.
  3. [3] Adamatzky, A. (Ed.). (2010). Game of Life Cellular Automata. Springer. doi:10.1007/978-1-84996-217-9
  4. [4] Berlekamp, E. R., Conway, J. H., & Guy, R. K. (2004). Winning Ways for Your Mathematical Plays (2nd ed., Vol. 4, ch. 25). A K Peters.
  5. [5] Roberts, S. (2015). Genius at Play: The Curious Mind of John Horton Conway (pp. 125–126). Bloomsbury.
  6. [6] Flake, G. W. (1998). The Computational Beauty of Nature. MIT Press. ISBN 978-0-262-56127-3.
  7. [7] Von Neumann, J. (1966). Theory of Self-Reproducing Automata (A. W. Burks, Ed.). University of Illinois Press.
  8. [8] SciPy developers. (2024). scipy.ndimage.convolve. SciPy v1.12 Documentation. scipy.org
  9. [9] ConwayLife.com. (2024). LifeWiki: The wiki for Conway’s Game of Life. conwaylife.com
  10. [10] Sweller, J. (1988). Cognitive load during problem solving: Effects on learning. Cognitive Science, 12(2), 257–285. doi:10.1207/s15516709cog1202_4
  11. [11] Mayer, R. E. (2020). Multimedia Learning (3rd ed.). Cambridge University Press. ISBN 978-1-316-63816-1.