Metabolism: rule program, song-driven life, tribe VMs; M2.7 design (strains & allegiances, living wells)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user