feat: align num_raises with Meerkat semantics and add voluntary_raises() helper
- Change GameInfo.num_raises to use Meerkat convention where BB counts as the first raise preflop (subtract 1 for SB only, not 2) - Add GameInfo::voluntary_raises() for Fixed-Limit cap enforcement - Update flock_bot, raise_bot, and random_bot to use voluntary_raises() for can_raise() checks - Add Evaluator::supersonic() accessor for incremental trie API - Update documentation across betting.rs, game_info.rs, hand_runner.rs, and README.md to clarify the two counting conventions
This commit is contained in:
parent
156904f2bf
commit
a46a800d11
8 changed files with 70 additions and 30 deletions
|
|
@ -723,6 +723,15 @@ impl Evaluator {
|
||||||
self.supersonic.is_some()
|
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
|
/// Evaluate a 5-card hand
|
||||||
pub fn evaluate_5_card(&self, cards: &[Card; 5]) -> HandValue {
|
pub fn evaluate_5_card(&self, cards: &[Card; 5]) -> HandValue {
|
||||||
// Priority 1: Supersonic LUT (O(5) array lookups)
|
// Priority 1: Supersonic LUT (O(5) array lookups)
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,8 @@ impl GameInfo {
|
||||||
/// Total pot size.
|
/// Total pot size.
|
||||||
pub fn get_pot_size(&self) -> ChipCount { ... }
|
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 { ... }
|
pub fn get_num_raises(&self) -> usize { ... }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,10 @@ pub struct BettingRound {
|
||||||
/// The size of the last raise increment (used to compute min-raise).
|
/// The size of the last raise increment (used to compute min-raise).
|
||||||
pub last_raise_size: ChipCount,
|
pub last_raise_size: ChipCount,
|
||||||
/// Number of raises made so far this round.
|
/// 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,
|
pub num_raises: usize,
|
||||||
/// Whether the round is considered complete.
|
/// Whether the round is considered complete.
|
||||||
pub is_complete: bool,
|
pub is_complete: bool,
|
||||||
|
|
|
||||||
|
|
@ -446,9 +446,9 @@ impl FlockBot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PreflopStrength::Strong => {
|
PreflopStrength::Strong => {
|
||||||
// num_raises is the count of voluntary raises (excludes blinds).
|
// num_raises uses Meerkat semantics: BB counts as the first raise preflop.
|
||||||
if num_raises == 0 {
|
if num_raises == 1 {
|
||||||
// No voluntary raise: raise to 3x BB
|
// Unraised pot (just BB): raise to 3x BB
|
||||||
let raise_target = 3 * big_blind;
|
let raise_target = 3 * big_blind;
|
||||||
self.compute_raise_total(game_info, raise_target)
|
self.compute_raise_total(game_info, raise_target)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
|
|
@ -458,7 +458,7 @@ impl FlockBot {
|
||||||
Action::Call
|
Action::Call
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if num_raises == 1 {
|
} else if num_raises == 2 {
|
||||||
// Facing one voluntary raise: call
|
// Facing one voluntary raise: call
|
||||||
if stack <= call_amount {
|
if stack <= call_amount {
|
||||||
Action::AllIn
|
Action::AllIn
|
||||||
|
|
@ -466,7 +466,7 @@ impl FlockBot {
|
||||||
Action::Call
|
Action::Call
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 2+ voluntary raises: fold (not premium)
|
// 2+ voluntary raises (3+ in Meerkat count): fold (not premium)
|
||||||
Action::Fold
|
Action::Fold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -800,7 +800,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
// Someone limped (call_amount = BB = 20, no voluntary raises)
|
// Someone limped (call_amount = BB = 20, no voluntary raises)
|
||||||
let mut game_info = make_game_info(20, 40, 1000, 0, Street::Preflop, vec![]);
|
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);
|
let action = bot.act(&game_info);
|
||||||
// Should raise to 3x BB = 60, not just call
|
// Should raise to 3x BB = 60, not just call
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -823,7 +823,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
// Facing one voluntary raise to 60 (3x BB)
|
// Facing one voluntary raise to 60 (3x BB)
|
||||||
let mut game_info = make_game_info(60, 80, 1000, 0, Street::Preflop, vec![]);
|
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);
|
assert_eq!(bot.act(&game_info), Action::Call);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -840,7 +840,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
// Facing a 3-bet to 180 (2 voluntary raises)
|
// Facing a 3-bet to 180 (2 voluntary raises)
|
||||||
let mut game_info = make_game_info(180, 240, 1000, 0, Street::Preflop, vec![]);
|
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);
|
assert_eq!(bot.act(&game_info), Action::Fold);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,12 @@ impl HoldemPlayer for RaiseBot {
|
||||||
.fixed_bet_size(game_info.street)
|
.fixed_bet_size(game_info.street)
|
||||||
.unwrap_or(small_bet);
|
.unwrap_or(small_bet);
|
||||||
|
|
||||||
// Check if raises are still allowed (typically max 3 raises per round)
|
// Check if raises are still allowed (typically max 3 voluntary raises per round).
|
||||||
// Use the authoritative raise counter from GameInfo.
|
// GameInfo.num_raises uses Meerkat semantics where BB counts as a raise preflop,
|
||||||
let raises_this_round = game_info.num_raises;
|
// 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
|
// Can't raise anymore, just call
|
||||||
return if call_amount == 0 {
|
return if call_amount == 0 {
|
||||||
Action::Check
|
Action::Check
|
||||||
|
|
@ -431,7 +432,7 @@ mod tests {
|
||||||
let mut bot = RaiseBot::new(PlayerId(3), "RaiseBot".to_string());
|
let mut bot = RaiseBot::new(PlayerId(3), "RaiseBot".to_string());
|
||||||
// Create game info with 3 raises already made on the flop (Fixed-Limit max)
|
// 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);
|
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);
|
let action = bot.act(&game_info);
|
||||||
// Can't raise anymore, should call
|
// Can't raise anymore, should call
|
||||||
assert_eq!(action, Action::Call);
|
assert_eq!(action, Action::Call);
|
||||||
|
|
|
||||||
|
|
@ -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
|
let can_raise = game_info
|
||||||
.bet_limit
|
.bet_limit
|
||||||
.can_raise(game_info.street, game_info.num_raises);
|
.can_raise(game_info.street, voluntary_raises);
|
||||||
|
|
||||||
if can_raise && stack > call_amount {
|
if can_raise && stack > call_amount {
|
||||||
// Player has chips remaining after calling
|
// Player has chips remaining after calling
|
||||||
|
|
|
||||||
|
|
@ -75,13 +75,16 @@ pub struct GameInfo {
|
||||||
///
|
///
|
||||||
/// **Meerkat convention:** On the preflop street (index 0), the small blind
|
/// **Meerkat convention:** On the preflop street (index 0), the small blind
|
||||||
/// and big blind postings are recorded as [`Action::Raise`] entries at the
|
/// and big blind postings are recorded as [`Action::Raise`] entries at the
|
||||||
/// beginning of the list. These are *not* voluntary player raises. Use
|
/// beginning of the list. Use [`num_raises`](Self::num_raises) for the
|
||||||
/// [`num_raises`](Self::num_raises) for the authoritative count of voluntary
|
/// authoritative raise count on the current street (Meerkat semantics where
|
||||||
/// raises on the current street, which excludes blind postings.
|
/// the BB counts as the first raise preflop).
|
||||||
pub actions_this_hand: Vec<Vec<(PlayerId, Action)>>,
|
pub actions_this_hand: Vec<Vec<(PlayerId, Action)>>,
|
||||||
|
|
||||||
// --- Action counters (authoritative, maintained by the engine) ---
|
// --- 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.
|
/// Reset to 0 when advancing to a new street.
|
||||||
pub num_raises: usize,
|
pub num_raises: usize,
|
||||||
|
|
||||||
|
|
@ -200,10 +203,24 @@ impl GameInfo {
|
||||||
self.pot
|
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 {
|
pub fn get_num_raises(&self) -> usize {
|
||||||
self.num_raises
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -194,11 +194,13 @@ pub struct HandRunner {
|
||||||
/// Index 0 = preflop, 1 = flop, 2 = turn, 3 = river.
|
/// Index 0 = preflop, 1 = flop, 2 = turn, 3 = river.
|
||||||
///
|
///
|
||||||
/// **⚠ Meerkat convention:** On preflop, the SB and BB blind postings are
|
/// **⚠ Meerkat convention:** On preflop, the SB and BB blind postings are
|
||||||
/// recorded as `Action::Raise` entries (the first two entries). Code that
|
/// recorded as `Action::Raise` entries (the first two entries). The
|
||||||
/// counts voluntary raises from this field must subtract 2 on preflop.
|
/// `GameInfo.num_raises` field uses Meerkat semantics where the BB counts
|
||||||
/// Use `blind_postings_snapshot` for accurate blind data in hand history
|
/// 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
|
/// 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<Vec<(PlayerId, Action)>>,
|
actions_this_hand: Vec<Vec<(PlayerId, Action)>>,
|
||||||
/// Optional RNG seed for reproducible shuffles.
|
/// Optional RNG seed for reproducible shuffles.
|
||||||
seed: Option<u64>,
|
seed: Option<u64>,
|
||||||
|
|
@ -1405,8 +1407,11 @@ impl HandRunner {
|
||||||
.and_then(|id| self.find_seat_index_by_player_id(id))
|
.and_then(|id| self.find_seat_index_by_player_id(id))
|
||||||
.map(|idx| self.seats[idx].index);
|
.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.
|
// 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 street_idx = self.street_to_action_index();
|
||||||
let num_raises = self
|
let num_raises = self
|
||||||
.actions_this_hand
|
.actions_this_hand
|
||||||
|
|
@ -1416,10 +1421,10 @@ impl HandRunner {
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, a)| matches!(a, Action::Raise { .. }))
|
.filter(|(_, a)| matches!(a, Action::Raise { .. }))
|
||||||
.count();
|
.count();
|
||||||
// On preflop, the SB and BB are recorded as raises (Meerkat convention).
|
// On preflop, subtract 1 for the SB posting only.
|
||||||
// Subtract them to get the count of voluntary player raises.
|
// The BB remains counted as the first raise (Meerkat semantics).
|
||||||
if self.current_street() == Street::Preflop {
|
if self.current_street() == Street::Preflop {
|
||||||
total_raises.saturating_sub(2)
|
total_raises.saturating_sub(1)
|
||||||
} else {
|
} else {
|
||||||
total_raises
|
total_raises
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue