<p class="hint">Click cells to toggle alive/dead. Press <b>Play</b> to run the rules, or step one generation at a time. Use presets to place classic patterns and watch three rules compute.</p>
<div class="toolbar">
<button id="play" type="button">Play</button>
<button id="step" type="button">Step</button>
<button id="clear" type="button" class="ghost">Clear</button>
<span class="sep"></span>
<button id="glider" type="button" class="preset">Glider</button>
<button id="blinker" type="button" class="preset">Blinker</button>
<button id="gun" type="button" class="preset">Pulsar</button>
</div>
<canvas id="life" width="400" height="360"></canvas>
<div class="info" id="info">Generation: 0 | Population: 0</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.toolbar { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .5rem; align-items: center; }
.sep { flex: 1; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.preset { background: #457b9d; border-color: #457b9d; }
canvas { display: block; border: 1.5px solid #cdd9e3; border-radius: 6px; cursor: crosshair;
max-width: 100%; touch-action: none; }
.info { font-size: .85rem; color: #555; margin-top: .4rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Conway's Game of Life — three rules:
// Birth: dead cell with exactly 3 live neighbors → alive
// Survival: live cell with 2 or 3 live neighbors → stays alive
// Death: all other live cells → dead
// The system is Turing-complete; predicting the long-run fate is undecidable.
const COLS = 40, ROWS = 36, CELL = 10;
const canvas = document.getElementById('life');
const ctx = canvas.getContext('2d');
let grid = Array.from({length: ROWS}, () => new Uint8Array(COLS));
let gen = 0, pop = 0, rafId = null, running = false;
function idx(r, c) { return (r + ROWS) % ROWS * COLS + ((c + COLS) % COLS); }
function get(g, r, c) { return g[(r + ROWS) % ROWS][(c + COLS) % COLS]; }
function step() {
const next = Array.from({length: ROWS}, () => new Uint8Array(COLS));
pop = 0;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
let n = 0;
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++)
if (dr || dc) n += get(grid, r + dr, c + dc);
const alive = grid[r][c];
// Birth: dead + 3 neighbors; Survival: alive + 2 or 3
next[r][c] = (!alive && n === 3) || (alive && (n === 2 || n === 3)) ? 1 : 0;
if (next[r][c]) pop++;
}
}
grid = next;
gen++;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#e8f0f7';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#1d3557';
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (grid[r][c]) ctx.fillRect(c * CELL + 1, r * CELL + 1, CELL - 1, CELL - 1);
// light grid lines
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 0.4;
for (let r = 0; r <= ROWS; r++) { ctx.beginPath(); ctx.moveTo(0, r*CELL); ctx.lineTo(canvas.width, r*CELL); ctx.stroke(); }
for (let c = 0; c <= COLS; c++) { ctx.beginPath(); ctx.moveTo(c*CELL, 0); ctx.lineTo(c*CELL, canvas.height); ctx.stroke(); }
}
function updateInfo() {
pop = 0;
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) pop += grid[r][c];
document.getElementById('info').innerHTML =
fmt('Generation: %gen% | Population: %pop%', { gen: gen, pop: pop });
}
function loop() { step(); draw(); updateInfo(); if (running) rafId = requestAnimationFrame(loop); }
canvas.addEventListener('pointerdown', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const c = Math.floor((e.clientX - rect.left) * scaleX / CELL);
const r = Math.floor((e.clientY - rect.top) * scaleY / CELL);
if (r >= 0 && r < ROWS && c >= 0 && c < COLS) {
grid[r][c] = grid[r][c] ? 0 : 1;
draw(); updateInfo();
}
});
document.getElementById('play').addEventListener('click', () => {
running = !running;
document.getElementById('play').textContent = running ? 'Pause' : 'Play';
if (running) loop();
else cancelAnimationFrame(rafId);
});
document.getElementById('step').addEventListener('click', () => {
if (running) return;
step(); draw(); updateInfo();
});
document.getElementById('clear').addEventListener('click', () => {
running = false; cancelAnimationFrame(rafId);
document.getElementById('play').textContent = 'Play';
grid = Array.from({length: ROWS}, () => new Uint8Array(COLS));
gen = 0; draw(); updateInfo();
});
// Glider: classic 5-cell shape that moves diagonally
// Pattern (top-left at r,c): .X. / ..X / XXX
function placeGlider(r, c) {
[[0,1],[1,2],[2,0],[2,1],[2,2]].forEach(([dr,dc]) => { grid[r+dr][c+dc] = 1; });
}
// Blinker: 3 cells in a row that oscillate between horizontal and vertical
function placeBlinker(r, c) {
[[0,0],[0,1],[0,2]].forEach(([dr,dc]) => { grid[r+dr][c+dc] = 1; });
}
// Pulsar: period-3 oscillator, one of Life's most beautiful patterns
// Encoded as relative coords from top-left bounding box
function placePulsar(r, c) {
const coords = [
[0,2],[0,3],[0,4],[0,8],[0,9],[0,10],
[2,0],[2,5],[2,7],[2,12],
[3,0],[3,5],[3,7],[3,12],
[4,0],[4,5],[4,7],[4,12],
[5,2],[5,3],[5,4],[5,8],[5,9],[5,10],
[7,2],[7,3],[7,4],[7,8],[7,9],[7,10],
[8,0],[8,5],[8,7],[8,12],
[9,0],[9,5],[9,7],[9,12],
[10,0],[10,5],[10,7],[10,12],
[12,2],[12,3],[12,4],[12,8],[12,9],[12,10]
];
coords.forEach(([dr,dc]) => {
if (r+dr < ROWS && c+dc < COLS) grid[r+dr][c+dc] = 1;
});
}
document.getElementById('glider').addEventListener('click', () => {
placeGlider(1, 1); draw(); updateInfo();
});
document.getElementById('blinker').addEventListener('click', () => {
placeBlinker(10, 18); draw(); updateInfo();
});
document.getElementById('gun').addEventListener('click', () => {
placePulsar(5, 12); draw(); updateInfo();
});
// Verify core rules at startup (logged to console):
(function verify() {
// Test 1: Blinker period 2 — horizontal → vertical → horizontal
const g0 = Array.from({length: ROWS}, () => new Uint8Array(COLS));
g0[5][4] = g0[5][5] = g0[5][6] = 1; // horizontal blinker
const saved = grid; grid = g0;
step(); // gen 1: should be vertical
const vertOk = grid[4][5] && grid[5][5] && grid[6][5] &&
!grid[5][4] && !grid[5][6];
step(); // gen 2: back to horizontal
const horzOk = grid[5][4] && grid[5][5] && grid[5][6] &&
!grid[4][5] && !grid[6][5];
grid = saved; gen = 0;
console.assert(vertOk, 'Blinker gen1 (vertical) failed');
console.assert(horzOk, 'Blinker gen2 (horizontal) failed');
// Test 2: Still life — 2×2 block never changes
const g2 = Array.from({length: ROWS}, () => new Uint8Array(COLS));
g2[10][10] = g2[10][11] = g2[11][10] = g2[11][11] = 1;
grid = g2;
step();
const blockOk = grid[10][10] && grid[10][11] && grid[11][10] && grid[11][11];
grid = saved; gen = 0;
console.assert(blockOk, '2x2 block (still life) failed');
// Test 3: Isolated cell dies (underpopulation)
const g3 = Array.from({length: ROWS}, () => new Uint8Array(COLS));
g3[15][15] = 1;
grid = g3; step();
const dieOk = !grid[15][15];
grid = saved; gen = 0;
console.assert(dieOk, 'Isolated cell should die');
console.log('Game of Life rule verification:', vertOk && horzOk && blockOk && dieOk ? 'ALL PASSED' : 'SOME FAILED');
})();
draw(); updateInfo();
<p class="hint">Click cells to toggle alive/dead. Press <b>Play</b> to run the rules, or step one generation at a time. Use presets to place classic patterns and watch three rules compute.</p>
<div class="toolbar">
<button id="play" type="button">Play</button>
<button id="step" type="button">Step</button>
<button id="clear" type="button" class="ghost">Clear</button>
<span class="sep"></span>
<button id="glider" type="button" class="preset">Glider</button>
<button id="blinker" type="button" class="preset">Blinker</button>
<button id="gun" type="button" class="preset">Pulsar</button>
</div>
<canvas id="life" width="400" height="360"></canvas>
<div class="info" id="info">Generation: 0 | Population: 0</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.toolbar { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .5rem; align-items: center; }
.sep { flex: 1; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.preset { background: #457b9d; border-color: #457b9d; }
canvas { display: block; border: 1.5px solid #cdd9e3; border-radius: 6px; cursor: crosshair;
max-width: 100%; touch-action: none; }
.info { font-size: .85rem; color: #555; margin-top: .4rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Conway's Game of Life — three rules:
// Birth: dead cell with exactly 3 live neighbors → alive
// Survival: live cell with 2 or 3 live neighbors → stays alive
// Death: all other live cells → dead
// The system is Turing-complete; predicting the long-run fate is undecidable.
const COLS = 40, ROWS = 36, CELL = 10;
const canvas = document.getElementById('life');
const ctx = canvas.getContext('2d');
let grid = Array.from({length: ROWS}, () => new Uint8Array(COLS));
let gen = 0, pop = 0, rafId = null, running = false;
function idx(r, c) { return (r + ROWS) % ROWS * COLS + ((c + COLS) % COLS); }
function get(g, r, c) { return g[(r + ROWS) % ROWS][(c + COLS) % COLS]; }
function step() {
const next = Array.from({length: ROWS}, () => new Uint8Array(COLS));
pop = 0;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
let n = 0;
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++)
if (dr || dc) n += get(grid, r + dr, c + dc);
const alive = grid[r][c];
// Birth: dead + 3 neighbors; Survival: alive + 2 or 3
next[r][c] = (!alive && n === 3) || (alive && (n === 2 || n === 3)) ? 1 : 0;
if (next[r][c]) pop++;
}
}
grid = next;
gen++;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#e8f0f7';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#1d3557';
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (grid[r][c]) ctx.fillRect(c * CELL + 1, r * CELL + 1, CELL - 1, CELL - 1);
// light grid lines
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 0.4;
for (let r = 0; r <= ROWS; r++) { ctx.beginPath(); ctx.moveTo(0, r*CELL); ctx.lineTo(canvas.width, r*CELL); ctx.stroke(); }
for (let c = 0; c <= COLS; c++) { ctx.beginPath(); ctx.moveTo(c*CELL, 0); ctx.lineTo(c*CELL, canvas.height); ctx.stroke(); }
}
function updateInfo() {
pop = 0;
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) pop += grid[r][c];
document.getElementById('info').innerHTML =
fmt('Generation: %gen% | Population: %pop%', { gen: gen, pop: pop });
}
function loop() { step(); draw(); updateInfo(); if (running) rafId = requestAnimationFrame(loop); }
canvas.addEventListener('pointerdown', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const c = Math.floor((e.clientX - rect.left) * scaleX / CELL);
const r = Math.floor((e.clientY - rect.top) * scaleY / CELL);
if (r >= 0 && r < ROWS && c >= 0 && c < COLS) {
grid[r][c] = grid[r][c] ? 0 : 1;
draw(); updateInfo();
}
});
document.getElementById('play').addEventListener('click', () => {
running = !running;
document.getElementById('play').textContent = running ? 'Pause' : 'Play';
if (running) loop();
else cancelAnimationFrame(rafId);
});
document.getElementById('step').addEventListener('click', () => {
if (running) return;
step(); draw(); updateInfo();
});
document.getElementById('clear').addEventListener('click', () => {
running = false; cancelAnimationFrame(rafId);
document.getElementById('play').textContent = 'Play';
grid = Array.from({length: ROWS}, () => new Uint8Array(COLS));
gen = 0; draw(); updateInfo();
});
// Glider: classic 5-cell shape that moves diagonally
// Pattern (top-left at r,c): .X. / ..X / XXX
function placeGlider(r, c) {
[[0,1],[1,2],[2,0],[2,1],[2,2]].forEach(([dr,dc]) => { grid[r+dr][c+dc] = 1; });
}
// Blinker: 3 cells in a row that oscillate between horizontal and vertical
function placeBlinker(r, c) {
[[0,0],[0,1],[0,2]].forEach(([dr,dc]) => { grid[r+dr][c+dc] = 1; });
}
// Pulsar: period-3 oscillator, one of Life's most beautiful patterns
// Encoded as relative coords from top-left bounding box
function placePulsar(r, c) {
const coords = [
[0,2],[0,3],[0,4],[0,8],[0,9],[0,10],
[2,0],[2,5],[2,7],[2,12],
[3,0],[3,5],[3,7],[3,12],
[4,0],[4,5],[4,7],[4,12],
[5,2],[5,3],[5,4],[5,8],[5,9],[5,10],
[7,2],[7,3],[7,4],[7,8],[7,9],[7,10],
[8,0],[8,5],[8,7],[8,12],
[9,0],[9,5],[9,7],[9,12],
[10,0],[10,5],[10,7],[10,12],
[12,2],[12,3],[12,4],[12,8],[12,9],[12,10]
];
coords.forEach(([dr,dc]) => {
if (r+dr < ROWS && c+dc < COLS) grid[r+dr][c+dc] = 1;
});
}
document.getElementById('glider').addEventListener('click', () => {
placeGlider(1, 1); draw(); updateInfo();
});
document.getElementById('blinker').addEventListener('click', () => {
placeBlinker(10, 18); draw(); updateInfo();
});
document.getElementById('gun').addEventListener('click', () => {
placePulsar(5, 12); draw(); updateInfo();
});
// Verify core rules at startup (logged to console):
(function verify() {
// Test 1: Blinker period 2 — horizontal → vertical → horizontal
const g0 = Array.from({length: ROWS}, () => new Uint8Array(COLS));
g0[5][4] = g0[5][5] = g0[5][6] = 1; // horizontal blinker
const saved = grid; grid = g0;
step(); // gen 1: should be vertical
const vertOk = grid[4][5] && grid[5][5] && grid[6][5] &&
!grid[5][4] && !grid[5][6];
step(); // gen 2: back to horizontal
const horzOk = grid[5][4] && grid[5][5] && grid[5][6] &&
!grid[4][5] && !grid[6][5];
grid = saved; gen = 0;
console.assert(vertOk, 'Blinker gen1 (vertical) failed');
console.assert(horzOk, 'Blinker gen2 (horizontal) failed');
// Test 2: Still life — 2×2 block never changes
const g2 = Array.from({length: ROWS}, () => new Uint8Array(COLS));
g2[10][10] = g2[10][11] = g2[11][10] = g2[11][11] = 1;
grid = g2;
step();
const blockOk = grid[10][10] && grid[10][11] && grid[11][10] && grid[11][11];
grid = saved; gen = 0;
console.assert(blockOk, '2x2 block (still life) failed');
// Test 3: Isolated cell dies (underpopulation)
const g3 = Array.from({length: ROWS}, () => new Uint8Array(COLS));
g3[15][15] = 1;
grid = g3; step();
const dieOk = !grid[15][15];
grid = saved; gen = 0;
console.assert(dieOk, 'Isolated cell should die');
console.log('Game of Life rule verification:', vertOk && horzOk && blockOk && dieOk ? 'ALL PASSED' : 'SOME FAILED');
})();
draw(); updateInfo();
Notice how the same three rules produce wildly different fates: some patterns vanish in a few generations, others cycle endlessly, and the glider travels indefinitely. Deciding in advance which will happen for an arbitrary pattern is the undecidable part.
Comments
Loading comments...