// The macro ecology (EVOLUTION.md M1, retargeted by M2.7). // // One population, many minds, several bodies. The world is a single // population field whose density is partitioned two ways: // // strains personality — the song's compiled LittleVM programs. Each // strain advects by its own program's decisions; the mix of // strains evolves by local selection (what eats and grows // where it stands, propagates there). // allegiance membership — which macro-organism ("body") the organisms in // a cell belong to. Allegiance is a composition field advected // frozen-in with the population flux, and it CHANGES: fronts // convert, collapsing bodies leak their members to neighbours, // cut-off splinters found new bodies, intermixed friends merge. // // Wells are alive too: a precompiled timeline plays births, drift and deaths // of small, weak, numerous fertile places — the natural fronts between bodies. // // The front-end is unchanged: events land food, the section's law switches // the rules at its boundary, palette comes from the key. The invariants: // // PERSISTENCE trail fields accumulate the geography for tens of seconds. // CAUSALITY a hit still lands food, a ring and a local bloom exactly // where the track put it; a well's birth and death are the // track's own harmony. // CONSEQUENCE every substep is a pure update of the previous one. // DETERMINISM no Math.random, no wall clock, no per-substep RNG: the static // jitter noise field is generated once from the seeded Rng and // the substep order is canonical. import { Rng } from '../rng.js'; import { wellPos, wellEnv, buildKernel } from './wells.js'; import { runProgram, ensureFlat, SENSE_COUNT, GRAD_COUNT, ACTION_COUNT, REGS, S_FOOD, S_SELF, S_KIN, S_FOE, S_TRAIL, S_LAW, S_HUNGER, S_EDGE, S_TIME, S_TOTAL, S_WELL, S_PEACE, S_ROCK, S_GARDEN, G_GARDEN, G_FOOD, G_SELF, G_KIN, G_FOE, G_WELL, G_ROCK, } from '../song/strains.js'; const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v); export const FIELD = 128; export const RENDER_FIELD = 256; // speck/texture resolution at render (2× up) export const SUBSTEPS = 5; // sim steps per rendered frame (the old world: 1) export const MAX_BODIES = 8; // Ink per individual per second, world units (tuned against a ~15 s half-life // so a full cell writes a visible line in seconds). const DEPOSIT_SCALE = 0.012; // The decision cadence: the strains think every VM_EVERY-th substep (their // decisions persist in the action buffer between thoughts). 2 = ~15 thoughts // per second of film at 5 substeps. const VM_EVERY = 2; // Politics cadence (frames at the sim's fps). Allegiance never changes sides: // bodies are founded once and persist; war kills, it does not convert. const COLONIZE_EVERY = 60; // ~2 s: a blessed frontier settlement may found /** A normalised Gaussian footprint (cell indices + weights) per coarse block. */ function buildBlockKernels(F, FC) { const kernels = []; const bs = F / FC; for (let b = 0; b < FC * FC; b++) { const cx = ((b % FC) + 0.5) * bs - 0.5; const cy = (((b / FC) | 0) + 0.5) * bs - 0.5; const sig = Math.max(1.4, bs * 0.9); const r = Math.ceil(sig * 2.2); const denom = 1 / (2 * sig * sig); const idx = []; const w = []; let total = 0; for (let dy = -r; dy <= r; dy++) { const py = Math.round(cy) + dy; if (py < 0 || py >= F) continue; for (let dx = -r; dx <= r; dx++) { const px = Math.round(cx) + dx; if (px < 0 || px >= F) continue; const g = Math.exp(-(dx * dx + dy * dy) * denom); if (g < 0.02) continue; idx.push(py * F + px); w.push(g); total += g; } } const k = total > 0 ? 1 / total : 0; kernels.push({ idx: Int32Array.from(idx), w: Float32Array.from(w, (v) => v * k) }); } return kernels; } export class World { constructor(song, { fps = 30, field = FIELD, seed = song.seed, substeps = SUBSTEPS, genesis, vmEvery = VM_EVERY } = {}) { this.song = song; this.fps = fps; this.dt = 1 / fps; this.field = field; this.F = field; this.substeps = substeps; this.genome = song.genome; this.vmEvery = Math.max(1, Math.round(vmEvery)); this.program = song.program; this.strains = song.strains; this.m = this.strains.length; this.palette = song.palette; this.wellTimeline = song.wells.wells; this.rng = new Rng(seed >>> 0).fork('world'); // Pre-simulation (genesis): how long the world evolves before frame 0, // so the opening is an old ecosystem, not a blank plate. The amount is // the song's own: a long intro matures the opening world longer, a // dense or beatless track settles more slowly. const g = genesis !== undefined ? genesis : 1.5 + 2.5 * clamp01(Math.min(1, (song.laws[1]?.start ?? 4) / 16)) + 1.5 * clamp01(song.summary.onsetDensity / 6) + 2 * (1 - clamp01(song.summary.beatStrength)); this.genesisFrames = Math.max(0, Math.round(clamp01((g - 2) / 7) * 9 * fps)); const n = field * field; this.n = n; this.resource = new Float32Array(n); this.density = new Float32Array(n); // total population per cell // The population, partitioned by personality (strain). this.pop = Array.from({ length: this.m }, () => new Float32Array(n)); this.popNext = Array.from({ length: this.m }, () => new Float32Array(n)); // Allegiance: the population partitioned by body. alleg[b][i] is the // DENSITY of body-b organisms at cell i, so Σ_b alleg[b][i] equals // the cell's total population — a concentration, advected frozen-in // with the flux and renormalised to the population every frame. this.B = this.program.bodies.max; this.alleg = Array.from({ length: this.B }, () => new Float32Array(n)); this.allegNext = Array.from({ length: this.B }, () => new Float32Array(n)); // Trail history, one tinted layer per body — the geography the // audience sees, coloured by who wrote it. this.trail = Array.from({ length: this.B }, () => new Float32Array(n)); // The macro-organisms themselves. this.bodies = Array.from({ length: this.B }, (_, b) => ({ id: b, alive: false, color: b, founded: -1, parent: -1, cx: 0.5, cy: 0.5, presence: 0, peak: 0, })); this.aliveBodies = []; // Diplomacy: pairwise affinity between bodies (consonance of their // founding harmony bends it; the section's peace bends it live). this.bodyAff = Array.from({ length: this.B }, () => new Float32Array(this.B)); this.warmth = 0; this.activity = new Float32Array(32 * 32); // Rock (M2.8): the inert resource. A barrier the organisms cannot // cross, that the song drops and shatters, and that builders quarry, // carry and wall with. rockMask marks the impassable cells. this.rock = new Float32Array(n); this.rockMask = new Uint8Array(n); this.rockFallen = 0; // total stone the song has dropped // Flora (M2.9): the wild plants — the world's pioneers, living in // ISLANDS scattered at birth. They ARE food, they regrow on their // own, and they BLOOM when the world is starving or dwindling // (seeding it back to life), then recede once the population is // strong and feeding itself again. Never enough to sustain a // large world, and never anywhere but on their own seed ground. this.flora = new Float32Array(n); this.floraSeed = new Uint8Array(n); // the islands: where flora may live this.famine = 0; // smoothed mean hunger of the population this.peakPop = 0; // recent population high-water mark this.hungerMass = 0; // per-frame accumulator (the reaction fills it) // Cultivated ground: worked stone that slowly yields food, then // petrifies back to rock. garden[i] is the fertility left. this.garden = new Float32Array(n); this.gardenSrc = new Float32Array(n); // Recent tending, per cell: a visited garden yields far more. this.tend = new Float32Array(n); // Deterministic jitter: static signed noise fields from the seeded RNG, // read every substep. The track's flatness sets how loud it is. this.noise = new Float32Array(n); this.noise2 = new Float32Array(n); for (let i = 0; i < n; i++) { this.noise[i] = this.rng.signed(); this.noise2[i] = this.rng.signed(); } // Fine-grid 4-neighbour indices, precomputed once (no bounds checks). this.nUp = new Int32Array(n); this.nDown = new Int32Array(n); this.nLeft = new Int32Array(n); this.nRight = new Int32Array(n); for (let y = 0; y < field; y++) { for (let x = 0; x < field; x++) { const i = y * field + x; this.nUp[i] = y === 0 ? i : i - field; this.nDown[i] = y === field - 1 ? i : i + field; this.nLeft[i] = x === 0 ? i : i - 1; this.nRight[i] = x === field - 1 ? i : i + 1; } } // Coarse force grid: block sums of food + strains + bodies, forces per // block, plus a fine→coarse index table. Field 128 → 64² (2×2 blocks). const fcSide = Math.max(16, Math.round(field / 2)); const fc = fcSide * fcSide; this.FC = fcSide; this.fc = fc; this.blkR = new Float32Array(fc); this.blkPop = Array.from({ length: this.m }, () => new Float32Array(fc)); this.blkTrail = Array.from({ length: this.B }, () => new Float32Array(fc)); this.blkBody = Array.from({ length: this.B }, () => new Float32Array(fc)); this.blkTotal = new Float32Array(fc); this.blkRock = new Float32Array(fc); this.blkGarden = new Float32Array(fc); this.domB = new Int8Array(fc).fill(-1); this.subB = new Int8Array(fc).fill(-1); this.fxX = Array.from({ length: this.m }, () => new Float32Array(fc)); this.fxY = Array.from({ length: this.m }, () => new Float32Array(fc)); this.velX = new Float32Array(fc); this.velY = new Float32Array(fc); this.wellField = new Float32Array(fc); this.rockField = new Float32Array(fc); // smoothed stone: the far smell this.ancX = Array.from({ length: this.m }, () => new Float32Array(fc)); this.ancY = Array.from({ length: this.m }, () => new Float32Array(fc)); this.synergy = new Float32Array(this.m * fc); this.valScratch = new Float64Array(this.m); this.growScratch = new Float64Array(this.m); this.hostility = new Float32Array(fc); this.hitAt = new Float32Array(fc); // The last aim per (strain, block): the senses of WORK look where // the work points, one substep back — sight follows intent. this.aimX = new Int8Array(this.m * fc); this.aimY = new Int8Array(this.m * fc); this.cUp = new Int32Array(fc); this.cDown = new Int32Array(fc); this.cLeft = new Int32Array(fc); this.cRight = new Int32Array(fc); for (let y = 0; y < fcSide; y++) { for (let x = 0; x < fcSide; x++) { const i = y * fcSide + x; this.cUp[i] = y === 0 ? i : i - fcSide; this.cDown[i] = y === fcSide - 1 ? i : i + fcSide; this.cLeft[i] = x === 0 ? i : i - 1; this.cRight[i] = x === fcSide - 1 ? i : i + 1; } } this.coarseOf = new Int32Array(n); const bs = field / fcSide; // fine cells per coarse side (2) for (let y = 0; y < field; y++) { const cy = Math.min(fcSide - 1, (y / bs) | 0); for (let x = 0; x < field; x++) { const cx = Math.min(fcSide - 1, (x / bs) | 0); this.coarseOf[y * field + x] = cy * fcSide + cx; } } // Quarried burden per strain, a fine field advected with the // population flux — stone is genuinely HAULED: dug at the quarry, // carried with the crowd, walled at home. this.carry = Array.from({ length: this.m }, () => new Float32Array(n)); this.carryNext = Array.from({ length: this.m }, () => new Float32Array(n)); // Rounded build footprints: a Gaussian kernel per coarse block, so // walls rise organically instead of as square tiles. this.buildKernel = buildBlockKernels(field, Math.max(16, Math.round(field / 2))); // Strain programs run on this decision grid: registers are the // strains' persistent memory, the action slots carry each decision. this.stMem = new Float32Array(this.m * REGS * fc); this.stAct = new Float32Array(this.m * ACTION_COUNT * fc); this.ss = new Float32Array(SENSE_COUNT); this.gg = new Float32Array(GRAD_COUNT); this.scratch = new Float32Array(ACTION_COUNT); // Economy per strain. this.caps = this.program.strains.map((s) => s.cap); this.growth = this.program.strains.map((s) => s.growth); this.eatR = this.program.strains.map((s) => s.eat); this.depositR = this.program.strains.map((s) => s.deposit); this.syn = this.program.strains.map((s) => s.synergy); this.cap = 0; for (let s = 0; s < this.m; s++) this.cap += this.caps[s] * n; this.avgSpeed = this.caps.length ? this.program.strains.reduce((a, s) => a + s.speed, 0) / this.m : 0.05; // Living wells: runtime views over the compiled timeline. this.wells = this.wellTimeline.map((w) => ({ ...w, kernel: null, kx: -9, ky: -9, peak: 1 })); // Home basins: every strain anchors to its NEAREST LIVING WELL — when // wells drift, the colonies drift with them; when one dies, the pull // moves to the next fertile place. this.homePull = this.m > 0 ? new Float32Array(this.m).fill(1.0) : new Float32Array(0); this.homeReach = 0.55; // History aligned with the analysis frames (metrics regress on it). this.history = { alive: new Float32Array(song.analysis.frames), movement: new Float32Array(song.analysis.frames), food: new Float32Array(song.analysis.frames), births: new Float32Array(song.analysis.frames), deaths: new Float32Array(song.analysis.frames), bodies: new Float32Array(song.analysis.frames), }; this.frame = -1; this.substepCount = 0; this.pulses = []; this.totalPop = 0; this.moveAccum = 0; this.birthsThisFrame = 0; this.deathsThisFrame = 0; this.seedRocks(); this.seedFlora(); this.refreshRockMask(); this.foundOpeningBodies(); this.seedInitial(); this.runGenesis(); } lawForFrame(frame) { const laws = this.song.laws; const t = frame / this.fps; for (let i = laws.length - 1; i >= 0; i--) { if (t >= laws[i].start) return laws[i]; } return laws[0]; } /** The rule program's current section: operator regime + energy. */ regimeForFrame(frame) { const sections = this.program.sections; const t = frame / this.fps; for (let i = sections.length - 1; i >= 0; i--) { if (t >= sections[i].start) return sections[i]; } return sections[0]; } /** How warm two bodies are to each other right now (−1 war … +1 truce). */ diplomacy(a, b) { if (a === b) return 1; return Math.max(-1, Math.min(1, this.bodyAff[a][b] + this.warmth)); } // ---- founding & seeding ------------------------------------------------ /** * Found the opening bodies: the song's first strong events are where its * macro-organisms take hold. Each founds a body; the founding pitch * classes set the pairwise affinity (harmonious founders stay kin). */ foundOpeningBodies() { const opening = this.program.bodies.opening; const strong = this.song.events .filter((e) => e.strength > 0.3) .slice(0, 24); const picks = []; for (const e of strong) { if (picks.length >= opening) break; if (picks.every((p) => Math.hypot(p.x - e.x, p.y - e.y) > 0.22)) picks.push(e); } while (picks.length < opening && strong.length) { picks.push(strong[picks.length % strong.length]); } const chroma = this.song.summary.chroma; picks.forEach((e, b) => { const body = this.bodies[b]; body.alive = true; body.founded = 0; body.cx = e.x; body.cy = e.y; this.aliveBodies.push(b); void chroma; }); // Affinity: founders born on nearby pitch classes are natural kin. for (let a = 0; a < this.aliveBodies.length; a++) { for (let c = a + 1; c < this.aliveBodies.length; c++) { const aff = 0.30 * (1 - Math.min(1, Math.hypot( this.bodies[a].cx - this.bodies[c].cx, this.bodies[a].cy - this.bodies[c].cy) / 0.5)); this.bodyAff[a][c] = aff; this.bodyAff[c][a] = aff; } } } /** * Furnish the dish and wake it into the song's own opening state. * * The wells furnish FOOD (the fertile places) but not life: the opening * colonies are spawned where the track's first bars actually land — the * song drives where organisms appear, and which body takes them. */ seedInitial() { const F = this.field; const res = this.resource; for (const well of this.wells) { if (well.born > 0) continue; this.refreshKernel(well, 0); const idx = well.kernel.idx; const w = well.kernel.w; for (let i = 0; i < idx.length; i++) { res[idx[i]] = Math.min(1.6, 1.2 * w[i] / well.peak); } } // The song's own opening, replayed as the genesis: each event places // food AND spawns life there — into the strain its voice speaks to, // under the flag of the nearest founding body. const reach = this.genesisFrames || this.fps * 3; const pool = []; for (const e of this.song.events) { if (pool.length >= 16 || e.frame >= reach) break; pool.push(e); } for (const e of this.song.events) { if (pool.length >= 6) break; if (!pool.includes(e)) pool.push(e); } const vs = this.program.events.voiceStrain; const splash = this.program.events.splash; for (const e of pool) { this.splat(e.x, e.y, e.spread, e.amount); const cell = Math.min(F - 1, (e.y * F) | 0) * F + Math.min(F - 1, (e.x * F) | 0); const body = this.nearestBody(e.x, e.y); const burst = 2.0 + 5.0 * e.strength; let spawned = burst; this.pop[vs[e.voice]][cell] += burst; for (let s = 0; s < this.m; s++) { if (s !== vs[e.voice]) { this.pop[s][cell] += burst * 0.5 * splash[s]; spawned += burst * 0.5 * splash[s]; } } this.addAllegiance(cell, body, spawned); } this.recount(); this.record(-1); } /** Body of the nearest living body-core (canonical tie-break: lowest id). */ nearestBody(x, y) { let best = -1; let bd = Infinity; for (const b of this.aliveBodies) { const body = this.bodies[b]; const d = Math.hypot(body.cx - x, body.cy - y); if (d < bd - 1e-9) { bd = d; best = b; } } return best >= 0 ? best : (this.aliveBodies[0] ?? 0); } /** Add body-density at a cell (the composition then tracks population). */ addAllegiance(i, body, amount) { this.alleg[body][i] += amount; } /** * Pre-simulate the genesis epoch (frames −G … −1). No events fire (the * index has nothing below 0), so the world settles under the intro regime: * several generations of growth, diffusion, deposition and decay decide * where the opening's bodies actually stand. History stays untouched. */ runGenesis() { const G = this.genesisFrames; for (let f = 0; f < G; f++) { this.frame++; const regime = this.regimeForFrame(this.frame); this.pulses.length = 0; this.birthsThisFrame = 0; this.deathsThisFrame = 0; this.moveAccum = 0; this.warmth = Math.max(-1, Math.min(1, this.program.diplomacy.base * 0.6 + 0.35 * (regime.operators.peace - 1))); const living = this.framePrep(regime); this.updateRockField(); this.inject(this.frame, regime); this.refreshRockMask(); for (let s = 0; s < this.substeps; s++) this.substep(regime); this.decay(regime, living); this.politics(regime, true); } this.frame = -1; this.pulses.length = 0; } recount() { const n = this.n; this.density.fill(0); let s = 0; for (let c = 0; c < this.m; c++) { const p = this.pop[c]; const d = this.density; for (let i = 0; i < n; i++) { const v = p[i]; d[i] += v; s += v; } } this.totalPop = s; } get count() { return this.totalPop; } // ---- field access ---------------------------------------------------- sample(fx, fy) { const F = this.field; const gx = Math.max(0, Math.min(F - 1.001, fx * (F - 1))); const gy = Math.max(0, Math.min(F - 1.001, fy * (F - 1))); const x0 = gx | 0; const y0 = gy | 0; const tx = gx - x0; const ty = gy - y0; const i = y0 * F + x0; const a = this.resource[i]; const b = this.resource[i + 1]; const c = this.resource[i + F]; const d = this.resource[i + F + 1]; return (a + (b - a) * tx) * (1 - ty) + (c + (d - c) * tx) * ty; } splat(x, y, spread, amount) { const F = this.field; const sigma = Math.max(0.8, spread * F); const r = Math.ceil(sigma * 2.4); const cx = Math.round(x * (F - 1)); const cy = Math.round(y * (F - 1)); const denom = 1 / (2 * sigma * sigma); let total = 0; const idx = []; const w = []; for (let dy = -r; dy <= r; dy++) { const py = cy + dy; if (py < 0 || py >= F) continue; for (let dx = -r; dx <= r; dx++) { const px = cx + dx; if (px < 0 || px >= F) continue; const k = Math.exp(-(dx * dx + dy * dy) * denom); if (k < 0.01) continue; idx.push(py * F + px); w.push(k); total += k; } } if (total <= 0) return; const k = amount / total; const res = this.resource; for (let i = 0; i < idx.length; i++) res[idx[i]] += w[i] * k; } // ---- rock --------------------------------------------------------------- /** Scatter the world's starting stone: deterministic, from the seed. */ seedRocks() { const n = this.program.rock.scatter; for (let k = 0; k < n; k++) { const x = this.rng.next(); const y = this.rng.next(); const sigma = 0.030 + 0.045 * this.rng.next(); const amount = 5.0 + 9.0 * this.rng.next(); this.rockSplat(x, y, sigma, amount); } } /** * Add (or, with a negative amount, shave) rock in a Gaussian blob. * Returns the touched cells so callers can crush or yield. */ rockSplat(x, y, spread, amount) { const F = this.field; const sigma = Math.max(0.8, spread * F); const r = Math.ceil(sigma * 2.4); const cx = Math.round(x * (F - 1)); const cy = Math.round(y * (F - 1)); const denom = 1 / (2 * sigma * sigma); let total = 0; const idx = []; const w = []; for (let dy = -r; dy <= r; dy++) { const py = cy + dy; if (py < 0 || py >= F) continue; for (let dx = -r; dx <= r; dx++) { const px = cx + dx; if (px < 0 || px >= F) continue; const k = Math.exp(-(dx * dx + dy * dy) * denom); if (k < 0.01) continue; idx.push(py * F + px); w.push(k); total += k; } } if (total <= 0) return { idx: [], w: [], k: 0 }; const k = amount / total; const rock = this.rock; const pop = this.pop; for (let i = 0; i < idx.length; i++) { const cell = idx[i]; if (amount >= 0) { const before = rock[cell]; rock[cell] = Math.min(2.2, rock[cell] + w[i] * k); const gained = rock[cell] - before; // A rockfall crushes what it lands on. if (gained > 0.05) { const crush = Math.max(0, 1 - gained * 0.55); for (let s = 0; s < this.m; s++) pop[s][cell] *= crush; } } else { rock[cell] = Math.max(0, rock[cell] + w[i] * k); } } return { idx, w, k }; } /** Scatter the wild flora: small patches, deterministic from the seed. */ seedFlora() { const n = 9 + Math.round(6 * clamp01(this.song.summary.onsetDensity / 6)); for (let k = 0; k < n; k++) { const x = this.rng.next(); const y = this.rng.next(); const sigma = 0.010 + 0.011 * this.rng.next(); const F = this.field; const cx = Math.round(x * (F - 1)); const cy = Math.round(y * (F - 1)); const sig = Math.max(1.2, sigma * F); const r = Math.ceil(sig * 2.2); const denom = 1 / (2 * sig * sig); for (let dy = -r; dy <= r; dy++) { const py = cy + dy; if (py < 0 || py >= F) continue; for (let dx = -r; dx <= r; dx++) { const px = cx + dx; if (px < 0 || px >= F) continue; const i = py * F + px; if (this.rockMask[i]) continue; const g = Math.exp(-(dx * dx + dy * dy) * denom); this.floraSeed[i] = 1; this.flora[i] = Math.min(0.75, this.flora[i] + g * (0.35 + 0.3 * this.rng.next())); } } } } /** The impassable frontier, recomputed after any rock change. */ refreshRockMask() { const rock = this.rock; const mask = this.rockMask; for (let i = 0; i < rock.length; i++) mask[i] = rock[i] > 0.25 ? 1 : 0; } // ---- wells --------------------------------------------------------------- /** (Re)build a living well's recharge kernel at its current position. */ refreshKernel(well, frame) { const [x, y] = wellPos(well, frame); const minStep = 0.6 / this.field; if (!well.kernel || Math.abs(x - well.kx) > minStep || Math.abs(y - well.ky) > minStep) { well.kernel = buildKernel(this.field, x, y, well.sigma); well.kx = x; well.ky = y; let peak = 0; for (let i = 0; i < well.kernel.w.length; i++) if (well.kernel.w[i] > peak) peak = well.kernel.w[i]; well.peak = Math.max(1e-6, peak); } } /** The wells alive at a frame, with their recharge envelope. */ livingWells(frame) { const out = []; for (const w of this.wells) { const env = wellEnv(w, frame, this.fps); if (env <= 0) continue; this.refreshKernel(w, frame); out.push({ well: w, env }); } return out; } /** A land-rush: a well is born — food floods in, the nearest body claims. */ wellBirth(well, frame, spawnOp = 1) { const [x, y] = wellPos(well, frame); this.splat(x, y, well.sigma * 1.6, 2.5); const F = this.field; const cell = Math.min(F - 1, (y * F) | 0) * F + Math.min(F - 1, (x * F) | 0); const body = this.nearestBody(x, y); const vs = this.program.events.voiceStrain; const burst = (0.25 + 1.2 * spawnOp) * 3.0; for (let s = 0; s < this.m; s++) { this.pop[s][cell] += burst; } this.addAllegiance(cell, body, burst); const ax = Math.max(0, Math.min(31, (x * 32) | 0)); const ay = Math.max(0, Math.min(31, (y * 32) | 0)); this.activity[ay * 32 + ax] += 4; } // ---- step --------------------------------------------------------------- /** Frame setup shared by step() and the genesis epoch. */ framePrep(regime) { const f = this.frame; // The section's warmth: the diplomacy base bent by the peace operator. const ops = regime.operators; this.warmth = Math.max(-1, Math.min(1, this.program.diplomacy.base * 0.6 + 0.35 * (ops.peace - 1))); // Living wells: births fire their land-rush (as hard as the section's // spawn operator allows); the well field (the coarse attraction the // strains smell) is rebuilt for this frame. for (const w of this.wells) { if (f === w.born) this.wellBirth(w, f, ops.spawn); } const living = this.livingWells(f); this.updateWellField(living); this.updateAnchors(living); return living; } step() { this.frame++; const f = this.frame; const regime = this.regimeForFrame(f); const cutoff = f - Math.ceil(this.fps * 1.6); while (this.pulses.length && this.pulses[0].frame < cutoff) this.pulses.shift(); this.birthsThisFrame = 0; this.deathsThisFrame = 0; this.moveAccum = 0; this.hungerMass = 0; const living = this.framePrep(regime); this.updateRockField(); this.inject(f, regime); this.refreshRockMask(); for (let s = 0; s < this.substeps; s++) this.substep(regime); this.decay(regime, living); this.politics(regime, false); this.record(f); return regime; } /** Coarse attraction field of the living wells (the strains smell this). */ updateWellField(living) { const FC = this.FC; const fc = FC * FC; this.wellField.fill(0); for (const { well, env } of living) { const [x, y] = wellPos(well, this.frame); const bx = x * (FC - 1); const by = y * (FC - 1); const sig = Math.max(1.4, well.sigma * this.field * 0.5); const r = Math.ceil(sig * 2.2); const denom = 1 / (2 * sig * sig); for (let dy = -r; dy <= r; dy++) { const py = Math.round(by) + dy; if (py < 0 || py >= FC) continue; for (let dx = -r; dx <= r; dx++) { const px = Math.round(bx) + dx; if (px < 0 || px >= FC) continue; const g = Math.exp(-(dx * dx + dy * dy) * denom) * env; this.wellField[py * FC + px] += g * 0.12; } } } } /** * The far smell of stone: the rock field, box-blurred twice on the * coarse grid. Adjacent-block gradients only pull at the rim; this is * what draws a quarry crew across the map. */ updateRockField() { const FC = this.FC; const fc = FC * FC; const f = this.rockField; f.fill(0); const rock = this.rock; const coarseOf = this.coarseOf; for (let i = 0; i < this.n; i++) f[coarseOf[i]] += rock[i]; for (let pass = 0; pass < 2; pass++) { const prev = Float32Array.from(f); for (let b = 0; b < fc; b++) { f[b] = (prev[b] * 4 + prev[this.cUp[b]] + prev[this.cDown[b]] + prev[this.cLeft[b]] + prev[this.cRight[b]]) / 8; } } } /** Home anchors: each strain is pulled to its nearest living well. */ updateAnchors(living) { const FC = this.FC; const fc = FC * FC; const m = this.m; const pull = 1.1; for (let b = 0; b < fc; b++) { const wx = (b % FC + 0.5) / FC; const wy = (((b / FC) | 0) + 0.5) / FC; let bx = 0; let by = 0; let bd = Infinity; for (const { well } of living) { const [x, y] = wellPos(well, this.frame); const dx = x - wx; const dy = y - wy; const d2 = dx * dx + dy * dy; if (d2 < bd) { bd = d2; bx = dx; by = dy; } } for (let s = 0; s < m; s++) { const i = b; if (bd < Infinity) { const d = Math.sqrt(bd) + 1e-6; const p = clamp01(1 - d / this.homeReach) * pull * this.homePull[s]; this.ancX[s][i] = (bx / d) * p; this.ancY[s][i] = (by / d) * p; } else { this.ancX[s][i] = 0; this.ancY[s][i] = 0; } } } } inject(frame, regime) { const events = this.song.index[frame]; if (!events) return; const F = this.field; const spawn = regime.operators.spawn; const ops = regime.operators; const vs = this.program.events.voiceStrain; const splash = this.program.events.splash; // The song spawns: how hard a hit births new life where it lands. The // drop regime's spawn operator floods the ground with colonies; the // intro barely seeds. const scale = 0.4 + 1.1 * spawn; for (const e of events) { // Food where the track put it. this.splat(e.x, e.y, e.spread, e.amount * scale); const cell = Math.min(F - 1, (e.y * F) | 0) * F + Math.min(F - 1, (e.x * F) | 0); const boost = (0.5 + 3.0 * e.strength) * scale; const body = this.nearestBody(e.x, e.y); const lead = vs[e.voice]; this.pop[lead][cell] += boost; let spawned = boost; for (let s = 0; s < this.m; s++) { if (s !== lead) { this.pop[s][cell] += boost * splash[s]; spawned += boost * splash[s]; } } this.addAllegiance(cell, body, spawned); const ax = Math.max(0, Math.min(31, (e.x * 32) | 0)); const ay = Math.max(0, Math.min(31, (e.y * 32) | 0)); this.activity[ay * 32 + ax] += 0.5 + 2 * e.strength; // Rock follows the song's own weather: strong low and mid hits // DROP stone where they land (and crush what they land on); // strong high hits SHATTER it — the percussion breaks the rock. if (e.strength > 0.6) { const rockPol = this.program.rock; if (e.voice === 2) { this.rockSplat(e.x, e.y, e.spread * 1.4, -e.strength * 2.4 * rockPol.shatter * ops.erode); } else { this.rockFallen += e.strength * 2.8 * ops.rockfall * rockPol.drop; this.rockSplat(e.x, e.y, e.spread * 1.3, e.strength * 2.8 * ops.rockfall * rockPol.drop); } this.activity[ay * 32 + ax] += 1.5; } // The clearest causality: a hit lands as a ring on the ground. if (e.strength > 0.42) { this.pulses.push({ x: e.x, y: e.y, frame, strength: e.strength, voice: e.voice }); } } } /** * Run every strain's program on every decision block. Senses are read * from the block sums + gradients — including the body the local * organisms belong to (KIN), the other bodies (FOE), the living wells, * and the section's PEACE. The MOVE actions become the advection forces; * the others are stored for the reaction phase. */ runStrains(regime) { const FC = this.FC; const fc = FC * FC; const blkRock = this.blkRock; const blkGarden = this.blkGarden; // The minds think slower than the world moves: programs run on a // fraction of the substeps (their decisions persist in the action // buffer), and empty ground thinks not at all. const decide = (this.substepCount % this.vmEvery) === 0; const m = this.m; const blkPop = this.blkPop; const blkTrail = this.blkTrail; const blkBody = this.blkBody; const blkR = this.blkR; const blkTotal = this.blkTotal; const domB = this.domB; const strains = this.strains; const ss = this.ss; const gg = this.gg; const act = this.scratch; const mem = this.stMem; const actBuf = this.stAct; const cU = this.cUp; const cD = this.cDown; const cL = this.cLeft; const cR = this.cRight; const fxX = this.fxX; const fxY = this.fxY; const synScratch = this.synergy; const wellField = this.wellField; const now = this.frame / this.fps; const start = regime.start; const span = Math.max(0.001, regime.end - regime.start); const progT = clamp01((now - start) / span); const lawSpawn = regime.operators.spawn; const peace = clamp01((this.warmth + 1) / 2); const actFC = FC * FC; const actBufAll = this.stAct; for (let b = 0; b < fc; b++) { const tot = blkTotal[b]; if (tot < 1e-4) { // Empty ground: no mind, no force — just make sure no stale // decision lingers in the action buffer. if (decide) { for (let s = 0; s < m; s++) { const base = s * ACTION_COUNT * actFC + b; for (let k = 0; k < ACTION_COUNT; k++) actBufAll[base + k * actFC] = 0; } } continue; } const ug = cU[b]; const dg = cD[b]; const lg = cL[b]; const rg = cR[b]; const r0 = blkR[b]; const dom = domB[b]; const domPres = dom >= 0 ? blkBody[dom][b] : 0; // Gradients of food and total population at this block. const gRx = blkR[rg] - blkR[lg]; const gRy = blkR[dg] - blkR[ug]; // Population pressure: crowded ground pushes outward. This is the // disperse operator's physics — a saturated colony spills over // its rim instead of packing to the ceiling and stalling. const dgx = blkTotal[rg] - blkTotal[lg]; const dgy = blkTotal[dg] - blkTotal[ug]; const pressure = 0.022 * regime.operators.disperse * clamp01(tot / 12); for (let s = 0; s < m; s++) { const own = blkPop[s][b]; const kinPres = dom >= 0 ? Math.max(0, blkBody[dom][b] - own) : 0; const foe = Math.max(0, tot - domPres); if (!decide) { // between decisions the strain coasts on its last one: // forces already sit in fxX/fxY, actions in the buffer. const syn = this.syn[s]; const kinShare = dom >= 0 ? clamp01((blkBody[dom][b] - own) / Math.max(1e-6, syn.threshold)) : 0; synScratch[s * FC + b] = syn.base + (1 - syn.base) * kinShare; continue; } const nb = (blkPop[s][ug] + blkPop[s][dg] + blkPop[s][lg] + blkPop[s][rg]) / 4; // ---- senses ------------------------------------------------- ss[S_FOOD] = clamp01(r0 * 0.12); ss[S_SELF] = clamp01(own * 0.008); ss[S_KIN] = clamp01(kinPres * 0.008); ss[S_FOE] = clamp01(foe * 0.008); ss[S_TRAIL] = dom >= 0 ? clamp01(blkTrail[dom][b] * 0.05) : 0; ss[S_LAW] = clamp01(lawSpawn / 2.2); ss[S_HUNGER] = 1 - ss[S_FOOD]; ss[S_EDGE] = clamp01(Math.sign(own - nb) * 0.5 + 0.5); ss[S_TIME] = progT; ss[S_TOTAL] = clamp01(tot * 0.005); ss[S_WELL] = clamp01(wellField[b]); ss[S_PEACE] = peace; // The work-senses look where the work points: the block the // strain aimed at last substep (its own feet when it stood). const aimIdx = s * fc + b; const axn = this.aimX[aimIdx]; const ayn = this.aimY[aimIdx]; const wb = (axn || ayn) ? Math.min(FC - 1, Math.max(0, (b % FC) + axn)) + FC * Math.min(FC - 1, Math.max(0, ((b / FC) | 0) + ayn)) : b; ss[S_ROCK] = clamp01(blkRock[wb] * 0.45); ss[S_GARDEN] = clamp01(blkGarden[wb] * 0.5); // ---- gradients (sign per field × axis) --------------------- gg[G_FOOD * 2] = Math.sign(gRx); gg[G_FOOD * 2 + 1] = Math.sign(gRy); gg[G_SELF * 2] = Math.sign(this.gradOf(blkPop[s], b, ug, dg, lg, rg)); gg[G_SELF * 2 + 1] = this.gradY(blkPop[s], b, ug, dg); gg[G_KIN * 2] = 0; gg[G_KIN * 2 + 1] = 0; gg[G_FOE * 2] = 0; gg[G_FOE * 2 + 1] = 0; if (dom >= 0) { gg[G_KIN * 2] = Math.sign(blkBody[dom][rg] - blkBody[dom][lg]); gg[G_KIN * 2 + 1] = Math.sign(blkBody[dom][dg] - blkBody[dom][ug]); // foe gradient: the other bodies' presence, summed. let fx = 0; let fy = 0; for (const c of this.aliveBodies) { if (c === dom) continue; fx += blkBody[c][rg] - blkBody[c][lg]; fy += blkBody[c][dg] - blkBody[c][ug]; } gg[G_FOE * 2] = Math.sign(fx); gg[G_FOE * 2 + 1] = Math.sign(fy); } gg[G_WELL * 2] = Math.sign(wellField[rg] - wellField[lg]); gg[G_WELL * 2 + 1] = Math.sign(wellField[dg] - wellField[ug]); gg[G_ROCK * 2] = Math.sign(this.rockField[rg] - this.rockField[lg]); gg[G_ROCK * 2 + 1] = Math.sign(this.rockField[dg] - this.rockField[ug]); gg[G_GARDEN * 2] = Math.sign(blkGarden[rg] - blkGarden[lg]); gg[G_GARDEN * 2 + 1] = Math.sign(blkGarden[dg] - blkGarden[ug]); const flat = ensureFlat(strains[s]); runProgram(this.stMem, s * REGS * FC + b, ss, gg, act, flat._flatI, flat._flatF, flat._flatN); // Remember the aim: the next substep's work-senses look there. let nx = act[12] !== 0 ? Math.sign(act[12]) : Math.sign(act[0]); let ny = act[13] !== 0 ? Math.sign(act[13]) : Math.sign(act[1]); this.aimX[aimIdx] = nx; this.aimY[aimIdx] = ny; // ---- actions → forces + reaction mods ---------------------- fxX[s][b] = act[0] + this.ancX[s][b] * 0.12 + gRx * 0.02 - dgx * pressure; fxY[s][b] = act[1] + this.ancY[s][b] * 0.12 + gRy * 0.02 - dgy * pressure; const off = s * ACTION_COUNT * actFC + b; actBuf[off + 2 * actFC] = act[2]; // EXCRETE actBuf[off + 3 * actFC] = act[3]; // EAT actBuf[off + 4 * actFC] = act[4]; // HIT actBuf[off + 5 * actFC] = act[5]; // SPAWN actBuf[off + 6 * actFC] = act[6]; // DIG actBuf[off + 7 * actFC] = act[7]; // BUILD actBuf[off + 8 * actFC] = act[8]; // TILL actBuf[off + 9 * actFC] = act[9]; // TEND actBuf[off + 12 * actFC] = act[12]; // AIM_X actBuf[off + 13 * actFC] = act[13]; // AIM_Y // Synergy: this strain breeds fastest in a mixed, same-body // crowd — the share of the block's population that is kin. const syn = this.syn[s]; const kinShare = dom >= 0 ? clamp01((blkBody[dom][b] - own) / Math.max(1e-6, syn.threshold)) : 0; synScratch[s * FC + b] = syn.base + (1 - syn.base) * kinShare; } } } gradOf(arr, b, ug, dg, lg, rg) { return Math.sign(arr[rg] - arr[lg]); } gradY(arr, b, ug, dg) { return Math.sign(arr[dg] - arr[ug]); } /** * One simulation substep: * 1. block sums (food, strains, body presence, dominant bodies); * 2. the strain programs decide the forces; * 3. advection + diffusion on the fine field (strains by their own * force; allegiance frozen-in with the population flux); * 4. reaction: eat → grow (with synergy) → war (casualties + * conversion) → deposit into the body's trail. * Deterministic throughout: the only randomness is the static noise field. */ substep(regime) { this.substepCount++; const F = this.field; const FC = this.FC; const fc = FC * FC; const sub = 1 / (this.fps * this.substeps); const ops = regime.operators; const prog = this.program; const strains = prog.strains; const m = this.m; const diff = prog.memory.diffusion; const jitterAmt = prog.memory.jitter * (0.4 + 1.35 * ops.turbulence); const capMul = 0.7 + 0.6 * ops.split; const pop = this.pop; const next = this.popNext; const alleg = this.alleg; const allegNext = this.allegNext; const trail = this.trail; const res = this.resource; const density = this.density; const nUp = this.nUp; const nDown = this.nDown; const nLeft = this.nLeft; const nRight = this.nRight; const coarseOf = this.coarseOf; const noise = this.noise; const noise2 = this.noise2; const n = this.n; const rock = this.rock; // ---- 1. block sums ---------------------------------------------------- const blkR = this.blkR; const blkPop = this.blkPop; const blkTrail = this.blkTrail; const blkBody = this.blkBody; const blkTotal = this.blkTotal; const blkRock = this.blkRock; const blkGarden = this.blkGarden; blkR.fill(0); blkTotal.fill(0); blkRock.fill(0); blkGarden.fill(0); for (let s = 0; s < m; s++) blkPop[s].fill(0); for (let b = 0; b < this.B; b++) { blkTrail[b].fill(0); blkBody[b].fill(0); } for (let i = 0; i < n; i++) { const b = coarseOf[i]; const d = density[i]; blkR[b] += res[i] + this.flora[i] * 0.5; blkRock[b] += rock[i]; blkGarden[b] += this.garden[i]; if (d > 1e-6) { blkTotal[b] += d; for (let s = 0; s < m; s++) blkPop[s][b] += pop[s][i]; for (let c = 0; c < this.B; c++) { const a = alleg[c][i]; if (a > 1e-6) { blkBody[c][b] += a; blkTrail[c][b] += trail[c][i]; } } } } // Dominant + second body per block (canonical: ties → lowest id). const domB = this.domB; const subB = this.subB; for (let b = 0; b < fc; b++) { let d0 = -1; let d1 = -1; let v0 = 0; let v1 = 0; for (const c of this.aliveBodies) { const v = blkBody[c][b]; if (v > v0) { v1 = v0; d1 = d0; v0 = v; d0 = c; } else if (v > v1) { v1 = v; d1 = c; } } domB[b] = d0; subB[b] = d1; } // ---- 2. the strain programs decide the forces -------------------------- this.runStrains(regime); // The per-block war pressure: hits land at their AIM target — where // the striker is heading, i.e. where the foe actually stands — and // siege saps the stone there. War has a front line now. const actBuf = this.stAct; const actFC = FC * FC; const hostility = this.hostility; hostility.fill(0); const hitAt = this.hitAt; hitAt.fill(0); const aimTarget = (b, s) => { const base = s * ACTION_COUNT * actFC + b; let ax = actBuf[base + 12 * actFC]; let ay = actBuf[base + 13 * actFC]; if (ax === 0) ax = actBuf[base + 0 * actFC]; // default: where we walk if (ay === 0) ay = actBuf[base + 1 * actFC]; const sx = Math.sign(ax); const sy = Math.sign(ay); const bx = b % FC; const by = (b / FC) | 0; const nx = Math.min(FC - 1, Math.max(0, bx + sx)); const ny = Math.min(FC - 1, Math.max(0, by + sy)); return ny * FC + nx; }; for (let b = 0; b < fc; b++) { for (let s = 0; s < m; s++) { const hits = actBuf[s * ACTION_COUNT * actFC + 4 * actFC + b]; if (hits > 1e-9) hitAt[aimTarget(b, s)] += hits; } } for (let b = 0; b < fc; b++) { const dom = domB[b]; const other = subB[b]; if (dom < 0 || other < 0) continue; const dip = this.diplomacy(dom, other); if (dip >= 0) continue; const foeShare = clamp01(blkBody[other][b] / Math.max(1e-6, blkTotal[b])); hostility[b] = Math.min(1, hitAt[b] * 8) * (-dip) * foeShare; } // ---- 3. advection + diffusion ------------------------------------------ // Allegiance rides the population flux: the density-weighted mean of // the strains' velocities. const velX = this.velX; const velY = this.velY; for (let b = 0; b < fc; b++) { let sx = 0; let sy = 0; let w = 0; for (let s = 0; s < m; s++) { const p = blkPop[s][b]; sx += this.fxX[s][b] * p; sy += this.fxY[s][b] * p; w += p; } velX[b] = w > 1e-6 ? sx / w : 0; velY[b] = w > 1e-6 ? sy / w : 0; } const advFrac = Math.min(0.4, this.avgSpeed * ops.speed * sub * 240); for (let s = 0; s < m; s++) { this.advect(pop[s], next[s], this.fxX[s], this.fxY[s], advFrac, jitterAmt, diff, sub, true); } const carry = this.carry; const allegDiff = diff; // allegiance is frozen-in: it travels exactly // with the population's own spreading for (let c = 0; c < this.B; c++) { this.advect(alleg[c], allegNext[c], velX, velY, advFrac, 0, allegDiff, sub, false); } // The quarried burden rides the same flux — this is the hauling. for (let s = 0; s < m; s++) { this.advect(carry[s], this.carryNext[s], velX, velY, advFrac, 0, diff * 0.15, sub, false); } // ---- 4. react ----------------------------------------------------------- const societyCeil = prog.society.ceiling; const globalCap = prog.society.global; const synScratch = this.synergy; const vals = this.valScratch; const grown = this.growScratch; // The dish's carrying capacity: as the whole world approaches it, // breeding eases off everywhere (soft, so it never bites hard). const globalDamp = Math.max(0, 1 - Math.max(0, this.totalPop - globalCap) / globalCap); for (let i = 0; i < n; i++) { const r0 = res[i]; const b = coarseOf[i]; const dom = domB[b]; if (r0 < 1e-6 && density[i] < 1e-6) continue; let total = 0; for (let s = 0; s < m; s++) vals[s] = next[s][i]; // Saturated food factor (rational approximation, no transcendentals). const foodFactor = (r0 * 5) / (1 + r0 * 5); this.hungerMass += density[i] * (1 - foodFactor); // The gate is nearly shut on bare ground: food is the substrate, not a // catalyst. A colony that outgrows its ground starves back and must // wander — the world tracks its wells instead of congealing. const gate = 0.03 + 0.97 * foodFactor; // War: the block's contest bleeds the losing body's share here. const host = hostility[b]; let casualty = 0; if (host > 1e-9 && dom >= 0) { const other = subB[b] >= 0 ? subB[b] : dom; const loseShare = dom !== other ? clamp01(allegNext[other][i] / Math.max(1e-6, density[i])) : 0; casualty = Math.min(0.35, host * 2.5 * sub * loseShare); } let demand = 0; for (let s = 0; s < m; s++) demand += vals[s] * this.eatR[s] * actBuf[s * ACTION_COUNT * actFC + 3 * actFC + b]; demand *= sub; // The store runs out → the crowd mows the wild flora (half a // stand at a time; it regrows if left alone). const fromRes = Math.min(r0, demand); // Grazing can only take a whisker of the stand per bite — a // crowd mows a meadow over seconds, never in an instant. const bite = Math.min(this.flora[i] * 0.004, demand - fromRes); this.flora[i] -= bite; res[i] = demand > 0 ? Math.max(0, r0 - fromRes) + bite * 0.55 : r0; const starve = (1 - foodFactor) * (0.28 + 0.9 * ops.spawn) * sub; const society = Math.max(0, 1 - density[i] / societyCeil); for (let s = 0; s < m; s++) { const p = vals[s]; const spn = actBuf[s * ACTION_COUNT * actFC + 5 * actFC + b]; const synB = synScratch[s * FC + b]; let g = p * this.growth[s] * ops.growth * globalDamp * (1 + spn) * synB * sub * (1 - p / (this.caps[s] * capMul)) * gate; g *= (1 - casualty * 8); // Starvation is the body's metabolic floor: bare ground kills, // slowly but surely — a colony that sits still on exhausted // ground dissolves, and the survivors must wander. This is // what keeps the world working instead of congealing. (The // eat action drives depletion above; it is not the price of // eating.) g = Math.max(0, p + g * society) - p * starve * this.eatR[s]; g = Math.max(0, g); if (casualty > 0) g = Math.max(0, g - g * casualty * 6); // war kills; it does not convert grown[s] = g; total += g; } // Deposit into the DOMINANT body's trail: each cell's organisms // write the geography of the body they belong to. if (total > 1e-9 && dom >= 0) { const tB = trail[dom]; for (let s = 0; s < m; s++) { const exc = actBuf[s * ACTION_COUNT * actFC + 2 * actFC + b]; tB[i] += grown[s] * this.depositR[s] * ops.deposit * (1 + exc) * sub * DEPOSIT_SCALE; } } let dNew = 0; for (let s = 0; s < m; s++) { const before = pop[s][i]; const after = grown[s]; next[s][i] = after; dNew += after; if (after > before) this.birthsThisFrame += after - before; else if (after < before) this.deathsThisFrame += before - after; } density[i] = dNew; // The composition grows with the cell: newborn organisms inherit // the standing mix; an allegiance-less cell joins the dominant // body wholesale. if (dNew > 1e-6) { let S = 0; for (let c = 0; c < this.B; c++) S += allegNext[c][i]; if (S > 1e-4) { const k = dNew / S; for (let c = 0; c < this.B; c++) allegNext[c][i] *= k; } else if (dom >= 0) { for (let c = 0; c < this.B; c++) allegNext[c][i] = 0; allegNext[dom][i] = dNew; } } } // ---- swap --------------------------------------------------------------- let totalPop = 0; for (let s = 0; s < m; s++) { const t = pop[s]; pop[s] = next[s]; next[s] = t; const p = pop[s]; for (let i = 0; i < n; i++) totalPop += p[i]; } for (let c = 0; c < this.B; c++) { const t = alleg[c]; alleg[c] = allegNext[c]; allegNext[c] = t; } for (let s = 0; s < m; s++) { const t = carry[s]; carry[s] = this.carryNext[s]; this.carryNext[s] = t; } this.totalPop = totalPop; // ---- rock economy: quarries and walls ----------------------------------- // Digging takes effort (a slow bite out of the stone) and yields // soil; the burden rides in the block's carry until the program // builds with it. This is how the world moves its own ground around. const rockPol = prog.rock; const mask = this.rockMask; const bs = F / FC; const cells = bs * bs; for (let b = 0; b < fc; b++) { for (let s = 0; s < m; s++) { const base = s * ACTION_COUNT * actFC + b; const dig = actBuf[base + 6 * actFC]; const build = actBuf[base + 7 * actFC]; const till = actBuf[base + 8 * actFC]; const tend = actBuf[base + 9 * actFC]; if (dig <= 1e-6 && build <= 1e-6 && till <= 1e-6 && tend <= 1e-6) continue; // The work lands AHEAD: at the block the aim points at. const tb = aimTarget(b, s); const bx = (tb % FC) * bs; const by = ((tb / FC) | 0) * bs; if (till > 1e-6) { // Work the stone into beds — one cell brought to // maturity before the next is started (a scattered // dusting of fertility would exhaust as fast as it is // laid and mint stone from nothing). The garden's own // production is the payoff; work yields no food. let want = till * rockPol.till * 3.0 * sub; for (let y = by; y < by + bs; y++) { for (let x = bx; x < bx + bs; x++) { if (want <= 1e-9) break; const i = y * F + x; if (this.garden[i] >= 0.2) continue; // already a bed const room = Math.min(rock[i], 0.6 - this.garden[i]); if (room <= 1e-6) continue; const t = Math.min(room, want); rock[i] -= t; this.garden[i] += t; want -= t; } } } if (dig > 1e-6) { let want = dig * rockPol.dig * 2.0 * sub; for (let y = by; y < by + bs; y++) { for (let x = bx; x < bx + bs; x++) { if (want <= 1e-9) break; const i = y * F + x; const t = Math.min(rock[i], want / cells); if (t <= 0) continue; rock[i] -= t; want -= t; carry[s][i] += t * 0.9; // shouldered burden } } } if (tend > 1e-6) { // Tended beds yield far more: the husbandman's visit // raises the garden's vigour for a while. for (let y = by; y < by + bs; y++) { for (let x = bx; x < bx + bs; x++) { const i = y * F + x; if (this.garden[i] < 0.02) continue; this.tend[i] = Math.min(1, this.tend[i] + tend * sub * 2.5); } } } if (build > 1e-6) { // The block's burden is pooled and mortared along a // rounded footprint: walls rise organically. const kernel = this.buildKernel[tb]; let avail = 0; let room = 0; for (let k = 0; k < kernel.idx.length; k++) { const i = kernel.idx[k]; avail += carry[s][i]; room += kernel.w[k] * Math.max(0, 2.4 - rock[i]); } if (avail > 1e-9 && room > 1e-9) { const deposit = Math.min(avail, build * rockPol.build * 4.0 * sub, room); const takeK = deposit / avail; for (let k = 0; k < kernel.idx.length; k++) { const i = kernel.idx[k]; carry[s][i] *= 1 - takeK; // each cell receives its weighted share of the // room it still has const roomK = kernel.w[k] * Math.max(0, 2.4 - rock[i]); if (roomK > 0) rock[i] += deposit * roomK / room; } } } } } } /** * Conservative L1 advection + diffusion of one field, shared by the * strains (which track activity) and the allegiance composition. `frac` * is the fraction of a cell that departs this substep. */ advect(src, dst, fxArr, fyArr, frac, jitterAmt, diff, sub, track) { const F = this.field; const nUp = this.nUp; const nDown = this.nDown; const nLeft = this.nLeft; const nRight = this.nRight; const coarseOf = this.coarseOf; const noise = this.noise; const noise2 = this.noise2; const mask = this.rockMask; const n = this.n; dst.fill(0); for (let i = 0; i < n; i++) { const v = src[i]; if (v <= 1e-6) { continue; } const b = coarseOf[i]; const vx = fxArr[b] + (track ? noise[i] * jitterAmt : 0); const vy = fyArr[b] + (track ? noise2[i] * jitterAmt : 0); const w = Math.abs(vx) + Math.abs(vy); const leave = v * frac; if (leave > 0 && w > 1e-6) { const mx = (leave * Math.abs(vx)) / w; const my = (leave * Math.abs(vy)) / w; dst[i] += v - leave; // Rock is a wall: mass aimed at a stone cell stays put and // piles against it. const tx = vx > 0.02 ? nRight[i] : vx < -0.02 ? nLeft[i] : i; const ty = vy > 0.02 ? nDown[i] : vy < -0.02 ? nUp[i] : i; dst[mask[tx] ? i : tx] += mx; dst[mask[ty] ? i : ty] += my; if (track) { const ax = ((i % F) * 32 / F) | 0; const ay = ((i / F) * 32 / F) | 0; this.activity[Math.min(31, ay) * 32 + Math.min(31, ax)] += leave * 6; this.moveAccum += leave; } } else { dst[i] += v; } // Diffusion through the OPEN neighbours only: rock reflects the // haze instead of drinking it (still conservative overall). let open = 0; let nb = 0; if (!mask[nUp[i]]) { nb += src[nUp[i]]; open++; } if (!mask[nDown[i]]) { nb += src[nDown[i]]; open++; } if (!mask[nLeft[i]]) { nb += src[nLeft[i]]; open++; } if (!mask[nRight[i]]) { nb += src[nRight[i]]; open++; } if (open > 0) { const d = (nb - open * v) * diff * sub; dst[i] += d * 0.5; const share = d * 0.5 / open; if (!mask[nUp[i]]) dst[nUp[i]] -= share; if (!mask[nDown[i]]) dst[nDown[i]] -= share; if (!mask[nLeft[i]]) dst[nLeft[i]] -= share; if (!mask[nRight[i]]) dst[nRight[i]] -= share; } } } // ---- the politics of allegiance (frame level) ---------------------------- /** * Allegiance changes here — never inside a substep: * defection a body whose presence collapsed releases its members to * the strongest neighbour, cell by cell; * fission a coherent, mixed, cut-off splinter hit by a strong onset * founds a new body (a schism: rival to its parent); * merger two intermixed bodies under a cooperative regime unify. */ politics(regime, silent) { const n = this.n; const F = this.field; const density = this.density; const alleg = this.alleg; const f = this.frame; void silent; // ---- the composition tracks the population: scale each cell's // allegiance to its actual population, claim frontier cells for the // nearest body, and fade the claims of abandoned ground. for (let i = 0; i < n; i++) { const target = density[i]; let S = 0; for (const b of this.aliveBodies) { if (alleg[b][i] < 0) alleg[b][i] = 0; // advection overshoot S += alleg[b][i]; } if (target > 1e-6) { if (S > 1e-4) { const k = Math.min(1e4, target / S); for (const b of this.aliveBodies) { alleg[b][i] = Math.min(target, alleg[b][i] * k); } } else { const x = (i % F) / F; const y = ((i / F) | 0) / F; let best = this.aliveBodies[0]; let bd = Infinity; for (const b of this.aliveBodies) { const d = Math.hypot(this.bodies[b].cx - x, this.bodies[b].cy - y); if (d < bd - 1e-9) { bd = d; best = b; } } if (best !== undefined) { for (let c = 0; c < this.B; c++) alleg[c][i] = 0; alleg[best][i] = target; } } } else if (S > 1e-9) { // the population left: the claim fades for (const b of this.aliveBodies) alleg[b][i] *= 0.82; } } // Presence per body + weighted centroid. for (const b of this.aliveBodies) { const body = this.bodies[b]; body.presence = 0; } const presX = new Float64Array(this.B); const presY = new Float64Array(this.B); for (let i = 0; i < n; i++) { for (const b of this.aliveBodies) { const a = alleg[b][i]; if (a > 1e-6) { this.bodies[b].presence += a; presX[b] += (i % F) * a; presY[b] += ((i / F) | 0) * a; } } } for (const b of this.aliveBodies) { const body = this.bodies[b]; body.cx = body.presence > 1e-6 ? presX[b] / body.presence / F : body.cx; body.cy = body.presence > 1e-6 ? presY[b] / body.presence / F : body.cy; body.peak = Math.max(body.peak, body.presence); } // ---- colonisation: a settlement far from every body core, blessed // by a strong onset, founds a NEW body. Rare, and it never flips an // existing body's ground — it plants a new flag on the frontier. if (f > 0 && f % COLONIZE_EVERY === 0 && this.aliveBodies.length < this.B) { const FC = this.FC; const fc = FC * FC; let best = -1; let bestScore = 0; for (let b = 0; b < fc; b++) { const tot = this.blkTotal[b]; if (tot < 10) continue; const wx = ((b % FC) + 0.5) / FC; const wy = (((b / FC) | 0) + 0.5) / FC; // Far from EVERY living body: virgin ground, not a border. let nearest = Infinity; for (const c of this.aliveBodies) { nearest = Math.min(nearest, Math.hypot(this.bodies[c].cx - wx, this.bodies[c].cy - wy)); } if (nearest < 0.22) continue; const score = tot * Math.min(1, nearest / 0.35); if (score > bestScore) { bestScore = score; best = b; } } // A founding is blessed by a strong onset nearby, recent. if (best >= 0 && bestScore > 8.0) { const FCs = this.FC; const wx = ((best % FCs) + 0.5) / FCs; const wy = (((best / FCs) | 0) + 0.5) / FCs; let blessed = false; for (const p of this.pulses) { if (p.strength > 0.7 && Math.hypot(p.x - wx, p.y - wy) < 0.14) { blessed = true; break; } } if (blessed) this.foundBody(best, f); } } } /** Found a new body from the splinter at coarse block `block`. */ foundBody(block, frame) { const slot = this.bodies.findIndex((b) => !b.alive); if (slot < 0) return -1; const F = this.field; const FC = this.FC; const fc = FC * FC; const body = this.bodies[slot]; const usedColors = new Set(this.aliveBodies.map((b) => this.bodies[b].color)); let color = 0; while (usedColors.has(color) && color < this.B) color++; body.alive = true; body.founded = frame; body.parent = -1; body.color = Math.min(color, this.B - 1); body.cx = ((block % FC) + 0.5) / FC; body.cy = (((block / FC) | 0) + 0.5) / FC; body.peak = 0; body.presence = 0; this.aliveBodies.push(slot); // A new polity has no friends yet: wary neutrality toward everyone. const alleg = this.alleg; const bs = F / FC; const bx = (block % FC) * bs; const by = ((block / FC) | 0) * bs; for (let y = by - 2; y < by + bs + 2; y++) { for (let x = bx - 2; x < bx + bs + 2; x++) { if (x < 0 || y < 0 || x >= F || y >= F) continue; const i = y * F + x; let S = 0; for (let c = 0; c < this.B; c++) S += alleg[c][i]; if (S < 1e-6) continue; // The settlement secedes: 65% of the ground's population // takes the new flag. for (let c = 0; c < this.B; c++) alleg[c][i] *= 0.35; alleg[slot][i] += 0.65 * S; } } for (const c of this.aliveBodies) { if (c === slot) continue; this.bodyAff[slot][c] = -0.15; this.bodyAff[c][slot] = -0.15; } return slot; } // ---- decay & record ------------------------------------------------------ decay(regime, living) { const dt = this.dt; const memory = this.program.memory; const ops = regime.operators; const fieldK = Math.pow(0.5, dt / (memory.fieldHalfLife * (0.8 + 0.4 * ops.spawn))); const trailK = Math.pow(0.5, dt / (memory.trailHalfLife * (0.75 + 0.35 * ops.deposit))); const res = this.resource; for (let i = 0; i < res.length; i++) res[i] *= fieldK; for (let c = 0; c < this.B; c++) { const t = this.trail[c]; for (let i = 0; i < t.length; i++) t[i] *= trailK; } const act = this.activity; const actK = Math.pow(0.5, dt / 1.4); for (let i = 0; i < act.length; i++) act[i] *= actK; // Erosion: the section's erode operator sets how fast the stone // weathers away. Breakdowns clear the ground; drops raise it. Even // walls weather — unmaintained works crumble, so the masons are // never done. const rock = this.rock; if (this.dbgRock) { let before = 0; for (let i = 0; i < rock.length; i++) before += rock[i]; this.dbgBefore = before; } const rockHalf = 80 / (0.3 + 0.7 * ops.erode); const rockK = Math.pow(0.5, dt / rockHalf); for (let i = 0; i < rock.length; i++) rock[i] *= rockK; // The flora is the world's pioneer, rooted in ISLANDS: it can only // ever live on its own seed ground. Its vigour tracks the famine — // a starving or dwindling world blooms with it (the seed-rain is // the lifeline); a strong, well-fed world grazes it back to a // whisper. The map keeps its green archipelago either way. const famine = this.famine; const popWeak = clamp01(1 - this.totalPop / Math.max(1, this.peakPop * 0.7)); const bloom = clamp01(famine * 0.6 + popWeak * 0.6); const flora = this.flora; const mask = this.rockMask; const seeds = this.floraSeed; for (let i = 0; i < flora.length; i++) { if (mask[i] || !seeds[i]) { flora[i] = 0; continue; } const f = flora[i]; flora[i] = Math.min(0.22, f + (0.008 * bloom + f * (1 - f / 0.22)) * (0.05 + 0.9 * bloom) * dt); res[i] = Math.min(1.6, res[i] + f * 0.02 * dt); } // The gardens feed the world: cultivated ground yields a few units // over its life, and spent ground petrifies back to rock. const garden = this.garden; const tendF = this.tend; const tendK = Math.pow(0.5, dt / 4); for (let i = 0; i < garden.length; i++) { tendF[i] *= tendK; const g = garden[i]; if (g < 0.01) continue; const make = g * 0.045 * (1 + 2 * tendF[i]) * dt; res[i] = Math.min(1.6, res[i] + make * 1.2); garden[i] = g - make; if (garden[i] < 0.03) { // The bed is spent: the worked soil sets back into stone — // almost all of it. Agriculture slowly eats the world's rock. const back = garden[i] * 0.85 + 0.15; rock[i] += back; garden[i] = 0; if (this.gardenSrc) this.gardenSrc[i] += back; } } if (this.dbgRock) { let after = 0; for (let i = 0; i < rock.length; i++) after += rock[i]; let g2r = 0; for (let i = 0; i < this.garden.length; i++) if (this.gardenSrc[i] > 0) g2r += this.gardenSrc[i]; console.log('rockΔ this frame:', (after - this.dbgBefore).toFixed(1), '(garden-petrify portion:', g2r.toFixed(1), ')'); this.gardenSrc.fill(0); } // Wells recharge to a ceiling — but not through stone: rock capped // ground stays barren until someone quarries it open. const scale = (0.22 + 0.7 * regime.energy) * dt; for (const { well, env } of living) { const add = well.rate * scale * env / well.peak; const idx = well.kernel.idx; const w = well.kernel.w; for (let i = 0; i < idx.length; i++) { const cell = idx[i]; if (mask[cell]) continue; res[cell] = Math.min(1.6, res[cell] + add * w[i]); } } } record(f) { // The famine signal: mean hunger of the living, smoothed; and the // population's high-water mark (slowly forgotten, so a crash can // ask the pioneers for help again). const meanHunger = this.totalPop > 1 ? Math.min(1, this.hungerMass / this.totalPop) : 1; this.famine += (meanHunger - this.famine) * 0.03; this.peakPop = Math.max(this.totalPop, this.peakPop * 0.9995); if (f < 0 || f >= this.history.alive.length) return; const h = this.history; h.alive[f] = this.totalPop; h.movement[f] = this.moveAccum; let food = 0; const res = this.resource; for (let i = 0; i < res.length; i += 3) food += res[i]; h.food[f] = (food * 3) / res.length; h.births[f] = this.birthsThisFrame; h.deaths[f] = this.deathsThisFrame; h.bodies[f] = this.aliveBodies.length; } /** The camera's view of where something just happened. */ activityCell(x, y) { const ax = Math.max(0, Math.min(31, (x * 32) | 0)); const ay = Math.max(0, Math.min(31, (y * 32) | 0)); return this.activity[ay * 32 + ax]; } stats() { const h = this.history; let peak = 0; for (let i = 0; i < h.alive.length; i++) peak = Math.max(peak, h.alive[i]); return { alive: Math.round(this.totalPop), peak, fieldMass: this.sampleFieldMass(), trailMass: this.sampleTrailMass(), strains: this.strains.map((s) => s.name), bodies: this.aliveBodies.length, name: this.genome.name, }; } sampleFieldMass() { let s = 0; const res = this.resource; for (let i = 0; i < res.length; i++) s += res[i]; return s / res.length; } sampleTrailMass() { let s = 0; for (let c = 0; c < this.trail.length; c++) { const t = this.trail[c]; for (let i = 0; i < t.length; i++) s += t[i]; } return s / (this.trail[0].length * this.trail.length); } }