← Emergence
Lab

Lab: Conway's Game of Life

25 min JavaScript

Lab: Conway’s Game of Life

Goal

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.

Setup

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.

Step 1: HTML Skeleton

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.

Step 2: Grid Constants and State

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.

Step 3: Random Initialization

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();

Step 4: Rendering

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.

Step 5: Count Neighbors

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;
}

Step 6: Apply the Four Rules

This is where emergence happens. The four rules are:

  1. Underpopulation: A live cell with fewer than 2 neighbors dies.
  2. Survival: A live cell with 2 or 3 neighbors lives.
  3. Overpopulation: A live cell with more than 3 neighbors dies.
  4. Reproduction: A dead cell with exactly 3 neighbors becomes alive.
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.

Step 7: Animation Loop

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.

Expected Output

After clicking Start, you will observe:

  • The random initial pattern rapidly evolves
  • Within 20-50 generations, chaotic regions settle into recognizable structures
  • Still lifes appear: stable 2x2 blocks and “beehive” shapes that never change
  • Oscillators appear: blinkers (3-cell lines) that flip between horizontal and vertical every generation
  • Gliders may appear: 5-cell patterns that translate diagonally across the grid, one cell per 4 generations
  • The generation counter increments with each frame

The population will typically decline from the initial 25% density, then stabilize as the grid fills with still lifes and oscillators.

Step 8 (Bonus): Click to Toggle Cells

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.

What You Learned