Evaluator overhaul: - Add Supersonic LUT trie evaluator (32M-entry, O(n) hand evaluation) - Add Cactus Kev perfect-hash 5-card evaluator (prime-product hashing) - 3-tier evaluation priority: Supersonic → Cactus Kev → Algorithmic - DashMap-cached 7-card evaluation for lock-free concurrent access - Rewrite JumpTable around CactusKevEvaluator + DashMap cache Bot framework: - Add StrategyBot adapter (Strategy trait → HoldemPlayer) - Add register_bot_type() for plugin-style bot registration - Add strategy module with modular composable strategies Repository split: - Remove holdem_bots from workspace (moved to private super_marvin repo) - Remove holdem_bots dependency from holdem_sim - Update cash_game.toml to use built-in bots only - Clean up .gitignore (remove stale holdem_bots exclusion) - Delete plans/ directory (internal planning artifacts) Documentation: - Add ARCHITECTURE_SUPERSONIC_EVALUATOR.md - Add REFERENCES.md - Update ROADMAP.md and VISION.md - Remove outdated docs (ACHIEVEMENTS, RAKE, TOURNAMENT_RAKE, meerkat-alignment)
13 KiB
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
-
Offline Table Generator — A one-time process that builds the
hand_rankstrie by enumerating all possible card combinations and ranking them via the existing Cactus Kev 5-card evaluator. The result is serialized to disk. -
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
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
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 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:
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 flushsub_rank(bits 0–11): relative rank within the category
This must be mapped to the existing HandValue 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:
- Start at index 53 (indices 1–52 are reserved for card offsets from root)
- For each unique rank combination, allocate trie nodes
- At leaf nodes, evaluate the corresponding 5-card hand using the existing
CactusKevEvaluatorand store the hand rank - 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 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.mdplan remains relevant
Serialization
The hand_ranks array is serialized to disk as a flat binary file:
- Format: Raw
u32little-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
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 format:
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
Add new sub-modules:
#[cfg(feature = "generator")]
pub mod supersonic_generator;
pub mod supersonic;
tables.rs — JumpTable
Add a supersonic field and modify evaluation methods:
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:
- Supersonic (if loaded) — 7 array lookups
- Cactus Kev + cache (always available) — 21 enumeration + HashMap
- Algorithmic (fallback) — rule-based evaluation
Bug Fixes Required
Fix 1: Evaluator Guard Condition
Current (broken — data is always empty in the Cactus Kev architecture):
// In evaluator.rs lines 697, 715
if self.tables.size > 0 && !self.tables.data.is_empty() {
Fixed:
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 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:
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 toJumpTablethat checks CK table population - Fix guard condition in
Evaluator::evaluate_5_card,evaluate_6_card,evaluate_7_cardto useis_built() - Fix 7-card caching: wrap
seven_card_cacheinRefCell<HashMap<u64, u32>> - Verify all existing tests still pass after bug fixes
Phase 2: Implement Supersonic Generator
- Create
supersonic_generator.rsbehindfeature = "generator"flag - Implement key normalization (suit stripping, Bose-Nelson sort)
- Implement two-pass trie population algorithm
- Use
CactusKevEvaluatorfor 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.rswithSupersonicEvaluatorstruct - 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>toJumpTable - Modify evaluation methods to try Supersonic first, fall back to Cactus Kev
- Add configuration option to enable/disable Supersonic loading
- Update
Evaluatorto 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.rswith Supersonic benchmark suite - Update
ACHIEVEMENTS.mdwhen 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 |