Metabolism: rule program, song-driven life, tribe VMs; M2.7 design (strains & allegiances, living wells)

This commit is contained in:
Dejvino
2026-10-28 10:07:05 +01:00
parent 16f4b4a805
commit f939351689
28 changed files with 3199 additions and 601 deletions
+119
View File
@@ -0,0 +1,119 @@
// The tribe programs (M2.6): the song compiles small deterministic behaviour
// programs with sensing, memory, actions and branching, and the world executes
// them. These tests pin down determinism, per-song structural variety, the
// branching itself, and that the world truly executes the program.
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 {
compileTribes, runTribe, OP, S_HUNGER, S_FOOD, S_SELF, A_MOVE_X, A_MOVE_Y, A_EXCRETE, A_SPAWN,
} from '../src/song/tribe.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`)));
return buildSong(analyse(mono(wav.channels), wav.sampleRate), seed);
}
function snapshot(code) {
return JSON.stringify(code.map((ins) => ins.join(':')));
}
test('tribes compile deterministically and differ per song', () => {
const a = load('drone').tribes;
const b = load('drone').tribes;
assert.equal(a.length, b.length);
for (let t = 0; t < a.length; t++) assert.equal(snapshot(a[t].code), snapshot(b[t].code));
const runner = load('runner').tribes;
const anyDiffer = a.some((ta, i) => snapshot(ta.code) !== snapshot(runner[i].code));
assert.ok(anyDiffer, 'runner should compile structurally different tribe programs');
});
test('tribes have bounded programs with real branching', () => {
const tribes = load('drone').tribes;
assert.ok(tribes.every((t) => t.code.length > 3 && t.code.length <= 24), 'programs bounded and non-trivial');
const ops = new Set(tribes.flatMap((t) => t.code.map((ins) => ins[0])));
assert.ok(ops.has(OP.IFGT) || ops.has(OP.IFLT), 'programs branch');
assert.ok(ops.has(OP.SENSE) && ops.has(OP.ACT), 'programs sense and act');
});
test('the VM branches on senses: starving seeks food, fed excreats', () => {
// A tiny program: if hunger high → move along food gradient; else excrete.
const code = [
[OP.SENSE, 3, S_HUNGER, 1],
[OP.IFGT, 3, 0.5, 4], // hungry → jump to 4
[OP.ACT, A_EXCRETE, 2, 1], // (fed) R2=0 → excrete ~0
[OP.HALT],
[OP.GRAD, 0, S_FOOD, 0], // hungry: heading x = +1
[OP.GRAD, 1, S_FOOD, 1], // heading y = +1
[OP.ACT, A_MOVE_X, 0, 1],
[OP.ACT, A_MOVE_Y, 1, 1],
[OP.HALT],
];
const mem = new Float32Array(4);
const senses = new Float32Array(10);
const grads = new Float32Array(20);
const act = new Float32Array(6);
// Hungry (HUNGER high) → moves, no excrete.
senses[S_HUNGER] = 0.9;
grads[0] = 1; grads[1] = -1;
runTribe(mem, 0, senses, grads, act, code);
assert.ok(act[A_MOVE_X] > 0 && act[A_MOVE_Y] < 0, 'hungry tribe should move toward food');
// Fed (HUNGER low) → no movement, excrete from R2 (memory persists? R2=0 → excrete 0).
senses[S_HUNGER] = 0.1;
mem.fill(0);
grads[0] = 1; grads[1] = 1;
runTribe(mem, 0, senses, grads, act, code);
assert.equal(act[A_MOVE_X], 0, 'fed tribe should not chase food');
assert.equal(act[A_EXCRETE], 0);
});
test('memories (registers) persist across runs', () => {
const code = [
[OP.SENSE, 0, S_SELF, 1], // R0 = local density
[OP.LERP, 1, 0, 0.5], // R1 decays toward R0 (memory)
[OP.HALT],
];
const mem = new Float32Array(4);
const senses = new Float32Array(10);
const grads = new Float32Array(20);
const act = new Float32Array(6);
senses[S_SELF] = 0.8;
runTribe(mem, 0, senses, grads, act, code);
runTribe(mem, 0, senses, grads, act, code);
assert.ok(mem[1] > 0, 'register kept value across runs');
// R1 after two 50% steps toward 0.8 is exactly 0.6 (float tolerance).
assert.ok(Math.abs(mem[1] - 0.6) < 1e-6, `memory integrates deterministically (${mem[1]})`);
});
test('the world executes the tribe program (a no-op program stops its actions)', () => {
const song = load('runner');
const run = new Run(song, { fps: 30, seed: 12345, width: 240, height: 135, field: 64, genesis: 0 });
for (let f = 0; f < 900; f++) run.world.step();
// Same song, same world — but every tribe program halts immediately: no
// movement or actions from the program (forces become ambient-only). The
// grown world must be materially different.
const inert = load('runner');
for (const tribe of inert.tribes) tribe.code = [[OP.HALT]];
const run2 = new Run(inert, { fps: 30, seed: 12345, width: 240, height: 135, field: 64, genesis: 0 });
for (let f = 0; f < 900; f++) run2.world.step();
let d = 0;
const a = run.world.history.movement;
const b = run2.world.history.movement;
for (let i = 0; i < a.length; i++) d += Math.abs(a[i] - b[i]);
assert.ok(d > 1e-3, `inert programs must change the film (movement Δ=${d.toFixed(3)})`);
});