// The world must be deterministic, bounded, and fundamentally unstable under // seed change — while two DIFFERENT songs must grow clearly different worlds. 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 { World } from '../src/sim/world.js'; import { Run } from '../src/film.js'; import { fingerprint, divergence } from '../src/metrics.js'; const HERE = dirname(fileURLToPath(import.meta.url)); const BANK = resolve(HERE, '../..', 'flow-state/test/songs'); function load(name) { const wav = decodeWav(readFileSync(resolve(BANK, `${name}.wav`))); return buildSong(analyse(mono(wav.channels), wav.sampleRate), 12345); } /** Run `seconds` of the world for song `name` with a seed. Returns world. */ function grow(name, seed, seconds = 20) { const song = load(name); const run = new Run(song, { fps: 30, seed, width: 240, height: 135, field: 64, genesis: 0 }); for (let f = 0; f < Math.floor(seconds * 30); f++) { run.world.step(); run.camera.update(f); } return { world: run.world, song }; } test('same seed reproduces identical history', () => { const { world: a } = grow('drone', 12345); const { world: b } = grow('drone', 12345); assert.deepEqual(Array.from(a.history.alive), Array.from(b.history.alive)); assert.deepEqual(Array.from(a.history.movement), Array.from(b.history.movement)); assert.deepEqual(Array.from(a.trail[0]), Array.from(b.trail[0])); assert.equal(a.frame, b.frame); }); test('different seeds give different ecologies', () => { const { world: a } = grow('drone', 12345); const { world: b } = grow('drone', 99999); const moved = a.history.movement.some((v, i) => Math.abs(v - b.history.movement[i]) > 1e-3); assert.ok(moved, 'ecologies should diverge under a different seed'); }); test('population stays within bounds and alive', () => { const { world: w } = grow('runner', 12345, 30); for (const v of w.history.alive) { assert.ok(v <= w.cap + 40, `population exceeded cap: ${v}`); } assert.ok(w.history.alive.some((v) => v > 0), 'the dish should stay inhabited'); }); test('field and trail never go negative', () => { const { world: w } = grow('drone', 7, 25); for (const v of w.resource) assert.ok(v >= -1e-9, `negative resource ${v}`); for (let c = 0; c < 3; c++) { for (const v of w.trail[c]) assert.ok(v >= -1e-9, `negative trail ${v}`); } }); test('trail mass reflects a busy vs sparse world', () => { const sparse = grow('drone', 12345, 25); const dense = grow('runner', 12345, 25); assert.ok(dense.world.count > sparse.world.count * 1.5); }); test('three songs grow fingerprints further apart than seeds of one song', () => { const a = fingerprint(grow('drone', 12345).world); const b = fingerprint(grow('runner', 12345).world); const c = fingerprint(grow('dusk', 12345).world); const cross = Math.max(divergence(a, b), divergence(a, c), divergence(b, c)); const a2 = fingerprint(grow('drone', 99).world); const within = divergence(a, a2); assert.ok(cross > within * 2, `cross ${cross} !> 2×within ${within}`); }); test('genesis pre-simulates generations so frame 0 is already an ecosystem', () => { const song = load('drone'); const withGenesis = new Run(song, { fps: 30, seed: 12345, width: 240, height: 135, field: 64 }); const without = new Run(song, { fps: 30, seed: 12345, width: 240, height: 135, field: 64, genesis: 0 }); assert.ok(withGenesis.world.genesisFrames > 0, 'song should demand a genesis epoch'); // The world at frame 0 is more evolved (generations already lived)… assert.ok(withGenesis.world.count > without.world.count * 1.1, `genesis world should be more populated (${withGenesis.world.count} vs ${without.world.count})`); // …and the ground already carries history, where a fresh plate has none. assert.ok(withGenesis.world.sampleTrailMass() > 0.00003, `genesis world should already hold ink (${withGenesis.world.sampleTrailMass()})`); assert.equal(withGenesis.world.history.alive[0], 0, 'genesis must not record into the film'); assert.equal(withGenesis.world.frame, -1, 'the film starts at frame 0'); }); test('the region pool is exact: parallel == serial, bit for bit', () => { const grow = (workers, { frames = 150, seed = 12345 } = {}) => { const song = load('drone'); const run = new Run(song, { fps: 30, seed, width: 240, height: 135, field: 64, genesis: 0, workers }); for (let f = 0; f < frames; f++) { run.world.step(); run.camera.update(f); } const history = Array.from(run.world.history.alive.slice(0, frames)); const trail = Array.from(run.world.trail[0].slice(0, 4000)); run.world.dispose?.(); return { history, trail }; }; const serial = grow(0); const two = grow(2); const four = grow(4); const eq = (a, b) => a.history.every((v, i) => v === b.history[i]) && a.trail.every((v, i) => v === b.trail[i]); assert.ok(eq(serial, two), '2 regions must match the serial world exactly'); assert.ok(eq(serial, four), '4 regions must match the serial world exactly'); // LONG: across a section boundary (drone's drop lands at frame 1545) and // with a seed distinct from song.seed (the workers must be told the same // seed — they seed their own jitter-noise fields). A 150-frame run inside // one section with the default seed passes even with both bugs present. const longSerial = grow(0, { frames: 1600, seed: 999 }); const longTwo = grow(2, { frames: 1600, seed: 999 }); const longEq = (a, b) => a.history.every((v, i) => v === b.history[i]) && a.trail.every((v, i) => v === b.trail[i]); assert.ok(longEq(longSerial, longTwo), '2 regions must match the serial world exactly across the section boundary'); }); test('genesis is deterministic and song-scaled', () => { const a = new Run(load('drone'), { fps: 30, seed: 12345, width: 240, height: 135, field: 64 }); const b = new Run(load('drone'), { fps: 30, seed: 12345, width: 240, height: 135, field: 64 }); assert.equal(a.world.genesisFrames, b.world.genesisFrames); assert.deepEqual(Array.from(a.world.pop[0]), Array.from(b.world.pop[0])); const runner = new Run(load('runner'), { fps: 30, seed: 12345, width: 240, height: 135, field: 64 }); assert.notEqual(a.world.genesisFrames, runner.world.genesisFrames, 'the genesis amount should depend on the song'); });