93 lines
4.4 KiB
JavaScript
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;
|
|
|
|
// Strains differ (speed/deposit/cap/growth from the track's stats).
|
|
const dr = JSON.stringify(drone.strains);
|
|
assert.notDeepEqual(JSON.parse(dr), JSON.parse(JSON.stringify(runner.strains)));
|
|
|
|
// Diplomacy differs (the tracks' own consonance).
|
|
assert.notEqual(JSON.stringify(drone.diplomacy), JSON.stringify(dusk.diplomacy));
|
|
|
|
// 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);
|
|
});
|