daynight-probe.js
54 lines · in natura-src-1.0.0.zip as natura/daynight-probe.js · a probe (a robustness or behaviour check run in node) · its entry in the module reference
// daynight-probe.js -- verify the DAY/NIGHT ACTIVITY verb by RUNNING it (the balance-trap lesson: never reason that
// a behaviour "should" stabilise; run it ON vs OFF, multi-seed, and read the ground truth). Checks (1) it does NOT
// destabilise the ecosystem (populations stay sane vs OFF), and (2) the rhythm actually manifests -- prey move slower
// at night (bedded) and kills concentrate in low light (the nocturnal/crepuscular hunt).
const N = require('./natura.js');
const { makeWeather } = require('./weather.js');
const { makeVegetation } = require('./vegetation.js');
const { makeEcosystem } = require('./ecosystem.js');
const { World, UNITS } = N;
const GRID = 40;
function run(seed, dayNight) {
const w = World({ seed, systems: [makeWeather({ N: GRID }), makeVegetation({ N: GRID, weather: 'weather', waterK: 13, drainDays: 40 }), makeEcosystem({ N: GRID })] });
for (let i = 0; i < 60 * 6; i++) w.step(UNITS.HOUR * 4); // grow vegetation + settle
const eco = w.get('ecosystem');
let hmin = 1e9, hmax = 0, hsum = 0, pmin = 1e9, pmax = 0, psum = 0, n = 0;
let killsLow = 0, killsDay = 0, prevKills = (eco.killsYoung + eco.killsAdult);
let spdNightSum = 0, spdNightN = 0, spdDaySum = 0, spdDayN = 0;
for (let d = 0; d < 220; d++) {
for (let s = 0; s < 6; s++) {
if (dayNight) eco.__tod = w.clock.dayPhase(); // feed the sim the time of day (else leave inert)
w.step(UNITS.HOUR * 4);
const tod = w.clock.dayPhase(), c = Math.cos(tod * 2 * Math.PI), lowLight = 1 - Math.max(0, -c);
const dk = (eco.killsYoung + eco.killsAdult) - prevKills; prevKills += dk;
if (dayNight) { if (lowLight > 0.6) killsLow += dk; else killsDay += dk; }
// sample mean herbivore speed, bucketed by deep-night vs day
const herbs = eco.agents.filter(a => a.alive && a.sp === 0);
if (herbs.length) { let sp = 0; for (const a of herbs) sp += Math.hypot(a.vx, a.vy); sp /= herbs.length;
if (c > 0.5) { spdNightSum += sp; spdNightN++; } else if (c < -0.5) { spdDaySum += sp; spdDayN++; } }
}
const p = eco.pop; hmin = Math.min(hmin, p.herbivore); hmax = Math.max(hmax, p.herbivore); hsum += p.herbivore;
pmin = Math.min(pmin, p.predator); pmax = Math.max(pmax, p.predator); psum += p.predator; n++;
}
return {
hMean: hsum / n, hMin: hmin, hMax: hmax, pMean: psum / n, pMin: pmin, pMax: pmax,
killsLow, killsDay, spdNight: spdNightN ? spdNightSum / spdNightN : 0, spdDay: spdDayN ? spdDaySum / spdDayN : 0,
alive: eco.pop.herbivore > 0 && eco.pop.predator > 0
};
}
let fail = 0; const ok = (c, m) => { if (!c) { console.log(' FAIL:', m); fail++; } else console.log(' ok:', m); };
for (const seed of [3, 7, 11]) {
console.log('\n== seed ' + seed + ' ==');
const off = run(seed, false), on = run(seed, true);
console.log(' OFF: herb ' + off.hMin + '..' + off.hMax + ' (mean ' + off.hMean.toFixed(0) + ') pred ' + off.pMin + '..' + off.pMax + ' (mean ' + off.pMean.toFixed(0) + ')');
console.log(' ON : herb ' + on.hMin + '..' + on.hMax + ' (mean ' + on.hMean.toFixed(0) + ') pred ' + on.pMin + '..' + on.pMax + ' (mean ' + on.pMean.toFixed(0) + ')');
console.log(' ON rhythm: kills low-light=' + on.killsLow + ' vs daylight=' + on.killsDay + ' | prey speed night=' + on.spdNight.toFixed(4) + ' day=' + on.spdDay.toFixed(4));
ok(on.alive, 'day/night world stays alive (both species present at end)');
ok(on.hMean > off.hMean * 0.4 && on.hMean < off.hMean * 2.5, 'herbivore mean stays in the same ballpark as OFF (not destabilised)');
ok(on.spdNight < on.spdDay * 0.85, 'prey move SLOWER at night than by day (bedded down)');
ok(on.killsLow > on.killsDay, 'predation concentrates in LOW LIGHT (the nocturnal/crepuscular hunt)');
}
console.log(fail ? '\nFAILED (' + fail + ')' : '\nALL PASS');
process.exit(fail ? 1 : 0);