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
bookIDandrngSeed, so reconstructing the exacteventsa 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>/:
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
| File | Role |
|---|---|
game_config.py | Declares 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.py | Pure function helpers, win evaluation across paylines/ways/clusters, scatter counting, multiplier application. No I/O, no randomness. Easy to unit test. |
game_executables.py | The 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.py | Typed 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.py | Convex 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.py | Force 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.py | The 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.py | Entrypoint. 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.
- The studio runs
python run.pylocally (or in CI). - The simulator executes the math package against millions of synthetic rounds.
- The optimizer adjusts symbol weights to hit the target RTP.
- The output is a book file per
(game, mode): a frequency weighted catalog of outcomes. - At
/wallet/playtime, the RGS picks a(game, mode), samples one book record proportional to its weight, and returns the record'seventsarray as the round'sstate.
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
{
"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
| Field | Type | Notes |
|---|---|---|
id | string | Globally unique within the file. Surfaced to the runtime as Round.bookID. Form: <gameId>_<mode>_<8 digit padded index>. |
weight | int | Relative sampling probability. The runtime samples proportional to weight / totalWeight. |
payoutMultiplier | number | Total round payout as a multiple of the bet. The runtime computes Round.payout = round(amount * payoutMultiplier). |
events | BookEvent[] | 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:
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:
type RoundState = { events: BookEvent[] };BookEvent discriminated union
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
eventsis 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
roundCompleteevent whosetotalPayoutequalssum(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:
revealSymbols×reelCountwinLine×wins(zero or more)- (optional)
scatter→freeSpinsAward→ free-spin sub-sequence 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:
revealSymbols×reelCountwinWays×winsroundComplete
cluster, adjacent cluster pays
Wins triggered by connected clusters of ≥minCluster same symbol cells (typically 5+).
Canonical event sequence:
revealGridwinCluster×wins- (optional)
cascadeRemove→cascadeFill→ re-evaluate → loop roundComplete
scatter, scatter only / pay anywhere
Wins triggered purely by the count of a symbol across the grid, regardless of position.
Canonical event sequence:
revealSymbols×reelCountwinScatter×winsroundComplete
expwilds, expanding wilds
Lines or ways game with reels that expand to fully wild after reveal.
Canonical event sequence:
revealSymbols×reelCountwildExpand×expansionswinLine/winWays×winsroundComplete
lines_feature_match, lines + bonus feature trigger
Standard lines plus a bonus feature minigame triggered by a symbol pattern.
Canonical event sequence:
revealSymbols×reelCountwinLine×winsbonusTrigger→bonusResult(if triggered)roundComplete
fifty_fifty, instant win / coin flip
The minimal game type. Single binary outcome.
Canonical event sequence:
bonusResult(with the outcome)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:
def force_next(gamestate, mode):
if dev_flag("force_bonus"):
return {"force_reels": [...], "force_features": ["FREE_SPINS_5"]}
return NoneForce 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.jsoncontent hash, recorded inRound.bookID's namespace). - The full
(bookID, rngSeed)log per settled bet. - A reproducible build script (
run.pyis 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.pyinvocation stamps amathVersionlike2026.05.11+<git-sha>into the book file. The RGS persists this on each bet. Two bets with differentmathVersionmay produce different outcomes for the samebookID; that is expected. - Book schema version. This document is
schemaVersion: 1. Future minor versions add fields only. Major versions ship behind the runtime API'sX-Slotdaddy-Schema-Versionheader. - 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. Themodetaxonomy 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.pyis specific to Python. - Streaming events. For long bonus rounds,
round.statecould 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
- Runtime API:
../api/runtime.md. How the book reaches the player.