← Natura·Docs/Modules/Source

niche-test.js

66 lines · in natura-src-1.0.0.zip as natura/niche-test.js · a test

// niche-test.js -- assertions for NICHE CONSTRUCTION (builders that engineer their own habitat; structures that last).
const N = require('./natura.js');
const { makeNiche } = require('./niche.js');
const { World, UNITS } = N;
const GRID = 40, DAY = UNITS.DAY;

let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log('  FAIL: ' + m); } };
const build = (cfg) => World({ seed: 3, systems: [makeNiche(Object.assign({ N: GRID }, cfg))] });
const days = (w, d) => { const n = Math.round(d / 2); for (let i = 0; i < n; i++) w.step(2 * DAY); };   // 2-day steps
const areaOver = (f, t) => { let c = 0; for (let k = 0; k < f.length; k++) if (f[k] > t) c++; return c; };

console.log('niche assertions:');

// 1. DETERMINISM
{
  const a = build({}); const b = build({}); days(a, 600); days(b, 600);
  const pa = a.fields().niche.built, pb = b.fields().niche.built;
  let same = true; for (let k = 0; k < GRID * GRID; k++) if (pa[k] !== pb[k]) { same = false; break; }
  ok(same, 'same seed -> identical engineered world (reproducible)');
}

// 2. BOUNDEDNESS
{
  const w = build({}); let okAll = true;
  for (let i = 0; i < 300; i++) { w.step(2 * DAY); const f = w.fields().niche;
    for (let k = 0; k < GRID * GRID; k++) if (!(f.pop[k] >= 0 && f.pop[k] <= 1.5 && f.built[k] >= 0 && f.built[k] <= 1.5)) okAll = false; }
  ok(okAll, 'builder density and built structure stay finite + bounded');
}

// 3. ENGINEERING LIFT -- building raises the population FAR above what the bare ground would hold
{
  const eng = build({}); days(eng, 1000);                       // builders engineer their habitat
  const bare = build({ buildGain: 0 }); days(bare, 1000);       // identical, but building does nothing
  const pe = eng.fields().niche.meanPop, pb = bare.fields().niche.meanPop;
  ok(pe > pb * 2.5, 'ecosystem engineering lifts the population above the bare-ground base (engineered mean ' + pe.toFixed(3) + ' vs bare ' + pb.toFixed(3) + ')');
}

// 4. THE LEGACY -- the structure OUTLASTS the builders (kill them; the dam still holds, decaying slowly)
{
  const w = build({}); days(w, 1000);
  const before = w.fields().niche.meanBuilt;
  ok(before > 0.15, 'the builders raise real structure (mean built ' + before.toFixed(3) + ')');
  w.get('niche').pop.fill(0);                                    // a die-off: every builder gone
  days(w, 150);                                                  // ...but the world keeps their work a while
  const f = w.fields().niche;
  ok(f.meanPop < 0.02, 'the builders are gone (mean pop ' + f.meanPop.toFixed(3) + ')');
  ok(f.meanBuilt > before * 0.55, 'the STRUCTURE OUTLASTS them -- the world remembers (built ' + before.toFixed(3) + ' -> ' + f.meanBuilt.toFixed(3) + ' after the die-off)');
}

// 5. NO SPONTANEOUS STRUCTURE -- without builders, nothing gets built (structure needs a life to make it)
{
  const w = build({ seed: 0 }); days(w, 600);
  ok(w.fields().niche.meanBuilt < 1e-4, 'no builders, no structure (nothing builds itself)');
}

// 6. IT SPREADS -- the engineered ground grows outward from where the builders began
{
  const w = build({});
  days(w, 120); const early = areaOver(w.fields().niche.built, 0.3);
  days(w, 700); const late = areaOver(w.fields().niche.built, 0.3);
  ok(late > early + 20, 'the engineered zone spreads as builders colonise + remake new ground (' + early + ' -> ' + late + ' cells)');
}

console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);