Skip to content

Commit

Permalink
feat: add a new single table tournament
Browse files Browse the repository at this point in the history
Summary:
Add a single table tournament and a builder struct for it. The goal is to be able to see how agents compete against each other from different positions and stack sizes.

Test Plan:
Added an example binary that runs the code.
cargo clippy
  • Loading branch information
elliottneilclark committed Apr 21, 2024
1 parent 67e11c6 commit 0a9b55e
Show file tree
Hide file tree
Showing 12 changed files with 278 additions and 11 deletions.
28 changes: 28 additions & 0 deletions examples/agent_tournament.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::vec;

use rs_poker::arena::{
agent::{CallingAgentBuilder, RandomAgentBuilder},
tournament, AgentBuilder,
};

fn main() {
let stacks = vec![100.0, 100.0, 50.0];

let agent_builders: Vec<Box<dyn AgentBuilder>> = vec![
Box::new(CallingAgentBuilder),
Box::<RandomAgentBuilder>::default(),
Box::<RandomAgentBuilder>::default(),
];

let game_state = rs_poker::arena::game_state::GameState::new(stacks, 10.0, 5.0, 0.0, 0);

let tournament = tournament::SingleTableTournamentBuilder::default()
.agent_builders(agent_builders)
.starting_game_state(game_state)
.build()
.unwrap();

let results = tournament.run().unwrap();

println!("Agent Results: {:?}", results);
}
13 changes: 11 additions & 2 deletions src/arena/agent/calling.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
use crate::arena::{action::AgentAction, game_state::GameState};

use super::Agent;
use super::{Agent, AgentBuilder};

/// A simple agent that always calls. This can
/// stand in for a player who is a calling
/// station for the rest of a hand.
#[derive(Debug, Clone, Copy, Default)]
pub struct CallingAgent {}
pub struct CallingAgent;

impl Agent for CallingAgent {
fn act(self: &mut CallingAgent, _id: &uuid::Uuid, game_state: &GameState) -> AgentAction {
AgentAction::Bet(game_state.current_round_bet())
}
}

/// Default Builder for `CallingAgent`.
pub struct CallingAgentBuilder;

impl AgentBuilder for CallingAgentBuilder {
fn build(&self, _game_state: &GameState) -> Box<dyn Agent> {
Box::new(CallingAgent)
}
}

#[cfg(test)]
mod tests {
use rand::thread_rng;
Expand Down
13 changes: 11 additions & 2 deletions src/arena/agent/folding.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::arena::{action::AgentAction, game_state::GameState};

use super::Agent;
use super::{Agent, AgentBuilder};

/// A simple agent that folds unless there is only one active player left.
#[derive(Default, Debug, Clone, Copy)]
pub struct FoldingAgent {}
pub struct FoldingAgent;

impl Agent for FoldingAgent {
fn act(self: &mut FoldingAgent, _id: &uuid::Uuid, game_state: &GameState) -> AgentAction {
Expand All @@ -16,6 +16,15 @@ impl Agent for FoldingAgent {
}
}

/// Default Builder for `FoldingAgent`.
pub struct FoldingAgentBuilder;

impl AgentBuilder for FoldingAgentBuilder {
fn build(&self, _game_state: &GameState) -> Box<dyn Agent> {
Box::new(FoldingAgent)
}
}

#[cfg(test)]
mod tests {
use approx::assert_relative_eq;
Expand Down
15 changes: 10 additions & 5 deletions src/arena/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
//!
//! Some basic agents are provided as a way of testing baseline value.
mod calling;
mod failing;
mod folding;
mod random;
mod replay;
Expand All @@ -20,8 +19,14 @@ pub trait Agent {
fn act(&mut self, id: &uuid::Uuid, game_state: &GameState) -> AgentAction;
}

pub use calling::CallingAgent;
pub use failing::FailingHistorian;
pub use folding::FoldingAgent;
pub use random::{RandomAgent, RandomPotControlAgent};
/// AgentBuilder is a trait that is used to build agents for tournaments
/// where each simulation needs a new agent.
pub trait AgentBuilder {
/// This method is called before each game to build a new agent.
fn build(&self, game_state: &GameState) -> Box<dyn Agent>;
}

pub use calling::{CallingAgent, CallingAgentBuilder};
pub use folding::{FoldingAgent, FoldingAgentBuilder};
pub use random::{RandomAgent, RandomAgentBuilder, RandomPotControlAgent};
pub use replay::{SliceReplayAgent, VecReplayAgent};
25 changes: 24 additions & 1 deletion src/arena/agent/random.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
holdem::MonteCarloGame,
};

use super::Agent;
use super::{Agent, AgentBuilder};

#[derive(Debug, Clone)]
pub struct RandomAgent {
Expand Down Expand Up @@ -91,6 +91,29 @@ impl Agent for RandomAgent {
}
}

pub struct RandomAgentBuilder {
percent_fold: Vec<f64>,
percent_call: Vec<f64>,
}

impl AgentBuilder for RandomAgentBuilder {
fn build(&self, _game_state: &GameState) -> Box<dyn Agent> {
Box::new(RandomAgent::new(
self.percent_fold.clone(),
self.percent_call.clone(),
))
}
}

impl Default for RandomAgentBuilder {
fn default() -> Self {
Self {
percent_fold: vec![0.25, 0.30, 0.50],
percent_call: vec![0.5, 0.6, 0.45],
}
}
}

/// This is an `Agent` implementation that chooses random actions in some
/// relation to the value of the pot. It assumes that it's up against totally
/// random cards for each hand then estimates the value of the pot for what
Expand Down
6 changes: 6 additions & 0 deletions src/arena/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ pub enum GameStateError {
pub enum HoldemSimulationError {
#[error("Builder needs a game state")]
NeedGameState,

#[error("Builder needs agents")]
NeedAgents,

#[error("Expected GameState to contain a winner (agent with all the money)")]
NoWinner,
}
1 change: 1 addition & 0 deletions src/arena/game_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ impl fmt::Debug for GameState {
.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)
Expand Down
File renamed without changes.
11 changes: 11 additions & 0 deletions src/arena/historian/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,20 @@ pub trait Historian {
) -> Result<(), HistorianError>;
}

/// HistorianBuilder is a trait that is used to build historians
/// for tournaments where each simulation needs a new historian.
pub trait HistorianBuilder {
/// This method is called before each game to build a new historian.
fn build(&self, game_state: &GameState) -> Box<dyn Historian>;
}

mod failing;
mod fn_historian;
mod null;
mod vec;

pub use failing::FailingHistorian;
pub use fn_historian::FnHistorian;
pub use null::NullHistorian;
use thiserror::Error;
pub use vec::VecHistorian;
14 changes: 14 additions & 0 deletions src/arena/historian/null.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use super::Historian;

pub struct NullHistorian;

impl Historian for NullHistorian {
fn record_action(
&mut self,
_id: &uuid::Uuid,
_game_state: &crate::arena::GameState,
_action: crate::arena::action::Action,
) -> Result<(), super::HistorianError> {
Ok(())
}
}
3 changes: 2 additions & 1 deletion src/arena/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,12 @@ pub mod game_state;
pub mod historian;
pub mod sim_builder;
pub mod simulation;
pub mod tournament;

#[cfg(any(test, feature = "arena-test-util"))]
pub mod test_util;

pub use agent::Agent;
pub use agent::{Agent, AgentBuilder};
pub use game_state::GameState;
pub use historian::{Historian, HistorianError};
pub use sim_builder::{HoldemSimulationBuilder, RngHoldemSimulationBuilder};
Expand Down
160 changes: 160 additions & 0 deletions src/arena/tournament.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use tracing::{event, trace_span};

use super::{errors::HoldemSimulationError, historian::HistorianBuilder, AgentBuilder, GameState};

/// A `SingleTableTournament` is a tournament that has multiple agents
/// playing holdem poker at a single table. The tournament is played
/// until a single agent has all the money.
///
/// This builder is used to create a `SingleTableTournament`.
#[derive(Default)]
pub struct SingleTableTournamentBuilder {
agent_builders: Option<Vec<Box<dyn AgentBuilder>>>,
historian_builders: Option<Vec<Box<dyn HistorianBuilder>>>,
starting_game_state: Option<GameState>,
panic_on_historian_error: bool,
}

pub struct SingleTableTournament {
agent_builders: Vec<Box<dyn AgentBuilder>>,
historian_builders: Vec<Box<dyn HistorianBuilder>>,
starting_game_state: GameState,
panic_on_historian_error: bool,
// TODO should this include payouts?
}

impl SingleTableTournamentBuilder {
pub fn agent_builders(mut self, agent_builders: Vec<Box<dyn AgentBuilder>>) -> Self {
self.agent_builders = Some(agent_builders);
self
}

pub fn historian_builders(
mut self,
historian_builders: Vec<Box<dyn HistorianBuilder>>,
) -> Self {
self.historian_builders = Some(historian_builders);
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_builders
.ok_or(HoldemSimulationError::NeedAgents)?;
let starting_game_state = self
.starting_game_state
.ok_or(HoldemSimulationError::NeedGameState)?;
let historian_builders = self.historian_builders.unwrap_or_default();
Ok(SingleTableTournament {
agent_builders,
historian_builders,
starting_game_state,
panic_on_historian_error: self.panic_on_historian_error,
})
}
}

impl SingleTableTournament {
// Returns a vector of the place that each agent finished in.
pub fn run(&self) -> Result<Vec<usize>, HoldemSimulationError> {
let span = trace_span!("SingleTableTournament::run");
let _enter = span.enter();

let mut place = self.agent_builders.len();
let mut results = vec![0; self.agent_builders.len()];
let mut game_state = self.starting_game_state.clone();
while place > 1 {
let agents = self
.agent_builders
.iter()
.map(|builder| builder.build(&game_state))
.collect::<Vec<_>>();
let historians = self
.historian_builders
.iter()
.map(|builder| builder.build(&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()?;

// Run the simulation
sim.run();

// Update the results
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<_>>();

// Sort by the starting stack going into the hand
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[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(
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[idx] = 1;
println!("Agent {} finished in place 1", idx);
event!(tracing::Level::INFO, "Agent {} finished in place 1", idx);
}
Ok(results)
}
}

0 comments on commit 0a9b55e

Please sign in to comment.