← Natura·Docs/Modules/Source

biome-test.js

79 lines · in natura-src-1.0.0.zip as natura/biome-test.js · a test

// biome-test.js -- assertions for Biome dynamics (climate-driven biome map that shifts over time).
const WG = require('./worldgen.js');
const N = require('./natura.js');
const { makeWeather } = require('./weather.js');
const { makeBiome, BIOMES, COLD } = require('./biome.js');
const { makeStellar } = require('./stellar.js');
const { World, UNITS } = N;
const GRID = 44, DAY = UNITS.DAY, YEAR = UNITS.YEAR;

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

function build(seed, biomeCfg) {
  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, latGradient: 17 }),
    makeBiome(Object.assign({ N: GRID, weather: 'weather', elevation: el, seaLevel: 0 }, biomeCfg))
  ] });
}
function years(w, y, dt) { const n = Math.round(y * YEAR / dt); for (let i = 0; i < n; i++) w.step(dt); }
function distinctLand(b) { const s = new Set(); for (let k = 0; k < b.length; k++) if (b[k] > 0) s.add(b[k]); return s.size; }
function coldFrac(b) { let c = 0, land = 0; for (let k = 0; k < b.length; k++) { if (b[k] <= 0) continue; land++; if (COLD.has(b[k])) c++; } return land ? c / land : 0; }

console.log('biome assertions:');

// 1. DETERMINISM
{
  const a = build(7); years(a, 10, DAY); const b = build(7); years(b, 10, DAY);
  const ba = a.fields().biome.biome, bb = b.fields().biome.biome;
  let same = true; for (let k = 0; k < GRID * GRID; k++) if (ba[k] !== bb[k]) { same = false; break; }
  ok(same, 'same seed -> identical biome map (reproducible)');
}

// 2. VARIED + SENSIBLE: several biomes settle, and the cold-classed biomes really do sit on colder ground
{
  const w = build(7); years(w, 14, DAY);
  const f = w.fields().biome, b = f.biome, cT = f.climateTemp;
  ok(distinctLand(b) >= 4, 'a varied biome map settles (' + distinctLand(b) + ' distinct land biomes)');
  let coldT = 0, cn = 0, warmT = 0, wn = 0;
  for (let k = 0; k < GRID * GRID; k++) { if (b[k] <= 0) continue; if (COLD.has(b[k])) { coldT += cT[k]; cn++; } else if (b[k] === 5 || b[k] === 6) { warmT += cT[k]; wn++; } }
  ok(cn > 0 && wn > 0 && coldT / cn < warmT / wn, 'cold biomes sit on colder climate than the temperate ones (' + (coldT / cn).toFixed(1) + 'C vs ' + (warmT / wn).toFixed(1) + 'C)');
}

// 3. STATIONARY STABILITY (hysteresis): with no climate trend, biomes settle and then mostly STOP shifting
{
  const w = build(7); years(w, 14, DAY);                          // let it settle
  let shifts = 0; const n = Math.round(8 * YEAR / DAY);
  for (let i = 0; i < n; i++) { const r = w.step(DAY); for (const e of r.events) if (e.kind === 'biomeShift') shifts += e.cells; }
  let land = 0; const b = w.fields().biome.biome; for (let k = 0; k < GRID * GRID; k++) if (b[k] > 0) land++;
  ok(shifts < land * 0.25, 'a stationary climate settles: few biome shifts once formed (' + shifts + ' over 8yr, ' + land + ' land cells)');
}

// 4. WARMING SHIFTS the map: a warming world LOSES its cold biomes (tundra retreats, forest advances)
{
  const w = build(7, { warmingPerYear: 0.3 });
  years(w, 6, DAY); const early = coldFrac(w.fields().biome.biome);
  years(w, 26, DAY); const late = coldFrac(w.fields().biome.biome);
  ok(late < early - 0.02, 'warming retreats the cold biomes (cold fraction ' + (early * 100).toFixed(0) + '% -> ' + (late * 100).toFixed(0) + '%)');
}

// 5. STELLAR COUPLING: the sun's insolation drives the climate -> a fainter (young) sun gives a colder map
{
  const buildS = (age0Gyr) => {
    const cont = WG.makeContinent(7, { period: 6, shore: 0.5 }); const el = (x, y) => cont.heightAt(x, y);
    return World({ seed: 7, systems: [
      makeStellar({ age0Gyr }),
      makeWeather({ N: GRID, seaLevel: 0, elevation: el, prevailU: GRID * 0.05, prevailV: GRID * 0.012, systems: 5, latGradient: 17 }),
      makeBiome({ N: GRID, weather: 'weather', elevation: el, seaLevel: 0, stellar: 'stellar' })
    ] });
  };
  const faint = buildS(0.3); years(faint, 14, DAY);               // a young, fainter sun (insolation ~0.72)
  const bright = buildS(4.6); years(bright, 14, DAY);             // the present sun (insolation ~1.0)
  const cf = coldFrac(faint.fields().biome.biome), cb = coldFrac(bright.fields().biome.biome);
  ok(cf > cb + 0.05, 'a fainter sun gives a colder world (cold fraction ' + (cf * 100).toFixed(0) + '% faint vs ' + (cb * 100).toFixed(0) + '% present)');
}

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