feat: refactor AlgorithmicEvaluator to stateless, add game engine module and holdem_sim CLI

- Refactor AlgorithmicEvaluator from stateful Arc<Evaluator> wrapper to
  pure stateless evaluator with zero-allocation construction
- Extract shared evaluation logic into pub(crate) methods, eliminating
  ~220 lines of duplicated code from Evaluator
- Add game module: table, hand runner, tournament, bots, config, stats
- Add holdem_sim CLI binary crate for running simulations
- Convert MathEvaluator to thread-safe singleton with Arc<Inner> pattern
- Replace heap-allocated combination generation with const-generics
  stack-based approach in AlgorithmicEvaluator::evaluate_n_card_hand
- Add thiserror 2 dependency for GameError derive
- Update docs: README, ACHIEVEMENTS, ROADMAP
This commit is contained in:
Jan-Peter von Hunnius 2026-05-18 15:12:34 +02:00
parent dd605e8a66
commit 17d0440c86
43 changed files with 12329 additions and 494 deletions

13
holdem_sim/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "holdem_sim"
version = "0.1.0"
edition = "2021"
description = "Texas Hold'em simulation runner"
[[bin]]
name = "holdem_sim"
path = "src/main.rs"
[dependencies]
holdem_core = { path = "../holdem_core" }
clap = { version = "4", features = ["derive"] }

199
holdem_sim/src/main.rs Normal file
View file

@ -0,0 +1,199 @@
//! Texas Hold'em simulation runner binary.
//!
//! Parses CLI arguments, loads a TOML configuration file, runs the requested
//! simulation (cash game or tournament), and prints a summary of each bot's
//! performance.
//!
//! # Usage
//!
//! ```text
//! holdem_sim <config_file> [--seed <seed>] [--hands <num_hands>]
//! ```
use std::collections::HashMap;
use std::path::PathBuf;
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::stats::SimulationStats;
use holdem_core::game::tournament::{GameType, Tournament};
use holdem_core::game::types::PlayerId;
// ---------------------------------------------------------------------------
// CLI definition
// ---------------------------------------------------------------------------
#[derive(Parser)]
#[command(name = "holdem_sim")]
#[command(about = "Texas Hold'em Poker Simulation Testbed")]
struct Cli {
/// Path to TOML configuration file
config: PathBuf,
/// Random seed for reproducibility
#[arg(long)]
seed: Option<u64>,
/// Override number of hands to simulate (cash games only)
#[arg(long)]
hands: Option<u64>,
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
fn main() {
let cli = Cli::parse();
// Validate config file path
if !cli.config.exists() {
eprintln!("Error: config file not found: {}", cli.config.display());
std::process::exit(1);
}
if !cli.config.is_file() {
eprintln!("Error: path is not a file: {}", cli.config.display());
std::process::exit(1);
}
// Load config
let config = SimulationConfig::from_toml_file(&cli.config).unwrap_or_else(|e| {
eprintln!("Error loading config: {}", e);
std::process::exit(1);
});
let mut game_config = config.game.clone();
// Apply CLI overrides
if let Some(seed) = cli.seed {
game_config.seed = Some(seed);
}
if let Some(hands) = cli.hands {
game_config.num_hands = hands;
}
// Run simulation based on game type
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),
}
}
// ---------------------------------------------------------------------------
// Cash game
// ---------------------------------------------------------------------------
fn run_cash_simulation(config: &holdem_core::game::config::GameConfig) {
println!("=== Cash Game Simulation ===");
println!("Hands: {}", config.num_hands);
println!("Blinds: {}/{}", config.small_blind, config.big_blind);
if config.ante > 0 {
println!("Ante: {}", config.ante);
}
println!("Starting chips: {}", config.starting_chips);
if let Some(seed) = config.seed {
println!("Seed: {}", seed);
}
println!();
let (stats, _table) = run_cash_game(config).unwrap_or_else(|e| {
eprintln!("Simulation error: {}", e);
std::process::exit(1);
});
println!("{}", stats.summary(config.starting_chips));
}
// ---------------------------------------------------------------------------
// Tournament (SNG / MTT)
// ---------------------------------------------------------------------------
fn run_tournament_simulation(config: &holdem_core::game::config::GameConfig) {
let game_type_str = match config.game_type {
GameType::SitAndGo => "Sit-and-Go",
GameType::MultiTableTournament => "Multi-Table Tournament",
_ => "Tournament",
};
println!("=== {} Simulation ===", game_type_str);
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!();
// Create tournament components from config
let table_config = config.to_table_config();
let blind_schedule = config.to_blind_schedule();
let prize_structure = config.to_prize_structure();
let mut tournament = match config.game_type {
GameType::SitAndGo => Tournament::new_sng(
table_config,
blind_schedule,
prize_structure,
config.starting_chips,
config.buy_in,
),
GameType::MultiTableTournament => Tournament::new_mtt(
config.num_tables,
table_config,
blind_schedule,
prize_structure,
config.starting_chips,
config.buy_in,
),
_ => unreachable!(),
};
// Warn if seed was requested but cannot be applied to tournaments yet
if config.seed.is_some() {
eprintln!(
"Warning: seed is not yet supported for tournament simulations; \
results will not be reproducible."
);
}
// Create and register bots
let mut player_names: HashMap<PlayerId, String> = HashMap::new();
for (i, bot_config) in config.bots.iter().enumerate() {
let player_id = PlayerId(i as u64);
let bot = create_bot(bot_config, player_id).unwrap_or_else(|e| {
eprintln!("Error creating bot '{}': {}", bot_config.bot_type, e);
std::process::exit(1);
});
let name = bot.name().to_string();
player_names.insert(player_id, name);
tournament.register_player(bot).unwrap_or_else(|e| {
eprintln!("Error registering bot: {}", e);
std::process::exit(1);
});
}
// Run tournament
let _result = tournament.run_to_completion().unwrap_or_else(|e| {
eprintln!("Tournament error: {}", e);
std::process::exit(1);
});
println!("Total hands played: {}", tournament.total_hands());
println!();
// Collect stats using SimulationStats
let mut stats = SimulationStats::new(game_type_str);
stats.record_tournament_result(&tournament, &player_names, config.buy_in);
println!("{}", stats.summary(config.buy_in));
}