// The rule program (M2) must be: deterministic to compile, structurally // different across songs, and genuinely EXECUTED by the world (editing an // operator changes behaviour, not just numbers). 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 { regimeFor } from '../src/song/rules.js'; import { Run } from '../src/film.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`))); const analysis = analyse(mono(wav.channels), wav.sampleRate); return buildSong(analysis, seed); } test('the program compiles deterministically', () => { const a = load('drone', 12345); const b = load('drone', 12345); assert.deepEqual(JSON.parse(JSON.stringify(a.program)), JSON.parse(JSON.stringify(b.program))); }); test('different songs get structurally different programs', () => { const drone = load('drone').program; const runner = load('runner').program; const dusk = load('dusk').program; // Castes differ (speed/seek/cap/growth from the track's stats). const dr = JSON.stringify(drone.castes); assert.notDeepEqual(JSON.parse(dr), JSON.parse(JSON.stringify(runner.castes))); // Interaction rules differ (band-correlation graph). assert.notEqual(JSON.stringify(drone.rules), JSON.stringify(dusk.rules)); // At least one operator family differs materially in the same section kind. const dropOf = (p) => p.sections.find((s) => s.kind === 'drop')?.operators; const drDrop = dropOf(drone); const ruDrop = dropOf(runner); const hasDrop = drDrop && ruDrop; const differs = Object.keys(drDrop ?? {}).some((k) => Math.abs(drDrop[k] - ruDrop[k]) > 0.05); assert.ok(!hasDrop || differs, 'drop regimes should differ across songs'); }); test('regimes are structurally distinct by kind', () => { const p = load('dusk').program; // rich: build → breakdown → drop → outro const byKind = (k) => p.sections.find((s) => s.kind === k)?.operators ?? p.sections[0].operators; // breakdown: swarming off, inscription on. assert.ok(byKind('breakdown').disperse < 0.3, 'breakdown should suppress swarming'); assert.ok(byKind('breakdown').deposit > 1.4, 'breakdown should turn on inscription'); assert.ok(byKind('breakdown').turbulence < 0.4, 'breakdown should calm'); // drop: scatter on, home off — and clearly opposite to the breakdown. assert.ok(byKind('drop').turbulence > byKind('breakdown').turbulence * 5 + 0.5, 'drop should be turbulent'); assert.ok(byKind('drop').home < byKind('breakdown').home * 0.5, 'drop should abandon home'); assert.ok(byKind('drop').spawn > byKind('breakdown').spawn * 4, 'drop should open the floodgates'); }); test('the world actually executes the program (a growth=0 edit halts breeding)', () => { const song = load('runner', 12345); const run = new Run(song, { fps: 30, seed: 12345, width: 240, height: 135, field: 64 }); for (let f = 0; f < 900; f++) run.world.step(); // Same song, but every regime loses its growth AND spawn operators: // the world must stop breeding (events may still seed a little, but no // logistic growth → population collapses far below the live run). const sterile = load('runner', 12345); for (const s of sterile.program.sections) { s.operators.growth = 0; s.operators.spawn = 0; } const run2 = new Run(sterile, { fps: 30, seed: 12345, width: 240, height: 135, field: 64 }); for (let f = 0; f < 900; f++) run2.world.step(); assert.ok(run.world.count > 500, `live world should be inhabited (${run.world.count})`); assert.ok(run2.world.count < run.world.count * 0.1, `sterile world should collapse (${run2.world.count} vs ${run.world.count})`); }); test('regimeFor picks the right operators at a boundary', () => { const p = load('drone').program; const mid = regimeFor(p, Math.round(p.sections[0].start * 30) + 30, 30); assert.equal(mid.kind, p.sections[0].kind); const later = regimeFor(p, Math.round(p.sections[1].start * 30) + 30, 30); assert.equal(later.kind, p.sections[1].kind); });