frost-test.js
67 lines · in natura-src-1.0.0.zip as natura/frost-test.js · a test
// frost-test.js -- assertions for the Frost/micro system (dendritic ice growth, dielectric-breakdown).
const N = require('./natura.js');
const { makeWeather } = require('./weather.js');
const { makeFrost } = require('./frost.js');
const { World, UNITS } = N;
let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log(' FAIL: ' + m); } };
function grow(seed, cfg, steps) {
const w = World({ seed, systems: [makeFrost(Object.assign({ N: 96 }, cfg))] });
for (let i = 0; i < steps; i++) w.step(1);
return w.fields().frost;
}
function bbox(ice, N) {
let mni = 1e9, mxi = -1, mnj = 1e9, mxj = -1;
for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) if (ice[j * N + i]) { mni = Math.min(mni, i); mxi = Math.max(mxi, i); mnj = Math.min(mnj, j); mxj = Math.max(mxj, j); }
return { w: mxi - mni + 1, h: mxj - mnj + 1, mni, mxi, mnj, mxj };
}
console.log('frost assertions:');
// 1. DETERMINISM
{
const a = grow(4, { growthPerStep: 3 }, 80).ice, b = grow(4, { growthPerStep: 3 }, 80).ice;
let same = true; for (let k = 0; k < 96 * 96; k++) if (a[k] !== b[k]) { same = false; break; }
ok(same, 'same seed -> identical frost pattern (reproducible)');
}
// 2. DENDRITIC (branching, not a solid blob): the ice spans a wide area but fills only a fraction of it
{
const f = grow(4, { growthPerStep: 3, eta: 1.2 }, 90);
const bb = bbox(f.ice, 96), area = bb.w * bb.h, fillRatio = f.count / area;
ok(f.count > 150, 'the frost grows (' + f.count + ' cells)');
ok(bb.w > 25 && bb.h > 25, 'it spreads across the surface (bbox ' + bb.w + 'x' + bb.h + ')');
ok(fillRatio < 0.55, 'it is dendritic, not a solid blob (fills only ' + (fillRatio * 100).toFixed(0) + '% of its bounding box)');
}
// 3. CONNECTED: every frost cell traces back to the seed (it grew, was not sprinkled)
{
const f = grow(4, { growthPerStep: 3 }, 80), ice = f.ice, S = 96;
const seen = new Uint8Array(S * S), c = (S >> 1) * S + (S >> 1), stack = [c]; seen[c] = 1; let reached = 0;
while (stack.length) { const k = stack.pop(); reached++; const i = k % S, j = (k / S) | 0;
const nb = []; if (i > 0) nb.push(k - 1); if (i < S - 1) nb.push(k + 1); if (j > 0) nb.push(k - S); if (j < S - 1) nb.push(k + S);
for (const m of nb) if (ice[m] && !seen[m]) { seen[m] = 1; stack.push(m); } }
ok(reached === f.count, 'the whole crystal is one connected growth from the seed (' + reached + '/' + f.count + ')');
}
// 4. BRANCHINESS scales with eta: a higher eta reaches FURTHER for the same growth (spidery vs feathery)
{
const lo = grow(4, { growthPerStep: 3, eta: 0.6 }, 80), hi = grow(4, { growthPerStep: 3, eta: 2.5 }, 80);
const rl = Math.max(bbox(lo.ice, 96).w, bbox(lo.ice, 96).h), rh = Math.max(bbox(hi.ice, 96).w, bbox(hi.ice, 96).h);
ok(rh >= rl, 'a higher eta reaches further (span ' + rl + ' at eta .6 -> ' + rh + ' at eta 2.5)');
}
// 5. ACTIVATION: rime forms only on a below-freezing surface -- over a warm surface it does not grow
{
const warm = World({ seed: 1, systems: [
makeWeather({ N: 48, elevation: () => 0.15, systems: 1 }), // low warm land -> center temp well above 0
makeFrost({ N: 48, weather: 'weather', growthPerStep: 3 })
] });
for (let i = 0; i < 40; i++) warm.step(UNITS.HOUR);
ok(warm.fields().frost.count === 0, 'no frost forms on a warm surface -- not even a seed (count ' + warm.fields().frost.count + ')');
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);