JesseMarkowitzandClaude Opus 5 cc59b81155 Add the engine core: path-addressed state, data-driven turns
The planning docs commit to one thing above all: adding content must never
require touching the engine. This lays the foundation that makes that true.

All game state is addressable by dotted path (money, skills.negotiation,
relationships.boss.likes_you, flags.took_loan), and conditions and effects are
generic operations over those paths. The schema in the planning docs had
`requires: { skill, min }` and `effects: { money, skills, relationships }`,
both of which hardcode the state shape into the engine; the first gate wanting
a relationship threshold or a flag would have meant an engine change.

There is deliberately no "random event" category. Every turn filters the whole
event pool by stage and conditions and draws by weight, so a routine turn, a
rare interruption and the hard-times branch differ only in their data.

Also here, neither in the planning docs but both cheap and load-bearing:

- A seeded, serializable RNG. Runs replay exactly from seed plus choices, which
  makes tests deterministic and playtest reports reproducible. The generator's
  position is one uint32 living in the save.
- A load-time content validator. Undeclared paths, unknown character ids, ids
  that cannot be path segments, events that can never fire and stages with no
  fallback event are all caught on load instead of forty turns into a game.

The turn loop is a pure function, so a saved game resumes into exactly the run
it left — covered by a test that plays thirty turns, plays the same thirty with
a JSON round trip in the middle, and deep-equals the two.

72 tests, no dependencies. Reasoning recorded in docs/DECISIONS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VMSFHyVPitUoosW5wyEADj
2026-09-09 21:43:46 -04:00
2026-09-10 00:27:28 +00:00

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

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.

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.<id>.<stat>, 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.<eventId>, lastSeen.<eventId>) 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.

S
Description
A turn-based career sim. The player starts in the mailroom of a modern corporate office.
Readme
30 MiB
Languages
JavaScript 97.1%
CSS 2.8%
HTML 0.1%