Metabolism M2.8: rock — inert resource the song drops and builders quarry, carry and build with; livelier metabolisms

This commit is contained in:
Dejvino
2026-10-28 11:36:30 +01:00
parent 8abefb16b7
commit 1d497e556e
9 changed files with 464 additions and 30 deletions
+153
View File
@@ -0,0 +1,153 @@
// M2.8 — rock: the inert resource. It is scattered at the world's birth,
// dropped and shattered by the song, impassable to the population, and
// quarried by builders — carried, walled, ground into soil.
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 { OP, S_ROCK, A_DIG, A_BUILD } from '../src/song/strains.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 run(name, seed = 12345, field = 64) {
const song = load(name, seed);
const r = new Run(song, { fps: 30, seed, width: 240, height: 135, field, genesis: 0 });
return r;
}
const rockMass = (w) => {
let s = 0;
for (let i = 0; i < w.n; i++) s += w.rock[i];
return s;
};
const maskedCells = (w) => {
let s = 0;
for (let i = 0; i < w.n; i++) s += w.rockMask[i];
return s;
};
test('the world is born with stone in it (deterministically)', () => {
const a = run('drone').world;
const b = run('drone').world;
assert.ok(rockMass(a) > 5, `scatter should hold stone (${rockMass(a).toFixed(1)})`);
assert.ok(maskedCells(a) > 0, 'some ground is impassable from birth');
assert.deepEqual(Array.from(a.rock), Array.from(b.rock));
});
test('the song drops rock on heavy hits and shatters it on high ones', () => {
const live = run('runner').world;
const mute = run('runner').world;
mute.program.rock.drop = 0;
mute.program.rock.shatter = 0;
for (let f = 0; f < 600; f++) { live.step(); mute.step(); }
assert.ok(rockMass(live) > rockMass(mute) * 1.3,
`rockfall should outpace a mute sky (live ${rockMass(live).toFixed(1)} vs ${rockMass(mute).toFixed(1)})`);
});
test('rock is a wall: the population cannot cross it', () => {
const w = run('drone').world;
const F = w.field;
// A solid vertical wall of stone down the middle, north to south.
for (let y = 0; y < F; y++) {
for (let x = 30; x < 34; x++) w.rock[y * F + x] = 1.5;
}
w.refreshRockMask();
// A crowd planted on the west side, food on both sides so the only
// reason not to spread east is the wall.
for (let y = 8; y < 56; y++) {
for (let x = 4; x < 26; x++) {
const i = y * F + x;
for (let s = 0; s < w.m; s++) w.pop[s][i] = 2;
w.alleg[0][i] = 2 * w.m;
w.resource[i] = 1.2;
}
for (let x = 40; x < 60; x++) w.resource[y * F + x] = 1.2;
}
w.bodies[0].cx = 0.2; w.bodies[0].cy = 0.5; w.bodies[0].alive = true;
if (!w.aliveBodies.includes(0)) w.aliveBodies.push(0);
w.recount();
for (let f = 0; f < 300; f++) w.step();
let east = 0;
for (let y = 0; y < F; y++) {
for (let x = 34; x < F; x++) east += w.density[y * F + x];
}
assert.ok(east < w.count * 0.02,
`the wall must hold (${east.toFixed(1)} of ${w.count.toFixed(0)} leaked east)`);
});
test('builders quarry: rock goes down, soil and burden appear', () => {
const w = run('dusk').world;
const F = w.field;
// Every strain digs where rock lies.
for (const s of w.strains) {
s.code = [
[OP.SENSE, 4, S_ROCK, 1],
[OP.IFGT, 4, 0.05, 3],
[OP.HALT],
[OP.ACT, A_DIG, 4, 1],
[OP.HALT],
];
}
// A rich seam under the crowd.
for (let y = 20; y < 40; y++) {
for (let x = 20; x < 40; x++) {
const i = y * F + x;
w.rock[i] = 1.4;
for (let s = 0; s < w.m; s++) w.pop[s][i] = 3;
w.alleg[0][i] = 3 * w.m;
}
}
w.refreshRockMask();
w.bodies[0].alive = true; w.bodies[0].cx = 0.45; w.bodies[0].cy = 0.45;
if (!w.aliveBodies.includes(0)) w.aliveBodies.push(0);
w.recount();
const before = rockMass(w);
let foodBefore = 0;
for (let i = 0; i < w.n; i++) foodBefore += w.resource[i];
for (let f = 0; f < 120; f++) w.step();
const after = rockMass(w);
let foodAfter = 0;
for (let i = 0; i < w.n; i++) foodAfter += w.resource[i];
let carry = 0;
for (let s = 0; s < w.m; s++) for (let b = 0; b < w.fc; b++) carry += w.carry[s * w.fc + b];
assert.ok(after < before - 1, `quarries must open (${before.toFixed(1)} → ${after.toFixed(1)})`);
assert.ok(carry > 0.2, `the burden must be carried (${carry.toFixed(2)})`);
assert.ok(foodAfter > foodBefore, `grinding yields soil (${foodBefore.toFixed(1)} → ${foodAfter.toFixed(1)})`);
});
test('builders build: carried rock rises as walls', () => {
const w = run('dusk').world;
// Every strain builds.
for (const s of w.strains) {
s.code = [
[OP.SET, 4, 1],
[OP.ACT, A_BUILD, 4, 1],
[OP.HALT],
];
}
// A loaded block in the middle of open ground.
const F = w.field;
const b = w.coarseOf[30 * F + 30];
for (let s = 0; s < w.m; s++) w.carry[s * w.fc + b] = 5;
const before = rockMass(w);
for (let f = 0; f < 60; f++) w.step();
const after = rockMass(w);
assert.ok(after > before + 0.5,
`carried rock must rise (${before.toFixed(2)} → ${after.toFixed(2)})`);
});