diff --git a/.gitignore b/.gitignore index 4b1654b..7e5cf05 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,3 @@ Thumbs.db # Proptest regressions (auto-generated) proptest-regressions/ - - -# Agent notes -AGENTS.md -AGENTS.md diff --git a/Cargo.lock b/Cargo.lock index bd17144..d99a47e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,25 +222,6 @@ dependencies = [ "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]] name = "crossbeam-utils" version = "0.8.21" @@ -302,12 +283,6 @@ dependencies = [ "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]] name = "encode_unicode" version = "1.0.0" @@ -464,7 +439,6 @@ dependencies = [ "once_cell", "proptest", "rand", - "rayon", "serde", "serde_json", "sha2", @@ -778,26 +752,6 @@ dependencies = [ "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]] name = "redox_syscall" version = "0.5.18" diff --git a/README.md b/README.md index 3ed031f..7c56b7b 100644 --- a/README.md +++ b/README.md @@ -161,27 +161,26 @@ The evaluator recognizes all standard poker hand types: ### Texas Hold'em Bot Analysis ```rust -use holdem_core::{Card, Hand, HoleCards, Board, Evaluator}; -use std::str::FromStr; +use holdem_core::hand_evaluator::LookupHandEvaluator; +use holdem_core::api::{card::Card, hand::Hand}; -let evaluator = Evaluator::new().unwrap(); +let evaluator = LookupHandEvaluator::new().unwrap(); // Bot analyzes hole cards + flop for decision making -let hole_cards = HoleCards::from_notation("AKs").unwrap(); -let board = Board::new() - .with_flop([ - Card::from_str("Td").unwrap(), - Card::from_str("Jc").unwrap(), - Card::from_str("Qh").unwrap(), - ]).unwrap(); +let hole_card1 = Card::from_string("As").unwrap().index() as u32; +let hole_card2 = Card::from_string("Ks").unwrap().index() as u32; + +let mut board = Hand::new(); +board.add_card(Card::from_string("Td").unwrap()).unwrap(); +board.add_card(Card::from_string("Jc").unwrap()).unwrap(); +board.add_card(Card::from_string("Qh").unwrap()).unwrap(); // Bot evaluates complete hand strength -let hand = Hand::from_hole_cards_and_board(&hole_cards, &board).unwrap(); -let hand_value = evaluator.evaluate_hand(&hand); -println!("Hand strength after flop: {:?}", hand_value.rank); +let hand_strength = evaluator.rank_hand_with_hole_cards(hole_card1, hole_card2, &board); +println!("Hand strength after flop: {}", hand_strength); // Bot uses this for post-flop decision making -if hand_value.rank as u8 >= 4 { // Straight or better +if hand_strength < 2000 { // Strong hand println!("Bot: Continuing with strong hand"); } ``` @@ -189,22 +188,25 @@ if hand_value.rank as u8 >= 4 { // Straight or better ### Bot Range Analysis ```rust -use holdem_core::{Card, Hand, Evaluator}; -use std::str::FromStr; +use holdem_core::hand_evaluator::LookupHandEvaluator; +use holdem_core::api::card::Card; -let evaluator = Evaluator::new().unwrap(); +let evaluator = LookupHandEvaluator::new().unwrap(); // Bot analyzes range of starting hands for pre-flop strategy let premium_hands = vec![ - Hand::from_notation("As Ah").unwrap(), - Hand::from_notation("Ks Kh").unwrap(), - Hand::from_notation("Qs Qh").unwrap(), - Hand::from_notation("As Ks").unwrap(), + (Card::from_string("AA").unwrap(), Card::from_string("KK").unwrap()), + (Card::from_string("QQ").unwrap(), Card::from_string("JJ").unwrap()), + (Card::from_string("AK").unwrap(), Card::from_string("AQ").unwrap()), ]; -for hand in premium_hands { - let hand_value = evaluator.evaluate_hand(&hand); - println!("Bot range analysis - {}: {:?}", hand, hand_value.rank); +for (card1, card2) in premium_hands { + let mut hand = Hand::new(); + hand.add_card(card1).unwrap(); + hand.add_card(card2).unwrap(); + + let strength = evaluator.rank_hand(&hand); + println!("Bot range analysis - {}: {}", hand.to_string(), strength); } ``` @@ -213,26 +215,29 @@ for hand in premium_hands { ### 7-Card Hand Evaluation for Bot Testing ```rust -use holdem_core::{Card, Hand, HoleCards, Board, Evaluator}; -use std::str::FromStr; +use holdem_core::hand_evaluator::LookupHandEvaluator; +use holdem_core::api::card::Card; -let evaluator = Evaluator::new().unwrap(); +let evaluator = LookupHandEvaluator::new().unwrap(); // Texas Hold'em complete hand: 2 hole cards + 5 community cards -let hole_cards = HoleCards::from_notation("AKs").unwrap(); -let board = Board::new() - .with_flop([ - Card::from_str("Td").unwrap(), - Card::from_str("Jc").unwrap(), - Card::from_str("Qh").unwrap(), - ]).unwrap() - .with_turn(Card::from_str("9s").unwrap()).unwrap() - .with_river(Card::from_str("2d").unwrap()).unwrap(); +let hole_cards = [ + Card::from_string("As").unwrap().index(), + Card::from_string("Ks").unwrap().index(), +]; -// Bot evaluates best possible hand from all 7 cards -let hand = Hand::from_hole_cards_and_board(&hole_cards, &board).unwrap(); -let hand_value = evaluator.evaluate_hand(&hand); -println!("Bot best hand rank: {:?}", hand_value.rank); +let community_cards = [ + Card::from_string("Td").unwrap().index(), + Card::from_string("Jc").unwrap().index(), + Card::from_string("Qh").unwrap().index(), + Card::from_string("9s").unwrap().index(), + Card::from_string("2d").unwrap().index(), +]; + +// Bot evaluates best possible hand from all cards +let all_cards: Vec = hole_cards.iter().chain(community_cards.iter()).cloned().collect(); +let best_rank = evaluator.rank_hand7(&all_cards); +println!("Bot best hand rank: {}", best_rank); ``` ## Testing diff --git a/holdem_core/Cargo.toml b/holdem_core/Cargo.toml index d549327..b7fbb5e 100644 --- a/holdem_core/Cargo.toml +++ b/holdem_core/Cargo.toml @@ -28,7 +28,6 @@ log = "0.4" thiserror = "2" dashmap = "6" dirs = "6" -rayon = "1.10" [dev-dependencies] proptest = "1.0" diff --git a/holdem_core/README.md b/holdem_core/README.md index 53c65fd..d179446 100644 --- a/holdem_core/README.md +++ b/holdem_core/README.md @@ -53,7 +53,8 @@ Key modules: ### Basic Usage ```rust -use holdem_core::{Card, Hand, Evaluator, evaluator::HandRank}; +use math::{Evaluator, HandRank}; +use holdem_core::{Card, Hand}; use std::str::FromStr; // Get the singleton evaluator instance @@ -66,30 +67,22 @@ let hand = Hand::from_notation("As Ks Qs Js Ts").unwrap(); let hand_value = evaluator.evaluate_hand(&hand); println!("Hand rank: {:?}", hand_value.rank); +println!("Hand value: {}", hand_value.value); ``` ### Advanced Usage with Integration ```rust -use holdem_core::{HoleCards, Board, Hand, Evaluator}; -use std::str::FromStr; +use math::evaluator::integration::HandEvaluator; +use holdem_core::{HoleCards, Board}; // Evaluate hole cards with a board -let hole_cards = HoleCards::from_notation("AKs").unwrap(); -let board = Board::new() - .with_flop([ - std::str::FromStr::from_str("Qs").unwrap(), - std::str::FromStr::from_str("Js").unwrap(), - std::str::FromStr::from_str("Ts").unwrap(), - ]).unwrap() - .with_turn(std::str::FromStr::from_str("7h").unwrap()).unwrap() - .with_river(std::str::FromStr::from_str("3d").unwrap()).unwrap(); +let hole_cards = HoleCards::from_strings(&["As", "Ks"]).unwrap(); +let board = Board::from_strings(&["Qs", "Js", "Ts", "7h", "3d"]).unwrap(); -let hand = Hand::from_hole_cards_and_board(&hole_cards, &board).unwrap(); -let evaluator = Evaluator::new().unwrap(); -let hand_value = evaluator.evaluate_hand(&hand); +let hand_value = HandEvaluator::evaluate_hole_cards_with_board(&hole_cards, &board).unwrap(); -println!("Final hand rank: {:?}", hand_value.rank); +println!("Final hand: {}", HandEvaluator::format_hand_value(hand_value)); ``` ## 🎯 Hand Ranking System @@ -177,7 +170,7 @@ Lookup tables are automatically managed: ### Poker Server Implementation ```rust -use holdem_core::Evaluator; +use math::Evaluator; use std::sync::Arc; struct PokerServer { @@ -190,6 +183,10 @@ impl PokerServer { Ok(Self { evaluator }) } + fn evaluate_hand(&self, cards: &[Card; 5]) -> HandValue { + self.evaluator.evaluate_5_card(cards) + } + fn process_game_state(&self, game_state: GameState) -> GameResult { // High-performance hand evaluation for poker server // Can handle 100,000+ hands per second @@ -200,10 +197,11 @@ impl PokerServer { ### Poker Analysis Tool ```rust -use holdem_core::{Hand, HoleCards, Board, Evaluator}; +use math::evaluator::integration::{HandEvaluator, HandEvaluation}; +use holdem_core::{Hand, HoleCards, Board}; struct PokerAnalyzer { - evaluator: Evaluator, + evaluator: math::Evaluator, } impl PokerAnalyzer { @@ -211,9 +209,9 @@ impl PokerAnalyzer { let mut analysis = RangeAnalysis::default(); for notation in hole_cards_list { - let hole_cards = HoleCards::from_notation(notation).unwrap(); + let hole_cards = HoleCards::from_strings(&[notation]).unwrap(); let hand = Hand::from_hole_cards_and_board(&hole_cards, board).unwrap(); - let hand_value = self.evaluator.evaluate_hand(&hand); + let hand_value = hand.evaluate(); // Analyze hand strength distribution analysis.total_hands += 1; @@ -230,16 +228,18 @@ impl PokerAnalyzer { Built-in performance monitoring and diagnostics: ```rust -use holdem_core::Evaluator; +use math::Evaluator; // Monitor evaluation performance let evaluator = Evaluator::instance(); -// Evaluate a hand to verify the evaluator is working -use holdem_core::Hand; -let hand = Hand::from_notation("As Ks Qs Js Ts").unwrap(); -let hand_value = evaluator.evaluate_hand(&hand); -println!("Hand rank: {:?}", hand_value.rank); +// Check table file status +let table_info = evaluator.get_table_info().unwrap(); +println!("Table files: {:?}", table_info); + +// Validate table integrity +let is_valid = evaluator.validate_table_files().unwrap(); +println!("Tables valid: {}", is_valid); ``` ## 🚨 Error Handling @@ -247,8 +247,7 @@ println!("Hand rank: {:?}", hand_value.rank); Comprehensive error handling for all failure modes: ```rust -use holdem_core::Evaluator; -use holdem_core::evaluator::errors::EvaluatorError; +use math::{Evaluator, evaluator::errors::EvaluatorError}; match Evaluator::new() { Ok(evaluator) => println!("Evaluator ready"), diff --git a/holdem_core/src/deck.rs b/holdem_core/src/deck.rs index ca7f762..262fa7e 100644 --- a/holdem_core/src/deck.rs +++ b/holdem_core/src/deck.rs @@ -210,42 +210,6 @@ impl Deck { pub fn cards(&self) -> &[Card] { &self.cards } - - /// Creates a new deck excluding the specified cards. - /// - /// Useful for constructing a "remaining deck" that omits known cards - /// (hero's hole cards, board cards) β€” typically resulting in a 45–47 card - /// deck for enumeration-based equity calculations. - /// - /// # Examples - /// - /// ```rust - /// use holdem_core::{Deck, Card}; - /// use std::str::FromStr; - /// - /// let deck = Deck::new(); - /// let excluded = vec![ - /// Card::from_str("As").unwrap(), - /// Card::from_str("Kh").unwrap(), - /// ]; - /// let remaining = deck.without_cards(&excluded); - /// assert_eq!(remaining.remaining(), 50); - /// ``` - /// - /// # Panics - /// - /// This method does not panic. - pub fn without_cards(&self, excluded: &[Card]) -> Deck { - use std::collections::HashSet; - let exclude_set: HashSet = excluded.iter().copied().collect(); - let cards: Vec = self - .cards - .iter() - .copied() - .filter(|c| !exclude_set.contains(c)) - .collect(); - Deck { cards } - } } impl Default for Deck { diff --git a/holdem_core/src/evaluator/evaluator.rs b/holdem_core/src/evaluator/evaluator.rs index d110d33..9d2fa91 100644 --- a/holdem_core/src/evaluator/evaluator.rs +++ b/holdem_core/src/evaluator/evaluator.rs @@ -723,15 +723,6 @@ impl Evaluator { self.supersonic.is_some() } - /// Returns a reference to the underlying [`SupersonicEvaluator`], if available. - /// - /// Use this for the incremental trie API ([`init_lookup`](SupersonicEvaluator::init_lookup), - /// [`advance_lookup`](SupersonicEvaluator::advance_lookup), etc.) which avoids - /// re-traversing shared card prefixes during rollouts. - pub fn supersonic(&self) -> Option<&SupersonicEvaluator> { - self.supersonic.as_deref() - } - /// Evaluate a 5-card hand pub fn evaluate_5_card(&self, cards: &[Card; 5]) -> HandValue { // Priority 1: Supersonic LUT (O(5) array lookups) diff --git a/holdem_core/src/evaluator/examples.rs b/holdem_core/src/evaluator/examples.rs index 93c0802..2716320 100644 --- a/holdem_core/src/evaluator/examples.rs +++ b/holdem_core/src/evaluator/examples.rs @@ -55,8 +55,8 @@ pub fn basic_usage_example() -> Result<(), EvaluatorError> { log::info!("JumpTable result: {:?}", result2); - // Ranks should be consistent across evaluators (value encoding may differ) - assert_eq!(result1.rank, result2.unwrap().rank); + // Results should be identical + assert_eq!(result1, result2.unwrap()); log::info!("βœ“ Basic evaluation complete\n"); Ok(()) diff --git a/holdem_core/src/evaluator/full_correctness_test.rs b/holdem_core/src/evaluator/full_correctness_test.rs deleted file mode 100644 index 3a30273..0000000 --- a/holdem_core/src/evaluator/full_correctness_test.rs +++ /dev/null @@ -1,330 +0,0 @@ -//! 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 = 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, - first_algo_ss: Option, - first_ck_ss: Option, -} - -// --------------------------------------------------------------------------- -// 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 = (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 = None; - let mut first_algo_ss: Option = None; - let mut first_ck_ss: Option = 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) { - if let Some(ref msg) = first { - eprintln!(" First mismatch : {}", msg); - } -} diff --git a/holdem_core/src/evaluator/integration.rs b/holdem_core/src/evaluator/integration.rs index 351e6fb..059682b 100644 --- a/holdem_core/src/evaluator/integration.rs +++ b/holdem_core/src/evaluator/integration.rs @@ -745,10 +745,12 @@ mod tests { #[test] fn test_benchmark_function() { - let _elapsed = benchmark_evaluation(|_cards| { + let elapsed = benchmark_evaluation(|_cards| { + // Simple evaluation for testing HandValue::new(HandRank::HighCard, 0) }); - // Test verifies benchmark_evaluation does not panic. + + assert!(elapsed.as_nanos() > 0); } #[test] diff --git a/holdem_core/src/evaluator/mod.rs b/holdem_core/src/evaluator/mod.rs index 447fc79..07346b2 100644 --- a/holdem_core/src/evaluator/mod.rs +++ b/holdem_core/src/evaluator/mod.rs @@ -46,9 +46,6 @@ 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; @@ -57,9 +54,6 @@ pub use evaluator::{Evaluator, HandRank, HandValue}; // Re-export math-specific types pub use tables::JumpTable; -// Re-export Supersonic incremental lookup types -pub use supersonic::TrieCursor; - // Module-level constants pub const MAX_CARDS_IN_HAND: usize = 7; pub const TOTAL_CARDS_IN_DECK: usize = 52; diff --git a/holdem_core/src/evaluator/supersonic.rs b/holdem_core/src/evaluator/supersonic.rs index 1fd70d6..785275b 100644 --- a/holdem_core/src/evaluator/supersonic.rs +++ b/holdem_core/src/evaluator/supersonic.rs @@ -37,49 +37,6 @@ const PAGE_SIZE: usize = 53; /// Initial trie root index (page 1 base entry). const INITIAL_STATE: usize = PAGE_SIZE; -// --------------------------------------------------------------------------- -// TrieCursor β€” opaque partial trie traversal state -// --------------------------------------------------------------------------- - -/// Opaque trie cursor β€” represents a partial traversal of the Supersonic LUT trie. -/// -/// Created by [`SupersonicEvaluator::init_lookup()`] and advanced by -/// [`SupersonicEvaluator::advance_lookup()`]. Cheaply `Copy`able (it's just two integers). -/// -/// # Example: Incremental equity rollout -/// ```ignore -/// // Advance through hole + flop ONCE -/// let base = SupersonicEvaluator::init_lookup(); -/// let base = eval.advance_lookup_slice(base, hole_cards); -/// let base = eval.advance_lookup_slice(base, flop_cards); -/// // base.depth() == 5 -/// -/// // For each turn card, resume from base -/// for turn in remaining_deck.iter() { -/// let after_turn = eval.advance_lookup(base, turn); -/// // after_turn.depth() == 6 -/// for river in remaining_deck.iter() { -/// let after_river = eval.advance_lookup(after_turn, river); -/// let value = eval.finish_7_card(after_river); -/// } -/// } -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TrieCursor { - /// Internal trie pointer β€” index into the `hand_ranks` table. - p: usize, - /// Number of cards that have been advanced through. - depth: u8, -} - -impl TrieCursor { - /// Returns the number of cards advanced through so far. - #[inline] - pub fn depth(&self) -> u8 { - self.depth - } -} - // --------------------------------------------------------------------------- // SupersonicEvaluator // --------------------------------------------------------------------------- @@ -250,27 +207,24 @@ impl SupersonicEvaluator { } } - // Table doesn't exist or is corrupt β€” generate a new one. + // Table doesn't exist or is corrupt β€” generate it. log::info!( - "Generating Supersonic LUT table at {} (takes a few seconds on first run)...", + "Supersonic table not found at {}, generating (takes a few seconds)...", path.display() ); - 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); - } + Ok(se) => { + // Persist for future runs. + if let Err(e) = se.save_to_file(&path) { + log::warn!("Failed to save Supersonic table to {}: {}", path.display(), e); + } else { + log::info!("Supersonic table saved to: {}", path.display()); } - Some(evaluator) + Some(se) } Err(e) => { - log::error!("Failed to create Supersonic evaluator from generated table: {}", e); + log::error!("Failed to create Supersonic evaluator: {}", e); None } } @@ -391,141 +345,6 @@ impl SupersonicEvaluator { pub fn hand_ranks(&self) -> &[u32] { &self.hand_ranks } - - // ----------------------------------------------------------------------- - // Incremental / partial lookup API - // ----------------------------------------------------------------------- - - /// Create a new trie cursor at the root (before any cards). - /// - /// Equivalent to the Java `int p = 53;` initialization. - #[inline] - pub fn init_lookup() -> TrieCursor { - TrieCursor { - p: INITIAL_STATE, - depth: 0, - } - } - - /// Advance the trie cursor by one card. - /// - /// Returns a **new** cursor; the original is not modified. - /// This is a single array lookup: `hand_ranks[p + card_index + 1]`. - #[inline] - pub fn advance_lookup(&self, cursor: TrieCursor, card: &Card) -> TrieCursor { - let idx = card_index(card); - let next = cursor.p + idx + 1; - debug_assert!( - next < self.hand_ranks.len(), - "Supersonic trie index out of bounds: next={}, len={}", - next, - self.hand_ranks.len() - ); - let new_p = self.hand_ranks[next] as usize; - debug_assert!( - new_p < self.hand_ranks.len() || cursor.depth + 1 == 7, - "Supersonic trie pointer out of bounds: p={}, len={}, depth={}", - new_p, - self.hand_ranks.len(), - cursor.depth + 1 - ); - TrieCursor { - p: new_p, - depth: cursor.depth + 1, - } - } - - /// Advance the trie cursor by multiple cards (convenience method). - #[inline] - pub fn advance_lookup_slice(&self, cursor: TrieCursor, cards: &[Card]) -> TrieCursor { - let mut c = cursor; - for card in cards { - c = self.advance_lookup(c, card); - } - c - } - - /// Finish a 5-card partial evaluation. - /// - /// Performs the extra `hand_ranks[p]` dereference needed for < 7 cards. - /// - /// # Panics - /// Debug builds panic if `cursor.depth() != 5`. - #[inline] - pub fn finish_5_card(&self, cursor: TrieCursor) -> HandValue { - debug_assert_eq!(cursor.depth, 5, "finish_5_card requires depth 5, got {}", cursor.depth); - debug_assert!(cursor.p < self.hand_ranks.len(), "cursor p out of bounds"); - packed_to_hand_value(self.hand_ranks[cursor.p]) - } - - /// Finish a 6-card partial evaluation. - /// - /// Performs the extra `hand_ranks[p]` dereference needed for < 7 cards. - /// - /// # Panics - /// Debug builds panic if `cursor.depth() != 6`. - #[inline] - pub fn finish_6_card(&self, cursor: TrieCursor) -> HandValue { - debug_assert_eq!(cursor.depth, 6, "finish_6_card requires depth 6, got {}", cursor.depth); - debug_assert!(cursor.p < self.hand_ranks.len(), "cursor p out of bounds"); - packed_to_hand_value(self.hand_ranks[cursor.p]) - } - - /// Finish a 7-card partial evaluation. - /// - /// The cursor value IS the packed hand rank (no extra dereference). - /// - /// # Panics - /// Debug builds panic if `cursor.depth() != 7`. - #[inline] - pub fn finish_7_card(&self, cursor: TrieCursor) -> HandValue { - debug_assert_eq!(cursor.depth, 7, "finish_7_card requires depth 7, got {}", cursor.depth); - packed_to_hand_value(cursor.p as u32) - } - - /// Finish a partial evaluation, auto-detecting the depth. - /// - /// Returns `None` if depth is not 5, 6, or 7. - #[inline] - pub fn finish_lookup(&self, cursor: TrieCursor) -> Option { - match cursor.depth { - 5 => Some(self.finish_5_card(cursor)), - 6 => Some(self.finish_6_card(cursor)), - 7 => Some(self.finish_7_card(cursor)), - _ => None, - } - } - - // ----------------------------------------------------------------------- - // Raw incremental API (zero-cost, no depth tracking) - // ----------------------------------------------------------------------- - - /// Raw initial state β€” just the constant `53`. - /// - /// Use with [`raw_advance`](Self::raw_advance) and - /// [`raw_finish_partial`](Self::raw_finish_partial) / [`raw_finish_7`](Self::raw_finish_7). - #[inline] - pub const fn raw_init() -> usize { - INITIAL_STATE - } - - /// Advance a raw trie pointer by one card. - #[inline] - pub fn raw_advance(&self, p: usize, card: &Card) -> usize { - self.hand_ranks[p + card_index(card) + 1] as usize - } - - /// Finish for < 7 cards (extra `hand_ranks[p]` dereference). - #[inline] - pub fn raw_finish_partial(&self, p: usize) -> HandValue { - packed_to_hand_value(self.hand_ranks[p]) - } - - /// Finish for exactly 7 cards (`p` is the packed hand rank). - #[inline] - pub fn raw_finish_7(&self, p: usize) -> HandValue { - packed_to_hand_value(p as u32) - } } // --------------------------------------------------------------------------- @@ -571,11 +390,12 @@ fn packed_to_hand_value(packed: u32) -> HandValue { } } _ => { - log::warn!( + log::error!( "Invalid hand category {} in packed value {}, falling back to HighCard", category, packed ); + debug_assert!(false, "Invalid hand category: {} (packed: {})", category, packed); HandRank::HighCard } }; @@ -696,9 +516,19 @@ mod tests { } /// Build a SupersonicEvaluator for testing. Uses the cached file if - /// available; otherwise generates one (takes ~1 second). + /// available; otherwise returns `None` (generation is too slow for unit + /// tests and may panic due to a known bug in the generator). fn supersonic() -> Option { - SupersonicEvaluator::load_or_generate() + let path = SupersonicEvaluator::default_file_path(); + 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. @@ -1008,7 +838,7 @@ mod tests { let mut total = 0u64; let mut rank_mismatches = 0u64; - let ordering_mismatches = 0u64; + let mut ordering_mismatches = 0u64; // Iterate all C(52,5) combinations via nested loops. for i0 in 0u32..52 { @@ -1060,239 +890,4 @@ mod tests { let suit = (idx % 4) as u8; Card::new(rank, suit).expect("valid card") } - - // ----------------------------------------------------------------------- - // Incremental / partial lookup API tests - // ----------------------------------------------------------------------- - - #[test] - fn test_trie_cursor_init() { - let cursor = SupersonicEvaluator::init_lookup(); - assert_eq!(cursor.depth(), 0); - } - - #[test] - fn test_incremental_5_card() { - let ss = match supersonic() { - Some(s) => s, - None => { - eprintln!("Skipping test_incremental_5_card: Supersonic table unavailable"); - return; - } - }; - - // Test several 5-card hands - let hands: Vec<[Card; 5]> = vec![ - // Royal Flush: As Ks Qs Js Ts - [card(12, 3), card(11, 3), card(10, 3), card(9, 3), card(8, 3)], - // Straight: Tc 9d 8s 7h 6c - [card(8, 2), card(7, 1), card(6, 3), card(5, 0), card(4, 2)], - // Two Pair: As Ah 4d 4c 9s - [card(12, 3), card(12, 0), card(2, 1), card(2, 2), card(7, 3)], - // High Card: Ad 9c 7s 4h 2c - [card(12, 1), card(7, 2), card(5, 3), card(2, 0), card(0, 2)], - // Full House: Ks Kh Kd 7c 7s - [card(11, 3), card(11, 0), card(11, 1), card(5, 2), card(5, 3)], - ]; - - for hand in hands { - let batch = ss.evaluate_5_card(&hand); - - // Incremental: one card at a time - let mut cursor = SupersonicEvaluator::init_lookup(); - for card in &hand { - cursor = ss.advance_lookup(cursor, card); - } - assert_eq!(cursor.depth(), 5); - let incremental = ss.finish_5_card(cursor); - assert_eq!(batch, incremental, "5-card mismatch for hand {:?}", hand); - } - } - - #[test] - fn test_incremental_6_card() { - let ss = match supersonic() { - Some(s) => s, - None => { - eprintln!("Skipping test_incremental_6_card: Supersonic table unavailable"); - return; - } - }; - - let hands: Vec<[Card; 6]> = vec![ - // AA + KK + 2s 3s - [card(12, 0), card(12, 1), card(11, 2), card(11, 3), card(0, 3), card(1, 3)], - // Flush draw + pair: Ah Th 8h 5h 2h Ad - [card(12, 0), card(10, 0), card(8, 0), card(5, 0), card(2, 0), card(12, 1)], - // Straight: Tc 9d 8s 7h 6c 5d - [card(8, 2), card(7, 1), card(6, 3), card(5, 0), card(4, 2), card(3, 1)], - ]; - - for hand in hands { - let batch = ss.evaluate_6_card(&hand); - - // Incremental: one card at a time - let mut cursor = SupersonicEvaluator::init_lookup(); - for card in &hand { - cursor = ss.advance_lookup(cursor, card); - } - assert_eq!(cursor.depth(), 6); - let incremental = ss.finish_6_card(cursor); - assert_eq!(batch, incremental, "6-card mismatch for hand {:?}", hand); - } - } - - #[test] - fn test_incremental_7_card() { - let ss = match supersonic() { - Some(s) => s, - None => { - eprintln!("Skipping test_incremental_7_card: Supersonic table unavailable"); - return; - } - }; - - let hands: Vec<[Card; 7]> = vec![ - // AA + KK + 5h 5d 3c - [card(12, 0), card(12, 1), card(11, 2), card(11, 3), card(5, 0), card(5, 1), card(3, 2)], - // Flush: Ah Th + 8h 5h 2h + Kd Tc - [card(12, 0), card(10, 0), card(8, 0), card(5, 0), card(2, 0), card(11, 1), card(9, 2)], - // Royal Flush on board: As Ks Qs Js Ts + 2h 3h - [card(12, 3), card(11, 3), card(10, 3), card(9, 3), card(8, 3), card(0, 0), card(1, 0)], - ]; - - for hand in hands { - let batch = ss.evaluate_7_card(&hand); - - // Incremental: one card at a time - let mut cursor = SupersonicEvaluator::init_lookup(); - for card in &hand { - cursor = ss.advance_lookup(cursor, card); - } - assert_eq!(cursor.depth(), 7); - let incremental = ss.finish_7_card(cursor); - assert_eq!(batch, incremental, "7-card mismatch for hand {:?}", hand); - } - } - - #[test] - fn test_incremental_slice_advance() { - let ss = match supersonic() { - Some(s) => s, - None => { - eprintln!("Skipping test_incremental_slice_advance: Supersonic table unavailable"); - return; - } - }; - - // 7-card hand: Ah Ad Ks Kc 5h 5d 3c - let hand: [Card; 7] = [ - card(12, 0), card(12, 1), card(11, 2), card(11, 3), - card(5, 0), card(5, 1), card(3, 2), - ]; - let batch = ss.evaluate_7_card(&hand); - - // Advance in slices: 2 + 3 + 2 - let cursor = SupersonicEvaluator::init_lookup(); - assert_eq!(cursor.depth(), 0); - - let cursor = ss.advance_lookup_slice(cursor, &hand[0..2]); - assert_eq!(cursor.depth(), 2); - - let cursor = ss.advance_lookup_slice(cursor, &hand[2..5]); - assert_eq!(cursor.depth(), 5); - - let cursor = ss.advance_lookup_slice(cursor, &hand[5..7]); - assert_eq!(cursor.depth(), 7); - - let incremental = ss.finish_7_card(cursor); - assert_eq!(batch, incremental); - } - - #[test] - fn test_raw_api() { - let ss = match supersonic() { - Some(s) => s, - None => { - eprintln!("Skipping test_raw_api: Supersonic table unavailable"); - return; - } - }; - - // 5-card hand: Straight Tc 9d 8s 7h 6c - let hand5: [Card; 5] = [card(8, 2), card(7, 1), card(6, 3), card(5, 0), card(4, 2)]; - let batch5 = ss.evaluate_5_card(&hand5); - - let mut p = SupersonicEvaluator::raw_init(); - for card in &hand5 { - p = ss.raw_advance(p, card); - } - let raw5 = ss.raw_finish_partial(p); - assert_eq!(batch5, raw5); - - // 7-card hand: AA + KK + 5h 5d 3c - let hand7: [Card; 7] = [ - card(12, 0), card(12, 1), card(11, 2), card(11, 3), - card(5, 0), card(5, 1), card(3, 2), - ]; - let batch7 = ss.evaluate_7_card(&hand7); - - let mut p = SupersonicEvaluator::raw_init(); - for card in &hand7 { - p = ss.raw_advance(p, card); - } - let raw7 = ss.raw_finish_7(p); - assert_eq!(batch7, raw7); - } - - #[test] - fn test_finish_lookup_auto_detect() { - let ss = match supersonic() { - Some(s) => s, - None => { - eprintln!("Skipping test_finish_lookup_auto_detect: Supersonic table unavailable"); - return; - } - }; - - // 5-card hand - let hand5: [Card; 5] = [card(12, 3), card(11, 3), card(10, 3), card(9, 3), card(8, 3)]; - let batch5 = ss.evaluate_5_card(&hand5); - let mut cursor = SupersonicEvaluator::init_lookup(); - for card in &hand5 { - cursor = ss.advance_lookup(cursor, card); - } - assert_eq!(ss.finish_lookup(cursor), Some(batch5)); - - // 6-card hand - let hand6: [Card; 6] = [ - card(12, 0), card(12, 1), card(11, 2), card(11, 3), card(0, 3), card(1, 3), - ]; - let batch6 = ss.evaluate_6_card(&hand6); - let mut cursor = SupersonicEvaluator::init_lookup(); - for card in &hand6 { - cursor = ss.advance_lookup(cursor, card); - } - assert_eq!(ss.finish_lookup(cursor), Some(batch6)); - - // 7-card hand - let hand7: [Card; 7] = [ - card(12, 0), card(12, 1), card(11, 2), card(11, 3), - card(5, 0), card(5, 1), card(3, 2), - ]; - let batch7 = ss.evaluate_7_card(&hand7); - let mut cursor = SupersonicEvaluator::init_lookup(); - for card in &hand7 { - cursor = ss.advance_lookup(cursor, card); - } - assert_eq!(ss.finish_lookup(cursor), Some(batch7)); - - // Invalid depths return None - let cursor0 = SupersonicEvaluator::init_lookup(); - assert_eq!(ss.finish_lookup(cursor0), None); - - let cursor1 = ss.advance_lookup(SupersonicEvaluator::init_lookup(), &card(12, 0)); - assert_eq!(cursor1.depth(), 1); - assert_eq!(ss.finish_lookup(cursor1), None); - } } diff --git a/holdem_core/src/evaluator/supersonic_generator.rs b/holdem_core/src/evaluator/supersonic_generator.rs index 9cbe372..4bc33ea 100644 --- a/holdem_core/src/evaluator/supersonic_generator.rs +++ b/holdem_core/src/evaluator/supersonic_generator.rs @@ -2396,97 +2396,71 @@ pub fn generate_hand_ranks() -> Vec { /// 2. Walk the keys again, populating `hand_ranks` with trie pointers and /// evaluated hand ranks. fn generate_tables(hand_ranks: &mut [u32]) { - use std::collections::HashMap; + let mut keys = vec![0i64; MAX_KEYS]; + let mut num_keys: usize = 1; // keys[0] = 0 (empty hand) + let mut max_key: i64 = 0; // ----------------------------------------------------------------------- - // Phase 1: Collect all unique keys via BFS from the empty hand + // Phase 1: Generate all unique keys // ----------------------------------------------------------------------- - log::info!("Phase 1: Generating unique keys..."); + log::info!("Phase 1: Generating and sorting keys..."); - // Map each unique key to its page index (assigned after sorting). - let mut key_to_index: HashMap = HashMap::with_capacity(MAX_KEYS); - key_to_index.insert(0, 0); // empty hand β†’ placeholder index 0 - - // BFS queue: keys whose children still need to be generated. - let mut queue: Vec = Vec::with_capacity(MAX_KEYS); - queue.push(0); // start with the empty hand - - let mut head = 0usize; - while head < queue.len() { - let base_key = queue[head]; - head += 1; + let mut key_index: usize = 0; + while key_index == 0 || keys[key_index] != 0 { + if key_index % (MAX_KEYS / 10) == 0 { + log::info!( + " Phase 1: {} / {} keys processed ({:.0}%)", + key_index, + MAX_KEYS, + (key_index as f64 / MAX_KEYS as f64) * 100.0 + ); + } for ci in 0..NUM_CARDS { - let (key, num_cards) = make_key(base_key, ci); - if key != 0 && num_cards < 7 { - // Only insert truly new keys. - if !key_to_index.contains_key(&key) { - key_to_index.insert(key, 0); // placeholder - queue.push(key); - } + let (key, num_cards) = make_key(keys[key_index], ci); + if num_cards < 7 { + insert_key(&mut keys, &mut num_keys, &mut max_key, key); } } - 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 - ); - } + key_index += 1; } - // Sort keys and assign sequential page indices. - let mut sorted_keys: Vec = 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!( - " Phase 1 complete: {} unique keys generated.", - num_keys + " Phase 1 complete: {} keys generated.", + key_index ); // ----------------------------------------------------------------------- - // Phase 2: Populate hand_ranks trie + // Phase 2: Populate hand ranks // ----------------------------------------------------------------------- log::info!("Phase 2: Generating hand ranks..."); - for key_index in 0..num_keys { - let base_key = sorted_keys[key_index]; - - if key_index > 0 && key_index % (MAX_KEYS / 10) == 0 { + key_index = 0; + while key_index == 0 || keys[key_index] != 0 { + if key_index % (MAX_KEYS / 10) == 0 { log::info!( " Phase 2: {} / {} keys processed ({:.0}%)", key_index, - num_keys, - (key_index as f64 / num_keys as f64) * 100.0 + MAX_KEYS, + (key_index as f64 / MAX_KEYS as f64) * 100.0 ); } for ci in 0..NUM_CARDS { - let (key, num_cards) = make_key(base_key, ci); + let (key, num_cards) = make_key(keys[key_index], ci); let hand_rank = if num_cards < 7 { - // Look up the child key's page index in the HashMap. - let &idx = key_to_index.get(&key).unwrap_or(&0); - 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 - } + // Insert key and get its page index + let idx = insert_key(&mut keys, &mut num_keys, &mut max_key, key); + (idx * PAGE_SIZE + PAGE_SIZE) as u32 } else { // Evaluate the 7-card hand get_hand_rank(key) }; // 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; debug_assert!( max_hand_rank_index < HAND_RANKS_SIZE, @@ -2498,12 +2472,14 @@ fn generate_tables(hand_ranks: &mut [u32]) { // For 6-card and 7-card keys, also populate the base entry if num_cards == 6 || num_cards == 7 { hand_ranks[key_index * PAGE_SIZE + PAGE_SIZE] = - get_hand_rank(base_key); + get_hand_rank(keys[key_index]); } } + + key_index += 1; } - log::info!(" Phase 2 complete: {} keys processed.", num_keys); + log::info!(" Phase 2 complete."); } // --------------------------------------------------------------------------- @@ -2614,6 +2590,73 @@ 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, + 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 // --------------------------------------------------------------------------- @@ -2848,6 +2891,25 @@ mod tests { 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] fn test_eval_5hand_royal_flush() { // Create a royal flush in Cactus Kev representation diff --git a/holdem_core/src/game/README.md b/holdem_core/src/game/README.md index fda441b..3529026 100644 --- a/holdem_core/src/game/README.md +++ b/holdem_core/src/game/README.md @@ -330,8 +330,7 @@ impl GameInfo { /// Total pot size. pub fn get_pot_size(&self) -> ChipCount { ... } - /// Number of raises on the current street (Meerkat semantics). - /// Preflop, the BB counts as the first raise. Postflop, a bet counts as a raise. + /// Number of raises made in the current betting round. pub fn get_num_raises(&self) -> usize { ... } } ``` diff --git a/holdem_core/src/game/action.rs b/holdem_core/src/game/action.rs index 90e8ef7..111a69f 100644 --- a/holdem_core/src/game/action.rs +++ b/holdem_core/src/game/action.rs @@ -296,7 +296,7 @@ mod tests { #[test] fn fold_is_always_valid() { - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; assert!(validate_action( &Action::Fold, pid(1), @@ -313,7 +313,7 @@ mod tests { #[test] fn check_allowed_when_no_bet() { - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; assert!(validate_action( &Action::Check, pid(1), @@ -330,7 +330,7 @@ mod tests { #[test] fn check_not_allowed_when_bet_exists() { - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let result = validate_action( &Action::Check, pid(1), @@ -347,7 +347,7 @@ mod tests { #[test] fn call_always_valid_with_chips() { - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; assert!(validate_action( &Action::Call, pid(1), @@ -364,7 +364,7 @@ mod tests { #[test] fn raise_below_minimum_rejected() { - let bet_limit = BetLimit::NoLimit { min_raise: 20, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 20 }; let result = validate_action( &Action::Raise { total: 30 }, pid(1), @@ -384,7 +384,7 @@ mod tests { #[test] fn raise_with_insufficient_chips_rejected() { - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let result = validate_action( &Action::Raise { total: 200 }, pid(1), @@ -404,7 +404,7 @@ mod tests { #[test] fn all_in_with_zero_chips_rejected() { - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let result = validate_action( &Action::AllIn, pid(1), diff --git a/holdem_core/src/game/betting.rs b/holdem_core/src/game/betting.rs index a6fc0ec..4cef8be 100644 --- a/holdem_core/src/game/betting.rs +++ b/holdem_core/src/game/betting.rs @@ -22,9 +22,6 @@ pub enum BetLimit { NoLimit { /// Minimum raise increment. min_raise: ChipCount, - /// Optional cap on voluntary raises per street (None = unlimited). - /// When set, the Nth raise is converted to a call automatically. - max_raises: Option, }, /// Pot-Limit β€” players may raise up to the current pot size. @@ -83,7 +80,7 @@ pub trait BettingRules { impl BettingRules for BetLimit { fn min_raise_amount(&self, _current_bet: ChipCount, previous_raise: ChipCount) -> ChipCount { match self { - BetLimit::NoLimit { min_raise, .. } => (*min_raise).max(previous_raise), + BetLimit::NoLimit { min_raise } => (*min_raise).max(previous_raise), BetLimit::PotLimit { min_raise } => (*min_raise).max(previous_raise), BetLimit::FixedLimit { small_bet, @@ -150,10 +147,6 @@ impl BettingRules for BetLimit { /// unused β€” the cap is intentionally the same on every street. fn can_raise(&self, _street: Street, raises_this_round: usize) -> bool { match self { - BetLimit::NoLimit { - max_raises: Some(cap), - .. - } => raises_this_round < *cap, BetLimit::NoLimit { .. } | BetLimit::PotLimit { .. } => true, BetLimit::FixedLimit { .. } => raises_this_round < 3, } @@ -178,10 +171,6 @@ pub struct BettingRound { /// The size of the last raise increment (used to compute min-raise). pub last_raise_size: ChipCount, /// Number of raises made so far this round. - /// - /// **Note:** This tracks *voluntary* raises only (for Fixed-Limit cap - /// enforcement). This is distinct from `GameInfo.num_raises`, which uses - /// Meerkat semantics where the BB counts as the first raise preflop. pub num_raises: usize, /// Whether the round is considered complete. pub is_complete: bool, @@ -274,7 +263,7 @@ mod tests { /// Helper β€” No-Limit with a minimum raise of 10 (big-blind sized). fn nl() -> BetLimit { - BetLimit::NoLimit { min_raise: 10, max_raises: None } + BetLimit::NoLimit { min_raise: 10 } } /// Helper β€” Pot-Limit with a minimum raise of 10. diff --git a/holdem_core/src/game/bot_factory.rs b/holdem_core/src/game/bot_factory.rs index 94d0ab8..5e6e88c 100644 --- a/holdem_core/src/game/bot_factory.rs +++ b/holdem_core/src/game/bot_factory.rs @@ -65,11 +65,10 @@ where { let mut map = CUSTOM_FACTORIES.write().unwrap_or_else(|e| e.into_inner()); if map.contains_key(bot_type) { - log::debug!( - "bot type '{}' is already registered; skipping re-registration", + panic!( + "bot type '{}' is already registered; choose a different name", bot_type ); - return; } map.insert(bot_type.to_string(), Box::new(factory)); } diff --git a/holdem_core/src/game/bots/call_bot.rs b/holdem_core/src/game/bots/call_bot.rs index 732e215..8babc15 100644 --- a/holdem_core/src/game/bots/call_bot.rs +++ b/holdem_core/src/game/bots/call_bot.rs @@ -101,7 +101,7 @@ mod tests { fn make_game_info(current_bet: ChipCount, wagered_this_round: ChipCount) -> GameInfo { GameInfo { num_seats: 2, - bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 10 }, small_blind: 5, big_blind: 10, ante: 0, @@ -126,14 +126,12 @@ mod tests { is_dealer: true, is_small_blind: false, is_big_blind: false, - external_id: None, bankroll: 0, })], actions_this_hand: vec![], num_raises: 0, hand_number: 1, active_players: 2, - table_id: String::new(), } } diff --git a/holdem_core/src/game/bots/chump_bot.rs b/holdem_core/src/game/bots/chump_bot.rs index cae62ae..5380979 100644 --- a/holdem_core/src/game/bots/chump_bot.rs +++ b/holdem_core/src/game/bots/chump_bot.rs @@ -321,7 +321,7 @@ mod tests { ) -> GameInfo { GameInfo { num_seats: 2, - bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 20 }, small_blind: 10, big_blind: 20, ante: 0, @@ -346,14 +346,12 @@ mod tests { is_dealer: true, is_small_blind: false, is_big_blind: false, - external_id: None, bankroll: 0, })], actions_this_hand: vec![], num_raises: 0, hand_number: 1, active_players: 2, - table_id: String::new(), } } diff --git a/holdem_core/src/game/bots/flock_bot.rs b/holdem_core/src/game/bots/flock_bot.rs index bff436d..0bc8319 100644 --- a/holdem_core/src/game/bots/flock_bot.rs +++ b/holdem_core/src/game/bots/flock_bot.rs @@ -446,9 +446,9 @@ impl FlockBot { } } PreflopStrength::Strong => { - // num_raises uses Meerkat semantics: BB counts as the first raise preflop. - if num_raises == 1 { - // Unraised pot (just BB): raise to 3x BB + // num_raises is the count of voluntary raises (excludes blinds). + if num_raises == 0 { + // No voluntary raise: raise to 3x BB let raise_target = 3 * big_blind; self.compute_raise_total(game_info, raise_target) .unwrap_or_else(|| { @@ -458,7 +458,7 @@ impl FlockBot { Action::Call } }) - } else if num_raises == 2 { + } else if num_raises == 1 { // Facing one voluntary raise: call if stack <= call_amount { Action::AllIn @@ -466,7 +466,7 @@ impl FlockBot { Action::Call } } else { - // 2+ voluntary raises (3+ in Meerkat count): fold (not premium) + // 2+ voluntary raises: fold (not premium) Action::Fold } } @@ -474,17 +474,6 @@ impl FlockBot { // Always raise/re-raise: 3x BB or 3x current bet, whichever is higher let raise_target = (3 * big_blind).max(3 * game_info.current_bet); self.compute_raise_total(game_info, raise_target) - .or_else(|| { - // If compute_raise_total fails (e.g., stack too small for - // min raise), try a call instead of forcing AllIn. - let call_amount = self.call_amount(game_info); - let stack = self.stack(game_info); - if stack > call_amount { - Some(Action::Call) - } else { - None - } - }) .unwrap_or(Action::AllIn) } } @@ -601,7 +590,7 @@ mod tests { ) -> GameInfo { GameInfo { num_seats: 2, - bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 20 }, small_blind: 10, big_blind: 20, ante: 0, @@ -626,14 +615,12 @@ mod tests { is_dealer: true, is_small_blind: false, is_big_blind: false, - external_id: None, bankroll: 0, })], actions_this_hand: vec![], num_raises: 0, hand_number: 1, active_players: 2, - table_id: String::new(), } } @@ -813,7 +800,7 @@ mod tests { ); // Someone limped (call_amount = BB = 20, no voluntary raises) let mut game_info = make_game_info(20, 40, 1000, 0, Street::Preflop, vec![]); - game_info.num_raises = 1; // Meerkat: BB counts as first raise (unraised pot) + game_info.num_raises = 0; // No voluntary raises yet let action = bot.act(&game_info); // Should raise to 3x BB = 60, not just call assert!( @@ -836,7 +823,7 @@ mod tests { ); // Facing one voluntary raise to 60 (3x BB) let mut game_info = make_game_info(60, 80, 1000, 0, Street::Preflop, vec![]); - game_info.num_raises = 2; // Meerkat: BB + one voluntary raise + game_info.num_raises = 1; // One voluntary raise assert_eq!(bot.act(&game_info), Action::Call); } @@ -853,7 +840,7 @@ mod tests { ); // Facing a 3-bet to 180 (2 voluntary raises) let mut game_info = make_game_info(180, 240, 1000, 0, Street::Preflop, vec![]); - game_info.num_raises = 3; // Meerkat: BB + two voluntary raises + game_info.num_raises = 2; // Two voluntary raises assert_eq!(bot.act(&game_info), Action::Fold); } diff --git a/holdem_core/src/game/bots/fold_bot.rs b/holdem_core/src/game/bots/fold_bot.rs index 257ed77..cef0f4f 100644 --- a/holdem_core/src/game/bots/fold_bot.rs +++ b/holdem_core/src/game/bots/fold_bot.rs @@ -102,7 +102,7 @@ mod tests { fn make_game_info(current_bet: ChipCount, wagered_this_round: ChipCount) -> GameInfo { GameInfo { num_seats: 2, - bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 10 }, small_blind: 5, big_blind: 10, ante: 0, @@ -127,14 +127,12 @@ mod tests { is_dealer: true, is_small_blind: false, is_big_blind: false, - external_id: None, bankroll: 0, })], actions_this_hand: vec![], num_raises: 0, hand_number: 1, active_players: 2, - table_id: String::new(), } } diff --git a/holdem_core/src/game/bots/raise_bot.rs b/holdem_core/src/game/bots/raise_bot.rs index 37ac870..9a6ffd9 100644 --- a/holdem_core/src/game/bots/raise_bot.rs +++ b/holdem_core/src/game/bots/raise_bot.rs @@ -148,12 +148,11 @@ impl HoldemPlayer for RaiseBot { .fixed_bet_size(game_info.street) .unwrap_or(small_bet); - // Check if raises are still allowed (typically max 3 voluntary raises per round). - // GameInfo.num_raises uses Meerkat semantics where BB counts as a raise preflop, - // but the Fixed-Limit cap applies to voluntary raises only. - let voluntary_raises = game_info.voluntary_raises(); + // Check if raises are still allowed (typically max 3 raises per round) + // Use the authoritative raise counter from GameInfo. + let raises_this_round = game_info.num_raises; - if !game_info.bet_limit.can_raise(game_info.street, voluntary_raises) { + if !game_info.bet_limit.can_raise(game_info.street, raises_this_round) { // Can't raise anymore, just call return if call_amount == 0 { Action::Check @@ -213,7 +212,7 @@ mod tests { ) -> GameInfo { GameInfo { num_seats: 2, - bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 10 }, small_blind: 5, big_blind: 10, ante: 0, @@ -238,14 +237,12 @@ mod tests { is_dealer: true, is_small_blind: false, is_big_blind: false, - external_id: None, bankroll: 0, })], actions_this_hand: vec![], num_raises: 0, hand_number: 1, active_players: 2, - table_id: String::new(), } } @@ -287,14 +284,12 @@ mod tests { is_dealer: true, is_small_blind: false, is_big_blind: false, - external_id: None, bankroll: 0, })], actions_this_hand: vec![], num_raises: 0, hand_number: 1, active_players: 2, - table_id: String::new(), } } @@ -332,14 +327,12 @@ mod tests { is_dealer: true, is_small_blind: false, is_big_blind: false, - external_id: None, bankroll: 0, })], actions_this_hand: vec![], num_raises: 0, hand_number: 1, active_players: 2, - table_id: String::new(), } } @@ -438,7 +431,7 @@ mod tests { let mut bot = RaiseBot::new(PlayerId(3), "RaiseBot".to_string()); // Create game info with 3 raises already made on the flop (Fixed-Limit max) let mut game_info = make_fl_game_info(40, 100, 1000, 0, Street::Flop); - game_info.num_raises = 3; // 3 raises already on the flop (postflop = Meerkat count) + game_info.num_raises = 3; // 3 voluntary raises already on the flop let action = bot.act(&game_info); // Can't raise anymore, should call assert_eq!(action, Action::Call); diff --git a/holdem_core/src/game/bots/random_bot.rs b/holdem_core/src/game/bots/random_bot.rs index f704eb5..de26d88 100644 --- a/holdem_core/src/game/bots/random_bot.rs +++ b/holdem_core/src/game/bots/random_bot.rs @@ -100,13 +100,10 @@ impl RandomBot { } } - // Check if raising is possible. - // GameInfo.num_raises uses Meerkat semantics where BB counts as a raise preflop, - // but the Fixed-Limit cap applies to voluntary raises only. - let voluntary_raises = game_info.voluntary_raises(); + // Check if raising is possible let can_raise = game_info .bet_limit - .can_raise(game_info.street, voluntary_raises); + .can_raise(game_info.street, game_info.num_raises); if can_raise && stack > call_amount { // Player has chips remaining after calling @@ -232,7 +229,7 @@ mod tests { ) -> GameInfo { GameInfo { num_seats: 2, - bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 10 }, small_blind: 5, big_blind: 10, ante: 0, @@ -257,14 +254,12 @@ mod tests { is_dealer: true, is_small_blind: false, is_big_blind: false, - external_id: None, bankroll: 0, })], actions_this_hand: vec![], num_raises: 0, hand_number: 1, active_players: 2, - table_id: String::new(), } } diff --git a/holdem_core/src/game/config.rs b/holdem_core/src/game/config.rs index a48483e..7a21b7a 100644 --- a/holdem_core/src/game/config.rs +++ b/holdem_core/src/game/config.rs @@ -186,9 +186,6 @@ pub enum BetLimitConfig { NoLimit { /// Minimum raise increment. min_raise: ChipCount, - /// Optional cap on voluntary raises per street (None = unlimited). - #[serde(default)] - max_raises: Option, }, /// Pot-Limit β€” players may raise up to the current pot size. #[serde(rename = "pot_limit")] @@ -209,7 +206,7 @@ pub enum BetLimitConfig { impl From for BetLimit { fn from(config: BetLimitConfig) -> Self { match config { - BetLimitConfig::NoLimit { min_raise, max_raises } => BetLimit::NoLimit { min_raise, max_raises }, + BetLimitConfig::NoLimit { min_raise } => BetLimit::NoLimit { min_raise }, BetLimitConfig::PotLimit { min_raise } => BetLimit::PotLimit { min_raise }, BetLimitConfig::FixedLimit { small_bet, big_bet } => { BetLimit::FixedLimit { small_bet, big_bet } @@ -221,7 +218,7 @@ impl From for BetLimit { impl From for BetLimitConfig { fn from(limit: BetLimit) -> Self { match limit { - BetLimit::NoLimit { min_raise, max_raises } => BetLimitConfig::NoLimit { min_raise, max_raises }, + BetLimit::NoLimit { min_raise } => BetLimitConfig::NoLimit { min_raise }, BetLimit::PotLimit { min_raise } => BetLimitConfig::PotLimit { min_raise }, BetLimit::FixedLimit { small_bet, big_bet } => { BetLimitConfig::FixedLimit { small_bet, big_bet } @@ -414,7 +411,6 @@ impl Default for GameConfig { num_seats: default_num_seats(), bet_limit: BetLimitConfig::NoLimit { min_raise: 20, - max_raises: None, }, small_blind: default_small_blind(), big_blind: default_big_blind(), @@ -878,7 +874,7 @@ big_bet = 20 fn bet_limit_config_to_bet_limit() { let config = BetLimitConfig::NoLimit { min_raise: 20 }; let limit: BetLimit = config.into(); - assert_eq!(limit, BetLimit::NoLimit { min_raise: 20, max_raises: None }); + assert_eq!(limit, BetLimit::NoLimit { min_raise: 20 }); let config = BetLimitConfig::FixedLimit { small_bet: 10, @@ -1174,7 +1170,7 @@ min_raise = 50 assert_eq!(table_config.small_blind, 10); assert_eq!(table_config.big_blind, 20); assert_eq!(table_config.ante, 5); - assert_eq!(table_config.bet_limit, BetLimit::NoLimit { min_raise: 20, max_raises: None }); + assert_eq!(table_config.bet_limit, BetLimit::NoLimit { min_raise: 20 }); } #[test] diff --git a/holdem_core/src/game/game_info.rs b/holdem_core/src/game/game_info.rs index 2030896..fcfcb4c 100644 --- a/holdem_core/src/game/game_info.rs +++ b/holdem_core/src/game/game_info.rs @@ -75,16 +75,13 @@ pub struct GameInfo { /// /// **Meerkat convention:** On the preflop street (index 0), the small blind /// and big blind postings are recorded as [`Action::Raise`] entries at the - /// beginning of the list. Use [`num_raises`](Self::num_raises) for the - /// authoritative raise count on the current street (Meerkat semantics where - /// the BB counts as the first raise preflop). + /// beginning of the list. These are *not* voluntary player raises. Use + /// [`num_raises`](Self::num_raises) for the authoritative count of voluntary + /// raises on the current street, which excludes blind postings. pub actions_this_hand: Vec>, // --- Action counters (authoritative, maintained by the engine) --- - /// Number of raises on the current street (Meerkat semantics). - /// - /// Preflop, the BB counts as the first raise. Postflop, a bet counts as - /// a raise. This means `to_call > 0` always implies `num_raises >= 1`. + /// Number of voluntary raises on the current street (excludes blinds). /// Reset to 0 when advancing to a new street. pub num_raises: usize, @@ -94,10 +91,6 @@ pub struct GameInfo { /// Number of active (non-folded, non-all-in) players. pub active_players: usize, - - /// Optional table identifier for external systems (e.g. live server). - /// Empty string in simulation; set by live server for live play. - pub table_id: String, } impl GameInfo { @@ -207,24 +200,10 @@ impl GameInfo { self.pot } - /// Returns the number of raises on the current street (Meerkat semantics). + /// Returns the number of voluntary raises on the current street. /// - /// Preflop, the BB counts as the first raise. Postflop, a bet counts as - /// a raise. Corresponds to Meerkat's `GameInfo.getNumRaises()`. + /// Corresponds to Meerkat's `GameInfo.getNumRaises()`. pub fn get_num_raises(&self) -> usize { self.num_raises } - - /// Returns the number of *voluntary* raises on the current street. - /// - /// This converts from Meerkat semantics (where the BB counts as the first - /// raise preflop) to the count of actual player-initiated raises. Useful - /// for Fixed-Limit cap enforcement where only voluntary raises are capped. - pub fn voluntary_raises(&self) -> usize { - if self.is_preflop() { - self.num_raises.saturating_sub(1) - } else { - self.num_raises - } - } } diff --git a/holdem_core/src/game/hand_runner.rs b/holdem_core/src/game/hand_runner.rs index 305314d..589ba95 100644 --- a/holdem_core/src/game/hand_runner.rs +++ b/holdem_core/src/game/hand_runner.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use rand::SeedableRng; use crate::board::Street; -use crate::evaluator::evaluator::Evaluator; +use crate::evaluator::algorithmic_evaluator::AlgorithmicEvaluator; use crate::evaluator::evaluator::{HandRank, HandValue}; use crate::{Board, Deck, Hand, HoleCards}; @@ -194,13 +194,11 @@ pub struct HandRunner { /// Index 0 = preflop, 1 = flop, 2 = turn, 3 = river. /// /// **⚠ Meerkat convention:** On preflop, the SB and BB blind postings are - /// recorded as `Action::Raise` entries (the first two entries). The - /// `GameInfo.num_raises` field uses Meerkat semantics where the BB counts - /// as the first raise preflop (SB is excluded). Use - /// `blind_postings_snapshot` for accurate blind data in hand history + /// recorded as `Action::Raise` entries (the first two entries). Code that + /// counts voluntary raises from this field must subtract 2 on preflop. + /// Use `blind_postings_snapshot` for accurate blind data in hand history /// consumers. The `BettingRound.num_raises` field tracks only voluntary - /// raises for Fixed-Limit cap enforcement and is unaffected by this - /// convention. + /// raises and is unaffected by this convention. actions_this_hand: Vec>, /// Optional RNG seed for reproducible shuffles. seed: Option, @@ -559,17 +557,6 @@ impl HandRunner { .unwrap_or(0); let street = self.current_street(); - // Enforce raise cap for No-Limit with max_raises: convert Raise to Call. - let action = if let Action::Raise { .. } = &action { - if !self.bet_limit.can_raise(street, raises_this_round) { - Action::Call - } else { - action - } - } else { - action - }; - validate_action( &action, next_id, @@ -743,7 +730,28 @@ impl HandRunner { let mut hand_values: HashMap = HashMap::new(); for (_, player_id, hole_cards) in &active_players { if let Ok(hand) = Hand::from_hole_cards_and_board(hole_cards, &self.board) { - let value = Evaluator::instance().evaluate_hand(&hand); + let cards = hand.cards(); + 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); } } @@ -1139,20 +1147,6 @@ impl HandRunner { } } - // 4b. Reveal all hole cards for RL training (no-op for production bots). - { - let all_cards: Vec<(PlayerId, HoleCards)> = self - .seats - .iter() - .filter_map(|s| s.hole_cards.map(|hc| (s.player_id, hc))) - .collect(); - let game_info = self.build_game_info(); - for seat in &mut self.seats { - seat.player - .cards_revealed_event(&all_cards, &game_info); - } - } - // 5. Set state to Complete. self.state = HandState::Complete; @@ -1411,11 +1405,8 @@ impl HandRunner { .and_then(|id| self.find_seat_index_by_player_id(id)) .map(|idx| self.seats[idx].index); - // Count raises on the current street using Meerkat semantics. + // Count voluntary raises on the current street (exclude blind postings). // On preflop, the first two Raise entries are the SB and BB postings. - // The BB counts as the first raise (Meerkat convention), but the SB does not. - // So we subtract 1 (for the SB only), keeping the BB as a counted raise. - // Postflop, all Raise entries are counted directly. let street_idx = self.street_to_action_index(); let num_raises = self .actions_this_hand @@ -1425,10 +1416,10 @@ impl HandRunner { .iter() .filter(|(_, a)| matches!(a, Action::Raise { .. })) .count(); - // On preflop, subtract 1 for the SB posting only. - // The BB remains counted as the first raise (Meerkat semantics). + // On preflop, the SB and BB are recorded as raises (Meerkat convention). + // Subtract them to get the count of voluntary player raises. if self.current_street() == Street::Preflop { - total_raises.saturating_sub(1) + total_raises.saturating_sub(2) } else { total_raises } @@ -1455,7 +1446,6 @@ impl HandRunner { num_raises, hand_number: self.hand_number, active_players: self.count_active_players(), - table_id: String::new(), } } } @@ -1668,7 +1658,7 @@ mod tests { let seats = vec![seat0, seat1]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); assert_eq!(runner.state(), HandState::Idle); @@ -1703,7 +1693,7 @@ mod tests { let seats = vec![seat0, seat1]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); runner.start(10, 20, 0); @@ -1739,7 +1729,7 @@ mod tests { let seats = vec![seat0, seat1, seat2]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); runner.start(10, 20, 0); @@ -1765,7 +1755,7 @@ mod tests { let seats = vec![seat0, seat1]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); runner.start(10, 20, 0); @@ -1787,7 +1777,7 @@ mod tests { let seats = vec![seat0, seat1]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); assert_eq!(runner.state(), HandState::Idle); @@ -1810,7 +1800,7 @@ mod tests { let seats = vec![seat0, seat1]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); let result = runner.step(); @@ -1830,7 +1820,7 @@ mod tests { let seats = vec![seat0, seat1]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); runner.start(10, 20, 0); @@ -1866,7 +1856,7 @@ mod tests { let seats = vec![seat0, seat1, seat2]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); runner.start(10, 20, 0); @@ -1982,7 +1972,7 @@ mod tests { let seats = vec![seat0, seat1, seat2]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); runner.start(10, 20, 0); @@ -2008,7 +1998,7 @@ mod tests { let seats = vec![seat0, seat1]; let deck = Deck::new(); - let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None }; + let bet_limit = BetLimit::NoLimit { min_raise: 10 }; let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1); runner.start(10, 20, 0); diff --git a/holdem_core/src/game/player.rs b/holdem_core/src/game/player.rs index 6ae0fe2..95edb2c 100644 --- a/holdem_core/src/game/player.rs +++ b/holdem_core/src/game/player.rs @@ -230,24 +230,6 @@ pub trait HoldemPlayer: Send + Sync { /// Corresponds to `com.biotools.meerkat.Player#gameOverEvent`. fn game_over_event(&mut self, game_info: &GameInfo); - /// Called after the hand ends with ALL players' hole cards revealed. - /// - /// **Training only** β€” allows RL agents to compute realized equity for - /// every decision point. The default implementation is a no-op; production - /// bots should never override this (they don't see opponents' cards). - /// - /// # Arguments - /// - /// * `all_hole_cards` β€” Slice of `(PlayerId, HoleCards)` for every player - /// who was dealt into this hand, including those who folded. - /// * `game_info` β€” Final game state snapshot. - fn cards_revealed_event( - &mut self, - _all_hole_cards: &[(PlayerId, HoleCards)], - _game_info: &GameInfo, - ) { - } - /// Signal an early-fold intent. /// /// Called by the framework layer when a player wants to fold before it is @@ -269,12 +251,6 @@ pub trait HoldemPlayer: Send + Sync { /// * `placement` β€” The player's finishing position (1-based). A placement /// of `1` means the player won the tournament. fn on_eliminated(&self, placement: u32); - - /// Called when the server is shutting down or the table is being removed. - /// - /// Override to persist state (e.g. opponent models, learned data) before - /// the process exits. The default implementation is a no-op. - fn shutdown(&mut self) {} } // --------------------------------------------------------------------------- @@ -412,7 +388,6 @@ impl Seat { is_dealer, is_small_blind, is_big_blind, - external_id: None, bankroll: self.bankroll, } } diff --git a/holdem_core/src/game/player_info.rs b/holdem_core/src/game/player_info.rs index e48be59..2ab1b19 100644 --- a/holdem_core/src/game/player_info.rs +++ b/holdem_core/src/game/player_info.rs @@ -66,11 +66,6 @@ pub struct PlayerInfo { /// Whether this player is the big blind. pub is_big_blind: bool, - /// Stable external player ID (e.g. Torn user ID). Used by Gen 4 opponent - /// modelling to track players across seat changes and name changes. - /// `None` in simulation. - pub external_id: Option, - /// The player's overall bankroll balance across all hands. /// Positive = profit, negative = loss. pub bankroll: i64, diff --git a/holdem_core/src/game/simulation.rs b/holdem_core/src/game/simulation.rs index 4be7da4..0675e47 100644 --- a/holdem_core/src/game/simulation.rs +++ b/holdem_core/src/game/simulation.rs @@ -8,7 +8,6 @@ use std::collections::{HashMap, HashSet}; use rand::seq::SliceRandom; use rand::{Rng, SeedableRng}; -use rayon::prelude::*; use super::bot_factory; use super::config::GameConfig; @@ -129,29 +128,10 @@ pub fn run_cash_game(config: &GameConfig) -> Result<(SimulationStats, Table), Ga } } } - Err(GameError::NotEnoughPlayers(count)) => { - // Not enough active players to continue. This should only - // happen if reset_bankroll_each_hand / auto_rebuy are both - // disabled, or if seats were lost due to a hand error. - log::warn!( - "Simulation stopped after {} hands: only {count} active players. \ - Set reset_bankroll_each_hand=true or auto_rebuy=true to prevent this.", - stats.total_hands - ); + Err(GameError::NotEnoughPlayers(_)) => { + // Not enough active players to continue. break; } - Err(GameError::RaiseBelowMinimum(raise, min)) => { - // A bot returned an illegal raise. Skip this hand and - // continue rather than aborting the entire simulation. - log::warn!( - "Bot returned illegal raise ({raise} < min {min}) β€” skipping hand" - ); - } - Err(GameError::InsufficientChips(pid, needed, has)) => { - log::warn!( - "Bot {pid} insufficient chips (needs {needed}, has {has}) β€” skipping hand" - ); - } Err(e) => { return Err(e); } @@ -248,122 +228,121 @@ pub fn run_sng_series( let mut result = SngSeriesResult::new(num_tournaments, buy_in, base_seed); - struct TourneyResult { - total_hands: u64, - player_results: Vec<(String, u32, ChipCount)>, - } + for tournament_index in 0..num_tournaments { + let table_config = config.to_table_config(); + let blind_schedule = config.to_blind_schedule(); + let prize_structure = config.to_prize_structure(); - let tournament_results: Vec = (0..num_tournaments) - .into_par_iter() - .map(|tournament_index| { - let table_config = config.to_table_config(); - let blind_schedule = config.to_blind_schedule(); - let prize_structure = config.to_prize_structure(); + // Create the appropriate tournament type based on game_type. + let mut tournament = match config.game_type { + super::tournament::GameType::MultiTableTournament => Tournament::new_mtt( + config.num_tables, + table_config, + blind_schedule, + prize_structure, + config.starting_chips, + buy_in, + ), + _ => Tournament::new_sng( + table_config, + blind_schedule, + prize_structure, + config.starting_chips, + buy_in, + ), + }; - let mut tournament = match config.game_type { - super::tournament::GameType::MultiTableTournament => Tournament::new_mtt( - config.num_tables, - table_config, - blind_schedule, - 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, + // shuffle the bot order using a seed derived from the base seed + + // tournament index for reproducibility. + let mut order: Vec = (0..config.bots.len()).collect(); + if config.randomize_seats { + 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); + } - let mut order: Vec = (0..config.bots.len()).collect(); - if config.randomize_seats { - 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); - } + // Create fresh bots and register them in the (possibly shuffled) order. + // We need to track names by PlayerId for this tournament. + let mut player_names: HashMap = HashMap::new(); - let mut player_names: HashMap = HashMap::new(); - for (seat_seq, &bot_idx) in order.iter().enumerate() { - let bot_config = &config.bots[bot_idx]; - let player_id = PlayerId(seat_seq as u64); - 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)?; - } + for (seat_seq, &bot_idx) in order.iter().enumerate() { + let bot_config = &config.bots[bot_idx]; + let player_id = PlayerId(seat_seq as u64); + let bot = bot_factory::create_bot(bot_config, player_id) + .map_err(GameError::Custom)?; - log::info!( - "Running tournament {}/{}...", + let name = bot.name().to_string(); + player_names.insert(player_id, name); + + 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, - num_tournaments + standings.len(), + expected_standings ); - tournament.run_to_completion()?; + } - let total_hands = tournament.total_hands(); - let standings = tournament.standings(); + // Track which player IDs have been recorded to prevent double-counting. + let mut recorded: HashSet = HashSet::new(); - let expected_standings = player_names.len().saturating_sub(1); - if standings.len() != expected_standings { - log::warn!( - "Tournament {} ended with {} standings (expected {})", - tournament_index + 1, - standings.len(), - expected_standings - ); - } + // Standings contain eliminated players with their finish position. + // The placement value is the correct finish position (2 = runner-up, + // N = first eliminated). Players eliminated in the same hand share + // the same position. + for &(player_id, finish_position) in standings { + let name = player_names + .get(&player_id) + .cloned() + .unwrap_or_else(|| format!("Player({})", player_id.0)); - let mut player_results = Vec::new(); - let mut recorded: HashSet = HashSet::new(); + let prize = tournament + .prize_structure() + .prize_for_placement(finish_position) + .unwrap_or(0); - for &(player_id, finish_position) in standings { + log::debug!( + "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 - .get(&player_id) + .get(&winner_id) .cloned() - .unwrap_or_else(|| format!("Player({})", player_id.0)); + .unwrap_or_else(|| format!("Player({})", winner_id.0)); + let prize = tournament .prize_structure() - .prize_for_placement(finish_position) + .prize_for_placement(1) .unwrap_or(0); - log::debug!( - "Tournament {}: {} finish={} prize={}", - tournament_index + 1, - name, - finish_position, - prize - ); - player_results.push((name, finish_position, prize)); - recorded.insert(player_id); + + result.record_player_finish(&name, 1, prize); } - - 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::, 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); } } diff --git a/holdem_core/src/game/stats.rs b/holdem_core/src/game/stats.rs index c7d2adf..64cf4bf 100644 --- a/holdem_core/src/game/stats.rs +++ b/holdem_core/src/game/stats.rs @@ -887,7 +887,7 @@ mod tests { fn record_tournament_result_processes_completed_tournament() { let config = TableConfig { num_seats: 9, - bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 20 }, small_blind: 10, big_blind: 20, ante: 0, diff --git a/holdem_core/src/game/table.rs b/holdem_core/src/game/table.rs index ace6e5e..028213b 100644 --- a/holdem_core/src/game/table.rs +++ b/holdem_core/src/game/table.rs @@ -56,7 +56,7 @@ impl Default for TableConfig { fn default() -> Self { Self { num_seats: 9, - bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 10 }, small_blind: 10, big_blind: 20, ante: 0, @@ -330,6 +330,11 @@ impl Table { return Err(GameError::HandAlreadyInProgress); } + // 2. Count active players (non-SittingOut). + let active_count = self.active_player_count(); + if active_count < MIN_PLAYERS as usize { + return Err(GameError::NotEnoughPlayers(active_count)); + } // 2.5. Bankroll management. if self.reset_bankroll_each_hand { @@ -411,36 +416,7 @@ impl Table { .ok_or(GameError::NoHandInProgress)?; // Run the hand to completion. - let result = match runner.run_to_completion() { - Ok(result) => result, - Err(e) => { - // CRITICAL: Restore seats even on error. Without this, the - // seats are trapped inside the dropped runner, leaving - // self.seats empty. The next start_hand() would see 0 active - // players, return NotEnoughPlayers, and silently terminate - // the entire simulation. - log::warn!("Hand execution failed ({e:?}) β€” restoring seats to table"); - let seats = runner.into_seats(); - for seat in seats { - let idx = seat.index as usize; - if idx < self.seats.len() { - let mut seat = seat; - // Refund wagered chips before resetting β€” the pot_manager - // is dropped with the runner, so wagered chips would - // vanish without this refund. - seat.stack += seat.wagered_this_hand; - if seat.stack == 0 { - seat.state = PlayerState::SittingOut; - } else { - seat.reset_for_new_hand(); - } - self.seats[idx] = Some(seat); - } - } - self.current_hand = None; - return Err(e); - } - }; + let result = runner.run_to_completion()?; // Extract seats from the runner and restore them to the table. // HandRunner::into_seats() consumes the runner and returns the seats @@ -587,7 +563,6 @@ impl Table { num_raises: 0, hand_number: self.hand_number, active_players: self.active_player_count(), - table_id: String::new(), } } @@ -858,7 +833,7 @@ mod tests { fn default_config() -> TableConfig { TableConfig { num_seats: 6, - bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 10 }, small_blind: 10, big_blind: 20, ante: 0, @@ -1313,7 +1288,7 @@ mod tests { fn test_table_full() { let config = TableConfig { num_seats: 2, - bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 10 }, small_blind: 10, big_blind: 20, ante: 0, diff --git a/holdem_core/src/game/tournament.rs b/holdem_core/src/game/tournament.rs index 33d0eb5..ccbb96c 100644 --- a/holdem_core/src/game/tournament.rs +++ b/holdem_core/src/game/tournament.rs @@ -1006,7 +1006,7 @@ mod tests { fn default_table_config() -> TableConfig { TableConfig { num_seats: 9, - bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 20 }, small_blind: 10, big_blind: 20, ante: 0, @@ -1188,7 +1188,7 @@ mod tests { fn test_sng_registration_fills_table() { let config = TableConfig { num_seats: 4, - bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 20 }, small_blind: 10, big_blind: 20, ante: 0, @@ -1217,7 +1217,7 @@ mod tests { fn test_sng_run_single_hand() { let config = TableConfig { num_seats: 4, - bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 20 }, small_blind: 10, big_blind: 20, ante: 0, @@ -1283,7 +1283,7 @@ mod tests { fn test_mtt_table_balancing() { let config = TableConfig { num_seats: 9, - bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 20 }, small_blind: 10, big_blind: 20, ante: 0, @@ -1328,7 +1328,7 @@ mod tests { fn test_blind_advancement() { let config = TableConfig { num_seats: 4, - bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None }, + bet_limit: BetLimit::NoLimit { min_raise: 20 }, small_blind: 10, big_blind: 20, ante: 0,