Files
music-video-gen/metabolism/src/sim/world.js
T

1827 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}
}