← Natura·Docs/Modules/Source

lifecycle-test.js

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

// lifecycle-test.js -- the LIFE-CYCLE system. Determinism, boundedness, and the continuum emergences: a
// population TURNS OVER (individuals die, the whole persists across generations, with an age structure), it
// rides its food's CARRYING CAPACITY, matter passes THROUGH life (deaths -> carcasses -> nutrients back to
// the soil), and creatures die of OLD AGE, not only starvation -- the full lifespan runs.
const N = require('./natura.js');
const { makeLifecycle, STAGE } = require('./lifecycle.js');
const { World, UNITS } = N;
const DAY = UNITS.DAY || UNITS.HOUR * 24;

let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log('  FAIL: ' + m); } };

function run(seed, cfg, days) {
  const w = World({ seed, systems: [makeLifecycle(cfg)] });
  for (let i = 0; i < (days || 400); i++) w.step(DAY);
  return w.fields().lifecycle;
}

console.log('lifecycle assertions:');

// 1. DETERMINISM
{
  const a = run(3, { food: () => 0.6 }, 200), b = run(3, { food: () => 0.6 }, 200);
  ok(a.births === b.births && a.deaths === b.deaths && a.nAlive === b.nAlive && a.dOld === b.dOld, 'same seed -> identical life history (births/deaths/pop reproduce)');
}

// 2. BOUNDED + FINITE -- ages >= 0, energy/health in [0,1.2], populations non-negative and within the cap
{
  const s = run(5, { food: () => 0.6 }, 500);
  let good = isFinite(s.meanAge) && s.nAlive >= 0 && s.nAlive <= s.cap && s.births >= 0 && s.deaths >= 0;
  for (let i = 0; i < s.cap; i++) { if (!isFinite(s.age[i]) || s.age[i] < -1e-3 || !isFinite(s.energy[i]) || s.energy[i] < -1e-3 || s.energy[i] > 1.2 + 1e-3 || s.health[i] < -1e-3 || s.health[i] > 1 + 1e-3) good = false; }
  for (let n = 0; n < s.nutrients.length; n++) if (!isFinite(s.nutrients[n]) || s.nutrients[n] < -1e-6) good = false;
  ok(good, 'ages, energy, health, populations, and the nutrient field stay finite and in range');
}

// 3. TURNOVER -- over generations the individuals die but the POPULATION persists, and holds an age structure
{
  const s = run(7, { food: () => 0.6 }, 500);
  ok(s.nAlive > 0, 'the population persists across generations (' + s.nAlive + ' alive after 500 days, past several lifespans)');
  ok(s.births > 40 && s.deaths > 40, 'a real turnover happened -- many born, many died (' + s.births + ' births, ' + s.deaths + ' deaths)');
  ok(s.nJuv > 0 && s.nAdult > 0, 'an AGE STRUCTURE emerges -- young and grown alive at once (' + s.nJuv + ' juv, ' + s.nAdult + ' adult)');
}

// 4. CARRYING CAPACITY -- a fed world sustains a population; a barren one collapses it
{
  const ample = run(9, { food: () => 0.9 }, 400).nAlive;
  const barren = run(9, { food: () => 0.0 }, 400).nAlive;
  ok(ample > barren + 30, 'population rides its food: a fed world sustains it, a barren one collapses it (' + ample + ' fed vs ' + barren + ' barren)');
  ok(barren < 10, 'with no food the population starves out (' + barren + ' left)');
}

// 5. MATTER PASSES THROUGH LIFE -- the dead decay into nutrients that return to the soil
{
  const s = run(11, { food: () => 0.55 }, 400);
  let nut = 0; for (let n = 0; n < s.nutrients.length; n++) nut += s.nutrients[n];
  ok(s.deaths > 0 && nut > 0, 'death returns matter to the ground -- carcasses decay into a nutrient field (' + s.deaths + ' deaths -> nutrient sum ' + nut.toFixed(1) + ')');
}

// 6. OLD AGE -- in a well-fed world creatures live out their span and die of age, not only hunger
{
  const s = run(13, { food: () => 0.95 }, 500);
  ok(s.dOld > 0, 'creatures live full lives and die of OLD AGE, not only starvation (' + s.dOld + ' of ' + s.deaths + ' deaths were old age)');
}

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