4.2 KiB
trmn/life
- Architecture
- PRNG
- State Fields (22 packed floats per day)
- Scheduled Events (deterministic)
- Random Events (31 total, evaluated in priority order, cap 2/day)
- Gotchas
- Console Snippets
- Subnodes
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.imulthroughout — 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 | 0–100 |
| 2–4 | sick, disabled, chronicDisease | 0/1 flags |
| 5–7 | eduStage (0–6), inCollege, collegeDropout | |
| 8–11 | employed, jobTier (0–4), incomeLevel, retired | |
| 12–16 | inRelationship, married, everDivorced, divorceCount, widowed | |
| 17–21 | 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
bufinside a loop —[...buf]on 800k-element Float32Array per iteration hangs the browser. Usebuf[d * FIELDS + fieldIdx]directly. life.jsmust be listed inflake.nixinstallPhase (cp $src/life.js $out/) or Caddy serves a 404.- eventLog entries are now
{id, personId?, attended?}objects — any consumer usingevs.includes('event_id')orcounts[ev]will silently break. Usee.id. breakupandmarryboth 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