ours(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)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)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)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-its-a-computer)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](https://dev48v.infy.uk/game/day21-game-of-life.html)