# Architecture: Hand History Logging & Tournament Improvements **Document Version:** 1.0 **Date:** 2026-05-19 **Scope:** Two major features for the rusty-marvin poker simulation engine --- ## Table of Contents 1. [Overview](#overview) 2. [Feature 1: Hand History Logging in iPoker Format](#feature-1-hand-history-logging) 3. [Feature 2: Tournament Improvements](#feature-2-tournament-improvements) 4. [Implementation Plan](#implementation-plan) 5. [Data Structures](#data-structures) 6. [Integration Points](#integration-points) --- ## Overview This document describes the architecture for two major features: 1. **Hand History Logging** — Log every hand played in PokerTracker 3 / iPoker format for later review, analysis, and a future hand tester function. 2. **Tournament Improvements** — Explicit blind arrays, SNG configuration with all 6 bot types, prize structure configuration, multi-tournament execution with balance tracking. --- ## Feature 1: Hand History Logging ### 1.1 Module Structure A new module `holdem_core/src/game/hand_history.rs` will be the sole location for all hand history formatting and file I/O. It will be registered in [`holdem_core/src/game/mod.rs`](holdem_core/src/game/mod.rs:21). ``` holdem_core/src/game/ ├── hand_history.rs ← NEW: iPoker formatter + file writer ├── hand_runner.rs ← MODIFY: expose action history in HandResult ├── table.rs ← MODIFY: pass HandHistoryContext to logger ├── simulation.rs ← MODIFY: integrate logging into cash game loop ├── tournament.rs ← MODIFY: integrate logging into tournament loop └── config.rs ← MODIFY: extend OutputConfig with log directory ``` ### 1.2 Data Flow The hand history logger needs data from multiple points in the hand lifecycle. The key challenge is that [`HandResult`](holdem_core/src/game/hand_runner.rs:87) currently does NOT include action history or player names — only pot winners, hand values, board, chip movements, and rake. ```mermaid flowchart TD A[Table::run_hand] --> B[HandRunner executes hand] B --> C[HandRunner collects actions_this_hand] B --> D[HandRunner produces HandResult] D --> E[Table::complete_hand restores seats] E --> F{log_hands enabled?} F -->|Yes| G[Build HandHistoryRecord] F -->|No| H[Skip logging] G --> I[HandHistoryLogger::log_hand] I --> J[Format iPoker text] J --> K[Write to logs/hand_N_YYYY-MM-DD_HH-MM-SS.txt] subgraph Data Sources L[HandResult: pot_winners, hand_values, board, chip_movements, rake] M[HandHistoryRecord: actions, player_names, hole_cards, blind_positions, timestamp] end L --> G M --> G ``` ### 1.3 Design Decision: Extend HandResult with Action History **Decision:** Add action history and supplementary data to [`HandResult`](holdem_core/src/game/hand_runner.rs:87) rather than exposing it separately from `HandRunner`. **Rationale:** - [`HandRunner::actions_this_hand`](holdem_core/src/game/hand_runner.rs:146) is a private field that already collects `Vec>` (4 elements: preflop, flop, turn, river). - [`HandRunner::into_seats()`](holdem_core/src/game/hand_runner.rs:838) consumes the runner, so actions must be extracted before or during that call. - Adding actions to `HandResult` keeps the API clean — callers get everything from one struct. - The `HandRunner::run_to_completion()` method already builds `HandResult` internally; we extend it to include actions. ### 1.4 New Data Structures #### `HandHistoryRecord` A self-contained record with everything needed to produce an iPoker hand history file. This is NOT stored in `HandResult` — it is assembled by the caller (Table or simulation loop) from `HandResult` plus external context. ```rust /// Complete record of a hand for history logging. pub struct HandHistoryRecord { /// Hand number within the session. pub hand_number: u64, /// Timestamp when the hand started. pub timestamp: chrono::DateTime, /// Table name (e.g., "Tournament 12345, Table 1" or "Cash Game Table 1"). pub table_name: String, /// Maximum seats at the table. pub max_seats: u8, /// Dealer button seat index (1-based for iPoker format). pub button_seat: u8, /// Player entries: seat number, name, starting stack. pub players: Vec, /// Blind postings: player name, amount, type (SB/BB/Ante). pub blind_postings: Vec, /// Actions per street: preflop, flop, turn, river. /// Each street is a list of (player_name, action_description). pub street_actions: Vec>, /// Board cards per street. pub board_cards: Vec>, /// Showdown entries: player name, hole cards, hand description. pub showdown_entries: Vec, /// Pot collection entries: player name, amount. pub collections: Vec, /// Total pot and rake. pub total_pot: ChipCount, pub total_rake: ChipCount, /// Summary information. pub summary: HandSummary, } ``` #### Supporting types: ```rust pub struct HandHistoryPlayer { pub seat: u8, // 1-based pub name: String, pub stack: ChipCount, } pub struct BlindPosting { pub player_name: String, pub amount: ChipCount, pub blind_type: BlindType, // SmallBlind, BigBlind, Ante } pub struct StreetAction { pub player_name: String, pub action_type: ActionDescription, pub amount: ChipCount, // 0 for fold/check } pub enum ActionDescription { Fold, Check, Call, Raise { total: ChipCount }, Bet { total: ChipCount }, AllIn { total: ChipCount }, } pub struct ShowdownEntry { pub player_name: String, pub seat: u8, pub hole_cards: Option, pub hand_description: String, // e.g., "a pair of Aces" pub result: ShowdownResult, // Won, Lost } pub struct PotCollection { pub player_name: String, pub amount: ChipCount, } pub struct HandSummary { pub total_pot: ChipCount, pub rake: ChipCount, pub board: Vec, pub player_summaries: Vec, } pub struct PlayerSummary { pub seat: u8, pub name: String, pub action: String, // e.g., "collected 800", "showed [As Kh] and lost" } ``` ### 1.5 Changes to HandResult Extend [`HandResult`](holdem_core/src/game/hand_runner.rs:87) to include action history: ```rust pub struct HandResult { // ... existing fields ... pub pot_winners: Vec>, pub hand_values: HashMap, pub board: Board, pub chip_movements: HashMap, pub total_rake: ChipCount, pub rake_per_pot: Vec, // NEW FIELDS: /// Actions taken during the hand, grouped by street. /// Index 0 = preflop, 1 = flop, 2 = turn, 3 = river. pub actions: Vec>, /// Hole cards for each player who was dealt cards. /// Keyed by PlayerId. Available for all players, not just showdown. pub hole_cards: HashMap, /// Seat assignments: PlayerId -> seat index (0-based). pub seat_assignments: HashMap, /// Blind positions: SB and BB seat indices. pub sb_seat: SeatIndex, pub bb_seat: SeatIndex, /// Dealer seat index. pub dealer_seat: SeatIndex, /// Small blind and big blind amounts used. pub small_blind: ChipCount, pub big_blind: ChipCount, pub ante: ChipCount, } ``` **Where to populate:** In [`HandRunner::run_to_completion()`](holdem_core/src/game/hand_runner.rs), after the hand finishes and before returning the result. The data is already available in `HandRunner`'s private fields: - `actions_this_hand` → `result.actions` - `seats[].hole_cards` → `result.hole_cards` - `seats[].index` → `result.seat_assignments` - `sb_seat_idx`, `bb_seat_idx` → `result.sb_seat`, `result.bb_seat` - `dealer_seat` → `result.dealer_seat` - `small_blind`, `big_blind`, `ante` → already available ### 1.6 HandHistoryLogger The logger is a struct that manages file output: ```rust /// Logs hand histories in iPoker format. pub struct HandHistoryLogger { /// Directory for log files. log_dir: PathBuf, /// Whether logging is enabled. enabled: bool, } impl HandHistoryLogger { pub fn new(log_dir: PathBuf, enabled: bool) -> Self; /// Ensures the log directory exists (creates if needed). pub fn ensure_dir(&self) -> std::io::Result<()>; /// Logs a hand to a file. pub fn log_hand(&self, record: &HandHistoryRecord) -> std::io::Result<()>; /// Generates the filename for a hand. fn filename(&self, record: &HandHistoryRecord) -> String { format!( "hand_{}_{}.txt", record.hand_number, record.timestamp.format("%Y-%m-%d_%H-%M-%S") ) } } ``` ### 1.7 iPoker Format Specification The formatter produces text matching the PokerTracker 3 / iPoker format. The format sections are: ``` Game # starts on Table "" (-max) Seat # is the button Seat : ( in chips) : posts small blind : posts big blind : posts ante *** HOLE CARDS *** Dealt to [ ] : folds : calls : raises to : checks *** FLOP *** [ ] ... actions ... *** TURN *** [ ] [] ... actions ... *** RIVER *** [ ] [] ... actions ... *** SHOW DOWN *** : shows [ ] () collects from pot *** SUMMARY *** Total pot | Rake Board: [ ] Seat : collected (showed [ ] and won with ) Seat : showed [ ] and lost with Seat : folded before flop (did not show) ``` **Card formatting:** Cards use the notation from [`Card::Display`](holdem_core/src/card.rs) — rank char + suit char, e.g., `As`, `Kh`, `Td`, `2c`. Rank mapping: `2-9, T, J, Q, K, A`. Suit mapping: `h, d, c, s`. **Hand description mapping** from [`HandRank`](holdem_core/src/evaluator/evaluator.rs:14): | HandRank | iPoker Description | |----------|-------------------| | HighCard | "high card Ace" / "high card King" etc. | | Pair | "a pair of Aces" | | TwoPair | "two pairs, Kings over Queens" | | ThreeOfAKind | "three of a kind, Aces" | | Straight | "a straight, Ace to Five" | | Flush | "a flush, Ace high" | | FullHouse | "a full house, Aces over Kings" | | FourOfAKind | "four of a kind, Aces" | | StraightFlush | "a straight flush, Ace to Five" | | RoyalFlush | "a royal flush" | ### 1.8 Integration Points #### Cash Game Integration In [`run_cash_game()`](holdem_core/src/game/simulation.rs:33), after each `table.run_hand()`: ```rust // Create logger from config let logger = HandHistoryLogger::new( PathBuf::from("logs"), config_output.log_hands, ); logger.ensure_dir()?; // In the hand loop: match table.run_hand() { Ok(result) => { stats.record_hand_result(&result, &player_names); if logger.is_enabled() { let record = build_hand_history_record( &result, &player_names, &table, "Cash Game Table 1" ); logger.log_hand(&record)?; } } // ... } ``` #### Tournament Integration In [`Tournament::run_next_hands()`](holdem_core/src/game/tournament.rs:362), after each hand result: ```rust // Tournament accepts an optional HandHistoryLogger if let Some(ref logger) = self.hand_logger { let record = build_hand_history_record( &hand_result, &self.player_names, &table, &table_name ); logger.log_hand(&record)?; } ``` ### 1.9 Building HandHistoryRecord A helper function constructs `HandHistoryRecord` from the components: ```rust /// Builds a HandHistoryRecord from a completed hand result. /// /// Requires: /// - HandResult (with extended action/seat data) /// - Player name mapping (PlayerId -> name) /// - Table reference (for table name, max seats) pub fn build_hand_history_record( result: &HandResult, player_names: &HashMap, table: &Table, table_name: &str, ) -> HandHistoryRecord { // 1. Map seat_assignments to HandHistoryPlayer list // 2. Extract blind postings from actions[0] (preflop) where action is Raise for SB/BB // 3. Map actions per street to StreetAction with player names // 4. Map board cards from result.board // 5. Build showdown entries from result.pot_winners + result.hand_values + result.hole_cards // 6. Build collections from result.pot_winners // 7. Build summary } ``` ### 1.10 Config Changes Extend [`OutputConfig`](holdem_core/src/game/config.rs:442): ```rust pub struct OutputConfig { pub directory: Option, pub log_hands: bool, pub log_actions: bool, // NEW: /// Directory for hand history log files. Defaults to "logs". #[serde(default = "default_log_dir")] pub log_dir: Option, } fn default_log_dir() -> String { "logs".to_string() } ``` ### 1.11 Dependency Addition Add `chrono` to [`holdem_core/Cargo.toml`](holdem_core/Cargo.toml) for timestamp support: ```toml [dependencies] chrono = { version = "0.4", features = ["serde"] } ``` --- ## Feature 2: Tournament Improvements ### 2.1 Explicit Blind Levels in TOML #### Config Changes Add optional `blind_levels` field to [`GameConfig`](holdem_core/src/game/config.rs:235): ```rust pub struct GameConfig { // ... existing fields ... /// Explicit blind levels. If present, overrides num_blind_levels and hands_per_level. #[serde(default)] pub blind_levels: Vec, } /// Serde-compatible blind level for TOML configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BlindLevelConfig { pub small_blind: ChipCount, pub big_blind: ChipCount, #[serde(default)] pub ante: ChipCount, #[serde(default = "default_hands_per_level")] pub hands: u32, } ``` TOML format: ```toml [[game.blind_levels]] small_blind = 10 big_blind = 20 ante = 0 hands = 10 [[game.blind_levels]] small_blind = 15 big_blind = 30 ante = 0 hands = 10 ``` #### Updated `to_blind_schedule()` Modify [`GameConfig::to_blind_schedule()`](holdem_core/src/game/config.rs:391) to check for explicit levels first: ```rust pub fn to_blind_schedule(&self) -> BlindSchedule { if !self.blind_levels.is_empty() { // Use explicit levels from config let levels: Vec = self.blind_levels .iter() .enumerate() .map(|(i, lc)| BlindLevel { level: (i + 1) as u32, small_blind: lc.small_blind, big_blind: lc.big_blind, ante: lc.ante, duration_hands: lc.hands, }) .collect(); BlindSchedule::custom(levels) } else { // Existing standard doubling schedule // ... (current code) } } ``` ### 2.2 Custom Prize Structure #### Config Changes Add optional `prizes` field to [`GameConfig`](holdem_core/src/game/config.rs:235): ```rust pub struct GameConfig { // ... existing fields ... /// Custom prize structure as absolute amounts. If present, overrides /// standard_sng / standard_mtt calculations. /// Example: [5, 3, 2] means 1st=5, 2nd=3, 3rd=2. #[serde(default)] pub prizes: Vec, } ``` TOML format: ```toml [game] prizes = [5, 3, 2] ``` #### Updated `to_prize_structure()` Modify [`GameConfig::to_prize_structure()`](holdem_core/src/game/config.rs:423): ```rust pub fn to_prize_structure(&self) -> PrizeStructure { if !self.prizes.is_empty() { PrizeStructure { prizes: self.prizes.clone() } } else { match self.game_type { GameType::Cash => PrizeStructure::standard_sng(self.buy_in, num_players), GameType::SitAndGo => PrizeStructure::standard_sng(self.buy_in, num_players), GameType::MultiTableTournament => { PrizeStructure::standard_mtt(self.buy_in, total_entries) } } } } ``` ### 2.3 Seat Switching Between Tournaments #### Current Behavior - [`TableConfig.randomize_seats`](holdem_core/src/game/table.rs:51) shuffles seats before each hand in cash games. - [`Table::shuffle_seats()`](holdem_core/src/game/table.rs) already exists and works. - In tournaments, [`Table::start_hand()`](holdem_core/src/game/table.rs:327) calls `shuffle_seats()` if `randomize_seats` is true, but this is per-hand, not per-tournament. #### Design For multi-tournament SNG, we need seat randomization **between tournaments**, not between hands within a tournament. The approach: 1. **Keep `randomize_seats` on `GameConfig`** with its current meaning for cash games. 2. **In the multi-tournament runner** (new function), call `table.shuffle_seats()` once before each tournament starts, but set `randomize_seats = false` on the `TableConfig` so it does not shuffle per-hand. ```rust /// Runs multiple SNG tournaments and aggregates results. pub fn run_sng_series( config: &GameConfig, num_tournaments: u32, ) -> Result { let mut aggregate_stats = SimulationStats::new("SitAndGo Series"); for tournament_num in 0..num_tournaments { let mut table_config = config.to_table_config(); table_config.randomize_seats = false; // don't shuffle per-hand let blind_schedule = config.to_blind_schedule(); let prize_structure = config.to_prize_structure(); let mut tournament = Tournament::new_sng( table_config, blind_schedule, prize_structure, config.starting_chips, config.buy_in, ); // Register bots (fresh instances each tournament) for (i, bot_config) in config.bots.iter().enumerate() { let player_id = PlayerId(i as u64); let bot = create_bot(bot_config, player_id)?; tournament.register_player(bot)?; } // Shuffle seats between tournaments if config.randomize_seats { tournament.shuffle_seats()?; // NEW method on Tournament } // Run tournament tournament.run_to_completion()?; // Record results aggregate_stats.record_tournament_result( &tournament, &player_names, config.buy_in ); } // ... compute aggregate stats } ``` #### New `Tournament::shuffle_seats()` Add a method to [`Tournament`](holdem_core/src/game/tournament.rs:228) that shuffles seats on all tables: ```rust /// Shuffles seat assignments on all tables. /// Used between tournaments in a series to randomize positions. pub fn shuffle_seats(&mut self) { for table in &mut self.tables { table.shuffle_seats(); } } ``` ### 2.4 Multi-Tournament Execution #### Config Changes Add `num_tournaments` to [`OutputConfig`](holdem_core/src/game/config.rs:442) (or `GameConfig`): ```rust pub struct OutputConfig { // ... existing fields ... /// Number of tournaments to run in a series. Default: 1. #[serde(default = "default_one")] pub num_tournaments: u32, } ``` TOML: ```toml [output] log_hands = true num_tournaments = 100 ``` #### New: SngSeriesResult Aggregate results across multiple tournaments: ```rust /// Results from a series of SNG tournaments. pub struct SngSeriesResult { /// Number of tournaments played. pub tournaments_played: u32, /// Per-player aggregate statistics. pub player_results: HashMap, /// Total hands played across all tournaments. pub total_hands: u64, } /// Aggregate results for one player across a tournament series. pub struct SngPlayerSeriesResult { pub player_id: PlayerId, pub name: String, /// Number of tournaments this player won (1st place). pub wins: u32, /// Number of times this player finished in each paid position. pub finishes: HashMap, // position -> count /// Total entry fees paid (num_tournaments * buy_in). pub total_entry_fees: ChipCount, /// Total prize money won. pub total_prize_money: ChipCount, /// Net profit = total_prize_money - total_entry_fees. pub net_profit: i64, /// ROI as percentage = (net_profit / total_entry_fees) * 100. pub roi_percent: f64, /// Average finishing position. pub avg_finish: f64, } ``` ### 2.5 Balance Tracking (Money, Not Chips) The balance tracking is about tracking **real money** (entry fees vs. prize money) across multiple tournaments, not chip balances within a single tournament. **Key insight:** Within a single tournament, chips are play money. The financial outcome is: - **Cost:** `buy_in` per tournament - **Revenue:** `prize` based on finishing position - **Profit:** `prize - buy_in` This is already partially implemented in [`SimulationStats::record_tournament_result()`](holdem_core/src/game/stats.rs:224) which computes `profit = prize - buy_in`. The multi-tournament aggregation adds: 1. Accumulating profits across N tournaments 2. Computing ROI across the series 3. Tracking position distribution (how often each player finishes 1st, 2nd, 3rd, etc.) ### 2.6 Data Flow for Multi-Tournament ```mermaid flowchart TD A[Load SimulationConfig] --> B[Parse blind_levels, prizes, num_tournaments] B --> C{For each tournament 1..N} C --> D[Create fresh Tournament instance] D --> E[Create fresh bot instances] E --> F{randomize_seats?} F -->|Yes| G[Tournament.shuffle_seats] F -->|No| H[Skip] G --> I[Register bots] H --> I I --> J[Tournament.run_to_completion] J --> K[Record tournament result] K --> L[Accumulate SngPlayerSeriesResult] L --> C C -->|Done| M[Compute aggregate stats] M --> N[Print summary table] M --> O[Optionally log individual tournament results] ``` ### 2.7 Main.rs Changes Update [`holdem_sim/src/main.rs`](holdem_sim/src/main.rs) to handle multi-tournament: ```rust fn run_tournament_simulation(config: &GameConfig, output_config: &OutputConfig) { let num_tournaments = output_config.num_tournaments.max(1); if num_tournaments > 1 { // Run SNG series run_sng_series(config, num_tournaments); } else { // Single tournament (existing behavior) // ... current code ... } } ``` The `SimulationConfig` needs to be threaded through to the tournament runner so `output.num_tournaments` is accessible. --- ## Implementation Plan ### Phase 1: Hand History Logging | Step | File | Action | |------|------|--------| | 1.1 | `holdem_core/Cargo.toml` | Add `chrono` dependency | | 1.2 | `holdem_core/src/game/hand_runner.rs` | Extend `HandResult` with `actions`, `hole_cards`, `seat_assignments`, `sb_seat`, `bb_seat`, `dealer_seat`, `small_blind`, `big_blind`, `ante` fields. Populate them in `run_to_completion()` before returning. | | 1.3 | `holdem_core/src/game/hand_history.rs` | Create new module with `HandHistoryRecord`, supporting types, `HandHistoryLogger`, `build_hand_history_record()`, and iPoker formatter | | 1.4 | `holdem_core/src/game/mod.rs` | Add `pub mod hand_history;` and re-export key types | | 1.5 | `holdem_core/src/game/config.rs` | Add `log_dir` field to `OutputConfig` | | 1.6 | `holdem_core/src/game/simulation.rs` | Integrate `HandHistoryLogger` into `run_cash_game()` | | 1.7 | `holdem_core/src/game/table.rs` | Expose `hand_number()` and `config()` publicly if not already | | 1.8 | Tests | Unit tests for iPoker formatter, integration test for full hand logging | ### Phase 2: Tournament Config Improvements | Step | File | Action | |------|------|--------| | 2.1 | `holdem_core/src/game/config.rs` | Add `BlindLevelConfig` struct, `blind_levels` field to `GameConfig`, `prizes` field to `GameConfig` | | 2.2 | `holdem_core/src/game/config.rs` | Update `to_blind_schedule()` to use explicit levels when present | | 2.3 | `holdem_core/src/game/config.rs` | Update `to_prize_structure()` to use custom prizes when present | | 2.4 | `holdem_core/examples/configs/sng_tournament.toml` | Update example config with explicit blind levels and prize structure | | 2.5 | Tests | Unit tests for TOML parsing of explicit blind levels and custom prizes | ### Phase 3: Multi-Tournament Execution | Step | File | Action | |------|------|--------| | 3.1 | `holdem_core/src/game/config.rs` | Add `num_tournaments` to `OutputConfig` | | 3.2 | `holdem_core/src/game/tournament.rs` | Add `Tournament::shuffle_seats()` method | | 3.3 | `holdem_core/src/game/simulation.rs` | Create `run_sng_series()` function with `SngSeriesResult` | | 3.4 | `holdem_core/src/game/stats.rs` | Add `SngPlayerSeriesResult` and aggregation logic | | 3.5 | `holdem_sim/src/main.rs` | Update to pass `OutputConfig` to tournament runner, handle `num_tournaments > 1` | | 3.6 | Tests | Integration test for multi-tournament series | ### Phase 4: Tournament Hand Logging Integration | Step | File | Action | |------|------|--------| | 4.1 | `holdem_core/src/game/tournament.rs` | Add optional `HandHistoryLogger` to `Tournament`, integrate into `run_next_hands()` | | 4.2 | `holdem_core/src/game/tournament.rs` | Track player names mapping inside Tournament for logging | | 4.3 | Tests | Integration test verifying hand logs from tournament play | --- ## Data Structures ### Summary of All New Types | Type | Location | Purpose | |------|----------|---------| | `HandHistoryRecord` | `hand_history.rs` | Complete hand record for logging | | `HandHistoryPlayer` | `hand_history.rs` | Player info in hand history | | `BlindPosting` | `hand_history.rs` | Blind/ante posting record | | `StreetAction` | `hand_history.rs` | Player action on a street | | `ActionDescription` | `hand_history.rs` | Action type for logging | | `ShowdownEntry` | `hand_history.rs` | Showdown participant | | `PotCollection` | `hand_history.rs` | Pot collection record | | `HandSummary` | `hand_history.rs` | Hand summary section | | `PlayerSummary` | `hand_history.rs` | Per-player summary | | `HandHistoryLogger` | `hand_history.rs` | File writer for hand histories | | `BlindLevelConfig` | `config.rs` | TOML-deserializable blind level | | `SngSeriesResult` | `simulation.rs` | Multi-tournament aggregate | | `SngPlayerSeriesResult` | `simulation.rs` | Per-player series statistics | ### Summary of Modified Types | Type | Location | Changes | |------|----------|---------| | `HandResult` | `hand_runner.rs` | Add `actions`, `hole_cards`, `seat_assignments`, blind position fields | | `GameConfig` | `config.rs` | Add `blind_levels: Vec`, `prizes: Vec` | | `OutputConfig` | `config.rs` | Add `log_dir: Option`, `num_tournaments: u32` | | `Tournament` | `tournament.rs` | Add `shuffle_seats()` method, optional `HandHistoryLogger` | --- ## Integration Points ### How New Code Connects to Existing Code ```mermaid flowchart LR subgraph Config Layer GC[GameConfig] OC[OutputConfig] end subgraph Engine Layer HR[HandRunner] T[Table] TN[Tournament] end subgraph New Modules HH[hand_history.rs] SS[run_sng_series] end subgraph Output LOG[logs/ directory] STATS[SimulationStats] SNG[SngSeriesResult] end GC -->|blind_levels| TN GC -->|prizes| TN OC -->|log_hands, log_dir| HH OC -->|num_tournaments| SS HR -->|Extended HandResult| HH HR -->|Extended HandResult| STATS T -->|run_hand result| HH TN -->|per-hand results| HH TN -->|standings| STATS SS -->|aggregate| SNG HH -->|writes| LOG ``` ### Key API Surfaces 1. **`HandResult` extensions** — The extended struct is backward-compatible. New fields are populated automatically by `HandRunner::run_to_completion()`. Existing callers that destructure `HandResult` will get compiler warnings for unused fields but no breakage. 2. **`HandHistoryLogger`** — Standalone struct with no trait dependencies. Created by the simulation runner and optionally passed to `Table`/`Tournament`. 3. **`BlindLevelConfig`** — Pure config struct. Only affects `GameConfig::to_blind_schedule()`. 4. **`run_sng_series()`** — New function in `simulation.rs`. Does not modify existing `run_cash_game()` or single-tournament flow. 5. **`Tournament::shuffle_seats()`** — Delegates to existing `Table::shuffle_seats()`. No new behavior, just an access method. ### Backward Compatibility - All new `GameConfig` fields use `#[serde(default)]`, so existing TOML configs continue to work. - `HandResult` extensions are additive — existing code that ignores new fields is unaffected. - `OutputConfig` extensions are additive with sensible defaults. - The single-tournament path in `main.rs` is preserved; multi-tournament is triggered only when `num_tournaments > 1`.