Conway's Game of Life

Conway's Game of Life is a classic example of how complexity can blossom from just a few straightforward rules. Picture an infinite grid of cells—each one can be "on" (alive) or "off" (dead). Each turn (or "generation"), every cell checks how many neighbors are alive. If it's overcrowded, it dies. If it's too lonely, it also dies. But with exactly three neighbors, a dead cell will spark into life.

This simple set of rules leads to endlessly fascinating behavior—from stable formations and repeating oscillators, to self-propagating "spaceships" that traverse the grid. It's a perfect demonstration of how complexity can arise from simplicity, and it's even Turing complete, meaning it can, in theory, simulate any computation.

How it works

Loading game board...

Pattern Examples

These classic patterns showcase the Game's emergent behavior. The "Glider" demonstrates movement across the grid, the "Pulsar" creates a mesmerizing oscillation, and the "Small Exploder" shows how simple shapes can evolve into complex forms.

The Algorithm

At its heart, the Game of Life runs on just a few elegant functions that determine life and death in our cellular universe:


// The core algorithm that determines life and death
function computeNextGeneration(currentGrid) {
  const newGrid = createEmptyGrid(rows, cols);
  
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const neighbors = countNeighbors(currentGrid, r, c);
      const cellState = currentGrid[r][c];

      // Apply Conway's rules of life
      if (cellState === 1) {
        // Live cell survives if it has 2 or 3 neighbors
        newGrid[r][c] = (neighbors === 2 || neighbors === 3) ? 1 : 0;
      } else {
        // Dead cell springs to life if it has exactly 3 neighbors
        newGrid[r][c] = (neighbors === 3) ? 1 : 0;
      }
    }
  }
  return newGrid;
}

The core algorithm applies Conway's rules to each cell in the grid. For each generation, it checks every cell's state and its neighbor count to determine if it lives, dies, or springs to life. This creates the mesmerizing patterns we see evolving on the board.


// Count live neighbors for each cell
function countNeighbors(g, row, col) {
  let count = 0;
  for (let i = 0; i < 8; i++) {
    const nr = row + NEIGHBOR_OFFSETS[i * 2];
    const nc = col + NEIGHBOR_OFFSETS[i * 2 + 1];
    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
      count += g[nr][nc];
    }
  }
  return count;
}

The neighbor counting function scans the eight cells surrounding each position. It carefully handles edge cases and provides the vital information needed to apply Conway's rules. This efficient implementation uses simple addition since living cells are represented as 1's and dead cells as 0's.


// Load and center a predefined pattern on the grid
function loadPattern(patternName) {
  if (!patterns[patternName]) return;
  
  // Calculate pattern dimensions
  const pattern = patterns[patternName];
  const bounds = calculatePatternBounds(pattern);
  
  // Center the pattern
  const rowOffset = Math.floor(rows / 2 - bounds.height / 2);
  const colOffset = Math.floor(cols / 2 - bounds.width / 2);
  
  // Place the pattern on the grid
  pattern.forEach(([r, c]) => {
    const newR = r + rowOffset;
    const newC = c + colOffset;
    if (isValidCell(newR, newC)) {
      grid[newR][newC] = 1;
    }
  });
}

Pattern loading is what allows us to place predefined structures onto the grid. It automatically centers the pattern and ensures it fits within the boundaries. This function transforms the simple coordinate lists below into living cells on our grid.


// Classic patterns that demonstrate emergent behavior
const patterns = {
  // Glider - Moves diagonally across the grid indefinitely
  glider: [
    [0, 1],                     // Top
    [1, 2],                     // Middle
    [2, 0], [2, 1], [2, 2]      // Bottom
  ],

  // Pulsar - Large symmetric oscillator with period 3
  pulsar: [
    // Outer frame
    [-4, -2], [-4, -1], [-4, 0], [-4, 1], [-4, 2],
    [4, -2], [4, -1], [4, 0], [4, 1], [4, 2],
    // Inner structure
    [-2, -4], [-1, -4], [0, -4], [1, -4], [2, -4],
    [-2, 4], [-1, 4], [0, 4], [1, 4], [2, 4]
  ],

  // Small Exploder - Rapidly expands into chaotic patterns
  smallExploder: [
    [0, 0],                     // Core
    [1, -1], [1, 0], [1, 1],    // Center
    [2, -1], [2, 1],            // Sides
    [3, 0]                      // Tail
  ]
};

These pattern definitions encode classic Game of Life structures using coordinate pairs. Each pattern demonstrates different emergent behaviors: the Glider moves endlessly across the grid, the Pulsar oscillates in place, and the Small Exploder creates expanding chaos from a tiny seed.