slotdaddy

Slotdaddy Math Contract

Audience: studios authoring math packages for Slotdaddy games.

This document specifies what a Slotdaddy "math package" is: the file layout a studio ships, the on the wire book format the math runtime produces and the RGS serves, the recommended book event schema the client consumes, and the conventions for replay, QA, and certification.

Design overview

A Slotdaddy math package is a typed Python package plus a deterministic, content addressed book file that the RGS serves at /wallet/play time. The split exists for three reasons:

  • Authoring stays readable. The math is written as ordinary Python with helpers, optimizers, and unit tests. No DSL to learn.
  • Runtime stays cheap and certifiable. The RGS samples a precomputed book; auditors verify RTP by inspecting weighted aggregates of the file directly.
  • Replay is free. Every settled bet surfaces a bookID and rngSeed, so reconstructing the exact events a player saw needs nothing more than the book file.

This document specifies the package layout, the book file format, the recommended typed event schema for round.state, and the conventions for replay, QA, and certification.


Package layout

A Slotdaddy game lives under games/<game_id>/:

code
games/<game_id>/
  game_config.py        # bet modes, reels, paytable, RTP target
  game_calculations.py  # mechanics helpers (wins, paylines, ways)
  game_executables.py   # spin/bonus runners, produce book events
  game_events.py        # bookEvent emitter definitions
  game_optimization.py  # CVXPY weight optimizer (optional)
  game_override.py      # per game force outcome hooks for QA
  gamestate.py          # per spin state machine
  reels/                # reelstrip CSVs
  run.py                # sim / lookup table generator entrypoint

File responsibilities

FileRole
game_config.pyDeclares the game's bet modes, RTP target per mode, paytable, scatter pays, feature triggers, reelstrip references, and any tunables the optimizer is allowed to vary. The single source of truth for "what game am I shipping."
game_calculations.pyPure function helpers, win evaluation across paylines/ways/clusters, scatter counting, multiplier application. No I/O, no randomness. Easy to unit test.
game_executables.pyThe runtime spin loop. Composes gamestate, game_calculations, and game_events to produce a complete round (base spin, free spin, bonus). Emits book events.
game_events.pyTyped emitters for every event the client will consume (e.g. emit_reveal_symbols(...), emit_win_line(...)). The bridge to the book event schema.
game_optimization.pyConvex optimizer based on CVXPY (optional). Given a target RTP and the configured reelstrips, solves for weights per symbol that hit RTP while satisfying volatility and hit frequency constraints.
game_override.pyForce outcome hooks for QA and certification. Lets a developer say "next spin: hit the bonus" or "produce exactly this book record." Not reachable in production.
gamestate.pyThe mutable state for a single round, current spin index, accumulated free spins, current multiplier, etc. Reset between rounds.
reels/CSV reelstrips, one column per reel, one symbol per row. Sized to match game_config.py's reel layout.
run.pyEntrypoint. Invokes the simulator to generate the book lookup tables the runtime serves at /wallet/play time.

Runtime requirement

Python 3.12+. Optional rustc/cargo for the optimizer's native solver path. Future versions of this spec may add a language agnostic, book only authoring path (see Future directions); v1 is Python only for the authoring step.


Execution model: precomputed books

Slotdaddy v1 uses a sampled book model rather than live math execution.

  1. The studio runs python run.py locally (or in CI).
  2. The simulator executes the math package against millions of synthetic rounds.
  3. The optimizer adjusts symbol weights to hit the target RTP.
  4. The output is a book file per (game, mode): a frequency weighted catalog of outcomes.
  5. At /wallet/play time, the RGS picks a (game, mode), samples one book record proportional to its weight, and returns the record's events array as the round's state.

Why this model:

  • Certifiable. Auditors can verify RTP by inspecting weighted aggregates of the book directly.
  • Deterministic. (bookID, math_version, rngSeed) reproduces a round exactly.
  • Fast. Sampling is O(log n) with prefix sum tables; math complexity is paid at build time, not spin time.
  • Swappable. New math ships as a new book file behind a feature flag.

Trade-offs (documented honestly):

  • Static. Books can't condition on per player state at spin time. For mechanics that need that (escalating multipliers across a session, RNG biases specific to a player), see Future directions.
  • Large artifacts. Books can run hundreds of MB per mode for high volatility games. Slotdaddy stores them content addressed and serves a deterministic shard at runtime.

Book file format

A book file is a deterministic, content addressed JSON document describing all possible outcomes for a single (game, mode) pair.

Top level shape

json
{
  "schemaVersion": 1,
  "gameId": "lines_v3",
  "mode": "BASE",
  "mathVersion": "2026.05.11+a1b2c3",
  "currency": "BASE_UNITS",
  "totalWeight": 100000000,
  "rtp": 0.9650,
  "records": [
    {
      "id": "lines_v3_base_00041527",
      "weight": 12,
      "payoutMultiplier": 2.5,
      "events": [ /* BookEvent[] */ ]
    }
  ]
}

BookRecord semantics

FieldTypeNotes
idstringGlobally unique within the file. Surfaced to the runtime as Round.bookID. Form: <gameId>_<mode>_<8 digit padded index>.
weightintRelative sampling probability. The runtime samples proportional to weight / totalWeight.
payoutMultipliernumberTotal round payout as a multiple of the bet. The runtime computes Round.payout = round(amount * payoutMultiplier).
eventsBookEvent[]Ordered list of events that, played in sequence, fully describe the round's outcome. See Book event schema.

Determinism guarantee

For any tuple (gameId, mathVersion, mode, bookID), the events array is byte for byte identical across all environments and all time. The RGS persists bookID on every settled bet; given that, the entire round can be regenerated from the book file alone; no replay log of the math is needed.

Sampling protocol

The RGS uses weighted reservoir sampling with a per bet rngSeed:

code
rngSeed = HMAC-SHA256(serverSecret, betID || gameId || mode)
record  = book.records[ weighted_index(rngSeed, totalWeight) ]

rngSeed is surfaced (opaquely) on Round.rngSeed for certifier reproducibility. The server secret never leaves Slotdaddy infrastructure.


Book event schema

round.state is opaque on the wire; the runtime SDK does not validate it. But the Slotdaddy web SDK and its bookEventHandlerMap pattern only work if state.events follows a recognized shape. Slotdaddy publishes a recommended discriminated union so studios get the stock handlers for free.

Envelope

round.state is always:

ts
type RoundState = { events: BookEvent[] };

BookEvent discriminated union

ts
type BookEvent =
  // Reel reveal: used by lines, ways, scatter, expwilds
  | { type: "revealSymbols"; reelIndex: number; symbols: string[] }
  | { type: "revealGrid"; rows: number; cols: number; symbols: string[][] }
 
  // Wins, one variant per evaluation model
  | { type: "winLine"; lineId: number; symbols: number[]; amount: number; multiplier?: number }
  | { type: "winWays"; symbol: string; count: number; ways: number; amount: number; multiplier?: number }
  | { type: "winCluster"; symbol: string; cells: [number, number][]; amount: number; multiplier?: number }
  | { type: "winScatter"; symbol: string; count: number; amount: number }
 
  // Features
  | { type: "scatter"; symbol: string; count: number; amount?: number }
  | { type: "wildExpand"; reelIndex: number; from: number; to: number }
  | { type: "freeSpinsAward"; count: number; reason?: string }
  | { type: "freeSpinStart"; index: number; total: number }
  | { type: "freeSpinEnd"; index: number; total: number }
  | { type: "multiplier"; value: number; scope: "spin" | "feature" | "global" }
 
  // Cascade / tumble mechanics
  | { type: "cascadeRemove"; cells: [number, number][] }
  | { type: "cascadeFill"; reelIndex: number; symbols: string[] }
 
  // Bonus minigames
  | { type: "bonusTrigger"; bonusId: string; payload?: Record<string, unknown> }
  | { type: "bonusResult"; bonusId: string; amount: number; payload?: Record<string, unknown> }
 
  // Round terminator (always last)
  | { type: "roundComplete"; totalPayout: number }
 
  // Escape hatch, studios may emit any event their client handles
  | { type: string; [k: string]: unknown };

Conventions

  • events is ordered. The client plays them sequentially.
  • Money fields (amount, totalPayout) are in the same units the runtime API uses (operator minor unit × API_MULTIPLIER).
  • Coordinates [row, col] are zero indexed from the top left of the visible grid.
  • Symbol identifiers are strings defined in game_config.py (e.g. "H1", "L3", "WILD", "SCATTER").
  • Every round must end with a roundComplete event whose totalPayout equals sum(amount) across win events. The Slotdaddy web SDK uses this to settle UI state.

Validation

Slotdaddy's CLI ships a slotdaddy math validate command (planned, not in v1) that statically checks emitted books against this schema and reports unknown fields. Failures are warnings (not blockers) because the escape hatch variant is intentional.


Game types

These are the canonical patterns Slotdaddy supports out of the box, with the typical event sequence written down. Studios can implement any of them, or compose new ones by emitting custom events.

lines, fixed paylines

Classic 5×3 (or similar) reel slot with N fixed paylines. A win is N+ matching symbols left to right on a payline starting from reel 1.

Canonical event sequence:

  1. revealSymbols × reelCount
  2. winLine × wins (zero or more)
  3. (optional) scatterfreeSpinsAward → free-spin sub-sequence
  4. roundComplete

ways, "X ways" (e.g. 243 ways, 1024 ways)

No fixed lines; pays for any combination of matching symbols on adjacent reels from reel 1.

Canonical event sequence:

  1. revealSymbols × reelCount
  2. winWays × wins
  3. roundComplete

cluster, adjacent cluster pays

Wins triggered by connected clusters of ≥minCluster same symbol cells (typically 5+).

Canonical event sequence:

  1. revealGrid
  2. winCluster × wins
  3. (optional) cascadeRemovecascadeFill → re-evaluate → loop
  4. roundComplete

scatter, scatter only / pay anywhere

Wins triggered purely by the count of a symbol across the grid, regardless of position.

Canonical event sequence:

  1. revealSymbols × reelCount
  2. winScatter × wins
  3. roundComplete

expwilds, expanding wilds

Lines or ways game with reels that expand to fully wild after reveal.

Canonical event sequence:

  1. revealSymbols × reelCount
  2. wildExpand × expansions
  3. winLine / winWays × wins
  4. roundComplete

lines_feature_match, lines + bonus feature trigger

Standard lines plus a bonus feature minigame triggered by a symbol pattern.

Canonical event sequence:

  1. revealSymbols × reelCount
  2. winLine × wins
  3. bonusTriggerbonusResult (if triggered)
  4. roundComplete

fifty_fifty, instant win / coin flip

The minimal game type. Single binary outcome.

Canonical event sequence:

  1. bonusResult (with the outcome)
  2. roundComplete

Replay & QA conventions

Force outcome via game_override.py

game_override.py exposes hooks the simulator calls before sampling, letting a developer force a deterministic outcome:

python
def force_next(gamestate, mode):
    if dev_flag("force_bonus"):
        return {"force_reels": [...], "force_features": ["FREE_SPINS_5"]}
    return None

Force overrides only fire in development simulations (run.py) and the planned /admin/force-result endpoint; never in production sampling.

Replay key

The runtime API surfaces Round.bookID and Round.rngSeed on every settled bet. Given a bookID, anyone with access to the original book file can reconstruct the exact events array; no replay log is needed. This is the audit story for certifiers and dispute resolution.

A future POST /admin/replay endpoint (reserved, not in v1) will accept { bookID } and return { events, payoutMultiplier, mathVersion }.

Auditability checklist (what a certifier gets)

  • The signed math package source (games/<id>/ directory hash).
  • The signed book file (book.json content hash, recorded in Round.bookID's namespace).
  • The full (bookID, rngSeed) log per settled bet.
  • A reproducible build script (run.py is deterministic given the same source + reel CSVs).

Together: from a betID, an auditor can derive (bookID, rngSeed), fetch the book record by id, and verify the events array and payoutMultiplier are exactly what the player saw.


Versioning

  • Math version. Every run.py invocation stamps a mathVersion like 2026.05.11+<git-sha> into the book file. The RGS persists this on each bet. Two bets with different mathVersion may produce different outcomes for the same bookID; that is expected.
  • Book schema version. This document is schemaVersion: 1. Future minor versions add fields only. Major versions ship behind the runtime API's X-Slotdaddy-Schema-Version header.
  • Game version. Up to the studio. Slotdaddy recommends suffixing gameId (e.g. lines_v3) and treating the math package as immutable per version.

Reserved / future directions

Explicitly out of scope for v1, but flagged so the spec doesn't paint itself into a corner:

  • Live math execution. For mechanics that need per player state at spin time (escalating multipliers, dynamic volatility, persistent state), v2 will define a mode: "LIVE" channel that bypasses book sampling and invokes a sandboxed math runner. The mode taxonomy is already open enough to add this.
  • WASM math modules. A language agnostic alternative to Python authoring. The book file format is already language agnostic; only run.py is specific to Python.
  • Streaming events. For long bonus rounds, round.state could become a stream rather than a single array. The runtime API's SSE channel (GET /wallet/stream) is the natural transport.
  • Session state across games. Progression across games (loyalty multipliers, jackpots). Requires a session state plane that doesn't exist yet.

None of these change v1; they are designed to drop in without breaking it.


See also