Game of Life
Conway's Game of Life: a random soup self-organizes into still-lifes, blinkers, and gliders — global order from one local rule.
Level: Beginner
Emergence
Discover how interactions between parts can create properties and behaviors that no individual component possesses alone.
Take the QuizConway's Game of Life: global order from local rules
Every cell obeys one trivial rule — count your eight neighbours, live if you have 2 or 3, otherwise die — yet from a random soup the grid self-organizes. It churns for a while, then settles into still-lifes, blinkers, and the occasional glider crawling across it. No cell can see the whole board; the structure emerges from local interactions alone.
The knob is initial_chance, the starting density. Too sparse and the
soup starves and freezes within a few steps; near the edge of persistent
activity it stays alive and churning. Watch the changed probe — how
many cells flipped this step. It decays toward zero as the grid freezes
and stays high while the grid is alive: the leading indicator of whether
the pattern has reached a fixed (or periodic) state.
from tys import progress, frame, probe
import random
Run the Game of Life simulation.
def simulate(cfg: dict):
rows = cfg["rows"]
cols = cfg["cols"]
steps = cfg["steps"]
chance = cfg.get("initial_chance", 0.3)Seed inside simulate so every browser Run is reproducible — without this the contrast would shift on every page load.
random.seed(cfg.get("seed", 0))
grid = [[1 if random.random() < chance else 0 for _ in range(cols)] for _ in range(rows)]
probe("alive", 0, sum(sum(row) for row in grid))
probe("changed", 0, 0)
def count_neighbors(r: int, c: int) -> int:
total = 0
for dr in (-1, 0, 1):
for dc in (-1, 0, 1):
if dr == 0 and dc == 0:
continue
total += grid[(r + dr) % rows][(c + dc) % cols]
return total
changed_history = []
collapse_threshold = 0.1 * sum(sum(row) for row in grid)
announced = False
for step in range(steps):
frame(step, grid)
new_grid = [[0] * cols for _ in range(rows)]
changed = 0
for r in range(rows):
for c in range(cols):
n = count_neighbors(r, c)
cell = grid[r][c]
nxt = 1 if (cell and n in (2, 3)) or (not cell and n == 3) else 0
new_grid[r][c] = nxt
if nxt != cell:
changed += 1
grid = new_grid
changed_history.append(changed)
alive = sum(sum(row) for row in grid)
probe("alive", step + 1, alive)
probe("changed", step + 1, changed)
if not announced and changed < collapse_threshold:
announced = True
progress(int(100 * (step + 1) / steps), "activity collapsing - grid settling")
else:
progress(int(100 * (step + 1) / steps))
frame(steps, grid)
alive = sum(sum(row) for row in grid)Average churn over the final stretch: near-zero once the grid has settled into still-lifes, high while it is still alive and evolving.
tail_activity = sum(changed_history[-10:]) / len(changed_history[-10:])
return {
"alive": alive,
"final_changed": changed_history[-1],
"tail_activity": tail_activity,
}
def requirements():
return {
"external": []
}
# dense.yaml — the default. A 30%-full random soup sits near the edge of
# persistent activity: it churns the whole run, throwing off blinkers and
# gliders instead of settling. (Seeded so every Run is identical.)
rows: 50
cols: 50
steps: 120
initial_chance: 0.30
seed: 0
| Metric | Value |
|---|---|
| alive | 272.00 |
| final_changed | 239.00 |
| tail_activity | 209.70 |
# sparse.yaml — the contrast: only initial_chance changes (0.30 -> 0.05).
# Too few cells to sustain each other, so the soup starves within a few
# steps and freezes into a sparse, near-static ash.
rows: 50
cols: 50
steps: 120
initial_chance: 0.05
seed: 0
| Metric | Value |
|---|---|
| alive | 26.00 |
| final_changed | 24.00 |
| tail_activity | 24.00 |
- What is the one idea here?
- Emergence: complex, lifelike global behavior arising from a single local rule with no central plan. Each cell only looks at its eight neighbours, yet the grid produces recognizable structures — blocks that sit still, blinkers that flash, and gliders that travel. Order is not imposed; it self-organizes from the bottom up.
- Why does the starting density flip the outcome?
- Life needs cooperation: a cell survives only with 2-3 neighbours and is born with exactly 3. Too sparse (the contrast) and most cells are isolated, so they die of loneliness and the grid freezes into a few static leftovers within a handful of steps. Around 30% (the default) there are enough interactions to keep generating and destroying structure indefinitely — the edge of persistent activity.
- What does the changed probe tell me?
- It counts how many cells flipped state this step — the system's pulse. It decays toward a low, flat value once the grid settles into still-lifes and short oscillators, and stays high while the grid is genuinely alive and evolving. It is the leading indicator of whether a run has reached a fixed or periodic state, far more telling than the raw alive count.
- Is the Game of Life actually powerful, or just pretty?
- It is Turing-complete: you can build logic gates, memory, and even a working computer out of gliders and still-lifes, so Life can in principle compute anything a real computer can. That a rule this simple is computationally universal is the headline result — simple local rules can encode arbitrary complexity.