mind.js
202 lines · in natura-src-1.0.0.zip as natura/mind.js · a module · its entry in the module reference · its test: mind-test.js
// mind.js -- Natura MIND (the ontology's MIND rung, previously untouched). The uncanny vein ONTOLOGY.md
// flagged: "the mind's model diverges from the world." A mind is not a sensor wired to reality; it holds
// its OWN MODEL (beliefs about nearby threat + food + a warm haven) that it updates only by PERCEIVING,
// only while it ATTENDS, coloured by EMOTION, and left to DECAY + DREAM while it sleeps. So the model comes
// apart from the world -- and that gap is NEUTRAL: the same physics is a curse or a grace depending on the
// emotion that drives it.
// FEAR conjures a threat that is not there and, past a threshold, TRAPS the mind believing it (a phantom;
// the disturbing pole). HOPE sustains a belief in a good place after the mind has left it -- it keeps
// homing to a hearth gone cold (a solace; the warm pole). One divergence, two colorings.
// The measurable emergence is that DIVERGENCE between what a mind believes and what is true: it grows with
// inattention + emotion, shrinks with waking attention. The map is not the territory, made to actually run.
//
// A Natura system (init/step/fields/events), deterministic + node-testable. Reads a world TRUTH it can only
// partially perceive (config.truth(x,y,ctx) -> {threat,food,warmth}; defaults to a roaming threat, patchy
// food, and a still hearth, so it stands alone, but in a coupled world truth() samples ctx.fields). Couples
// OUT: fields.mind.intent / fear / hope / arousal drive a behaviour/LIFE system (which verb a creature runs).
// UMD (node test / window.Mind).
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.Mind = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
const TAU = Math.PI * 2;
// intents (the decision output a behaviour system reads). FLEE is the dark pole; SEEK the warm one.
const REST = 0, FORAGE = 1, FLEE = 2, EXPLORE = 3, SEEK = 4;
// the default WORLD the minds live in when run alone. Two poles, both of which OUTLIVE perception so a
// belief can come apart from them: a threat that ROAMS (circles once per day, so a mind it has passed can
// still FEAR it), and a hearth that STAYS (a fixed warm place, so a mind that has wandered off can still
// HOME to it) -- over static patchy food. In a coupled world, pass config.truth to sample ctx.fields.
function defaultTruth(x, y, t) {
const ang = t * TAU / 86400; // the threat circles once per sim-day
const tx = 0.6 * Math.cos(ang), ty = 0.6 * Math.sin(ang);
const dT = (x - tx) * (x - tx) + (y - ty) * (y - ty);
const threat = Math.exp(-dT / 0.22); // strong near the roaming threat, fades with distance
const hx = -0.35, hy = 0.4; // the hearth: a still, warm place
const dH = (x - hx) * (x - hx) + (y - hy) * (y - hy);
const warmth = Math.exp(-dH / 0.30); // strong near the hearth, fades with distance
const food = 0.5 + 0.5 * Math.sin(x * 3.1 + 1.0) * Math.cos(y * 2.7 - 0.4); // patchy, static
return { threat: clamp(threat, 0, 1), food: clamp(food, 0, 1), warmth: clamp(warmth, 0, 1) };
}
// make a Mind system. config: { minds, truth(x,y,ctx), learn, forget, dream, fearBias, hopeBias, metab,
// eat, priorThreat, priorWarmth, sleepThresh, forceSleep }. All optional.
function makeMind(config) {
config = config || {};
const M = config.minds || 24;
const truth = config.truth || null; // (x,y,ctx) -> {threat,food,warmth}; else defaultTruth
const learn = config.learn != null ? config.learn : 0.9; // how fast attention corrects a belief (per hour)
const forget = config.forget != null ? config.forget : 0.12; // belief decay toward the prior when unattended (per hour)
const dream = config.dream != null ? config.dream : 0.20; // belief wander amplitude while asleep (per hour)
const dreamBias = config.dreamBias != null ? config.dreamBias : 0.3; // how strongly a sleeping mind DREAMS in the direction of its feeling -- nightmares of what it fears, reveries of what it hopes
const fearBias = config.fearBias != null ? config.fearBias : 0.5; // how much fear conjures threat into perception (the dark mirror)
const hopeBias = config.hopeBias != null ? config.hopeBias : 0.5; // how much hope sustains warmth into perception (the warm mirror)
const metab = config.metab != null ? config.metab : 0.05; // hunger accrual (per hour)
const eat = config.eat != null ? config.eat : 0.35; // hunger relief while foraging on believed food (per hour)
const priorThreat = config.priorThreat != null ? config.priorThreat : 0.15; // the neutral baseline a lapsed threat-belief decays to
const priorWarmth = config.priorWarmth != null ? config.priorWarmth : 0.25; // the baseline a lapsed warmth-belief decays to (a little hope is the resting state)
const sleepThresh = config.sleepThresh != null ? config.sleepThresh : 0.25; // arousal below this = asleep (attention ~0)
const forceSleep = !!config.forceSleep; // pin the minds asleep (a hibernation / anaesthesia knob; also for tests)
const social = config.society || null; // name of a SOCIETY system to conform toward (CONTAGION); null = no coupling, mind runs alone
const conform = config.conform != null ? config.conform : 0; // how strongly a mind conforms its belief to the mood around it (per hour)
const gShare = config.conformGlobal != null ? config.conformGlobal : 0.5; // share of the GLOBAL zeitgeist vs the LOCAL room in what it conforms to
return {
name: config.name || 'mind',
domain: 'mind',
scale: 'individual',
params: config,
init(rng, params, clock) {
const x = new Float32Array(M), y = new Float32Array(M);
const bThreat = new Float32Array(M), bFood = new Float32Array(M), bWarmth = new Float32Array(M);
const fear = new Float32Array(M), hope = new Float32Array(M), hunger = new Float32Array(M);
const content = new Float32Array(M), curiosity = new Float32Array(M);
const arousal = new Float32Array(M), intent = new Uint8Array(M), phase = new Float32Array(M), asleepPrev = new Uint8Array(M);
for (let i = 0; i < M; i++) {
x[i] = rng() * 2 - 1; y[i] = rng() * 2 - 1; // scattered across the world
bThreat[i] = priorThreat; bFood[i] = 0.4; bWarmth[i] = priorWarmth; // it begins with neutral priors, not the truth
hunger[i] = 0.3 * rng(); arousal[i] = 0.6 + 0.3 * rng();
phase[i] = rng() * TAU; // an individual circadian offset (owls vs larks)
}
return { M, x, y, bThreat, bFood, bWarmth, fear, hope, hunger, content, curiosity, arousal, intent, phase, asleepPrev, evs: [] };
},
step(st, ctx, dt) {
const hh = clamp(dt / 3600, 0, 6); if (hh <= 0) return st; // sim-hours this step (rate-integrate in hours)
const t = ctx.clock ? ctx.clock.t : 0;
const dayPh = ctx.clock && ctx.clock.dayPhase ? ctx.clock.dayPhase() : (t % 86400) / 86400; // 0..1 through the day
const rng = ctx.rng || Math.random;
const truthFn = truth || ((x, y) => defaultTruth(x, y, t));
const s = st; s.evs.length = 0;
for (let i = 0; i < M; i++) {
const T = truthFn(s.x[i], s.y[i], ctx) || { threat: 0, food: 0, warmth: 0 };
const tThreat = clamp(T.threat, 0, 1), tFood = clamp(T.food, 0, 1), tWarmth = clamp(T.warmth != null ? T.warmth : 0, 0, 1);
const att = forceSleep ? 0 : clamp(s.arousal[i], 0, 1); // ATTENTION = how awake it is; asleep -> perceives nothing
// --- PERCEPTION (only while attending) + EMOTION-DISTORTION + NOISE ---
// The two poles are mirror images. Fear conjures threat (quadratic -> panic sees threats everywhere);
// hope conjures warmth (a longing sees the good place everywhere). Inattention adds noise + dims signal.
const sensedThreat = tThreat * (0.45 + 0.55 * att);
const conjuredThreat = fearBias * s.fear[i] * s.fear[i];
const pThreat = clamp(sensedThreat + conjuredThreat + (rng() - 0.5) * 0.30 * (1 - att), 0, 1);
const sensedWarmth = tWarmth * (0.45 + 0.55 * att);
const conjuredWarmth = hopeBias * s.hope[i] * s.hope[i];
const pWarmth = clamp(sensedWarmth + conjuredWarmth + (rng() - 0.5) * 0.24 * (1 - att), 0, 1);
const pFood = clamp(tFood * (0.45 + 0.55 * att) + (rng() - 0.5) * 0.20 * (1 - att), 0, 1);
// belief tracks perception in proportion to attention; ALWAYS forgets slowly toward the prior; DREAMS when asleep.
// ...and DREAMS while unattended: belief drifts toward the world its FEELING imagines (a nightmare of
// the feared, a reverie of the hoped), on top of the aimless wander. A sleeping mind makes its own threat.
const bThreatOld = s.bThreat[i]; // remembered, to catch the moment a dread lifts (relief, below)
// only a GENUINELY afraid/hopeful sleeper dreams (fear/hope past a threshold), so a calm mind at its
// baseline prior just drifts -- the nightmare belongs to the mind that carried the feeling into sleep.
s.bThreat[i] += (learn * att * (pThreat - s.bThreat[i]) + forget * (priorThreat - s.bThreat[i])) * hh + (dream * (rng() - 0.5) + dreamBias * Math.max(0, s.fear[i] - 0.35)) * (1 - att) * hh;
s.bWarmth[i] += (learn * att * (pWarmth - s.bWarmth[i]) + forget * (priorWarmth - s.bWarmth[i])) * hh + (dream * (rng() - 0.5) + dreamBias * Math.max(0, s.hope[i] - 0.35)) * (1 - att) * hh;
s.bFood[i] += (learn * att * (pFood - s.bFood[i]) + forget * (0.4 - s.bFood[i])) * hh + dream * (1 - att) * (rng() - 0.5) * hh;
// CONTAGION (only when coupled to a SOCIETY): a mind comes to believe what those around it feel --
// swayed partly by the local room (fearField / hopeField at its cell) and partly by the global
// zeitgeist (the society's mean mood), gated by attention. This is how a fear born in one corner
// becomes a shared conviction: each mind's belief props up its neighbours', consensus without truth.
if (social && conform > 0) {
const soc = ctx.fields ? ctx.fields[social] : null;
if (soc && soc.N) {
const n = soc.N, ci = clamp(Math.floor((s.x[i] + 1) / 2 * n), 0, n - 1), cj = clamp(Math.floor((s.y[i] + 1) / 2 * n), 0, n - 1), idx = cj * n + ci;
const tgtF = soc.fearField[idx] * (1 - gShare) + soc.meanFear * gShare;
const tgtH = soc.hopeField[idx] * (1 - gShare) + soc.meanHope * gShare;
s.bThreat[i] += conform * att * (tgtF - s.bThreat[i]) * hh;
s.bWarmth[i] += conform * att * (tgtH - s.bWarmth[i]) * hh;
}
}
s.bThreat[i] = clamp(s.bThreat[i], 0, 1); s.bWarmth[i] = clamp(s.bWarmth[i], 0, 1); s.bFood[i] = clamp(s.bFood[i], 0, 1);
// --- EMOTION (arises from BELIEF, not truth -- it feels what it believes) ---
const panic = s.fear[i] > 0.6 ? 0.6 * (s.fear[i] - 0.6) : 0; // fear begets fear
s.fear[i] = clamp(s.fear[i] + (1.5 * (s.bThreat[i] - s.fear[i]) + panic) * hh, 0, 1);
const solaceGain = s.hope[i] > 0.58 ? 0.5 * (s.hope[i] - 0.58) : 0; // hope warms itself, the warm twin of panic
s.hope[i] = clamp(s.hope[i] + (1.35 * (s.bWarmth[i] - s.hope[i]) + solaceGain) * hh, 0, 1);
const foraging = s.intent[i] === FORAGE;
s.hunger[i] = clamp(s.hunger[i] + (metab - (foraging ? eat * s.bFood[i] : 0)) * hh, 0, 1);
// two different positive states: CONTENT is "all is well HERE now" (safe + fed -> rest); HOPE is
// "good is OUT THERE" (a believed haven -> yearning, homing). Keeping them apart is what lets a
// hungry mind stay awake and hope, instead of a warm belief simply lulling it to sleep.
s.content[i] = clamp(s.content[i] + 0.5 * ((1 - s.bThreat[i]) * (1 - s.hunger[i]) - s.content[i]) * hh, 0, 1);
s.curiosity[i] = clamp(s.curiosity[i] + 0.3 * ((1 - s.fear[i]) * (1 - s.hunger[i]) * (1 - 0.5 * s.content[i]) - s.curiosity[i]) * hh, 0, 1);
// --- AROUSAL / the sleep-wake cycle (fear + hunger keep it up, contentment lulls it; a circadian rhythm underneath) ---
const circadian = 0.5 + 0.5 * Math.sin(dayPh * TAU + s.phase[i]); // awake by its own day
const target = clamp(0.15 + 0.95 * s.fear[i] + 0.55 * s.hunger[i] + 0.45 * circadian - 0.6 * s.content[i], 0, 1);
s.arousal[i] = clamp(s.arousal[i] + 1.2 * (target - s.arousal[i]) * hh, 0, 1);
const asleep = forceSleep || s.arousal[i] < sleepThresh;
// --- DECISION: the dominant drive picks the intent (which LIFE/behaviour verb runs) ---
// survival first (flee, then feed), then the warm pull to home, then idle curiosity, else rest.
let intent = REST;
if (asleep) intent = REST;
else if (s.fear[i] > 0.5) intent = FLEE;
else if (s.hunger[i] > 0.5) intent = FORAGE;
else if (s.hope[i] > 0.5) intent = SEEK; // home toward the believed haven
else if (s.curiosity[i] > 0.4) intent = EXPLORE;
s.intent[i] = intent;
// --- EVENTS: the uncanny moments, both poles ---
const wasAsleep = s.asleepPrev[i];
if (asleep && !wasAsleep) s.evs.push({ kind: 'sleeping', mind: i });
else if (!asleep && wasAsleep) s.evs.push({ kind: 'waking', mind: i });
s.asleepPrev[i] = asleep ? 1 : 0;
if (intent === FLEE && tThreat < 0.2) s.evs.push({ kind: 'startle', mind: i, phantom: true }); // fleeing a threat that is NOT there (the dark phantom)
if (intent === SEEK && tWarmth < 0.2) s.evs.push({ kind: 'solace', mind: i, phantom: true }); // homing to a hearth that is NOT there (the warm phantom)
// RELIEF -- the map re-fitting the world: a mind that BELIEVED a threat, now awake and seeing nothing
// there, lets the dread fall past the line. The ghost dissolves -- the grace that answers the phantom.
if (att > 0.5 && bThreatOld > 0.5 && s.bThreat[i] <= 0.5 && tThreat < 0.2) s.evs.push({ kind: 'relief', mind: i });
if (Math.abs(s.bThreat[i] - tThreat) > 0.5) s.evs.push({ kind: 'falseBelief', mind: i, of: 'threat', believes: s.bThreat[i], truth: tThreat });
if (Math.abs(s.bWarmth[i] - tWarmth) > 0.5) s.evs.push({ kind: 'falseBelief', mind: i, of: 'warmth', believes: s.bWarmth[i], truth: tWarmth });
}
return s;
},
fields(st) {
// beliefs + emotions + the decision, plus population summaries. The reader computes DIVERGENCE
// (the map vs the territory) against its own truth, for either pole -- see mind-test.js.
let asleep = 0, afraid = 0, hopeful = 0, content = 0, dreaming = 0;
for (let i = 0; i < st.M; i++) {
if (st.arousal[i] < 0.25) { asleep++; if (st.bThreat[i] > priorThreat + 0.18 || st.bWarmth[i] > priorWarmth + 0.18) dreaming++; } // asleep + still vividly believing = a dream
if (st.fear[i] > 0.5) afraid++; if (st.hope[i] > 0.5) hopeful++; if (st.content[i] > 0.5) content++;
}
return {
M: st.M, x: st.x, y: st.y,
bThreat: st.bThreat, bFood: st.bFood, bWarmth: st.bWarmth,
fear: st.fear, hope: st.hope, hunger: st.hunger, content: st.content, curiosity: st.curiosity,
arousal: st.arousal, intent: st.intent,
fracAsleep: asleep / st.M, fracAfraid: afraid / st.M, fracHopeful: hopeful / st.M, fracContent: content / st.M, fracDreaming: dreaming / st.M
};
},
events(st) { return st.evs; }
};
}
return { makeMind, defaultTruth, INTENT: { REST, FORAGE, FLEE, EXPLORE, SEEK } };
});