Fixed hand evaluator LUT generation

This commit is contained in:
Jan-Peter von Hunnius 2025-11-16 16:02:26 +01:00
parent 7819b1ee27
commit 51e1f58ef9
17 changed files with 2045 additions and 660 deletions

View file

@ -23,9 +23,11 @@ sha2 = "0.10"
tempfile = "3.23.0"
once_cell = "1.19"
chrono = { version = "0.4", features = ["serde"] }
indicatif = "0.17"
[dev-dependencies]
proptest = "1.0"
indicatif = "0.17"
[package.metadata.docs.rs]
all-features = true

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@
use super::errors::EvaluatorError;
use super::evaluator::{HandRank, HandValue};
use super::integration::{benchmark_evaluation, utils, EvaluatorComparison, MathEvaluator};
use super::integration::{EvaluatorComparison, MathEvaluator};
use super::tables::{CanonicalMapping, JumpTable};
use crate::card::PackedCard;
use crate::{Card, Hand};
@ -250,7 +250,7 @@ pub fn canonicalization_example() -> Result<(), EvaluatorError> {
mapping
.canonical_cards
.iter()
.map(|&c| (c & 0x03))
.map(|&c| c & 0x03)
.collect::<Vec<_>>()
);
println!(" Suit mapping: {:?}", mapping.suit_map);
@ -444,14 +444,14 @@ fn extract_suits(cards: &[PackedCard]) -> Vec<u8> {
}
/// Helper function to extract ranks from cards
fn extract_ranks(cards: &[PackedCard]) -> Vec<u8> {
cards.iter().map(|card| card.rank()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn extract_ranks(cards: &[PackedCard]) -> Vec<u8> {
cards.iter().map(|card| card.rank()).collect()
}
#[test]
fn test_basic_usage_example() {
assert!(basic_usage_example().is_ok());

View file

@ -64,7 +64,7 @@
use super::errors::EvaluatorError;
use super::evaluator::{HandRank, HandValue};
use super::tables::{CanonicalMapping, JumpTable, JumpTableEntry};
use super::tables::JumpTable;
use crate::card::PackedCard;
use crate::{Card, Hand};
use std::time::{Duration, Instant};
@ -215,7 +215,7 @@ impl MathEvaluator {
pub fn convert_cards(cards: &[Card]) -> Vec<PackedCard> {
cards
.iter()
.map(|card| PackedCard::from_card(card))
.map(|card| PackedCard::new(card.rank(), card.suit()).unwrap())
.collect()
}
@ -223,7 +223,7 @@ pub fn convert_cards(cards: &[Card]) -> Vec<PackedCard> {
pub fn convert_cards_fixed<const N: usize>(cards: &[Card; N]) -> [PackedCard; N] {
let mut result = [PackedCard::new(0, 0).unwrap(); N];
for (i, card) in cards.iter().enumerate() {
result[i] = PackedCard::from_card(card);
result[i] = PackedCard::new(card.rank(), card.suit()).unwrap();
}
result
}
@ -392,13 +392,13 @@ impl EvaluatorComparison {
let mut core_times = Vec::new();
// Benchmark math evaluator
for cards in &test_cases {
for _cards in &test_cases {
let math_time = benchmark_evaluation(|c| self.math_evaluator.evaluate_7_card(c));
math_times.push(math_time);
}
// Benchmark core evaluator
for cards in &test_cases {
for _cards in &test_cases {
let core_time = benchmark_evaluation(|c| self.core_evaluator.evaluate_7_card(c));
core_times.push(core_time);
}
@ -473,8 +473,6 @@ pub mod utils {
/// Validate that math evaluator produces same results as holdem_core
pub fn validate_evaluator_compatibility() -> Result<(), EvaluatorError> {
use std::str::FromStr;
let comparison = EvaluatorComparison::new()?;
// Test a few representative hands
@ -647,19 +645,7 @@ mod tests {
#[test]
fn test_benchmark_function() {
use std::str::FromStr;
let test_cards = [
Card::from_str("As").unwrap(),
Card::from_str("Ks").unwrap(),
Card::from_str("Qs").unwrap(),
Card::from_str("Js").unwrap(),
Card::from_str("Ts").unwrap(),
Card::from_str("7h").unwrap(),
Card::from_str("6d").unwrap(),
];
let elapsed = benchmark_evaluation(|cards| {
let elapsed = benchmark_evaluation(|_cards| {
// Simple evaluation for testing
HandValue::new(HandRank::HighCard, 0)
});
@ -672,7 +658,7 @@ mod tests {
let comparison = EvaluatorComparison::new();
assert!(comparison.is_ok());
let mut comparison = EvaluatorComparison::new().unwrap();
let comparison = EvaluatorComparison::new().unwrap();
let results = comparison.compare_evaluations(&[]);
// Should handle empty hand list gracefully

View file

@ -13,16 +13,25 @@
//! - **`property_tests`**: Property-based testing for evaluation correctness
//! - **`examples`**: Usage examples and performance benchmarks
pub mod algorithmic_evaluator;
pub mod benchmark_utils;
pub mod errors;
pub mod evaluator;
pub mod examples;
pub mod file_io;
pub mod integration;
pub mod integration_utils;
pub mod property_tests;
pub mod singleton;
pub mod tables;
pub mod tables_new;
pub mod test_utils;
#[cfg(test)]
mod algorithmic_evaluator_test;
// Re-export commonly used types from local modules
pub use algorithmic_evaluator::{AlgorithmicEvaluator, EvaluationStats};
pub use errors::EvaluatorError;
pub use evaluator::{Evaluator, HandRank, HandValue};

View file

@ -22,11 +22,11 @@
//! 4. **Coverage**: Ensure comprehensive testing of all code paths
use super::errors::EvaluatorError;
use super::evaluator::{HandRank, HandValue};
use super::integration::{benchmark_evaluation, utils, EvaluatorComparison, MathEvaluator};
use super::evaluator::HandRank;
use super::integration::{benchmark_evaluation, EvaluatorComparison, MathEvaluator};
use super::tables::{CanonicalMapping, JumpTable};
use crate::card::PackedCard;
use crate::{Card, Hand};
use crate::Card;
use std::str::FromStr;
/// Comprehensive test suite for the math evaluator system
@ -103,6 +103,19 @@ impl EvaluatorTestSuite {
let mut passed = 0;
let mut failed = 0;
// Create progress bar for correctness tests
let total_tests = test_hands.len() * 3; // 3 tests per hand (5, 6, 7 card)
let pb = indicatif::ProgressBar::new(total_tests as u64);
pb.set_style(
indicatif::ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
)
.unwrap()
.progress_chars("#>-"),
);
pb.set_message("Testing correctness");
for (i, cards) in test_hands.iter().enumerate() {
// Test 5-card evaluation
let math_result_5 = self
@ -112,6 +125,7 @@ impl EvaluatorTestSuite {
.core_evaluator
.evaluate_5_card(&cards[..5].try_into().unwrap());
pb.inc(1);
if math_result_5 == core_result_5 {
passed += 1;
} else {
@ -131,6 +145,7 @@ impl EvaluatorTestSuite {
.core_evaluator
.evaluate_6_card(&cards[..6].try_into().unwrap());
pb.inc(1);
if math_result_6 == core_result_6 {
passed += 1;
} else {
@ -147,6 +162,7 @@ impl EvaluatorTestSuite {
let math_result_7 = self.math_evaluator.evaluate_7_card(cards);
let core_result_7 = self.core_evaluator.evaluate_7_card(cards);
pb.inc(1);
if math_result_7 == core_result_7 {
passed += 1;
} else {
@ -161,6 +177,8 @@ impl EvaluatorTestSuite {
self.stats.total_tests += 3;
}
pb.finish_with_message("Correctness tests completed");
self.stats.passed_tests += passed;
self.stats.failed_tests += failed;
@ -172,16 +190,6 @@ impl EvaluatorTestSuite {
fn run_performance_tests(&mut self) -> Result<(), EvaluatorError> {
println!("Running performance tests...");
let test_cards = [
Card::from_str("As").unwrap(),
Card::from_str("Ks").unwrap(),
Card::from_str("Qs").unwrap(),
Card::from_str("Js").unwrap(),
Card::from_str("Ts").unwrap(),
Card::from_str("7h").unwrap(),
Card::from_str("6d").unwrap(),
];
// Test 5-card performance
let time_5 = benchmark_evaluation(|cards| {
self.math_evaluator
@ -259,7 +267,6 @@ impl EvaluatorTestSuite {
// Test evaluator comparison utility
let comparison = EvaluatorComparison::new()?;
let test_hands = utils::generate_test_hands();
let results = comparison.compare_evaluations(&[]);
// Should handle empty input gracefully
@ -301,21 +308,21 @@ impl EvaluatorTestSuite {
let result_5 = self
.math_evaluator
.evaluate_5_card(&cards[..5].try_into().unwrap());
assert!(result_5.rank as u8 <= HandRank::HighCard as u8);
assert!(result_5.rank as u8 <= HandRank::RoyalFlush as u8);
}
if cards.len() >= 6 {
let result_6 = self
.math_evaluator
.evaluate_6_card(&cards[..6].try_into().unwrap());
assert!(result_6.rank as u8 <= HandRank::HighCard as u8);
assert!(result_6.rank as u8 <= HandRank::RoyalFlush as u8);
}
if cards.len() >= 7 {
let result_7 = self
.math_evaluator
.evaluate_7_card(&cards[..7].try_into().unwrap());
assert!(result_7.rank as u8 <= HandRank::HighCard as u8);
assert!(result_7.rank as u8 <= HandRank::RoyalFlush as u8);
}
println!("Regression test {} passed", i);
@ -480,6 +487,7 @@ pub fn test_jump_table_properties() -> Result<(), EvaluatorError> {
// Test memory usage is within bounds
let memory_usage = table.memory_usage();
let max_memory = 200_000_000; // 200MB absolute max
let target_memory = 130_000_000; // ~130MB target for Cactus Kev's algorithm
assert!(
memory_usage <= max_memory,
@ -487,6 +495,14 @@ pub fn test_jump_table_properties() -> Result<(), EvaluatorError> {
memory_usage
);
// Verify memory usage is close to target (~130MB)
assert!(
memory_usage < target_memory * 2, // Allow some tolerance but should be much less than 2x target
"Memory usage far exceeds target: {} (target ~{}MB)",
memory_usage,
target_memory / 1024 / 1024
);
// Test that all entries are properly initialized
for i in 0..100 {
let entry = table.get(i);
@ -502,10 +518,23 @@ pub fn run_stress_tests() -> Result<(), EvaluatorError> {
println!("Running stress tests...");
let mut evaluator = MathEvaluator::new()?;
let num_hands = 1000;
// Create progress bar for stress test setup
let setup_pb = indicatif::ProgressBar::new(num_hands);
setup_pb.set_style(
indicatif::ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
)
.unwrap()
.progress_chars("#>-"),
);
setup_pb.set_message("Generating test hands");
// Generate many test hands
let mut test_hands = Vec::new();
for i in 0..1000 {
for i in 0..num_hands {
let rank1 = (i * 7) % 13;
let suit1 = (i * 7) % 4;
let rank2 = (i * 11) % 13;
@ -526,18 +555,37 @@ pub fn run_stress_tests() -> Result<(), EvaluatorError> {
];
test_hands.push(cards);
}
setup_pb.inc(1);
}
setup_pb.finish_with_message("Test hands generated");
// Create progress bar for evaluation
let eval_pb = indicatif::ProgressBar::new(test_hands.len() as u64);
eval_pb.set_style(
indicatif::ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) - {msg}")
.unwrap()
.progress_chars("#>-")
);
eval_pb.set_message("Evaluating hands");
// Evaluate all hands and measure performance
let start_time = std::time::Instant::now();
for cards in &test_hands {
for (i, cards) in test_hands.iter().enumerate() {
let _result = evaluator.evaluate_7_card(cards);
// Update progress every 100 hands to minimize overhead
if i % 100 == 0 || i == test_hands.len() - 1 {
eval_pb.set_position((i + 1) as u64);
}
}
let elapsed = start_time.elapsed();
let avg_time = elapsed / test_hands.len() as u32;
eval_pb.finish_with_message("Stress test completed");
println!(
"Stress test: {} hands in {:?}, average: {:?}",
test_hands.len(),

File diff suppressed because it is too large Load diff

View file

@ -355,7 +355,7 @@ impl Hand {
/// println!("Card: {}", card);
/// }
/// ```
pub fn iter(&self) -> std::slice::Iter<Card> {
pub fn iter(&self) -> std::slice::Iter<'_, Card> {
self.cards[0..self.len].iter()
}

View file

@ -76,6 +76,8 @@ pub use board::Street;
/// Re-export error types for convenience
pub use errors::PokerError;
/// Re-export algorithmic evaluator for direct evaluation
pub use evaluator::algorithmic_evaluator::{AlgorithmicEvaluator, EvaluationStats};
/// Re-export evaluator types for convenience
pub use evaluator::evaluator::{Evaluator, HandRank, HandValue};

View file

@ -403,11 +403,8 @@ fn test_integration_data_consistency() {
// This module provides integration tests for the advanced evaluator system,
// ensuring compatibility and correct interaction with the holdem_core evaluator.
use holdem_core::card::PackedCard;
use holdem_core::evaluator::integration::{EvaluatorComparison, MathEvaluator};
use holdem_core::evaluator::tables::JumpTable;
use holdem_core::evaluator::{HandRank, HandValue};
use holdem_core::{Card, Hand};
use holdem_core::evaluator::integration::MathEvaluator;
use holdem_core::Card;
use std::str::FromStr;
/// Integration test suite for the math evaluator

View file

@ -53,9 +53,9 @@ mod card_properties {
((12, 3), (12, 0)), // Ace spades > Ace hearts (Spades=4 > Hearts=3)
((12, 0), (12, 1)), // Ace hearts > Ace diamonds (Hearts=3 > Diamonds=2)
((12, 1), (12, 2)), // Ace diamonds > Ace clubs (Diamonds=2 > Clubs=1)
((5, 3), (5, 0)), // Five spades > Five hearts
((5, 0), (5, 1)), // Five hearts > Five diamonds
((5, 1), (5, 2)), // Five diamonds > Five clubs
((3, 3), (3, 0)), // Five spades > Five hearts
((3, 0), (3, 1)), // Five hearts > Five diamonds
((3, 1), (3, 2)), // Five diamonds > Five clubs
];
for ((rank1, suit1), (rank2, suit2)) in test_cases {