# Architecture decisions Why the code is shaped the way it is. Append to this file as decisions are made; the reasoning is worth more later than the conclusion. ## 1. All game state is addressable by path Every value in a run — `money`, `skills.negotiation`, `relationships.boss.likes_you`, `flags.took_loan` — is reachable by a dotted path string, and conditions and effects are expressed against those paths. The planning docs' schema had `requires: { skill, min }` and `effects: { money, skills, relationships }`. Both hardcode the state shape into the engine: the first gate that wants a relationship threshold, a money floor or a flag, and the first effect that touches a fourth kind of state, means editing engine code. Path addressing costs one `getPath`/`setPath` pair and removes the whole class of change. Everything else in the engine is built on this primitive: option gates, event triggers, the soft-failure branch, and — later — promotion tiers. ## 2. Conditions and effects are data, evaluated generically A condition is `{ path, op, value }` or a combinator (`all` / `any` / `not`). An effect is `{ path, op, value }` with `add` / `set` / `push` / `remove`. The engine knows the operators; it never knows what a path *means*. Bounds live in the pack's state declaration (`min` / `max`) and are applied by the effect layer, so clamping is content's decision, not the engine's. ## 3. There is no such thing as a "random event" Every turn, the engine filters the entire event pool by stage and conditions, then draws by weight. A routine turn is a high-weight event with loose conditions; a rare interruption is a low-weight one; the hard-times branch is an event whose conditions include a money threshold. The engine branches on none of it, which is what the planning docs asked for in `03-claude-code-build-prompt.md` §3 — but achieved by deleting the category rather than by carefully not special-casing it. Consequence: every stage needs at least one event that no exclusion rule can remove (no `requires`, no `once`, no `cooldown`), or the pool can run dry mid-run. The validator enforces this. ## 4. The engine owns some state; content may read it but not write it `turn`, `stage`, `seen`, `lastSeen`, `history` and `meta` are engine bookkeeping. Content can read them in conditions — "only after day 10", "only if this event has never fired" — which is useful and safe. Writing them is a validation error, so data cannot corrupt the loop. `seen.` is a count and `lastSeen.` a turn number, which is why event ids must be valid path segments. ## 5. Seeded, serializable RNG Not in the planning docs; added because it is nearly free and pays three ways: tests are deterministic, playtest reports are reproducible from seed plus choice sequence, and the feedback log becomes replayable rather than merely descriptive. mulberry32 was chosen because its entire state is one uint32, so the generator's position round-trips through JSON with no special handling. The seed and position live in `meta` inside the save file. Nothing about a run exists outside its state object. ## 6. The turn loop is a pure function `takeTurn(pack, state, optionId) -> { state, record }`. No mutable game object, no hidden state. A saved game resumes into exactly the run it left — there is a test that plays 30 turns straight, plays the same 30 turns with a JSON round trip in the middle, and deep-equals the results. ## 7. Content is validated at load, not discovered at play `validatePack` checks that every path referenced by a condition or effect is declared, every character id resolves, ids are usable as path segments, no event is unreachable, and every stage has a fallback. Without it, a typo in content fails silently forty turns into a playthrough — the exact failure mode that makes "just add data" unsafe for a non-programmer. ## 8. Distribution: ES modules in dev, one inlined file to ship Chrome and Firefox block ES module imports and `fetch` over `file://`, so "open the HTML file" and "modular JS with JSON content" are in direct conflict. Resolved by serving during development (`npm run serve`) and inlining everything into a single self-contained HTML file for distribution (`npm run build`). Related: content is authored as `.js` modules exporting plain objects rather than `.json`. Same data, but comments and multiline strings are available — which matters a great deal for narrative text — and it sidesteps `fetch` entirely. ## 9. Tuning values live in the content pack Relationship stats run 0–100 starting at 50, clamped; typical choice effects are a few points, so one choice matters but several turns of consistent behaviour are needed to move someone. Money is dollars. Turn is a day. None of these are engine constants. They are declared in the pack manifest, so retuning the game — or a pack that runs on weeks instead of days — touches no code. ## 10. Naming The product is **The Ladder**; the default content pack is **Corporate Ladder**. The planning docs use "Corporate Ladder" as the working title for both, which would have baked one setting into the product name, against the explicit goal that corporate is one pack among many.