mind-test.js
156 lines · in natura-src-1.0.0.zip as natura/mind-test.js · a test
// mind-test.js -- assertions for the MIND system. Determinism, boundedness, and the EMERGENCE proofs that
// the model is real: a mind's belief tracks the world when it attends, DIVERGES when it does not, and is
// DISTORTED by fear (a self-sustaining phantom). "The map is not the territory," made to run.
const N = require('./natura.js');
const { makeMind, INTENT } = require('./mind.js');
const { World, UNITS } = N;
let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log(' FAIL: ' + m); } };
function run(seed, cfg, steps, dt) {
dt = dt || UNITS.HOUR;
const w = World({ seed, systems: [makeMind(cfg)] });
for (let i = 0; i < steps; i++) w.step(dt);
return w;
}
function meanDiv(f, truthThreat) { let s = 0; for (let i = 0; i < f.M; i++) s += Math.abs(f.bThreat[i] - truthThreat); return s / f.M; }
function mean(a) { let s = 0; for (let i = 0; i < a.length; i++) s += a[i]; return s / a.length; }
console.log('mind assertions:');
// 1. DETERMINISM -- same seed + dt sequence -> identical minds
{
const a = run(3, { minds: 30 }, 60).fields().mind, b = run(3, { minds: 30 }, 60).fields().mind;
let same = true; for (let i = 0; i < a.M; i++) if (a.bThreat[i] !== b.bThreat[i] || a.fear[i] !== b.fear[i] || a.arousal[i] !== b.arousal[i]) { same = false; break; }
ok(same, 'same seed -> identical minds (reproducible)');
}
// 2. BOUNDED + FINITE -- every belief/emotion/arousal stays in [0,1], no NaN, over a long varied run
{
const f = run(5, { minds: 40 }, 300).fields().mind;
let good = true;
for (const arr of [f.bThreat, f.bFood, f.fear, f.hunger, f.content, f.curiosity, f.arousal])
for (let i = 0; i < f.M; i++) { const v = arr[i]; if (!isFinite(v) || v < -1e-6 || v > 1 + 1e-6) { good = false; break; } }
ok(good, 'all beliefs + emotions + arousal stay finite and in [0,1]');
let intentOk = true; for (let i = 0; i < f.M; i++) if (f.intent[i] < 0 || f.intent[i] > 4) intentOk = false;
ok(intentOk, 'every intent is a valid decision (REST/FORAGE/FLEE/EXPLORE/SEEK)');
let warmOk = true; for (let i = 0; i < f.M; i++) { const v = f.bWarmth[i], h = f.hope[i]; if (!isFinite(v) || v < -1e-6 || v > 1 + 1e-6 || !isFinite(h) || h < -1e-6 || h > 1 + 1e-6) warmOk = false; }
ok(warmOk, 'the warm pole (warmth-belief + hope) stays finite and in [0,1] too');
}
// 3. ATTENTION TRACKS TRUTH -- awake, undistorted (fearBias 0), a mind's belief converges on the real threat
{
const f = run(7, { minds: 50, fearBias: 0, truth: () => ({ threat: 0.6, food: 0.5 }) }, 80).fields().mind;
const d = meanDiv(f, 0.6);
ok(d < 0.18, 'an attending mind\'s belief tracks the truth (mean divergence ' + d.toFixed(3) + ' from a true 0.6)');
}
// 4. THE MAP DIVERGES -- a mind that does not attend (asleep) holds a belief detached from the world.
// Same high-threat world; the awake mind learns it, the asleep mind never does. The core uncanny emergence.
{
const cfg = { minds: 50, truth: () => ({ threat: 0.9, food: 0.4 }) };
const awake = run(9, cfg, 90).fields().mind;
const asleep = run(9, Object.assign({ forceSleep: true }, cfg), 90).fields().mind;
const dAwake = meanDiv(awake, 0.9), dAsleep = meanDiv(asleep, 0.9);
ok(dAwake < 0.25, 'the WAKING mind closes on the world (divergence ' + dAwake.toFixed(3) + ')');
ok(dAsleep > 0.5, 'the SLEEPING mind\'s model is detached from the world (divergence ' + dAsleep.toFixed(3) + ')');
ok(dAsleep > dAwake + 0.35, 'attention is what binds the model to the world (' + dAwake.toFixed(2) + ' awake vs ' + dAsleep.toFixed(2) + ' asleep)');
}
// 5. FEAR DISTORTS -- after a threat is GONE, a strongly fear-prone mind stays GRIPPED by a phantom (the
// fear conjures the threat it fears -> a self-sustaining loop, above a threshold: trauma). An undistorted
// mind lets the belief fall to the truth. Emotion warps the model; past a threshold it TRAPS it.
{
const truth = (x, y, ctx) => ({ threat: ctx.clock.t < 20 * 3600 ? 0.85 : 0.0, food: 0.5 }); // threat for 20h, then gone
const fearful = run(11, { minds: 60, fearBias: 1.3, truth }, 40).fields().mind; // 40h: 20 with threat, 20 long after it left
const clear = run(11, { minds: 60, fearBias: 0.0, truth }, 40).fields().mind;
const bFearful = mean(fearful.bThreat), bClear = mean(clear.bThreat);
ok(bClear < 0.25, 'the undistorted mind\'s belief falls with the vanished threat (' + bClear.toFixed(3) + ')');
ok(bFearful > 0.5, 'a fear-gripped mind stays trapped believing a threat long gone -- a self-sustaining PHANTOM (' + bFearful.toFixed(2) + ')');
ok(bFearful > bClear + 0.3, 'FEAR warps the model away from the world (' + bFearful.toFixed(2) + ' fearful vs ' + bClear.toFixed(2) + ' clear)');
}
// 6. SELF-REGULATION -- a threatening world keeps minds AWAKE (fear rouses); a safe, fed world lets them sleep.
{
const threatened = run(13, { minds: 50, truth: () => ({ threat: 0.8, food: 0.5 }) }, 60).fields().mind;
const safe = run(13, { minds: 50, truth: () => ({ threat: 0.0, food: 0.95 }) }, 60).fields().mind;
ok(mean(threatened.arousal) > mean(safe.arousal), 'threat keeps minds awake, safety lets them rest (arousal ' + mean(threatened.arousal).toFixed(2) + ' vs ' + mean(safe.arousal).toFixed(2) + ')');
}
// 7. EVENTS -- the uncanny moments surface: fleeing a threat that is not there (a phantom startle)
{
// a threat pulse then gone, with strong fear-distortion -> phantom flees after it leaves
const truth = (x, y, ctx) => ({ threat: ctx.clock.t < 12 * 3600 ? 0.9 : 0.0, food: 0.5 });
const w = World({ seed: 17, systems: [makeMind({ minds: 60, fearBias: 0.9, truth })] });
let phantoms = 0, falseBeliefs = 0;
for (let i = 0; i < 40; i++) { w.step(UNITS.HOUR); for (const e of (w.events() || [])) { if (e.kind === 'startle' && e.phantom) phantoms++; if (e.kind === 'falseBelief') falseBeliefs++; } }
ok(phantoms > 0, 'a frightened mind FLEES a threat that is not there (' + phantoms + ' phantom startles)');
ok(falseBeliefs > 0, 'false beliefs are surfaced as events (' + falseBeliefs + ')');
}
// 8. THE WARM MIRROR -- hope sustains a good place after it is gone (the grace-phantom: a mind keeps
// homing to a hearth gone cold). The exact mirror of the fear-trap in test 5, and the warm mode the
// piece needs so the experience is not rooted only in the disturbing.
{
// lean food keeps the minds awake and yearning (a warm belief alone would just lull them to sleep) -- a
// hungry creature homing across the cold toward a remembered hearth.
const truth = (x, y, ctx) => ({ threat: 0.0, food: 0.15, warmth: ctx.clock.t < 20 * 3600 ? 0.85 : 0.0 }); // a hearth for 20h, then gone cold
const hopeful = run(11, { minds: 60, hopeBias: 1.5, truth }, 40).fields().mind; // 40h: 20 warmed, 20 long after it went out
const hopeless = run(11, { minds: 60, hopeBias: 0.0, truth }, 40).fields().mind;
const wHopeful = mean(hopeful.bWarmth), wHopeless = mean(hopeless.bWarmth);
ok(wHopeless < 0.35, 'the undistorted mind lets the warmth-belief fade with the cold hearth (' + wHopeless.toFixed(3) + ')');
ok(wHopeful > 0.5, 'a hopeful mind keeps believing in the good place long after it is gone -- a warm PHANTOM, solace (' + wHopeful.toFixed(2) + ')');
ok(wHopeful > wHopeless + 0.3, 'HOPE sustains the model of a good that has left the world (' + wHopeful.toFixed(2) + ' hopeful vs ' + wHopeless.toFixed(2) + ' hopeless)');
}
// 9. THE WARM PHANTOM SURFACES AS AN EVENT -- a mind at peace homes (SEEK) to a hearth that is no longer
// there. The gentle twin of the phantom startle.
{
const truth = (x, y, ctx) => ({ threat: 0.0, food: 0.15, warmth: ctx.clock.t < 12 * 3600 ? 0.9 : 0.0 });
const w = World({ seed: 17, systems: [makeMind({ minds: 60, hopeBias: 1.2, truth })] });
let solaces = 0; for (let i = 0; i < 40; i++) { w.step(UNITS.HOUR); for (const e of (w.events() || [])) if (e.kind === 'solace' && e.phantom) solaces++; }
ok(solaces > 0, 'a hopeful mind homes toward a hearth that is not there (' + solaces + ' solace phantoms)');
}
// 10. BALANCE -- one world holding BOTH a roaming threat and a still hearth grows BOTH poles at once: some
// minds afraid and fleeing, others hopeful and homing. The population is not monolithic; the piece can
// run either mode because the model carries both, and their contrast sharpens each.
{
const w = World({ seed: 23, systems: [makeMind({ minds: 80 })] }); // defaultTruth: roaming threat + fixed hearth + patchy food
const seen = new Set();
for (let i = 0; i < 40; i++) { w.step(UNITS.HOUR); const f = w.fields().mind; for (let m = 0; m < f.M; m++) seen.add(f.intent[m]); }
const f = w.fields().mind;
ok(f.fracAfraid > 0 && f.fracHopeful > 0, 'fear AND hope coexist in one world (afraid ' + f.fracAfraid.toFixed(2) + ', hopeful ' + f.fracHopeful.toFixed(2) + ')');
ok(seen.has(INTENT.FLEE) && seen.has(INTENT.SEEK), 'some minds FLEE the dark while others SEEK the warm -- both modes live at once');
}
// 11. DREAMING -- a mind that falls asleep still afraid DREAMS the threat and keeps believing it, even with
// nothing in the world; without the dream that belief would just fade. The sleeping mind sustains its own
// dread. (White-box: the same afraid-sleeper, dreaming vs not, aimless wander off, no real threat.)
{
const mk = (db) => {
const sys = makeMind({ minds: 1, dreamBias: db, forceSleep: true, truth: () => ({ threat: 0, food: 0.5, warmth: 0 }) });
const rng = () => 0.5, st = sys.init(rng);
st.fear[0] = 0.6; st.bThreat[0] = 0.6; // it fell asleep afraid, belief + feeling aligned
const ctx = { clock: { t: 0, dayPhase: () => 0.5 }, rng, fields: {} };
for (let k = 0; k < 8; k++) sys.step(st, ctx, 3600);
return st.bThreat[0];
};
const dreamer = mk(0.5), noDream = mk(0.0);
ok(dreamer > 0.4 && dreamer > noDream + 0.2, 'a mind that sleeps still afraid DREAMS the threat and keeps believing it -- a nightmare that would otherwise have faded (dreaming ' + dreamer.toFixed(2) + ' vs no-dream ' + noDream.toFixed(2) + ')');
}
// 12. RELIEF -- the grace that answers the phantom: a mind awake, believing a threat that is not there,
// SEES clearly and lets the dread fall past the line -- the map re-fitting the world, the ghost dissolving.
{
const sys = makeMind({ minds: 1, truth: () => ({ threat: 0, food: 0.5, warmth: 0 }) });
const rng = () => 0.5, st = sys.init(rng);
st.bThreat[0] = 0.75; st.fear[0] = 0.7; st.arousal[0] = 0.8; // awake, gripped by a threat that is not really there
const ctx = { clock: { t: 0, dayPhase: () => 0.5 }, rng, fields: {} };
let relief = false; for (let k = 0; k < 12; k++) { sys.step(st, ctx, 3600); for (const e of sys.events(st)) if (e.kind === 'relief') relief = true; }
ok(relief && st.bThreat[0] < 0.4, 'a mind awake sees the threat is gone and its dread FALLS -- relief, the ghost dissolving (belief now ' + st.bThreat[0].toFixed(2) + ')');
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);