// The strains (EVOLUTION.md M2.7) — the song decides how many kinds of minds // its world needs, then writes them. // // There are no castes and no tribes. The population is ONE species whose // density is partitioned by *strain* — a personality, a LittleVM program the // song compiled for it. A biofilm, not three societies: specialists working // the same ground, and the mix itself evolves (the world runs local selection // on the strain proportions). // // The VM is small and total: senses → registers → branches → actions, run // per coarse decision block, every substep. Senses now include the body the // local organisms belong to (KIN: same body, other strains; FOE: other // bodies), the living wells (proximity + recharge), and the section's PEACE — // so a program can literally branch on "war or truce". // // Everything compiles deterministically from (analysis summary, genome, seed // stream): same song, same seed → same programs, bit for bit. export const S_FOOD = 0; export const S_SELF = 1; // own strain, this block export const S_KIN = 2; // same body, other strains export const S_FOE = 3; // other bodies' presence export const S_TRAIL = 4; // own body's trail here export const S_LAW = 5; // section's spawn operator export const S_HUNGER = 6; export const S_EDGE = 7; export const S_TIME = 8; export const S_TOTAL = 9; export const S_WELL = 10; // nearest living well's pull export const S_PEACE = 11; // section's peace operator (war ↔ truce) export const S_ROCK = 12; // rock under this block: the quarriable ground export const S_GARDEN = 13; // cultivated ground here: is this stone worked yet? export const SENSE_COUNT = 14; // Gradient fields, two slots each (x, y): food, own strain, kin, foe, well. export const G_FOOD = 0; export const G_SELF = 1; export const G_KIN = 2; export const G_FOE = 3; export const G_WELL = 4; export const G_ROCK = 5; export const G_GARDEN = 6; export const G_FIELDS = 7; export const GRAD_COUNT = G_FIELDS * 2; export const A_MOVE_X = 0; export const A_MOVE_Y = 1; export const A_EXCRETE = 2; export const A_EAT = 3; export const A_HIT = 4; export const A_SPAWN = 5; export const A_DRIFT = 6; // gradual direction change following a gradient export const A_ACCELERATE = 7; // speed up / slow down export const A_DIG = 8; // quarry rock here (rock → carried → soil) export const A_BUILD = 9; // deposit carried rock as a wall export const A_TILL = 10; // work stone into a garden (a food generator) export const A_TEND = 11; // tend a garden: it yields far more while tended export const A_AIM_X = 12; // aim the work: act ahead along x (set, not add) export const A_AIM_Y = 13; // aim the work: act ahead along y export const ACTION_COUNT = 14; export const REGS = 6; export const MAX_INSTR = 56; export const MAX_STEPS = 64; export const OP = { SET: 0, ADD: 1, MUL: 2, SUB: 3, CLAMP: 4, LERP: 5, SENSE: 6, GRAD: 7, IFGT: 8, IFLT: 9, IFEQ: 10, IFNEQ: 11, ACT: 12, HALT: 13, }; const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v); const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); // ---- a tiny assembler ------------------------------------------------------- // // Programs are built from motifs; jumps need patching, so the assembler hands // out patch slots and labels. No control flow beyond the VM's own. class Asm { constructor() { this.code = []; } label() { return this.code.length; } emit(...ins) { this.code.push(ins); return this.code.length - 1; } // Conditional jumps return a patch slot; call .jump(patch, target) later. ifgt(reg, thr) { return this.emit(OP.IFGT, reg, thr, 0); } iflt(reg, thr) { return this.emit(OP.IFLT, reg, thr, 0); } jump(patch, target) { this.code[patch][3] = target; return this; } sense(reg, s, w = 1) { return this.emit(OP.SENSE, reg, s, w); } grad(reg, field, axis, w = 1) { return this.emit(OP.GRAD, reg, field, axis, w); } act(a, reg, w = 1) { return this.emit(OP.ACT, a, reg, w); } mul(reg, k) { return this.emit(OP.MUL, reg, k); } halt() { return this.emit(OP.HALT); } } // ---- roles ------------------------------------------------------------------ // // Each role is a motif family plus the economy the world needs to run it. // The song picks which roles exist (m of them) and tunes every threshold; // the numbers below are bases, not destiny. const ROLES = { builder: { base: { speed: 0.038, deposit: 1.50, cap: 1.25, growth: 0.90, eat: 0.032 }, hungryAt: 0.70, spawnW: 0.30, }, grazer: { base: { speed: 0.095, deposit: 0.75, cap: 1.00, growth: 1.00, eat: 0.042 }, hungryAt: 0.60, spawnW: 0.45, }, scout: { base: { speed: 0.155, deposit: 0.18, cap: 0.80, growth: 1.05, eat: 0.046 }, hungryAt: 0.75, spawnW: 0.60, }, sentinel: { base: { speed: 0.070, deposit: 0.55, cap: 1.00, growth: 0.85, eat: 0.040 }, hungryAt: 0.70, spawnW: 0.25, foeAt: 0.10, }, breeder: { base: { speed: 0.062, deposit: 0.50, cap: 0.90, growth: 1.25, eat: 0.036 }, hungryAt: 0.70, spawnW: 0.85, kinAt: 0.12, }, farmer: { base: { speed: 0.055, deposit: 0.40, cap: 0.95, growth: 1.05, eat: 0.038 }, hungryAt: 0.65, spawnW: 0.45, gardenAt: 0.15, }, }; /** How many minds the song needs: independent voices, texture, seed. */ function strainCount(summary, genome, rng) { const density = clamp01(summary.onsetDensity / 6); const noise = clamp01(summary.flatness); // A band that lives its own life (weak correlation to the others) is an // independent voice — it earns its own specialist. let independent = 0; const corr = summary.bandCorrelation || [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; for (let i = 0; i < corr.length; i++) { let maxOff = -2; for (let j = 0; j < corr.length; j++) if (j !== i) maxOff = Math.max(maxOff, corr[i][j] ?? 0); if (maxOff < 0.6) independent++; } let m = 2 + (density > 0.35 ? 1 : 0) // a busy arrangement needs hands + (independent >= 1 ? 1 : 0) // a self-willed voice needs a mind + (noise > 0.5 ? 1 : 0); // dense texture: more specialists if (rng.next() < 0.35) m += 1; // the seed may add one more voice return clamp(m, 2, 5); } /** * Compile the population's strains for a song. * @returns {Array<{name, role, code, init, speed, deposit, cap, growth, eat}>} */ export function compileStrains(summary, genome, rng) { const m = strainCount(summary, genome, rng); const density = clamp01(summary.onsetDensity / 6); const pace = genome.pace; const cohesion = genome.cohesion; const balance = summary.bandBalance || [0.33, 0.33, 0.33]; const noise = clamp01(summary.flatness); // The role order is the song's; the seed rotates the casting so two // similar songs still differ in who they specialised. const order = Object.keys(ROLES); const rot = rng.int(0, order.length - 1); const roles = order.slice(rot).concat(order.slice(0, rot)).slice(0, m); return roles.map((role, i) => { const spec = ROLES[role]; const band = balance[i % 3]; const asm = new Asm(); // R0 hunger, R1 law(spawn), R2 well pull, R3 peace, R4 kin, R5 foe. asm.sense(0, S_HUNGER, 1); asm.sense(1, S_LAW, 1); asm.sense(2, S_WELL, 1); asm.sense(3, S_PEACE, 1); // ---- the sentinel's war branch: a body at war fights before it // eats. PEACE decides strike vs shadow; the branch sits at the end, // so the fall-through below is the ordinary life. let foePatch = -1; if (spec.foeAt !== undefined) { asm.sense(5, S_FOE, 1); foePatch = asm.ifgt(5, spec.foeAt * (0.8 + 0.4 * rng.next())); } // ---- the breeder's bloom branch: breed only in a fed crowd. let kinPatch = -1; if (spec.kinAt !== undefined) { asm.sense(4, S_KIN, 1); kinPatch = asm.ifgt(4, spec.kinAt * (0.8 + 0.4 * rng.next())); } // ---- hungry: follow the smell of food — and WORK on the way, // because for a mason or a husbandman work IS foraging: the aimed // ground pays wages in food. const hungryPatch = asm.iflt(0, spec.hungryAt); asm.grad(4, G_FOOD, 0, 1); asm.grad(5, G_FOOD, 1, 1); asm.act(A_MOVE_X, 4, 1); asm.act(A_MOVE_Y, 5, 1); asm.act(A_AIM_X, 4, 1); asm.act(A_AIM_Y, 5, 1); asm.act(A_EAT, 0, 1); if (role === 'builder' || role === 'farmer') { // The husbandman tends first (a tended bed pays best), the // mason breaks stone first; both eat their wages. if (role === 'farmer') { asm.sense(5, S_GARDEN, 1); const bedH = asm.ifgt(5, 0.05); asm.sense(5, S_ROCK, 1); const seamH = asm.ifgt(5, 0.06); asm.halt(); const tendH = asm.label(); asm.jump(bedH, tendH); asm.act(A_TEND, 5, 1); asm.act(A_EAT, 0, 1); asm.halt(); const tillH = asm.label(); asm.jump(seamH, tillH); asm.act(A_TILL, 5, 1.2); asm.act(A_EAT, 0, 1); asm.halt(); } else { asm.sense(5, S_ROCK, 1); const seamH = asm.ifgt(5, 0.06); asm.sense(5, S_GARDEN, 1); const spentH = asm.ifgt(5, 0.15); asm.halt(); const tillH = asm.label(); asm.jump(seamH, tillH); asm.act(A_TILL, 5, 1.2); asm.act(A_EAT, 0, 1); asm.halt(); const digH = asm.label(); asm.jump(spentH, digH); asm.act(A_DIG, 5, 1.2); asm.act(A_EAT, 0, 1); asm.halt(); } } else { asm.halt(); } // ---- fed: the role's own work. const fed = asm.label(); asm.jump(hungryPatch, fed); if (role === 'builder') { // The mason: it smells stone and walks to it; its WORK lands // AHEAD of it, along its aim — it carves the face of the wall, // not the ground beneath its feet. On fresh stone ahead it // tills (a garden: food for a while, then stone again); on // spent ground it quarries (burden for the walls); on clear // ground it builds what the burden brought. asm.sense(4, S_ROCK, 1); const quarry = asm.ifgt(4, 0.06); // Clear ground: toward the stone. asm.grad(4, G_ROCK, 0, 1); asm.grad(5, G_ROCK, 1, 1); asm.act(A_MOVE_X, 4, 0.6); asm.act(A_MOVE_Y, 5, 0.6); asm.act(A_AIM_X, 4, 1); asm.act(A_AIM_Y, 5, 1); asm.sense(5, S_FOOD, 1); asm.act(A_BUILD, 5, 1.5); asm.act(A_EXCRETE, 3, 1.0); asm.act(A_SPAWN, 1, spec.spawnW); asm.halt(); const onRock = asm.label(); asm.jump(quarry, onRock); // Aim INTO the stone face ahead. asm.grad(4, G_ROCK, 0, 1); asm.grad(5, G_ROCK, 1, 1); asm.act(A_AIM_X, 4, 1); asm.act(A_AIM_Y, 5, 1); // Fresh stone becomes a garden: worked, it will feed the body // for a while — and then petrify back to rock. asm.sense(5, S_GARDEN, 1); const spent = asm.ifgt(5, 0.15); asm.act(A_TILL, 4, 1.6); asm.halt(); const dig = asm.label(); asm.jump(spent, dig); // Worked-out ground: quarry what is left for the walls. asm.act(A_DIG, 4, 1.6); asm.act(A_EXCRETE, 3, 0.3); asm.halt(); } else if (role === 'grazer') { // Commutes: follows the food edge, leaves a road behind. asm.grad(4, G_FOOD, 0, 0.7); asm.grad(5, G_FOOD, 1, 0.7); asm.act(A_MOVE_X, 4, 0.9); asm.act(A_MOVE_Y, 5, 0.9); asm.act(A_EXCRETE, 3, 0.6); asm.act(A_SPAWN, 1, spec.spawnW); asm.halt(); } else if (role === 'scout') { // Restless: re-aims every step, speeds up, writes almost nothing. asm.grad(4, G_FOOD, 0, 0.5); asm.act(A_DRIFT, 4, 1.4); asm.act(A_ACCELERATE, 3, 1); asm.act(A_SPAWN, 1, spec.spawnW * (0.6 + 0.8 * noise)); asm.halt(); } else if (role === 'sentinel') { // Patrol: sweeps along the body's frontier, never parked. asm.grad(4, G_KIN, 0, 0.8); asm.grad(5, G_KIN, 1, 0.8); asm.act(A_MOVE_X, 4, 0.6); asm.act(A_MOVE_Y, 5, 0.6); asm.grad(4, G_SELF, 0, 0.5); asm.act(A_DRIFT, 4, 1); asm.act(A_EXCRETE, 3, 0.5); asm.act(A_SPAWN, 1, spec.spawnW); asm.halt(); } else if (role === 'farmer') { // The husbandman: walks the field row, its work ahead of it — // tending the bed it is walking toward (a tended bed yields // far more), opening new beds into fresh stone. asm.sense(4, S_GARDEN, 1); const field = asm.ifgt(4, spec.gardenAt * (0.8 + 0.4 * rng.next())); asm.sense(5, S_ROCK, 1); const seam = asm.ifgt(5, 0.06); // roam toward the fields, aiming where we walk asm.grad(4, G_GARDEN, 0, 1); asm.grad(5, G_GARDEN, 1, 1); asm.act(A_MOVE_X, 4, 0.8); asm.act(A_MOVE_Y, 5, 0.8); asm.act(A_AIM_X, 4, 1); asm.act(A_AIM_Y, 5, 1); asm.act(A_TEND, 4, 1); asm.act(A_SPAWN, 1, spec.spawnW * 0.5); asm.halt(); const tillNew = asm.label(); asm.jump(seam, tillNew); asm.grad(4, G_ROCK, 0, 1); asm.grad(5, G_ROCK, 1, 1); asm.act(A_AIM_X, 4, 1); asm.act(A_AIM_Y, 5, 1); asm.act(A_TILL, 5, 1); asm.halt(); const tend = asm.label(); asm.jump(field, tend); asm.grad(4, G_GARDEN, 0, 1); asm.grad(5, G_GARDEN, 1, 1); asm.act(A_AIM_X, 4, 1); asm.act(A_AIM_Y, 5, 1); asm.act(A_TEND, 4, 1); asm.act(A_EXCRETE, 3, 0.4); asm.act(A_SPAWN, 1, spec.spawnW); asm.halt(); } else { // breeder // Follows the crowd it feeds on; spawns when the law allows. asm.grad(4, G_KIN, 0, 1); asm.grad(5, G_KIN, 1, 1); asm.act(A_MOVE_X, 4, 0.7); asm.act(A_MOVE_Y, 5, 0.7); asm.act(A_SPAWN, 1, spec.spawnW * (0.8 + 0.6 * density)); asm.act(A_EXCRETE, 3, 0.4); asm.halt(); } // ---- the breeder's bloom tail (reached only from the kin branch). if (kinPatch >= 0) { const bloom = asm.label(); asm.jump(kinPatch, bloom); asm.act(A_SPAWN, 1, spec.spawnW * (0.7 + 0.9 * density)); asm.act(A_EXCRETE, 3, 0.4); asm.halt(); } // ---- the sentinel's war tail (reached only from the foe branch): // peace low → close and strike; peace high → shadow the border. if (foePatch >= 0) { const war = asm.label(); asm.jump(foePatch, war); const strike = asm.iflt(3, 0.45); // truce: fall back from the border, keep writing. asm.grad(4, G_FOE, 0, -0.8); asm.grad(5, G_FOE, 1, -0.8); asm.act(A_MOVE_X, 4, 0.5); asm.act(A_MOVE_Y, 5, 0.5); asm.act(A_EXCRETE, 3, 0.5); asm.halt(); const strikeLabel = asm.label(); asm.jump(strike, strikeLabel); asm.grad(4, G_FOE, 0, 1); asm.grad(5, G_FOE, 1, 1); asm.act(A_MOVE_X, 4, 1); asm.act(A_MOVE_Y, 5, 1); asm.act(A_HIT, 5, 1.2); asm.halt(); } // Economy: the track tunes the role's body. const bandBoost = 0.7 + 0.6 * band; return { name: role, role, code: asm.code.slice(0, MAX_INSTR), init: [0, 0, 0, 0, 0, 0], speed: spec.base.speed * (0.35 + 1.10 * pace) * (role === 'scout' ? 1 : bandBoost), deposit: spec.base.deposit * (0.35 + 1.25 * cohesion), cap: 10 * spec.base.cap * (1.20 - 0.30 * density), growth: ((0.92 / (1 + 0.10 * density)) + 0.24) * spec.base.growth, eat: spec.base.eat * (0.85 + 0.4 * genome.aggression), }; }); } // ---- the VM ----------------------------------------------------------------- function clampVM(v) { return v < 0 ? 0 : v > 1 ? 1 : v; } /** * Run one strain program for one block. Registers live in `memBuf` at `off` * (persistent across substeps — that is the strain's memory); `senses` and * `grads` are this block's read of the world; `act` accumulates the decision. */ export function runProgram(memBuf, off, senses, grads, act, code) { let pc = 0; let steps = 0; const n = code.length; act[0] = 0; act[1] = 0; act[2] = 0; act[3] = 1; act[4] = 0; act[5] = 0; act[6] = 0; act[7] = 0; act[8] = 0; act[9] = 0; act[12] = 0; act[13] = 0; const mem = memBuf; while (steps++ < MAX_STEPS && pc < n) { const ins = code[pc]; const op = ins[0]; switch (op) { case OP.SET: mem[off + ins[1]] = ins[2]; break; case OP.ADD: mem[off + ins[1]] += mem[off + ins[2]]; break; case OP.MUL: mem[off + ins[1]] *= ins[2]; break; case OP.SUB: mem[off + ins[1]] -= mem[off + ins[2]]; break; case OP.CLAMP: mem[off + ins[1]] = clampVM(mem[off + ins[1]]); break; case OP.LERP: mem[off + ins[1]] += (mem[off + ins[2]] - mem[off + ins[1]]) * ins[3]; break; case OP.SENSE: mem[off + ins[1]] = senses[ins[2]] * ins[3]; break; case OP.GRAD: mem[off + ins[1]] = grads[ins[2] * 2 + ins[3]] * (ins[4] ?? 1); break; case OP.IFGT: if (mem[off + ins[1]] > ins[2]) { pc = ins[3]; continue; } break; case OP.IFLT: if (mem[off + ins[1]] < ins[2]) { pc = ins[3]; continue; } break; case OP.IFEQ: if (mem[off + ins[1]] == ins[2]) { pc = ins[3]; continue; } break; case OP.IFNEQ: if (mem[off + ins[1]] != ins[2]) { pc = ins[3]; continue; } break; case OP.ACT: { const drive = mem[off + ins[2]]; const w = ins[3] ?? 1; switch (ins[1]) { case A_MOVE_X: act[0] += drive * w; break; case A_MOVE_Y: act[1] += drive * w; break; case A_EXCRETE: act[2] += Math.max(0, drive) * w * 1.5; break; case A_EAT: act[3] *= drive * w + 1; break; case A_HIT: act[4] += Math.max(0, drive) * w * 0.02; break; case A_SPAWN: act[5] += Math.max(0, drive) * w * 0.012; break; case A_DRIFT: act[0] += drive * w * 0.1; act[1] += drive * w * 0.1; break; case A_ACCELERATE: act[3] *= drive * w + 1; break; case A_DIG: act[6] += Math.max(0, drive) * w; break; case A_BUILD: act[7] += Math.max(0, drive) * w; break; case A_TILL: act[8] += Math.max(0, drive) * w; break; case A_TEND: act[9] += Math.max(0, drive) * w; break; case A_AIM_X: act[12] = drive; break; // aim is SET, not summed case A_AIM_Y: act[13] = drive; break; default: break; } break; } case OP.HALT: default: return act; } pc++; } return act; }