use tracing::{event, trace_span};
use crate::arena::{
GameState, agent::AgentGenerator, errors::HoldemSimulationError, historian::HistorianGenerator,
};
#[derive(Default)]
pub struct SingleTableTournamentBuilder {
agent_generators: Option<Vec<Box<dyn AgentGenerator>>>,
historian_generators: Option<Vec<Box<dyn HistorianGenerator>>>,
starting_game_state: Option<GameState>,
panic_on_historian_error: bool,
}
#[derive(Debug, Clone)]
pub struct TournamentResults {
places: Vec<usize>,
max_stacks: Vec<f32>,
rounds: usize,
}
pub struct SingleTableTournament {
agent_generators: Vec<Box<dyn AgentGenerator>>,
historian_generators: Vec<Box<dyn HistorianGenerator>>,
starting_game_state: GameState,
panic_on_historian_error: bool,
}
impl TournamentResults {
pub fn new(starting_stacks: &[f32]) -> Self {
TournamentResults {
places: vec![0; starting_stacks.len()],
max_stacks: starting_stacks.to_vec(),
rounds: 0,
}
}
pub fn update_max(&mut self, stacks: &[f32]) {
self.rounds += 1;
for (idx, stack) in stacks.iter().enumerate() {
if *stack > self.max_stacks[idx] {
self.max_stacks[idx] = *stack;
}
}
}
pub fn set_place(&mut self, idx: usize, place: usize) {
self.places[idx] = place;
}
pub fn places(&self) -> &[usize] {
&self.places
}
pub fn rounds(&self) -> usize {
self.rounds
}
pub fn max_stacks(&self) -> &[f32] {
&self.max_stacks
}
}
impl SingleTableTournamentBuilder {
pub fn agent_generators(mut self, agent_generators: Vec<Box<dyn AgentGenerator>>) -> Self {
self.agent_generators = Some(agent_generators);
self
}
pub fn historian_generators(
mut self,
historian_generators: Vec<Box<dyn HistorianGenerator>>,
) -> Self {
self.historian_generators = Some(historian_generators);
self
}
pub fn starting_game_state(mut self, starting_game_state: GameState) -> Self {
self.starting_game_state = Some(starting_game_state);
self
}
pub fn panic_on_historian_error(mut self, panic_on_historian_error: bool) -> Self {
self.panic_on_historian_error = panic_on_historian_error;
self
}
pub fn build(self) -> Result<SingleTableTournament, HoldemSimulationError> {
let agent_builders = self
.agent_generators
.ok_or(HoldemSimulationError::NeedAgents)?;
let starting_game_state = self
.starting_game_state
.ok_or(HoldemSimulationError::NeedGameState)?;
let historian_builders = self.historian_generators.unwrap_or_default();
Ok(SingleTableTournament {
agent_generators: agent_builders,
historian_generators: historian_builders,
starting_game_state,
panic_on_historian_error: self.panic_on_historian_error,
})
}
}
impl SingleTableTournament {
pub fn run(self) -> Result<TournamentResults, HoldemSimulationError> {
let span = trace_span!("SingleTableTournament::run");
let _enter = span.enter();
let mut rand = rand::rng();
let mut place = self.agent_generators.len();
let mut results = TournamentResults::new(&self.starting_game_state.stacks);
let mut game_state = self.starting_game_state;
while place > 1 {
let agents = self
.agent_generators
.iter()
.map(|builder| builder.generate(&game_state))
.collect::<Vec<_>>();
let historians = self
.historian_generators
.iter()
.map(|builder| builder.generate(&game_state))
.collect::<Vec<_>>();
let mut sim = crate::arena::HoldemSimulationBuilder::default()
.game_state(game_state.clone())
.agents(agents)
.historians(historians)
.panic_on_historian_error(self.panic_on_historian_error)
.build()?;
sim.run(&mut rand);
results.update_max(&sim.game_state.stacks);
let mut out = sim
.game_state
.stacks
.iter()
.enumerate()
.filter(|(_, stack)| **stack == 0.0)
.filter(|(idx, _)| sim.game_state.starting_stacks[*idx] != 0.0)
.map(|(idx, _)| idx)
.collect::<Vec<_>>();
out.sort_by(|a, b| {
sim.game_state.starting_stacks[*b]
.partial_cmp(&sim.game_state.starting_stacks[*a])
.unwrap()
.reverse()
});
for idx in out {
event!(
tracing::Level::INFO,
"Agent {} finished in place {}",
idx,
place
);
results.set_place(idx, place);
place -= 1;
}
let mut dealer_idx = (sim.game_state.dealer_idx + 1) % sim.game_state.stacks.len();
while sim.game_state.stacks[dealer_idx] == 0.0 {
dealer_idx = (dealer_idx + 1) % sim.game_state.stacks.len();
}
game_state = GameState::new_starting(
sim.game_state.stacks,
sim.game_state.big_blind,
sim.game_state.small_blind,
sim.game_state.ante,
dealer_idx,
);
}
if place == 1 {
let winners: Vec<usize> = game_state
.stacks
.iter()
.enumerate()
.filter(|(_, stack)| **stack > 0.0)
.map(|(idx, _)| idx)
.collect();
if winners.len() != 1 {
return Err(HoldemSimulationError::NoWinner);
}
let idx = winners[0];
results.set_place(idx, 1);
event!(tracing::Level::INFO, "Agent {} finished in place 1", idx);
}
Ok(results)
}
}
#[cfg(test)]
mod tests {
use crate::arena::agent::{AllInAgentGenerator, FoldingAgentGenerator};
use super::*;
#[test]
fn test_all_in() {
let stacks = vec![50.0; 4];
let gens: Vec<Box<dyn AgentGenerator>> = vec![
Box::<AllInAgentGenerator>::default(),
Box::<AllInAgentGenerator>::default(),
Box::<AllInAgentGenerator>::default(),
Box::<AllInAgentGenerator>::default(),
];
let game_state = GameState::new_starting(stacks, 10.0, 5.0, 1.0, 0);
let tournament = SingleTableTournamentBuilder::default()
.agent_generators(gens)
.starting_game_state(game_state)
.build()
.unwrap();
let results = tournament.run().unwrap();
for i in 1..4 {
assert!(results.places().contains(&i));
}
}
#[test]
fn test_headsup_tournament_folding_never_wins() {
let stacks = vec![50.0; 4];
let agent_gens: Vec<Box<dyn AgentGenerator>> = vec![
Box::<AllInAgentGenerator>::default(),
Box::<FoldingAgentGenerator>::default(),
Box::<FoldingAgentGenerator>::default(),
Box::<FoldingAgentGenerator>::default(),
];
let game_state = GameState::new_starting(stacks, 10.0, 5.0, 1.0, 0);
let tournament = SingleTableTournamentBuilder::default()
.agent_generators(agent_gens)
.starting_game_state(game_state)
.build()
.unwrap();
let results = tournament.run().unwrap();
assert_eq!(1, results.places()[0]);
assert!(results.places()[1] > 1);
assert!(results.places()[2] > 1);
}
}