← Natura·Docs/Modules/Source

chemistry-test.js

81 lines · in natura-src-1.0.0.zip as natura/chemistry-test.js · a test

// chemistry-test.js -- assertions for the CHEMICAL LAYER (materials that weather by exposure over sim-time).
const N = require('./natura.js');
const { makeChemistry, IDX, matColor } = require('./chemistry.js');
const { World, UNITS } = N;
const GRID = 40, YEAR = UNITS.YEAR;

let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log('  FAIL: ' + m); } };
const build = (cfg) => World({ seed: 3, systems: [makeChemistry(Object.assign({ N: GRID }, cfg))] });
const years = (w, y) => { for (let i = 0; i < Math.round(y); i++) w.step(YEAR); };
const meanHalf = (aged, left) => { let s = 0, c = 0; for (let j = 0; j < GRID; j++) for (let i = 0; i < GRID; i++) { const x = (i / (GRID - 1)) * 2 - 1; if ((left && x < 0) || (!left && x >= 0)) { s += aged[j * GRID + i]; c++; } } return c ? s / c : 0; };

console.log('chemistry assertions:');

// 1. DETERMINISM
{
  const a = build({ material: 'iron', moisture: 0.6 }); const b = build({ material: 'iron', moisture: 0.6 });
  years(a, 12); years(b, 12);
  const aa = a.fields().chemistry.aged, bb = b.fields().chemistry.aged;
  let same = true; for (let k = 0; k < GRID * GRID; k++) if (aa[k] !== bb[k]) { same = false; break; }
  ok(same, 'same config -> identical weathering (reproducible)');
}

// 2. BOUNDEDNESS
{
  const w = build({ material: 'iron', moisture: 1 }); let okAll = true;
  for (let i = 0; i < 30; i++) { w.step(YEAR); const ag = w.fields().chemistry.aged; for (let k = 0; k < GRID * GRID; k++) if (!(ag[k] >= 0 && ag[k] <= 1)) okAll = false; }
  ok(okAll, 'aging stays in [0,1] everywhere');
}

// 3. THE EMERGENCE -- composition tracks the CONDITIONS: the same iron rusts on the wet coast, stays bright in the desert
{
  const w = build({ material: 'iron', moisture: (x, y) => x < 0 ? 0.9 : 0.03 });   // left wet, right dry
  years(w, 12);
  const ag = w.fields().chemistry.aged, wet = meanHalf(ag, true), dry = meanHalf(ag, false);
  ok(wet > 0.8, 'iron in the damp rusts to completion (wet-side aging ' + wet.toFixed(2) + ')');
  ok(dry < 0.15, 'the same iron in the dry barely ages (dry-side aging ' + dry.toFixed(2) + ')');
  ok(wet > dry + 0.6, 'the DRIVER decides, not the clock: same material + same time, opposite fate');
}

// 4. DRIVER SPECIFICITY -- in a DRY world, time-driven copper still patinas but moisture-driven iron does not
{
  const w = build({ material: (x, y) => x < 0 ? 'copper' : 'iron', moisture: 0.02 });
  years(w, 45);
  const ag = w.fields().chemistry.aged, cop = meanHalf(ag, true), iron = meanHalf(ag, false);
  ok(cop > iron + 0.3, 'a dry world patinas copper but not iron (copper ' + cop.toFixed(2) + ' vs iron ' + iron.toFixed(2) + ') -- the reaction, not just exposure');
}

// 5. DIFFERENT MATERIALS, DIFFERENT RATES -- silver tarnishes fast, copper patinas slow
{
  const s = build({ material: 'silver', moisture: 0.5 }); years(s, 5);
  const c = build({ material: 'copper', moisture: 0.5 }); years(c, 5);
  const sa = s.fields().chemistry.meanAged, ca = c.fields().chemistry.meanAged;
  ok(sa > ca + 0.3, 'silver tarnishes far faster than copper patinas (silver ' + sa.toFixed(2) + ' vs copper ' + ca.toFixed(2) + ' at 5yr)');
}

// 6. WEATHER COUPLING -- the chemistry reads a live moisture FIELD from another system
{
  const wx = { name: 'weather', init() { const m = new Float32Array(GRID * GRID); for (let j = 0; j < GRID; j++) for (let i = 0; i < GRID; i++) m[j * GRID + i] = i < GRID / 2 ? 0.9 : 0.05; return { m }; }, step(s) { return s; }, fields(s) { return { N: GRID, moisture: s.m }; } };
  const w = World({ seed: 3, systems: [wx, makeChemistry({ N: GRID, material: 'iron', weather: 'weather' })] });
  years(w, 12);
  const ag = w.fields().chemistry.aged;
  ok(meanHalf(ag, true) > meanHalf(ag, false) + 0.4, 'chemistry reads WEATHER moisture: the wet side rusts, the dry side does not');
}

// 7. THE COLOUR turns with the reaction -- pristine copper is coppery, weathered copper is green
{
  const fresh = matColor(IDX.copper, 0), old = matColor(IDX.copper, 1);
  ok(fresh[0] > fresh[2] && old[1] > old[0], 'copper runs coppery -> green as it patinas (fresh R>' + 'B, aged G>R)');
}

// 8. LIGHT DRIVER -- paint FADES in the sun, holds in the shade (the other named vein: light-driven chemistry)
{
  const sun = build({ material: 'paint', light: 0.9 }); years(sun, 6);
  const shade = build({ material: 'paint', light: 0.05 }); years(shade, 6);
  const s = sun.fields().chemistry.meanAged, h = shade.fields().chemistry.meanAged;
  ok(s > 0.7 && h < 0.2, 'paint fades in the SUN but holds in the shade (sun ' + s.toFixed(2) + ' vs shade ' + h.toFixed(2) + ')');
}

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