← Natura·Docs/Modules/Source

ocean-test.js

82 lines · in natura-src-1.0.0.zip as natura/ocean-test.js · a test

// ocean-test.js -- assertions for the Ocean/Hydrology system (rivers, lakes, tidal coast).
const WG = require('./worldgen.js');
const N = require('./natura.js');
const { makeWeather } = require('./weather.js');
const { makeCelestial } = require('./celestial.js');
const { makeOcean } = require('./ocean.js');
const { World, UNITS } = N;
const GRID = 48, HOUR = UNITS.HOUR, DAY = UNITS.DAY;

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

function build(seed, oceanCfg) {
  const cont = WG.makeContinent(seed, { period: 6, shore: 0.5 });
  const el = (x, y) => cont.heightAt(x, y);
  return World({ seed, systems: [
    makeWeather({ N: GRID, seaLevel: 0, elevation: el, prevailU: GRID * 0.05, prevailV: GRID * 0.012, systems: 5 }),
    makeCelestial({}),
    makeOcean(Object.assign({ N: GRID, seaLevel: 0, elevation: el, weather: 'weather', celestial: 'celestial' }, oceanCfg))
  ] });
}
function spin(w, days, dt) { const n = Math.round(days * 86400 / dt); for (let i = 0; i < n; i++) w.step(dt); }

console.log('ocean assertions:');

// 1. DETERMINISM
{
  const a = build(7); spin(a, 30, HOUR * 3);
  const b = build(7); spin(b, 30, HOUR * 3);
  const fa = a.fields().ocean, fb = b.fields().ocean;
  let same = Math.abs(fa.seaNow - fb.seaNow) < 1e-9;
  for (let k = 0; k < GRID * GRID && same; k++) if (Math.abs(fa.riverFlow[k] - fb.riverFlow[k]) > 1e-6) same = false;
  ok(same, 'same seed -> identical rivers + sea level (reproducible)');
}

// 2. RIVERS CHANNELIZE: flow accumulation concentrates into trunks -- the peak discharge is many times
//    the average, a river NETWORK, not a uniform sheet of runoff.
{
  const w = build(7); spin(w, 40, HOUR * 3);
  const f = w.fields().ocean, land = f.land, fl = f.riverFlow;
  let mx = 0, sum = 0, cnt = 0;
  for (let k = 0; k < GRID * GRID; k++) if (land[k]) { mx = Math.max(mx, fl[k]); sum += fl[k]; cnt++; }
  const mean = sum / cnt;
  ok(mx > 10 * mean && mean > 0, 'rivers channelize: peak discharge >> mean (peak ' + mx.toFixed(1) + ', mean ' + mean.toFixed(2) + ')');
}

// 3. RIVERS RUN TO LOW GROUND: the biggest river is on low land near the sea, not perched on a peak
{
  const w = build(7); spin(w, 40, HOUR * 3);
  const f = w.fields().ocean, land = f.land, fl = f.riverFlow, el = f.elev;
  let peak = -1, els = [];
  for (let k = 0; k < GRID * GRID; k++) if (land[k]) { els.push(el[k]); if (peak < 0 || fl[k] > fl[peak]) peak = k; }
  els.sort((a, b) => a - b); const median = els[els.length >> 1];
  ok(el[peak] < median, 'the trunk river sits on low ground (peak-flow elev ' + el[peak].toFixed(3) + ' < median ' + median.toFixed(3) + ')');
}

// 4. TIDAL COAST: the sea level breathes with the moon's tide over a day
{
  const w = build(7); spin(w, 10, HOUR * 3);
  let mn = 9, mx = -9;
  for (let i = 0; i < 24; i++) { w.step(HOUR); const s = w.fields().ocean.seaNow; mn = Math.min(mn, s); mx = Math.max(mx, s); }
  ok(mx - mn > 0.008, 'the sea rises and falls with the tide over a day (range ' + (mx - mn).toFixed(4) + ')');
}

// 5. LAKES: water pools in the hollows (some land cells hold standing water)
{
  const w = build(3); spin(w, 50, HOUR * 3);
  const f = w.fields().ocean, land = f.land, water = f.water;
  let ponded = 0; for (let k = 0; k < GRID * GRID; k++) if (land[k] && water[k] > 0.05) ponded++;
  ok(ponded > 0, 'water pools in the hollows -> lakes (' + ponded + ' ponded cells)');
}

// 6. FLOODS fire when a big catchment tops its banks (a sensitive bank threshold on a wet run)
{
  const w = build(7, { floodQ: 5 }); const evs = [];   // 5 sits above the ~2 dry trunk, below the ~6-9 storm peak
  const n = Math.round(60 * 86400 / (HOUR * 3));
  for (let i = 0; i < n; i++) { const r = w.step(HOUR * 3); for (const e of r.events) if (e.kind === 'flood') evs.push(e); }
  ok(evs.length > 0, 'a flood fires when the trunk river tops its banks (' + evs.length + ' flood events)');
}

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