test: add exhaustive multi-threaded 7-card correctness test across all three evaluators
This commit is contained in:
parent
458b8af580
commit
ed911e3c3a
2 changed files with 333 additions and 0 deletions
330
holdem_core/src/evaluator/full_correctness_test.rs
Normal file
330
holdem_core/src/evaluator/full_correctness_test.rs
Normal file
|
|
@ -0,0 +1,330 @@
|
||||||
|
//! Full correctness test: exhaustive C(52,7) validation across all three evaluators.
|
||||||
|
//!
|
||||||
|
//! Compares [`AlgorithmicEvaluator`], [`CactusKevEvaluator`], and
|
||||||
|
//! [`SupersonicEvaluator`] on every possible 7-card poker hand
|
||||||
|
//! (133,784,560 combinations) using multi-threaded parallelism via **rayon**.
|
||||||
|
//!
|
||||||
|
//! # What is checked
|
||||||
|
//!
|
||||||
|
//! | Pair | Rank match | Value match |
|
||||||
|
//! |--------------------------------|:----------:|:-----------:|
|
||||||
|
//! | Algorithmic vs Cactus Kev | ✅ | ✅ |
|
||||||
|
//! | Algorithmic vs Supersonic | ✅ | —¹ |
|
||||||
|
//! | Cactus Kev vs Supersonic | ✅ | —¹ |
|
||||||
|
//!
|
||||||
|
//! ¹ Supersonic uses a different sub-rank encoding; only the `HandRank`
|
||||||
|
//! category is compared.
|
||||||
|
//!
|
||||||
|
//! # Running
|
||||||
|
//!
|
||||||
|
//! ```sh
|
||||||
|
//! cargo test -p holdem_core --lib \
|
||||||
|
//! evaluator::full_correctness_test::test_full_7card_correctness \
|
||||||
|
//! -- --ignored --nocapture
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use super::algorithmic_evaluator::AlgorithmicEvaluator;
|
||||||
|
use super::cactus_kev::CactusKevEvaluator;
|
||||||
|
use super::evaluator::{HandRank, HandValue};
|
||||||
|
use super::supersonic::SupersonicEvaluator;
|
||||||
|
use crate::Card;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Convert a 0–51 card index to a [`Card`].
|
||||||
|
#[inline]
|
||||||
|
fn idx_to_card(idx: u32) -> Card {
|
||||||
|
Card {
|
||||||
|
rank: (idx / 4) as u8,
|
||||||
|
suit: (idx % 4) as u8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluate a 7-card hand with [`CactusKevEvaluator`] by enumerating all
|
||||||
|
/// C(7,5) = 21 five-card subsets and returning the best result.
|
||||||
|
fn ck_evaluate_7(ck: &CactusKevEvaluator, cards: &[Card; 7]) -> HandValue {
|
||||||
|
let mut best = HandValue::new(HandRank::HighCard, 0);
|
||||||
|
for a in 0..7 {
|
||||||
|
for b in (a + 1)..7 {
|
||||||
|
let mut five = [Card { rank: 0, suit: 0 }; 5];
|
||||||
|
let mut k = 0;
|
||||||
|
for (i, &c) in cards.iter().enumerate() {
|
||||||
|
if i != a && i != b {
|
||||||
|
five[k] = c;
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug_assert_eq!(k, 5);
|
||||||
|
let hv = ck.evaluate_5_card(&five);
|
||||||
|
if hv > best {
|
||||||
|
best = hv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a mismatch for the error report.
|
||||||
|
fn fmt_mismatch(label: &str, cards: &[Card; 7], a: HandValue, b: HandValue) -> String {
|
||||||
|
let cards_str: Vec<String> = cards.iter().map(|c| format!("{}", c)).collect();
|
||||||
|
format!(
|
||||||
|
"{}: [{}] → {:?} vs {:?}",
|
||||||
|
label,
|
||||||
|
cards_str.join(" "),
|
||||||
|
a,
|
||||||
|
b
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Per-chunk statistics (one per rayon worker thread)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ChunkStats {
|
||||||
|
total: u64,
|
||||||
|
/// Algorithmic vs CactusKev — rank category differs.
|
||||||
|
algo_ck_rank_mm: u64,
|
||||||
|
/// Algorithmic vs CactusKev — rank OK, value differs.
|
||||||
|
algo_ck_value_mm: u64,
|
||||||
|
/// Algorithmic vs Supersonic — rank category differs.
|
||||||
|
algo_ss_rank_mm: u64,
|
||||||
|
/// CactusKev vs Supersonic — rank category differs.
|
||||||
|
ck_ss_rank_mm: u64,
|
||||||
|
/// First recorded mismatch for each evaluator pair (for diagnostics).
|
||||||
|
first_algo_ck: Option<String>,
|
||||||
|
first_algo_ss: Option<String>,
|
||||||
|
first_ck_ss: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// The test
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Exhaustive 7-card correctness test across all three evaluators.
|
||||||
|
///
|
||||||
|
/// Iterates every C(52,7) = 133,784,560 hand in parallel (outer loop
|
||||||
|
/// parallelised over 52 chunks via rayon) and verifies that all three
|
||||||
|
/// evaluators agree.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_full_7card_correctness() {
|
||||||
|
const EXPECTED: u64 = 133_784_560; // C(52,7)
|
||||||
|
|
||||||
|
eprintln!("═══════════════════════════════════════════════════════════════════");
|
||||||
|
eprintln!(" Full 7-Card Correctness Test");
|
||||||
|
eprintln!(" {} hands × 3 evaluators, multi-threaded via rayon", EXPECTED);
|
||||||
|
eprintln!("═══════════════════════════════════════════════════════════════════");
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 1. Build evaluators
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
eprintln!("[1/3] AlgorithmicEvaluator — stateless, no init");
|
||||||
|
eprintln!("[2/3] CactusKevEvaluator — building lookup tables …");
|
||||||
|
let ck = CactusKevEvaluator::new();
|
||||||
|
eprintln!(
|
||||||
|
" → flush entries = {}, unsuited entries = {}",
|
||||||
|
ck.flush_table_size(),
|
||||||
|
ck.unsuited_table_size()
|
||||||
|
);
|
||||||
|
eprintln!("[3/3] SupersonicEvaluator — loading / generating LUT …");
|
||||||
|
let ss = match SupersonicEvaluator::load_or_generate() {
|
||||||
|
Some(s) => {
|
||||||
|
eprintln!(" → {} entries loaded", s.table_size());
|
||||||
|
s
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
eprintln!(" ✗ Supersonic table unavailable — skipping test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 2. Parallel enumeration of all C(52,7) hands
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
eprintln!("\nEnumerating all 7-card hands …");
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
let chunks: Vec<ChunkStats> = (0u32..52)
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|i0| {
|
||||||
|
let mut st = ChunkStats::default();
|
||||||
|
|
||||||
|
for i1 in (i0 + 1)..52 {
|
||||||
|
for i2 in (i1 + 1)..52 {
|
||||||
|
for i3 in (i2 + 1)..52 {
|
||||||
|
for i4 in (i3 + 1)..52 {
|
||||||
|
for i5 in (i4 + 1)..52 {
|
||||||
|
for i6 in (i5 + 1)..52 {
|
||||||
|
let cards = [
|
||||||
|
idx_to_card(i0),
|
||||||
|
idx_to_card(i1),
|
||||||
|
idx_to_card(i2),
|
||||||
|
idx_to_card(i3),
|
||||||
|
idx_to_card(i4),
|
||||||
|
idx_to_card(i5),
|
||||||
|
idx_to_card(i6),
|
||||||
|
];
|
||||||
|
|
||||||
|
let algo = AlgorithmicEvaluator::evaluate_7_card(&cards);
|
||||||
|
let ck_hv = ck_evaluate_7(&ck, &cards);
|
||||||
|
let ss_hv = ss.evaluate_7_card(&cards);
|
||||||
|
|
||||||
|
st.total += 1;
|
||||||
|
|
||||||
|
// --- Algorithmic vs CactusKev (exact match) ---
|
||||||
|
if algo.rank != ck_hv.rank {
|
||||||
|
st.algo_ck_rank_mm += 1;
|
||||||
|
if st.first_algo_ck.is_none() {
|
||||||
|
st.first_algo_ck = Some(fmt_mismatch(
|
||||||
|
"ALGO≠CK rank",
|
||||||
|
&cards,
|
||||||
|
algo,
|
||||||
|
ck_hv,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if algo.value != ck_hv.value {
|
||||||
|
st.algo_ck_value_mm += 1;
|
||||||
|
if st.first_algo_ck.is_none() {
|
||||||
|
st.first_algo_ck = Some(fmt_mismatch(
|
||||||
|
"ALGO≠CK value",
|
||||||
|
&cards,
|
||||||
|
algo,
|
||||||
|
ck_hv,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Algorithmic vs Supersonic (rank only) ---
|
||||||
|
if algo.rank != ss_hv.rank {
|
||||||
|
st.algo_ss_rank_mm += 1;
|
||||||
|
if st.first_algo_ss.is_none() {
|
||||||
|
st.first_algo_ss = Some(fmt_mismatch(
|
||||||
|
"ALGO≠SS rank",
|
||||||
|
&cards,
|
||||||
|
algo,
|
||||||
|
ss_hv,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CactusKev vs Supersonic (rank only) ---
|
||||||
|
if ck_hv.rank != ss_hv.rank {
|
||||||
|
st.ck_ss_rank_mm += 1;
|
||||||
|
if st.first_ck_ss.is_none() {
|
||||||
|
st.first_ck_ss = Some(fmt_mismatch(
|
||||||
|
"CK≠SS rank",
|
||||||
|
&cards,
|
||||||
|
ck_hv,
|
||||||
|
ss_hv,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
st
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 3. Aggregate results
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
let mut total: u64 = 0;
|
||||||
|
let mut algo_ck_rank_mm: u64 = 0;
|
||||||
|
let mut algo_ck_value_mm: u64 = 0;
|
||||||
|
let mut algo_ss_rank_mm: u64 = 0;
|
||||||
|
let mut ck_ss_rank_mm: u64 = 0;
|
||||||
|
let mut first_algo_ck: Option<String> = None;
|
||||||
|
let mut first_algo_ss: Option<String> = None;
|
||||||
|
let mut first_ck_ss: Option<String> = None;
|
||||||
|
|
||||||
|
for ch in chunks {
|
||||||
|
total += ch.total;
|
||||||
|
algo_ck_rank_mm += ch.algo_ck_rank_mm;
|
||||||
|
algo_ck_value_mm += ch.algo_ck_value_mm;
|
||||||
|
algo_ss_rank_mm += ch.algo_ss_rank_mm;
|
||||||
|
ck_ss_rank_mm += ch.ck_ss_rank_mm;
|
||||||
|
if first_algo_ck.is_none() {
|
||||||
|
first_algo_ck = ch.first_algo_ck;
|
||||||
|
}
|
||||||
|
if first_algo_ss.is_none() {
|
||||||
|
first_algo_ss = ch.first_algo_ss;
|
||||||
|
}
|
||||||
|
if first_ck_ss.is_none() {
|
||||||
|
first_ck_ss = ch.first_ck_ss;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 4. Report
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
let throughput = total as f64 / elapsed.as_secs_f64();
|
||||||
|
|
||||||
|
eprintln!("\n═══════════════════════════════════════════════════════════════════");
|
||||||
|
eprintln!(" Results");
|
||||||
|
eprintln!("═══════════════════════════════════════════════════════════════════");
|
||||||
|
eprintln!(" Hands evaluated : {} / {}", total, EXPECTED);
|
||||||
|
eprintln!(" Wall-clock time : {:.2?}", elapsed);
|
||||||
|
eprintln!(" Throughput : {:.0} hands/sec", throughput);
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(" Algorithmic vs CactusKev:");
|
||||||
|
eprintln!(" Rank mismatches : {}", algo_ck_rank_mm);
|
||||||
|
eprintln!(" Value mismatches : {}", algo_ck_value_mm);
|
||||||
|
print_first(&first_algo_ck);
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(" Algorithmic vs Supersonic:");
|
||||||
|
eprintln!(" Rank mismatches : {}", algo_ss_rank_mm);
|
||||||
|
print_first(&first_algo_ss);
|
||||||
|
eprintln!();
|
||||||
|
eprintln!(" CactusKev vs Supersonic:");
|
||||||
|
eprintln!(" Rank mismatches : {}", ck_ss_rank_mm);
|
||||||
|
print_first(&first_ck_ss);
|
||||||
|
eprintln!("═══════════════════════════════════════════════════════════════════");
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// 5. Assertions
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
assert_eq!(
|
||||||
|
total, EXPECTED,
|
||||||
|
"must enumerate all C(52,7) = {} hands",
|
||||||
|
EXPECTED
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
algo_ck_rank_mm, 0,
|
||||||
|
"Algorithmic vs CactusKev: {} rank mismatches",
|
||||||
|
algo_ck_rank_mm
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
algo_ck_value_mm, 0,
|
||||||
|
"Algorithmic vs CactusKev: {} value mismatches",
|
||||||
|
algo_ck_value_mm
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
algo_ss_rank_mm, 0,
|
||||||
|
"Algorithmic vs Supersonic: {} rank mismatches",
|
||||||
|
algo_ss_rank_mm
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ck_ss_rank_mm, 0,
|
||||||
|
"CactusKev vs Supersonic: {} rank mismatches",
|
||||||
|
ck_ss_rank_mm
|
||||||
|
);
|
||||||
|
|
||||||
|
eprintln!("\n✓ All three evaluators agree on all {} hands.", total);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_first(first: &Option<String>) {
|
||||||
|
if let Some(ref msg) = first {
|
||||||
|
eprintln!(" First mismatch : {}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,9 @@ pub mod tables;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod algorithmic_evaluator_test;
|
mod algorithmic_evaluator_test;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod full_correctness_test;
|
||||||
|
|
||||||
// Re-export commonly used types from local modules
|
// Re-export commonly used types from local modules
|
||||||
pub use algorithmic_evaluator::AlgorithmicEvaluator;
|
pub use algorithmic_evaluator::AlgorithmicEvaluator;
|
||||||
pub use errors::EvaluatorError;
|
pub use errors::EvaluatorError;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue