rainforest-life-test.js
83 lines · in natura-src-1.0.0.zip as natura/rainforest-life-test.js · a test
// rainforest-life-test.js -- the whole jungle SIM proven HEADLESS (no renderer): a Natura World of
// [canopy + organisms(rainforest species)] behaves correctly across the day. This is the render/core split made
// concrete -- everything the deployed jungle shows is decided here, with zero canvas.
const N = require('./natura.js');
const { makeOrganisms } = require('./organisms.js');
const { makeCanopy } = require('./canopy.js');
const { SPECIES } = require('./rainforest-life.js');
const { World, UNITS } = N;
const DAY = UNITS.DAY;
let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log(' FAIL: ' + m); } };
const mean = (arr, f) => arr.length ? arr.reduce((a, x) => a + f(x), 0) / arr.length : 0;
function world(seed) { return World({ seed: seed, systems: [makeCanopy({}), makeOrganisms({ name: 'life', species: SPECIES, structure: 'canopy' })] }); }
// hold the clock at day-phase dp and step a few times so the chronotype/behaviour settle at THAT time of day
// (a single step from a fresh init can catch a transient -- e.g. one morpho momentarily startled awake)
function at(w, dp, dt) { dt = dt || 0.05; for (let i = 0; i < 5; i++) { w.clock.t = dp * DAY; w.step(dt); } return w.fields().life; }
console.log('rainforest-life assertions:');
// 1. COMPOSITION: the biome stands up as a World of two systems
{ const w = world(11); const L = w.fields().life;
ok(L.count >= 52, 'the rainforest stands up its full roster (>=52; the exact composition is guarded below): ' + L.count);
ok(w.fields().canopy && typeof w.fields().canopy.sunAt === 'function', 'the canopy field is present for the life to read its local sun');
ok(L.of('flower').length === 12 && L.of('morpho').length === 8 && L.of('monkey').length === 4 && L.of('cat').length === 1 && L.of('fruit').length === 10, 'species groups populated (incl. the canopy food web)');
}
// 1b. CANOPY FOOD WEB (fruit -> monkey -> cat): monkeys diurnal, fruit ripens by day, curl at night
{ const noon = at(world(11), 0.5);
ok(mean(noon.of('monkey'), m => m.state.rest) < 0.5, 'noon: the monkey troop is awake (mean rest ' + mean(noon.of('monkey'), m => m.state.rest).toFixed(2) + ')');
ok(mean(noon.of('fruit'), f => f.state.active) > 0.5, 'noon: fruit ripening in the canopy light');
const midn = at(world(11), 0.0);
ok(mean(midn.of('monkey'), m => m.state.rest) > 0.9, 'midnight: monkeys curled on their branches');
}
// 1c. FROG (nocturnal, calls at night) + SLOTH (cathemeral, keeps NO hour)
{ const noon = at(world(11), 0.5), midn = at(world(11), 0.0);
ok(mean(noon.of('frog'), f => f.state.rest) > 0.6, 'noon: tree frogs cling still on their leaves (rest ' + mean(noon.of('frog'), f => f.state.rest).toFixed(2) + ' > the 0.5 active threshold)');
ok(mean(midn.of('frog'), f => f.state.rest) < 0.2, 'midnight: tree frogs are active and calling');
const sN = noon.of('sloth')[0].state.rest, sM = midn.of('sloth')[0].state.rest;
ok(sN > 0.25 && sN < 0.75 && sM > 0.25 && sM < 0.75, 'the sloth is CATHEMERAL -- never fully asleep nor awake (noon rest ' + sN.toFixed(2) + ', midnight ' + sM.toFixed(2) + ')');
}
// 2. NOON: plants open, morphos awake, bats hang, snails retracted
{ const L = at(world(11), 0.5);
ok(mean(L.of('flower'), f => f.state.active) > 0.35, 'noon: understory flowers open (mean active ' + mean(L.of('flower'), f => f.state.active).toFixed(2) + ')');
ok(mean(L.of('morpho'), m => m.state.rest) < 0.5, 'noon: morphos awake (mean rest ' + mean(L.of('morpho'), m => m.state.rest).toFixed(2) + ')');
ok(mean(L.of('bat'), b => b.state.rest) > 0.55, 'noon: bats hang (mean rest ' + mean(L.of('bat'), b => b.state.rest).toFixed(2) + ')');
ok(mean(L.of('snail'), s => s.state.out || 0) < 0.4, 'noon: snails retracted');
}
// 3. MIDNIGHT: plants shut, morphos folded, bats aflight, snails out
{ const L = at(world(11), 0.0);
ok(mean(L.of('flower'), f => f.state.active) < 0.06, 'midnight: flowers shut (mean active ' + mean(L.of('flower'), f => f.state.active).toFixed(2) + ')');
ok(mean(L.of('morpho'), m => m.state.rest) > 0.9, 'midnight: morphos folded');
ok(mean(L.of('bat'), b => b.state.rest) < 0.1, 'midnight: bats aflight');
ok(mean(L.of('snail'), s => s.state.out) > 0.9, 'midnight: snails out and crawling');
}
// 4. the MOULD colony spreads through the night (its activity is its growth)
{ const w = world(11); w.clock.t = 0; const g0 = w.fields().life.of('mould')[0].state.grow;
for (let i = 0; i < 200; i++) { w.clock.t = (0.9 + 0.1 * i / 200) * DAY; w.step(0.5); }
const g1 = w.fields().life.of('mould')[0].state.grow;
ok(g1 > g0, 'the mould colony spreads through the small hours (grow ' + g0.toFixed(3) + ' -> ' + g1.toFixed(3) + ')');
}
// 5. DETERMINISM: same seed -> identical sim (seeded rng in the behaviours)
{ const a = world(7), b = world(7);
for (let i = 0; i < 40; i++) { a.clock.t = 0.5 * DAY; a.step(0.05); b.clock.t = 0.5 * DAY; b.step(0.05); }
const ea = a.fields().life.entities, eb = b.fields().life.entities; let same = true;
for (let k = 0; k < ea.length; k++) if (ea[k].pos[0] !== eb[k].pos[0] || ea[k].pos[1] !== eb[k].pos[1]) { same = false; break; }
ok(same, 'same seed -> identical sim after 40 steps (reproducible)');
}
// 6. a full day runs clean, every organism stays in bounds
{ const w = world(3); let inb = true;
for (let i = 0; i < 400 && inb; i++) { w.step(DAY / 400); for (const e of w.fields().life.entities) if (e.pos[0] < -0.01 || e.pos[0] > 1.01 || e.pos[1] < -0.01 || e.pos[1] > 1.01) { inb = false; break; } }
ok(inb, 'a full simulated day runs clean, every organism stays within [0,1]');
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);