Files
roam/trmnlife.org
2026-06-17 07:36:46 +03:00

4.2 KiB
Raw Permalink Blame History

trmn/life

Architecture

life.js is a standalone ES module (no dependencies) that exports:

  • simulateLife(masterSeed){ traits, buf, eventLog, people }
  • getDay(buf, day) → plain object (unpack one day from typed array)
  • makeRng(masterSeed, day) → mulberry32 closure (day-local PRNG)
  • DAYS (36501), FIELDS (22), F (field index map)

State is packed into Float32Array(DAYS * FIELDS) (3.2MB). ~eventLog is Map<day, Array<{id, personId?, attended?}>> (entries are objects, NOT plain strings — changed when NPC system was added). people is Map<personId, PersonRecord> for all tracked NPCs. window.__life exposes { traits, buf, eventLog, people, today, currentDay, masterSeed }.

PRNG

mulberry32 seeded per-day via splitmix32 hash:

makeRng(masterSeed, day)  // derives seed from hash(masterSeed ^ day*0x9e3779b9)
  • Math.imul throughout — bitwise-identical on all IEEE-754 JS engines
  • Any day independently computable; no need to simulate prior days
  • masterSeed = (Date.now() / 1000) | 0 (wall-clock seconds)
  • Reserved day slots: -1 = protagonist traits; -2 = family layout offsets (NPC system)
  • NPC streams are fully isolated — see trmn/people

State Fields (22 packed floats per day)

Index Field Notes
0 alive 0/1
1 health 0100
24 sick, disabled, chronicDisease 0/1 flags
57 eduStage (06), inCollege, collegeDropout
811 employed, jobTier (04), incomeLevel, retired
1216 inRelationship, married, everDivorced, divorceCount, widowed
1721 numChildren, friendCount, loneliness, wealthLevel, debtLevel

Fixed-at-birth traits (not packed): sex, introversion, ambition, resilience, baseHealth Internal-only (not packed): friendList, friendLifetimeIndex, childrenList, childLifetimeIndex

Scheduled Events (deterministic)

birth(0), kindergarten(1826), primary(3652), middle(5113), hs_grad(6575), college_grad(8036), retirement(23725), death_cap(36500) Plus dynamically injected sibling_born entries (built inside simulateLife(), merged + sorted into mergedSchedule).

Random Events (31 total, evaluated in priority order, cap 2/day)

Categories: death, disaster, health, financial, relationship, family, career, social Death uses Gompertz curve: 0.000008 * exp((age-40)/60 * 5) * healthMod * resilienceMod NPC-triggered events (not in EVENTS array): family_member_death (day loop step 4), friend_death (post-apply of lose_friend)

Gotchas

  • NEVER spread buf inside a loop — [...buf] on 800k-element Float32Array per iteration hangs the browser. Use buf[d * FIELDS + fieldIdx] directly.
  • life.js must be listed in flake.nix installPhase (cp $src/life.js $out/) or Caddy serves a 404.
  • eventLog entries are now {id, personId?, attended?} objects — any consumer using evs.includes('event_id') or counts[ev] will silently break. Use e.id.
  • breakup and marry both have weight 0.0008/day → equal competition → many characters die "in relationship, not married". May need tuning.
  • "Unexpected end of input" on import('./life.js') usually means 404 (not rebuilt yet), not a real syntax error.

Console Snippets

// Import and run
const { simulateLife, getDay, DAYS, FIELDS } = await import('/life.js');
const sim = simulateLife(42);

// Readable timeline (eventLog entries are objects now)
const timeline = [...sim.eventLog.entries()].map(([d, evs]) => ({
  age: (d / 365.25 | 0), day: d, events: evs.map(e => e.id).join(', ')
}));
console.table(timeline);

// Inspect people (family + friends + children)
[...sim.people.entries()].forEach(([id, p]) =>
  console.log(id, 'sex', p.sex, 'born', p.birthDay, 'dies', p.deathDay)
);

// Find death day (safe — no buf spreading)
const death = Array.from({length: DAYS}, (_, d) => d).find(d => d > 0 && sim.buf[d * FIELDS] === 0);
console.log('died age', death && (death / 365.25 | 0));

Subnodes

  • trmn/people — NPC system: PersonRecord schema, PRNG isolation, family generation, post-apply hooks, gotchas