verbs/verbs-test.js
101 lines · in natura-src-1.0.0.zip as natura/verbs/verbs-test.js · a test
// verbs/verbs-test.js -- rich assertions for the shared LIFE ONTOLOGY VERBS (daynight / forage / flee + the router).
// These are framework code shared ACROSS PROJECTS, so they are TEST RICH: every chronotype, the local-space reads
// (shade + region phase), the sun adapters incl. the celestial path, forage + flee edge cases, purity/determinism,
// and the router assembly. Run: `node verbs/verbs-test.js` (exit 0 = all pass).
const V = require('./index');
const DN = require('./daynight'), FO = require('./forage'), FL = require('./flee');
let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log(' FAIL: ' + m); } };
const near = (a, b, e, m) => ok(Math.abs(a - b) <= (e == null ? 1e-9 : e), m + ' (' + a + ' vs ' + b + ')');
console.log('life-verbs assertions:');
// ---- the thin router assembles the small files, sharing (not copying) the functions ----
ok(typeof V.activity === 'function' && typeof V.atTod === 'function' && typeof V.forage === 'function' && typeof V.flee === 'function', 'router exposes activity/atTod/forage/flee');
ok(Array.isArray(V.CHRONOTYPES) && V.CHRONOTYPES.length === 7, 'router carries the 7 chronotypes');
ok(V.activity === DN.activity && V.forage === FO.forage && V.flee === FL.flee, 'router re-exports the SAME functions (no logic, no copies)');
// ---- daynight: invariants across every chronotype and sun elevation ----
for (const ct of V.CHRONOTYPES) {
for (const s of [-1, -0.5, 0, 0.5, 1]) {
const a = V.activity(s, ct);
ok(a.active >= 0 && a.active <= 1, ct + ' active in [0,1] at sun=' + s);
near(a.rest, 1 - a.active, 1e-12, ct + ' rest=1-active at sun=' + s);
}
}
ok(V.activity(0.3, 'diurnal').active === V.activity(0.3, 'diurnal').active, 'activity is pure/deterministic');
// ---- daynight: each chronotype peaks at the RIGHT sun ----
ok(V.activity(1, 'diurnal').active > 0.99 && V.activity(-1, 'diurnal').active < 0.01, 'diurnal: awake at noon, asleep at midnight');
ok(V.activity(-1, 'nocturnal').active > 0.99 && V.activity(1, 'nocturnal').active < 0.01, 'nocturnal: awake at midnight, asleep at noon');
ok(V.activity(0, 'crepuscular').active > 0.95 && V.activity(1, 'crepuscular').active < 0.05 && V.activity(-1, 'crepuscular').active < 0.05, 'crepuscular: peaks at twilight, dips at BOTH noon and midnight');
ok(V.activity(1, 'phototroph').active > 0.99 && V.activity(-1, 'phototroph').active < 0.01, 'phototroph: open/fixing by day, shut by night (a PLANT wears the same verb)');
ok(V.activity(1, 'cathemeral').active > 0.5 && V.activity(-1, 'cathemeral').active > 0.2 && V.activity(-1, 'cathemeral').active < 0.5, 'cathemeral: never fully rests, faint day preference');
ok(V.activity(1, 'nope').active > 0.99, 'unknown chronotype falls back to diurnal');
// ---- sun adapters: turn the world's sun signal into the [-1,1] elevation the verb reads ----
near(V.sunFromDayPhase(0), -1, 1e-9, 'dayPhase 0 (midnight) -> sun -1');
near(V.sunFromDayPhase(0.25), 0, 1e-9, 'dayPhase 0.25 (dawn) -> sun 0');
near(V.sunFromDayPhase(0.5), 1, 1e-9, 'dayPhase 0.5 (noon) -> sun +1');
near(V.sunFromDayPhase(0.75), 0, 1e-9, 'dayPhase 0.75 (dusk) -> sun 0');
ok(V.sunFromSinAlt(0.8) === 0.8 && V.sunFromSinAlt(2) === 1 && V.sunFromSinAlt(-2) === -1, 'sunFromSinAlt clamps sin(sunAltitude) to [-1,1] (the celestial path)');
ok(V.sunFromTod === V.sunFromDayPhase, 'sunFromTod is the dayPhase adapter (tod == dayPhase)');
near(V.activity(V.sunFromSinAlt(0.8), 'diurnal').active, V.activity(0.8, 'diurnal').active, 1e-12, 'celestial sun feeds activity the same as a raw elevation');
// ---- atTod: back-compat convenience + LOCAL SPACE (shade = canopy/depth, phase = region) ----
near(V.atTod(0, 'diurnal').rest, 1, 1e-9, 'atTod diurnal midnight rest ~1');
near(V.atTod(0.5, 'diurnal').rest, 0, 1e-9, 'atTod diurnal noon rest ~0');
ok(V.atTod(null, 'nocturnal').active === 1, 'no clock (tod null) -> awake/open for any chronotype');
near(V.atTod(0.4, 'diurnal').active, V.activity(V.sunFromDayPhase(0.4), 'diurnal').active, 1e-9, 'atTod == activity(sunFromDayPhase(tod))');
ok(V.atTod(0.30, 'diurnal', { shade: 0 }).active > V.atTod(0.30, 'diurnal', { shade: 0.45 }).active, 'SHADE delays the morning wake: a shaded floor wakes after the canopy');
ok(V.atTod(0.5, 'nocturnal', { shade: 0.3 }).rest > 0.6, 'SHADE is not night: a modest shade keeps a nocturnal thing asleep at noon');
ok(V.atTod(0.24, 'diurnal', { phase: 0.06 }).active > V.atTod(0.24, 'diurnal', { phase: -0.06 }).active, 'REGION phase: the east (+) wakes before the west (-)');
// ---- forage: move to the best local resource ----
ok(V.forage({ x: 0, y: 0 }, [], {}).target === null, 'forage of nothing -> null target (wander/rest)');
{ const r = V.forage({ x: 0.5, y: 0.5 }, [{ x: 0.9, y: 0.5, amount: 1 }, { x: 0.52, y: 0.5, amount: 0.6 }], { reach: 0.03 });
ok(r.target && Math.abs(r.target.x - 0.52) < 1e-9, 'forage: a near-enough source beats a fuller far one (richest-over-nearest)'); }
{ const r = V.forage({ x: 0.5, y: 0.5 }, [{ x: 0.9, y: 0.5, amount: 1 }], { reach: 0.03 });
ok(r.target && !r.atSource, 'forage: targets a far source but reports not-yet-AT it'); }
{ const r = V.forage({ x: 0.5, y: 0.5 }, [{ x: 0.515, y: 0.5, amount: 0.9 }], { reach: 0.03 });
ok(r.atSource, 'forage: reports atSource once within reach'); }
{ const r = V.forage({ x: 0.5, y: 0.5 }, [{ x: 0.5, y: 0.5, amount: 0.02 }], { min: 0.05 });
ok(r.target === null, 'forage: ignores a source below the min threshold (a spent flower)'); }
// ---- flee: move from the nearest threat (lifted out of ecosystem's inline flight) ----
{ const r = V.flee({ x: 0.5, y: 0.5 }, [], { near: 0.1 }); ok(!r.fleeing && !r.wary, 'flee from nothing -> calm'); }
{ const r = V.flee({ x: 0.5, y: 0.5 }, [{ x: 0.55, y: 0.5 }], { near: 0.1 });
ok(r.fleeing && r.fx < 0, 'flee a +x threat -> escape toward -x'); near(Math.sqrt(r.fx * r.fx + r.fy * r.fy), 1, 1e-9, 'flee escape vector is unit length'); }
{ const r = V.flee({ x: 0.5, y: 0.5 }, [{ x: 0.65, y: 0.5 }], { near: 0.1, perceive: 0.3 });
ok(r.wary && !r.fleeing, 'a threat within PERCEIVE but outside NEAR -> wary, not yet bolting'); }
{ const r = V.flee({ x: 0.5, y: 0.5 }, [{ x: 0.45, y: 0.5 }, { x: 0.55, y: 0.5 }], { near: 0.2 });
ok(r.fleeing && Math.abs(r.fx) < 1e-6, 'two symmetric threats -> pinned (their escape directions cancel)'); }
// ---- flee RAW mode + DELTA hook (the extensions that let the shared verb serve ecosystem's toroidal, distance-weighted flight) ----
{ const r = V.flee({ x: 0.5, y: 0.5 }, [{ x: 0.45, y: 0.5 }], { near: 0.1, raw: true });
ok(Math.abs(r.fx - 0.05) < 1e-9 && Math.abs(r.fy) < 1e-9, 'raw mode returns the UN-normalized displacement (fx=0.05, not a unit vector)'); }
{ const r = V.flee({ x: 0.5, y: 0.5 }, [{ x: 0.48, y: 0.5 }, { x: 0.42, y: 0.5 }], { near: 0.1, raw: true });
ok(Math.abs(r.fx - 0.10) < 1e-9, 'raw mode is DISTANCE-WEIGHTED: the farther-within-near threat pushes harder (0.02+0.08=0.10)'); }
{ const rU = V.flee({ x: 0.5, y: 0.5 }, [{ x: 0.48, y: 0.5 }], { near: 0.1 });
ok(Math.abs(Math.hypot(rU.fx, rU.fy) - 1) < 1e-9, 'default (unit) mode is UNCHANGED by the extension -- still a unit escape vector'); }
{ const td = (a, b) => { let d = a - b; if (d > 0.5) d -= 1; else if (d < -0.5) d += 1; return d; };
const tdelta = (p, t) => [td(p.x, t.x), td(p.y, t.y)];
const flat = V.flee({ x: 0.02, y: 0.5 }, [{ x: 0.98, y: 0.5 }], { near: 0.1 });
const torus = V.flee({ x: 0.02, y: 0.5 }, [{ x: 0.98, y: 0.5 }], { near: 0.1, raw: true, delta: tdelta });
ok(!flat.fleeing, 'euclidean metric: a threat across the seam looks FAR -> no flight');
ok(torus.fleeing && torus.fx > 0, 'delta hook: the toroidal metric sees it NEAR across the seam -> flees across the wrap (+x)'); }
{ // PARITY: raw + a toroidal delta reproduces ecosystem.js's inline herbivore flight EXACTLY -- de-risks the adoption.
const td = (a, b) => { let d = a - b; if (d > 0.5) d -= 1; else if (d < -0.5) d += 1; return d; };
const tdelta = (p, t) => [td(p.x, t.x), td(p.y, t.y)];
const agent = { x: 0.5, y: 0.5 }, preds = [{ x: 0.56, y: 0.53 }, { x: 0.95, y: 0.02 }, { x: 0.47, y: 0.44 }, { x: 0.65, y: 0.5 }];
const vis = 0.13, vigil = 1.5, nr = vis, pc = vis * vigil, n2 = nr * nr, p2 = pc * pc; // ecosystem uses squared thresholds; the verb squares nr/pc internally
let efx = 0, efy = 0, ewary = false; // hand-rolled replica of ecosystem's inline loop
for (const p of preds) { const dx = td(agent.x, p.x), dy = td(agent.y, p.y), d2 = dx * dx + dy * dy;
if (d2 < p2) { ewary = true; if (d2 < n2) { efx += dx; efy += dy; } } }
const r = V.flee(agent, preds, { near: nr, perceive: pc, raw: true, delta: tdelta });
ok(Math.abs(r.fx - efx) < 1e-12 && Math.abs(r.fy - efy) < 1e-12 && r.wary === ewary,
'raw+delta flee EXACTLY reproduces the ecosystem inline flight (fx ' + r.fx.toFixed(4) + '=' + efx.toFixed(4) + ', wary=' + r.wary + ')'); }
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);