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

25 KiB
Raw Blame History

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:


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:

  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

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 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 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

  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

  1. Create hand_analyzer.rs — HandAnalyzer struct using Arc<Evaluator>; methods like is_over_pair(), is_flush_draw(), num_outs_to_monster()
  2. Create hand_helper.rs — HandHelper with const preflop ranking tables, kill_phil_group(), board_texture(), count_outs()
  3. Create hand_range.rs — HandRange with [[f64; 13]; 13] weighted grid, normalize(), iteration
  4. Create hand_potential.rs — HandPotential with hand_strength(), positive_potential(), negative_potential() using Monte Carlo or exhaustive enumeration
  5. Write property tests — HandAnalyzer against known hand values, HandHelper against known preflop rankings

Phase 3: Private Bots Crate

  1. Create holdem_bots/ crate skeleton — Cargo.toml, lib.rs, cash/mod.rs
  2. Create factory.rs — register_advanced_bots() function
  3. Create cash/preflop/ strategies — preflop tables and decision logic for big stack and short stack
  4. Create cash/postflop.rs — shared postflop logic using HandPotential
  5. Create cash/street/ strategies — flop, turn, river specific logic
  6. Create cash/big_stack.rs — BigStackBot implementing HoldemPlayer, composing strategies via ModularStrategy
  7. Create cash/short_stack.rs — ShortStackBot implementing HoldemPlayer, composing strategies via ModularStrategy
  8. Update workspace Cargo.toml — add holdem_bots member
  9. Update holdem_sim — add optional holdem_bots dependency and feature flag

Phase 4: Integration and Validation

  1. Integration test — run a cash game simulation with BigStackBot vs simple bots
  2. Integration test — run a tournament with mixed bot types
  3. Validate public mirror — confirm holdem_core compiles and all tests pass without holdem_bots
  4. Update documentation — add holdem_bots/README.md, update main README.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 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() 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:

  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.