# The Ladder A turn-based career simulation game that runs entirely in the browser. You start at the bottom of an organisation and play turns — a day at a time — making choices that move your money, your skills and your standing with the people around you. There is no win state and no game over: a bad run means worse options, not an ending, and you can stop whenever you like and read the retrospective. The engine knows nothing about offices, mailrooms or bosses. A **setting is a content pack** — characters, events, choices, stat names and tuning, all data. `corporateladder` is the default pack; a Wild West or lemonade-stand pack would be new data and no new code. ## Status Engine and the first content pack are built and tested. No UI yet — the game is currently playable only from a test harness. | Piece | State | | --- | --- | | Path-addressed state, conditions, effects | done | | Seeded RNG, save/resume | done | | Event selection, option gating | done | | Turn loop, per-turn upkeep | done | | Content validator + reachability tests | done | | Corporate Ladder pack — 17 events, 2 characters | done | | Turn screen, retrospective, feedback widget | done | | Local-storage save, file export/import | done | | Single-file build | done | | Calendar, scheduled wages and rent | done | | Played in a real browser | done — first playtest 2026-09-09 | The first playtest confirmed the interface, saves and the exported log all work in a real browser. Its findings — an unexplained daily drain, no sense of the week, and an economy nobody could get ahead in — are what the calendar and the retuned economy are for. Note that the development machine has no browser that can render a page, so the UI tests drive the real modules against a minimal fake DOM. That covers wiring, not layout: **styling and dark mode are only ever verified by a human opening it.** ## Playing it ```sh npm run serve # then open http://localhost:8080 ``` A server is needed during development because browsers refuse ES module imports over `file://`. If port 8080 is taken, run `python3 -m http.server ` instead. To hand the game to someone else: ```sh npm run build # writes dist/theladder.html ``` That is one self-contained file with the stylesheet and every module inlined. It plays by double-clicking it — no server, no network, nothing installed. ```sh npm test # 109 tests: engine, content, UI wiring, and the build ``` There are no runtime dependencies and no build step for development. Node is used only to run the tests and the single-file build. ## Layout ``` src/engine/ the game engine — imports nothing from content/, or from src/ui/ paths.js dotted-path get/set over state conditions.js generic predicates: { path, op, value }, all/any/not effects.js generic mutations: add/set/push/remove, with clamping state.js pack -> schema and initial state select.js which event fires this turn, which options are available game.js the turn loop rng.js seeded, serializable RNG validate.js load-time content checking src/ui/ screens, formatting and the app controller app.js the only module that names a content pack screens/ the turn screen and the retrospective format.js state paths and conditions rendered as English src/io/ local-storage save, file export/import, the feedback log content/ content packs (settings) tools/build.js flattens everything into one self-contained HTML file test/ engine tests against a fixture pack; content and UI tests docs/ DECISIONS.md — why things are the way they are Planning/ the original design documents ``` **The rule that keeps this honest: nothing in `src/engine/` may import from `content/`.** The engine names no stat, no character and no setting of its own. ## How content works A pack declares what state exists and what it starts at, the relationship stats every character carries, the characters, and the events. Everything else falls out of that. ```js export default { id: 'corporateladder', startingStage: 'mailroom', state: { 'money': { initial: 1200, min: -5000, max: 1000000 }, 'skills.negotiation': { initial: 1, min: 0, max: 100 }, 'flags.took_loan': { initial: false }, }, // Expanded per character into relationships.., so adding a // character adds its relationship state automatically. relationshipStats: { likes_you: { initial: 50, min: 0, max: 100 }, fears_you: { initial: 50, min: 0, max: 100 }, wants_to_help_you: { initial: 50, min: 0, max: 100 }, }, characters: [ { id: 'boss', name: '...', role_type: 'boss', description: '...' }, ], events: [ { id: 'mail_run', stage: 'mailroom', weight: 100, // relative likelihood among eligible events description: 'A cart of mail and two hours to move it.', options: [ { id: 'hustle', label: 'Get it done fast.', requires: [{ path: 'skills.negotiation', op: '>=', value: 3 }], effects: [ { path: 'money', op: 'add', value: 40 }, { path: 'relationships.boss.likes_you', op: 'add', value: 3 }, ], }, ], }, ], }; ``` An event may also carry `once: true`, a `cooldown` in turns, and `requires` conditions of its own. There is no separate notion of a random event: every turn draws from the whole eligible pool by weight. Conditions may read engine bookkeeping (`turn`, `stage`, `seen.`, `lastSeen.`) as well as pack state. Effects may not write it. A pack also declares `upkeep` — a list of `{ label, requires?, effects }` applied at the end of a turn, after the choice. Adding `every` and `offset` makes an entry periodic rather than daily: Corporate Ladder pays wages on alternate Fridays (`{ every: 14, offset: 4 }`), charges rent every Monday, and adds a loan service charge that steps up as the debt grows. A pack may also declare a `calendar`: ```js calendar: { cycleLength: 7, cycleName: 'Week', unitNames: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], }, ``` The engine derives `calendar.index`, `calendar.cycle` and `calendar.name` from the turn number before each event is drawn, so content can require a Friday and the interface can title the turn. Declare no calendar and there is no week. Run `validatePack(pack)` when loading in development — it catches undeclared paths, unknown characters, unusable ids, unreachable events and stages with no fallback event, all of which otherwise fail silently mid-playthrough.