12 KiB
evolution — from a petri dish to a world
Course correction after watching the demo, agreed with the code:
What works. The song→analysis→events→wells→laws front-end genuinely connects
to the sim (coupling/residual and fingerprint divergence are real). The
determinism discipline is intact and now proven bit-for-bit in the browser.
The demo shell and the tools share one Run.
What failed. The world behind the Run. All songs grow the same three castes on the same 128² dish with the same opening and the same laws-as-multipliers. Statistically different, structurally identical. Two skins, not two habitats. And the camera watches it at the microscopic scale, so nothing larger ever has room to appear.
The correction is one idea: the world must be a macro ecology — population density, not individual dots — governed by structural rules the song compiles to, watched by a camera that travels instead of cutting.
Milestones
M0 — agreement ✔
Representation: population fields. Rules: a scoped rule program (compiled
per song, deterministic). Plan: this document. DESIGN.md updated to the new
end-goal.
M1 — the macro world: density, substeps, geography ✔
The entity sim is replaced by a population-field sim.
- World:
FIELD=128integrated density grid (rendered asRENDER_FIELD=256speck texture); layers: food, three caste populations, three tinted trail fields, wells, a static seeded jitter pair. - Time:
SUBSTEPS=5sim steps per rendered frame (the old world: 1), so local interactions propagate into coherent macro patterns. - Physics: forces are integrated on a coarse 64² grid (2×2 block sums — long-range coupling at a fraction of the cost), then advected on the fine grid with a square-root-free L1 axis split. Reaction: eat → logistic growth with a total-density gate → deposit; starvation on empty ground. Events still splat food and bloom the voice that spoke; rings land on strong hits.
- Home basins: each caste anchors to its own wells (the field-world "builder orbits its well"), so colonies persist as continents instead of melting into a uniform haze.
- Look: trails compose the geography; density renders as a glow plus deterministic speck swarms — "billions of little guys" is texture, at zero entity cost.
- Opening: the dish wakes into the song's own first bars.
Outcome: at 60 s the population is still geographically structured (8-block CV 1.5–1.9 vs ~0.1 before), songs and seeds diverge at a glance in-browser, openings differ per song, realtime holds at 640×360 (960×540 on reasonable hardware), and the test suite is green including a prepared-artifact bit-identity check. M2 then replaced the multiplier laws with the rule program; the camera's travel grammar is M3.
M2 — the rule program: structure, not knobs ✔
The laws-as-multipliers are replaced by a scoped rule program
(src/song/rules.js) — declarative per-song rules:
- castes are rule entries: speed / seek / dispersion / cap / growth / eat / deposit, plus growth rules — mutualism (a caste breeds fastest among the others, so mixed crowds beat monochrome blobs) and a social floor (sparks only bloom in a luminous crowd);
- interaction rules per caste pair (the signed band-correlation graph);
- per-section operator regimes — a breakdown drives disperse≈0 and deposit high (the world settles and inscribes); a drop drives home≈0 and turbulence+spawn high (the flood). Structural, scoped by the track's own character (noisy → turbulence up, tonal → deposit up);
- the event→response mapping: which voice feeds which caste, and how much of every hit splashes to the others;
- memory (trail/field half-lives, diffusion) and a global carrying capacity so one song cannot flood while another starves;
- compiled once per song, deterministically — the program is part of the song, rebuilt bit-identically by the prepared-artifact path, and v3's rule search now searches programs, not floats.
Outcome: 23 tests green, including: program compilation is deterministic; different songs get structurally different programs; regimes are structurally distinct by kind (a growth=0 edit visibly halts the world — the program is executed, not decoration); genesis pre-simulates generations so frame 0 is already an ecosystem, with the amount tuned per song; the opening is seeded where the song's own hits landed. Mixing is a program rule: castes co-mingle in one shared geography (drone 0.93, dusk 0.78, runner 0.48 block-mix; was ~0.02–0.15), the lens holds the whole world (zoom ~0.9–1.6), and divergence between songs grew to 16–40× where seeds stay ~0.01–0.05.
M2.5 — song-driven life, more generations, higher altitude ✔
- The song spawns: colonies are no longer placed at a pre-chosen set of spots. Every hit lands food and births life there (the voice first and loudest, splashes to the others); the drop regime visibly floods the ground with new colonies where the track hits.
- Genesis: the world pre-simulates several generations before frame 0 (the amount comes from the song — intro length, onset density, pulse), so the opening is an old, settled ecosystem, never a blank plate, and the film's history starts clean at frame 0.
- Altitude: the lens sits higher (zoom ~0.6–1.5), holding the whole map — different cultures and the roads and fronts between them — in frame.
M2.6 — the tribes are programs ✔
The M2 rule program steered how hard behaviours ran. Tribes go deeper: each
tribe is a small deterministic behaviour program (src/song/tribe.js) — a
tiny VM with
- sensing (food, own/other densities, trail, law, hunger, rim-edge, time),
- memory (four persistent registers, smoothed by the track's pulse),
- actions (move, excrete, eat, hit, spawn),
- basic branching (if hunger > threshold → seek food, else hold-and-ink),
compiled by the song: the band-correlation matrix signs who a tribe approaches vs flees, flatness sets how much it branches on gradients, onset density sets its spawning, the seed jitters thresholds. The world no longer executes a hand-tuned force formula on the coarse decision grid — it runs what each tribe's program decides, every substep. Surprising behaviour is possible precisely because the programs branch: a starving tribe behaves differently from a fed one, and the threshold is musical.
Outcome: the three societies are now balanced and mixed (drone 0.86 / runner 0.87 / dusk 0.95 block-mix; populations 40–76 k, castes near-equal), the geography holds (population CV 2.2–2.8). New tests pin the VM: programs compile deterministically and differ by song (restless/social/hoarding/breeding archetypes selected from the stats); branching changes behaviour; registers persist as memory; a no-op program visibly changes the film. Realtime holds at 640×360 (tribe runs add ~4 ms/frame at 960×540 in node).
Honest limit: the compiler's vocabulary is still a small archetype family, so the structural divergence between songs is modest (cross ≈ 0.16–0.18 vs within-seed ≈ 0.02–0.04, ~4–7×) even though the films look different. Broadening the instruction grammar (more branch shapes, richer memory, more action archetypes) is the natural next step for this layer.
M2.7 — one species, many minds: strains & allegiances ✔
Built — with one correction agreed mid-build, after watching the first films:
Allegiance does not change sides. The original draft had fronts converting, collapsed bodies leaking their members, splinters seceding and friendly bodies merging. On screen that read as one thing: every colour blurring into every other within a minute — ugly, and it erased the very polities the mechanic was meant to create. So the fluid politics were removed: bodies are founded once and persist; war kills, it does not convert; there is no defection and no merger. What keeps the cast plural is colonisation — a blessed settlement on virgin ground, far from every body core, plants a new flag. And the renderer now paints each cell in its DOMINANT body's colour, so territories read as territories.
What was built:
- strains (
src/song/strains.js) — the song decides how many minds it needs (2–5, from onset density, an independent spectral voice, texture noisiness, seed jitter) and compiles a LittleVM program per strain from motif roles: builder, grazer, scout, sentinel, breeder. Senses include the body's presence (KIN/FOE), the living wells, and the section's PEACE — a program can branch on war or truce. The world runs local selection: strains that thrive where they stand propagate. - living wells (
src/sim/wells.js) — a precompiled deterministic timeline of small, weak, numerous wells (4+ at open, ≤12 alive): born where a pitch class enters and holds, drifting along its harmony's compass, starving when it leaves. A birth is a land-rush; a death a retreat. - bodies & diplomacy — allegiance is a concentration field advected frozen-in with the population flux (Σ bodies = density exactly, a test). Diplomacy = the track's consonance bent by the section's peace operator; hostile fronts bleed the loser (casualties, never conversion). Colonisation plants new bodies on blessed virgin ground.
- a food-limited economy — the old world was accidentally cap-limited: the gate floor let life grow on bare ground, so the dish filled to a uniform saturated carpet and stopped moving. Now the gate is nearly shut without food and starvation dissolves colonies off bare ground: the population tracks its wells, blooms on them, and must keep moving to keep eating.
Outcome: 39 tests green, including the allegiance invariant (no negatives, composition = population), war-kills-not-converts (a war world ends poorer than its truce twin, with each body's mass still in its own heartland), colonisation, strain-mix drift, well determinism/drift/land-rush. Realtime holds at 640×360 (~12 ms/frame sim+render). Songs still diverge ≫ seeds on the fingerprint (classMix now sorted — the seed permutes the casting, the song decides its shape).
M3 — the lens: travel, don't cut
Replace the cut-to-a-point grammar with a move grammar: track (follow a moving front), reveal (pan along a coastline), establish (hold wide and slow). Cuts are reserved for musical punctuation — strong onsets in drop/build. Because the world is macro, a pan teaches the map instead of disorienting.
Exit: a 120 s render where a single front can be followed across the world without losing bearings; cuts read as beats, not jumps.
M4 — the film
Mux frames + PCM to video. No longer the bottleneck: the preview pipeline and the lens exist; once M1–M3 make the world worth exporting, muxing is plumbing.
The prepare step (pre-calculation)
Viewing should be fast because the expensive work happens ahead of time, once:
tools/prepare.jsruns the WAV → analysis → compile pipeline in node and writes a prepared song (out/prepared/<song>-<seed>.json): the compiled rule program and every analysis structure the world, camera, and metrics need — but not the raw PCM.- The browser fetches the prepared song instead of analysing the WAV. The WAV is still fetched, but only to feed the WebAudio clock (decoded straight to a buffer; analysis skipped).
src/song/serialize.jsis the single serialize/deserialize for both sides — the browser'sdeserializeSongmust rebuild a song that is bit-identical to one built by analysing the WAV, so prepare can never drift from the live path. That equality is a test.
This is exactly the DESIGN promise inverted and kept: the pipeline still defines the world; prepare only pays its cost once, off the viewing path.
Invariants, restated for the field world
- Determinism: same song + same seed + same frame ⇒ same pixel. No
Math.random, no wall clock, no per-substep RNG. The static jitter noise field is generated once from the seeded RNG at construction. - Causality: events still place food and rings where the track put them; a section boundary still changes the rules at the boundary only.
- Persistence: population fields carry their history; trails accumulate in the same three tinted layers the renderer already composes.
- Consequence: everything is a pure update rule over the previous substep; nothing is re-rolled.