154 lines
5.5 KiB
JavaScript
154 lines
5.5 KiB
JavaScript
// 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)})`);
|
|
});
|