Files
music-video-gen/metabolism/test/rules.test.js
T

93 lines
4.4 KiB
JavaScript

// 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);
});