From 8556da5b5d37130f3a926ae3979ea0694f61ac73 Mon Sep 17 00:00:00 2001 From: Elliott Clark Date: Sat, 29 Jun 2024 18:33:21 -0500 Subject: [PATCH] feat: keep track of the round before simlation completes Summary: When running a simulation we can transition from lots of different rounds to complete via folding or via showdown. So keep track of what the last round we have seen before complete'ing the sim. Test Plan: Ran `cargo run --release --example agent_battle` Got: ``` Current Competition Stats: HoldemCompetition { num_rounds: 2500, total_change: [422.86856, -2458.051, 3308.6985, -1273.5121, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], max_change: [846.8667, 643.11194, 636.79913, 471.92227, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], min_change: [-641.6119, -835.31396, -411.79034, -774.67834, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], win_count: [975, 1111, 207, 212, 0, 0, 0, 0, 0, 0, 0, 0], zero_count: [273, 333, 1117, 1085, 0, 0, 0, 0, 0, 0, 0, 0], loss_count: [1252, 1056, 1176, 1203, 0, 0, 0, 0, 0, 0, 0, 0], round_before: {Preflop: 1300, Flop: 515, Showdown: 275, Turn: 262, River: 148} } ``` That's about the distribution I would have expected. Lots of folding before starting with non-premium hands. Then we lose a lot on the flop. Rounds after that not many fold. I doubt that the actions are GTO but they look reasonable. --- src/arena/competition/holdem_competition.rs | 18 +++++- src/arena/game_state.rs | 68 ++++++++------------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/arena/competition/holdem_competition.rs b/src/arena/competition/holdem_competition.rs index 35b692a3..81600963 100644 --- a/src/arena/competition/holdem_competition.rs +++ b/src/arena/competition/holdem_competition.rs @@ -1,6 +1,9 @@ -use std::{collections::VecDeque, fmt::Debug}; +use std::{ + collections::{HashMap, VecDeque}, + fmt::Debug, +}; -use crate::arena::{errors::HoldemSimulationError, HoldemSimulation}; +use crate::arena::{errors::HoldemSimulationError, game_state::Round, HoldemSimulation}; use super::sim_gen::HoldemSimulationGenerator; @@ -24,6 +27,8 @@ pub struct HoldemCompetition { pub loss_count: Vec, // How many times the agent has lost no money pub zero_count: Vec, + // Count of the round before the simulation stopped + pub before_count: HashMap, /// Maximum number of HoldemSimulation's to /// keep in a long call to `run` @@ -50,6 +55,8 @@ impl HoldemCompetition { win_count: vec![0; MAX_PLAYERS], loss_count: vec![0; MAX_PLAYERS], zero_count: vec![0; MAX_PLAYERS], + // Round before stopping + before_count: HashMap::new(), } } @@ -115,6 +122,12 @@ impl HoldemCompetition { self.zero_count[idx] += 1; } } + // Update the count + let count = self + .before_count + .entry(running_sim.game_state.round_before) + .or_default(); + *count += 1; } } impl Debug for HoldemCompetition { @@ -127,6 +140,7 @@ impl Debug for HoldemCompetition { .field("win_count", &self.win_count) .field("zero_count", &self.zero_count) .field("loss_count", &self.loss_count) + .field("round_before", &self.before_count) .finish() } } diff --git a/src/arena/game_state.rs b/src/arena/game_state.rs index abeb826f..30777d18 100644 --- a/src/arena/game_state.rs +++ b/src/arena/game_state.rs @@ -76,7 +76,7 @@ impl Round { } } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Debug)] pub struct RoundData { // Which players were active starting this round. pub starting_player_active: PlayerBitSet, @@ -168,7 +168,7 @@ impl RoundData { } } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Debug)] pub struct GameState { /// The number of players that started pub num_players: usize, @@ -199,6 +199,10 @@ pub struct GameState { pub dealer_idx: usize, // What round this is currently pub round: Round, + /// This is the round before we completed the game. + /// Sometimes the game completes because of + /// all the players fold in the preflop. + pub round_before: Round, // ALl the current state of the round. pub round_data: RoundData, // The community cards. @@ -243,6 +247,7 @@ impl GameState { total_pot: 0.0, hands: vec![Hand::default(); num_players], round: Round::Starting, + round_before: Round::Starting, board: vec![], round_data: RoundData::new(num_players, big_blind, player_active, dealer_idx), computed_rank: vec![None; num_players], @@ -304,6 +309,9 @@ impl GameState { fn advance_normal(&mut self) { self.round = self.round.advance(); + // We're advancing (not completing) so + // keep advanding the round_before field as well. + self.round_before = self.round; let mut round_data = RoundData::new( self.num_players, @@ -321,6 +329,7 @@ impl GameState { } pub fn complete(&mut self) { + self.round_before = self.round; self.round = Round::Complete; let round_data = RoundData::new( self.num_players, @@ -453,45 +462,6 @@ impl GameState { } } -impl fmt::Debug for RoundData { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RoundData") - .field("needs_action", &self.needs_action) - .field("num_players_need_action", &self.num_players_need_action()) - .field("min_raise", &self.min_raise) - .field("bet", &self.bet) - .field("player_bet", &self.player_bet) - .field("bet_count", &self.bet_count) - .field("raise_count", &self.raise_count) - .field("to_act_idx", &self.to_act_idx) - .finish() - } -} - -impl fmt::Debug for GameState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("GameState") - .field("num_players", &self.num_players) - .field("num_active_players", &self.num_active_players()) - .field("player_active", &self.player_active) - .field("player_all_in", &self.player_all_in) - .field("total_pot", &self.total_pot) - .field("stacks", &self.stacks) - .field("player_winnings", &self.player_winnings) - .field("big_blind", &self.big_blind) - .field("small_blind", &self.small_blind) - .field("ante", &self.ante) - .field("hands", &self.hands) - .field("dealer_idx", &self.dealer_idx) - .field("round", &self.round) - .field("round_data", &self.round_data) - .field("board", &self.board) - .field("sb_posted", &self.sb_posted) - .field("bb_posted", &self.bb_posted) - .finish() - } -} - pub trait GameStateGenerator { fn generate(&mut self) -> GameState; } @@ -688,6 +658,7 @@ mod tests { // Do the start and ante rounds and setup next to act game_state.advance_round(); game_state.advance_round(); + game_state.advance_round(); game_state.do_bet(10.0, true).unwrap(); game_state.do_bet(20.0, true).unwrap(); @@ -702,4 +673,19 @@ mod tests { game_state.do_bet(33.0, false) ); } + + #[test] + fn test_gamestate_keeps_round_before_complete() { + let stacks = vec![100.0; 3]; + let mut game_state = GameState::new(stacks, 10.0, 5.0, 0.0, 0); + // Simulate a game where everyone folds and the big blind wins + game_state.advance_round(); + game_state.advance_round(); + game_state.advance_round(); + game_state.fold(); + game_state.fold(); + game_state.complete(); + assert_eq!(Round::Complete, game_state.round); + assert_eq!(Round::Preflop, game_state.round_before); + } }