DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Four Rules, Infinite Worlds: Building Conway's Game of Life from Scratch

Conway's Game of Life is the most famous program that isn't really a game. There's no player, no score, no way to win. You draw a few cells on a grid, press play, and watch. What comes back is uncanny: patterns that crawl across the screen, blink forever, collide, and occasionally build machines. All of it falls out of four tiny rules that a mathematician named John Conway scribbled down in 1970. Today (Day 21 of GameFromZero) we built a real, running Game of Life in plain vanilla JavaScript. Here's exactly how it works.

Play the finished version here: https://dev48v.infy.uk/game/day21-game-of-life.html

The grid is just an array of ones and zeros

Every cell is in one of two states — alive or dead — so the whole universe is a flat array of 0s and 1s. We use 40 columns by 30 rows, which is 1,200 cells.

const COLS = 40, ROWS = 30, CELLS = COLS * ROWS;
let grid = new Uint8Array(CELLS);   // 0 = dead, 1 = alive

const rowOf = i => Math.floor(i / COLS);
const colOf = i => i % COLS;
const idx   = (r, c) => r * COLS + c;
Enter fullscreen mode Exit fullscreen mode

We keep it flat — one array, not a grid of arrays — because scanning and copying become simple loops. When we need a cell's position we convert its index; when we need the index we convert its row and column back. That's the entire data model.

Everything depends on counting eight neighbours

Each cell has eight neighbours: up, down, left, right, and the four diagonals. The single number that drives the whole simulation is how many of those eight are currently alive. We loop over the nine offsets around a cell and skip the centre.

function neighbours(g, r, c){
  let n = 0;
  for (let dr = -1; dr <= 1; dr++)
    for (let dc = -1; dc <= 1; dc++){
      if (dr === 0 && dc === 0) continue;      // skip the cell itself
      const rr = (r + dr + ROWS) % ROWS;        // wrap top/bottom
      const cc = (c + dc + COLS) % COLS;        // wrap left/right
      n += g[idx(rr, cc)];
    }
  return n;                                     // always 0..8
}
Enter fullscreen mode Exit fullscreen mode

Notice the wrap. Adding ROWS before the % keeps the number positive, so a cell on the top edge treats the bottom edge as its neighbour. That turns the flat grid into a torus — a doughnut. A glider that walks off the right side reappears on the left. (You could instead treat the edges as permanently dead; wrapping just makes the little world feel endless.)

The four rules collapse into one line

Here's where the magic hides. Conway's four rules are almost boring on their own:

  1. A live cell with fewer than two live neighbours dies (underpopulation).
  2. A live cell with two or three live neighbours survives.
  3. A live cell with more than three live neighbours dies (overpopulation).
  4. A dead cell with exactly three live neighbours becomes alive (reproduction).

Read them again and you'll see they fold into two conditions: a live cell survives on 2 or 3, and a dead cell is born on exactly 3. Everyone writes this as B3/S23 — Born on 3, Survives on 2 or 3.

function nextState(alive, n){
  if (alive) return (n === 2 || n === 3) ? 1 : 0;   // survive
  return (n === 3) ? 1 : 0;                          // birth
}
Enter fullscreen mode Exit fullscreen mode

That one function is the entire law of the universe. Tweak the numbers — say B36/S23 — and you get a completely different automaton called HighLife, with patterns that replicate themselves. Conway tried dozens of variants by hand before settling on B3/S23 because it sat right on the edge between fizzling out and blowing up.

The one bug everyone hits: mutating in place

Now the crux, and the mistake nearly every beginner makes. It's tempting to walk the grid and update each cell as you go. Don't. If you kill a cell and then evaluate its neighbour, that neighbour sees a grid that's already half-changed — but the rules demand every cell decide from the same snapshot of the current generation.

The fix is double buffering. Read from the current grid, write every result into a brand-new array, and only swap it in once every cell is done.

function step(){
  const next = new Uint8Array(CELLS);   // a fresh buffer
  for (let i = 0; i < CELLS; i++){
    const n = neighbours(grid, rowOf(i), colOf(i));
    next[i] = nextState(grid[i] === 1, n);
  }
  grid = next;              // swap in — the old grid is never touched mid-scan
  generation++;
  render();
}
Enter fullscreen mode Exit fullscreen mode

I verified this with a quick test: stamp a Glider, run four generations, and check that its five cells kept their exact shape but shifted one row down and one column right. They did — the Glider walks. A Blinker flips between horizontal and vertical and returns to itself every two steps. If you'd mutated in place, none of that would come out right; you'd get garbage that looks vaguely alive but breaks the rules.

Drawing, playing, and famous patterns

Rendering is cheap: build one small <div> per cell once, then on each frame just toggle an alive class so CSS paints it green or white. Seeding is a click-and-drag paint tool using pointer events (so it works on phones too). Playing is setInterval(step, 1000 / speed), where a speed slider of 10 means ten generations a second.

The fun part is the presets — famous patterns are just lists of live coordinates:

const PATTERNS = {
  glider:  [[0,1],[1,2],[2,0],[2,1],[2,2]],   // a spaceship that walks diagonally
  blinker: [[1,0],[1,1],[1,2]],               // period-2 oscillator
  // pulsar, LWSS, and the Gosper Glider Gun are bigger lists
};
Enter fullscreen mode Exit fullscreen mode

Drop in a Glider and it strolls across the screen forever. A Blinker ticks. A Pulsar pulses on a three-beat. A Lightweight Spaceship slides sideways. And the Gosper Glider Gun — the pattern that won Conway's own bet — sits still and fires a fresh glider every 30 generations, a factory that never runs out.

The punchline: it's a computer

Here's the fact that keeps people up at night. The Game of Life is Turing-complete. Using glider guns as signal sources, streams of gliders as wires carrying bits, and engineered collisions as logic gates, people have built AND, OR and NOT gates, memory cells, clocks — even a working calculator — entirely out of Life patterns. In principle, anything your laptop can compute, a big enough Life board can compute too.

That's the whole thing: a grid, an eight-neighbour count, four rules, and a fresh buffer each step. From that you get gliders, guns, oscillators, and universal computation. Simple local rules, endless emergence — that's why this "game" has fascinated programmers for over fifty years.

Try it, draw your own pattern, and press play: https://dev48v.infy.uk/game/day21-game-of-life.html

Top comments (0)