# 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 core is built and tested. No UI yet. | Piece | State | | --- | --- | | Path-addressed state, conditions, effects | done | | Seeded RNG, save/resume | done | | Event selection, option gating | done | | Turn loop | done | | Content validator | done | | Corporate Ladder content pack | not started | | UI, save/export, feedback log | not started | ## Running ```sh npm test # engine test suite (node's built-in runner, no dependencies) npm run serve # static server on :8080 for development ``` There are no runtime dependencies and no build step for development. Node is used only to run tests and, later, the single-file build. ## Layout ``` src/engine/ the game engine — imports nothing from content/ 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 content/ content packs (settings) test/ engine tests, run against a fixture pack, never real content 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. 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.