Files

265 lines
10 KiB
JavaScript

// M2.7 — one species, many minds: strains & allegiances, living wells.
// The tests here pin the new story layer: the allegiance invariant, the
// bodies' politics (war converts, splinters fission, friends merge), the
// strain mix evolving by local selection, and the wells' lifecycle.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve, dirname } from 'node:path';
import { analyse } from '../src/audio/analyse.js';
import { decodeWav, mono } from '../src/audio/wav.js';
import { buildSong } from '../src/song/song.js';
import { wellPos } from '../src/sim/wells.js';
import { Run } from '../src/film.js';
import { OP, S_FOE, G_FOE, A_MOVE_X, A_MOVE_Y, A_HIT } from '../src/song/strains.js';
const HERE = dirname(fileURLToPath(import.meta.url));
const BANK = resolve(HERE, '../..', 'flow-state/test/songs');
function load(name, seed = 12345) {
const wav = decodeWav(readFileSync(resolve(BANK, `${name}.wav`)));
return buildSong(analyse(mono(wav.channels), wav.sampleRate), seed);
}
function run(name, opts = {}) {
const song = load(name, opts.seed ?? 12345);
const r = new Run(song, {
fps: 30, width: 240, height: 135, field: 64, genesis: 0,
...(opts.run ?? {}),
});
return r;
}
// ---- living wells -----------------------------------------------------------
test('well timeline compiles deterministically', () => {
const a = load('drone').wells;
const b = load('drone').wells;
assert.equal(a.wells.length, b.wells.length);
for (let i = 0; i < a.wells.length; i++) {
assert.equal(a.wells[i].born, b.wells[i].born);
assert.equal(a.wells[i].died, b.wells[i].died);
assert.deepEqual(Array.from(a.wells[i].px), Array.from(b.wells[i].px));
}
});
test('the world wakes on living wells and never exceeds the cap', () => {
const song = load('drone');
const w = run('drone').world;
assert.ok(w.livingWells(0).length >= 3, 'at least three wells alive at frame 0');
for (let f = 0; f < 600; f++) {
const alive = w.livingWells(f).length;
assert.ok(alive <= song.wells.maxActive, `too many wells at f${f}: ${alive}`);
if (f < 600) w.step();
}
});
test('wells drift: at least one well moves a visible distance', () => {
const w = run('drone').world;
let maxDrift = 0;
for (const well of w.wells) {
const life = well.died - well.born;
if (life < 300) continue;
const [x0, y0] = wellPos(well, well.born);
const [x1, y1] = wellPos(well, well.born + Math.floor(life / 2));
maxDrift = Math.max(maxDrift, Math.hypot(x1 - x0, y1 - y0));
}
assert.ok(maxDrift > 0.02, `wells should drift (max ${maxDrift.toFixed(4)})`);
});
test('a well birth is a land-rush: food lands where the well sprouts', () => {
const w = run('runner').world;
const soon = w.wells.find((well) => well.born > 10 && well.born < 450);
assert.ok(soon, 'runner should grow a well within 15 s');
// A control world where the same well is born one frame later: the only
// difference at the birth frame is the land-rush itself.
const cw = run('runner').world;
const twin = cw.wells.find((well) => well.id === soon.id);
twin.born += 1;
const [x, y] = wellPos(soon, soon.born);
const near = (world) => {
let s = 0;
const F = world.field;
const cx = Math.round(x * (F - 1));
const cy = Math.round(y * (F - 1));
for (let dy = -3; dy <= 3; dy++) {
for (let dx = -3; dx <= 3; dx++) {
const px = Math.min(F - 1, Math.max(0, cx + dx));
const py = Math.min(F - 1, Math.max(0, cy + dy));
s += world.resource[py * F + px];
}
}
return s;
};
for (let f = w.frame + 1; f < soon.born; f++) { w.step(); cw.step(); }
const pre = near(w);
w.step();
cw.step();
const post = near(w);
const controlPost = near(cw);
assert.ok(post > pre, `a well birth must add ground (${pre.toFixed(3)} → ${post.toFixed(3)})`);
assert.ok(post > controlPost, `the birth feeds more than the same frame without one (${post.toFixed(3)} vs ${controlPost.toFixed(3)})`);
});
// ---- allegiance: the invariant ----------------------------------------------
test('allegiance tracks the population exactly (Σ bodies = density)', () => {
const w = run('dusk').world;
for (let f = 0; f < 240; f++) w.step();
let worst = 0;
let negatives = 0;
for (let i = 0; i < w.n; i++) {
let s = 0;
for (let b = 0; b < w.B; b++) {
if (w.alleg[b][i] < -1e-9) negatives++;
s += w.alleg[b][i];
}
if (w.density[i] > 1e-4) worst = Math.max(worst, Math.abs(s - w.density[i]));
}
assert.equal(negatives, 0, 'no negative allegiance');
assert.ok(worst < 1e-2, `composition must track population (worst ${worst.toFixed(4)})`);
});
test('the song opens several bodies and the count stays sane', () => {
const song = load('dusk');
const w = run('dusk').world;
assert.ok(w.aliveBodies.length >= 2, 'the song opens more than one body');
const horizon = Math.min(600, song.analysis.frames - 1);
for (let f = 0; f < horizon; f++) {
w.step();
assert.ok(w.aliveBodies.length >= 1 && w.aliveBodies.length <= w.B);
assert.ok(w.history.bodies[w.frame] >= 1, 'history records the body count');
}
});
// ---- allegiance: war converts -------------------------------------------------
test('hostile bodies contest a front: war kills, but never converts', () => {
const make = (base) => {
const r = run('runner');
const w = r.world;
// War (or truce) footing: every strain marches toward the foe and
// strikes on contact.
r.song.program.diplomacy.base = base;
for (const s of w.strains) {
s.code = [
[OP.SENSE, 5, S_FOE, 1],
[OP.IFGT, 5, 0.02, 7],
[OP.GRAD, 4, G_FOE, 0, 1],
[OP.GRAD, 5, G_FOE, 1, 1],
[OP.ACT, A_MOVE_X, 4, 1],
[OP.ACT, A_MOVE_Y, 5, 1],
[OP.HALT],
[OP.GRAD, 4, G_FOE, 0, 1],
[OP.GRAD, 5, G_FOE, 1, 1],
[OP.ACT, A_MOVE_X, 4, 1],
[OP.ACT, A_MOVE_Y, 5, 1],
[OP.ACT, A_HIT, 5, 2],
[OP.HALT],
];
}
// Two bodies planted face to face across the meridian.
const F = w.field;
const seedSide = (xa, xb, body) => {
for (let y = 20; y < 44; y++) {
for (let x = xa; x < xb; x++) {
const i = y * F + x;
for (let s = 0; s < w.m; s++) w.pop[s][i] = 4;
w.alleg[body][i] = 4 * w.m;
}
}
};
seedSide(12, 30, 0);
seedSide(34, 52, 1);
w.bodies[0].cx = 14 / F; w.bodies[0].cy = 0.5; w.bodies[0].alive = true;
w.bodies[1].cx = 50 / F; w.bodies[1].cy = 0.5; w.bodies[1].alive = true;
if (!w.aliveBodies.includes(0)) w.aliveBodies.push(0);
if (!w.aliveBodies.includes(1)) w.aliveBodies.push(1);
w.recount();
return w;
};
const war = make(-1);
const peace = make(1);
for (let f = 0; f < 240; f++) { war.step(); peace.step(); }
// War kills: the contested world ends up with fewer organisms than the
// same world at truce.
assert.ok(war.count < peace.count * 0.98,
`war should cost lives (war ${war.count.toFixed(0)} vs peace ${peace.count.toFixed(0)})`);
// But allegiance never changes hands: each body's mass stays (to within
// organic growth) where it was planted — nothing joined the other side.
const F = war.field;
const heartland = (w, b, xa, xb) => {
let inside = 0;
let outside = 0;
for (let y = 0; y < F; y++) {
for (let x = 0; x < F; x++) {
const i = y * F + x;
if (x >= xa && x < xb) inside += w.alleg[b][i];
else outside += w.alleg[b][i];
}
}
return { inside, outside };
};
const h0 = heartland(war, 0, 0, 32); // body 0 planted left
const h1 = heartland(war, 1, 32, F); // body 1 planted right
assert.ok(h0.outside < h0.inside * 0.2, `body 0 must not take the east (${h0.outside.toFixed(1)} vs ${h0.inside.toFixed(1)})`);
assert.ok(h1.outside < h1.inside * 0.2, `body 1 must not take the west (${h1.outside.toFixed(1)} vs ${h1.inside.toFixed(1)})`);
});
// ---- allegiance: war & colonisation ---------------------------------------------
test('colonisation: a settlement on virgin ground can plant a new flag', () => {
const w = run('dusk').world;
const F = w.field;
// A colony with allegiance to body 0 far from any other body's core.
for (let y = 18; y < 26; y++) {
for (let x = 18; x < 26; x++) {
const i = y * F + x;
for (let s = 0; s < w.m; s++) w.pop[s][i] = 6;
w.alleg[0][i] = 6 * w.m;
}
}
w.bodies[0].alive = true;
w.bodies[0].cx = 0.34; w.bodies[0].cy = 0.34;
if (!w.aliveBodies.includes(0)) w.aliveBodies.push(0);
w.recount();
const FC = w.FC;
const block = 10 * FC + 10;
const n0 = w.aliveBodies.length;
const founded = w.foundBody(block, w.frame + 1);
assert.ok(founded >= 0, 'a settlement can found a body');
assert.equal(w.aliveBodies.length, n0 + 1);
// The new body holds the seceded ground.
let newMass = 0;
for (let i = 0; i < w.n; i++) newMass += w.alleg[founded][i];
assert.ok(newMass > 10, `the new body took ground (${newMass.toFixed(1)})`);
});
// ---- the strain mix evolves ----------------------------------------------------
test('the strain mix evolves by local selection', () => {
const w = run('runner', { run: { field: 64 } }).world;
const share = () => w.pop.map((p) => {
let s = 0;
for (let i = 0; i < p.length; i++) s += p[i];
return s;
});
const early = share();
for (let f = 0; f < 600; f++) w.step();
const late = share();
const total0 = early.reduce((a, v) => a + v, 0);
const total1 = late.reduce((a, v) => a + v, 0);
let drift = 0;
for (let s = 0; s < w.m; s++) {
drift += Math.abs(early[s] / total0 - late[s] / total1);
}
assert.ok(drift > 0.02, `strain proportions must drift (L1 ${drift.toFixed(3)})`);
});