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)]
|
||||
mod algorithmic_evaluator_test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod full_correctness_test;
|
||||
|
||||
// Re-export commonly used types from local modules
|
||||
pub use algorithmic_evaluator::AlgorithmicEvaluator;
|
||||
pub use errors::EvaluatorError;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue