OS-022 · Agents & cellular automata

Conway's Game of Life

A grid of cells, each alive or dead, updated all at once by one rule: a dead cell with exactly three live neighbors comes alive, a live cell with two or three live neighbors stays alive, and everything else dies. From that rule come moving gliders, glider guns and long, unpredictable histories.

Interactive model

OS-022 · game-of-life · seeded
liveborn this generationdied this generation
changed state oftennever changed
live cellsbirthsdeathsrepeat begins
Parameters
Random soup uses the seed and density; the named patterns are placed exactly
Chance that a cell starts alive (random soup only)
Length of the run; the lab animates up to this generation
Same seed, same run — here, in simulation.js and in simulation.py.
Generation
800
Live cells
132
Births in last step
16
Deaths in last step
16
Live cells at start
1,256
Peak live cells
1,359
State
period-2 oscillation
Repeating since generation
661
Period
2

Example run: default parameters, seed 42.

Watching a soup settle

John Conway designed the rule in 1970 so that nothing would be obvious: small patterns should neither die out at once nor grow without limit, and it should be hard to predict which one happens. Martin Gardner's column in Scientific American made it famous the same year. Nothing in the rule mentions motion, yet some patterns travel; nothing mentions memory, yet the automaton can simulate a universal computer.

The default run starts from a random soup on a 64 × 64 grid whose edges wrap around. Watch three phases:

  • Collapse. The population falls fast at first, with hundreds of births and deaths per generation: crowded cells die, isolated cells die, and only some gaps fill.
  • Long transient. Active regions churn for hundreds of generations, occasionally releasing a glider (five cells that crawl diagonally) that crosses the grid and collides with debris elsewhere.
  • Ash. Eventually only still lifes (blocks, beehives, boats) and blinkers remain. The lab notices the exact generation from which the whole grid repeats and marks it on the chart.

Orange cells were born in the last step and pale gray cells just died, so a blinker shows its two new end cells in orange at every flip and a glider leaves a faint wake. The activity map counts how often each cell has changed state: regions where the transient lived longest turn dark, regions that froze early stay pale, and a glider on a torus draws a diagonal stripe. Switch the pattern menu to see engineered objects instead of chance.

The rule, stated exactly

Each cell has eight neighbors (the Moore neighborhood). Let n be the number of live neighbors of a cell at generation t. Its state at t + 1 is

alivet+1 = [n = 3] or (alivet and [n = 2])

In rule notation that is B3/S23: birth with 3, survival with 2 or 3. All cells update simultaneously from the previous generation, which is why the code writes into a second array instead of editing the grid in place.

  1. For every cell, count live cells among its eight neighbors in the current grid.
  2. Write the new state into the next grid using B3/S23; count births (dead → alive) and deaths (alive → dead).
  3. Swap the grids, then store an exact key of the new state. If that key was seen before at generation g₀, the grid has entered a cycle that began at g₀, with period equal to the difference.

The rule is defined on an infinite plane; a computer grid has to end somewhere. Wrap around glues opposite edges into a torus, so a glider leaving on the right re-enters on the left. Dead cells beyond the edge treats everything outside as permanently dead, so gliders crash into the border (usually leaving a block). Both are approximations, and results for patterns that grow large depend on the choice.

The patterns in the menu

  • Glider. Five cells that return to their original shape after 4 generations, shifted one cell diagonally: a speed of one cell per four generations, written c/4. On an L × L torus it comes back to its starting cells after 4L generations, which the lab reports as a cycle of that period.
  • Gosper glider gun. Found by Bill Gosper in 1970, it answered Conway's question of whether any finite pattern can grow forever. The gun repeats every 30 generations and emits a new glider each cycle, so the population rises by five cells every 30 generations — until the gliders hit the edge or, on a torus, fly back into the gun.
  • R-pentomino. Five cells that explode into a long, chaotic history. On an unbounded plane it settles only after 1,103 generations; on the lab's finite grids its debris wraps around or hits the edge, so the finite history is different and the lab makes no claim about when it settles.
  • Acorn. Seven cells with an even longer transient on the unbounded plane, running for thousands of generations and throwing off many gliders.
  • Pulsar. A period-3 oscillator: 48 cells in its first phase, cycling through three shapes.

Random soups are different for every seed. On the default 64 × 64 torus, middle densities (roughly 0.2 to 0.5) stay active longest; very sparse soups freeze or die within a few generations, and very dense ones lose almost every cell in the first step from overcrowding.

How the lab detects still lifes and oscillators

Life is deterministic, and a finite grid has finitely many states (2L²), so every run on a finite grid must eventually revisit a state and cycle forever. The lab keeps an exact key of every generation's grid — not a lossy checksum, so there are no false matches — and reports the first repeat: period 1 is a still life, period 2 means every remaining object is still or a period-2 oscillator such as a blinker. Longer periods arise when gliders keep circling a torus or when oscillators with different periods coexist: when the parts do not interact, the period of the whole grid is the least common multiple of theirs.

"No repeat yet" means only that no repeat occurred within the requested number of generations. A soup that still contains a glider on a torus may need hundreds more generations before the whole grid repeats.

Example result

With the defaults — a 64 × 64 torus, 30% initial density, seed 42 — the soup starts with 1,256 live cells and briefly grows to 1,359. Most of it then dies off. From generation 661 the whole grid repeats every 2 generations (period-2 oscillation), and at generation 800 132 cells are alive.

Output of game-of-life with pattern=soup size=64 density=0.3 boundary=wrap generations=800 seed=42
Generation800
Live cells132
Births in last step16
Deaths in last step16
Live cells at start1,256
Peak live cells1,359
Stateperiod-2 oscillation
Repeating since generation661
Period2

Implementation

The page runs simulation.js from the package below; the Python file is a line-by-line port with the same random-number generator, so a given seed produces the same result in both languages. The excerpts show the core of each.

JavaScript — simulation.js
// One generation of B3/S23 on an L×L grid (flat array, row by row):
// a dead cell with exactly 3 live neighbors is born, a live cell with
// 2 or 3 live neighbors survives, every other cell is dead next step.
export function step(cur, next, L, wrap) {
  let births = 0, deaths = 0;
  for (let y = 0; y < L; y++) {
    for (let x = 0; x < L; x++) {
      let n = 0;
      for (let dy = -1; dy <= 1; dy++) {
        for (let dx = -1; dx <= 1; dx++) {
          if (dx === 0 && dy === 0) continue;
          let yy = y + dy, xx = x + dx;
          if (wrap) { yy = (yy + L) % L; xx = (xx + L) % L; }
          else if (yy < 0 || yy >= L || xx < 0 || xx >= L) continue; // dead edge
          n += cur[yy * L + xx];
        }
      }
      const alive = cur[y * L + x] === 1;
      const live = n === 3 || (alive && n === 2);
      if (live && !alive) births++;
      if (alive && !live) deaths++;
      next[y * L + x] = live ? 1 : 0;
    }
  }
  return { births, deaths };
}

// Remember every state seen; the first repeat reveals the cycle and its period.
export function observe(tracker, grid, generation) {
  if (tracker.period !== null) return;
  const key = stateKey(grid);
  const first = tracker.seen.get(key);
  if (first === undefined) tracker.seen.set(key, generation);
  else { tracker.cycleStart = first; tracker.period = generation - first; tracker.seen.clear(); }
}
Python — simulation.py
def step(grid, wrap):
    """One B3/S23 generation on a list of rows of 0/1.

    t = live cells in the 3x3 block around a cell, the cell included.
    The cell is alive next step when t == 3, or when t == 4 and it is alive now.
    """
    L = len(grid)
    zero = [0] * L
    new, births, deaths = [], 0, 0
    for y in range(L):
        mid = grid[y]
        if wrap:
            up, down = grid[y - 1], grid[(y + 1) % L]
        else:
            up = grid[y - 1] if y > 0 else zero
            down = grid[y + 1] if y < L - 1 else zero
        col = [a + b + c for a, b, c in zip(up, mid, down)]
        if wrap:
            left, right = col[-1:] + col[:-1], col[1:] + col[:1]
        else:
            left, right = [0] + col[:-1], col[1:] + [0]
        row = [1 if (l + c + r == 3 or (l + c + r == 4 and m)) else 0
               for l, c, r, m in zip(left, col, right, mid)]
        both = sum(a & b for a, b in zip(row, mid))
        births += sum(row) - both
        deaths += sum(mid) - both
        new.append(row)
    return new, births, deaths


def observe(tracker, grid, generation):
    """Remember every state; the first repeat gives the cycle start and period."""
    if tracker["period"] is not None:
        return
    key = b"".join(bytes(r) for r in grid)
    first = tracker["seen"].get(key)
    if first is None:
        tracker["seen"][key] = generation
    else:
        tracker["cycle_start"], tracker["period"] = first, generation - first
        tracker["seen"].clear()

Run the full model from a terminal:

Shell
node simulation.js --pattern soup --size 64 --seed 42
python simulation.py --pattern soup --size 64 --seed 42

Download the model

game-of-life.zip6 files · 15.9 KBDownload .zip
Files in game-of-life.zip
FileContentsSize
README.mdHow to run it, parameters, example result2.6 KB
model.jsonParameters with ranges and defaults, outputs, the seed-42 example3.2 KB
simulation.jsThe model used on this page plus a command-line runner (Node 18+)11.4 KB
simulation.pyPython 3.9+ port, standard library only9.5 KB
example.csvResult table from the example run11.4 KB
LICENSE.txtMIT License1.0 KB

Assumptions & limitations

  • A finite grid, not the infinite plane. The rule is defined on an unbounded plane. Wrapping edges and dead edges are two different approximations; a large or long-lived pattern behaves differently under each, and neither reproduces its unbounded history.
  • Synchronous update. Every cell changes at once from the previous generation. Updating cells one at a time (asynchronously) is a different model with different behavior.
  • Soups are one kind of random start. Each cell is alive independently with the given probability, using a seeded pseudo-random generator; see pseudo-random numbers and seeds. Other random starts (a small random patch in a large empty grid) give different statistics.
  • Cycle detection is exact but bounded. The lab only sees repeats that happen within the requested generations.
  • A mathematical system, not a biological model. The "birth" and "death" names are metaphors.

References

  1. Gardner, M. (1970). Mathematical Games: The fantastic combinations of John Conway's new solitaire game "life". Scientific American, 223(4), 120–123.
  2. Berlekamp, E. R., Conway, J. H. & Guy, R. K. (1982). Winning Ways for Your Mathematical Plays, Vol. 2. Academic Press.