society-test.js
167 lines · in natura-src-1.0.0.zip as natura/society-test.js · a test
// society-test.js -- the SOCIETY system reading the MIND system beneath it. Determinism, boundedness, and
// the collective-observable emergences: a shared mood FORMS over a crowd, and CONSENSUS reflects whether the
// population truly agrees. (The contagion / collective-phantom emergence -- minds conforming to the mood and
// a false belief spreading to consensus -- is proven in mind-test.js once the coupling is wired.)
const N = require('./natura.js');
const { makeMind } = require('./mind.js');
const { makeSociety } = require('./society.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, mindCfg, steps) {
const w = World({ seed, systems: [makeMind(mindCfg), makeSociety({ mind: 'mind' })] });
let evs = [];
for (let i = 0; i < (steps || 50); i++) { w.step(UNITS.HOUR); for (const e of w.events()) if (e.kind === 'panic' || e.kind === 'rally' || e.kind === 'schism') evs.push(e); }
return { soc: w.fields().society, evs };
}
console.log('society assertions:');
// 1. DETERMINISM -- the collective is reproducible when the minds beneath it are
{
const a = run(3, { minds: 40 }, 40).soc, b = run(3, { minds: 40 }, 40).soc;
let same = a.meanFear === b.meanFear && a.meanHope === b.meanHope && a.consensus === b.consensus;
for (let n = 0; n < a.fearField.length && same; n++) if (a.fearField[n] !== b.fearField[n]) same = false;
ok(same, 'same seed -> identical society (the mood field + observables reproduce)');
}
// 2. BOUNDED + FINITE -- the mood field and every observable stay in [0,1]
{
const s = run(5, { minds: 50 }, 120).soc;
let good = isFinite(s.meanFear) && isFinite(s.meanHope) && s.consensus >= -1e-6 && s.consensus <= 1 + 1e-6;
for (const F of [s.fearField, s.hopeField]) for (let n = 0; n < F.length; n++) { const v = F[n]; if (!isFinite(v) || v < -1e-6 || v > 1 + 1e-6) good = false; }
ok(good, 'the mood field + consensus stay finite and in [0,1]');
}
// 3. A COLLECTIVE FEAR FORMS -- a threatening world grows a shared dread, and a panic grips the whole
{
const r = run(7, { minds: 60, truth: () => ({ threat: 0.8, food: 0.5, warmth: 0 }) }, 50);
ok(r.soc.meanFear > 0.4 && r.soc.dominant === 'fear', 'a threatening world grows a COLLECTIVE fear (meanFear ' + r.soc.meanFear.toFixed(2) + ', dominant ' + r.soc.dominant + ')');
ok(r.evs.some(e => e.kind === 'panic'), 'a PANIC event grips the whole population (' + r.evs.filter(e => e.kind === 'panic').length + ' fired)');
}
// 4. A COLLECTIVE HOPE FORMS -- a warm, lean world (awake + warmed) grows a shared hope instead
{
const r = run(9, { minds: 60, hopeBias: 1.0, truth: () => ({ threat: 0, food: 0.15, warmth: 0.8 }) }, 50);
ok(r.soc.meanHope > r.soc.meanFear && r.soc.dominant === 'hope', 'a warm world grows a COLLECTIVE hope (meanHope ' + r.soc.meanHope.toFixed(2) + ' > meanFear ' + r.soc.meanFear.toFixed(2) + ', dominant ' + r.soc.dominant + ')');
}
// 5. CONSENSUS REFLECTS REAL ALIGNMENT -- a UNIFORM world is of one mind; a DIVIDED world (danger on one
// side, warmth on the other) is not. The society's consensus tracks whether the population truly agrees.
{
const uniform = run(11, { minds: 80, truth: () => ({ threat: 0.8, food: 0.5, warmth: 0 }) }, 50).soc;
const divided = run(11, { minds: 80, hopeBias: 1.0, truth: (x) => x < 0 ? { threat: 0.85, food: 0.5, warmth: 0 } : { threat: 0, food: 0.15, warmth: 0.85 } }, 50).soc;
ok(uniform.consensus > divided.consensus + 0.15, 'consensus is high in a world of one mind, low in a divided one (' + uniform.consensus.toFixed(2) + ' uniform vs ' + divided.consensus.toFixed(2) + ' divided)');
}
// 6. THE COLLECTIVE PHANTOM + THE TIPPING POINT (the SOCIETY-scale uncanny) -- a brief scare met by only a
// ~10% MINORITY in one corner, then gone, either fizzles or sweeps the WHOLE population into a shared
// conviction, depending on whether contagion crosses a critical strength. It is a phase transition (the
// threshold model of collective behaviour): below critical, the fear stays local and fades; above it, each
// mind's fear props up its neighbours' and the society converges on a total belief in a danger that never
// was -- consensus without truth, the map diverging together. That a tenth of a population can convince
// everyone of a phantom is the point.
{
const scare = (x, y, ctx) => ({ threat: (x < -0.35 && y < -0.35 && ctx.clock.t < 20 * 3600) ? 0.9 : 0.0, food: 0.4, warmth: 0 }); // ~10% of minds, first 20h, then GONE
function runC(conform) {
const w = World({ seed: 4, systems: [makeMind({ minds: 100, society: 'society', conform, conformGlobal: 0.6, fearBias: 0.8, truth: scare }), makeSociety({ mind: 'mind', hierarchy: false })] }); // pure contagion, no leaders (the hierarchy's minority-leads effect is tested separately, #8)
let panic = false; for (let i = 0; i < 46; i++) { w.step(UNITS.HOUR); for (const e of w.events()) if (e.kind === 'panic') panic = true; }
return { soc: w.fields().society, panic };
}
const spread = runC(2.0), isolated = runC(0.6); // 2.0 is supercritical, 0.6 subcritical; true threat is 0 by the end
ok(isolated.soc.meanDread < 0.3, 'below the critical contagion the scare stays local and FIZZLES (meanDread ' + isolated.soc.meanDread.toFixed(2) + ')');
ok(spread.soc.meanDread > 0.6, 'above it a ~10% scare SWEEPS the whole into believing a danger that never was -- a COLLECTIVE PHANTOM (meanDread ' + spread.soc.meanDread.toFixed(2) + ' while the true threat is 0)');
ok(spread.soc.meanDread > isolated.soc.meanDread + 0.4, 'the same seed either fizzles or engulfs -- a TIPPING POINT (' + spread.soc.meanDread.toFixed(2) + ' supercritical vs ' + isolated.soc.meanDread.toFixed(2) + ' subcritical)');
ok(spread.soc.consensus > 0.8 && spread.panic, 'the engulfed society reaches near-total consensus on the phantom, a PANIC (consensus ' + spread.soc.consensus.toFixed(2) + ')');
}
// 7. COLLECTIVE MEMORY -- "the country that remembers itself" (the CULTURE rung). A society keeps a slow,
// decaying record of its own past feeling: sustained collective FEAR scars the culture, sustained
// collective HOPE builds a legacy, and that memory colours the air the minds breathe -- so HISTORY shapes
// the present reaction. Verified the balance-trap way, by RUNNING identical fresh scares under different
// histories (never by reasoning): a scarred people fear the next shadow HARDER, a people with a legacy of
// hope answer it SOFTER, and a long rest HEALS the scar back toward baseline. The loop runs THROUGH the
// minds (accumulation + events key off the RAW mind feeling, not the memory), so it self-regulates.
{
// run: pre-condition the society (a threat that scars, a warmth that heartens, or nothing), a lull, then an
// IDENTICAL moderate fresh scare -- and measure the RAW collective fear it evokes (the mechanism, not the
// memory constant). Sub-saturating params so a scarred society reads clearly higher WITHOUT pegging at 1.
function runMem(o) {
const preH = 8 * 24, scareStart = preH + o.lullH, scareEnd = scareStart + 48;
const truth = (x, y, ctx) => {
const th = ctx.clock.t / 3600;
if (th < preH) return { threat: o.preThreat || 0, food: 0.4, warmth: o.preWarmth || 0 }; // the history
if (th < scareStart) return { threat: 0, food: 0.4, warmth: 0 }; // a lull (may heal)
if (th < scareEnd) return { threat: 0.3, food: 0.4, warmth: 0 }; // the identical fresh scare
return { threat: 0, food: 0.4, warmth: 0 };
};
const w = World({ seed: o.seed, systems: [
makeMind({ minds: 80, society: 'society', conform: 0.35, conformGlobal: 0.6, fearBias: 0.35, hopeBias: 0.2, truth }),
makeSociety({ mind: 'mind', memory: o.memory !== false })
] });
let peak = 0, scarAtScare = 0, legAtScare = 0;
for (let h = 0; h < scareEnd + 6; h++) {
w.step(UNITS.HOUR); const s = w.fields().society; const th = h + 1;
if (th === scareStart) { scarAtScare = s.scar; legAtScare = s.legacy; }
if (th >= scareStart && th <= scareEnd) peak = Math.max(peak, s.meanFearRaw); // RAW collective fear (through the minds)
}
const s = w.fields().society;
return { peak, scarAtScare, legAtScare, scar: s.scar, legacy: s.legacy };
}
const naive = runMem({ seed: 4, lullH: 24, preThreat: 0 }); // no history
const scarred = runMem({ seed: 4, lullH: 24, preThreat: 0.7 }); // a history of collective fear
const rested = runMem({ seed: 4, lullH: 60 * 24, preThreat: 0.7 }); // ...then a long rest
const legacy = runMem({ seed: 4, lullH: 24, preWarmth: 0.9 }); // a history of collective hope
const off = runMem({ seed: 4, lullH: 24, preThreat: 0.7, memory: false }); // the memory rung disabled
ok(scarred.scarAtScare > 0.6 && legacy.legAtScare > 0.6, 'the reservoirs FILL -- sustained collective fear scars the culture, sustained hope builds a legacy (scar ' + scarred.scarAtScare.toFixed(2) + ', legacy ' + legacy.legAtScare.toFixed(2) + ')');
ok(scarred.peak > naive.peak + 0.1, 'a SCARRED people fear the next shadow HARDER than a naive one -- history amplifies (raw fear ' + naive.peak.toFixed(2) + ' naive -> ' + scarred.peak.toFixed(2) + ' scarred, same scare)');
ok(legacy.peak < naive.peak - 0.03, 'a people with a LEGACY OF HOPE answer the same shadow SOFTER -- a culture steadies its own (raw fear ' + legacy.peak.toFixed(2) + ' vs ' + naive.peak.toFixed(2) + ' naive)');
ok(rested.peak < scarred.peak - 0.05 && rested.scarAtScare < scarred.scarAtScare * 0.4, 'a long REST HEALS the scar -- the reaction falls back toward baseline as the memory decays (' + scarred.peak.toFixed(2) + ' immediate -> ' + rested.peak.toFixed(2) + ' rested; scar ' + scarred.scarAtScare.toFixed(2) + ' -> ' + rested.scarAtScare.toFixed(2) + ')');
ok(off.scar === 0 && off.legacy === 0, 'memory OFF -> a clean toggle: no scar, no legacy accrues (the atmosphere rung stands alone)');
console.log(' memory: naive fear ' + naive.peak.toFixed(2) + ' | scarred ' + scarred.peak.toFixed(2) + ' | rested ' + rested.peak.toFixed(2) + ' | legacy ' + legacy.peak.toFixed(2));
}
// 8. HIERARCHY / INFLUENCE -- an emergent pecking order (the SOCIETY domain's leading/following verb). Not
// everyone shapes the mood equally: a mind that is awake and holds a strong, clear feeling is SALIENT and
// heard, and the more it is heeded the more its salience compounds (a replicator on a fixed budget of
// attention). Verified the balance-trap way, by RUNNING (never reasoning): a pecking order EMERGES from a
// flat crowd, the LEADERS are the salient, a committed salient MINORITY leads the whole where headcount
// would not, and -- the synthesis with the contagion rung -- INFLUENCE LOWERS THE TIPPING POINT.
{
function runU(hierarchy, seed) { // uniform threat: salience differs only by circadian phase + noise
const truth = () => ({ threat: 0.5, food: 0.4, warmth: 0 });
const w = World({ seed, systems: [ makeMind({ minds: 100, society: 'society', conform: 0.4, conformGlobal: 0.6, fearBias: 0.3, hopeBias: 0.2, truth }), makeSociety({ mind: 'mind', hierarchy }) ] });
for (let h = 0; h < 10 * 24; h++) w.step(UNITS.HOUR);
return w.fields().society;
}
function runD(hierarchy, seed) { // a DIVIDED world: an afraid corner MINORITY (salient) vs a calm majority
const truth = (x, y) => (x < -0.3 && y < -0.3) ? { threat: 0.85, food: 0.4, warmth: 0 } : { threat: 0, food: 0.4, warmth: 0.3 };
const w = World({ seed, systems: [ makeMind({ minds: 100, society: 'society', conform: 1.0, conformGlobal: 0.6, fearBias: 0.4, hopeBias: 0.2, truth }), makeSociety({ mind: 'mind', hierarchy }) ] });
for (let h = 0; h < 8 * 24; h++) w.step(UNITS.HOUR);
const soc = w.fields().society, mind = w.fields().mind, infl = soc.influence;
let cN = 0, cI = 0, rN = 0, rI = 0;
for (let k = 0; k < mind.M; k++) { const corner = mind.x[k] < -0.3 && mind.y[k] < -0.3; if (corner) { cN++; cI += infl ? infl[k] : 1; } else { rN++; rI += infl ? infl[k] : 1; } }
return { soc, cornerInfl: cI / Math.max(1, cN), restInfl: rI / Math.max(1, rN), cPct: Math.round(100 * cN / mind.M) };
}
function runScare(hierarchy) { // the contagion-rung scare (a ~10% corner, first 20h then GONE), but SUBCRITICAL contagion
const scare = (x, y, ctx) => ({ threat: (x < -0.35 && y < -0.35 && ctx.clock.t < 20 * 3600) ? 0.9 : 0.0, food: 0.4, warmth: 0 });
const w = World({ seed: 4, systems: [ makeMind({ minds: 100, society: 'society', conform: 0.6, conformGlobal: 0.6, fearBias: 0.8, truth: scare }), makeSociety({ mind: 'mind', hierarchy }) ] });
for (let i = 0; i < 46; i++) w.step(UNITS.HOUR);
return w.fields().society.meanDread;
}
const uOn = runU(true, 7), uOff = runU(false, 7);
ok(uOn.inflHerf > 1.1 && Math.abs(uOff.inflHerf - 1) < 1e-6, 'a PECKING ORDER emerges from a flat crowd -- influence concentrates on the salient (concentration ' + uOn.inflHerf.toFixed(2) + ' with a hierarchy vs a flat ' + uOff.inflHerf.toFixed(2) + ')');
ok(uOff.inflMax === 1 && uOn.inflMax > 1.4, 'a TOP LEADER rises well above the average (inflMax ' + uOn.inflMax.toFixed(2) + 'x vs a flat ' + uOff.inflMax.toFixed(2) + 'x -- and OFF is a clean uniform toggle)');
const dOn = runD(true, 3), dOff = runD(false, 3);
ok(dOn.cornerInfl > dOn.restInfl * 5, 'the LEADERS are the salient -- the afraid minority holds far more influence than the calm majority (' + dOn.cornerInfl.toFixed(1) + ' vs ' + dOn.restInfl.toFixed(2) + ')');
ok(dOn.soc.meanFearRaw > 0.6 && dOff.soc.meanFearRaw < 0.3, 'a committed, salient MINORITY (~' + dOn.cPct + '%) LEADS the whole into its fear, where headcount alone would not (population fear ' + dOn.soc.meanFearRaw.toFixed(2) + ' with leaders vs ' + dOff.soc.meanFearRaw.toFixed(2) + ' flat)');
const scOn = runScare(true), scOff = runScare(false);
ok(scOff < 0.3 && scOn > 0.6, 'INFLUENCE LOWERS THE TIPPING POINT -- a subcritical scare that FIZZLES in a flat crowd is CARRIED by leaders (meanDread ' + scOff.toFixed(2) + ' flat -> ' + scOn.toFixed(2) + ' with a hierarchy)');
console.log(' hierarchy: flat-concentration ' + uOff.inflHerf.toFixed(2) + ' -> ' + uOn.inflHerf.toFixed(2) + ' | minority-leads fear ' + dOff.soc.meanFearRaw.toFixed(2) + ' -> ' + dOn.soc.meanFearRaw.toFixed(2) + ' | tipping ' + scOff.toFixed(2) + ' -> ' + scOn.toFixed(2));
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);