How a zero-player cellular automaton became a useful case study in simulation design, rendering, testing, and shareable state.

Conway’s Game of Life is one of those ideas that is easy to explain and difficult to stop thinking about.

Conway’s Game of Life starts with four simple rules — then produces motion, memory, and surprising complexity. This post explores the ideas behind it and how Rulespace turns them into a clean, shareable interactive simulation.

The setup is almost disarmingly small. Start with a grid of cells. Every cell is either alive or dead. At each generation, every cell looks at its eight neighbors and applies a short set of rules:

There are no players after the initial pattern is placed. No instructions tell a glider to travel, a pulsar to oscillate, or a glider gun to emit new patterns. Those behaviors emerge from repeated application of the same local rules.

That tension — simple rules, surprising global behavior — is why the Game of Life remains a compelling programming project. It is not only a simulation problem. It is a compact way to think about state, time, rendering, data formats, testing, and the boundary between a system’s rules and the interface around them.

I built Rulespace, an interactive Game of Life sandbox, to explore those concerns in a small but complete application. It supports custom Life-like rules, pattern placement, shareable URLs, population analytics, and bounded cycle detection. More importantly, it prevents those features from collapsing into a single large React component.

You can try it here: Rulespace.

Life is a model of local information

The most important property of the Game of Life is that a cell does not need to know anything about the whole board. It only needs its current state and the number of live cells in its Moore neighborhood — the eight surrounding positions.

That makes each generation a pure transformation:

nextGrid = step(currentGrid, width, height, rule);

For a fixed grid and rule, the result is always the same. This is a powerful design constraint. The simulation does not need access to browser APIs, React state, a canvas, or a clock. It only needs input data and returns output data.

In Rulespace, the grid is a flattened `Uint8Array`, not a two-dimensional array of objects:

type Grid = Uint8Array;
const index = y * width + x;

Each byte is either `0` for a dead cell or `1` for a live cell. Flattening the grid keeps the engine compact and makes the memory layout predictable. It also prevents the simulation model from becoming coupled to how the board is displayed.

The engine uses toroidal edges: moving past the right edge wraps to the left, and moving past the bottom wraps to the top. That choice makes the world finite while avoiding special-case behavior at borders.

export function getIndexToroidal(
x: number,
y: number,
width: number,
height: number,
): number {
const wrappedX = (x % width + width) % width;
const wrappedY = (y % height + height) % height;
return wrappedY * width + wrappedX;
}

There is a small lesson here that extends beyond cellular automata: define the rules of a boundary once, in the model layer, instead of sprinkling edge checks throughout the rest of the application.

The rule is data, not a constant

Classic Conway Life is written as `B3/S23`: a cell is **born** with three neighbors and **survives** with two or three. But that notation also describes a family of related cellular automata.

For example:

Treating the rule as data rather than hard-coding Conway’s behavior changes the project from a single demo into an explorable system. The simulation step receives a rule object, and the interface can validate and switch rules without changing the algorithm itself.

type Rule = {
born: number[];
survive: number[];
};

Inside the hot loop, Rulespace converts those arrays into small lookup tables. The code stays readable while avoiding repeated array searches for every cell in every generation:

if (currentState === 1) {
if (surviveMap[neighbors]) nextGrid[index] = 1;
} else if (bornMap[neighbors]) {
nextGrid[index] = 1;
}

This is a useful distinction: make configuration flexible at the edges of a system, then normalize it into the simplest representation for the work being performed.

Rendering should not own the simulation

An interactive Life board changes frequently, and it can contain many cells. Rendering a DOM element for every cell would make the component tree responsible for thousands of tiny visual objects.

Rulespace instead renders the board through a single `<canvas>`. The renderer receives the current grid and draws the live cells. It does not decide which cells survive, manage the generation count, parse rules, or detect cycles.

That separation matters because simulation time and render time are different concerns. The simulation loop decides when a new generation should be produced. The canvas renderer is responsible for presenting the current generation, using `requestAnimationFrame` to coordinate drawing with the browser.

const draw = () => {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = cellColor;

for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) {
if (grid[y * width + x] === 1) {
context.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
}
}
}
};

Keeping this boundary explicit made one subtle issue easier to diagnose: changing a canvas element’s dimensions clears its drawing buffer. Dimensions now update only when the board size changes, while grid updates redraw the existing canvas. The result is a renderer whose behavior is easier to reason about during both interactive use and release-asset capture.

A simulation engine earns its tests

The visual output of Game of Life can look convincing even when the algorithm is wrong. A glider might appear to move, but wrap incorrectly at an edge. A blinker might oscillate, leaving a stale cell behind. That is why the core rules are tested independently of the UI.

Rulespace starts with three recognizable behaviors:

Those patterns make excellent tests because they have clear, known outcomes. Additional tests cover toroidal wrapping, rule normalization, URL-state round-trip, malformed sharing payloads, and cycle-detection behavior.

This is the practical advantage of a pure engine. Tests can create a grid, call `step`, and compare bytes. They do not need to mount React components or simulate canvas clicks to establish that the automaton itself is correct.

Sharing a board means choosing a format

A Game of Life board is just state, so it should be possible to share it without adding accounts or a database. Rulespace encodes the current grid and rule into a URL query parameter.

The grid format is a deliberately simplified version of RLE, the run-length encoding format familiar to the Life community. Instead of recording every dead cell individually, it represents runs using `b` for dead, `o` for live, `$` for a new row, and `!` for the end of the pattern. That payload is paired with the rule string, serialized, made URL-safe, and copied into the share URL.

The important design decision is not the encoding trick itself. It is choosing a narrow, explicit contract. Rulespace supports its own raw RLE subset and validates it on decode; it does not claim to implement every header and comment convention in the broader RLE specification.

That keeps sharing static, portable, and compatible with GitHub Pages. The state is in the link, not in an opaque server-side record.

Detecting cycles without trusting a hash blindly

Some Life patterns stop changing. Others repeat after a fixed number of generations. A block is a period-one still life; a blinker is a period-two oscillator.

Rulespace keeps a rolling window of the last 50 states. A hash makes it cheap to find candidate repeats, but hashes are not proof of equality. To avoid reporting a false cycle from a collision, a matching hash is followed by a byte-for-byte grid comparison.

if (candidate.hash === hash && gridsEqual(candidate.grid, grid)) {
return history.length - index;
}

This is a small example of defensive system design: use a fast approximate signal to narrow the search, then verify the condition that actually matters.

The project is small; the boundaries are the point

Game of Life is not new, and a Life demo alone is not a differentiator. The interesting part of building Rulespace was deciding where each responsibility belonged:

Those boundaries make the application easier to test, change, and explain. They also turn a familiar toy problem into a more realistic engineering exercise: a system with data modeling, performance-sensitive rendering, validation, user interaction, and a deliberate deployment model.

The next time a problem seems too small to be interesting, it may be worth looking again. Small systems are often where design decisions become easiest to see.

Rulespace is open source and deployed as a static GitHub Pages project. Explore the live sandbox at turhancan97.github.io/rulespace or view the code on GitHub.

<hr><p>Conway’s Game of Life Is Small, but It Teaches Big Engineering Lessons was originally published in DataDrivenInvestor on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>