# 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 1. **Public/Private separation** — Advanced bots live in a private crate excludable from public mirrors 2. **Framework in `holdem_core`** — Traits, helpers, analysis tools remain public and reusable 3. **Idiomatic Rust** — Traits instead of interfaces, enums instead of inheritance hierarchies, `Option` instead of null 4. **Minimal disruption** — Existing bots and engine code continue to work unchanged --- ## Current State The existing codebase already has: - [`HoldemPlayer`](holdem_core/src/game/player.rs:78) trait — the Meerkat-compatible bot interface with `act()`, `action_event()`, etc. - [`GameInfo`](holdem_core/src/game/game_info.rs:21) — read-only game state snapshot passed to bots - [`bot_factory`](holdem_core/src/game/bot_factory.rs:52) — string-based factory mapping `"call_bot"` → `Box` - Six simple bots in [`holdem_core/src/game/bots/`](holdem_core/src/game/bots/mod.rs) — `CallBot`, `FoldBot`, `RaiseBot`, `RandomBot`, `FlockBot`, `ChumpBot` - [`Evaluator`](holdem_core/src/evaluator/evaluator.rs) + [`EvaluatorSingleton`](holdem_core/src/evaluator/singleton.rs:9) — Two-Plus-Two lookup table evaluator - [`Card`](holdem_core/src/card.rs) with zero-based rank/suit, [`HandValue`](holdem_core/src/evaluator/evaluator.rs:108), [`Street`](holdem_core/src/board.rs) enum --- ## 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`](holdem_core/src/game/player.rs:78) trait — the bot interface | Public | | `game::game_info` | [`GameInfo`](holdem_core/src/game/game_info.rs:21) — 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 ```mermaid classDiagram class HoldemPlayer { <> +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 { <> +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 { <> 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: ```rust /// 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: ```rust /// 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: ```rust /// 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, pub num_raises: usize, pub num_callers: usize, } ``` ### ModularStrategy The composite pattern. It implements both `Strategy` and `HoldemPlayer`: ```rust /// Composite strategy that delegates to sub-strategies via a voting mechanism. pub struct ModularStrategy { strategies: Vec>, voting_mode: VotingMode, // Internal state managed across the hand lifecycle hole_cards: Option, 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` from [`EvaluatorSingleton`](holdem_core/src/evaluator/singleton.rs:9) | `OnceLock` singleton already exists | | `HandHelper` static methods | `HandHelper` struct with associated functions / methods | Preflop tables as `const` arrays | | `int card = 0..51` | [`Card`](holdem_core/src/card.rs) struct with rank/suit | Already exists in codebase | | `null` | `Option` | Idiomatic throughout | | `Vector` | `Vec` | 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()`](holdem_core/src/game/bot_factory.rs:52) uses a hardcoded `match` statement. To support the private crate registering its bots without modifying `holdem_core`, we introduce a **registry pattern**: ```rust use std::sync::RwLock; use std::collections::HashMap; type BotConstructor = fn(&BotConfig, PlayerId) -> Result, String>; static REGISTRY: RwLock> = 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, 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:** 1. `holdem_core` exports a `register_builtin_bots()` function that registers the six built-in bots 2. `holdem_bots` exports a `register_advanced_bots()` function that registers BigStack, ShortStack, etc. 3. `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 ```mermaid 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 ```mermaid 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:** ```toml [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`: ```gitignore # Public mirror excludes proprietary bots holdem_bots/ ``` **3. Conditional compilation in `holdem_sim`:** ```rust // 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:** ```toml [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 `evaluator` module ### What Stays Private - `BigStackStrategy`, `ShortStackStrategy` implementations - Street-specific strategies: flop, turn, river - Preflop strategy tables for big stack / short stack - `PostFlopStrategyV3` shared 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`](holdem_core/src/game/bot_factory.rs) | **Refactor** | Replace hardcoded match with plugin registry | | [`holdem_core/src/game/mod.rs`](holdem_core/src/game/mod.rs) | **Add** `pub mod strategy;` | New strategy module | | [`Cargo.toml`](Cargo.toml) | **Add** `holdem_bots` member | New private crate | | [`holdem_sim/Cargo.toml`](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 1. **Create `holdem_core/src/game/strategy/mod.rs`** — module root with re-exports 2. **Create `action_recorder.rs`** — `ActionRecorder` struct; pure data, no dependencies beyond `Action` and `Street` 3. **Create `context.rs`** — `StrategyContext` struct; references `GameInfo`, `HoleCards`, `ActionRecorder` 4. **Create `modular.rs`** — `Strategy` trait, `VotingMode` enum, `ModularStrategy` struct implementing both `Strategy` and `HoldemPlayer` 5. **Refactor `bot_factory.rs`** — replace hardcoded match with plugin registry; add `register_builtin_bots()` function 6. **Update `game/mod.rs`** — add `pub mod strategy;` 7. **Write unit tests** — `ActionRecorder` recording, `ModularStrategy` voting modes, factory registration ### Phase 2: Hand Analysis Tools 8. **Create `hand_analyzer.rs`** — `HandAnalyzer` struct using `Arc`; methods like `is_over_pair()`, `is_flush_draw()`, `num_outs_to_monster()` 9. **Create `hand_helper.rs`** — `HandHelper` with `const` preflop ranking tables, `kill_phil_group()`, `board_texture()`, `count_outs()` 10. **Create `hand_range.rs`** — `HandRange` with `[[f64; 13]; 13]` weighted grid, `normalize()`, iteration 11. **Create `hand_potential.rs`** — `HandPotential` with `hand_strength()`, `positive_potential()`, `negative_potential()` using Monte Carlo or exhaustive enumeration 12. **Write property tests** — HandAnalyzer against known hand values, HandHelper against known preflop rankings ### Phase 3: Private Bots Crate 13. **Create `holdem_bots/` crate skeleton** — `Cargo.toml`, `lib.rs`, `cash/mod.rs` 14. **Create `factory.rs`** — `register_advanced_bots()` function 15. **Create `cash/preflop/` strategies** — preflop tables and decision logic for big stack and short stack 16. **Create `cash/postflop.rs`** — shared postflop logic using `HandPotential` 17. **Create `cash/street/` strategies** — flop, turn, river specific logic 18. **Create `cash/big_stack.rs`** — `BigStackBot` implementing `HoldemPlayer`, composing strategies via `ModularStrategy` 19. **Create `cash/short_stack.rs`** — `ShortStackBot` implementing `HoldemPlayer`, composing strategies via `ModularStrategy` 20. **Update workspace `Cargo.toml`** — add `holdem_bots` member 21. **Update `holdem_sim`** — add optional `holdem_bots` dependency and feature flag ### Phase 4: Integration and Validation 22. **Integration test** — run a cash game simulation with BigStackBot vs simple bots 23. **Integration test** — run a tournament with mixed bot types 24. **Validate public mirror** — confirm `holdem_core` compiles and all tests pass without `holdem_bots` 25. **Update documentation** — add `holdem_bots/README.md`, update main `README.md` --- ## Dependency Graph ```mermaid 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`](holdem_core/src/game/player.rs:78) 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 `HoldemPlayer` implementation - Strategies to be tested in isolation with mock `StrategyContext` - The `ModularStrategy` adapter bridges `Strategy` → `HoldemPlayer` ### Why plugin registry over hardcoded match? The current [`create_bot()`](holdem_core/src/game/bot_factory.rs:52) 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`](holdem_core/src/evaluator/singleton.rs:17) 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: 1. **Test fixtures** — used in engine tests and property tests 2. **Examples** — demonstrate how to implement `HoldemPlayer` 3. **Baseline opponents** — needed even in the public crate for simulation They contain no proprietary logic and are intentionally simple.