← Natura·Docs/Modules/Source

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);