Langton’s Ant
A two-rule machine that draws highways.
Beginner / cellular-automaton / emergence
An ant on a grid follows two rules: turn right on a black cell, left on a white one, then flip the cell and step forward. For the first few hundred steps it looks lost; then it builds a recurring diagonal highway. Nothing in the rules mentions a highway.
Tagged: cellular automaton, turing machine, emergence
Live simulation
Each step applies a fixed number of ant rules. Keyboard: Space runs or pauses, . steps, r resets.
Speed
Each step applies a fixed number of ant rules.
Mathematics
The ant implements the simplest interesting rule machine on a grid: at each step, turn right on black or left on white, flip the cell's color, move forward one cell.
direction ← rotate(direction, ±90°) on black / white
cell ← 1 − cell; position ← position + heading
after ≈ 10,000 chaotic steps: unbounded diagonal highway, period 104
The highway is the famous result: the ant's seemingly random walk spontaneously organizes into a repeating construction pattern that extends forever. It has been proved that this two-rule ant is computationally universal; it can simulate any Turing machine given the right starting configuration.
How it works
The grid is a flat Uint8Array and the ant is four integers: position and heading. Each tick applies a fixed number of moves, flipping cells and counting the black population during the pass. Leaving the grid either wraps the torus or bounces the ant, depending on the edge mode.
Start from an empty grid and step slowly: the symmetric first few hundred steps, the messy middle, and the highway's emergence are all visible within a few thousand ticks.
Implementation
src/engine/experiments/langton-ant.ts · 131 lines · the module running above
TypeScript · fixed-timestep engine
| 1 | import { BaseExperiment } from "../core/base-experiment"; |
| 2 | import type { ParameterDef, ThemeColors } from "../core/types"; |
| 3 | |
| 4 | /** |
| 5 | * Langton's ant: a two-dimensional Turing machine. One tick applies a fixed |
| 6 | * number of move rules. Simple, deterministic, and a perfect demonstration |
| 7 | * that computed behavior can look designed without being designed. |
| 8 | */ |
| 9 | export class LangtonAntExperiment extends BaseExperiment { |
| 10 | readonly id = "langton-ant"; |
| 11 | |
| 12 | private cols = 0; |
| 13 | private rows = 0; |
| 14 | private offsetX = 0; |
| 15 | private offsetY = 0; |
| 16 | private cells = new Uint8Array(0); |
| 17 | private x = 0; |
| 18 | private y = 0; |
| 19 | private dir = 0; // 0 up, 1 right, 2 down, 3 left |
| 20 | private steps = 0; |
| 21 | private blackCells = 0; |
| 22 | |
| 23 | protected params(): ParameterDef[] { |
| 24 | return [ |
| 25 | { key: "cell", label: "Cell size", min: 3, max: 12, step: 1, defaultValue: 6, unit: "px" }, |
| 26 | { key: "rate", label: "Steps per tick", min: 1, max: 100, step: 1, defaultValue: 8 }, |
| 27 | { key: "density", label: "Initial noise", min: 0, max: 30, step: 1, defaultValue: 0, unit: "%" }, |
| 28 | { |
| 29 | key: "edges", |
| 30 | label: "Edges", |
| 31 | options: [ |
| 32 | { value: "wrap", label: "Wrap (torus)" }, |
| 33 | { value: "closed", label: "Bounce" }, |
| 34 | ], |
| 35 | defaultValue: "wrap", |
| 36 | }, |
| 37 | ]; |
| 38 | } |
| 39 | |
| 40 | protected onReset(): void { |
| 41 | this.cols = Math.max(8, Math.floor(this.width / this.num("cell"))); |
| 42 | this.rows = Math.max(8, Math.floor(this.height / this.num("cell"))); |
| 43 | this.offsetX = Math.floor((this.width - this.cols * this.num("cell")) / 2); |
| 44 | this.offsetY = Math.floor((this.height - this.rows * this.num("cell")) / 2); |
| 45 | this.cells = new Uint8Array(this.cols * this.rows); |
| 46 | const density = this.num("density") / 100; |
| 47 | for (let i = 0; i < this.cells.length; i++) { |
| 48 | this.cells[i] = this.rng() < density ? 1 : 0; |
| 49 | } |
| 50 | this.x = this.cols >> 1; |
| 51 | this.y = this.rows >> 1; |
| 52 | this.dir = 0; |
| 53 | this.steps = 0; |
| 54 | this.blackCells = 0; |
| 55 | for (let i = 0; i < this.cells.length; i++) this.blackCells += this.cells[i]; |
| 56 | } |
| 57 | |
| 58 | protected onParameterChange(key: string): void { |
| 59 | if (key !== "rate") this.reset(); |
| 60 | } |
| 61 | |
| 62 | resize(width: number, height: number): void { |
| 63 | this.width = width; |
| 64 | this.height = height; |
| 65 | this.reset(); |
| 66 | } |
| 67 | |
| 68 | protected onUpdate(): void { |
| 69 | const rate = this.num("rate"); |
| 70 | const wrap = this.str("edges") === "wrap"; |
| 71 | for (let s = 0; s < rate; s++) { |
| 72 | const i = this.y * this.cols + this.x; |
| 73 | const black = this.cells[i] === 1; |
| 74 | // Turn right on black, left on white; then flip the cell and move. |
| 75 | this.dir = (this.dir + (black ? 1 : 3)) % 4; |
| 76 | this.cells[i] = black ? 0 : 1; |
| 77 | this.blackCells += black ? -1 : 1; |
| 78 | if (this.dir === 0) this.y -= 1; |
| 79 | else if (this.dir === 1) this.x += 1; |
| 80 | else if (this.dir === 2) this.y += 1; |
| 81 | else this.x -= 1; |
| 82 | if (wrap) { |
| 83 | this.x = (this.x + this.cols) % this.cols; |
| 84 | this.y = (this.y + this.rows) % this.rows; |
| 85 | } else { |
| 86 | if (this.x < 0 || this.y < 0 || this.x >= this.cols || this.y >= this.rows) { |
| 87 | // Bounce back into the grid and reverse. |
| 88 | this.x = Math.min(this.cols - 1, Math.max(0, this.x)); |
| 89 | this.y = Math.min(this.rows - 1, Math.max(0, this.y)); |
| 90 | this.dir = (this.dir + 2) % 4; |
| 91 | } |
| 92 | } |
| 93 | this.steps += 1; |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | render(ctx: CanvasRenderingContext2D, theme: ThemeColors): void { |
| 98 | const { cols, rows, offsetX, offsetY } = this; |
| 99 | const cell = this.num("cell"); |
| 100 | ctx.fillStyle = theme.bg; |
| 101 | ctx.fillRect(0, 0, this.width, this.height); |
| 102 | ctx.fillStyle = theme.fg; |
| 103 | for (let y = 0; y < rows; y++) { |
| 104 | for (let x = 0; x < cols; x++) { |
| 105 | if (this.cells[y * cols + x] === 1) { |
| 106 | ctx.fillRect(offsetX + x * cell, offsetY + y * cell, cell - 1, cell - 1); |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | ctx.fillStyle = theme.accent; |
| 111 | ctx.fillRect(offsetX + this.x * cell, offsetY + this.y * cell, cell - 1, cell - 1); |
| 112 | } |
| 113 | |
| 114 | getMetrics() { |
| 115 | return { |
| 116 | steps: this.steps, |
| 117 | blackCells: this.blackCells, |
| 118 | position: `${this.x}, ${this.y}`, |
| 119 | heading: ["up", "right", "down", "left"][this.dir], |
| 120 | }; |
| 121 | } |
| 122 | |
| 123 | describe(): string { |
| 124 | return `The ant has taken ${this.steps.toLocaleString("en-US")} steps and flipped ${this.blackCells.toLocaleString("en-US")} cells to black.`; |
| 125 | } |
| 126 | |
| 127 | entities(): number { |
| 128 | return this.cols * this.rows; |
| 129 | } |
| 130 | } |
| 131 | |
Performance
- Time per tick
- O(steps per tick) with an O(1) rule per step
- Space
- One byte per cell plus four integers of ant state
- Notes
- The initial-noise parameter tests how sturdy the highway is: scatter random black cells and the ant still finds its road.
Live numbers (FPS, frame time, steps per second, microseconds per step, entity count) are in the performance overlay on the simulation above: the pulse icon in its top-right corner.