Framework (holdem_core::game::strategy): - HandAnalyzer: 15 hand analysis methods (overpair, flush draw, OESD, outs, etc.) - HandHelper: 14 board texture and hand strength methods (Kill Phil grouping, etc.) - ActionRecorder: per-street action tracking with player filtering - StrategyContext: read-only game state snapshot with position/pot odds helpers - Strategy trait + ModularStrategy: composite pattern with 3 voting modes (FirstApplicable, Democratic, Random) Cash Game Bots (holdem_bots crate — private, exclude from public mirrors): - PostFlopStrategy: shared postflop logic using hand strength approximation - BigStackBot: NL cash strategy for stack > 25 BB (preflop + postflop) - ShortStackBot: NL cash strategy for stack ≤ 25 BB (preflop + postflop) - Full preflop decision trees ported from Java Supersonic framework Repository restructuring: - Created holdem_bots crate with feature-gated dependency from holdem_sim - Added game::strategy module to holdem_core - Cleaned up 4 unused evaluator stubs - Moved plans/ to docs/, added proptest-regressions/ to .gitignore - Added ARCHITECTURE_BOT_FRAMEWORK.md documentation Tests: 58 new strategy tests + 17 bot tests (all passing)
9 KiB
Meerkat API Alignment — Bug Fixes and Improvements Plan
Executive Summary
After a thorough comparison of the Rust holdem_core implementation against the Java Meerkat API reference (Meerkat-API), I identified several bugs, behavioral discrepancies, and missing features. The core architecture is solid and well-structured, but there are concrete issues that need fixing for correct poker gameplay.
Bug Fixes
BUG-1: FixedLimit raise validation uses small_bet for all streets
File: action.rs
Problem: In validate_action(), the FixedLimit branch always compares raise_increment against small_bet, even on turn/river streets where big_bet should be used. The validator doesn't know the current street.
// Current (WRONG for turn/river):
BetLimit::FixedLimit { small_bet, big_bet: _ } => {
if raise_increment != *small_bet { ... }
}
Fix: Pass the current Street to validate_action() so it can use the correct bet size. On preflop/flop use small_bet, on turn/river use big_bet.
Impact: Fixed-limit games on turn/river incorrectly reject valid raises or accept invalid ones.
BUG-2: Pot-limit max_raise_amount calculation is wrong
File: betting.rs
Problem: The pot-limit max raise formula returns pot + call_amount, but the standard pot-limit formula should be:
max_raise_total = pot + call_amount + current_bet
Wait, let me re-examine. The trait says it returns the max raise amount, but the semantics are unclear. In standard pot-limit:
- A player can raise up to:
pot_size + amount_to_call - Where
pot_sizeis the pot BEFORE the player's call - So the max total they can put in is:
current_bet + call_amount + pot_size
The current code returns pot + call_amount which is ambiguous. The caller needs to know whether this is the raise increment or the total. This needs clarification and the actual pot-limit cap needs to be enforced in HandRunner.
Fix: Clarify the semantics of max_raise_amount — it should return the maximum total wager for the player (not just the increment). Then enforce it in validate_action or apply_action.
BUG-3: FixedLimit max_raise_amount returns None
File: betting.rs
Problem: For FixedLimit, max_raise_amount() returns None, meaning "no cap." But Fixed-Limit definitely has a cap — the fixed bet size. The comment even acknowledges this confusion.
Fix: Return Some(fixed_bet_size) for the current street. This requires passing street to the method, or restructuring the trait.
BUG-4: All-in that doesn't exceed current bet doesn't reopen action
File: hand_runner.rs
Problem: When a player goes all-in for less than the current bet, the code calls br.apply_call() and does NOT reset has_acted_this_round for other players. However, consider this scenario:
- Player A bets 100
- Player B raises to 200
- Player C goes all-in for 150 (doesn't exceed 200)
- Player D has already acted and called the 200
In this case, Player D doesn't need to act again, which is correct. BUT if Player C's all-in somehow changes the action flow, there could be issues. Actually, this behavior is correct for standard poker rules — an all-in that doesn't exceed the current bet does NOT reopen the betting. So this is actually fine.
Revised: This is NOT a bug. The current behavior is correct.
BUG-5: run_to_completion may double-credit winnings
File: hand_runner.rs
Problem: When step() resolves the hand via resolve_last_player_wins(), it calls finalize_result() which credits winnings to stacks and sets state to Complete. Then run_to_completion() enters the Complete branch and calls distribute() again, computing chip movements from a second distribution. However, distribute() only reads pot state (doesn't mutate), and credit_winnings() is NOT called again in the Complete branch. So the stacks are only credited once.
Wait, let me re-read more carefully...
In resolve_last_player_wins():
- Calls
finalize_result()which credits winnings, sets state to Complete - Returns
Ok(HandResult)but the result is discarded bystep()which only returnsHandState
Then in run_to_completion():
- The
Completebranch reconstructs aHandResultbut does NOT callcredit_winnings()again - It only computes
chip_movementsfor the result struct
So actually the stacks are only credited once (in finalize_result), but the HandResult constructed in the Complete branch of run_to_completion calls distribute() again, which returns a fresh result from the pot manager. This is fine because distribute is read-only.
Revised: This is NOT a double-credit bug. The code is correct but slightly confusing. The Complete branch in run_to_completion should ideally reuse the result from finalize_result rather than reconstructing it, but functionally it's correct.
BUG-6: Missing game_state_changed callback
File: player.rs
Problem: The Meerkat API has a gameStateChanged() callback that is called after an action event has been fully processed. The Rust implementation does not have this callback. In Meerkat, the sequence is:
actionEvent(pos, act)— the action is observedgameStateChanged()— the game info state has been updated
This is important because in Meerkat, actionEvent is called BEFORE the game state is updated, and gameStateChanged is called AFTER. In the Rust implementation, action_event is called with an already-updated GameInfo, which is actually better. But the gameStateChanged callback is missing.
Fix: This is a minor API gap. The current Rust design is arguably better (providing updated state in action_event), so this is low priority. Could add a game_state_changed method with a default no-op implementation.
BUG-7: validate_action doesn't validate FixedLimit raise count cap
File: action.rs
Problem: In Meerkat's Fixed-Limit, there's a cap of 3 raises per round (or 4 total bets). The can_raise method in BettingRules checks this, but validate_action doesn't check it. The validation only checks the raise amount, not the number of raises.
Fix: Pass raises_this_round to validate_action and check bet_limit.can_raise(street, raises_this_round) for FixedLimit.
Meerkat API Alignment
ALIGN-1: Add get_amount_to_call convenience method to GameInfo
File: game_info.rs
Problem: Meerkat's GameInfo.getAmountToCall(seat) is used extensively by bots. The Rust GameInfo has current_bet and players have wagered_this_round, but there's no convenience method to compute amount_to_call = current_bet - wagered_this_round.
Fix: Add:
pub fn get_amount_to_call(&self, player_id: PlayerId) -> ChipCount
pub fn is_preflop(&self) -> bool
ALIGN-2: Add check_or_fold Action helper
Problem: Meerkat has Action.checkOrFoldAction(toCall) which returns CHECK if toCall is 0, FOLD otherwise. The Rust implementation doesn't have this convenience.
Fix: Add a method to Action:
pub fn check_or_fold(amount_to_call: ChipCount) -> Action
ALIGN-3: Add bet_action helper
Problem: Meerkat has Action.betAction(gi) which creates a raise/bet of the minimum amount. The Rust implementation requires manually computing the raise total.
Fix: Add a constructor or helper that computes the correct raise total from game state.
Test Coverage Gaps
TEST-1: Fixed-limit raise validation on turn/river
No tests verify that Fixed-Limit raises on turn/river use big_bet instead of small_bet.
TEST-2: Heads-up blind positions
No tests verify that in heads-up, the dealer is the small blind and acts first preflop.
TEST-3: Side pot edge cases
No tests for:
- Three players all-in with different amounts
- Folded player with money in the pot creating side pots
- Split pots with odd chips
TEST-4: All-in that reopens betting
No tests verify that a full all-in raise correctly resets has_acted_this_round for other players.
Priority Order
- BUG-1 — FixedLimit validation uses wrong bet size (gameplay-breaking for fixed-limit)
- BUG-7 — FixedLimit raise count not validated (gameplay-breaking for fixed-limit)
- BUG-2 — Pot-limit max raise calculation needs enforcement (gameplay-breaking for pot-limit)
- BUG-3 — FixedLimit max_raise_amount returns None (related to BUG-2)
- ALIGN-1 — Missing GameInfo convenience methods (bot compatibility)
- ALIGN-2 — Missing check_or_fold helper (bot compatibility)
- ALIGN-3 — Missing bet_action helper (bot compatibility)
- TEST-1 through TEST-4 — Comprehensive test coverage
- BUG-6 — Missing game_state_changed callback (low priority)