← Natura·Docs/Modules/Source

sensing-test.js

78 lines · in natura-src-1.0.0.zip as natura/sensing-test.js · a test

// sensing-test.js -- the emergence proofs for SENSING: detection is an information game, not a fact.
const { makeSensing } = require('./sensing.js');
const N = 64;
let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log('  FAIL: ' + m); } };
const idx = (x, y) => { const i = Math.round((x + 1) / 2 * (N - 1)), j = Math.round((y + 1) / 2 * (N - 1)); return j * N + i; };
const DAY = 86400;
function run(cfg, days) { const s = makeSensing(Object.assign({ N }, cfg)); const st = s.init(); for (let d = 0; d < days; d++) s.step(st, { fields: {} }, DAY); return { s, st, f: s.fields(st) }; }
const finite = (a) => { for (let k = 0; k < a.length; k++) if (!isFinite(a[k])) return false; return true; };
const max = (a) => { let m = 0; for (let k = 0; k < a.length; k++) if (a[k] > m) m = a[k]; return m; };

console.log('sensing assertions:');

// 1. determinism
{
  const a = run({ sources: [{ x: 0, y: 0, scent: 1, sound: 1, heat: 1, vis: 1 }], wind: { x: 1, y: 0 } }, 12);
  const b = run({ sources: [{ x: 0, y: 0, scent: 1, sound: 1, heat: 1, vis: 1 }], wind: { x: 1, y: 0 } }, 12);
  let same = true; for (let k = 0; k < N * N; k++) if (a.f.scent[k] !== b.f.scent[k] || a.f.detect[k] !== b.f.detect[k]) { same = false; break; }
  ok(same, 'deterministic (same config -> identical fields)');
}

// 2. finiteness + boundedness (detect is a 0..1 detectability)
{
  const { f } = run({ wind: { x: 1, y: 0 } }, 15);
  ok(finite(f.scent) && finite(f.sound) && finite(f.sight) && finite(f.detect), 'all channel fields stay finite');
  ok(max(f.detect) <= 1.0001 && max(f.sight) <= 1.0001, 'detectability is bounded in 0..1');
}

// 3. SCENT ASYMMETRY -- the plume goes DOWNWIND: a nose downwind smells prey a nose upwind never will
{
  const { f } = run({ sources: [{ x: 0, y: 0, scent: 1 }], wind: { x: 1, y: 0 }, scentDiffuse: 0.08 }, 14);
  const down = f.scent[idx(0.45, 0)], up = f.scent[idx(-0.45, 0)];
  ok(down > 0.05 && down > up * 5, `scent is carried downwind, not up (down ${down.toFixed(3)} >> up ${up.toFixed(3)})`);
}

// 4. NIGHT blinds SIGHT but not scent/sound -- the nocturnal hunt must use other senses
{
  const day = run({ light: 1, fog: 0, wind: { x: 1, y: 0 } }, 12).f;
  const night = run({ light: 0, fog: 0, wind: { x: 1, y: 0 } }, 12).f;
  ok(max(day.sight) > 0.3, 'by day, sight reaches the prey');
  ok(max(night.sight) < 0.02, 'at night, sight goes near-blind');
  ok(max(night.sound) > 0.3 && max(night.scent) > 0.1, 'but sound and scent still carry in the dark (the nocturnal channels)');
}

// 5. FOG blinds sight the same way (medium, not just darkness)
{
  const clear = run({ light: 1, fog: 0 }, 8).f, foggy = run({ light: 1, fog: 1 }, 8).f;
  ok(max(clear.sight) > 0.3 && max(foggy.sight) < 0.02, 'fog blinds sight though it is broad day');
}

// 6. SOUND out-ranges SIGHT (equal, clear medium) -- there are cells where you can HEAR prey you cannot SEE
{
  const { f } = run({ light: 1, fog: 0, sources: [{ x: 0, y: 0, sound: 1, vis: 1 }] }, 6);
  let heardNotSeen = 0;
  for (let k = 0; k < N * N; k++) if (f.sound[k] > 0.15 && f.sight[k] < 0.05) heardNotSeen++;
  ok(heardNotSeen > 40, `sound reaches farther than sight (${heardNotSeen} cells you can hear but not see)`);
}

// 7. HEAT is a SHORT-range sense -- a warm body betrays itself only up close (and it works in the dark)
{
  const { f } = run({ light: 0, sources: [{ x: 0, y: 0, heat: 1 }], heatRange: 0.28 }, 4);
  ok(f.heat[idx(0.08, 0)] > 0.05, 'heat is detectable close to the warm body');
  ok(f.heat[idx(0.7, 0)] === 0, 'heat is useless past its short range');
  ok(max(f.heat) > 0.05, 'heat works even at night (light-independent)');
}

// 8. THE INFORMATION GAME -- at night (no sight), the wind TILTS the hunt: a predator downwind catches the
// scent trail while one the same distance upwind gets only the faint, symmetric sound. The same prey is far
// more findable from one quarter than the other -- detection is a game the weather plays a hand in.
{
  const { f } = run({ light: 0, fog: 0, sources: [{ x: 0, y: 0, scent: 1.5, sound: 0.08, heat: 0, vis: 0 }], wind: { x: 1, y: 0 } }, 12);
  const downwind = f.detect[idx(0.25, 0)], upwind = f.detect[idx(-0.25, 0)];
  ok(downwind > 0.15, `a predator downwind in the dark catches the scent trail (${downwind.toFixed(3)})`);
  ok(downwind > upwind * 2, `the same prey, far more findable from downwind than up (${downwind.toFixed(3)} vs ${upwind.toFixed(3)}) -- the wind tilts the hunt`);
}

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