14 KiB
Developer Guide
Architecture Overview
The Poker Hand Evaluator implements the Meerkat algorithm for perfect hash-based poker hand evaluation. This section covers the internal architecture, algorithms, and design decisions.
System Architecture
Core Components
┌─────────────────────────────────────────────────────────┐
│ LookupHandEvaluator │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Hand Ranking Table │ │
│ │ (32M+ entries) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────┘
│ Uses
▼
┌─────────────────────────────────────────────────────────┐
│ StateTableGenerator │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Algorithm Modules │ │
│ │ • Flushes • Products • Unique • Values │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Data Flow
-
Table Generation (one-time):
Card Combinations → Perfect Hash → Hand Evaluation → Rank Storage -
Hand Evaluation (runtime):
Input Hand → Perfect Hash → Table Lookup → Rank Value
Algorithm Details
Perfect Hash Function
The core innovation is a perfect hash function that maps every possible card combination to a unique index:
fn perfect_hash(cards: &[u32]) -> usize {
let mut hash = 53u32; // Initial value
for &card in cards {
let index = card.index();
hash = TABLE[(hash + index + 1) as usize] as u8;
}
hash as usize
}
Key Properties:
- Perfect: No collisions for valid inputs
- Minimal: Uses minimal memory for given input size
- Fast: O(1) computation time
- Deterministic: Same input always produces same output
Card Encoding
Each card is encoded using multiple techniques:
struct EncodedCard {
prime: u32, // Prime number for rank (2, 3, 5, 7, 11, ...)
rank_bits: u32, // Bit position for rank identification
suit_bits: u32, // Bit mask for suit detection
meta_bits: u32, // Additional metadata bits
}
Prime Encoding:
const PRIMES: [u32; 13] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41];
let prime = PRIMES[rank]; // Unique prime for each rank
Hand Evaluation Strategy
5-Card Hands
Direct evaluation using specialized lookup tables:
fn evaluate_5card(cards: [u32; 5]) -> u32 {
// 1. Check for flush
if is_flush(cards) {
return FLUSH_TABLE[flush_index(cards)];
}
// 2. Check for unique hands (straight flush, quads, etc.)
let unique_index = unique_hash(cards);
if let Some(rank) = UNIQUE_TABLE.get(unique_index) {
return rank;
}
// 3. Standard hand evaluation
let product_index = prime_product(cards);
VALUES_TABLE[product_index]
}
6-7 Card Hands
Brute force evaluation of all 5-card combinations:
fn evaluate_7card(cards: [u32; 7]) -> u32 {
let mut best_rank = 0;
// Generate all C(7,5) = 21 combinations
for combination in combinations(cards, 5) {
let rank = evaluate_5card(combination);
best_rank = best_rank.max(rank);
}
best_rank
}
Implementation Details
Table Generation Process
Phase 1: Card Combination Enumeration
const TABLE_SIZE: usize = 612_978; // All possible combinations
for key in 0..TABLE_SIZE {
let cards = decode_key_to_cards(key);
// Validate card combination
if is_valid_combination(cards) {
let rank = evaluate_hand(cards);
table[key] = rank;
}
}
Phase 2: Perfect Hash Construction
The perfect hash is constructed by:
- Initial Mapping: Map card combinations to initial hash values
- Conflict Resolution: Resolve collisions using secondary tables
- Optimization: Minimize table size while maintaining perfection
Phase 3: Table Serialization
fn save_tables(table: &[u32]) -> Result<(), io::Error> {
let file = File::create("math/HandRanks.dat")?;
let mut writer = BufWriter::new(file);
for &rank in table {
writer.write_all(&rank.to_ne_bytes())?;
}
Ok(())
}
Memory Layout
Runtime Table Structure
┌─────────────────────────────────────────────────────────┐
│ HandRanks.dat │
├─────────────────────────────────────────────────────────┤
│ Entry 0: [u32] Rank for combination 0 │
│ Entry 1: [u32] Rank for combination 1 │
│ ... │
│ Entry N: [u32] Rank for combination N │
└─────────────────────────────────────────────────────────┘
↑
│
32,487,834 entries × 4 bytes = 129,951,336 bytes
Generation Working Tables
struct WorkingTables {
// Main ranking table during generation
hand_ranks: Box<[u32; 612_978]>,
// Specialized evaluation tables
flushes: FlushesTable,
products: ProductsTable,
unique: UniqueTable,
values: ValuesTable,
}
Performance Optimizations
SIMD Opportunities
Current implementation is scalar, but offers SIMD optimization potential:
// Potential SIMD optimization (future enhancement)
#[cfg(target_feature = "avx2")]
fn evaluate_hands_simd(hands: &[Hand]) -> Vec<u32> {
// Process multiple hands in parallel using AVX instructions
// Each hand evaluation becomes a vector operation
}
Memory Access Patterns
The current design optimizes for:
- Sequential Generation: Tables generated in linear order
- Random Access Evaluation: O(1) lookup for any hand
- Cache Efficiency: Compact data structures for better cache utilization
Algorithmic Optimizations
Hand-Specific Algorithms
Different hand types use specialized evaluation methods:
impl HandEvaluator {
fn evaluate_straight_flush(cards: [u32; 5]) -> u32 {
// Optimized straight flush detection
let ranks = extract_ranks(cards);
if is_consecutive(ranks) && is_same_suit(cards) {
calculate_straight_flush_rank(ranks[0])
} else {
0 // Not a straight flush
}
}
fn evaluate_four_of_a_kind(cards: [u32; 5]) -> u32 {
// Optimized quad detection using prime products
let product = calculate_prime_product(cards);
QUAD_TABLE[product]
}
}
Testing Strategy
Unit Tests
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn test_royal_flush() {
let hand = create_royal_flush();
let rank = evaluator.rank_hand(&hand);
assert!(is_royal_flush_rank(rank));
}
#[test]
fn test_perfect_hash() {
// Verify no hash collisions
let mut seen = HashSet::new();
for combination in all_possible_combinations() {
let hash = perfect_hash(combination);
assert!(!seen.contains(&hash));
seen.insert(hash);
}
}
}
Integration Tests
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_vs_reference_implementation() {
let evaluator = LookupHandEvaluator::new().unwrap();
// Test against known reference implementation
for _ in 0..1_000_000 {
let hand = generate_random_hand();
let our_rank = evaluator.rank_hand(&hand);
let ref_rank = reference_evaluate(&hand);
assert_eq!(our_rank, ref_rank);
}
}
}
Performance Benchmarks
#[cfg(test)]
mod benchmarks {
use super::*;
use std::time::Instant;
#[test]
fn benchmark_hand_evaluation() {
let evaluator = LookupHandEvaluator::new().unwrap();
let hands = generate_test_hands(1_000_000);
let start = Instant::now();
for hand in hands {
let _rank = evaluator.rank_hand(&hand);
}
let duration = start.elapsed();
println!("1M evaluations in {:?}", duration);
println!("Average: {:?} per hand", duration / 1_000_000);
}
}
Development Workflow
Code Organization
src/
├── api/ # Public API definitions
│ ├── hand_eval.rs # HandEval trait
│ ├── card.rs # Card struct
│ └── hand.rs # Hand struct
├── hand_evaluator.rs # Main evaluator implementation
├── evaluator_generator/ # Table generation algorithms
│ ├── mod.rs
│ ├── state_table_generator.rs
│ ├── flushes.rs
│ ├── products.rs
│ ├── unique.rs
│ └── values.rs
└── lib.rs # Library root
Adding New Features
New Hand Type Support
- Define evaluation algorithm in
evaluator_generator/ - Add lookup tables for the new hand type
- Update
eval_5handto use new tables - Add tests for the new hand type
- Update documentation
Performance Improvements
- Profile current performance
- Identify bottlenecks (CPU, memory, cache)
- Implement optimization with benchmarks
- Verify correctness with existing tests
- Update performance documentation
Debugging and Development Tools
Debug Table Generation
fn debug_table_generation() {
let mut generator = StateTableGenerator::new();
// Enable debug output
generator.set_debug_mode(true);
// Generate with detailed logging
generator.generate_tables();
// Inspect specific entries
println!("Entry 12345: {}", generator.hand_ranks[12345]);
}
Hand Evaluation Tracing
fn trace_hand_evaluation(hand: &Hand) -> EvaluationTrace {
let mut trace = EvaluationTrace::new();
// Step through evaluation with logging
trace.log("Starting evaluation");
let rank = evaluator.rank_hand_traced(&hand, &mut trace);
trace.log(&format!("Final rank: {}", rank));
trace
}
Mathematical Background
Combinatorics
Card Combination Space
| Cards | Combinations | Calculation |
|---|---|---|
| 5-Card | 2,598,960 | C(52,5) |
| 6-Card | 20,358,520 | C(52,6) |
| 7-Card | 133,784,560 | C(52,7) |
Table Size Calculation
Working Table: C(52,5) + C(52,6) + C(52,7) = 156,741,540 entries
Final Table: 32,487,834 unique combinations (after deduplication)
Hash Function Design
The perfect hash function uses:
- Initial Value: 53 (empirically chosen)
- Recursive Application: Each card updates the hash
- Table Lookup: Final mapping through ranking table
fn perfect_hash(cards: &[u32]) -> u32 {
let mut hash = 53;
for &card in cards {
let card_index = card.index();
hash = TABLE[(hash + card_index + 1) as usize] as u8;
}
hash as u32
}
Future Enhancements
Potential Improvements
- SIMD Optimization: Vectorize hand evaluation for batch processing
- Memory Mapping: Use memory-mapped files for table loading
- Compression: Reduce table size using compression techniques
- Hardware Acceleration: GPU-accelerated evaluation for massive scale
- Distributed Generation: Parallel table generation across multiple machines
Research Directions
- Alternative Hash Functions: Explore more efficient perfect hash constructions
- Machine Learning: Use ML to optimize evaluation algorithms
- Hardware Design: Custom ASIC for poker hand evaluation
- Algorithmic Improvements: Reduce computational complexity further
Contributing
Development Setup
- Clone repository
- Install Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - Run tests:
cargo test - Generate documentation:
cargo doc --open
Code Style
- Use
rustfmtfor consistent formatting - Add comprehensive documentation comments
- Include unit tests for new functionality
- Follow existing error handling patterns
Performance Guidelines
- Profile before optimizing
- Maintain algorithm correctness
- Add benchmarks for performance regressions
- Document performance characteristics
This developer guide provides the technical foundation for understanding and extending the poker hand evaluation system. For API usage, see the user guide. For detailed reference, see the API documentation.