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:
parent
dd605e8a66
commit
17d0440c86
43 changed files with 12329 additions and 494 deletions
13
holdem_sim/Cargo.toml
Normal file
13
holdem_sim/Cargo.toml
Normal 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
199
holdem_sim/src/main.rs
Normal 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));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue