feat: hand history logging, tournament series, MTT consolidation, Meerkat API

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
This commit is contained in:
Jan-Peter von Hunnius 2026-05-20 21:05:22 +02:00
parent 9dfd9c3ee3
commit c041cde3fe
18 changed files with 4601 additions and 208 deletions

View file

@ -17,7 +17,7 @@ use clap::Parser;
use holdem_core::game::bot_factory::create_bot;
use holdem_core::game::config::SimulationConfig;
use holdem_core::game::simulation::run_cash_game;
use holdem_core::game::simulation::{run_cash_game, run_sng_series};
use holdem_core::game::stats::SimulationStats;
use holdem_core::game::tournament::{GameType, Tournament};
use holdem_core::game::types::PlayerId;
@ -75,11 +75,16 @@ fn main() {
game_config.num_hands = hands;
}
// Run simulation based on game type
// Run simulation based on game type and number of tournaments
match game_config.game_type {
GameType::Cash => run_cash_simulation(&game_config),
GameType::SitAndGo => run_tournament_simulation(&game_config),
GameType::MultiTableTournament => run_tournament_simulation(&game_config),
GameType::SitAndGo | GameType::MultiTableTournament => {
if game_config.num_tournaments > 1 {
run_tournament_series_simulation(&game_config, game_config.num_tournaments);
} else {
run_tournament_simulation(&game_config);
}
}
}
}
@ -197,3 +202,35 @@ fn run_tournament_simulation(config: &holdem_core::game::config::GameConfig) {
println!("{}", stats.summary(config.buy_in));
}
// ---------------------------------------------------------------------------
// Tournament Series (SNG or MTT)
// ---------------------------------------------------------------------------
fn run_tournament_series_simulation(config: &holdem_core::game::config::GameConfig, num_tournaments: u32) {
let game_type_name = match config.game_type {
GameType::SitAndGo => "SNG",
GameType::MultiTableTournament => "MTT",
GameType::Cash => "Cash",
};
println!("=== {} Tournament Series ===", game_type_name);
println!("Tournaments: {}", num_tournaments);
println!("Buy-in: {}", config.buy_in);
println!("Starting chips: {}", config.starting_chips);
println!("Blinds: {}/{}", config.small_blind, config.big_blind);
println!("Players: {}", config.bots.len());
if config.game_type == GameType::MultiTableTournament {
println!("Tables: {}", config.num_tables);
}
if let Some(seed) = config.seed {
println!("Seed: {}", seed);
}
println!();
let result = run_sng_series(config, num_tournaments).unwrap_or_else(|e| {
eprintln!("Tournament series error: {}", e);
std::process::exit(1);
});
println!("{}", result.summary());
}