Why use a set of coordinates?
A large empty grid does not need to be stored. Only a live cell or one of its neighbours can be alive next generation. The coordinate set can expand in every direction, including negative coordinates, without a wrapping rule or a clipped edge.
The complete example
// Unbounded B3/S23. Coordinates are integer [x, y] pairs.
const key = (x, y) => `${x},${y}`;
export function nextGeneration(live) {
const counts = new Map();
for (const position of live) {
const [x, y] = position.split(',').map(Number);
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
const neighbour = key(x + dx, y + dy);
counts.set(neighbour, (counts.get(neighbour) ?? 0) + 1);
}
}
}
const next = new Set();
for (const [position, count] of counts) {
if (count === 3 || (count === 2 && live.has(position))) {
next.add(position);
}
}
return next;
}
let live = new Set([key(0, 1), key(1, 1), key(2, 1)]);
for (let generation = 0; generation <= 2; generation++) {
console.log(generation, [...live].sort());
live = nextGeneration(live);
}
Download game-of-life.mjsRun and verify it
Save the file and run node game-of-life.mjs. At generation 0 the live cells are (0,1), (1,1), (2,1). At generation 1 they are (1,0), (1,1), (1,2). Generation 2 returns to the first set.
Check the same two-step cycle in the browser.
Three checks before changing the rule
- Exclude the centre cell from its own neighbour count.
- Keep the old live set unchanged while constructing the next set.
- Test an isolated cell (it dies), a block (it stays unchanged), and a glider (the same shape translates after four steps).
Limits and next experiments
This direct sparse algorithm is for small experiments, not a replacement for Hashlife on huge patterns. A long-running seed can still use a lot of time and memory. The visual teaching board uses finite edges, while this example uses an unbounded plane.
Inspect the small-seed experiment · Read the B3/S23 rules · Open the advanced viewer