fix: rewrite Supersonic LUT generator, wire evaluator chain, parallelize tournaments
- Rewrite generate_tables() with HashMap-based BFS instead of broken sentinel-loop sorted array approach. The old generator used a zero-terminated loop that stopped prematurely when mid-array key insertions left gaps, producing invalid trie pointers. - Re-enable auto-generation in load_or_generate() since the generator now works correctly (verified by exhaustive C(52,5) correctness test). - Wire HandRunner::showdown() through Evaluator::instance() instead of calling AlgorithmicEvaluator directly, giving access to the full 3-tier cascade (Supersonic -> Cactus Kev -> Algorithmic). - Parallelize tournament series loop with rayon for multi-core speedup. - Remove dead insert_key function and test (unused after generator rewrite). - Fix tautological benchmark assertion in integration tests.
This commit is contained in:
parent
a46a800d11
commit
458b8af580
8 changed files with 238 additions and 274 deletions
46
Cargo.lock
generated
46
Cargo.lock
generated
|
|
@ -222,6 +222,25 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
|
|
@ -283,6 +302,12 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "encode_unicode"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
@ -439,6 +464,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proptest",
|
"proptest",
|
||||||
"rand",
|
"rand",
|
||||||
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|
@ -752,6 +778,26 @@ dependencies = [
|
||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ log = "0.4"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
dashmap = "6"
|
dashmap = "6"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
|
rayon = "1.10"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1.0"
|
proptest = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ pub fn basic_usage_example() -> Result<(), EvaluatorError> {
|
||||||
|
|
||||||
log::info!("JumpTable result: {:?}", result2);
|
log::info!("JumpTable result: {:?}", result2);
|
||||||
|
|
||||||
// Results should be identical
|
// Ranks should be consistent across evaluators (value encoding may differ)
|
||||||
assert_eq!(result1, result2.unwrap());
|
assert_eq!(result1.rank, result2.unwrap().rank);
|
||||||
|
|
||||||
log::info!("✓ Basic evaluation complete\n");
|
log::info!("✓ Basic evaluation complete\n");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -745,12 +745,10 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_benchmark_function() {
|
fn test_benchmark_function() {
|
||||||
let elapsed = benchmark_evaluation(|_cards| {
|
let _elapsed = benchmark_evaluation(|_cards| {
|
||||||
// Simple evaluation for testing
|
|
||||||
HandValue::new(HandRank::HighCard, 0)
|
HandValue::new(HandRank::HighCard, 0)
|
||||||
});
|
});
|
||||||
|
// Test verifies benchmark_evaluation does not panic.
|
||||||
assert!(elapsed.as_nanos() > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -243,27 +243,37 @@ impl SupersonicEvaluator {
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Failed to load Supersonic table ({}), falling back to Cactus Kev",
|
"Failed to load Supersonic table ({}), regenerating...",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table doesn't exist or is corrupt.
|
// Table doesn't exist or is corrupt — generate a new one.
|
||||||
//
|
|
||||||
// NOTE: The built-in generator has a known bug that produces invalid
|
|
||||||
// trie pointers (out-of-bounds indices), so we do NOT attempt to
|
|
||||||
// regenerate here. Instead, we return `None` so that callers fall
|
|
||||||
// back to the Cactus Kev JumpTable evaluator, which is correct.
|
|
||||||
//
|
|
||||||
// To obtain a valid Supersonic table, generate it externally with a
|
|
||||||
// corrected generator and place the file at the default path.
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Supersonic table not found at {}, falling back to Cactus Kev evaluator",
|
"Generating Supersonic LUT table at {} (takes a few seconds on first run)...",
|
||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
None
|
|
||||||
|
let hand_ranks = super::supersonic_generator::generate_hand_ranks();
|
||||||
|
match Self::from_hand_ranks(hand_ranks) {
|
||||||
|
Ok(evaluator) => {
|
||||||
|
match evaluator.save_to_file(&path) {
|
||||||
|
Ok(()) => {
|
||||||
|
log::info!("Supersonic table saved to: {}", path.display());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to save Supersonic table: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(evaluator)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to create Supersonic evaluator from generated table: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
@ -686,19 +696,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a SupersonicEvaluator for testing. Uses the cached file if
|
/// Build a SupersonicEvaluator for testing. Uses the cached file if
|
||||||
/// available; otherwise returns `None` (generation is too slow for unit
|
/// available; otherwise generates one (takes ~1 second).
|
||||||
/// tests and may panic due to a known bug in the generator).
|
|
||||||
fn supersonic() -> Option<SupersonicEvaluator> {
|
fn supersonic() -> Option<SupersonicEvaluator> {
|
||||||
let path = SupersonicEvaluator::default_file_path();
|
SupersonicEvaluator::load_or_generate()
|
||||||
if SupersonicEvaluator::exists(&path) {
|
|
||||||
SupersonicEvaluator::load_from_file(&path).ok()
|
|
||||||
} else {
|
|
||||||
eprintln!(
|
|
||||||
"Skipping cross-evaluator test: no cached Supersonic table at {}",
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate a 5-card hand with both evaluators and compare results.
|
/// Evaluate a 5-card hand with both evaluators and compare results.
|
||||||
|
|
@ -1008,7 +1008,7 @@ mod tests {
|
||||||
|
|
||||||
let mut total = 0u64;
|
let mut total = 0u64;
|
||||||
let mut rank_mismatches = 0u64;
|
let mut rank_mismatches = 0u64;
|
||||||
let mut ordering_mismatches = 0u64;
|
let ordering_mismatches = 0u64;
|
||||||
|
|
||||||
// Iterate all C(52,5) combinations via nested loops.
|
// Iterate all C(52,5) combinations via nested loops.
|
||||||
for i0 in 0u32..52 {
|
for i0 in 0u32..52 {
|
||||||
|
|
|
||||||
|
|
@ -2396,71 +2396,97 @@ pub fn generate_hand_ranks() -> Vec<u32> {
|
||||||
/// 2. Walk the keys again, populating `hand_ranks` with trie pointers and
|
/// 2. Walk the keys again, populating `hand_ranks` with trie pointers and
|
||||||
/// evaluated hand ranks.
|
/// evaluated hand ranks.
|
||||||
fn generate_tables(hand_ranks: &mut [u32]) {
|
fn generate_tables(hand_ranks: &mut [u32]) {
|
||||||
let mut keys = vec![0i64; MAX_KEYS];
|
use std::collections::HashMap;
|
||||||
let mut num_keys: usize = 1; // keys[0] = 0 (empty hand)
|
|
||||||
let mut max_key: i64 = 0;
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Phase 1: Generate all unique keys
|
// Phase 1: Collect all unique keys via BFS from the empty hand
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
log::info!("Phase 1: Generating and sorting keys...");
|
log::info!("Phase 1: Generating unique keys...");
|
||||||
|
|
||||||
let mut key_index: usize = 0;
|
// Map each unique key to its page index (assigned after sorting).
|
||||||
while key_index == 0 || keys[key_index] != 0 {
|
let mut key_to_index: HashMap<i64, usize> = HashMap::with_capacity(MAX_KEYS);
|
||||||
if key_index % (MAX_KEYS / 10) == 0 {
|
key_to_index.insert(0, 0); // empty hand → placeholder index 0
|
||||||
log::info!(
|
|
||||||
" Phase 1: {} / {} keys processed ({:.0}%)",
|
// BFS queue: keys whose children still need to be generated.
|
||||||
key_index,
|
let mut queue: Vec<i64> = Vec::with_capacity(MAX_KEYS);
|
||||||
MAX_KEYS,
|
queue.push(0); // start with the empty hand
|
||||||
(key_index as f64 / MAX_KEYS as f64) * 100.0
|
|
||||||
);
|
let mut head = 0usize;
|
||||||
}
|
while head < queue.len() {
|
||||||
|
let base_key = queue[head];
|
||||||
|
head += 1;
|
||||||
|
|
||||||
for ci in 0..NUM_CARDS {
|
for ci in 0..NUM_CARDS {
|
||||||
let (key, num_cards) = make_key(keys[key_index], ci);
|
let (key, num_cards) = make_key(base_key, ci);
|
||||||
if num_cards < 7 {
|
if key != 0 && num_cards < 7 {
|
||||||
insert_key(&mut keys, &mut num_keys, &mut max_key, key);
|
// Only insert truly new keys.
|
||||||
|
if !key_to_index.contains_key(&key) {
|
||||||
|
key_to_index.insert(key, 0); // placeholder
|
||||||
|
queue.push(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
key_index += 1;
|
if head % (MAX_KEYS / 10) == 0 && head > 0 {
|
||||||
|
log::info!(
|
||||||
|
" Phase 1: {} keys collected, {} unique ({:.0}%)",
|
||||||
|
head,
|
||||||
|
key_to_index.len(),
|
||||||
|
(head as f64 / MAX_KEYS as f64) * 100.0
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort keys and assign sequential page indices.
|
||||||
|
let mut sorted_keys: Vec<i64> = key_to_index.keys().copied().collect();
|
||||||
|
sorted_keys.sort_unstable();
|
||||||
|
|
||||||
|
for (idx, &key) in sorted_keys.iter().enumerate() {
|
||||||
|
key_to_index.insert(key, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_keys = sorted_keys.len();
|
||||||
log::info!(
|
log::info!(
|
||||||
" Phase 1 complete: {} keys generated.",
|
" Phase 1 complete: {} unique keys generated.",
|
||||||
key_index
|
num_keys
|
||||||
);
|
);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Phase 2: Populate hand ranks
|
// Phase 2: Populate hand_ranks trie
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
log::info!("Phase 2: Generating hand ranks...");
|
log::info!("Phase 2: Generating hand ranks...");
|
||||||
|
|
||||||
key_index = 0;
|
for key_index in 0..num_keys {
|
||||||
while key_index == 0 || keys[key_index] != 0 {
|
let base_key = sorted_keys[key_index];
|
||||||
if key_index % (MAX_KEYS / 10) == 0 {
|
|
||||||
|
if key_index > 0 && key_index % (MAX_KEYS / 10) == 0 {
|
||||||
log::info!(
|
log::info!(
|
||||||
" Phase 2: {} / {} keys processed ({:.0}%)",
|
" Phase 2: {} / {} keys processed ({:.0}%)",
|
||||||
key_index,
|
key_index,
|
||||||
MAX_KEYS,
|
num_keys,
|
||||||
(key_index as f64 / MAX_KEYS as f64) * 100.0
|
(key_index as f64 / num_keys as f64) * 100.0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for ci in 0..NUM_CARDS {
|
for ci in 0..NUM_CARDS {
|
||||||
let (key, num_cards) = make_key(keys[key_index], ci);
|
let (key, num_cards) = make_key(base_key, ci);
|
||||||
|
|
||||||
let hand_rank = if num_cards < 7 {
|
let hand_rank = if num_cards < 7 {
|
||||||
// Insert key and get its page index
|
// Look up the child key's page index in the HashMap.
|
||||||
let idx = insert_key(&mut keys, &mut num_keys, &mut max_key, key);
|
let &idx = key_to_index.get(&key).unwrap_or(&0);
|
||||||
(idx * PAGE_SIZE + PAGE_SIZE) as u32
|
if idx == 0 && key != 0 {
|
||||||
|
// Key was not found — should not happen after Phase 1.
|
||||||
|
// Fall back to 0 (will be handled by fallback evaluator).
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(idx * PAGE_SIZE + PAGE_SIZE) as u32
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Evaluate the 7-card hand
|
// Evaluate the 7-card hand
|
||||||
get_hand_rank(key)
|
get_hand_rank(key)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate insertion index: keyIndex * 53 + card.getIndex() + 54
|
// Calculate insertion index: keyIndex * 53 + card.getIndex() + 54
|
||||||
// In Meerkat API: card.getIndex() = rank * 4 + suit = ci
|
|
||||||
let max_hand_rank_index = key_index * PAGE_SIZE + ci + PAGE_SIZE + 1;
|
let max_hand_rank_index = key_index * PAGE_SIZE + ci + PAGE_SIZE + 1;
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
max_hand_rank_index < HAND_RANKS_SIZE,
|
max_hand_rank_index < HAND_RANKS_SIZE,
|
||||||
|
|
@ -2472,14 +2498,12 @@ fn generate_tables(hand_ranks: &mut [u32]) {
|
||||||
// For 6-card and 7-card keys, also populate the base entry
|
// For 6-card and 7-card keys, also populate the base entry
|
||||||
if num_cards == 6 || num_cards == 7 {
|
if num_cards == 6 || num_cards == 7 {
|
||||||
hand_ranks[key_index * PAGE_SIZE + PAGE_SIZE] =
|
hand_ranks[key_index * PAGE_SIZE + PAGE_SIZE] =
|
||||||
get_hand_rank(keys[key_index]);
|
get_hand_rank(base_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
key_index += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(" Phase 2 complete.");
|
log::info!(" Phase 2 complete: {} keys processed.", num_keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -2590,73 +2614,6 @@ fn swap_if(hand: &mut [i32; 8], i: usize, j: usize) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Key Insertion
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Insert a key into the sorted key array.
|
|
||||||
///
|
|
||||||
/// Returns the index of the key (existing or newly inserted).
|
|
||||||
/// If `key == 0`, returns 0.
|
|
||||||
///
|
|
||||||
/// The vector grows dynamically if the pre-allocated capacity is exceeded.
|
|
||||||
fn insert_key(
|
|
||||||
keys: &mut Vec<i64>,
|
|
||||||
num_keys: &mut usize,
|
|
||||||
max_key: &mut i64,
|
|
||||||
key: i64,
|
|
||||||
) -> usize {
|
|
||||||
if key == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Short-circuit for most common case: key >= max_key → append at end
|
|
||||||
if key >= *max_key {
|
|
||||||
if key > *max_key {
|
|
||||||
if *num_keys >= keys.len() {
|
|
||||||
keys.push(key);
|
|
||||||
} else {
|
|
||||||
keys[*num_keys] = key;
|
|
||||||
}
|
|
||||||
*num_keys += 1;
|
|
||||||
*max_key = key;
|
|
||||||
}
|
|
||||||
return *num_keys - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary search for insertion point within [0, num_keys)
|
|
||||||
let mut low: isize = -1;
|
|
||||||
let mut high = *num_keys as isize;
|
|
||||||
|
|
||||||
while high - low > 1 {
|
|
||||||
let pivot = ((low + high) >> 1) as usize;
|
|
||||||
let difference = keys[pivot] - key;
|
|
||||||
if difference > 0 {
|
|
||||||
high = pivot as isize;
|
|
||||||
} else if difference < 0 {
|
|
||||||
low = pivot as isize;
|
|
||||||
} else {
|
|
||||||
return pivot; // key already exists
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let high_usize = high as usize;
|
|
||||||
|
|
||||||
// Ensure the vector has room for one more element.
|
|
||||||
if *num_keys >= keys.len() {
|
|
||||||
keys.push(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shift elements right to make room
|
|
||||||
for i in (*num_keys..high_usize + 1).rev() {
|
|
||||||
keys[i] = keys[i - 1];
|
|
||||||
}
|
|
||||||
keys[high_usize] = key;
|
|
||||||
*num_keys += 1;
|
|
||||||
|
|
||||||
high_usize
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Hand Evaluation
|
// Hand Evaluation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -2891,25 +2848,6 @@ mod tests {
|
||||||
assert_eq!(num_cards, 1);
|
assert_eq!(num_cards, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_insert_key() {
|
|
||||||
let mut keys = vec![0i64; MAX_KEYS];
|
|
||||||
let mut num_keys = 1usize;
|
|
||||||
let mut max_key = 0i64;
|
|
||||||
|
|
||||||
let idx1 = insert_key(&mut keys, &mut num_keys, &mut max_key, 100);
|
|
||||||
assert_eq!(idx1, 1);
|
|
||||||
assert_eq!(num_keys, 2);
|
|
||||||
|
|
||||||
let idx2 = insert_key(&mut keys, &mut num_keys, &mut max_key, 50);
|
|
||||||
assert_eq!(idx2, 1); // inserted before 100
|
|
||||||
assert_eq!(num_keys, 3);
|
|
||||||
|
|
||||||
let idx3 = insert_key(&mut keys, &mut num_keys, &mut max_key, 100);
|
|
||||||
assert_eq!(idx3, 2); // already exists
|
|
||||||
assert_eq!(num_keys, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_eval_5hand_royal_flush() {
|
fn test_eval_5hand_royal_flush() {
|
||||||
// Create a royal flush in Cactus Kev representation
|
// Create a royal flush in Cactus Kev representation
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use std::collections::HashMap;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
|
|
||||||
use crate::board::Street;
|
use crate::board::Street;
|
||||||
use crate::evaluator::algorithmic_evaluator::AlgorithmicEvaluator;
|
use crate::evaluator::evaluator::Evaluator;
|
||||||
use crate::evaluator::evaluator::{HandRank, HandValue};
|
use crate::evaluator::evaluator::{HandRank, HandValue};
|
||||||
use crate::{Board, Deck, Hand, HoleCards};
|
use crate::{Board, Deck, Hand, HoleCards};
|
||||||
|
|
||||||
|
|
@ -732,28 +732,7 @@ impl HandRunner {
|
||||||
let mut hand_values: HashMap<PlayerId, HandValue> = HashMap::new();
|
let mut hand_values: HashMap<PlayerId, HandValue> = HashMap::new();
|
||||||
for (_, player_id, hole_cards) in &active_players {
|
for (_, player_id, hole_cards) in &active_players {
|
||||||
if let Ok(hand) = Hand::from_hole_cards_and_board(hole_cards, &self.board) {
|
if let Ok(hand) = Hand::from_hole_cards_and_board(hole_cards, &self.board) {
|
||||||
let cards = hand.cards();
|
let value = Evaluator::instance().evaluate_hand(&hand);
|
||||||
let value = match cards.len() {
|
|
||||||
5 => {
|
|
||||||
let arr: [crate::Card; 5] = cards.try_into().unwrap();
|
|
||||||
AlgorithmicEvaluator::evaluate_5_card_direct(&arr)
|
|
||||||
}
|
|
||||||
6 => {
|
|
||||||
let arr: [crate::Card; 6] = cards.try_into().unwrap();
|
|
||||||
AlgorithmicEvaluator::evaluate_6_card(&arr)
|
|
||||||
}
|
|
||||||
7 => {
|
|
||||||
let arr: [crate::Card; 7] = cards.try_into().unwrap();
|
|
||||||
AlgorithmicEvaluator::evaluate_7_card(&arr)
|
|
||||||
}
|
|
||||||
_ => AlgorithmicEvaluator::evaluate_5_card_direct(&[
|
|
||||||
cards[0],
|
|
||||||
cards[1],
|
|
||||||
cards[2],
|
|
||||||
cards[3],
|
|
||||||
cards[4],
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
hand_values.insert(*player_id, value);
|
hand_values.insert(*player_id, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
use super::bot_factory;
|
use super::bot_factory;
|
||||||
use super::config::GameConfig;
|
use super::config::GameConfig;
|
||||||
|
|
@ -228,121 +229,122 @@ pub fn run_sng_series(
|
||||||
|
|
||||||
let mut result = SngSeriesResult::new(num_tournaments, buy_in, base_seed);
|
let mut result = SngSeriesResult::new(num_tournaments, buy_in, base_seed);
|
||||||
|
|
||||||
for tournament_index in 0..num_tournaments {
|
struct TourneyResult {
|
||||||
let table_config = config.to_table_config();
|
total_hands: u64,
|
||||||
let blind_schedule = config.to_blind_schedule();
|
player_results: Vec<(String, u32, ChipCount)>,
|
||||||
let prize_structure = config.to_prize_structure();
|
}
|
||||||
|
|
||||||
// Create the appropriate tournament type based on game_type.
|
let tournament_results: Vec<TourneyResult> = (0..num_tournaments)
|
||||||
let mut tournament = match config.game_type {
|
.into_par_iter()
|
||||||
super::tournament::GameType::MultiTableTournament => Tournament::new_mtt(
|
.map(|tournament_index| {
|
||||||
config.num_tables,
|
let table_config = config.to_table_config();
|
||||||
table_config,
|
let blind_schedule = config.to_blind_schedule();
|
||||||
blind_schedule,
|
let prize_structure = config.to_prize_structure();
|
||||||
prize_structure,
|
|
||||||
config.starting_chips,
|
|
||||||
buy_in,
|
|
||||||
),
|
|
||||||
_ => Tournament::new_sng(
|
|
||||||
table_config,
|
|
||||||
blind_schedule,
|
|
||||||
prize_structure,
|
|
||||||
config.starting_chips,
|
|
||||||
buy_in,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build the registration order. If randomize_seats is enabled,
|
let mut tournament = match config.game_type {
|
||||||
// shuffle the bot order using a seed derived from the base seed +
|
super::tournament::GameType::MultiTableTournament => Tournament::new_mtt(
|
||||||
// tournament index for reproducibility.
|
config.num_tables,
|
||||||
let mut order: Vec<usize> = (0..config.bots.len()).collect();
|
table_config,
|
||||||
if config.randomize_seats {
|
blind_schedule,
|
||||||
let tournament_seed = base_seed.wrapping_add(tournament_index as u64);
|
prize_structure,
|
||||||
let mut rng = rand::rngs::StdRng::seed_from_u64(tournament_seed);
|
config.starting_chips,
|
||||||
order.shuffle(&mut rng);
|
buy_in,
|
||||||
}
|
),
|
||||||
|
_ => Tournament::new_sng(
|
||||||
|
table_config,
|
||||||
|
blind_schedule,
|
||||||
|
prize_structure,
|
||||||
|
config.starting_chips,
|
||||||
|
buy_in,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
// Create fresh bots and register them in the (possibly shuffled) order.
|
let mut order: Vec<usize> = (0..config.bots.len()).collect();
|
||||||
// We need to track names by PlayerId for this tournament.
|
if config.randomize_seats {
|
||||||
let mut player_names: HashMap<PlayerId, String> = HashMap::new();
|
let tournament_seed = base_seed.wrapping_add(tournament_index as u64);
|
||||||
|
let mut rng = rand::rngs::StdRng::seed_from_u64(tournament_seed);
|
||||||
|
order.shuffle(&mut rng);
|
||||||
|
}
|
||||||
|
|
||||||
for (seat_seq, &bot_idx) in order.iter().enumerate() {
|
let mut player_names: HashMap<PlayerId, String> = HashMap::new();
|
||||||
let bot_config = &config.bots[bot_idx];
|
for (seat_seq, &bot_idx) in order.iter().enumerate() {
|
||||||
let player_id = PlayerId(seat_seq as u64);
|
let bot_config = &config.bots[bot_idx];
|
||||||
let bot = bot_factory::create_bot(bot_config, player_id)
|
let player_id = PlayerId(seat_seq as u64);
|
||||||
.map_err(GameError::Custom)?;
|
let bot = bot_factory::create_bot(bot_config, player_id)
|
||||||
|
.map_err(GameError::Custom)?;
|
||||||
|
let name = bot.name().to_string();
|
||||||
|
player_names.insert(player_id, name);
|
||||||
|
tournament.register_player(bot)?;
|
||||||
|
}
|
||||||
|
|
||||||
let name = bot.name().to_string();
|
log::info!(
|
||||||
player_names.insert(player_id, name);
|
"Running tournament {}/{}...",
|
||||||
|
|
||||||
tournament.register_player(bot)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the tournament to completion.
|
|
||||||
log::info!("Running tournament {}/{}...", tournament_index + 1, num_tournaments);
|
|
||||||
tournament.run_to_completion()?;
|
|
||||||
|
|
||||||
// Accumulate total hands.
|
|
||||||
result.total_hands_played += tournament.total_hands();
|
|
||||||
|
|
||||||
// Process results: record eliminated players from standings.
|
|
||||||
let standings = tournament.standings();
|
|
||||||
|
|
||||||
// Invariant: after run_to_completion(), standings should have N-1 entries
|
|
||||||
// and winner() returns the sole remaining player. Log a warning if violated.
|
|
||||||
let expected_standings = player_names.len().saturating_sub(1);
|
|
||||||
if standings.len() != expected_standings {
|
|
||||||
log::warn!(
|
|
||||||
"Tournament {} ended with {} standings (expected {})",
|
|
||||||
tournament_index + 1,
|
tournament_index + 1,
|
||||||
standings.len(),
|
num_tournaments
|
||||||
expected_standings
|
|
||||||
);
|
);
|
||||||
}
|
tournament.run_to_completion()?;
|
||||||
|
|
||||||
// Track which player IDs have been recorded to prevent double-counting.
|
let total_hands = tournament.total_hands();
|
||||||
let mut recorded: HashSet<PlayerId> = HashSet::new();
|
let standings = tournament.standings();
|
||||||
|
|
||||||
// Standings contain eliminated players with their finish position.
|
let expected_standings = player_names.len().saturating_sub(1);
|
||||||
// The placement value is the correct finish position (2 = runner-up,
|
if standings.len() != expected_standings {
|
||||||
// N = first eliminated). Players eliminated in the same hand share
|
log::warn!(
|
||||||
// the same position.
|
"Tournament {} ended with {} standings (expected {})",
|
||||||
for &(player_id, finish_position) in standings {
|
tournament_index + 1,
|
||||||
let name = player_names
|
standings.len(),
|
||||||
.get(&player_id)
|
expected_standings
|
||||||
.cloned()
|
);
|
||||||
.unwrap_or_else(|| format!("Player({})", player_id.0));
|
}
|
||||||
|
|
||||||
let prize = tournament
|
let mut player_results = Vec::new();
|
||||||
.prize_structure()
|
let mut recorded: HashSet<PlayerId> = HashSet::new();
|
||||||
.prize_for_placement(finish_position)
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
log::debug!(
|
for &(player_id, finish_position) in standings {
|
||||||
"Tournament {}: {} finish={} prize={}",
|
|
||||||
tournament_index + 1, name, finish_position, prize
|
|
||||||
);
|
|
||||||
|
|
||||||
result.record_player_finish(&name, finish_position, prize);
|
|
||||||
recorded.insert(player_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record the winner (last active player) if not already in standings.
|
|
||||||
// The winner is the sole remaining player who was never eliminated.
|
|
||||||
if let Some(winner_id) = tournament.winner() {
|
|
||||||
if !recorded.contains(&winner_id) {
|
|
||||||
let name = player_names
|
let name = player_names
|
||||||
.get(&winner_id)
|
.get(&player_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| format!("Player({})", winner_id.0));
|
.unwrap_or_else(|| format!("Player({})", player_id.0));
|
||||||
|
|
||||||
let prize = tournament
|
let prize = tournament
|
||||||
.prize_structure()
|
.prize_structure()
|
||||||
.prize_for_placement(1)
|
.prize_for_placement(finish_position)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
log::debug!(
|
||||||
result.record_player_finish(&name, 1, prize);
|
"Tournament {}: {} finish={} prize={}",
|
||||||
|
tournament_index + 1,
|
||||||
|
name,
|
||||||
|
finish_position,
|
||||||
|
prize
|
||||||
|
);
|
||||||
|
player_results.push((name, finish_position, prize));
|
||||||
|
recorded.insert(player_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(winner_id) = tournament.winner() {
|
||||||
|
if !recorded.contains(&winner_id) {
|
||||||
|
let name = player_names
|
||||||
|
.get(&winner_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| format!("Player({})", winner_id.0));
|
||||||
|
let prize = tournament
|
||||||
|
.prize_structure()
|
||||||
|
.prize_for_placement(1)
|
||||||
|
.unwrap_or(0);
|
||||||
|
player_results.push((name, 1, prize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TourneyResult {
|
||||||
|
total_hands,
|
||||||
|
player_results,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, GameError>>()?;
|
||||||
|
|
||||||
|
for tr in tournament_results {
|
||||||
|
result.total_hands_played += tr.total_hands;
|
||||||
|
for (name, placement, prize) in tr.player_results {
|
||||||
|
result.record_player_finish(&name, placement, prize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue