Game Structure¶
This document explains the architecture and file organization of games in the Math SDK.
Game Directory Layout¶
New Architecture (Refactored - January 2026)
Each game lives in games/<game_name>/ with this simplified structure:
games/<game_name>/
├── run.py # Pure execution script (reads from TOML)
├── run_config.toml # Runtime settings (threads, compression, pipeline)
├── game_config.py # Game configuration and BetMode setup
├── gamestate.py # ALL game logic in one file (~100-400 lines)
├── game_optimization.py # Optimization parameters (optional)
├── game_events.py # Custom event generation (optional, rare)
├── reels/ # Reel strip CSV files (source files, committed)
│ ├── base.csv
│ ├── free.csv
│ ├── wincap.csv
│ └── ...
├── 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_*.py
Key Changes from Old Architecture¶
Removed (Old Structure):
- ❌ game_override.py - Logic merged into gamestate.py
- ❌ game_executables.py - Logic merged into gamestate.py
- ❌ game_calculations.py - Logic merged into gamestate.py
- ❌ library/ folder - Renamed to build/ to match modern conventions
Added (New Structure):
- ✅ run_config.toml - TOML-based runtime configuration
- ✅ gamestate.py - Single consolidated file for all game logic
- ✅ build/ folder - Clear separation of source vs generated artifacts
Benefits: - 67% reduction in inheritance complexity (6 → 2 layers) - 75% reduction in files per game (4 → 1 file per game) - Easier to understand and maintain - Single file contains all game-specific logic
Class Inheritance Hierarchy¶
New Simplified Architecture:
Games follow a 2-layer 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)
Base Classes¶
GameState (src/state/game_state.py)
- Core simulation infrastructure (~850+ lines)
- Books management
- Event system with EventFilter integration
- State management
- Common actions (reset, special symbols, etc.)
Board (src/calculations/board.py)
- Random board generation
- Forced symbols
- Special symbol tracking
- Inherits from GameState
Tumble (src/calculations/tumble.py)
- Cascade/tumble mechanics
- For games with falling symbols
- Inherits from Board
GameState (games/
GameState File Structure¶
The gamestate.py file is organized into clear sections:
from src.calculations.board import Board # or Tumble
class GameState(Board): # or Tumble
"""Game-specific logic for <game_name>"""
# ============================================================
# SECTION 1: SPECIAL SYMBOL HANDLERS
# ============================================================
def assign_special_symbol_functions(self):
"""Map symbols to handler functions"""
self.special_symbol_functions = {
"M": [self.assign_multiplier_property],
"W": [self.handle_wild],
}
def assign_multiplier_property(self, symbol):
"""Assign multiplier to symbol"""
multiplier = get_random_outcome(...)
symbol.assign_attribute({"multiplier": multiplier})
# ============================================================
# SECTION 2: STATE MANAGEMENT OVERRIDES
# ============================================================
def reset_book(self):
"""Reset per-spin state"""
super().reset_book()
self.custom_state = 0
# ============================================================
# SECTION 3: GAME-SPECIFIC MECHANICS
# ============================================================
def check_bonus_trigger(self):
"""Check if bonus game should trigger"""
scatter_count = self.board.count_symbol("scatter")
return scatter_count >= 3
# ============================================================
# SECTION 4: WIN EVALUATION
# ============================================================
def evaluate_wins(self):
"""Calculate wins based on win_type"""
if self.config.win_type == "cluster":
self.cluster_wins()
elif self.config.win_type == "lines":
self.line_wins()
# ============================================================
# SECTION 5: MAIN GAME LOOPS
# ============================================================
def run_spin(self):
"""Main spin logic for base game"""
self.draw_board()
self.evaluate_wins()
if self.check_bonus_trigger():
self.run_freespin_from_base()
return self.book
def run_freespin(self):
"""Free spin logic"""
self.draw_board()
self.evaluate_wins()
return self.book
Typical File Size¶
- Simple games: ~100-150 lines
- Medium complexity: ~200-300 lines
- Complex games: ~300-400 lines
All in ONE readable file instead of scattered across 4+ files.
Configuration System¶
GameConfig Class¶
Located in game_config.py:
from src.config.config import Config
from src.config.bet_mode import BetMode
from src.formatter import OutputMode
class GameConfig(Config):
def __init__(self):
super().__init__()
# Basic settings
self.game_id = "tower_treasures"
self.game_name = "Tower Treasures"
self.win_type = "cluster" # or "lines", "ways", "scatter"
self.rtp = 0.9700
# Board dimensions
self.num_reels = 5
self.num_rows = 3
# Paytable
self.paytable = {
"A": {5: 50, 4: 20, 3: 5},
"K": {5: 40, 4: 15, 3: 4},
# ...
}
# Output optimization (Phase 3)
self.output_mode = OutputMode.COMPACT # or OutputMode.VERBOSE
self.simple_symbols = True
self.compress_positions = True
# BetModes
self.betmodes = {
"base": BetMode(...),
"bonus": BetMode(...)
}
Run Configuration (TOML)¶
New in January 2026: Runtime settings separated into run_config.toml:
[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
[pipeline]
run_sims = true # Generate simulation books
run_optimization = false # Run Rust optimization
run_analysis = false # Generate PAR sheets
run_format_checks = true # Run RGS verification
target_modes = ["base", "bonus"]
[analysis]
custom_keys = [{ symbol = "scatter" }]
Benefits:
- Clear separation: game_config.py = rules, run_config.toml = execution
- Easy to edit without touching code
- Multiple configs per game (dev/prod/test)
BetMode System¶
Each game mode has its own configuration:
from src.config.bet_mode import BetMode
self.betmodes = {
"base": BetMode(
reels={"base": "base.csv"}, # Reel file mapping
distributions={
"multiplier_values": [2, 3, 5, 10],
"weights": [50, 30, 15, 5]
},
conditions={
"min_scatters": 3, # Trigger condition
}
),
"bonus": BetMode(
reels={"free": "free.csv", "wincap": "wincap.csv"},
# ... bonus-specific config
)
}
Event System¶
All events use constants from src/events/constants.py:
from src.events.constants import EventConstants
# Create an event
event = {
"index": len(self.book.events),
"type": EventConstants.WIN.value,
"amount": 1000,
"details": [{"symbol": "A", "positions": [[0, 0], [0, 1]]}],
}
# Add to book (automatically filtered based on config)
self.book.add_event(event)
Standard Event Types¶
- Wins:
WIN,SET_FINAL_WIN,SET_WIN,SET_TOTAL_WIN,WIN_CAP - Free Spins:
TRIGGER_FREE_SPINS,RETRIGGER_FREE_SPINS,END_FREE_SPINS,UPDATE_FREE_SPINS - Tumbles:
TUMBLE,SET_TUMBLE_WIN,UPDATE_TUMBLE_WIN - Special:
UPDATE_GLOBAL_MULTIPLIER,UPGRADE,REVEAL
Event Filtering (Phase 3.2)¶
Events are automatically filtered based on configuration:
# In game_config.py
config.skip_derived_wins = True # Skip SET_WIN, SET_TOTAL_WIN
config.skip_progress_updates = True # Skip UPDATE_FREE_SPINS
config.verbose_event_level = "standard" # "full", "standard", or "minimal"
Win Calculation Types¶
The SDK supports multiple win calculation methods:
Cluster Pay¶
# game_config.py
self.win_type = "cluster"
# Uses src/calculations/cluster.py
# Adjacent matching symbols form winning clusters
Line Pay¶
# game_config.py
self.win_type = "lines"
self.paylines = [
[1, 1, 1, 1, 1], # Middle line
[0, 0, 0, 0, 0], # Top line
# ...
]
# Uses src/calculations/lines.py
# Symbols along defined paylines
Ways Pay¶
# game_config.py
self.win_type = "ways"
# Uses src/calculations/ways.py
# Left-to-right matching symbols (any position)
Scatter Pay¶
Simulation Flow¶
- Initialization
- Load
GameConfigfromgame_config.py - Load
RunConfigfromrun_config.toml - Create
GameStateinstance -
Set up reel strips and distributions
-
Run Simulations
create_books()fromsrc/state/run_sims-
Runs N simulations per bet mode
-
Single Simulation
-
Books Generation
- Write simulation results to
build/books/ - Generate probability CSV files
- Optional: Format, compress, verify
Build Output Structure¶
The build/ folder contains all generated artifacts:
games/<game_name>/build/
├── books/ # Simulation results
│ ├── <game>_base_books.json
│ ├── <game>_base_probs.csv
│ └── ...
├── configs/ # Generated config files
├── forces/ # Force files for optimization
├── lookup_tables/ # Lookup tables
├── optimization_files/ # Optimization results
└── temp_multi_threaded_files/ # Temporary files
All build/ contents are gitignored - these are generated artifacts, not source files.
Creating a New Game¶
1. Copy Template¶
2. Update Config¶
Edit games/my_new_game/game_config.py:
self.game_id = "my_new_game"
self.game_name = "My New Game"
self.win_type = "cluster" # Choose win type
# ... configure paytable, betmodes, etc.
3. Update Run Config¶
Edit games/my_new_game/run_config.toml:
[simulation]
base = 1000 # Start small for testing
[execution]
compression = false # Readable output for debugging
[pipeline]
run_sims = true
run_optimization = false # Disable initially
run_analysis = false
4. Create Reel Strips¶
Create CSV files in games/my_new_game/reels/:
5. Implement Game Logic¶
Edit games/my_new_game/gamestate.py:
from src.calculations.board import Board
class GameState(Board):
"""My new game logic"""
def assign_special_symbol_functions(self):
"""Define special symbols"""
self.special_symbol_functions = {
"M": [self.assign_mult],
}
def evaluate_wins(self):
"""Calculate wins"""
if self.config.win_type == "cluster":
self.cluster_wins()
def run_spin(self):
"""Main game loop"""
self.draw_board()
self.evaluate_wins()
return self.book
def run_freespin(self):
"""Free spin loop"""
self.draw_board()
self.evaluate_wins()
return self.book
6. Test¶
Best Practices¶
Use EventConstants¶
Single File Organization¶
# ✅ Good - all logic in gamestate.py, organized by sections
class GameState(Board):
# Special symbols section
def assign_special_symbol_functions(self): ...
# Mechanics section
def check_bonus(self): ...
# Game loops section
def run_spin(self): ...
# ❌ Bad - don't split into multiple files
# game_override.py, game_executables.py, etc.
Use Distributions¶
# ✅ Good - use distributions from config
multiplier = get_random_outcome(
self.get_current_distribution_conditions()["multiplier_values"]
)
# ❌ Bad - hardcoded random
multiplier = random.choice([2, 3, 5, 10])
Configuration vs Execution Settings¶
# ✅ Good - game rules in game_config.py
class GameConfig(Config):
self.paytable = {...}
self.rtp = 0.97
# ✅ Good - execution settings in run_config.toml
[simulation]
base = 10000
# ❌ Bad - don't mix concerns
Migration from Old Architecture¶
If you have an old game with multiple files:
- Merge files into gamestate.py:
- Copy special symbol functions from
game_override.py - Copy win calculation from
game_executables.py - Copy helper functions from
game_calculations.py -
Copy game loops from
game_state.py -
Organize into sections (see GameState File Structure above)
-
Update inheritance:
GameState(Board)instead ofGameState(GameStateOverride) -
Test thoroughly:
make run GAME=<game>andmake unit-test GAME=<game>
See Migration History for complete refactoring details.
See Also¶
- Running Games - How to run simulations
- Event System - Working with events
- Optimization - Optimizing distributions
- Migration History - Complete refactoring story
- CLAUDE.md - Comprehensive technical reference