rusty_marvin/docs/ARCHITECTURE_BOT_FRAMEWORK.md
Jan-Peter v. Hunnius f81f0579d1 feat: port Java Supersonic bot framework and cash game bots to Rust
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)
2026-05-22 16:50:11 +02:00

648 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<dyn HoldemPlayer>`
- 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 {
<<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:
```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<PlayerId>,
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<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`](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<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()`](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<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:**
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<Evaluator>`; 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.