Extension Surface
The whole point of engine purity is that the common extensions are data, not reducer surgery. Match the row:
| You want to add… | Where it goes | Reducer change? |
|---|---|---|
| A new bet | New BetDef entry in engine/src/bets.ts (DEFAULT_CATALOG) | No |
| A new chip | New entry in engine/src/chips.ts (DEFAULT_REGISTRY) | No |
| A new action | Extend the Action union in engine/src/actions.ts + a reducer case | Yes |
| A new player role | Extend PlayerState["role"] in engine/src/state.ts | Maybe |
Adding a bet
A new BetDef entry, plus a test in engine/src/resolver.test.ts asserting the real-casino payout (not “what feels right”). The existing tests are the pattern. Most bets need nothing more.
Multi-roll bets (fire, sharpshooter, ATS, repeaters) read shooter/tracker state via an optional ResolveContext on BetDef.resolve. The reducer pre-computes post-roll trackers for any “wins on the completing roll” bet and threads them through to resolution, while leaving pre-roll state available for bets that need the pre-clear count (fire, sharpshooter). See Bet Catalog → Multi-roll bets.
Adding a chip
A new entry in DEFAULT_REGISTRY with exactly one side:
- Shooter chip → implement
onPostRoll(raw, ctx)returning aRoll. The compile-time union forbidsonPostResolve. The output is bounded byassertValidRoll(dice must stay integer 1-6). - Bettor chip → implement
onPostResolve(events, ctx)returning the settlement list. The union forbidsonPostRoll. The output is bounded byassertValidBettorSettlements(1:1 bijection, finite non-negative integer amounts, known kinds).
Include a test that the chip’s effect happens in the right slot, and that disabling all chips still yields a casino-faithful resolution (the purist test). See Chip Dispatch Contract.
Optional metadata: rarity (common | uncommon | rare | legendary) and synergyTags drive the A2 shop drafting weights with no other edits. A shooter chip can declare skillThresholds to gate on SkillInput.
Adding an action
This is the case that does touch the reducer. Extend the Action union in actions.ts, then handle it in the reducer. The reducer’s switch is exhaustive — every action must be handled. Keep it pure: consume state.rng, never Math.random(); never Date.now() (the server stamps time). Preserve the JSON round-trip invariant.
Already extended-and-shipped
Examples of past extensions, as patterns to follow:
- Come / don’t-come added an
establish_pointsettlement outcome that mutatesbet.pointon the first roll. - Fire bet added
shooterStats.pointsMadeand widenedBetDef.resolvewith the optionalResolveContext— the template for every multi-roll bet since. - ATS / repeaters added their own trackers (
atsTracker,repeaterTracker) advanced by the reducer before resolution. - EQUIP_CHIP / UNEQUIP_CHIP for runtime chip toggling.
- A2 primitives —
START_RUN,BUY_CHIP,REROLL_SHOP,CHANGE_SHOOTER,LEVEL_BET. - A3 mid-game join —
JOIN_TABLE+ the"spectator"role.