← Natura·Docs/Modules/Source

compose-test.js

84 lines · in natura-src-1.0.0.zip as natura/compose-test.js · a test

// compose-test.js -- the capstone of the weekend's framework growth: the FIVE new systems (CRYOSPHERE, CHEMISTRY,
// NICHE, WATER CYCLE, SENSING) actually COMPOSE -- they run together on ONE World + clock without breaking, each
// exposes its fields, and real emergences survive the shared clock: a CROSS-SYSTEM COUPLING fires (chemistry reads
// the water cycle's moisture, so iron RUSTS where hydro pooled water and stays bright where it is dry), and each
// system's own signature holds inside the composed world (sensing's scent still goes downwind). Composability is
// the framework's whole point; this proves the new systems honour it. (Cryosphere is deep-time and barely moves at
// a day-step -- the test asserts it RUNS + exposes finite fields, not that it glaciates.)
const N = require('./natura.js');
const { makeHydro } = require('./hydro.js');
const { makeChemistry } = require('./chemistry.js');
const { makeCryosphere } = require('./cryosphere.js');
const { makeNiche } = require('./niche.js');
const { makeSensing } = require('./sensing.js');
const { World, UNITS } = N;
const GRID = 36, DAY = UNITS.DAY;

let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log('  FAIL: ' + m); } };
const bowl = (x, y) => 0.08 + 0.6 * (x * x + y * y);            // shared terrain: a basin (wet low centre, dry high rim)
function build(seed) {
  return World({ seed: seed || 5, systems: [
    makeHydro({ N: GRID, elevation: bowl, temperature: 16, initLiquid: 0.34, runoffK: 0.8 }),
    makeChemistry({ N: GRID, material: 'iron', weather: 'hydro' }),   // <-- reads the WATER CYCLE's moisture
    makeCryosphere({ N: GRID, elevation: bowl, forcing: -3 }),
    makeNiche({ N: GRID, suitability: (x, y) => Math.max(0, 1 - (x * x + y * y)) }),
    makeSensing({ N: GRID, sources: [{ x: 0, y: 0, scent: 1, sound: 1, heat: 1, vis: 1 }], wind: { x: 1, y: 0 }, light: 1 })
  ] });
}
const days = (w, d) => { for (let i = 0; i < Math.round(d / 2); i++) w.step(2 * DAY); };
const finiteAll = (a) => { for (let k = 0; k < a.length; k++) if (!isFinite(a[k])) return false; return true; };

console.log('compose assertions:');

// 1. THEY COMPOSE -- all five systems run together on one clock and expose finite fields
{
  const w = build(5); days(w, 400);
  const f = w.fields();
  ok(f.hydro && f.chemistry && f.cryosphere && f.niche && f.sensing, 'all five systems are present in one World');
  ok(finiteAll(f.hydro.liquid) && finiteAll(f.chemistry.aged) && finiteAll(f.cryosphere.ice) && finiteAll(f.niche.built) && finiteAll(f.sensing.detect),
    'every system keeps its fields finite while sharing the clock (no cross-talk blowup)');
}

// 2. DETERMINISM of the whole STACK -- same seed -> identical coupled run
{
  const a = build(5), b = build(5); days(a, 300); days(b, 300);
  const aa = a.fields().chemistry.aged, bb = b.fields().chemistry.aged;
  let same = true; for (let k = 0; k < GRID * GRID; k++) if (aa[k] !== bb[k]) { same = false; break; }
  ok(same, 'the coupled four-system stack is reproducible (same seed -> identical run)');
}

// 3. THE CROSS-SYSTEM COUPLING FIRES -- iron rusts where the WATER CYCLE has pooled water, not on the dry ground
{
  const w = build(5); days(w, 500);
  const f = w.fields(), liq = f.hydro.liquid, aged = f.chemistry.aged;
  // rank cells by how wet the water cycle made them; compare iron's rust in the wettest vs the driest third
  const idx = Array.from({ length: GRID * GRID }, (_, k) => k).sort((p, q) => liq[q] - liq[p]);
  const third = Math.floor(idx.length / 3);
  let wetRust = 0, dryRust = 0;
  for (let i = 0; i < third; i++) wetRust += aged[idx[i]];
  for (let i = idx.length - third; i < idx.length; i++) dryRust += aged[idx[i]];
  wetRust /= third; dryRust /= third;
  ok(wetRust > dryRust + 0.1, 'CHEMISTRY couples to HYDRO: iron rusts where the water pooled, holds where it is dry (wet ' + wetRust.toFixed(2) + ' vs dry ' + dryRust.toFixed(2) + ')');
}

// 4. EACH SYSTEM ACTUALLY ADVANCED -- it is a live coupled world, not four frozen stubs sharing a clock
{
  const w = build(5); days(w, 500); const f = w.fields();
  let liqVar = 0, mL = 0; for (let k = 0; k < GRID * GRID; k++) mL += f.hydro.liquid[k]; mL /= GRID * GRID;
  for (let k = 0; k < GRID * GRID; k++) liqVar += Math.abs(f.hydro.liquid[k] - mL);
  ok(liqVar > 1, 'the water cycle redistributed the water (it pooled -- not a flat field)');
  ok(f.chemistry.meanAged > 0.05, 'chemistry weathered the iron under the shared conditions (mean ' + f.chemistry.meanAged.toFixed(2) + ')');
  ok(f.niche.meanPop >= 0 && f.niche.meanBuilt >= 0, 'niche ran its builders in the shared world');
}

// 5. A SYSTEM'S OWN SIGNATURE SURVIVES the shared clock -- SENSING's scent still goes DOWNWIND inside the five-system world
{
  const w = build(5); days(w, 300);
  const sc = w.fields().sensing.scent, ii = (x) => Math.round((x + 1) / 2 * (GRID - 1)), row = ii(0) * GRID;
  const down = sc[row + ii(0.4)], up = sc[row + ii(-0.4)];
  ok(down > up + 0.02, 'SENSING composes: its scent plume still drifts downwind inside the five-system world (down ' + down.toFixed(3) + ' vs up ' + up.toFixed(3) + ')');
}

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