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);