terrain-test.js
77 lines · in natura-src-1.0.0.zip as natura/terrain-test.js · a test
// terrain-test.js -- assertions for the Terrain erosion system, coupled to Weather. node terrain-test.js.
const N = require('./natura.js');
const { makeWeather } = require('./weather.js');
const { makeTerrain } = require('./terrain.js');
const { World, UNITS } = N;
let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log(' FAIL: ' + m); } };
function terrain(x, y) { if (x < -0.55) return -0.2; return 0.15 + 0.6 * Math.exp(-(x * x) / 0.03); }
const GRID = 48;
const gx = (i) => (i / (GRID - 1)) * 2 - 1;
function mkWorld(seed) {
return World({ seed, systems: [
makeWeather({ N: GRID, elevation: terrain, seaLevel: 0 }),
makeTerrain({ N: GRID, elevation: terrain, seaLevel: 0 })
] });
}
function meanBand(F, land, xlo, xhi) {
let s = 0, n = 0;
for (let j = 0; j < GRID; j++) for (let i = 0; i < GRID; i++) { const k = j * GRID + i; const x = gx(i); if (land[k] && x >= xlo && x <= xhi) { s += F[k]; n++; } }
return n ? s / n : 0;
}
console.log('terrain erosion assertions:');
// finite, bounded above the floor, erosion happens, sea untouched
{
const w = mkWorld(7);
const H0 = Float32Array.from(w.get('terrain').H); // snapshot the initial heightfield
for (let i = 0; i < 200 * 6; i++) w.step(UNITS.HOUR * 4); // ~200 sim-days
const t = w.get('terrain');
let nan = false, belowFloor = 0, totalErosion = 0, seaChanged = 0;
for (let k = 0; k < t.H.length; k++) {
if (!isFinite(t.H[k])) nan = true;
if (t.H[k] < -0.351) belowFloor++;
if (t.land[k]) totalErosion += (H0[k] - t.H[k]); else if (Math.abs(t.H[k] - H0[k]) > 1e-9) seaChanged++;
}
ok(!nan, 'no NaN in the heightfield over 200 sim-days');
ok(belowFloor === 0, 'heightfield never erodes below the bedrock floor');
ok(totalErosion > 0, 'erosion happened (land lowered where the rain ran)');
ok(seaChanged === 0, 'the sea floor is untouched (erosion is on land)');
}
// determinism
{
const a = mkWorld(11), b = mkWorld(11);
for (let i = 0; i < 100 * 6; i++) { a.step(UNITS.HOUR * 4); b.step(UNITS.HOUR * 4); }
let same = true; const ha = a.get('terrain').H, hb = b.get('terrain').H;
for (let k = 0; k < ha.length; k++) if (ha[k] !== hb[k]) { same = false; break; }
ok(same, 'same seed -> identical eroded terrain (reproducible)');
}
// THE emergence test: erosion inherits the RAIN-SHADOW -- windward wears down more than the dry lee
{
const w = mkWorld(3);
const H0 = Float32Array.from(w.get('terrain').H);
for (let i = 0; i < 260 * 6; i++) w.step(UNITS.HOUR * 4);
const t = w.get('terrain');
const ero = new Float32Array(GRID * GRID); for (let k = 0; k < ero.length; k++) ero[k] = H0[k] - t.H[k];
const windward = meanBand(ero, t.land, -0.45, -0.1);
const lee = meanBand(ero, t.land, 0.1, 0.45);
ok(windward > lee * 1.3, 'windward erosion > lee (windward ' + windward.toFixed(4) + ' vs lee ' + lee.toFixed(4) + ')');
console.log(' windward erosion ' + windward.toFixed(4) + ', lee erosion ' + lee.toFixed(4) + ' (ratio ' + (windward / (lee + 1e-9)).toFixed(2) + 'x)');
}
// coupling guard: terrain alone (no weather) still erodes by slope but is finite + safe
{
const w = World({ seed: 1, systems: [makeTerrain({ N: GRID, elevation: terrain, seaLevel: 0 })] });
for (let i = 0; i < 50; i++) w.step(UNITS.HOUR * 24);
const t = w.get('terrain');
let nan = false; for (let k = 0; k < t.H.length; k++) if (!isFinite(t.H[k])) nan = true;
ok(!nan, 'terrain with no weather present is a safe finite no-drama (erodes by slope only)');
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);