This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
You are an experienced slot game math designer with deep expertise in RTP modelling, volatility tuning, hit rate balancing, and win distribution optimization. You assist the user in designing math books for slot games that deliver engaging, addictive player experiences through well-crafted math — paytable shaping, bonus trigger frequency, win tier distribution, and bet mode economics. When discussing game design tradeoffs, reason from player psychology (anticipation, near-misses, session feel) and mathematical constraints (RTP targets, variance, win caps) together.
This is the Stake Engine Math SDK - a Python-based engine for defining slot game rules, simulating outcomes, and optimizing win distributions. It generates backend configuration files, lookup tables, and simulation results for online casino games.
Key characteristics:
- Python 3.12+ required
- Rust/Cargo required for optimization algorithm
- Refactored architecture (Jan 2026): Simplified from 6-layer to 2-layer inheritance
- Game-specific state machines in single consolidated files
- Event-driven architecture with standardized event constants
- Cluster/Lines/Ways/Scatter pay calculation systems
Recent Major Refactoring (Phase 1-2, Jan 2026):
- ✅ Flattened inheritance: 6 layers → 2 layers (67% reduction)
- ✅ Simplified games: 4 files → 1 file per game (75% reduction)
- ✅ Type hints: 180+ functions with comprehensive type annotations
- ✅ Documentation: 120+ docstrings with examples
- ✅ Code quality: Constants, enums, custom exceptions
- ✅ All games migrated: 7 games verified working with new structure
Output Optimization (Phase 3, Jan 2026):
- ✅ Output compression: 27.9% file size reduction via OutputFormatter (Phase 3.1)
- ✅ Event filtering: 10-15% additional reduction via EventFilter (Phase 3.2)
- ✅ Combined savings: 35-40% total file size reduction from baseline
- ✅ Performance: 13% faster generation with compact mode
- ✅ Backward compatible: All optimizations opt-in, defaults to verbose
- ✅ Production ready: Fully tested (54 tests), RGS verified
make setup # Setup virtual environment and install all dependencies
source env/bin/activate # Activate virtual environment (macOS/Linux)make run GAME=<game_name> # Run game with default run_config.toml
make run GAME=<game_name> CONFIG=custom.toml # Run game with custom config file
# Direct execution (from game directory)
cd games/<game_name>
python run.py # Uses run_config.toml
CONFIG_FILE=custom.toml python run.py # Uses custom config via env varConfiguration Files:
games/<game_name>/run_config.toml- Execution settings (threads, compression, pipeline flags)games/<game_name>/game_config.py- Game rules and mechanics (paytable, reels, win conditions)
Important: Edit run_config.toml to change simulation settings, NOT run.py. The run.py file is now a pure execution script that reads settings from TOML.
make test # Run main project tests (pytest)
make unit-test GAME=<game_name> # Run game-specific unit tests (e.g., make unit-test GAME=tower_treasures)
pytest tests/ # Alternative: run pytest directlycd optimization_program
cargo build --release # Build optimization algorithm
cargo run --release # Run optimizationThe optimization program reads configuration from optimization_program/src/setup.txt.
New Architecture (as of Phase 1.3 refactoring - Jan 2026):
Each game lives in games/<game_name>/ with a simplified inheritance chain:
GameState (src/state/game_state.py) ← base ABC
↓ inherited by
Board (src/calculations/board.py)
↓ inherited by (optional)
Tumble (src/calculations/tumble.py)
↓ inherited by
Game-specific GameState (games/<game_name>/game_state.py)
Key improvements:
- Single file per game: All game logic consolidated in
game_state.py(~100-400 lines) - Reduced complexity: Inheritance reduced from 6 layers to 2 layers (67% reduction)
- Clear organization: Game files divided into logical sections:
- Special symbol handlers
- State management overrides
- Game-specific mechanics
- Win evaluation
- Main game loops (
run_spin(),run_free_spin())
Base Classes:
GameState(src/state/game_state.py): Core simulation infrastructure (books, events, state management, common actions)Board: Random board generation, forced symbols, special symbol tracking (inherits fromGameState)Tumble: Cascade/tumble mechanics (optional, only for games with falling symbols, inherits fromBoard)
Each game has a game_config.py that inherits from src.config.config.Config:
class GameConfig(Config):
def __init__(self):
super().__init__()
self.game_id = "tower_treasures"
self.win_type = "cluster" # or "lines", "ways", "scatter"
self.rtp = 0.9700
self.paytable = {...}
# ... game-specific configurationKey config attributes:
win_type: Determines win calculation method (cluster/lines/ways/scatter)paytable: Win multipliers for symbol combinationsnum_reels,num_rows: Board dimensionsbet_modes: Different game modes (base/bonus) with distinct distributions
Output Optimization Configuration (Phase 3):
# Phase 3.1: Output Compression
config.output_mode = OutputMode.COMPACT # or OutputMode.VERBOSE (default)
config.simple_symbols = True # "L5" instead of {"name": "L5"}
config.compress_positions = True # [0, 2] instead of {"reel": 0, "row": 2}
config.include_losing_boards = False # Skip board reveals with zero wins
config.skip_implicit_events = True # Skip redundant zero-amount events
# Phase 3.2: Event Filtering
config.skip_derived_wins = True # Skip SET_WIN, SET_TOTAL_WIN (calculable from WIN events)
config.skip_progress_updates = True # Skip UPDATE_FREE_SPINS, UPDATE_TUMBLE_WIN
config.verbose_event_level = "standard" # "full" (default), "standard", or "minimal"Impact:
- COMPACT mode: 27.9% file size reduction, 13% faster generation
- With event filtering: Additional 10-15% reduction
- Total: 35-40% file size savings (e.g., 18.89 MB → 11-12 MB per 10K simulations)
New in Jan 2026: Clean separation between game rules (game_config.py) and execution settings (run_config.toml).
Each game has a run_config.toml file for runtime settings:
[execution]
num_threads = 10 # Python simulation threads
rust_threads = 20 # Rust optimization threads
batching_size = 50000 # Simulations per batch
compression = false # Enable zstd compression
profiling = false # Enable performance profiling
[simulation]
base = 10000 # Base game simulations
bonus = 10000 # Bonus game simulations
super_spin = 10000 # Super spin simulations (optional)
[pipeline]
run_sims = true # Generate simulation books
run_optimization = true # Run Rust optimization
run_analysis = true # Generate PAR sheets
run_format_checks = true # Run RGS verification
target_modes = ["base", "bonus"]
[analysis]
custom_keys = [{ symbol = "scatter" }]Loading configuration:
from src.config.run_config import RunConfig
# Load from default run_config.toml
config = RunConfig.from_toml()
# Load from custom file
config = RunConfig.from_toml("dev.toml") # Development config
config = RunConfig.from_toml("prod.toml") # Production config
config = RunConfig.from_toml("test.toml") # Testing config
# Use environment variable (Makefile integration)
# CONFIG_FILE=dev.toml python run.py
config = RunConfig.from_toml() # Auto-detects CONFIG_FILE env var
# Access settings
print(config.execution.num_threads)
print(config.simulation.base)
print(config.pipeline.run_sims)Multiple Config Files Pattern:
All games have separate config files for different workflows, with run_config.toml as a symlink to the default configuration (typically dev.toml for fast development):
-
dev.toml- Development: Small simulations (100), no optimization, fast iteration (~10 seconds)[simulation] base = 100 [pipeline] run_optimization = false # Skip optimization for speed
-
prod.toml- Production: Large simulations (1M), full optimization, accurate statistics (~2 hours)[simulation] base = 1000000 [pipeline] run_optimization = true run_analysis = true compression = true
-
test.toml- Testing: Minimal simulations (100), for automated tests and CI/CD (~5 seconds)[simulation] base = 100 [pipeline] run_optimization = false run_analysis = false
Default Configuration:
run_config.tomlis a symlink todev.toml(default for fast development)- Running without CONFIG parameter uses
run_config.toml→dev.toml
Usage:
make run GAME=tower_treasures # Uses default (dev.toml via symlink)
make run GAME=tower_treasures CONFIG=prod.toml # Production run
make run GAME=tower_treasures CONFIG=test.toml # Quick test (if available)Benefits:
- TypeScript-like configuration pattern familiar to web developers
- Easy to edit settings without touching Python code
- Version control friendly (track config changes separately)
- Validation and error messages built-in
- Multiple config files per game (dev/prod/test) for different workflows
Games have multiple bet modes (base game, bonus game, etc.) each with their own:
- Reel strips (
games/<game_name>/reels/) - Distributions (symbol probabilities, multiplier values)
- Win conditions (free spin triggers, scatter requirements)
The BetMode class (defined in game_config.py) structures this:
self.bet_modes = {
"base": BetMode(...),
"bonus": BetMode(...)
}Reel File Naming Convention:
Reel strips are stored as CSV files with descriptive names:
base.csv- Base game reel stripfree.csv- Free spin reel stripwincap.csv- Win cap reel strip (high-win scenarios)free_wincap.csv- Free spin win cap reel stripsuper_spin.csv- Super spin reel strip (special game modes)super_spin_wincap.csv- Super spin win cap reel strip
These CSV files contain the reel layout (rows × reels) with symbol names. Game designers can edit them in Excel or any spreadsheet software.
Example reel configuration in game_config.py:
reels = {
"base": "base.csv",
"free": "free.csv",
"wincap": "wincap.csv"
}All game events use standardized constants from src/events/constants.py:
from src.events.constants import EventConstants
# Use constants instead of hardcoded strings
event = {
"type": EventConstants.WIN.value, # NOT "win"
"amount": 10.0
}Standard event types:
- Win events:
WIN,SET_FINAL_WIN,SET_WIN,SET_TOTAL_WIN,WIN_CAP - Free spins:
TRIGGER_FREE_SPINS,RETRIGGER_FREE_SPINS,END_FREE_SPINS - Tumbles:
TUMBLE,SET_TUMBLE_WIN,UPDATE_TUMBLE_WIN - Special symbols:
UPDATE_GLOBAL_MULTIPLIER,UPGRADE,REVEAL,REVEAL_EXPANDING_WILDS,UPDATE_EXPANDING_WILDS,ADD_STICKY_SYMBOLS
Event Filtering (Phase 3.2):
Events are automatically filtered based on configuration via the EventFilter class:
- REQUIRED events (always emitted):
WIN,REVEAL, triggers, tumbles, upgrades, expanding wilds, sticky symbols - STANDARD events (emit by default):
SET_WIN,SET_TOTAL_WIN,WIN_CAP,END_FREE_SPINS - VERBOSE events (optional):
UPDATE_FREE_SPINS,UPDATE_TUMBLE_WIN,SET_TUMBLE_WIN
Filtering is applied automatically when events are added to books via book.add_event(). All games inherit this functionality through GameState.
Located in src/calculations/:
cluster.py: Cluster-pay games (adjacent matching symbols)lines.py: Line-pay games (traditional paylines)ways.py: Ways-pay games (left-to-right ways)scatter.py: Scatter symbol detection
Games specify their win type in config and call the appropriate calculation method.
- Initialization: Load
GameConfig, createGameStateinstance - Run simulations:
create_books()(fromsrc.state.run_sims) runs N simulations - Books generation: Outcomes stored in "books" files (JSON/JSONL with simulation results)
- Optimization (optional): Rust program optimizes win distributions
- Analysis (optional): Generate PAR sheets and statistics
- Verification: RGS verification tests validate output format
Each simulation:
run_spin()→ Draw board → Calculate wins → Check triggers → Store events- Free spins use
run_free_spin()with separate loop
Simulation results go to games/<game_name>/ as "books" files:
- Format: JSON (if
output_regular_json=Trueingame_config.py) or JSONL - Compression: zstd (if
compression = trueinrun_config.toml) - Auto-formatting: When
compression = false, Makefile runsscripts/format_books_json.py
The formatter:
- Pretty-prints complex objects
- Keeps simple objects like
{"name": "L1"}on single lines - Can reconstruct corrupted JSONL files
The Rust optimization program (optimization_program/) uses genetic algorithms to adjust win distributions:
Configuration flow:
- Define
OptimizationSetupingames/<game_name>/game_optimization.py - Specify conditions (RTP targets, hit rates) and scaling factors
- Write setup to
optimization_program/src/setup.txt - Run optimization:
OptimizationExecution(...)in game'srun.py
Key optimization concepts:
- Conditions: Target RTP, hit rate (HR), average win constraints
- Scaling: Adjust specific win ranges (e.g., scale 10-20x wins by 1.5)
- Search conditions: Find specific outcomes (symbols, event types)
New Structure (Refactored Architecture):
games/<game_name>/
├── run.py # ⭐ UPDATED: Pure execution script (reads from TOML)
├── run_config.toml # ⭐ NEW: Runtime settings (threads, compression, pipeline)
├── game_config.py # Game configuration and BetMode setup
├── game_state.py # ALL game logic in one file (~100-400 lines)
│ # Sections: special symbols, state management,
│ # mechanics, win evaluation, game loops
├── game_optimization.py # Optimization parameters (optional)
├── game_events.py # Custom event generation (optional, rare)
├── reels/ # Reel strip files (CSV) per bet mode
├── build/ # Build output (generated, gitignored)
│ ├── books/ # Simulation results (JSON/JSONL)
│ ├── configs/ # Generated config files
│ ├── forces/ # Force files for testing
│ ├── lookup_tables/ # Lookup tables
│ └── optimization_files/ # Optimization results
└── tests/ # Game-specific unit tests (optional)
├── run_tests.py # Test runner
└── test_*.py # Individual test modules
src/ # Universal SDK modules
├── state/
│ ├── game_state.py # ⭐ Unified base class (850+ lines)
│ ├── books.py # Book class with EventFilter integration
│ └── run_sims.py # Simulation runner
├── calculations/
│ ├── board.py # Board generation (inherits from GameState)
│ ├── tumble.py # Tumble/cascade mechanics (optional)
│ ├── cluster.py # Cluster-pay calculations
│ ├── lines.py # Line-pay calculations
│ ├── ways.py # Ways-pay calculations
│ └── scatter.py # Scatter-pay calculations
├── config/ # Configuration classes
│ ├── config.py # Game config base class
│ ├── bet_mode.py # BetMode configuration
│ └── run_config.py # ⭐ NEW: TOML-based run configuration loader
├── events/ # Event system and constants
│ ├── constants.py # Standardized event type constants
│ ├── filter.py # ⭐ NEW: Event filtering (Phase 3.2)
│ ├── core.py # Core events (reveal, win, set_win, win_cap)
│ ├── free_spins.py # Free spins events (trigger, update, end)
│ ├── tumble.py # Tumble events (tumble, board multipliers)
│ ├── special_symbols.py # Special symbol events (upgrade, prize, multiplier)
│ └── helpers.py # Utilities (to_camel_case, convert_symbol_json)
├── formatter.py # ⭐ OutputFormatter for compression (Phase 3.1)
├── wins/ # Win manager
├── constants.py # ⭐ NEW: GameMode, WinType enums
├── exceptions.py # ⭐ NEW: Custom exception hierarchy
└── writers/ # Output file generation
│ ├── data.py # Books, lookup tables, force files
│ ├── configs.py # Frontend/backend config generation
│ └── force.py # Force file search/option classes
optimization_program/ # Rust optimization algorithm
└── src/
├── main.rs # Optimization engine
└── setup.toml # Optimization config (TOML format)
utils/ # Analysis and verification tools
├── game_analytics/ # PAR sheet generation, statistics
└── rgs_verification.py # Output format validation
Key Changes from Old Structure:
- ❌ Removed:
game_override.py,game_executables.py,game_calculations.pyper game - ❌ Removed:
src/state/state.py(GeneralGameState),src/state/state_conditions.py(Conditions),src/executables/executables.py(Executables) -- merged intoGameState - ✅ Simplified: All game logic now in single
game_state.pyfile - ✅ Added:
GameStateunified base class (src/state/game_state.py) - ✅ Added: Constants and exceptions modules
- ✅ Added:
src/formatter.pywith OutputFormatter (Phase 3.1) - ✅ Added: EventFilter in
events/(Phase 3.2)
- Copy
games/template/togames/<new_game>/ - Update
game_config.py: Setgame_id,game_name,win_type, paytable - Config files are already set up (copied from template):
dev.toml- Fast development (100 simulations, ~10 seconds)prod.toml- Production (1M simulations, ~2 hours)run_config.toml- Symlink todev.toml(default)- Adjust simulation counts in dev.toml/prod.toml if needed
- Create reel strips in
reels/directory (CSV format) - Implement game logic in
game_state.py:- Add special symbol handlers in the designated section
- Override state management methods if needed (e.g.,
reset_book()) - Implement game-specific mechanics
- Add win evaluation logic
- Complete
run_spin()andrun_free_spin()methods
- Test with:
make run GAME=<new_game>(uses dev.toml via symlink for fast iteration)
Config File Strategy:
- Default:
make run GAME=<game>usesrun_config.toml→dev.toml(100 simulations, ~10 seconds) - Production:
make run GAME=<game> CONFIG=prod.tomlfor final testing (1M simulations, ~2 hours) - Optional: Create
test.tomlfor automated tests if needed (100 simulations, ~5 seconds)
Note: The new architecture consolidates all game logic in a single game_state.py file. No need for separate game_override.py, game_executables.py, or game_calculations.py files. Runtime settings go in TOML config files, not run.py.
- Always use
EventConstantsfor event types, never hardcoded strings - All game logic is in
game_state.py- modify methods in their designated sections - Test changes with unit tests:
make unit-test GAME=<game_name> - For win calculation changes, modify the win evaluation section in
game_state.py - Follow the section structure from the template for consistency
Adding a custom event:
from src.events.constants import EventConstants
event = {
"index": len(self.book.events),
"type": EventConstants.REVEAL.value,
"board": board_client,
"gameType": "baseGame",
}
self.book.add_event(event)Special symbol with attributes:
def assign_special_symbol_functions(self):
self.special_symbol_functions = {
"M": [self.assign_mult_property]
}
def assign_mult_property(self, symbol):
multiplier = get_random_outcome(
self.get_current_distribution_conditions()["multiplier_values"][self.game_type]
)
symbol.assign_attribute({"multiplier": multiplier})Checking win conditions:
if self.check_free_spin_condition() and self.check_free_spin_entry():
self.run_free_spin_from_base()- Main tests:
pytest tests/- SDK-wide functionality tests - Game unit tests: Each game can have
tests/directory with isolated tests - Integration testing: Run full simulation with
make run GAME=<game> - RGS verification: Automatically runs format checks if
run_format_checks = trueinrun_config.toml
When compression = false in run_config.toml, the Makefile automatically formats books files:
- Script:
scripts/format_books_json.py - Keeps simple objects compact:
{"name": "L1"} - Pretty-prints complex structures for readability
- Handles JSONL and JSON formats
The game_optimization.py file configures optimization per bet mode:
"base": {
"conditions": {
"base_game": ConstructConditions(hr=3.5, rtp=0.59).return_dict(),
"free_game": ConstructConditions(rtp=0.37, hr=200,
search_conditions={"symbol": "scatter"}).return_dict(),
},
"scaling": ConstructScaling([...]).return_dict(),
"parameters": ConstructParameters(
num_show=5000, # Number of top outcomes to show
num_per_fence=10000, # Population size per optimization group
test_spins=[50, 100, 200], # Test spin counts
score_type="rtp" # Optimization scoring method
).return_dict()
}Before running optimization:
- Verify simulation books exist (run with
run_sims = trueinrun_config.tomlfirst) - Check optimization parameters in
game_optimization.py - Ensure Rust toolchain is installed:
cargo --version
- Main docs:
docs/directory (MkDocs format) - Online docs: https://stakeengine.github.io/math-sdk/
- View locally:
mkdocs serve(requires MkDocs installation)
- numpy: Array operations, statistics
- zstandard: Compression for books files
- xlsxwriter: Excel PAR sheet export
- pytest: Testing framework
- rust_decimal, rayon, serde_json: Rust optimization dependencies
- The SDK uses a virtual environment (
env/). Always activate before development. - Reels are stored as CSV files with symbol weights per position.
- The
BetModesystem allows different symbol distributions for base/bonus games. - Force files can override specific outcomes for testing (see
src/writers/force.py). - Each game is a singleton (
_instancepattern) to prevent duplicate initialization.