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 goesReducer change?
A new betNew BetDef entry in engine/src/bets.ts (DEFAULT_CATALOG)No
A new chipNew entry in engine/src/chips.ts (DEFAULT_REGISTRY)No
A new actionExtend the Action union in engine/src/actions.ts + a reducer caseYes
A new player roleExtend PlayerState["role"] in engine/src/state.tsMaybe

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 a Roll. The compile-time union forbids onPostResolve. The output is bounded by assertValidRoll (dice must stay integer 1-6).
  • Bettor chip → implement onPostResolve(events, ctx) returning the settlement list. The union forbids onPostRoll. The output is bounded by assertValidBettorSettlements (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_point settlement outcome that mutates bet.point on the first roll.
  • Fire bet added shooterStats.pointsMade and widened BetDef.resolve with the optional ResolveContext — 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 primitivesSTART_RUN, BUY_CHIP, REROLL_SHOP, CHANGE_SHOOTER, LEVEL_BET.
  • A3 mid-game joinJOIN_TABLE + the "spectator" role.