Game of Life is an interactive cellular automata playground built to explore algorithmic simulation and front-end performance together. I used Conway’s Game of Life as a known problem space, then treated the project like a production-style app with a modular architecture, automated tests, and continuous deployment to GitHub Pages.
Key Features
I built a playable simulation app with controls, diagnostics, and pattern workflows:
Start, pause, single-step, reset, and speed controls for generation playback.
Pattern loading and custom board editing for quick experimentation.
URL state sharing so current board state can be copied and reopened.
Rule toggles (survival/birth variants) to inspect behavior changes.
Stats and diagnostics (live cells, births, deaths, generations) surfaced during execution.
Theming and localization support in the app shell for a more complete UX.
Screenshots
Conway’s Game of Life
Rules
The universe of the Game of Life is an infinite, two-dimensional orthogonal grid of squarecells, each of which is in one of two possible states, live or dead (or populated and unpopulated, respectively). Every cell interacts with its eight neighbours (its Moore neighborhood), which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:
Any live cell with fewer than two live neighbours dies, as if by underpopulation.
Two neighboring live cells die from underpopulation.
Before
→
Transform
→
Final
An isolated live cell dies immediately from underpopulation.
Before
→
Transform
→
Final
A horizontal pair dies because each cell has too few neighbors.
Before
→
Transform
→
Final
2. Survival
Any live cell with two or three live neighbours lives on to the next generation.
A blinker rotates while the center cell survives.
Before
→
Transform
→
Final
An L-shape grows into a stable 2x2 block.
Before
→
Transform
→
Final
A 2x2 block remains stable across generations.
Before
→
Transform
→
Final
3. Overpopulation
Any live cell with more than three live neighbours dies, as if by overpopulation.
A plus pattern overpopulates at the center and expands outward.
Before
→
Transform
→
Final
A dense cluster sheds overcrowded center cells.
Before
→
Transform
→
Final
A crowded corner cluster loses overpopulated cells.
Before
→
Transform
→
Final
4. Reproduction
Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
Three neighbors reproduce a new center cell.
Before
→
Transform
→
Final
A diagonal trio reproduces at center while isolated cells die.
Before
→
Transform
→
Final
The mirrored diagonal trio reproduces at center while isolated cells die.
Before
→
Transform
→
Final
Patterns
Patterns are organized using the same category model as the live app pattern library, with examples aligned to the Conway wiki taxonomy.
Still Life
Still lifes are stable patterns that remain unchanged from one generation to the next.
Pattern
Notes
Animated Example
Block
Minimal 2x2 stable structure.
Generation: 0
Beehive
Common 6-cell still life.
Generation: 0
Loaf
Stable asymmetric 7-cell form.
Generation: 0
Boat
Compact 5-cell stable pattern.
Generation: 0
Tub
4-cell diamond still life.
Generation: 0
Pond
Larger stable hollow shape.
Generation: 0
Oscillator
Oscillators repeat a fixed cycle of states over a predictable period.
Pattern
Notes
Animated Example
Blinker
Period-2 line flip.
Generation: 0
Toad
Period-2 six-cell oscillator.
Generation: 0
Beacon
Period-2 corner interaction.
Generation: 0
Spaceship
Spaceships translate across the grid while repeating their shape after a set number of generations.
Pattern
Notes
Animated Example
Glider
Canonical diagonal spaceship.
Generation: 0
LWSS
Lightweight orthogonal spaceship.
Generation: 0
Methuselah
Methuselahs begin as small seeds but evolve for many generations before stabilizing or vanishing.
Pattern
Notes
Animated Example
R-pentomino
Classic long-running seed.
Generation: 0
Diehard
Dies out after many generations.
Generation: 0
Acorn
Small seed with long evolution.
Generation: 0
Additional wiki categories for advanced exploration:
Guns: Gosper glider gun, Simkin glider gun
Puffers: puffer train variants
Breeders: puffer-type breeder constructions
Overview
The Game of Life project is structured as a layered monorepo: a deterministic simulation core and a separate React application shell for rendering and interaction. This separation keeps rule logic testable, keeps UI iteration fast, and allows performance tuning of simulation and rendering paths independently.
Planning
Problem Space
I wanted a project that did more than render a toy grid. The intent was to build an implementation that demonstrates practical engineering decisions around correctness, performance, and maintainability in a simulation-driven UI.
Architecture Goals
Preserve deterministic, testable simulation behavior independent of UI code.
Use sparse-state data structures so work scales with active cells instead of full-grid size.
Provide interactive controls and shareable state without coupling product UX to rule execution.
Keep package boundaries explicit so simulation and presentation layers can evolve independently.
Key Decisions
Key decisions that shaped the project:
Monorepo split (core + app): made game logic independently testable and reusable.
Sparse grid model: optimized for typical Game of Life patterns where most cells are dead.
Canvas-oriented rendering path: chosen to better support larger boards than naive DOM-cell rendering.
URL-encoded state sharing: improved collaboration and reproducibility of interesting board states.
CI + GitHub Pages deploys: ensured the project is always runnable from a public URL.
Challenges
The most persistent challenge has been UI performance as grid size and update cadence increase. I addressed this by keeping simulation logic decoupled from rendering, using a sparse state model in the core, and iterating on the rendering path separately from rule execution.
Another challenge was balancing feature depth with maintainability. Splitting into core and app packages, plus backing rules/pattern behavior with tests, helped reduce regressions while still allowing rapid UI iteration.
Architectural Overview
The implementation separates simulation concerns from presentation concerns so performance work and UX work can evolve independently.
flowchart LR
U[User input] --> A[React app shell]
A --> G[Game wrapper]
G --> C[Core simulation engine]
C --> A
Basic view of how user actions flow through the app and core simulation.
Core Simulation Engine
The core engine executes Conway rules as pure transformations: given a current sparse state map, it computes the next generation plus derived stats. Keeping this logic side-effect free makes behavior deterministic and straightforward to unit test.
This engine operates directly on the LifeGrid structure described in State and Data Model, so the data model and simulation step logic are intentionally paired.
The foundational board data structure is LifeGrid, which lives in the @game-of-life/core package at packages/core/src/interfaces/index.ts. LifeGrid is a coordinate-keyed hash map where each key is an "x,y" coordinate string and each value is a boolean cell state.
Board state is represented sparsely with this coordinate map instead of a dense full-grid matrix. This keeps both memory use and per-tick computation proportional to active regions, which is critical for larger boards where most cells are dead.
In practice, each simulation tick in the core engine accepts a LifeGrid, applies neighbor/rule evaluation, and returns the next LifeGrid, making this model the contract between data representation and rule execution.
// Example sparse state using LifeGrid.constgliderSeed:LifeGrid={'1,0':true,'2,1':true,'0,2':true,'1,2':true,'2,2':true,};
Rendering and Interaction Layer
The app package owns playback controls, user interactions, and canvas drawing. A thin Game wrapper adapts core outputs for the UI, so rendering concerns (frame pacing, viewport, theming, controls) can evolve without changing rule execution.
The repository uses package-level testing for simulation correctness and CI-backed deployments for repeatable release quality. This supports fast iteration while preserving confidence that rule logic and core behavior remain stable over time.
flowchart TD
PR[Pull request to master/main] --> TESTS[Run Unit Tests]
PR --> PREVIEW[Deploy Pages preview]
PREVIEW --> COMMENT[Post preview URL to PR]
MERGE[Push/merge to master/main] --> TESTS_MAIN[Run Unit Tests]
MERGE --> PAGES[Deploy to GitHub Pages]
MERGE --> CODEQL[CodeQL analysis]
CLOSE[PR closed] --> CLEANUP[Cleanup PR preview]
GitHub Actions pipelines for test, preview deploy, production deploy, security scan, and preview cleanup.
The test pyramid is intentionally weighted toward the core simulation layer. Unit tests cover rule evaluation, coordinate handling, and sparse state transitions at the base of the pyramid, while a smaller set of integration tests exercises the Game wrapper and key UI flows.
Coverage is strongest where behavior is deterministic and easiest to verify: the core package, pattern seed behavior, and transition logic. The UI layer keeps a lighter coverage profile focused on smoke tests, user controls, and regression checks around rendering and state syncing, which keeps the suite fast without losing confidence in the app shell.
Runtime Generation Loop
The core simulation uses a sparse dictionary/grid representation keyed by coordinates, keeping work proportional to active areas instead of a fixed full-board matrix. Rule execution and neighbor checks stay in the core package, while the UI consumes results through a Game wrapper and renders the snapshot to canvas.
sequenceDiagram
box rgba(45, 79, 128, 0.95) Presentation Layer
participant User
participant App as React App
end
box rgba(47, 124, 83, 0.95) Simulation Engine
participant Game as Game Wrapper
participant Core as Core Engine
end
box rgba(125, 74, 168, 0.95) Render Output
participant Render as Canvas Renderer
end
User->>App: Start simulation
loop each generation tick
App->>Game: step()
Game->>Core: calculateNextGeneration(state)
Core-->>Game: next state + stats
Game-->>App: updated snapshot
App->>Render: draw(snapshot)
Render-->>User: updated board and metrics
end
Generation loop from user action to rendered frame
Packages
The project is organized as an Nx monorepo with clear package boundaries and explicit TypeScript project references.
@game-of-life/core
@game-of-life/core is the simulation engine package. It owns deterministic rule evaluation, sparse grid transitions, coordinate utilities, and reusable pattern seed data, and remains framework-agnostic so it can be tested in isolation.
@game-of-life/app
@game-of-life/app is the product-facing UI package. It owns the React + Vite interface, playback controls, canvas rendering, and URL-based state sharing, and consumes the core package through a thin Game adapter layer.
flowchart LR
UI["@game-of-life/app\nReact + Vite UI"] --> GAME["Game class\nadapter layer"]
GAME --> CORE["@game-of-life/core\nPure simulation logic"]
CORE --> RULES["Rule engine\nneighbor checks + transitions"]
CORE --> PATTERNS["Pattern library\nseed presets"]
UI --> CANVAS["Canvas renderer\nviewport + draw loop"]
UI --> CONTROLS["Controls + URL state\nplayback, speed, sharing"]
classDef app fill:#2d4f80,stroke:#88a9d8,stroke-width:1.5px,color:#ffffff;
classDef core fill:#2f7c53,stroke:#9fd7b8,stroke-width:1.5px,color:#ffffff;
classDef supporting fill:#a86f12,stroke:#f3c97a,stroke-width:1.5px,color:#ffffff;
classDef output fill:#7d4aa8,stroke:#d9b3f7,stroke-width:1.5px,color:#ffffff;
class UI app;
class GAME supporting;
class CORE core;
class RULES supporting;
class PATTERNS supporting;
class CANVAS output;
class CONTROLS output;
click UI "https://github.com/wintermuted/game-of-life/tree/master/packages/app" "Open app package" _blank
click CORE "https://github.com/wintermuted/game-of-life/tree/master/packages/core" "Open core package" _blank
click PATTERNS "https://github.com/wintermuted/game-of-life/tree/master/packages/core/src/data" "Open pattern data" _blank
click CONTROLS "https://wintermuted.github.io/game-of-life/" "Open live demo" _blank
Monorepo package boundaries and runtime data flow
Outcomes
The project now functions as both an interactive demo and a maintainable engineering sample:
Rules and canonical patterns are validated by automated tests.
The simulation is publicly deployed on GitHub Pages for easy sharing.
Pull request preview deployments support faster iteration and review.
The architecture provides a clear path for future optimization work without rewriting core logic.
Roadmap
The next phase focuses on performance headroom, richer scenario authoring, and stronger collaboration workflows.
gantt
title Game of Life Future Roadmap
dateFormat YYYY-MM-DD
axisFormat %b '%y
section Performance
Profile render + tick loop :p1, 2026-07-05, 21d
Canvas batching + redraw minimization :p2, after p1, 28d
Worker-based simulation experiments :p3, after p2, 21d
section Features
Scenario presets + metadata model :f1, 2026-08-10, 20d
Rule-set editor UX improvements :f2, after f1, 24d
Replay timeline and scrub controls :f3, after f2, 18d
section Quality and Delivery
Expanded benchmark and regression suite :q1, 2026-09-01, 20d
PR preview UX polish pass :q2, after q1, 14d
Release hardening + docs refresh :q3, after q2, 10d
Planned milestones for the next optimization and feature cycle