tectonics-test.js
77 lines · in natura-src-1.0.0.zip as natura/tectonics-test.js · a test
// tectonics-test.js -- assertions for Tectonics (drifting plates build mountains at convergent margins).
const N = require('./natura.js');
const { makeTectonics } = require('./tectonics.js');
const { World, UNITS } = N;
const YEAR = UNITS.YEAR, MYR = 1e6 * YEAR;
let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log(' FAIL: ' + m); } };
function run(seed, myrPerStep, steps, cfg) {
const w = World({ seed, systems: [makeTectonics(Object.assign({ N: 64, elevation: () => 0.05 }, cfg))] });
for (let i = 0; i < steps; i++) w.step(myrPerStep * MYR);
return w.get('tectonics');
}
// recompute the convergence field the same way the system does, to classify boundaries
function convergence(st) {
const { N: n, plate, seeds } = st, conv = new Float32Array(n * n);
for (let j = 0; j < n; j++) for (let i = 0; i < n; i++) {
const pA = plate[j * n + i], vA = seeds[pA]; let c = 0;
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue; const ni = i + dx, nj = j + dy; if (ni < 0 || ni >= n || nj < 0 || nj >= n) continue;
const pB = plate[nj * n + ni]; if (pB === pA) continue; const vB = seeds[pB], nl = Math.hypot(dx, dy);
c += ((vA.vx - vB.vx) * dx + (vA.vy - vB.vy) * dy) / nl;
}
conv[j * n + i] = c;
}
return conv;
}
const mean = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
console.log('tectonics assertions:');
// 1. DETERMINISM
{
const a = run(5, 4, 12).elev, b = run(5, 4, 12).elev;
let same = true; for (let k = 0; k < 64 * 64; k++) if (Math.abs(a[k] - b[k]) > 1e-9) { same = false; break; }
ok(same, 'same seed -> identical plates + heightfield (reproducible)');
}
// 2. MOUNTAINS at CONVERGENT margins: after tens of Myr the converging boundaries stand higher than the
// rifting ones, and the flat craton has grown real relief
{
const st = run(5, 4, 12); // ~48 Myr from a flat 0.05 craton
const conv = convergence(st), elev = st.elev;
const upE = [], downE = [];
for (let k = 0; k < 64 * 64; k++) { if (conv[k] > 0.15) upE.push(elev[k]); else if (conv[k] < -0.15) downE.push(elev[k]); }
let mx = 0; for (let k = 0; k < 64 * 64; k++) mx = Math.max(mx, elev[k]);
ok(mx > 0.2, 'a flat craton grows real mountains (max elevation ' + mx.toFixed(2) + ' from 0.05)');
ok(upE.length && downE.length && mean(upE) > mean(downE) + 0.05, 'convergent margins stand higher than rifting ones (' + mean(upE).toFixed(2) + ' vs ' + mean(downE).toFixed(2) + ')');
}
// 3. It EVOLVES: the heightfield genuinely changes over deep time (it is not the static starting craton)
{
const st = run(7, 4, 10);
let changed = 0; for (let k = 0; k < 64 * 64; k++) if (Math.abs(st.elev[k] - 0.05) > 0.02) changed++;
ok(changed > 100, 'the ground evolves over Myr (' + changed + ' cells moved from the starting height)');
}
// 4. The crust is PARTITIONED into plates
{
const st = run(7, 4, 2, { plates: 5 }); const s = new Set();
for (let k = 0; k < 64 * 64; k++) s.add(st.plate[k]);
ok(s.size >= 3, 'the crust is broken into drifting plates (' + s.size + ' present)');
}
// 5. ISOSTASY: a highland with NO convergence under it (a plate interior) erodes back toward baseline
{
const w = World({ seed: 5, systems: [makeTectonics({ N: 64, elevation: () => 0.05 })] });
const st = w.get('tectonics'), conv = convergence(st);
let idx = -1; for (let k = 0; k < 64 * 64; k++) if (conv[k] === 0) { idx = k; break; } // a plate-interior cell, no uplift
if (idx >= 0) st.elev[idx] = 0.6; // drop an artificial highland on it
for (let i = 0; i < 12; i++) w.step(4 * MYR); // ~48 Myr
ok(idx >= 0 && st.elev[idx] < 0.55, 'isostasy: an interior highland erodes back down (0.60 -> ' + (idx >= 0 ? st.elev[idx].toFixed(2) : 'n/a') + ')');
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);