diff --git a/README.md b/README.md index 01ad64a..7c56b7b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# Poker Testbed - Java-Compatible Hand Evaluator for Bot Testing +# Poker Simulation Testbed [![Crates.io](https://img.shields.io/crates/v/holdem_core.svg)](https://crates.io/crates/holdem_core) [![Documentation](https://docs.rs/holdem_core/badge.svg)](https://docs.rs/holdem_core) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -A comprehensive poker bot testing framework with Java-compatible hand evaluation. This library implements the Meerkat API algorithm with full Java compatibility, specifically designed for automated poker bot testing, development, and integration with existing poker tools. +A comprehensive poker simulation testbed and unit test framework for bot development. Features two Rust-native hand evaluators β€” the **Cactus Kev** perfect-hash evaluator (~100 KB tables) and the **Supersonic** LUT trie evaluator (~124 MB tables) β€” designed for automated poker bot testing, strategy analysis, and simulation at scale. ## Recent Updates -- **πŸš€ Perfect Hash Jump Table**: Revolutionary 79% memory reduction (625MB β†’ ~130MB) with enhanced performance -- **Suit Canonicalization**: Advanced isomorphic hand reduction for optimal memory usage -- **Bottom-up Trie Construction**: Cache-optimized memory layout for improved evaluation speed +- **Supersonic Evaluator**: New high-throughput LUT trie evaluator (~124 MB) for O(n) hand evaluation +- **Dual Evaluator Architecture**: Choose between Cactus Kev (compact) and Supersonic (fast) per use case +- **Simulation Testbed**: Run cash games, SNGs, and MTTs with configurable bots via TOML - **Input Validation**: Added comprehensive validation to `Card::new()` ensuring rank (0-12) and suit (0-3) are within valid ranges - **Enhanced Documentation**: All public methods now include working code examples and panic condition documentation - **Error Handling**: Improved error handling throughout the codebase with proper `Result` types @@ -18,30 +18,39 @@ A comprehensive poker bot testing framework with Java-compatible hand evaluation ## Features - **Bot Testing Framework**: Specifically designed for automated poker bot testing and development -- **Blazing Fast**: O(1) hand evaluation using perfect hash jump tables -- **Memory Efficient**: ~130MB for complete evaluation tables (79% reduction from 625MB) -- **Perfect Hash Jump Tables**: Revolutionary memory optimization with suit canonicalization -- **Standards Compliant**: Based on proven poker evaluation algorithms with Java compatibility -- **Multi-Hand Support**: Evaluate 5, 6, or 7-card poker hands with Java-compatible results +- **Dual Evaluators**: Cactus Kev (compact ~100 KB) and Supersonic LUT trie (~124 MB) hand evaluators +- **Blazing Fast**: Sub-microsecond hand evaluation using perfect hash and trie-based lookup +- **Memory Efficient**: Choose between compact Cactus Kev tables or high-throughput Supersonic trie +- **Standards Compliant**: Based on proven poker evaluation algorithms +- **Multi-Hand Support**: Evaluate 5, 6, or 7-card poker hands - **Texas Hold'em Ready**: Optimized for Texas Hold'em bot analysis and testing - **Comprehensive Testing**: Extensive test suite with millions of verification cases - **Deterministic Results**: Consistent hand evaluation for reproducible bot testing -- **Tool Integration**: Compatible with existing Java poker tools and frameworks +- **Simulation Engine**: Run cash games, SNGs, and MTTs with configurable bots via TOML ## Performance -| Hand Type | Evaluation Time | Combinations Evaluated | -|-----------|----------------|----------------------| -| 5-Card | ~10-20ns | Direct lookup | -| 6-Card | ~50-80ns | 6 combinations | -| 7-Card | ~150-250ns | 21 combinations | +### Cactus Kev Evaluator + +| Hand Type | Evaluation Time | Method | +|-----------|----------------|--------| +| 5-Card | ~50-100ns | Single prime-product hash β†’ table lookup | +| 6-Card | ~300-500ns | Six 5-card lookups | +| 7-Card | ~1-2Β΅s | 21 enumeration + HashMap cache | + +### Supersonic Evaluator + +| Hand Type | Evaluation Time | Method | +|-----------|----------------|--------| +| 5-Card | O(5) array lookups | Direct trie traversal | +| 6-Card | O(6) array lookups | Direct trie traversal | +| 7-Card | O(7) array lookups | Direct trie traversal | ## Documentation For detailed project information, see: - **[VISION.md](docs/VISION.md)** - Project goals and architectural vision -- **[ACHIEVEMENTS.md](docs/ACHIEVEMENTS.md)** - Completed milestones and success metrics - **[ROADMAP.md](docs/ROADMAP.md)** - Future development phases and implementation plan ## Installation @@ -90,16 +99,15 @@ println!("Hand rank: {:?}", hand_value.rank); This framework provides specialized capabilities for poker bot testing and development: ### Automated Bot Testing -- **Standardized Evaluation**: Java-compatible hand evaluation for consistent bot testing +- **Standardized Evaluation**: Consistent hand evaluation across both Cactus Kev and Supersonic evaluators - **Performance Benchmarking**: Fast evaluation for large-scale bot performance testing - **Range Analysis**: Efficient hand range evaluation for bot strategy analysis - **Decision Tree Testing**: Quick hand strength calculation for bot decision trees -### Java Tool Integration -- **Cross-Platform Compatibility**: Works seamlessly with existing Java poker tools -- **Consistent Results**: Identical hand rankings to Java Meerkat API -- **Tool Chain Integration**: Compatible with Java-based poker analysis frameworks -- **Bot Framework Support**: Integrates with popular Java poker bot frameworks +### Dual Evaluator Support +- **Cactus Kev**: Compact ~100 KB tables with prime-product hashing β€” ideal for memory-constrained environments +- **Supersonic**: High-throughput ~124 MB LUT trie with O(n) array lookups β€” fastest for bulk evaluation +- **Algorithmic Fallback**: Rule-based evaluator serves as correctness oracle for validation ### Bot Development Features - **Hole Card Analysis**: Fast evaluation of starting hand strength for pre-flop bots @@ -109,54 +117,28 @@ This framework provides specialized capabilities for poker bot testing and devel ## Algorithm Overview -This library implements the **Perfect Hash Jump Table algorithm**, a revolutionary memory-optimized approach with advanced suit canonicalization: +This library provides **two hand evaluators** with different performance/memory tradeoffs: -1. **Suit Canonicalization**: Cards are mapped to lexicographically smallest suit permutation for isomorphic reduction -2. **Perfect Hash Jump Table**: Three-level trie structure (Level 5/6/7) for optimal memory layout and cache performance -3. **Bottom-up Construction**: Cache-optimized table construction using streaming algorithms -4. **Memory Efficiency**: 79% memory reduction (625MB β†’ ~130MB) while maintaining O(1) lookup performance -5. **Best Hand Selection**: For 6-7 card hands, finds optimal 5-card combination using jump table optimization +### Cactus Kev Evaluator (compact) +Uses prime-product hashing into two small lookup tables (~100 KB total): +1. **Prime Product Hashing**: Each rank maps to a unique prime; the product of 5 primes uniquely identifies the hand +2. **Dual Lookup Tables**: 1,287 flush entries + 6,175 unsuited entries = 7,462 total (~30 KB) +3. **7-card Cache**: C(7,5)=21 enumeration with HashMap cache for amortized O(1) lookups +4. **Zero Collisions**: Validated against all 2,598,960 five-card hands -### Advanced Architecture - -#### Jump Table System -The evaluator uses a sophisticated three-level jump table architecture: - -- **Level 5 (Terminal)**: Direct hand values for all canonical 5-card combinations -- **Level 6 (Intermediate)**: Jump offsets pointing to best Level 5 combinations -- **Level 7 (Root)**: Jump offsets pointing to best Level 6 combinations - -Each level is optimized for memory efficiency and cache performance with sequential access patterns. - -#### Suit Canonicalization Algorithm -The canonicalization process reduces isomorphic hand variations: - -1. **Suit Analysis**: Identify unique suits in the hand -2. **Permutation Generation**: Generate all possible suit assignments -3. **Lexicographic Optimization**: Find lexicographically smallest representation -4. **Mapping Creation**: Create bidirectional suit mapping for conversion - -#### Memory Layout Strategy -The jump table uses advanced memory optimization: - -- **Target Size**: Exactly ~130MB (32-35 million u32 entries) -- **Trie-based Construction**: Bottom-up building for optimal memory usage -- **Table Flattening**: Reorganized layout for cache-friendly access -- **Streaming Construction**: Handles large datasets without excessive memory allocation +### Supersonic Evaluator (high-throughput) +Uses a pre-computed LUT trie (~124 MB) for O(n) array lookups: +1. **Trie Structure**: 32,487,834 `u32` entries organized in pages of 53 (one per card + base) +2. **Direct Lookup**: Follow `hand_ranks[p + card_index + 1]` for each card β€” no enumeration needed +3. **Packed Hand Rank**: 16-bit format (category + equivalency class) for compact result representation +4. **Best for Bulk**: Fastest evaluator for large-scale simulations and batch evaluation ### Table Generation -The first time an evaluator is created, it generates the Perfect Hash Jump Table: +Lookup tables are generated automatically on first use and persisted to disk: -```rust -// Jump table is generated automatically on first use -let evaluator = LookupHandEvaluator::new().unwrap(); -// Output: "Generating jump table..." (first run only) -// Output: "Hand evaluation tables saved to math/data/" -``` - -The jump table is saved to `math/data/` directory as a single optimized file and reused for subsequent runs: -- `jump_table.bin` (~130 MB) - Complete evaluation table with 79% memory reduction +- **Cactus Kev**: Small tables (~100 KB) generated in-memory from prime products +- **Supersonic**: Large LUT trie (~124 MB) loaded from pre-generated binary file ## Hand Types @@ -258,16 +240,6 @@ let best_rank = evaluator.rank_hand7(&all_cards); println!("Bot best hand rank: {}", best_rank); ``` -### Custom Table Generation - -```rust -use holdem_core::evaluator_generator::state_table_generator::StateTableGenerator; - -let mut generator = StateTableGenerator::new(); -generator.generate_tables(); // ~1-2 seconds -generator.save_tables().unwrap(); // Save to disk -``` - ## Testing Run the comprehensive test suite: @@ -360,41 +332,37 @@ Player Statistics: ## Mathematical Foundation -### Perfect Hash Algorithm +### Cactus Kev β€” Perfect Hash Algorithm -The evaluator uses a perfect hash function that maps each possible card combination to a unique index: +The Cactus Kev evaluator uses prime-product hashing: ``` -Hash = f(card₁, cardβ‚‚, ..., cardβ‚™) +Hash = prime(rank₁) Γ— prime(rankβ‚‚) Γ— ... Γ— prime(rankβ‚…) Index = Hash % TABLE_SIZE Rank = TABLE[Index] ``` -### Card Encoding +Each rank maps to a unique prime (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41), so the product of 5 primes uniquely identifies the hand pattern. -Each card is encoded using multiple techniques: +### Supersonic β€” LUT Trie Algorithm -- **Prime Numbers**: Unique primes for rank identification (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41) -- **Bit Patterns**: Suit detection using bit manipulation -- **Binary Representation**: Compact 8-bit card encoding +The Supersonic evaluator uses a pre-computed trie of 32.5M `u32` entries: -### Hand Strength Calculation +``` +p = INITIAL_STATE (page 1 base) +for each card: + p = hand_ranks[p + card_index + 1] +rank = p // (for 7-card hands) +``` -Hand strength is calculated using: - -1. **Flush Detection**: Bitwise suit comparison -2. **Rank Analysis**: Prime product uniqueness -3. **Pattern Matching**: Specialized algorithms per hand type -4. **Relative Ranking**: Comparison against all possible hands +Each step is a single array lookup β€” no hashing, no enumeration, no branching. ## References -- **Perfect Hash Jump Tables**: Revolutionary memory optimization with suit canonicalization -- **Suit Canonicalization**: Lexicographically smallest permutation algorithm for isomorphic reduction -- **Bottom-up Trie Construction**: Cache-optimized memory layout for modern CPU architectures -- **Poker Standards**: 2+2 Poker hand ranking standards (ranking reference) +- **Cactus Kev**: Perfect hash algorithm for 5-card poker hand evaluation +- **Supersonic**: LUT trie evaluator for high-throughput hand evaluation +- **Poker Standards**: Standard poker hand ranking rules - **Poker Testbed**: Framework designed for automated bot testing and analysis -- **Java Integration**: Compatible with existing Java poker tools and frameworks ## License @@ -421,4 +389,4 @@ For questions, issues, or contributions, please: --- -**Performance Note**: First run includes ~1-2 second table generation. Subsequent runs load precomputed tables in ~100-200ms. \ No newline at end of file +**Performance Note**: The Cactus Kev evaluator generates its small lookup tables (~100 KB) in-memory on first use. The Supersonic evaluator requires a pre-generated ~124 MB LUT file loaded at startup. diff --git a/docs/ARCHITECTURE_HAND_LOGGING_TOURNAMENT_IMPROVEMENTS.md b/docs/ARCHITECTURE_HAND_LOGGING_TOURNAMENT_IMPROVEMENTS.md deleted file mode 100644 index 5bca10a..0000000 --- a/docs/ARCHITECTURE_HAND_LOGGING_TOURNAMENT_IMPROVEMENTS.md +++ /dev/null @@ -1,862 +0,0 @@ -# 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`. diff --git a/docs/ARCHITECTURE_SUPERSONIC_EVALUATOR.md b/docs/ARCHITECTURE_SUPERSONIC_EVALUATOR.md deleted file mode 100644 index aa7d8bb..0000000 --- a/docs/ARCHITECTURE_SUPERSONIC_EVALUATOR.md +++ /dev/null @@ -1,362 +0,0 @@ -# Supersonic LUT Evaluator β€” Architecture Plan - -## Overview - -The **Supersonic evaluator** is a port of a Java "Supersonic" poker hand evaluator that uses a pre-computed trie structure to enable 7-card hand evaluation in **exactly 7 array lookups** with no runtime enumeration. This replaces the current Cactus Kev + HashMap approach with a pure array-based lookup that trades memory (~124 MB) for deterministic O(7) performance. - -### Two-Phase Design - -1. **Offline Table Generator** β€” A one-time process that builds the `hand_ranks` trie by enumerating all possible card combinations and ranking them via the existing Cactus Kev 5-card evaluator. The result is serialized to disk. - -2. **Runtime Lookup Evaluator** β€” Loads the pre-computed trie from disk and evaluates any 5/6/7-card hand with a simple chain of array lookups. - ---- - -## Architecture Diagram - -```mermaid -flowchart TB - subgraph Offline [Offline: Table Generator] - CK[Cactus Kev 5-card Evaluator] - GEN[StateTableGenerator] - DISK[HandRanks.dat - 124 MB on disk] - - GEN -->|uses| CK - GEN -->|writes| DISK - end - - subgraph Runtime [Runtime: Supersonic Evaluator] - LOAD[Load HandRanks.dat] - LUT[hand_ranks: Vec u32 - 32.5M entries] - EVAL[evaluate_7_card] - - LOAD --> LUT - EVAL -->|7 array lookups| LUT - end - - subgraph Fallback [Fallback: Cactus Kev + Cache] - CK2[CactusKevEvaluator] - CACHE[HashMap u64 to u32 cache] - ENUM[C of 7,5 = 21 enumeration] - - ENUM -->|uses| CK2 - ENUM -->|populates| CACHE - end - - DISK -->|loaded by| LOAD - - style Offline fill:#e8f5e9 - style Runtime fill:#e3f2fd - style Fallback fill:#fff3e0 -``` - -### Evaluation Flow - -```mermaid -flowchart LR - CARDS[7 cards] --> ENCODE[Encode: rank * 4 + suit] - ENCODE --> LOOP[Chain lookup loop] - - LOOP --> P1[p = hand_ranks 53 + card0 + 1] - P1 --> P2[p = hand_ranks p + card1 + 1] - P2 --> P3[p = hand_ranks p + card2 + 1] - P3 --> P4[p = hand_ranks p + card3 + 1] - P4 --> P5[p = hand_ranks p + card4 + 1] - P5 --> P6[p = hand_ranks p + card5 + 1] - P6 --> P7[p = hand_ranks p + card6 + 1] - P7 --> RESULT[p = final hand rank] - - style LOOP fill:#fce4ec - style RESULT fill:#c8e6c9 -``` - ---- - -## Data Structures - -### `hand_ranks: Vec` β€” The Main Trie - -- **Size**: 32,487,834 entries (~124 MB in memory) -- **Layout**: A flat array where each entry is either: - - A **pointer** (index into the same array) for intermediate nodes - - A **hand rank value** for leaf nodes (7-card terminal) - - A **pointer to a 5-card sub-table** for 5/6-card evaluation - -### Card Encoding - -Cards are encoded as `rank * 4 + suit`, yielding indices 0–51. This is compatible with the existing [`PackedCard`](holdem_core/src/card.rs) representation where: -- `rank()` returns 0–12 (Two through Ace) -- `suit()` returns 0–3 (clubs, diamonds, hearts, spades) - -The conversion function already exists in [`tables.rs`](holdem_core/src/evaluator/tables.rs:181): -```rust -fn packed_card_to_index(pc: PackedCard) -> usize { - pc.rank() as usize * 4 + pc.suit() as usize -} -``` - -### Hand Rank Encoding - -Each hand rank value is packed as a 16-bit integer: -``` -hand_rank = (category << 12) | sub_rank -``` - -Where: -- `category` (bits 12–15): 0 = high card, 1 = pair, ..., 9 = royal flush -- `sub_rank` (bits 0–11): relative rank within the category - -This must be mapped to the existing [`HandValue`](holdem_core/src/evaluator/evaluator.rs:108) struct which uses `(rank << 20) | value` packing. - ---- - -## Table Generation - -### Algorithm Overview - -The generator builds the trie in two passes: - -#### Pass 1: Generate and Sort Keys - -For each hand size (5, 6, 7 cards), generate all unique rank combinations (ignoring suit), normalize them by sorting, and create a sorted list of unique keys. This uses: - -- **Suit stripping**: Remove suit information to get rank-only combinations -- **Sorting**: Bose-Nelson sorting network for deterministic ordering of ranks within each hand -- **Deduplication**: Only store unique rank combinations - -#### Pass 2: Populate Hand Ranks - -Walk the sorted keys and populate the `hand_ranks` array: - -1. Start at index 53 (indices 1–52 are reserved for card offsets from root) -2. For each unique rank combination, allocate trie nodes -3. At leaf nodes, evaluate the corresponding 5-card hand using the existing [`CactusKevEvaluator`](holdem_core/src/evaluator/cactus_kev.rs) and store the hand rank -4. For 6-card and 7-card nodes, propagate the best 5-card sub-hand rank upward - -### Key Normalization - -The Bose-Nelson sorting network provides a branch-free, deterministic way to sort up to 7 elements. This ensures that the same set of ranks always produces the same trie path regardless of input order. - -### Dependency on Cactus Kev - -The generator uses the existing [`CactusKevEvaluator`](holdem_core/src/evaluator/cactus_kev.rs) to evaluate 5-card hands during table population. This means: -- The Cactus Kev module remains a build-time dependency -- It does **not** need to be loaded at runtime when using the Supersonic evaluator -- The [`plans/cactus-kev-evaluator.md`](plans/cactus-kev-evaluator.md) plan remains relevant - -### Serialization - -The `hand_ranks` array is serialized to disk as a flat binary file: -- **Format**: Raw `u32` little-endian array -- **Size**: 32,487,834 Γ— 4 bytes β‰ˆ 124 MB -- **Checksum**: CRC32 or SHA-256 for integrity verification -- **Location**: Configurable, default alongside the existing jump table file - ---- - -## Runtime Evaluation - -### Chain Lookup Algorithm - -```rust -fn evaluate_7_card(&self, cards: &[PackedCard; 7]) -> HandValue { - let mut p: usize = 53; // root node offset - for card in cards { - let idx = packed_card_to_index(*card); - p = self.hand_ranks[p + idx + 1] as usize; - } - // p is now the final hand rank value - decode_hand_rank(p as u32) -} -``` - -### Evaluation by Hand Size - -| Hand Size | Lookups | Final Step | -|-----------|---------|------------| -| 5 cards | 5 chain lookups | `hand_ranks[p]` β€” extra dereference | -| 6 cards | 6 chain lookups | `hand_ranks[p]` β€” extra dereference | -| 7 cards | 7 chain lookups | `p` is the final value β€” no extra step | - -The extra dereference for 5/6-card hands exists because the trie stores intermediate pointers at depth 5 and 6, and the actual hand rank is one more lookup away. - -### Hand Rank Decoding - -The raw `u32` from the trie must be decoded into the existing [`HandValue`](holdem_core/src/evaluator/evaluator.rs:108) format: - -```rust -fn decode_hand_rank(raw: u32) -> HandValue { - let category = (raw >> 12) as u8; // top 4 bits - let sub_rank = raw & 0xFFF; // bottom 12 bits - let rank = HandRank::from_u8(category).unwrap_or(HandRank::HighCard); - HandValue::new(rank, sub_rank) -} -``` - ---- - -## Integration Plan - -### New Files - -| File | Purpose | -|------|---------| -| `holdem_core/src/evaluator/supersonic.rs` | Runtime Supersonic evaluator | -| `holdem_core/src/evaluator/supersonic_generator.rs` | Offline table generator (behind `feature = "generator"` flag) | - -### Module Changes - -#### [`mod.rs`](holdem_core/src/evaluator/mod.rs) - -Add new sub-modules: -```rust -#[cfg(feature = "generator")] -pub mod supersonic_generator; -pub mod supersonic; -``` - -#### [`tables.rs`](holdem_core/src/evaluator/tables.rs) β€” `JumpTable` - -Add a `supersonic` field and modify evaluation methods: - -```rust -pub struct JumpTable { - // ... existing fields ... - - /// Supersonic LUT evaluator (loaded from disk, ~124 MB). - /// When present, used as the primary evaluation path. - supersonic: Option, -} -``` - -The evaluation priority becomes: -1. **Supersonic** (if loaded) β€” 7 array lookups -2. **Cactus Kev + cache** (always available) β€” 21 enumeration + HashMap -3. **Algorithmic** (fallback) β€” rule-based evaluation - -### Bug Fixes Required - -#### Fix 1: Evaluator Guard Condition - -**Current** (broken β€” `data` is always empty in the Cactus Kev architecture): -```rust -// In evaluator.rs lines 697, 715 -if self.tables.size > 0 && !self.tables.data.is_empty() { -``` - -**Fixed**: -```rust -if self.tables.is_built() { -``` - -The `is_built()` method should check that the Cactus Kev tables are populated (which they always are after `build()`), not whether `data` is non-empty. - -#### Fix 2: 7-Card Caching with Interior Mutability - -**Problem**: [`evaluate_7_card`](holdem_core/src/evaluator/tables.rs:399) takes `&self` but the HashMap cache requires `&mut self` for insertion. Currently, cache misses just recompute every time. - -**Fix**: Use `RefCell>` for interior mutability: -```rust -seven_card_cache: RefCell>, -``` - -Or, since the `Evaluator` wraps `JumpTable` in `Arc`, use `RwLock` or accept that the Cactus Kev path is always uncached when the Supersonic LUT is available. - ---- - -## Performance Comparison - -| Approach | 7-card eval | Memory | Startup | Dependencies | -|----------|------------|--------|---------|-------------| -| **Algorithmic** | O[C of 7,5 x eval] | 0 | Instant | None | -| **Cactus Kev + Cache** | O[21] HashMap lookups on miss | ~50 KB tables + cache growth | ~ms | Build CK tables | -| **Supersonic LUT** | O[7] array lookups | ~124 MB | Load from disk | Pre-generated .dat file | - -### Detailed Comparison - -| Metric | Algorithmic | Cactus Kev | Supersonic | -|--------|------------|------------|------------| -| 5-card eval | ~2 Β΅s | ~50 ns | ~35 ns | -| 7-card eval first | ~42 Β΅s | ~1 Β΅s | ~50 ns | -| 7-card eval cached | N/A | ~50 ns | ~50 ns | -| Memory usage | 0 | ~50 KB + growing cache | ~124 MB fixed | -| Startup time | 0 | ~1 ms | ~100 ms disk load | -| Deterministic | Yes | No β€” HashMap ordering | Yes β€” array indexing | -| Cache-friendly | N/A | No β€” HashMap | Yes β€” sequential array | - -### When to Use Which - -- **Supersonic**: Production simulations, mass hand evaluation, any scenario where 124 MB is acceptable -- **Cactus Kev**: Development, testing, memory-constrained environments -- **Algorithmic**: Correctness oracle, zero-dependency fallback - ---- - -## Phased Implementation Plan - -### Phase 1: Fix Immediate Bugs - -- [ ] Add `is_built()` method to [`JumpTable`](holdem_core/src/evaluator/tables.rs:118) that checks CK table population -- [ ] Fix guard condition in [`Evaluator::evaluate_5_card`](holdem_core/src/evaluator/evaluator.rs:672), [`evaluate_6_card`](holdem_core/src/evaluator/evaluator.rs:697), [`evaluate_7_card`](holdem_core/src/evaluator/evaluator.rs:715) to use `is_built()` -- [ ] Fix 7-card caching: wrap `seven_card_cache` in `RefCell>` -- [ ] Verify all existing tests still pass after bug fixes - -### Phase 2: Implement Supersonic Generator - -- [ ] Create `supersonic_generator.rs` behind `feature = "generator"` flag -- [ ] Implement key normalization (suit stripping, Bose-Nelson sort) -- [ ] Implement two-pass trie population algorithm -- [ ] Use [`CactusKevEvaluator`](holdem_core/src/evaluator/cactus_kev.rs) for 5-card hand ranking during generation -- [ ] Implement serialization to binary file with checksum -- [ ] Add CLI command or example to run the generator -- [ ] Validate generated table against all C(52,7) = 133,784,560 combinations - -### Phase 3: Implement Supersonic Runtime Evaluator - -- [ ] Create `supersonic.rs` with `SupersonicEvaluator` struct -- [ ] Implement file loading with checksum verification -- [ ] Implement chain lookup for 5/6/7-card evaluation -- [ ] Implement hand rank decoding (Supersonic format β†’ existing `HandValue`) -- [ ] Add fallback to Cactus Kev if LUT file is missing or corrupt - -### Phase 4: Wire Up as Default Evaluator - -- [ ] Add `Option` to [`JumpTable`](holdem_core/src/evaluator/tables.rs:118) -- [ ] Modify evaluation methods to try Supersonic first, fall back to Cactus Kev -- [ ] Add configuration option to enable/disable Supersonic loading -- [ ] Update [`Evaluator`](holdem_core/src/evaluator/evaluator.rs:591) to attempt loading Supersonic LUT at startup - -### Phase 5: Update Tests and Benchmarks - -- [ ] Add correctness test: compare Supersonic results against AlgorithmicEvaluator for all hand types -- [ ] Add property test: Supersonic agrees with Cactus Kev for random hands -- [ ] Add benchmark: Supersonic vs Cactus Kev vs Algorithmic throughput -- [ ] Update [`benchmarks.rs`](holdem_core/src/evaluator/benchmarks.rs) with Supersonic benchmark suite -- [ ] Update [`ACHIEVEMENTS.md`](docs/ACHIEVEMENTS.md) when complete - ---- - -## File Impact Summary - -| File | Action | Phase | -|------|--------|-------| -| `evaluator/tables.rs` | Add `is_built()`, fix guard, add `Option` | 1, 4 | -| `evaluator/evaluator.rs` | Fix guard conditions | 1 | -| `evaluator/supersonic.rs` | **Create** β€” runtime evaluator | 3 | -| `evaluator/supersonic_generator.rs` | **Create** β€” offline generator | 2 | -| `evaluator/mod.rs` | Add new sub-modules | 3 | -| `Cargo.toml` | Add `generator` feature flag | 2 | -| `evaluator/benchmarks.rs` | Add Supersonic benchmarks | 5 | -| `evaluator/property_tests.rs` | Add Supersonic property tests | 5 | - ---- - -## Risks and Mitigations - -| Risk | Impact | Mitigation | -|------|--------|------------| -| 124 MB memory footprint | High for embedded/constrained environments | Make Supersonic optional; Cactus Kev remains default | -| Generator produces incorrect table | Silent correctness bugs | Validate against all 133M 7-card combinations | -| Binary file format portability | Cross-platform issues | Use explicit little-endian encoding + version header | -| Startup latency for disk load | ~100ms delay on cold start | Load asynchronously; use Cactus Kev until ready | -| `RefCell` panic in multithreaded context | Runtime crash | Use `RwLock` instead, or make cache per-thread | diff --git a/docs/REFERENCES.md b/docs/REFERENCES.md deleted file mode 100644 index c40e893..0000000 --- a/docs/REFERENCES.md +++ /dev/null @@ -1,68 +0,0 @@ -# Reference Implementations - -## Java Supersonic Evaluator - -The Rust Supersonic evaluator is a port of a Java implementation that uses a pre-computed trie for O(7) hand evaluation. - -### Source Location - -- **Path**: `/home/jan/Development/poker/old_bot/SNG-Testbed/src/pokerai/pokerbots/common/supersonic/` - -### Key Files - -| File | Purpose | -|------|---------| -| `LookupHandEvaluator.java` | Runtime evaluator β€” singleton that loads `HandRanks.dat` and provides 5/6/7-card evaluation via chain lookups | -| `generator/StateTableGenerator.java` | Offline table generator β€” builds the 32.5M-entry `hand_ranks` trie using two-pass algorithm | -| `generator/Flushes.java` | Flush and straight-flush ranking table β€” pre-computed hand rank values for suited hands | -| `generator/Unique.java` | Straight and high-card ranking table β€” pre-computed hand rank values for non-flush unsuited hands | -| `generator/Products.java` | Sorted unique 5-prime products β€” used during key normalization for the trie | -| `generator/Values.java` | Hand rank values by product position β€” maps sorted prime products to hand rank encodings | -| `test/CorrectnessTest.java` | Correctness validation β€” tests 5M random hands against a reference evaluator | - -### Algorithm Notes - -- The Java implementation uses `int[]` for the trie (32,487,834 entries) -- Card encoding: `rank * 4 + suit` (same as Rust's `PackedCard` index) -- Hand rank encoding: `(category << 12) | sub_rank` (16-bit packed) -- Root node starts at index 53 (indices 1–52 reserved for initial card offsets) -- The generator uses the Cactus Kev 5-card evaluator during table population - -### Porting Considerations - -- Java `int` is 32-bit signed β†’ Rust `u32` (hand ranks are never negative) -- Java array initialization to 0 β†’ Rust `vec![0u32; 32_487_834]` -- Java singleton pattern β†’ Rust `OnceLock` or `Arc` -- Java `ObjectOutputStream` β†’ Rust custom binary serialization with checksum - ---- - -## Cactus Kev Algorithm - -The existing Rust Cactus Kev implementation in [`cactus_kev.rs`](holdem_core/src/evaluator/cactus_kev.rs) was based on the original Cactus Kev algorithm. - -### Source References - -- **Original**: http://suffe.coolpage.cash/poker/evaluator.html (archived) -- **Algorithm**: Uses prime number products to create perfect hashes for 5-card hand equivalence classes -- **Tables**: 1,287 flush entries + 6,175 unsuited entries = 7,462 total lookup entries - -### Relationship to Supersonic - -The Cactus Kev evaluator serves two roles in the Supersonic architecture: -1. **Build-time dependency**: Used by the table generator to evaluate 5-card hands -2. **Runtime fallback**: Used when the Supersonic LUT file is not available - ---- - -## Other References - -### Meerkat API (Java) - -- **Purpose**: Reference API for bot interface design - -### Algorithmic Evaluator - -- **Location**: [`algorithmic_evaluator.rs`](holdem_core/src/evaluator/algorithmic_evaluator.rs) -- **Purpose**: Rule-based evaluator used as correctness oracle -- **Note**: Not ported from Java; implemented directly in Rust from poker hand ranking rules diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f8015bf..f556075 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # Poker AI Testbed & Live Server Roadmap -## Phase 2: Simulation Testbed βœ… +## Simulation Testbed **Remaining for mass simulation** - Parallel simulation engine (multi-threading) @@ -9,25 +9,7 @@ --- -## Phase 4: Bot Framework -**Goal:** All bot types with playability enhancement - -**Bot core implementations** (partially complete) -- Logging of decision-making process - -**Advanced bots** -- Nash equilibrium calculator for SNG endgame – port from Java -- Statistical tracking bot – port from Java -- GPU acceleration for mass rollouts -- Hand-reading opponent modeling (possibly with GPU integration) -- Flop playability precomputation & score integration -- Basic reinforcement learning skeleton -- Hybrid bot combining multiple approaches -- Genetic algorithm for bot evolution (via configuration files) - ---- - -### Phase 5: "Unit Test" Framework +### "Unit Test" Framework **Goal:** Bot behavior can be tested against predefined hands **Hand Replayer** @@ -35,39 +17,4 @@ - Action comparison to expected outcome - Selection of hands from different sources (books) - Action likelihoods (multiple replays) - ---- - -### Phase 6: Live Server Infrastructure -**Goal:** Real-time bot deployment - -**Server core** -- Network protocol design (TBD: WebSocket, JSON, MQ, etc.) -- Actions (incl. early fold and table selection), events (incl. table switches and available tables) -- Client registration and management -- Game state synchronization -- Action dispatch system -- Event logging for analysis of problems - -**Client adapter system** -- Client factory for different poker environments -- Screen / protocol scraping adapters -- Table selection logic -- Configuration-driven game selection - ---- - -### Phase 7: Integration & Polish -**Goal:** Production-ready system - -**Advanced features** -- Real-time strategy adaptation -- Multi-table coordination -- Bankroll management -- Risk adjustment based on results - -**Optimization and deployment** -- Performance profiling and optimization -- Error handling and recovery -- Documentation and examples -- Deployment scripts and configuration \ No newline at end of file +- GUI for test runs with chosen hand-sets and bots diff --git a/docs/VISION.md b/docs/VISION.md index cd49469..40b04c9 100644 --- a/docs/VISION.md +++ b/docs/VISION.md @@ -1,16 +1,4 @@ -# Poker AI Testbed & Live Server Vision - -## Overall Goal - -Create a **comprehensive poker AI ecosystem** with two main components: - -- **Massive Simulation Testbed** – For developing and testing bot strategies across all game types (NL/FL/PL, Cash/SNG/MTT) with configurable parameters -- **Live Action Server** – For deploying bots in real poker environments with automatic table selection ---- - -## Core Architecture Components - -### 1. Simulation Testbed +# Poker AI Simulation Testbed - **Configurable bots** (Rule-based, Nash EQ, Statistical, Hand-reading, Reinforcement Learning) - **Mass parallel simulations** with GPU acceleration (e.g., for turn and river rollouts) @@ -18,29 +6,6 @@ Create a **comprehensive poker AI ecosystem** with two main components: - **TOML-driven configuration** for all parameters of bots and simulations - **Comprehensive result analysis** and bot evolution -### 2. Live Action Server - -- **Generic protocol** for client communication -- **Early folding capability** (bots can fold without being polled) -- **Multiple client adapters** for different poker sites -- **Automated table selection** based on game configuration -- **Real-time strategy adaptation** - -### 3. Bot Concepts - -- **Modular Strategy** per stage and game situation – e.g., low stack in a tournament, preflop vs. postflop. - Plugable sub-strategies and decision methods (first applicable, random, most aggressive, least aggressive, etc.) -- **Generic helper functions** for strategies like ICM, ppot, not, hand strength, against one and multiple opponents -- **Incremental, asynchronous rollout** of turn and river on flop and river on turn -- **Flop Playability score** for preflop based on precalculated rollouts for all possible flops and starting hands -- **Opponent hand prediction and statistics**, for instance in Poker Tracker format -- **Real-time strategy adaptation**, e.g., reinforcement learning - ---- - ## Expected Outcomes - **Research Platform:** Systematic testing of poker AI strategies -- **Live Deployment:** Bots that can play on real poker sites -- **Strategy Insights:** Understanding what works in different game conditions -- **Bot Evolution:** Self-improving systems through simulation and adaptation \ No newline at end of file diff --git a/holdem_core/README.md b/holdem_core/README.md index e964784..d179446 100644 --- a/holdem_core/README.md +++ b/holdem_core/README.md @@ -1,11 +1,12 @@ -# Math Library - High-Performance Poker Hand Evaluation +# holdem_core - High-Performance Poker Hand Evaluation -A state-of-the-art Rust library for ultra-fast poker hand evaluation using Cactus Kev's perfect hash algorithm. This library provides sub-microsecond hand evaluation for Texas Hold'em poker hands with a tiny memory footprint (~100 KB for lookup tables). +A state-of-the-art Rust library for ultra-fast poker hand evaluation featuring two evaluators: the **Cactus Kev** perfect-hash evaluator (~100 KB tables) and the **Supersonic** LUT trie evaluator (~124 MB tables). Supports 5, 6, and 7-card Texas Hold'em hands. ## πŸš€ Features -- **Ultra-Fast Evaluation**: Sub-microsecond hand evaluation using Cactus Kev perfect hashing -- **Memory Efficient**: Only ~100 KB for lookup tables (7,462 entries) β€” no multi-GB trie +- **Dual Evaluators**: Cactus Kev (compact ~100 KB) and Supersonic LUT trie (~124 MB) hand evaluators +- **Ultra-Fast Evaluation**: Sub-microsecond hand evaluation using perfect hash or O(n) trie lookups +- **Memory Options**: Choose between ~100 KB Cactus Kev tables or ~124 MB Supersonic trie depending on use case - **Zero Collisions**: Validated against all 2,598,960 five-card hands - **Thread Safe**: Singleton pattern with lazy initialization for shared resources - **Mathematically Correct**: Comprehensive property-based testing and validation @@ -26,15 +27,20 @@ A state-of-the-art Rust library for ultra-fast poker hand evaluation using Cactu ## πŸ—οΈ Architecture -The library uses a **Cactus Kev perfect hash** algorithm for 5-card evaluation and a **caching enumeration** strategy for 7-card hands: +The library provides **two hand evaluators** with different performance/memory tradeoffs: +### Cactus Kev Evaluator (compact) - **5-card evaluation**: O(1) via prime-product hashing into two small tables (7,462 entries total, ~30 KB). Each rank maps to a prime; the product of 5 primes uniquely identifies the hand. Zero collisions, validated against all 2,598,960 hands. - **7-card evaluation**: Amortized O(1) via C(7,5)=21 enumeration over the 5-card tables, with a `HashMap` cache keyed by the product of 52 distinct card primes. +### Supersonic Evaluator (high-throughput) +- **5/6/7-card evaluation**: O(n) array lookups via a pre-computed LUT trie (~124 MB, 32,487,834 `u32` entries). No enumeration or hashing needed β€” each card is a single array dereference. Packed 16-bit hand rank format (category + equivalency class). + Key modules: - [`evaluator`] - Core hand evaluation engine with public `Evaluator` API - [`evaluator::cactus_kev`] - Cactus Kev perfect-hash 5-card evaluator +- [`evaluator::supersonic`] - Supersonic LUT trie evaluator (5/6/7-card) - [`evaluator::tables`] - `JumpTable` wrapper around Cactus Kev + 7-card cache - [`evaluator::algorithmic_evaluator`] - Rule-based evaluator (correctness oracle) - [`evaluator::singleton`] - Thread-safe singleton pattern for shared evaluator instance @@ -100,20 +106,20 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -math = { path = "./math" } -holdem_core = { path = "./holdem_core" } +holdem_core = "0.1.0" ``` ## πŸ“š Documentation ### API Documentation -- **[Crate Documentation](https://docs.rs/math)** - Complete API reference -- **[Examples](math/src/evaluator/examples.rs)** - Comprehensive usage examples -- **[Benchmarks](math/src/evaluator/benchmarks.rs)** - Performance characteristics +- **[Crate Documentation](https://docs.rs/holdem_core)** - Complete API reference +- **[Examples](src/evaluator/examples.rs)** - Comprehensive usage examples +- **[Benchmarks](src/evaluator/benchmarks.rs)** - Performance characteristics ### Module Documentation - **[Evaluator Module](src/evaluator/evaluator.rs)** - Core evaluation engine and public API - **[Cactus Kev Module](src/evaluator/cactus_kev.rs)** - 5-card perfect hash evaluator +- **[Supersonic Module](src/evaluator/supersonic.rs)** - LUT trie evaluator (5/6/7-card) - **[Tables Module](src/evaluator/tables.rs)** - JumpTable with Cactus Kev + 7-card cache - **[File I/O Module](src/evaluator/file_io.rs)** - Persistent storage - **[Integration Module](src/evaluator/integration.rs)** - holdem_core integration @@ -296,16 +302,17 @@ This project is licensed under the MIT License - see the LICENSE file for detail ## πŸ™ Acknowledgments - **Cactus Kev**: Original perfect hash algorithm implementation +- **Supersonic**: LUT trie evaluator for high-throughput hand evaluation - **PokerStove**: Inspiration for lookup table approach - **Rust Community**: Excellent tools and libraries for systems programming ## πŸ”— Links - **[Repository](https://github.com/yourusername/rusty-marvin)** -- **[Documentation](https://docs.rs/math)** +- **[Documentation](https://docs.rs/holdem_core)** - **[Issues](https://github.com/yourusername/rusty-marvin/issues)** - **[Discussions](https://github.com/yourusername/rusty-marvin/discussions)** --- -**Built with ❀️ for the poker and Rust communities** \ No newline at end of file +**Built with ❀️ for the poker and Rust communities**