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).
This commit is contained in:
Jan-Peter von Hunnius 2026-09-08 16:44:06 +02:00
parent b9963bfbb1
commit b4c501ec8c
4 changed files with 51 additions and 6 deletions

View file

@ -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(),
}
}

View file

@ -258,6 +258,11 @@ pub struct BotConfig {
#[serde(default)]
pub seat: Option<SeatIndex>,
/// Per-bot starting stack override (`None` = game starting_chips).
/// Enables asymmetric stack-depth experiments.
#[serde(default)]
pub starting_chips: Option<ChipCount>,
/// Custom parameters for the bot.
#[serde(default)]
pub parameters: HashMap<String, toml::Value>,
@ -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

View file

@ -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(),
},
],

View file

@ -83,6 +83,11 @@ pub struct Table {
config: TableConfig,
/// All seats at the table; `None` for empty seats.
seats: Vec<Option<Seat>>,
/// 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<ChipCount>,
/// 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;