From b4c501ec8c4d2f6cff88b90fe525887b8fcde2b3 Mon Sep 17 00:00:00 2001 From: Jan-Peter von Hunnius Date: Tue, 8 Sep 2026 16:44:06 +0200 Subject: [PATCH] feat(sim): per-bot starting_chips override BotConfig.starting_chips (None = game default). Table tracks per-seat starting stacks so reset_bankroll_each_hand restores each bot to its own depth. Enables asymmetric stack experiments (hero deep vs opponents short and vice versa). --- holdem_core/src/game/bot_factory.rs | 2 ++ holdem_core/src/game/config.rs | 14 ++++++++++++++ holdem_core/src/game/simulation.rs | 26 ++++++++++++++++++++++++-- holdem_core/src/game/table.rs | 15 +++++++++++---- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/holdem_core/src/game/bot_factory.rs b/holdem_core/src/game/bot_factory.rs index 94d0ab8..8884b14 100644 --- a/holdem_core/src/game/bot_factory.rs +++ b/holdem_core/src/game/bot_factory.rs @@ -96,6 +96,7 @@ where /// bot_type: "call_bot".to_string(), /// name: Some("Alice".to_string()), /// seat: None, +/// starting_chips: None, /// parameters: HashMap::new(), /// }; /// @@ -141,6 +142,7 @@ mod tests { bot_type: bot_type.to_string(), name: name.map(|s| s.to_string()), seat: None, + starting_chips: None, parameters: HashMap::new(), } } diff --git a/holdem_core/src/game/config.rs b/holdem_core/src/game/config.rs index 810e011..af34c8d 100644 --- a/holdem_core/src/game/config.rs +++ b/holdem_core/src/game/config.rs @@ -258,6 +258,11 @@ pub struct BotConfig { #[serde(default)] pub seat: Option, + /// Per-bot starting stack override (`None` = game starting_chips). + /// Enables asymmetric stack-depth experiments. + #[serde(default)] + pub starting_chips: Option, + /// Custom parameters for the bot. #[serde(default)] pub parameters: HashMap, @@ -1091,6 +1096,7 @@ min_raise = 50 bot_type: "call_bot".to_string(), name: Some("Alice".to_string()), seat: None, + starting_chips: None, parameters: HashMap::new(), }, ], @@ -1277,18 +1283,21 @@ min_raise = 50 bot_type: "call_bot".to_string(), name: Some("A".to_string()), seat: None, + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "fold_bot".to_string(), name: Some("B".to_string()), seat: None, + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "random_bot".to_string(), name: Some("C".to_string()), seat: None, + starting_chips: None, parameters: HashMap::new(), }, ], @@ -1335,6 +1344,7 @@ min_raise = 50 bot_type: "bot".to_string(), name: None, seat: None, + starting_chips: None, parameters: HashMap::new(), }; 9 @@ -1387,18 +1397,21 @@ min_raise = 50 bot_type: "call_bot".to_string(), name: Some("A".to_string()), seat: None, + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "fold_bot".to_string(), name: Some("B".to_string()), seat: None, + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "random_bot".to_string(), name: Some("C".to_string()), seat: None, + starting_chips: None, parameters: HashMap::new(), }, ], @@ -1450,6 +1463,7 @@ min_raise = 50 bot_type: "bot".to_string(), name: None, seat: None, + starting_chips: None, parameters: HashMap::new(), }; 9 diff --git a/holdem_core/src/game/simulation.rs b/holdem_core/src/game/simulation.rs index ee1681c..282be7b 100644 --- a/holdem_core/src/game/simulation.rs +++ b/holdem_core/src/game/simulation.rs @@ -61,14 +61,15 @@ pub fn run_cash_game(config: &GameConfig) -> Result<(SimulationStats, Table), Ga // Store the name before seating (seating consumes the bot). let name = bot.name().to_string(); player_names.insert(player_id, name); - starting_stacks.insert(player_id, config.starting_chips); + let bot_stack = bot_config.starting_chips.unwrap_or(config.starting_chips); + starting_stacks.insert(player_id, bot_stack); // 3. Seat the bot. let seat_index = bot_config .seat .unwrap_or_else(|| table.find_empty_seat().unwrap_or(i as u8)); - table.seat_player(seat_index, bot, config.starting_chips)?; + table.seat_player(seat_index, bot, bot_stack)?; } // 4. Run hands and collect stats. @@ -423,6 +424,7 @@ mod tests { rake_percentage: 0.0, rake_cap: 0, reset_bankroll_each_hand: false, + cash_out_threshold: 0, bankroll_refill_threshold: 0, randomize_seats: false, auto_rebuy: false, @@ -437,18 +439,21 @@ mod tests { bot_type: "call_bot".to_string(), name: Some("Alice".to_string()), seat: Some(0), + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "call_bot".to_string(), name: Some("Bob".to_string()), seat: Some(1), + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "fold_bot".to_string(), name: Some("Charlie".to_string()), seat: Some(2), + starting_chips: None, parameters: HashMap::new(), }, ], @@ -536,6 +541,7 @@ mod tests { rake_percentage: 0.0, rake_cap: 0, reset_bankroll_each_hand: false, + cash_out_threshold: 0, bankroll_refill_threshold: 0, randomize_seats: false, auto_rebuy: false, @@ -550,6 +556,7 @@ mod tests { bot_type: "call_bot".to_string(), name: Some("Lonely".to_string()), seat: None, + starting_chips: None, parameters: HashMap::new(), }], }; @@ -577,6 +584,7 @@ mod tests { rake_percentage: 0.0, rake_cap: 0, reset_bankroll_each_hand: false, + cash_out_threshold: 0, bankroll_refill_threshold: 0, randomize_seats: false, auto_rebuy: false, @@ -591,12 +599,14 @@ mod tests { bot_type: "call_bot".to_string(), name: Some("Alice".to_string()), seat: Some(0), + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "super_ai".to_string(), name: Some("Bob".to_string()), seat: Some(1), + starting_chips: None, parameters: HashMap::new(), }, ], @@ -625,6 +635,7 @@ mod tests { rake_percentage: 0.0, rake_cap: 0, reset_bankroll_each_hand: false, + cash_out_threshold: 0, bankroll_refill_threshold: 0, randomize_seats: false, auto_rebuy: false, @@ -640,12 +651,14 @@ mod tests { bot_type: "call_bot".to_string(), name: Some("A".to_string()), seat: None, // auto-assign + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "fold_bot".to_string(), name: Some("B".to_string()), seat: None, // auto-assign + starting_chips: None, parameters: HashMap::new(), }, ], @@ -692,6 +705,7 @@ mod tests { rake_percentage: 0.0, rake_cap: 0, reset_bankroll_each_hand: false, + cash_out_threshold: 0, bankroll_refill_threshold: 0, randomize_seats: false, auto_rebuy: true, @@ -706,12 +720,14 @@ mod tests { bot_type: "call_bot".to_string(), name: Some("Alice".to_string()), seat: Some(0), + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "call_bot".to_string(), name: Some("Bob".to_string()), seat: Some(1), + starting_chips: None, parameters: HashMap::new(), }, ], @@ -773,6 +789,7 @@ mod tests { rake_percentage: 0.0, rake_cap: 0, reset_bankroll_each_hand: false, + cash_out_threshold: 0, bankroll_refill_threshold: 0, randomize_seats: false, auto_rebuy: true, @@ -787,12 +804,14 @@ mod tests { bot_type: "call_bot".to_string(), name: Some("Alice".to_string()), seat: Some(0), + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "fold_bot".to_string(), name: Some("Bob".to_string()), seat: Some(1), + starting_chips: None, parameters: HashMap::new(), }, ], @@ -836,6 +855,7 @@ mod tests { rake_percentage: 0.0, rake_cap: 0, reset_bankroll_each_hand: false, + cash_out_threshold: 0, bankroll_refill_threshold: 0, randomize_seats: false, auto_rebuy: true, @@ -850,12 +870,14 @@ mod tests { bot_type: "call_bot".to_string(), name: Some("Alice".to_string()), seat: Some(0), + starting_chips: None, parameters: HashMap::new(), }, BotConfig { bot_type: "fold_bot".to_string(), name: Some("Bob".to_string()), seat: Some(1), + starting_chips: None, parameters: HashMap::new(), }, ], diff --git a/holdem_core/src/game/table.rs b/holdem_core/src/game/table.rs index ace6e5e..34eabbd 100644 --- a/holdem_core/src/game/table.rs +++ b/holdem_core/src/game/table.rs @@ -83,6 +83,11 @@ pub struct Table { config: TableConfig, /// All seats at the table; `None` for empty seats. seats: Vec>, + + /// Per-seat starting stack for resets (defaults to config.starting_chips; + /// records the buy-in of each seated player so per-bot overrides reset + /// to their own depth). + starting_by_seat: Vec, /// Current dealer button position (seat index). button: SeatIndex, /// The currently running hand, if any. @@ -118,9 +123,11 @@ impl Table { let bankroll_refill_threshold = config.bankroll_refill_threshold; let randomize_seats = config.randomize_seats; let seats = (0..config.num_seats).map(|_| None).collect(); + let starting_by_seat = (0..config.num_seats).map(|_| starting_chips).collect(); Self { config, seats, + starting_by_seat, button: 0, current_hand: None, hand_number: 0, @@ -168,6 +175,7 @@ impl Table { } let mut seat_obj = Seat::new(seat, player, buy_in); + self.starting_by_seat[seat as usize] = buy_in; // Build a GameInfo snapshot so the player can initialise. let game_info = self.build_game_info(); @@ -598,15 +606,14 @@ impl Table { /// Reset all seated players' stacks to starting_chips. /// Track the change in their bankroll. fn reset_stacks_to_starting(&mut self) { - let starting = self.starting_chips; - for slot in &mut self.seats { + for (slot, starting) in &mut self.seats.iter_mut().zip(self.starting_by_seat.iter()) { if let Some(seat) = slot.as_mut() { // The "cost" of resetting is the difference between // starting and current stack. // If they have more than starting, they profit. // If they have less, they lost. - seat.bankroll += seat.stack as i64 - starting as i64; - seat.stack = starting; + seat.bankroll += seat.stack as i64 - *starting as i64; + seat.stack = *starting; // Re-activate sitting-out players (they have chips again). if seat.state == PlayerState::SittingOut { seat.state = PlayerState::Active;