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:
parent
b9963bfbb1
commit
b4c501ec8c
4 changed files with 51 additions and 6 deletions
|
|
@ -96,6 +96,7 @@ where
|
||||||
/// bot_type: "call_bot".to_string(),
|
/// bot_type: "call_bot".to_string(),
|
||||||
/// name: Some("Alice".to_string()),
|
/// name: Some("Alice".to_string()),
|
||||||
/// seat: None,
|
/// seat: None,
|
||||||
|
/// starting_chips: None,
|
||||||
/// parameters: HashMap::new(),
|
/// parameters: HashMap::new(),
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
|
|
@ -141,6 +142,7 @@ mod tests {
|
||||||
bot_type: bot_type.to_string(),
|
bot_type: bot_type.to_string(),
|
||||||
name: name.map(|s| s.to_string()),
|
name: name.map(|s| s.to_string()),
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,11 @@ pub struct BotConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub seat: Option<SeatIndex>,
|
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.
|
/// Custom parameters for the bot.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub parameters: HashMap<String, toml::Value>,
|
pub parameters: HashMap<String, toml::Value>,
|
||||||
|
|
@ -1091,6 +1096,7 @@ min_raise = 50
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("Alice".to_string()),
|
name: Some("Alice".to_string()),
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -1277,18 +1283,21 @@ min_raise = 50
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("A".to_string()),
|
name: Some("A".to_string()),
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "fold_bot".to_string(),
|
bot_type: "fold_bot".to_string(),
|
||||||
name: Some("B".to_string()),
|
name: Some("B".to_string()),
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "random_bot".to_string(),
|
bot_type: "random_bot".to_string(),
|
||||||
name: Some("C".to_string()),
|
name: Some("C".to_string()),
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -1335,6 +1344,7 @@ min_raise = 50
|
||||||
bot_type: "bot".to_string(),
|
bot_type: "bot".to_string(),
|
||||||
name: None,
|
name: None,
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
};
|
};
|
||||||
9
|
9
|
||||||
|
|
@ -1387,18 +1397,21 @@ min_raise = 50
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("A".to_string()),
|
name: Some("A".to_string()),
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "fold_bot".to_string(),
|
bot_type: "fold_bot".to_string(),
|
||||||
name: Some("B".to_string()),
|
name: Some("B".to_string()),
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "random_bot".to_string(),
|
bot_type: "random_bot".to_string(),
|
||||||
name: Some("C".to_string()),
|
name: Some("C".to_string()),
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -1450,6 +1463,7 @@ min_raise = 50
|
||||||
bot_type: "bot".to_string(),
|
bot_type: "bot".to_string(),
|
||||||
name: None,
|
name: None,
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
};
|
};
|
||||||
9
|
9
|
||||||
|
|
|
||||||
|
|
@ -61,14 +61,15 @@ pub fn run_cash_game(config: &GameConfig) -> Result<(SimulationStats, Table), Ga
|
||||||
// Store the name before seating (seating consumes the bot).
|
// Store the name before seating (seating consumes the bot).
|
||||||
let name = bot.name().to_string();
|
let name = bot.name().to_string();
|
||||||
player_names.insert(player_id, name);
|
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.
|
// 3. Seat the bot.
|
||||||
let seat_index = bot_config
|
let seat_index = bot_config
|
||||||
.seat
|
.seat
|
||||||
.unwrap_or_else(|| table.find_empty_seat().unwrap_or(i as u8));
|
.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.
|
// 4. Run hands and collect stats.
|
||||||
|
|
@ -423,6 +424,7 @@ mod tests {
|
||||||
rake_percentage: 0.0,
|
rake_percentage: 0.0,
|
||||||
rake_cap: 0,
|
rake_cap: 0,
|
||||||
reset_bankroll_each_hand: false,
|
reset_bankroll_each_hand: false,
|
||||||
|
cash_out_threshold: 0,
|
||||||
bankroll_refill_threshold: 0,
|
bankroll_refill_threshold: 0,
|
||||||
randomize_seats: false,
|
randomize_seats: false,
|
||||||
auto_rebuy: false,
|
auto_rebuy: false,
|
||||||
|
|
@ -437,18 +439,21 @@ mod tests {
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("Alice".to_string()),
|
name: Some("Alice".to_string()),
|
||||||
seat: Some(0),
|
seat: Some(0),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("Bob".to_string()),
|
name: Some("Bob".to_string()),
|
||||||
seat: Some(1),
|
seat: Some(1),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "fold_bot".to_string(),
|
bot_type: "fold_bot".to_string(),
|
||||||
name: Some("Charlie".to_string()),
|
name: Some("Charlie".to_string()),
|
||||||
seat: Some(2),
|
seat: Some(2),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -536,6 +541,7 @@ mod tests {
|
||||||
rake_percentage: 0.0,
|
rake_percentage: 0.0,
|
||||||
rake_cap: 0,
|
rake_cap: 0,
|
||||||
reset_bankroll_each_hand: false,
|
reset_bankroll_each_hand: false,
|
||||||
|
cash_out_threshold: 0,
|
||||||
bankroll_refill_threshold: 0,
|
bankroll_refill_threshold: 0,
|
||||||
randomize_seats: false,
|
randomize_seats: false,
|
||||||
auto_rebuy: false,
|
auto_rebuy: false,
|
||||||
|
|
@ -550,6 +556,7 @@ mod tests {
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("Lonely".to_string()),
|
name: Some("Lonely".to_string()),
|
||||||
seat: None,
|
seat: None,
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
@ -577,6 +584,7 @@ mod tests {
|
||||||
rake_percentage: 0.0,
|
rake_percentage: 0.0,
|
||||||
rake_cap: 0,
|
rake_cap: 0,
|
||||||
reset_bankroll_each_hand: false,
|
reset_bankroll_each_hand: false,
|
||||||
|
cash_out_threshold: 0,
|
||||||
bankroll_refill_threshold: 0,
|
bankroll_refill_threshold: 0,
|
||||||
randomize_seats: false,
|
randomize_seats: false,
|
||||||
auto_rebuy: false,
|
auto_rebuy: false,
|
||||||
|
|
@ -591,12 +599,14 @@ mod tests {
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("Alice".to_string()),
|
name: Some("Alice".to_string()),
|
||||||
seat: Some(0),
|
seat: Some(0),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "super_ai".to_string(),
|
bot_type: "super_ai".to_string(),
|
||||||
name: Some("Bob".to_string()),
|
name: Some("Bob".to_string()),
|
||||||
seat: Some(1),
|
seat: Some(1),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -625,6 +635,7 @@ mod tests {
|
||||||
rake_percentage: 0.0,
|
rake_percentage: 0.0,
|
||||||
rake_cap: 0,
|
rake_cap: 0,
|
||||||
reset_bankroll_each_hand: false,
|
reset_bankroll_each_hand: false,
|
||||||
|
cash_out_threshold: 0,
|
||||||
bankroll_refill_threshold: 0,
|
bankroll_refill_threshold: 0,
|
||||||
randomize_seats: false,
|
randomize_seats: false,
|
||||||
auto_rebuy: false,
|
auto_rebuy: false,
|
||||||
|
|
@ -640,12 +651,14 @@ mod tests {
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("A".to_string()),
|
name: Some("A".to_string()),
|
||||||
seat: None, // auto-assign
|
seat: None, // auto-assign
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "fold_bot".to_string(),
|
bot_type: "fold_bot".to_string(),
|
||||||
name: Some("B".to_string()),
|
name: Some("B".to_string()),
|
||||||
seat: None, // auto-assign
|
seat: None, // auto-assign
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -692,6 +705,7 @@ mod tests {
|
||||||
rake_percentage: 0.0,
|
rake_percentage: 0.0,
|
||||||
rake_cap: 0,
|
rake_cap: 0,
|
||||||
reset_bankroll_each_hand: false,
|
reset_bankroll_each_hand: false,
|
||||||
|
cash_out_threshold: 0,
|
||||||
bankroll_refill_threshold: 0,
|
bankroll_refill_threshold: 0,
|
||||||
randomize_seats: false,
|
randomize_seats: false,
|
||||||
auto_rebuy: true,
|
auto_rebuy: true,
|
||||||
|
|
@ -706,12 +720,14 @@ mod tests {
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("Alice".to_string()),
|
name: Some("Alice".to_string()),
|
||||||
seat: Some(0),
|
seat: Some(0),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("Bob".to_string()),
|
name: Some("Bob".to_string()),
|
||||||
seat: Some(1),
|
seat: Some(1),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -773,6 +789,7 @@ mod tests {
|
||||||
rake_percentage: 0.0,
|
rake_percentage: 0.0,
|
||||||
rake_cap: 0,
|
rake_cap: 0,
|
||||||
reset_bankroll_each_hand: false,
|
reset_bankroll_each_hand: false,
|
||||||
|
cash_out_threshold: 0,
|
||||||
bankroll_refill_threshold: 0,
|
bankroll_refill_threshold: 0,
|
||||||
randomize_seats: false,
|
randomize_seats: false,
|
||||||
auto_rebuy: true,
|
auto_rebuy: true,
|
||||||
|
|
@ -787,12 +804,14 @@ mod tests {
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("Alice".to_string()),
|
name: Some("Alice".to_string()),
|
||||||
seat: Some(0),
|
seat: Some(0),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "fold_bot".to_string(),
|
bot_type: "fold_bot".to_string(),
|
||||||
name: Some("Bob".to_string()),
|
name: Some("Bob".to_string()),
|
||||||
seat: Some(1),
|
seat: Some(1),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -836,6 +855,7 @@ mod tests {
|
||||||
rake_percentage: 0.0,
|
rake_percentage: 0.0,
|
||||||
rake_cap: 0,
|
rake_cap: 0,
|
||||||
reset_bankroll_each_hand: false,
|
reset_bankroll_each_hand: false,
|
||||||
|
cash_out_threshold: 0,
|
||||||
bankroll_refill_threshold: 0,
|
bankroll_refill_threshold: 0,
|
||||||
randomize_seats: false,
|
randomize_seats: false,
|
||||||
auto_rebuy: true,
|
auto_rebuy: true,
|
||||||
|
|
@ -850,12 +870,14 @@ mod tests {
|
||||||
bot_type: "call_bot".to_string(),
|
bot_type: "call_bot".to_string(),
|
||||||
name: Some("Alice".to_string()),
|
name: Some("Alice".to_string()),
|
||||||
seat: Some(0),
|
seat: Some(0),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
BotConfig {
|
BotConfig {
|
||||||
bot_type: "fold_bot".to_string(),
|
bot_type: "fold_bot".to_string(),
|
||||||
name: Some("Bob".to_string()),
|
name: Some("Bob".to_string()),
|
||||||
seat: Some(1),
|
seat: Some(1),
|
||||||
|
starting_chips: None,
|
||||||
parameters: HashMap::new(),
|
parameters: HashMap::new(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,11 @@ pub struct Table {
|
||||||
config: TableConfig,
|
config: TableConfig,
|
||||||
/// All seats at the table; `None` for empty seats.
|
/// All seats at the table; `None` for empty seats.
|
||||||
seats: Vec<Option<Seat>>,
|
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).
|
/// Current dealer button position (seat index).
|
||||||
button: SeatIndex,
|
button: SeatIndex,
|
||||||
/// The currently running hand, if any.
|
/// The currently running hand, if any.
|
||||||
|
|
@ -118,9 +123,11 @@ impl Table {
|
||||||
let bankroll_refill_threshold = config.bankroll_refill_threshold;
|
let bankroll_refill_threshold = config.bankroll_refill_threshold;
|
||||||
let randomize_seats = config.randomize_seats;
|
let randomize_seats = config.randomize_seats;
|
||||||
let seats = (0..config.num_seats).map(|_| None).collect();
|
let seats = (0..config.num_seats).map(|_| None).collect();
|
||||||
|
let starting_by_seat = (0..config.num_seats).map(|_| starting_chips).collect();
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
seats,
|
seats,
|
||||||
|
starting_by_seat,
|
||||||
button: 0,
|
button: 0,
|
||||||
current_hand: None,
|
current_hand: None,
|
||||||
hand_number: 0,
|
hand_number: 0,
|
||||||
|
|
@ -168,6 +175,7 @@ impl Table {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut seat_obj = Seat::new(seat, player, buy_in);
|
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.
|
// Build a GameInfo snapshot so the player can initialise.
|
||||||
let game_info = self.build_game_info();
|
let game_info = self.build_game_info();
|
||||||
|
|
@ -598,15 +606,14 @@ impl Table {
|
||||||
/// Reset all seated players' stacks to starting_chips.
|
/// Reset all seated players' stacks to starting_chips.
|
||||||
/// Track the change in their bankroll.
|
/// Track the change in their bankroll.
|
||||||
fn reset_stacks_to_starting(&mut self) {
|
fn reset_stacks_to_starting(&mut self) {
|
||||||
let starting = self.starting_chips;
|
for (slot, starting) in &mut self.seats.iter_mut().zip(self.starting_by_seat.iter()) {
|
||||||
for slot in &mut self.seats {
|
|
||||||
if let Some(seat) = slot.as_mut() {
|
if let Some(seat) = slot.as_mut() {
|
||||||
// The "cost" of resetting is the difference between
|
// The "cost" of resetting is the difference between
|
||||||
// starting and current stack.
|
// starting and current stack.
|
||||||
// If they have more than starting, they profit.
|
// If they have more than starting, they profit.
|
||||||
// If they have less, they lost.
|
// If they have less, they lost.
|
||||||
seat.bankroll += seat.stack as i64 - starting as i64;
|
seat.bankroll += seat.stack as i64 - *starting as i64;
|
||||||
seat.stack = starting;
|
seat.stack = *starting;
|
||||||
// Re-activate sitting-out players (they have chips again).
|
// Re-activate sitting-out players (they have chips again).
|
||||||
if seat.state == PlayerState::SittingOut {
|
if seat.state == PlayerState::SittingOut {
|
||||||
seat.state = PlayerState::Active;
|
seat.state = PlayerState::Active;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue