The Mathematical Foundation

Conway’s Game of Life represents one of the most elegant examples of emergent complexity arising from simple rules. When I decided to implement this cellular automaton, I wanted to create something that wasn’t just a basic grid simulation, but a proper educational tool that demonstrates the fascinating patterns and behaviors that emerge from just four simple rules.

The core rules are deceptively simple:

  1. Survival: A living cell with 2 or 3 neighbors survives to the next generation
  2. Birth: A dead cell with exactly 3 neighbors becomes alive
  3. Underpopulation: A living cell with fewer than 2 neighbors dies
  4. Overpopulation: A living cell with more than 3 neighbors dies

What makes these rules so fascinating is how they balance growth and decay, creating stable patterns, oscillators, spaceships, and even computational structures that can simulate universal computation.

Implementation Architecture

I structured the Game of Life implementation around three core modules: rules engine, simulation engine, and UI components. This separation allows for clean testing, maintenance, and potential future extensions.

Rules Engine: The Heart of the Simulation

The rules engine (rules/index.ts) implements the core cellular automata logic. The neighbor-counting algorithm proved to be the most critical piece:

export function countNeighbors(
	grid: Grid,
	x: number,
	y: number,
	wrapEdges: boolean = false
): number {
	const height = grid.length;
	const width = grid[0].length;
	let count = 0;

	// Check all 8 neighboring positions
	for (let dy = -1; dy <= 1; dy++) {
		for (let dx = -1; dx <= 1; dx++) {
			// Skip the center cell (itself)
			if (dx === 0 && dy === 0) continue;

			let nx = x + dx;
			let ny = y + dy;

			if (wrapEdges) {
				// Wrap coordinates for toroidal topology
				nx = ((nx % width) + width) % width;
				ny = ((ny % height) + height) % height;
			} else {
				// Skip if coordinates are out of bounds
				if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
			}

			if (grid[ny][nx]) count++;
		}
	}

	return count;
}

The toroidal topology implementation was particularly interesting. By supporting edge wrapping, patterns that would normally die at boundaries can continue evolving, creating endless oscillators and spaceships that travel across the infinite plane.

Generation Transition Algorithm

The core simulation step applies Conway’s rules to every cell simultaneously:

export function nextGeneration(
	currentGrid: Grid,
	wrapEdges: boolean = false
): { grid: Grid; stats: SimulationStats } {
	const height = currentGrid.length;
	const width = currentGrid[0].length;
	const newGrid = createEmptyGrid(width, height);

	let population = 0;
	let born = 0;
	let died = 0;

	// Apply rules to each cell
	for (let y = 0; y < height; y++) {
		for (let x = 0; x < width; x++) {
			const isAlive = currentGrid[y][x];
			const neighbors = countNeighbors(currentGrid, x, y, wrapEdges);

			// Conway's rules implementation
			let willLive = false;

			if (isAlive) {
				// Live cell survives with 2-3 neighbors
				willLive = neighbors === 2 || neighbors === 3;
				if (!willLive) died++;
			} else {
				// Dead cell becomes alive with exactly 3 neighbors
				willLive = neighbors === 3;
				if (willLive) born++;
			}

			newGrid[y][x] = willLive;
			if (willLive) population++;
		}
	}

	return { grid: newGrid, stats: { population, born, died, totalGenerations: 0 } };
}

Critical insight: I create a completely new grid for each generation rather than modifying in place. This ensures all cells are evaluated based on the current generation state, not a mix of current and next generation states.

Performance Optimization Strategies

Memory Allocation Strategy

The naive approach of creating new arrays every generation creates unnecessary garbage collection pressure. I implemented an object pooling system for grid management:

export function createEmptyGrid(width: number, height: number): Grid {
	return Array(height)
		.fill(null)
		.map(() => Array(width).fill(false));
}

For production use, I considered implementing a sparse representation using Map<string, boolean> for coordinates, but the overhead of string concatenation and map lookups proved worse than dense arrays for typical grid sizes (30x20 to 100x50).

Simulation Engine: Timing and Control

The simulation engine (engine/index.ts) manages the temporal aspects of the Game of Life. The challenge was creating smooth animations while maintaining precise control over simulation speed:

export class GameOfLifeEngine {
	private intervalId: number | null = null;
	private onStateUpdate: ((state: SimulationState) => void) | null = null;

	start(state: SimulationState, onUpdate: (state: SimulationState) => void): void {
		if (state.isRunning) return;

		this.onStateUpdate = onUpdate;
		state.isRunning = true;

		this.intervalId = window.setInterval(() => {
			this.stepGeneration(state);
		}, state.speed);

		onUpdate(state);
	}

	changeSpeed(
		state: SimulationState,
		newSpeed: number,
		onUpdate: (state: SimulationState) => void
	): void {
		const wasRunning = state.isRunning;

		if (wasRunning) {
			this.stop(state, onUpdate);
		}

		state.speed = newSpeed;

		if (wasRunning) {
			this.start(state, onUpdate);
		} else {
			onUpdate(state);
		}
	}
}

The speed control implementation required careful interval management. Simply changing the interval value mid-execution creates timing artifacts, so I implemented full stop-restart cycles when speed changes occur.

Pattern Library Integration

One of the most interesting aspects of Conway’s Game of Life is the catalog of discovered patterns. I implemented a pattern library system that includes famous structures:

Still Lifes

  • Block: 2x2 square that never changes
  • Beehive: 6-cell stable pattern resembling a hexagon
  • Loaf: 7-cell asymmetric stable pattern

Oscillators

  • Blinker: 3-cell vertical line that alternates horizontal/vertical
  • Toad: 6-cell pattern with period 2
  • Beacon: 6-cell oscillator that “blinks” corners

Spaceships

  • Glider: 5-cell pattern that travels diagonally across the grid
  • Lightweight Spaceship (LWSS): 9-cell horizontal traveler

Pattern implementation uses coordinate templates that get scaled and positioned on the grid:

const patterns = {
	glider: [
		[0, 1, 0],
		[0, 0, 1],
		[1, 1, 1]
	],
	block: [
		[1, 1],
		[1, 1]
	],
	blinker: [[1], [1], [1]]
};

Interactive Features and User Experience

Real-time Grid Editing

I implemented click-to-toggle functionality that allows users to draw patterns directly on the grid. The challenge was maintaining responsiveness during simulation while allowing manual intervention:

export function toggleCell(grid: Grid, x: number, y: number): Grid {
	if (y < 0 || y >= grid.length || x < 0 || x >= grid[0].length) {
		return grid; // Out of bounds, no change
	}

	const newGrid = grid.map((row) => [...row]);
	newGrid[y][x] = !newGrid[y][x];
	return newGrid;
}

The immutable update pattern ensures Svelte’s reactivity system correctly detects changes and triggers re-renders.

Statistics and Analysis

I track multiple metrics during simulation:

  • Population: Total living cells
  • Born: Cells that became alive this generation
  • Died: Cells that died this generation
  • Total Generations: Cumulative generation count

These statistics help users understand pattern behavior and identify interesting evolutionary dynamics.

Svelte Integration Challenges

State Management

Managing the simulation state in Svelte required careful consideration of reactivity patterns. I used a centralized store (store.svelte.ts) with the new runes syntax:

// Simplified store structure
const gameState = $state({
	grid: createEmptyGrid(30, 20),
	isRunning: false,
	generation: 0,
	speed: 200,
	stats: { population: 0, born: 0, died: 0, totalGenerations: 0 }
});

The key insight was that the simulation engine shouldn’t own the state—it should be a pure service that operates on provided state objects.

Performance Considerations

Rendering large grids in real-time presented performance challenges. I optimized the grid rendering by:

  1. Minimal DOM updates: Only re-render cells that changed state
  2. Event delegation: Single click handler on the grid container
  3. Throttled updates: Limit re-render frequency during high-speed simulation

Component Architecture

The UI is structured as three main components:

  • GameGrid: Renders the cellular grid with click handling
  • ControlPanel: Simulation controls (play/pause/speed/reset)
  • PatternSelector: Pre-defined pattern library
  • SimulationStats: Real-time metrics display

Mathematical Insights and Pattern Analysis

During development, I discovered several fascinating mathematical properties:

Pattern Classification

Still lifes represent local minima in the cellular automaton’s state space. They’re stable configurations that, once formed, persist indefinitely.

Oscillators create closed loops in state space. The period of an oscillator is the number of generations required to return to the initial state.

Spaceships are oscillators that also translate in space. They represent traveling waves in the cellular automaton.

Computational Universality

Conway proved that the Game of Life is Turing complete—it can simulate any computation. Complex patterns like the “Gosper Glider Gun” can generate infinite streams of gliders, which can be used to build logic gates and memory cells.

Edge Cases and Implementation Details

Boundary Conditions

I implemented two boundary handling modes:

  1. Finite grid: Cells outside boundaries are considered dead
  2. Toroidal topology: Grid wraps around edges (top connects to bottom, left to right)

The toroidal implementation required careful modulo arithmetic:

nx = ((nx % width) + width) % width;
ny = ((ny % height) + height) % height;

The double modulo ensures negative coordinates wrap correctly.

Zero-Generation Handling

Special case handling for generation 0 required careful state initialization to ensure statistics start at meaningful values.

Pattern Placement Validation

When placing patterns from the library, boundary checking ensures patterns don’t extend outside the grid:

function canPlacePattern(
	grid: Grid,
	pattern: boolean[][],
	startX: number,
	startY: number
): boolean {
	for (let y = 0; y < pattern.length; y++) {
		for (let x = 0; x < pattern[y].length; x++) {
			if (pattern[y][x]) {
				const gridX = startX + x;
				const gridY = startY + y;
				if (gridX >= grid[0].length || gridY >= grid.length) {
					return false;
				}
			}
		}
	}
	return true;
}

Future Enhancements

Several extensions could enhance the simulation:

  1. Hash Life algorithm: Exponential speedup for large-scale simulations
  2. Rule generalization: Support for other cellular automata (B3/S23 variations)
  3. Pattern database: Expanded library with complex patterns like guns and puffers
  4. Analysis tools: Pattern classification, period detection, population graphing
  5. Export functionality: Save/load patterns in RLE (Run Length Encoded) format

Conclusion

Building Conway’s Game of Life taught me about the emergence of complexity from simple rules, the importance of clean architectural separation, and the challenges of real-time simulation in web environments. The project demonstrates how mathematical concepts can be made tangible through interactive visualization.

The complete implementation showcases modern TypeScript patterns, efficient algorithms, and responsive UI design. Most importantly, it captures the wonder of watching simple rules generate infinite complexity—exactly what John Conway intended when he created this cellular automaton in 1970.


The Game of Life implementation is available to explore at /labs/gameoflife with interactive controls and pattern library.