docs: clean up documentation - remove obsolete architecture docs, simplify roadmap and vision, update READMEs
- Delete ARCHITECTURE_HAND_LOGGING_TOURNAMENT_IMPROVEMENTS.md (implemented) - Delete ARCHITECTURE_SUPERSONIC_EVALUATOR.md (implemented) - Delete REFERENCES.md (no longer needed) - Simplify ROADMAP.md: remove Phases 4-7 (live server, advanced bots) - Simplify VISION.md: focus on simulation testbed only - Update README.md: dual evaluator architecture, fix broken links - Update holdem_core/README.md: fix title, add Supersonic evaluator docs
This commit is contained in:
parent
09a0777f96
commit
14c56485e7
7 changed files with 87 additions and 1492 deletions
160
README.md
160
README.md
|
|
@ -1,16 +1,16 @@
|
||||||
# Poker Testbed - Java-Compatible Hand Evaluator for Bot Testing
|
# Poker Simulation Testbed
|
||||||
|
|
||||||
[](https://crates.io/crates/holdem_core)
|
[](https://crates.io/crates/holdem_core)
|
||||||
[](https://docs.rs/holdem_core)
|
[](https://docs.rs/holdem_core)
|
||||||
[](LICENSE)
|
[](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
|
## Recent Updates
|
||||||
|
|
||||||
- **🚀 Perfect Hash Jump Table**: Revolutionary 79% memory reduction (625MB → ~130MB) with enhanced performance
|
- **Supersonic Evaluator**: New high-throughput LUT trie evaluator (~124 MB) for O(n) hand evaluation
|
||||||
- **Suit Canonicalization**: Advanced isomorphic hand reduction for optimal memory usage
|
- **Dual Evaluator Architecture**: Choose between Cactus Kev (compact) and Supersonic (fast) per use case
|
||||||
- **Bottom-up Trie Construction**: Cache-optimized memory layout for improved evaluation speed
|
- **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
|
- **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
|
- **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
|
- **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
|
## Features
|
||||||
|
|
||||||
- **Bot Testing Framework**: Specifically designed for automated poker bot testing and development
|
- **Bot Testing Framework**: Specifically designed for automated poker bot testing and development
|
||||||
- **Blazing Fast**: O(1) hand evaluation using perfect hash jump tables
|
- **Dual Evaluators**: Cactus Kev (compact ~100 KB) and Supersonic LUT trie (~124 MB) hand evaluators
|
||||||
- **Memory Efficient**: ~130MB for complete evaluation tables (79% reduction from 625MB)
|
- **Blazing Fast**: Sub-microsecond hand evaluation using perfect hash and trie-based lookup
|
||||||
- **Perfect Hash Jump Tables**: Revolutionary memory optimization with suit canonicalization
|
- **Memory Efficient**: Choose between compact Cactus Kev tables or high-throughput Supersonic trie
|
||||||
- **Standards Compliant**: Based on proven poker evaluation algorithms with Java compatibility
|
- **Standards Compliant**: Based on proven poker evaluation algorithms
|
||||||
- **Multi-Hand Support**: Evaluate 5, 6, or 7-card poker hands with Java-compatible results
|
- **Multi-Hand Support**: Evaluate 5, 6, or 7-card poker hands
|
||||||
- **Texas Hold'em Ready**: Optimized for Texas Hold'em bot analysis and testing
|
- **Texas Hold'em Ready**: Optimized for Texas Hold'em bot analysis and testing
|
||||||
- **Comprehensive Testing**: Extensive test suite with millions of verification cases
|
- **Comprehensive Testing**: Extensive test suite with millions of verification cases
|
||||||
- **Deterministic Results**: Consistent hand evaluation for reproducible bot testing
|
- **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
|
## Performance
|
||||||
|
|
||||||
| Hand Type | Evaluation Time | Combinations Evaluated |
|
### Cactus Kev Evaluator
|
||||||
|-----------|----------------|----------------------|
|
|
||||||
| 5-Card | ~10-20ns | Direct lookup |
|
| Hand Type | Evaluation Time | Method |
|
||||||
| 6-Card | ~50-80ns | 6 combinations |
|
|-----------|----------------|--------|
|
||||||
| 7-Card | ~150-250ns | 21 combinations |
|
| 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
|
## Documentation
|
||||||
|
|
||||||
For detailed project information, see:
|
For detailed project information, see:
|
||||||
|
|
||||||
- **[VISION.md](docs/VISION.md)** - Project goals and architectural vision
|
- **[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
|
- **[ROADMAP.md](docs/ROADMAP.md)** - Future development phases and implementation plan
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
@ -90,16 +99,15 @@ println!("Hand rank: {:?}", hand_value.rank);
|
||||||
This framework provides specialized capabilities for poker bot testing and development:
|
This framework provides specialized capabilities for poker bot testing and development:
|
||||||
|
|
||||||
### Automated Bot Testing
|
### 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
|
- **Performance Benchmarking**: Fast evaluation for large-scale bot performance testing
|
||||||
- **Range Analysis**: Efficient hand range evaluation for bot strategy analysis
|
- **Range Analysis**: Efficient hand range evaluation for bot strategy analysis
|
||||||
- **Decision Tree Testing**: Quick hand strength calculation for bot decision trees
|
- **Decision Tree Testing**: Quick hand strength calculation for bot decision trees
|
||||||
|
|
||||||
### Java Tool Integration
|
### Dual Evaluator Support
|
||||||
- **Cross-Platform Compatibility**: Works seamlessly with existing Java poker tools
|
- **Cactus Kev**: Compact ~100 KB tables with prime-product hashing — ideal for memory-constrained environments
|
||||||
- **Consistent Results**: Identical hand rankings to Java Meerkat API
|
- **Supersonic**: High-throughput ~124 MB LUT trie with O(n) array lookups — fastest for bulk evaluation
|
||||||
- **Tool Chain Integration**: Compatible with Java-based poker analysis frameworks
|
- **Algorithmic Fallback**: Rule-based evaluator serves as correctness oracle for validation
|
||||||
- **Bot Framework Support**: Integrates with popular Java poker bot frameworks
|
|
||||||
|
|
||||||
### Bot Development Features
|
### Bot Development Features
|
||||||
- **Hole Card Analysis**: Fast evaluation of starting hand strength for pre-flop bots
|
- **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
|
## 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
|
### Cactus Kev Evaluator (compact)
|
||||||
2. **Perfect Hash Jump Table**: Three-level trie structure (Level 5/6/7) for optimal memory layout and cache performance
|
Uses prime-product hashing into two small lookup tables (~100 KB total):
|
||||||
3. **Bottom-up Construction**: Cache-optimized table construction using streaming algorithms
|
1. **Prime Product Hashing**: Each rank maps to a unique prime; the product of 5 primes uniquely identifies the hand
|
||||||
4. **Memory Efficiency**: 79% memory reduction (625MB → ~130MB) while maintaining O(1) lookup performance
|
2. **Dual Lookup Tables**: 1,287 flush entries + 6,175 unsuited entries = 7,462 total (~30 KB)
|
||||||
5. **Best Hand Selection**: For 6-7 card hands, finds optimal 5-card combination using jump table optimization
|
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
|
### Supersonic Evaluator (high-throughput)
|
||||||
|
Uses a pre-computed LUT trie (~124 MB) for O(n) array lookups:
|
||||||
#### Jump Table System
|
1. **Trie Structure**: 32,487,834 `u32` entries organized in pages of 53 (one per card + base)
|
||||||
The evaluator uses a sophisticated three-level jump table architecture:
|
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
|
||||||
- **Level 5 (Terminal)**: Direct hand values for all canonical 5-card combinations
|
4. **Best for Bulk**: Fastest evaluator for large-scale simulations and batch evaluation
|
||||||
- **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
|
|
||||||
|
|
||||||
### Table Generation
|
### 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
|
- **Cactus Kev**: Small tables (~100 KB) generated in-memory from prime products
|
||||||
// Jump table is generated automatically on first use
|
- **Supersonic**: Large LUT trie (~124 MB) loaded from pre-generated binary file
|
||||||
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
|
|
||||||
|
|
||||||
## Hand Types
|
## Hand Types
|
||||||
|
|
||||||
|
|
@ -258,16 +240,6 @@ let best_rank = evaluator.rank_hand7(&all_cards);
|
||||||
println!("Bot best hand rank: {}", best_rank);
|
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
|
## Testing
|
||||||
|
|
||||||
Run the comprehensive test suite:
|
Run the comprehensive test suite:
|
||||||
|
|
@ -360,41 +332,37 @@ Player Statistics:
|
||||||
|
|
||||||
## Mathematical Foundation
|
## 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
|
Index = Hash % TABLE_SIZE
|
||||||
Rank = TABLE[Index]
|
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)
|
The Supersonic evaluator uses a pre-computed trie of 32.5M `u32` entries:
|
||||||
- **Bit Patterns**: Suit detection using bit manipulation
|
|
||||||
- **Binary Representation**: Compact 8-bit card encoding
|
|
||||||
|
|
||||||
### 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:
|
Each step is a single array lookup — no hashing, no enumeration, no branching.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- **Perfect Hash Jump Tables**: Revolutionary memory optimization with suit canonicalization
|
- **Cactus Kev**: Perfect hash algorithm for 5-card poker hand evaluation
|
||||||
- **Suit Canonicalization**: Lexicographically smallest permutation algorithm for isomorphic reduction
|
- **Supersonic**: LUT trie evaluator for high-throughput hand evaluation
|
||||||
- **Bottom-up Trie Construction**: Cache-optimized memory layout for modern CPU architectures
|
- **Poker Standards**: Standard poker hand ranking rules
|
||||||
- **Poker Standards**: 2+2 Poker hand ranking standards (ranking reference)
|
|
||||||
- **Poker Testbed**: Framework designed for automated bot testing and analysis
|
- **Poker Testbed**: Framework designed for automated bot testing and analysis
|
||||||
- **Java Integration**: Compatible with existing Java poker tools and frameworks
|
|
||||||
|
|
||||||
## License
|
## 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.
|
**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.
|
||||||
|
|
|
||||||
|
|
@ -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<Vec<(PlayerId, Action)>>` (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<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:
|
|
||||||
|
|
||||||
```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<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`](holdem_core/src/game/hand_runner.rs:87) to include action history:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
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()`](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 #<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`](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<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`](holdem_core/src/game/config.rs:442):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
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`](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<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:
|
|
||||||
|
|
||||||
```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<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`](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<ChipCount>,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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<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`](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<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_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<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
|
|
||||||
|
|
||||||
```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`.
|
|
||||||
|
|
@ -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<u32>` — 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<SupersonicEvaluator>,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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<HashMap<u64, u32>>` for interior mutability:
|
|
||||||
```rust
|
|
||||||
seven_card_cache: RefCell<HashMap<u64, u32>>,
|
|
||||||
```
|
|
||||||
|
|
||||||
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<HashMap<u64, u32>>`
|
|
||||||
- [ ] 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<SupersonicEvaluator>` 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<SupersonicEvaluator>` | 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 |
|
|
||||||
|
|
@ -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<SupersonicEvaluator>`
|
|
||||||
- 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
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Poker AI Testbed & Live Server Roadmap
|
# Poker AI Testbed & Live Server Roadmap
|
||||||
|
|
||||||
## Phase 2: Simulation Testbed ✅
|
## Simulation Testbed
|
||||||
|
|
||||||
**Remaining for mass simulation**
|
**Remaining for mass simulation**
|
||||||
- Parallel simulation engine (multi-threading)
|
- Parallel simulation engine (multi-threading)
|
||||||
|
|
@ -9,25 +9,7 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 4: Bot Framework
|
### "Unit Test" 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
|
|
||||||
**Goal:** Bot behavior can be tested against predefined hands
|
**Goal:** Bot behavior can be tested against predefined hands
|
||||||
|
|
||||||
**Hand Replayer**
|
**Hand Replayer**
|
||||||
|
|
@ -35,39 +17,4 @@
|
||||||
- Action comparison to expected outcome
|
- Action comparison to expected outcome
|
||||||
- Selection of hands from different sources (books)
|
- Selection of hands from different sources (books)
|
||||||
- Action likelihoods (multiple replays)
|
- Action likelihoods (multiple replays)
|
||||||
|
- GUI for test runs with chosen hand-sets and bots
|
||||||
---
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,4 @@
|
||||||
# Poker AI Testbed & Live Server Vision
|
# Poker AI Simulation Testbed
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
- **Configurable bots** (Rule-based, Nash EQ, Statistical, Hand-reading, Reinforcement Learning)
|
- **Configurable bots** (Rule-based, Nash EQ, Statistical, Hand-reading, Reinforcement Learning)
|
||||||
- **Mass parallel simulations** with GPU acceleration (e.g., for turn and river rollouts)
|
- **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
|
- **TOML-driven configuration** for all parameters of bots and simulations
|
||||||
- **Comprehensive result analysis** and bot evolution
|
- **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
|
## Expected Outcomes
|
||||||
|
|
||||||
- **Research Platform:** Systematic testing of poker AI strategies
|
- **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
|
|
||||||
|
|
@ -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
|
## 🚀 Features
|
||||||
|
|
||||||
- **Ultra-Fast Evaluation**: Sub-microsecond hand evaluation using Cactus Kev perfect hashing
|
- **Dual Evaluators**: Cactus Kev (compact ~100 KB) and Supersonic LUT trie (~124 MB) hand evaluators
|
||||||
- **Memory Efficient**: Only ~100 KB for lookup tables (7,462 entries) — no multi-GB trie
|
- **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
|
- **Zero Collisions**: Validated against all 2,598,960 five-card hands
|
||||||
- **Thread Safe**: Singleton pattern with lazy initialization for shared resources
|
- **Thread Safe**: Singleton pattern with lazy initialization for shared resources
|
||||||
- **Mathematically Correct**: Comprehensive property-based testing and validation
|
- **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
|
## 🏗️ 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.
|
- **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.
|
- **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:
|
Key modules:
|
||||||
|
|
||||||
- [`evaluator`] - Core hand evaluation engine with public `Evaluator` API
|
- [`evaluator`] - Core hand evaluation engine with public `Evaluator` API
|
||||||
- [`evaluator::cactus_kev`] - Cactus Kev perfect-hash 5-card evaluator
|
- [`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::tables`] - `JumpTable` wrapper around Cactus Kev + 7-card cache
|
||||||
- [`evaluator::algorithmic_evaluator`] - Rule-based evaluator (correctness oracle)
|
- [`evaluator::algorithmic_evaluator`] - Rule-based evaluator (correctness oracle)
|
||||||
- [`evaluator::singleton`] - Thread-safe singleton pattern for shared evaluator instance
|
- [`evaluator::singleton`] - Thread-safe singleton pattern for shared evaluator instance
|
||||||
|
|
@ -100,20 +106,20 @@ Add this to your `Cargo.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
math = { path = "./math" }
|
holdem_core = "0.1.0"
|
||||||
holdem_core = { path = "./holdem_core" }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📚 Documentation
|
## 📚 Documentation
|
||||||
|
|
||||||
### API Documentation
|
### API Documentation
|
||||||
- **[Crate Documentation](https://docs.rs/math)** - Complete API reference
|
- **[Crate Documentation](https://docs.rs/holdem_core)** - Complete API reference
|
||||||
- **[Examples](math/src/evaluator/examples.rs)** - Comprehensive usage examples
|
- **[Examples](src/evaluator/examples.rs)** - Comprehensive usage examples
|
||||||
- **[Benchmarks](math/src/evaluator/benchmarks.rs)** - Performance characteristics
|
- **[Benchmarks](src/evaluator/benchmarks.rs)** - Performance characteristics
|
||||||
|
|
||||||
### Module Documentation
|
### Module Documentation
|
||||||
- **[Evaluator Module](src/evaluator/evaluator.rs)** - Core evaluation engine and public API
|
- **[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
|
- **[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
|
- **[Tables Module](src/evaluator/tables.rs)** - JumpTable with Cactus Kev + 7-card cache
|
||||||
- **[File I/O Module](src/evaluator/file_io.rs)** - Persistent storage
|
- **[File I/O Module](src/evaluator/file_io.rs)** - Persistent storage
|
||||||
- **[Integration Module](src/evaluator/integration.rs)** - holdem_core integration
|
- **[Integration Module](src/evaluator/integration.rs)** - holdem_core integration
|
||||||
|
|
@ -296,13 +302,14 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
||||||
## 🙏 Acknowledgments
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
- **Cactus Kev**: Original perfect hash algorithm implementation
|
- **Cactus Kev**: Original perfect hash algorithm implementation
|
||||||
|
- **Supersonic**: LUT trie evaluator for high-throughput hand evaluation
|
||||||
- **PokerStove**: Inspiration for lookup table approach
|
- **PokerStove**: Inspiration for lookup table approach
|
||||||
- **Rust Community**: Excellent tools and libraries for systems programming
|
- **Rust Community**: Excellent tools and libraries for systems programming
|
||||||
|
|
||||||
## 🔗 Links
|
## 🔗 Links
|
||||||
|
|
||||||
- **[Repository](https://github.com/yourusername/rusty-marvin)**
|
- **[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)**
|
- **[Issues](https://github.com/yourusername/rusty-marvin/issues)**
|
||||||
- **[Discussions](https://github.com/yourusername/rusty-marvin/discussions)**
|
- **[Discussions](https://github.com/yourusername/rusty-marvin/discussions)**
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue