diff --git a/holdem_core/src/evaluator/evaluator.rs b/holdem_core/src/evaluator/evaluator.rs index 9d2fa91..d110d33 100644 --- a/holdem_core/src/evaluator/evaluator.rs +++ b/holdem_core/src/evaluator/evaluator.rs @@ -723,6 +723,15 @@ 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/game/README.md b/holdem_core/src/game/README.md index 3529026..fda441b 100644 --- a/holdem_core/src/game/README.md +++ b/holdem_core/src/game/README.md @@ -330,7 +330,8 @@ impl GameInfo { /// Total pot size. pub fn get_pot_size(&self) -> ChipCount { ... } - /// Number of raises made in the current betting round. + /// Number of raises on the current street (Meerkat semantics). + /// Preflop, the BB counts as the first raise. Postflop, a bet counts as a raise. pub fn get_num_raises(&self) -> usize { ... } } ``` diff --git a/holdem_core/src/game/betting.rs b/holdem_core/src/game/betting.rs index 4cef8be..5008794 100644 --- a/holdem_core/src/game/betting.rs +++ b/holdem_core/src/game/betting.rs @@ -171,6 +171,10 @@ 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, diff --git a/holdem_core/src/game/bots/flock_bot.rs b/holdem_core/src/game/bots/flock_bot.rs index 0bc8319..98ba576 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 is the count of voluntary raises (excludes blinds). - if num_raises == 0 { - // No voluntary raise: raise to 3x BB + // num_raises uses Meerkat semantics: BB counts as the first raise preflop. + if num_raises == 1 { + // Unraised pot (just BB): 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 == 1 { + } else if num_raises == 2 { // Facing one voluntary raise: call if stack <= call_amount { Action::AllIn @@ -466,7 +466,7 @@ impl FlockBot { Action::Call } } else { - // 2+ voluntary raises: fold (not premium) + // 2+ voluntary raises (3+ in Meerkat count): fold (not premium) Action::Fold } } @@ -800,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 = 0; // No voluntary raises yet + game_info.num_raises = 1; // Meerkat: BB counts as first raise (unraised pot) let action = bot.act(&game_info); // Should raise to 3x BB = 60, not just call assert!( @@ -823,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 = 1; // One voluntary raise + game_info.num_raises = 2; // Meerkat: BB + one voluntary raise assert_eq!(bot.act(&game_info), Action::Call); } @@ -840,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 = 2; // Two voluntary raises + game_info.num_raises = 3; // Meerkat: BB + two voluntary raises assert_eq!(bot.act(&game_info), Action::Fold); } diff --git a/holdem_core/src/game/bots/raise_bot.rs b/holdem_core/src/game/bots/raise_bot.rs index 9a6ffd9..d8d6e85 100644 --- a/holdem_core/src/game/bots/raise_bot.rs +++ b/holdem_core/src/game/bots/raise_bot.rs @@ -148,11 +148,12 @@ impl HoldemPlayer for RaiseBot { .fixed_bet_size(game_info.street) .unwrap_or(small_bet); - // 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; + // 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(); - if !game_info.bet_limit.can_raise(game_info.street, raises_this_round) { + if !game_info.bet_limit.can_raise(game_info.street, voluntary_raises) { // Can't raise anymore, just call return if call_amount == 0 { Action::Check @@ -431,7 +432,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 voluntary raises already on the flop + game_info.num_raises = 3; // 3 raises already on the flop (postflop = Meerkat count) 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 de26d88..2cd2051 100644 --- a/holdem_core/src/game/bots/random_bot.rs +++ b/holdem_core/src/game/bots/random_bot.rs @@ -100,10 +100,13 @@ impl RandomBot { } } - // Check if raising is possible + // 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(); let can_raise = game_info .bet_limit - .can_raise(game_info.street, game_info.num_raises); + .can_raise(game_info.street, voluntary_raises); if can_raise && stack > call_amount { // Player has chips remaining after calling diff --git a/holdem_core/src/game/game_info.rs b/holdem_core/src/game/game_info.rs index fcfcb4c..bcec2cb 100644 --- a/holdem_core/src/game/game_info.rs +++ b/holdem_core/src/game/game_info.rs @@ -75,13 +75,16 @@ 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. 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. + /// 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). pub actions_this_hand: Vec>, // --- Action counters (authoritative, maintained by the engine) --- - /// Number of voluntary raises on the current street (excludes blinds). + /// 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`. /// Reset to 0 when advancing to a new street. pub num_raises: usize, @@ -200,10 +203,24 @@ impl GameInfo { self.pot } - /// Returns the number of voluntary raises on the current street. + /// Returns the number of raises on the current street (Meerkat semantics). /// - /// Corresponds to Meerkat's `GameInfo.getNumRaises()`. + /// Preflop, the BB counts as the first raise. Postflop, a bet counts as + /// a raise. 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 589ba95..3e0e595 100644 --- a/holdem_core/src/game/hand_runner.rs +++ b/holdem_core/src/game/hand_runner.rs @@ -194,11 +194,13 @@ 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). Code that - /// counts voluntary raises from this field must subtract 2 on preflop. - /// Use `blind_postings_snapshot` for accurate blind data in hand history + /// 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 /// consumers. The `BettingRound.num_raises` field tracks only voluntary - /// raises and is unaffected by this convention. + /// raises for Fixed-Limit cap enforcement and is unaffected by this + /// convention. actions_this_hand: Vec>, /// Optional RNG seed for reproducible shuffles. seed: Option, @@ -1405,8 +1407,11 @@ impl HandRunner { .and_then(|id| self.find_seat_index_by_player_id(id)) .map(|idx| self.seats[idx].index); - // Count voluntary raises on the current street (exclude blind postings). + // Count raises on the current street using Meerkat semantics. // 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 @@ -1416,10 +1421,10 @@ impl HandRunner { .iter() .filter(|(_, a)| matches!(a, Action::Raise { .. })) .count(); - // On preflop, the SB and BB are recorded as raises (Meerkat convention). - // Subtract them to get the count of voluntary player raises. + // On preflop, subtract 1 for the SB posting only. + // The BB remains counted as the first raise (Meerkat semantics). if self.current_street() == Street::Preflop { - total_raises.saturating_sub(2) + total_raises.saturating_sub(1) } else { total_raises }