// The evaluation layer must be well-behaved: every world gets a finite, // clamped score, coupling and residual are separable, and the fitness // discriminates between songs (in the sense that the spread across songs is // clearly larger than the spread across seeds). 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 { Run } from '../src/film.js'; import { evaluate, fingerprint, divergence } from '../src/metrics.js'; const HERE = dirname(fileURLToPath(import.meta.url)); const BANK = resolve(HERE, '../..', 'flow-state/test/songs'); function grow(name, seed, seconds = 45) { const wav = decodeWav(readFileSync(resolve(BANK, `${name}.wav`))); const song = buildSong(analyse(mono(wav.channels), wav.sampleRate), seed); const run = new Run(song, { fps: 30, seed, width: 240, height: 135 }); run.simulate(); return { world: run.world, song }; } test('evaluation returns finite, clamped numbers', () => { for (const name of ['drone', 'runner', 'dusk']) { const { world, song } = grow(name, 12345); const m = evaluate(world, song); assert.ok(Number.isFinite(m.coupling), `${name} coupling`); assert.ok(Number.isFinite(m.residual), `${name} residual`); assert.ok(Number.isFinite(m.fitness), `${name} fitness`); assert.ok(m.coupling >= 0 && m.coupling <= 1); assert.ok(m.residual >= 0 && m.residual <= 1); } }); test('fitness is stable across seeds of one song', () => { const songs = ['drone', 'runner', 'dusk'].map((n) => grow(n, 12345).song); for (const song of songs) { const s1 = evaluate(growSong(song, 111).world, growSong(song, 111).song); const s2 = evaluate(growSong(song, 222).world, growSong(song, 222).song); assert.ok(Math.abs(s1.fitness - s2.fitness) < 0.05, `${song.name}: ${s1.fitness} vs ${s2.fitness}`); } }); test('fingerprint distances: songs farther than seeds', () => { const a = fingerprint(grow('drone', 12345).world); const b = fingerprint(grow('runner', 12345).world); const within = divergence(a, fingerprint(grow('drone', 42).world)); assert.ok(divergence(a, b) > within); }); function growSong(song, seed) { const run = new Run(song, { fps: 30, seed, width: 240, height: 135 }); run.simulate(); return { world: run.world, song }; }