INTRO
Petriarch is a god game about a petri dish. A closed 2D world holds a food-bound population, typically seven to twelve thousand agents against a 20,000-agent buffer cap, foraging a food field that regrows, spending energy, reproducing when they can afford it, and dying when they can't. You don't control an agent directly. You bloom food, drop hazards, and smite a region: you perturb the dish and watch what it 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.
Behavior and body are driven by a genome, and protecting that mechanism is the constraint the whole project sits under. Nothing scores an agent and breeds the winners: every trait, sense radius, speed, aggression, metabolism, rides on the 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. A function that ranks agents and reproduces the good ones is ruled out by the project's own conventions, because it would substitute an authored judgment of 'good' for whatever the world happens to reward. Underneath that substrate is the part worth walking through in an interview: a structure-of-arrays engine disciplined enough that moving it onto the GPU was a rewrite against a fixed buffer contract, checked pass by pass against the CPU as the reference implementation.
SECTION
What decides who breeds
One way to build this project scores each agent, kills the low scorers, and clones the high ones. That's a genetic algorithm, and the designer is the one deciding what 'good' means, which is a different experiment than the one I wanted to run. Petriarch doesn't do that. 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 comes from the world's economics instead. 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 behaviors like predation niches opening up and frontiers flipping from fighting to trading show up as findings rather than features: nobody wrote 'form tribes.' Kin clumping itself is authored, a KIN_COHESION gene steers agents toward the centroid of their own kind, but which agents count as kin is not: the social signature that defines it drifts and mutates on its own, so the lineages that end up clumping together are an emergent result even though the pull toward kin is a scripted one. The rule against a scoring-and-breeding function has no mechanical guardrail behind it, no test, no lint, no CI: it's written down in the project's own conventions and held by discipline alone, because the moment it creeps in, selection starts coming from the designer instead of the world.

SECTION
One index, twenty thousand agents
There is no `Agent` class anywhere in the codebase. Every per-agent field is its own typed array of length `MAX_AGENTS`: `posX` is a `Float32Array`, `energy` is a `Float32Array`, `lineageId` is an `Int32Array`, and agent `i` is index `i` in all of them. The genome is the one exception: it's a single flat `Float32Array` of length `MAX_AGENTS * GENE_COUNT`, with agent `i`'s eighteen genes packed contiguously rather than one array per gene, which is exactly the layout the GPU stride contract below depends on. 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 check I use to catch a regression.
This is the layout that keeps the whole thing debuggable and gives the GPU port below a fixed contract to port against. Randomness runs through a seeded `mulberry32` PRNG, and `Math.random()` doesn't appear in the sim, so a run replays deterministically from its seed. You can snapshot and restore, fast-forward headlessly, and answer 'why did that lineage win' by replaying it. All neighbor queries go through a single uniform-grid spatial hash built with a counting sort instead of a distance loop over every pair, because at twenty thousand agents that pairwise loop would dominate the frame budget.
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 SIZE gene is `genes[i * GENE_COUNT + GENE.SIZE]`, the same expression 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 means translating the same arithmetic against the same contract, not rethinking the logic. The CPU implementation is the reference; the GPU version runs 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 the check 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 ignoring. 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 checked it's the same computation.'

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, added one at a time in a fixed build order. 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 that speeds crossings. The trade riding that road is what stamps the ground with amity, and it's the amity, read by the conflict system, that actually raises the threshold for a fight there. 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. The systems are only worth watching because of what's underneath: a trade route only matters because the surplus being hauled was earned by agents the designer didn't hand-pick. A seeded 20,000-tick run tracked fights per capita as trade develops: it starts around 1.25 fights per thousand ticks per agent and falls to about 0.38 by the end of the run, roughly a 70 percent drop, while the population itself keeps growing. That's checked with headless study harnesses that run seeded scenarios and print the statistics, not by eyeballing the render.

OUTCOME
Live at petriarch.brac.dev, actively developed. The simulation runs on the CPU everywhere; WebGPU is an optional accelerator for it, not a requirement, and falls back to the CPU path automatically when a browser lacks support. Rendering does need WebGL, and a browser without it gets a clear unsupported-browser panel rather than a blank canvas: that's the one real constraint. Backing the live demo is a verification habit: a GPU-vs-CPU golden-reference runner (a hand-run Playwright and SwiftShader tool, not wired into CI) and nine study harnesses, each one labeled in its own header a temporary research harness, that run seeded scenarios and print statistics on emergent behavior, from predation to trade. The distinctive part isn't the a-life concept, it's the structure-of-arrays discipline that let a 20,000-agent CPU simulation move to GPU compute as a rewrite instead of a redesign.
