Files
music-video-gen/metabolism/test/world.test.js
T
Dejvino 13b0939221 Metabolism: song as substrate, ecology grown from the track
A new top-level section building the metabolism concept: the track
supplies the food field, event impulses and per-section laws; the seed
picks the species (a rule set, not a parameter vector). Same song + seed
=> bit-identical frames; different songs => different worlds.

- analysis: single FFT pass -> per-frame feature table; score (onsets
  folded into world geometry via voice ring + pitch-class sector), wells
  (food placed by the harmony), graph (band correlations -> interaction
  matrix), laws (per-section regimes)
- sim: typed-array world, 3 castes (builder/orbit, forager/commute,
  spark/orbit-wide) with causality rings, persistence trails,
  consequence field; fully deterministic
- lens: software framebuffer renderer (PNG in node, ImageData in
  browser) + event-reactive camera (cuts on activity change, alternating
  shot sizes, drift-and-push)
- metrics: coupling x residual fitness (not per-frame descriptors) plus
  a grown-structure fingerprint; report + strip tools; 15 tests green
- DESIGN.md captures the concept, invariants and build order

3 reference test signals (drone/runner/dusk) grow measurably distinct
worlds while seeds of one song stay in the same biome.
2026-10-24 08:23:33 +02:00

81 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The world must be deterministic, bounded, and fundamentally unstable under
// seed change — while two DIFFERENT songs must grow clearly different worlds.
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 { World } from '../src/sim/world.js';
import { Run } from '../src/film.js';
import { fingerprint, divergence } from '../src/metrics.js';
const HERE = dirname(fileURLToPath(import.meta.url));
const BANK = resolve(HERE, '../..', 'flow-state/test/songs');
function load(name) {
const wav = decodeWav(readFileSync(resolve(BANK, `${name}.wav`)));
return buildSong(analyse(mono(wav.channels), wav.sampleRate), 12345);
}
/** Run `seconds` of the world for song `name` with a seed. Returns world. */
function grow(name, seed, seconds = 20) {
const song = load(name);
const run = new Run(song, { fps: 30, seed, width: 240, height: 135 });
for (let f = 0; f < Math.floor(seconds * 30); f++) {
run.world.step();
run.camera.update(f);
}
return { world: run.world, song };
}
test('same seed reproduces identical history', () => {
const { world: a } = grow('drone', 12345);
const { world: b } = grow('drone', 12345);
assert.deepEqual(Array.from(a.history.alive), Array.from(b.history.alive));
assert.deepEqual(Array.from(a.history.movement), Array.from(b.history.movement));
assert.deepEqual(Array.from(a.trail[0]), Array.from(b.trail[0]));
assert.equal(a.frame, b.frame);
});
test('different seeds give different ecologies', () => {
const { world: a } = grow('drone', 12345);
const { world: b } = grow('drone', 99999);
const moved = a.history.movement.some((v, i) => Math.abs(v - b.history.movement[i]) > 1e-3);
assert.ok(moved, 'ecologies should diverge under a different seed');
});
test('population stays within bounds and alive', () => {
const { world: w } = grow('runner', 12345, 30);
for (const v of w.history.alive) {
assert.ok(v <= w.cap + 40, `population exceeded cap: ${v}`);
}
assert.ok(w.history.alive.some((v) => v > 0), 'the dish should stay inhabited');
});
test('field and trail never go negative', () => {
const { world: w } = grow('drone', 7, 25);
for (const v of w.resource) assert.ok(v >= -1e-9, `negative resource ${v}`);
for (let c = 0; c < 3; c++) {
for (const v of w.trail[c]) assert.ok(v >= -1e-9, `negative trail ${v}`);
}
});
test('trail mass reflects a busy vs sparse world', () => {
const sparse = grow('drone', 12345, 25);
const dense = grow('runner', 12345, 25);
assert.ok(dense.world.count > sparse.world.count * 1.5);
});
test('three songs grow fingerprints further apart than seeds of one song', () => {
const a = fingerprint(grow('drone', 12345).world);
const b = fingerprint(grow('runner', 12345).world);
const c = fingerprint(grow('dusk', 12345).world);
const cross = Math.max(divergence(a, b), divergence(a, c), divergence(b, c));
const a2 = fingerprint(grow('drone', 99).world);
const within = divergence(a, a2);
assert.ok(cross > within * 2, `cross ${cross} !> 2×within ${within}`);
});