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

cellular-automatagridemergenceself-organization

  • Probes: alive, changed

Emergence

Discover how interactions between parts can create properties and behaviors that no individual component possesses alone.

Take the Quiz
simulation.py

Conway'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 (alive).yaml
# 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
Charts (Dense (alive))

alive

Samples121 @ 0.00–120.00
Valuesmin 211.00, mean 337.02, median 305.00, max 879.00, σ 112.81

changed

Samples121 @ 0.00–120.00
Valuesmin 0.00, mean 283.35, median 247.00, max 776.00, σ 116.97
Final Frame (Dense (alive))
final frame
Final Results (Dense (alive))
MetricValue
alive272.00
final_changed239.00
tail_activity209.70
Sparse (freezes).yaml
# 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
Charts (Sparse (freezes))

alive

Samples121 @ 0.00–120.00
Valuesmin 20.00, mean 27.11, median 26.00, max 154.00, σ 11.74

changed

Samples121 @ 0.00–120.00
Valuesmin 0.00, mean 24.76, median 24.00, max 169.00, σ 13.60
Final Frame (Sparse (freezes))
final frame
Final Results (Sparse (freezes))
MetricValue
alive26.00
final_changed24.00
tail_activity24.00
FAQ
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.