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)
25 KiB
Bot Framework Architecture — Supersonic Port
Overview
This document describes the module architecture for porting the Java Supersonic bot framework to the Rust rusty-marvin project. The design introduces a strategy framework with hand analysis, modular strategy composition, and advanced cash-game bots — while keeping proprietary bot logic in a separate private crate.
Goals
- Public/Private separation — Advanced bots live in a private crate excludable from public mirrors
- Framework in
holdem_core— Traits, helpers, analysis tools remain public and reusable - Idiomatic Rust — Traits instead of interfaces, enums instead of inheritance hierarchies,
Optioninstead of null - Minimal disruption — Existing bots and engine code continue to work unchanged
Current State
The existing codebase already has:
HoldemPlayertrait — the Meerkat-compatible bot interface withact(),action_event(), etc.GameInfo— read-only game state snapshot passed to botsbot_factory— string-based factory mapping"call_bot"→Box<dyn HoldemPlayer>- Six simple bots in
holdem_core/src/game/bots/—CallBot,FoldBot,RaiseBot,RandomBot,FlockBot,ChumpBot Evaluator+EvaluatorSingleton— Two-Plus-Two lookup table evaluatorCardwith zero-based rank/suit,HandValue,Streetenum
Proposed Workspace Structure
rusty-marvin/
├── Cargo.toml # workspace root — members listed here
├── holdem_core/ # PUBLIC crate — framework + simple bots
│ └── src/
│ ├── lib.rs
│ ├── card.rs, deck.rs, board.rs, hand.rs, hole_cards.rs
│ ├── errors.rs
│ ├── evaluator/ # unchanged
│ └── game/
│ ├── mod.rs
│ ├── action.rs, betting.rs, config.rs
│ ├── game_info.rs, player_info.rs
│ ├── hand_runner.rs, hand_history.rs
│ ├── player.rs # HoldemPlayer trait + Seat
│ ├── pot.rs, table.rs, tournament.rs
│ ├── simulation.rs, stats.rs, types.rs
│ ├── bot_factory.rs # EXTENDED: plugin-style registration
│ ├── bots/ # simple test bots — unchanged
│ │ ├── mod.rs, call_bot.rs, fold_bot.rs
│ │ ├── raise_bot.rs, random_bot.rs
│ │ ├── chump_bot.rs, flock_bot.rs
│ └── strategy/ # NEW: strategy framework
│ ├── mod.rs
│ ├── context.rs # StrategyContext — aggregated decision state
│ ├── modular.rs # ModularStrategy — composite pattern
│ ├── action_recorder.rs # ActionRecorder — per-street action log
│ ├── hand_analyzer.rs # HandAnalyzer — postflop hand analysis
│ ├── hand_helper.rs # HandHelper — preflop tables + utilities
│ ├── hand_range.rs # HandRange — 52x52 weighted opponent range
│ └── hand_potential.rs # HandPotential — hand strength estimation
├── holdem_bots/ # PRIVATE crate — advanced bots
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs
│ ├── factory.rs # registers bots with holdem_core factory
│ └── cash/ # cash game strategies
│ ├── mod.rs
│ ├── big_stack.rs # BigStackStrategy
│ ├── short_stack.rs # ShortStackStrategy
│ ├── postflop.rs # PostFlopStrategyV3 — shared postflop logic
│ ├── preflop/
│ │ ├── mod.rs
│ │ ├── big_stack.rs # BigStackPreFlopStrategy
│ │ └── short_stack.rs # ShortStackPreFlopStrategy
│ └── street/
│ ├── mod.rs
│ ├── flop.rs # BigStackFlopStrategy
│ ├── turn.rs # BigStackTurnStrategy
│ └── river.rs # BigStackRiverStrategy
├── holdem_sim/ # unchanged
│ └── src/main.rs
├── docs/
└── plans/
Crate Responsibilities
holdem_core — Public Framework Crate
Contains: Everything needed to build a poker bot.
| Module | Purpose | Visibility |
|---|---|---|
card, deck, board, hand, hole_cards |
Core card types | Public |
evaluator |
Hand evaluation via lookup tables | Public |
game::player |
HoldemPlayer trait — the bot interface |
Public |
game::game_info |
GameInfo — read-only game state |
Public |
game::bot_factory |
Bot instantiation with plugin registration | Public |
game::bots |
Simple test bots: CallBot, FoldBot, etc. | Public |
game::strategy |
NEW — Strategy framework traits and helpers | Public |
game::strategy::modular |
NEW — Composite strategy with voting | Public |
game::strategy::action_recorder |
NEW — Per-street action tracking | Public |
game::strategy::hand_analyzer |
NEW — Postflop hand analysis | Public |
game::strategy::hand_helper |
NEW — Preflop rankings + board texture | Public |
game::strategy::hand_range |
NEW — 52×52 weighted opponent range grid | Public |
game::strategy::hand_potential |
NEW — Hand strength / potential estimation | Public |
holdem_bots — Private Advanced Bots Crate
Contains: Proprietary bot implementations that implement HoldemPlayer.
| Module | Purpose | Visibility |
|---|---|---|
factory |
Registers bot types with core factory on init | Private |
cash::big_stack |
BigStackStrategy — stack > 25 BB | Private |
cash::short_stack |
ShortStackStrategy — stack ≤ 25 BB | Private |
cash::postflop |
Shared postflop logic using HandPotential | Private |
cash::preflop::big_stack |
BigStack preflop strategy | Private |
cash::preflop::short_stack |
ShortStack preflop strategy | Private |
cash::street::flop |
BigStack flop strategy | Private |
cash::street::turn |
BigStack turn strategy | Private |
cash::street::river |
BigStack river strategy | Private |
Key Traits and Relationships
classDiagram
class HoldemPlayer {
<<trait>>
+id() PlayerId
+name() str
+init(game_info)
+game_start_event(game_info)
+hole_cards_dealt(hole_cards, game_info)
+stage_event(game_info)
+act(game_info) Action
+action_event(player_id, action, game_info)
+showdown_event(player_id, hole_cards, hand_value, game_info)
+game_over_event(game_info)
}
class Strategy {
<<trait>>
+is_applicable(context) bool
+act(context) Action
}
class ModularStrategy {
-strategies: Vec~Box dyn Strategy~
-mode: VotingMode
+act(context) Action
}
class StrategyContext {
+game_info: GameInfo
+hole_cards: HoleCards
+action_recorder: ActionRecorder
+aggressor_id: Option~PlayerId~
+num_raises: usize
+num_callers: usize
}
class ActionRecorder {
-actions: Vec~Vec~ActionRecord~~
+record(street, player_id, action)
+actions_on_street(street) Vec
+last_raise_on_street(street) Option
+reset()
}
class HandAnalyzer {
-evaluator: Arc~Evaluator~
+is_over_pair(hole_cards, board) bool
+is_paired(board) bool
+is_trips_or_higher(hand_value) bool
+num_outs_to_monster(hole_cards, board) u8
+is_oesd(hole_cards, board) bool
+is_flush_draw(hole_cards, board) bool
+suit_count(hole_cards, board) u8
}
class HandHelper {
+preflop_rank_full_ring(hole_cards) f64
+preflop_rank_hu(hole_cards) f64
+kill_phil_group(hole_cards) u8
+board_texture(board) BoardTexture
+count_outs(hole_cards, board) u8
}
class HandRange {
-weights: [[f64; 13]; 13]
+weight(rank1, rank2) f64
+set_weight(rank1, rank2, weight)
+normalize()
}
class HandPotential {
+hand_strength(hole_cards, board, range) f64
+positive_potential(hole_cards, board, range) f64
+negative_potential(hole_cards, board, range) f64
}
class VotingMode {
<<enum>>
FirstApplicable
Democratic
Random
}
Strategy <|.. ModularStrategy : implements
ModularStrategy o-- Strategy : contains N
ModularStrategy --> VotingMode : uses
StrategyContext --> ActionRecorder : contains
Strategy --> StrategyContext : reads
HandAnalyzer --> Evaluator : uses
HandPotential --> HandRange : uses
HoldemPlayer <|.. ModularStrategy : implements via adapter
HoldemPlayer <|.. CallBot : implements
HoldemPlayer <|.. BigStackBot : implements
Strategy Trait
The core abstraction for composable bot behavior:
/// A single strategy component that can decide whether it applies
/// to the current game situation and produce an action.
pub trait Strategy: Send + Sync {
/// Whether this strategy should participate in the current decision.
fn is_applicable(&self, context: &StrategyContext) -> bool;
/// Produce an action. Called only when `is_applicable` returns true.
fn act(&self, context: &StrategyContext) -> Action;
}
VotingMode Enum
Replaces the Java inheritance hierarchy for ModularStrategy subclasses:
/// How a ModularStrategy selects among multiple applicable sub-strategies.
pub enum VotingMode {
/// Use the first strategy where `is_applicable` returns true.
FirstApplicable,
/// Each applicable strategy votes; majority wins.
Democratic,
/// Pick a random applicable strategy.
Random,
}
StrategyContext
Aggregated decision state passed to strategies — replaces the Java pattern of passing GameInfo, hole cards, position, etc. as separate parameters:
/// Read-only context provided to strategies at decision time.
pub struct StrategyContext<'a> {
pub game_info: &'a GameInfo,
pub hole_cards: &'a HoleCards,
pub action_recorder: &'a ActionRecorder,
pub aggressor_id: Option<PlayerId>,
pub num_raises: usize,
pub num_callers: usize,
}
ModularStrategy
The composite pattern. It implements both Strategy and HoldemPlayer:
/// Composite strategy that delegates to sub-strategies via a voting mechanism.
pub struct ModularStrategy {
strategies: Vec<Box<dyn Strategy>>,
voting_mode: VotingMode,
// Internal state managed across the hand lifecycle
hole_cards: Option<HoleCards>,
action_recorder: ActionRecorder,
}
Java → Rust Idiom Mapping
| Java Construct | Rust Equivalent | Notes |
|---|---|---|
interface Strategy |
trait Strategy |
Send + Sync bounds for thread safety |
ModularStrategy extends Strategy |
ModularStrategy struct implementing Strategy trait |
Composition over inheritance |
BigStackStrategy extends ModularStrategy |
BigStackBot struct implementing HoldemPlayer directly, containing a ModularStrategy |
No inheritance chain — flat delegation |
HandAnalyzer with singleton evaluator |
HandAnalyzer struct holding Arc<Evaluator> from EvaluatorSingleton |
OnceLock singleton already exists |
HandHelper static methods |
HandHelper struct with associated functions / methods |
Preflop tables as const arrays |
int card = 0..51 |
Card struct with rank/suit |
Already exists in codebase |
null |
Option<T> |
Idiomatic throughout |
Vector<Action> |
Vec<Action> |
Standard |
HandRange extends WeightTable |
HandRange struct with [[f64; 13]; 13] grid |
Flat array, no inheritance |
ActionRecorder per-hand state |
ActionRecorder struct, reset on game_start_event |
Owned by ModularStrategy |
KillPhilGrouping enum-like class |
Rust enum with #[repr(u8)] |
Type-safe |
Bot Factory: Plugin Registration Pattern
The current create_bot() uses a hardcoded match statement. To support the private crate registering its bots without modifying holdem_core, we introduce a registry pattern:
use std::sync::RwLock;
use std::collections::HashMap;
type BotConstructor = fn(&BotConfig, PlayerId) -> Result<Box<dyn HoldemPlayer>, String>;
static REGISTRY: RwLock<HashMap<String, BotConstructor>> = RwLock::new(HashMap::new());
/// Register a bot type. Called by holdem_core for built-in bots
/// and by holdem_bots for advanced bots.
pub fn register_bot_type(bot_type: &str, constructor: BotConstructor) {
REGISTRY.write().unwrap().insert(bot_type.to_string(), constructor);
}
/// Create a bot by type name. Falls back to the registry.
pub fn create_bot(config: &BotConfig, id: PlayerId) -> Result<Box<dyn HoldemPlayer>, String> {
let registry = REGISTRY.read().unwrap();
if let Some(ctor) = registry.get(&config.bot_type) {
ctor(config, id)
} else {
Err(format!("unknown bot type: '{}'", config.bot_type))
}
}
Initialization flow:
holdem_coreexports aregister_builtin_bots()function that registers the six built-in botsholdem_botsexports aregister_advanced_bots()function that registers BigStack, ShortStack, etc.holdem_sim(or any binary) calls both at startup
This avoids holdem_core depending on holdem_bots — the dependency flows the correct direction.
Data Flow: Bot Decision Pipeline
flowchart TD
A[Engine calls HoldemPlayer::act] --> B[ModularStrategy::act]
B --> C[Build StrategyContext]
C --> D{For each sub-strategy}
D --> E[is_applicable?]
E -->|Yes| F[Collect action vote]
E -->|No| D
F --> D
D -->|All checked| G{VotingMode}
G -->|FirstApplicable| H[Return first applicable action]
G -->|Democratic| I[Majority vote]
G -->|Random| J[Pick random applicable]
I --> K[Return winning action]
J --> K
H --> K
K --> L[Return Action to engine]
Data Flow: Hand Analysis Pipeline
flowchart TD
A[HoleCards + Board] --> B[HandAnalyzer]
A --> C[HandHelper]
B --> D[EvaluatorSingleton]
D --> E[HandValue + HandRank]
B --> F{Analysis Results}
F --> F1[is_over_pair]
F --> F2[is_flush_draw]
F --> F3[is_oesd]
F --> F4[num_outs_to_monster]
F --> F5[suit_count]
C --> G{Preflop + Texture}
G --> G1[preflop_rank_full_ring]
G --> G2[kill_phil_group]
G --> G3[board_texture]
G --> G4[count_outs]
F --> H[HandPotential]
G --> H
H --> I[hand_strength: f64]
H --> J[positive_potential: f64]
H --> K[negative_potential: f64]
Public vs Private Separation Strategy
Approach: Separate Crate + .gitignore for Public Mirror
The private holdem_bots crate is excluded when publishing to public mirrors:
1. Workspace Cargo.toml — conditional member:
[workspace]
resolver = "2"
members = [
"holdem_core",
"holdem_sim",
"holdem_bots", # present in private repo
]
2. .gitignore for public mirror export:
When creating a public mirror, use a .gitattributes export-ignore or a separate .gitignore:
# Public mirror excludes proprietary bots
holdem_bots/
3. Conditional compilation in holdem_sim:
// holdem_sim/src/main.rs
use holdem_core::game::bot_factory;
fn main() {
// Register built-in bots (always available)
bot_factory::register_builtin_bots();
// Register advanced bots (only when holdem_bots is in workspace)
#[cfg(feature = "advanced-bots")]
holdem_bots::register_advanced_bots();
// ... run simulation
}
4. holdem_sim/Cargo.toml optional dependency:
[dependencies]
holdem_core = { path = "../holdem_core" }
holdem_bots = { path = "../holdem_bots", optional = true }
[features]
default = []
advanced-bots = ["holdem_bots"]
What Stays Public
- All traits:
HoldemPlayer,Strategy - All analysis tools:
HandAnalyzer,HandHelper,HandPotential,HandRange ModularStrategy,ActionRecorder,StrategyContext- Simple test bots:
CallBot,FoldBot,RaiseBot,RandomBot,FlockBot,ChumpBot - The entire
evaluatormodule
What Stays Private
BigStackStrategy,ShortStackStrategyimplementations- Street-specific strategies: flop, turn, river
- Preflop strategy tables for big stack / short stack
PostFlopStrategyV3shared logic- Any future proprietary bot implementations
Files to Clean Up / Reorganize
No Deletions Needed
All existing files remain. The change is purely additive:
| File | Action | Reason |
|---|---|---|
holdem_core/src/game/bot_factory.rs |
Refactor | Replace hardcoded match with plugin registry |
holdem_core/src/game/mod.rs |
Add pub mod strategy; |
New strategy module |
Cargo.toml |
Add holdem_bots member |
New private crate |
holdem_sim/Cargo.toml |
Add optional holdem_bots dep |
Feature-gated advanced bots |
New Files to Create
| File | Purpose |
|---|---|
holdem_core/src/game/strategy/mod.rs |
Module root, re-exports |
holdem_core/src/game/strategy/context.rs |
StrategyContext struct |
holdem_core/src/game/strategy/modular.rs |
ModularStrategy + VotingMode |
holdem_core/src/game/strategy/action_recorder.rs |
ActionRecorder |
holdem_core/src/game/strategy/hand_analyzer.rs |
HandAnalyzer |
holdem_core/src/game/strategy/hand_helper.rs |
HandHelper + preflop tables |
holdem_core/src/game/strategy/hand_range.rs |
HandRange 52×52 grid |
holdem_core/src/game/strategy/hand_potential.rs |
HandPotential estimation |
holdem_bots/Cargo.toml |
Private crate manifest |
holdem_bots/src/lib.rs |
Crate root + register_advanced_bots() |
holdem_bots/src/factory.rs |
Bot registration helpers |
holdem_bots/src/cash/mod.rs |
Cash game module root |
holdem_bots/src/cash/big_stack.rs |
BigStack bot |
holdem_bots/src/cash/short_stack.rs |
ShortStack bot |
holdem_bots/src/cash/postflop.rs |
Shared postflop logic |
holdem_bots/src/cash/preflop/mod.rs |
Preflop submodule |
holdem_bots/src/cash/preflop/big_stack.rs |
BigStack preflop |
holdem_bots/src/cash/preflop/short_stack.rs |
ShortStack preflop |
holdem_bots/src/cash/street/mod.rs |
Street submodule |
holdem_bots/src/cash/street/flop.rs |
Flop strategy |
holdem_bots/src/cash/street/turn.rs |
Turn strategy |
holdem_bots/src/cash/street/river.rs |
River strategy |
Implementation Order
The implementation should proceed in dependency order — each step produces usable, testable artifacts:
Phase 1: Strategy Framework Core
- Create
holdem_core/src/game/strategy/mod.rs— module root with re-exports - Create
action_recorder.rs—ActionRecorderstruct; pure data, no dependencies beyondActionandStreet - Create
context.rs—StrategyContextstruct; referencesGameInfo,HoleCards,ActionRecorder - Create
modular.rs—Strategytrait,VotingModeenum,ModularStrategystruct implementing bothStrategyandHoldemPlayer - Refactor
bot_factory.rs— replace hardcoded match with plugin registry; addregister_builtin_bots()function - Update
game/mod.rs— addpub mod strategy; - Write unit tests —
ActionRecorderrecording,ModularStrategyvoting modes, factory registration
Phase 2: Hand Analysis Tools
- Create
hand_analyzer.rs—HandAnalyzerstruct usingArc<Evaluator>; methods likeis_over_pair(),is_flush_draw(),num_outs_to_monster() - Create
hand_helper.rs—HandHelperwithconstpreflop ranking tables,kill_phil_group(),board_texture(),count_outs() - Create
hand_range.rs—HandRangewith[[f64; 13]; 13]weighted grid,normalize(), iteration - Create
hand_potential.rs—HandPotentialwithhand_strength(),positive_potential(),negative_potential()using Monte Carlo or exhaustive enumeration - Write property tests — HandAnalyzer against known hand values, HandHelper against known preflop rankings
Phase 3: Private Bots Crate
- Create
holdem_bots/crate skeleton —Cargo.toml,lib.rs,cash/mod.rs - Create
factory.rs—register_advanced_bots()function - Create
cash/preflop/strategies — preflop tables and decision logic for big stack and short stack - Create
cash/postflop.rs— shared postflop logic usingHandPotential - Create
cash/street/strategies — flop, turn, river specific logic - Create
cash/big_stack.rs—BigStackBotimplementingHoldemPlayer, composing strategies viaModularStrategy - Create
cash/short_stack.rs—ShortStackBotimplementingHoldemPlayer, composing strategies viaModularStrategy - Update workspace
Cargo.toml— addholdem_botsmember - Update
holdem_sim— add optionalholdem_botsdependency and feature flag
Phase 4: Integration and Validation
- Integration test — run a cash game simulation with BigStackBot vs simple bots
- Integration test — run a tournament with mixed bot types
- Validate public mirror — confirm
holdem_corecompiles and all tests pass withoutholdem_bots - Update documentation — add
holdem_bots/README.md, update mainREADME.md
Dependency Graph
flowchart BT
subgraph holdem_core
card[card, deck, board, hand, hole_cards]
evaluator[evaluator]
game_engine[game engine: hand_runner, table, pot, betting]
game_info[game_info, player_info, types, action]
player[player: HoldemPlayer trait]
simple_bots[bots: CallBot, FoldBot, etc.]
strategy_framework[strategy: Strategy trait, ModularStrategy]
analysis[strategy: HandAnalyzer, HandHelper, HandRange, HandPotential]
action_recorder[strategy: ActionRecorder, StrategyContext]
factory[bot_factory: registry pattern]
end
subgraph holdem_bots
bots_factory[factory: register_advanced_bots]
big_stack[cash::big_stack]
short_stack[cash::short_stack]
postflop[cash::postflop]
preflop[cash::preflop]
street[cash::street]
end
subgraph holdem_sim
sim_main[main.rs]
end
card --> evaluator
card --> game_engine
evaluator --> analysis
game_info --> player
player --> simple_bots
player --> strategy_framework
game_info --> strategy_framework
action_recorder --> strategy_framework
analysis --> strategy_framework
strategy_framework --> factory
simple_bots --> factory
player --> big_stack
strategy_framework --> big_stack
analysis --> big_stack
big_stack --> preflop
big_stack --> postflop
short_stack --> preflop
short_stack --> postflop
postflop --> street
big_stack --> bots_factory
short_stack --> bots_factory
factory --> sim_main
bots_factory --> sim_main
Design Decisions
Why Strategy trait separate from HoldemPlayer?
HoldemPlayer is a lifecycle interface — it receives events across the entire hand. Strategy is a decision interface — it only produces actions. Separating them allows:
- Multiple strategies to be composed inside a single
HoldemPlayerimplementation - Strategies to be tested in isolation with mock
StrategyContext - The
ModularStrategyadapter bridgesStrategy→HoldemPlayer
Why plugin registry over hardcoded match?
The current create_bot() match statement requires modifying holdem_core every time a new bot type is added. The registry pattern allows holdem_bots to register itself without holdem_core knowing about it. This is critical for the public/private separation.
Why [[f64; 13]; 13] for HandRange instead of [[f64; 52]; 52]?
The Java WeightTable uses a 52×52 grid, but most entries are redundant because suit combinations collapse to 13×13 when using suited/offsuit notation. The 13×13 representation is more memory-efficient and aligns with standard poker range notation (e.g., "AA", "AKs", "AKo").
Why OnceLock over Lazy for the evaluator?
OnceLock is already used in the codebase and is in std::sync. No need for the once_cell dependency for new singletons.
Why keep simple bots in holdem_core?
The six existing bots (CallBot, FoldBot, RaiseBot, RandomBot, FlockBot, ChumpBot) serve as:
- Test fixtures — used in engine tests and property tests
- Examples — demonstrate how to implement
HoldemPlayer - Baseline opponents — needed even in the public crate for simulation
They contain no proprietary logic and are intentionally simple.