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.
This commit is contained in:
Dejvino
2026-10-24 08:23:33 +02:00
parent 6a2af2e45f
commit 13b0939221
26 changed files with 4469 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
// 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}`);
});