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.
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.