Build a working implementation of Conway’s Game of Life in a single HTML file. You will watch complex structures – gliders, oscillators, still lifes – emerge from four simple rules applied to random initial conditions. This is emergence in action: no rule says “make a glider,” yet gliders appear.
You need a modern web browser (Chrome, Firefox, Safari, or Edge). No other dependencies.
Create a single file called game-of-life.html in any directory. All code goes in this one file.
Create game-of-life.html with the basic structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Conway's Game of Life</title>
<style>
body {
margin: 0;
background: #111;
display: flex;
flex-direction: column;
align-items: center;
font-family: monospace;
color: #ccc;
}
canvas {
margin-top: 20px;
border: 1px solid #333;
cursor: crosshair;
}
.controls {
margin-top: 10px;
}
button {
font-family: monospace;
padding: 6px 16px;
margin: 0 4px;
background: #222;
color: #ccc;
border: 1px solid #555;
cursor: pointer;
}
button:hover { background: #333; }
</style>
</head>
<body>
<h2>Conway's Game of Life</h2>
<canvas id="grid"></canvas>
<div class="controls">
<button id="start">Start</button>
<button id="step">Step</button>
<button id="reset">Reset</button>
</div>
<p id="gen">Generation: 0</p>
<script>
// All JavaScript goes here in the following steps
</script>
</body>
</html>
Open this file in your browser. You should see a dark page with an empty bordered canvas and three buttons.
Replace the // All JavaScript goes here comment with the following code. This defines the grid dimensions and creates two arrays – one for the current state and one for computing the next state.
const COLS = 80;
const ROWS = 60;
const CELL = 8;
const canvas = document.getElementById("grid");
const ctx = canvas.getContext("2d");
canvas.width = COLS * CELL;
canvas.height = ROWS * CELL;
let grid = createGrid();
let running = false;
let generation = 0;
function createGrid() {
return Array.from({ length: ROWS }, () =>
Array.from({ length: COLS }, () => 0)
);
}
Each cell is either 0 (dead) or 1 (alive). The grid is a 2D array of integers.
Add a function that fills the grid with random live and dead cells. Roughly 25% of cells start alive – enough to produce interesting dynamics without overcrowding.
function randomize() {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
grid[r][c] = Math.random() < 0.25 ? 1 : 0;
}
}
generation = 0;
}
randomize();
Add the draw function. Live cells are drawn as bright green squares; dead cells are left as the dark background.
function draw() {
ctx.fillStyle = "#111";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#0f0";
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (grid[r][c]) {
ctx.fillRect(c * CELL, r * CELL, CELL - 1, CELL - 1);
}
}
}
document.getElementById("gen").textContent = "Generation: " + generation;
}
draw();
Reload the page. You should see a random scattering of green cells on the canvas.
The core of the Game of Life is counting each cell’s live neighbors. Each cell has up to 8 neighbors (orthogonal and diagonal). The grid wraps at the edges (toroidal topology), so a glider that exits the right side re-enters on the left.
function countNeighbors(r, c) {
let count = 0;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const nr = (r + dr + ROWS) % ROWS;
const nc = (c + dc + COLS) % COLS;
count += grid[nr][nc];
}
}
return count;
}
This is where emergence happens. The four rules are:
function step() {
const next = createGrid();
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const n = countNeighbors(r, c);
if (grid[r][c]) {
// Rules 1-3: live cell survives only with 2 or 3 neighbors
next[r][c] = (n === 2 || n === 3) ? 1 : 0;
} else {
// Rule 4: dead cell with exactly 3 neighbors is born
next[r][c] = (n === 3) ? 1 : 0;
}
}
}
grid = next;
generation++;
}
Notice: no rule mentions gliders, blinkers, or any specific pattern. Those structures emerge from these four simple rules.
Add the main loop and wire up the buttons:
function loop() {
if (running) {
step();
draw();
requestAnimationFrame(loop);
}
}
document.getElementById("start").addEventListener("click", () => {
running = !running;
document.getElementById("start").textContent = running ? "Pause" : "Start";
if (running) loop();
});
document.getElementById("step").addEventListener("click", () => {
if (!running) {
step();
draw();
}
});
document.getElementById("reset").addEventListener("click", () => {
running = false;
document.getElementById("start").textContent = "Start";
randomize();
draw();
});
Reload and click Start. You should see the grid animate.
After clicking Start, you will observe:
The population will typically decline from the initial 25% density, then stabilize as the grid fills with still lifes and oscillators.
Add interactivity so you can draw your own patterns. Add this code after the button event listeners:
canvas.addEventListener("click", (e) => {
const rect = canvas.getBoundingClientRect();
const c = Math.floor((e.clientX - rect.left) / CELL);
const r = Math.floor((e.clientY - rect.top) / CELL);
if (r >= 0 && r < ROWS && c >= 0 && c < COLS) {
grid[r][c] = grid[r][c] ? 0 : 1;
draw();
}
});
Now pause the simulation, click Reset, then manually place a glider by toggling these five cells:
. X .
. . X
X X X
Click Start and watch it travel diagonally across the grid.