← BB / INDEX
CASE STUDY·2026·LIVESOLO — DESIGN, ARCHITECTURE, ENGINEERING

Petriarch

A 20,000-agent artificial-life god game where evolution is real — no authored fitness function — and the CPU sim ports to WebGPU compute as a slot-in.

STACK

  • TypeScript
  • WebGPU
  • WGSL
  • PixiJS
  • Vite
  • SoA
Petriarch world at full population — up to 20,000 agents over a regrowing food field
A mature colony climbing toward the 20,000-agent ceiling: agents clumping by lineage over a regrowing food field, rendered as a live cyber-net.

INTRO

Petriarch is a god game about a petri dish. A population of around twenty thousand agents lives in a closed 2D world, foraging a food field that regrows, spending energy, reproducing when they can afford it, and dying when they can't. You never control an agent. You bloom food, drop hazards, and smite a region — you perturb the dish and watch what the dish does back. The north star was the Simpsons 'Treehouse of Horror' bit where a petri-dish civilization rises, goes to war, invents, and starts worshipping, all on fast-forward. The question was how far that gets pushed on a browser stack.

The agents actually evolve, and that's the constraint the whole project is built to protect. There is no fitness function. Nothing scores an agent and breeds the winners. Every trait — sense radius, speed, aggression, metabolism — rides on a genome, and the only selection pressure is the world itself: an agent that stays fed leaves more copies of its genes, and one that doesn't, doesn't. Writing a function that ranks agents and reproduces the good ones is, by the project's own rules, a bug. Underneath that living substrate is the part worth defending in an interview: a structure-of-arrays engine disciplined enough that moving it onto the GPU was a body-rewrite against a fixed contract, verified pass-by-pass against the CPU as ground truth.

01

SECTION

No fitness function, or it isn't evolution

The easy version of this project scores each agent, kills the low scorers, and clones the high ones. That's a genetic algorithm, and it produces evolution-shaped results that mean nothing, because the designer chose what 'good' was. Petriarch refuses it. Traits live on a per-agent genome, offspring inherit a mutated copy, and the only thing that decides who reproduces is whether an agent gathered enough energy to afford a child before something killed it.

Selection is entirely emergent from the world's economics. Food regrows at a rate; sensing costs energy; moving costs more; a hazard costs everything. What survives is whatever the current world rewards, and it shifts as the world shifts. That's why the interesting behaviors — lineages that clump with their own kin, predation niches opening up, frontiers that flip from fighting to trading — are findings rather than features. Nobody wrote 'form tribes.' The rule that a scoring-and-breeding function is a bug is enforced culturally and in review, because the moment it creeps in, the results stop being real.

Same-lineage agents clumping into distinct colored territories
Same-lineage clumping, unscripted: agents drift toward their own kin and distinct territories fall out of local rules.
02

SECTION

One index, twenty thousand agents

There is no `Agent` class anywhere in the codebase. Every gene and every field is its own typed array of length `MAX_AGENTS` — `x` is a `Float32Array`, `energy` is a `Float32Array`, `lineage` is an `Int32Array` — and agent `i` is index `i` in all of them. State is a stack of parallel columns, not a heap of objects. Death is an O(1) swap-remove: copy the last live agent over the dead slot and shrink the count. Birth reuses a freed slot. Nothing is allocated in the hot path; the buffers are sized to capacity once, and a flat browser heap timeline is the acceptance test.

This is what makes twenty thousand agents at interactive frame rates possible in a browser, but it's also what makes the whole thing debuggable. Randomness runs through a seeded `mulberry32` PRNG — `Math.random()` never appears in the sim — so a run is fully reproducible. You can snapshot and restore, fast-forward headlessly, and answer 'why did that lineage win' by replaying it bit-for-bit. All neighbor queries go through a single uniform-grid spatial hash built with a counting sort; there are no O(n²) loops, because at twenty thousand agents an O(n²) loop is the end of the frame budget.

03

SECTION

The buffer contract that made the GPU port a rewrite, not a redesign

The simulation is split into two tiers against one rule. Tier A — sense, steer, integrate, metabolism — is written as pure passes over flat buffers to a fixed stride contract: agent `i`'s x is `genes[i * GENE_COUNT + GENE_X]`, everywhere, on both CPU and GPU. Tier B — reproduction, death, conflict, trade, the god tools — stays branchy and stays on the CPU, where irregular control flow belongs. Because Tier A only ever touches the buffer through that stride contract, porting it to a WGSL compute shader is a mechanical translation of the same arithmetic, not a rethink. The CPU implementation is the golden reference; the GPU version slots in behind it.

The part that earns the architecture is the verification. A dedicated golden-reference harness runs each ported GPU pass against the CPU pass on a frozen snapshot of world state and compares the outputs, so a shader that diverges from the reference fails loudly instead of quietly producing a different universe. It also has to reckon with the sim continuing to tick while an async GPU readback is in flight — a race the verifier documents and controls for rather than pretending isn't there. This is the layer that separates 'I moved it to the GPU and it looks about the same' from 'I moved it to the GPU and proved it's the same computation.'

GPU-vs-CPU golden-reference verification output comparing pass results
The golden-reference runner: each WebGPU compute pass checked against the CPU reference on frozen state, divergence flagged.
04

SECTION

Civilization as authored layers on an unauthored substrate

Evolution gives you agents that survive; it doesn't give you a civilization. The social layers are authored on top of the evolved substrate, in the order a civilization tends to find them. Conflict comes first: agents contest ground and defend it. Trade comes next — caravans haul surplus across a dead zone between two populations, and the repeated route hardens into a road, which pacifies the frontier it crosses. Territory comes last: home-ground defense strengthens, and coherent borders fall out of it.

The agents participate in these systems; they don't invent them. But the systems are only interesting because the substrate underneath is real — a trade route only matters because the surplus being hauled was earned by agents the designer didn't hand-pick. The measurable payoff is a documented time trend from conflict toward commerce: turn trade off and populations stay poorer and more violent, turn it on and frontiers that were fighting start exchanging instead. That's checked the way the rest of the project is checked — not with an eyeball, but with headless study harnesses that run seeded scenarios and print the statistics.

Trade caravans crossing a dead zone between two populations, a road hardening along the route
Caravans hauling surplus across a frontier; the repeated route hardens into a road that pacifies the border it crosses.

OUTCOME

Live at petriarch.brac.dev, actively developed. The flagship path is WebGPU-only, so browsers without support get a blank canvas rather than a fallback — that's the one known gap. Backing the live demo is the verification culture: a GPU-vs-CPU golden-reference runner (Playwright plus SwiftShader in headless CI) and thirteen study harnesses that run seeded scenarios and print statistics on emergent behavior, from predation to trade. The distinctive part was never the a-life concept — it's the structure-of-arrays discipline that made a 20,000-agent CPU sim port to GPU compute as a slot-in.