Major changes: - Add hand history module (iPoker format) with HandHistoryLogger - Add run_sng_series() for multi-tournament aggregate statistics - Fix MTT table consolidation deadlock (sub-minimum-player tables) - Add Meerkat-compatible convenience methods on Action and GameInfo - Add custom blind levels, prize structures, auto-rebuy to GameConfig - Add SngSeriesResult/SngPlayerSeriesResult for series tracking - Add Tournament::winner(), tables_mut(), shuffle_seats() Review fixes: - Fix TOML prizes field ordering (must precede [[game.blind_levels]]) - Add sensible serde defaults for small_blind (10) and big_blind (20) - Fix prize assignment in run_sng_series (use raw_placement directly) - Handle hand history filename collisions with suffixed retry - Add debug_assert guard on collect_hand_history_data double-consumption
29 KiB
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
- Overview
- Feature 1: Hand History Logging in iPoker Format
- Feature 2: Tournament Improvements
- Implementation Plan
- Data Structures
- Integration Points
Overview
This document describes the architecture for two major features:
- Hand History Logging — Log every hand played in PokerTracker 3 / iPoker format for later review, analysis, and a future hand tester function.
- 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/
├── 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 currently does NOT include action history or player names — only pot winners, hand values, board, chip movements, and rake.
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 rather than exposing it separately from HandRunner.
Rationale:
HandRunner::actions_this_handis a private field that already collectsVec<Vec<(PlayerId, Action)>>(4 elements: preflop, flop, turn, river).HandRunner::into_seats()consumes the runner, so actions must be extracted before or during that call.- Adding actions to
HandResultkeeps the API clean — callers get everything from one struct. - The
HandRunner::run_to_completion()method already buildsHandResultinternally; 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.
/// 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<chrono::Utc>,
/// 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<HandHistoryPlayer>,
/// Blind postings: player name, amount, type (SB/BB/Ante).
pub blind_postings: Vec<BlindPosting>,
/// Actions per street: preflop, flop, turn, river.
/// Each street is a list of (player_name, action_description).
pub street_actions: Vec<Vec<StreetAction>>,
/// Board cards per street.
pub board_cards: Vec<Vec<Card>>,
/// Showdown entries: player name, hole cards, hand description.
pub showdown_entries: Vec<ShowdownEntry>,
/// Pot collection entries: player name, amount.
pub collections: Vec<PotCollection>,
/// Total pot and rake.
pub total_pot: ChipCount,
pub total_rake: ChipCount,
/// Summary information.
pub summary: HandSummary,
}
Supporting types:
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<HoleCards>,
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<Card>,
pub player_summaries: Vec<PlayerSummary>,
}
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 to include action history:
pub struct HandResult {
// ... existing fields ...
pub pot_winners: Vec<Vec<PotWinner>>,
pub hand_values: HashMap<PlayerId, HandValue>,
pub board: Board,
pub chip_movements: HashMap<PlayerId, i64>,
pub total_rake: ChipCount,
pub rake_per_pot: Vec<ChipCount>,
// NEW FIELDS:
/// Actions taken during the hand, grouped by street.
/// Index 0 = preflop, 1 = flop, 2 = turn, 3 = river.
pub actions: Vec<Vec<(PlayerId, Action)>>,
/// Hole cards for each player who was dealt cards.
/// Keyed by PlayerId. Available for all players, not just showdown.
pub hole_cards: HashMap<PlayerId, HoleCards>,
/// Seat assignments: PlayerId -> seat index (0-based).
pub seat_assignments: HashMap<PlayerId, SeatIndex>,
/// 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(), after the hand finishes and before returning the result. The data is already available in HandRunner's private fields:
actions_this_hand→result.actionsseats[].hole_cards→result.hole_cardsseats[].index→result.seat_assignmentssb_seat_idx,bb_seat_idx→result.sb_seat,result.bb_seatdealer_seat→result.dealer_seatsmall_blind,big_blind,ante→ already available
1.6 HandHistoryLogger
The logger is a struct that manages file output:
/// 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 #<hand_number> starts on <YYYY/MM/DD HH:MM:SS>
Table "<table_name>" (<max_seats>-max) Seat #<button_seat_1based> is the button
Seat <N>: <name> (<stack> in chips)
<name>: posts small blind <amount>
<name>: posts big blind <amount>
<name>: posts ante <amount>
*** HOLE CARDS ***
Dealt to <name> [<card1> <card2>]
<name>: folds
<name>: calls <amount>
<name>: raises <raise_amount> to <total>
<name>: checks
*** FLOP *** [<c1> <c2> <c3>]
... actions ...
*** TURN *** [<c1> <c2> <c3>] [<c4>]
... actions ...
*** RIVER *** [<c1> <c2> <c3> <c4>] [<c5>]
... actions ...
*** SHOW DOWN ***
<name>: shows [<c1> <c2>] (<hand_description>)
<name> collects <amount> from pot
*** SUMMARY ***
Total pot <amount> | Rake <amount>
Board: [<c1> <c2> <c3> <c4> <c5>]
Seat <N>: <name> collected <amount> (showed [<c1> <c2>] and won with <hand_desc>)
Seat <N>: <name> showed [<c1> <c2>] and lost with <hand_desc>
Seat <N>: <name> folded before flop (did not show)
Card formatting: Cards use the notation from Card::Display — 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:
| 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(), after each table.run_hand():
// 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(), after each hand result:
// 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:
/// 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<PlayerId, String>,
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:
pub struct OutputConfig {
pub directory: Option<String>,
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<String>,
}
fn default_log_dir() -> String {
"logs".to_string()
}
1.11 Dependency Addition
Add chrono to holdem_core/Cargo.toml for timestamp support:
[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:
pub struct GameConfig {
// ... existing fields ...
/// Explicit blind levels. If present, overrides num_blind_levels and hands_per_level.
#[serde(default)]
pub blind_levels: Vec<BlindLevelConfig>,
}
/// 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:
[[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() to check for explicit levels first:
pub fn to_blind_schedule(&self) -> BlindSchedule {
if !self.blind_levels.is_empty() {
// Use explicit levels from config
let levels: Vec<BlindLevel> = 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:
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<ChipCount>,
}
TOML format:
[game]
prizes = [5, 3, 2]
Updated to_prize_structure()
Modify GameConfig::to_prize_structure():
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_seatsshuffles seats before each hand in cash games.Table::shuffle_seats()already exists and works.- In tournaments,
Table::start_hand()callsshuffle_seats()ifrandomize_seatsis 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:
- Keep
randomize_seatsonGameConfigwith its current meaning for cash games. - In the multi-tournament runner (new function), call
table.shuffle_seats()once before each tournament starts, but setrandomize_seats = falseon theTableConfigso it does not shuffle per-hand.
/// Runs multiple SNG tournaments and aggregates results.
pub fn run_sng_series(
config: &GameConfig,
num_tournaments: u32,
) -> Result<SngSeriesResult, GameError> {
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 that shuffles seats on all tables:
/// 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 (or GameConfig):
pub struct OutputConfig {
// ... existing fields ...
/// Number of tournaments to run in a series. Default: 1.
#[serde(default = "default_one")]
pub num_tournaments: u32,
}
TOML:
[output]
log_hands = true
num_tournaments = 100
New: SngSeriesResult
Aggregate results across multiple tournaments:
/// 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<PlayerId, SngPlayerSeriesResult>,
/// 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<u32, u32>, // 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_inper tournament - Revenue:
prizebased on finishing position - Profit:
prize - buy_in
This is already partially implemented in SimulationStats::record_tournament_result() which computes profit = prize - buy_in. The multi-tournament aggregation adds:
- Accumulating profits across N tournaments
- Computing ROI across the series
- Tracking position distribution (how often each player finishes 1st, 2nd, 3rd, etc.)
2.6 Data Flow for Multi-Tournament
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 to handle multi-tournament:
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<BlindLevelConfig>, prizes: Vec<ChipCount> |
OutputConfig |
config.rs |
Add log_dir: Option<String>, num_tournaments: u32 |
Tournament |
tournament.rs |
Add shuffle_seats() method, optional HandHistoryLogger |
Integration Points
How New Code Connects to Existing Code
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
-
HandResultextensions — The extended struct is backward-compatible. New fields are populated automatically byHandRunner::run_to_completion(). Existing callers that destructureHandResultwill get compiler warnings for unused fields but no breakage. -
HandHistoryLogger— Standalone struct with no trait dependencies. Created by the simulation runner and optionally passed toTable/Tournament. -
BlindLevelConfig— Pure config struct. Only affectsGameConfig::to_blind_schedule(). -
run_sng_series()— New function insimulation.rs. Does not modify existingrun_cash_game()or single-tournament flow. -
Tournament::shuffle_seats()— Delegates to existingTable::shuffle_seats(). No new behavior, just an access method.
Backward Compatibility
- All new
GameConfigfields use#[serde(default)], so existing TOML configs continue to work. HandResultextensions are additive — existing code that ignores new fields is unaffected.OutputConfigextensions are additive with sensible defaults.- The single-tournament path in
main.rsis preserved; multi-tournament is triggered only whennum_tournaments > 1.