use rand::{Rng, rng};
use crate::{
arena::{
action::AgentAction,
game_state::{GameState, Round},
},
core::Hand,
holdem::MonteCarloGame,
};
use super::{Agent, AgentGenerator};
#[derive(Debug, Clone)]
pub struct RandomAgent {
percent_fold: Vec<f64>,
percent_call: Vec<f64>,
}
impl RandomAgent {
pub fn new(percent_fold: Vec<f64>, percent_call: Vec<f64>) -> Self {
Self {
percent_call,
percent_fold,
}
}
}
impl Default for RandomAgent {
fn default() -> Self {
Self {
percent_fold: vec![0.25, 0.30, 0.50],
percent_call: vec![0.5, 0.6, 0.45],
}
}
}
impl Agent for RandomAgent {
fn act(self: &mut RandomAgent, _id: u128, game_state: &GameState) -> AgentAction {
let round_data = &game_state.round_data;
let player_bet = round_data.current_player_bet();
let player_stack = game_state.stacks[round_data.to_act_idx];
let curr_bet = round_data.bet;
let raise_count = round_data.total_raise_count;
let mut rng = rng();
let min = (curr_bet + round_data.min_raise).min(player_bet + player_stack);
let pot_value = (round_data.num_players_need_action() as f32 + 1.0) * game_state.total_pot;
let max = (player_bet + player_stack).min(pot_value).max(min);
let can_fold = curr_bet > player_bet;
let fold_idx = raise_count.min((self.percent_fold.len() - 1) as u8) as usize;
let percent_fold = self.percent_fold.get(fold_idx).map_or_else(|| 1.0, |v| *v);
let call_idx = raise_count.min((self.percent_call.len() - 1) as u8) as usize;
let percent_call = self.percent_call.get(call_idx).map_or_else(|| 1.0, |v| *v);
if can_fold && rng.random_bool(percent_fold) {
AgentAction::Fold
} else if rng.random_bool(percent_call) {
AgentAction::Bet(curr_bet)
} else if max > min {
AgentAction::Bet(rng.random_range(min..max))
} else {
AgentAction::Bet(max)
}
}
}
pub struct RandomAgentGenerator {
percent_fold: Vec<f64>,
percent_call: Vec<f64>,
}
impl AgentGenerator for RandomAgentGenerator {
fn generate(&self, _game_state: &GameState) -> Box<dyn Agent> {
Box::new(RandomAgent::new(
self.percent_fold.clone(),
self.percent_call.clone(),
))
}
}
impl Default for RandomAgentGenerator {
fn default() -> Self {
Self {
percent_fold: vec![0.25, 0.30, 0.50],
percent_call: vec![0.5, 0.6, 0.45],
}
}
}
#[derive(Debug, Clone)]
pub struct RandomPotControlAgent {
percent_call: Vec<f64>,
}
impl RandomPotControlAgent {
fn expected_pot(&self, game_state: &GameState) -> f32 {
if game_state.round == Round::Preflop {
(3.0 * game_state.big_blind).max(game_state.total_pot)
} else {
game_state.total_pot
}
}
fn clean_hands(&self, game_state: &GameState) -> Vec<Hand> {
let mut default_hand = Hand::new();
default_hand.extend(game_state.board.iter().cloned());
let to_act_idx = game_state.to_act_idx();
game_state
.hands
.clone()
.into_iter()
.enumerate()
.map(|(hand_idx, hand)| {
if hand_idx == to_act_idx {
hand
} else {
default_hand
}
})
.collect()
}
fn monte_carlo_based_action(
&self,
game_state: &GameState,
mut monte: MonteCarloGame,
) -> AgentAction {
let expected_pot = self.expected_pot(game_state);
let values: Vec<f32> = monte.estimate_equity(1_000).into_iter().collect();
let to_act_idx = game_state.to_act_idx();
let my_value = values.get(to_act_idx).unwrap_or(&0.0_f32) * expected_pot;
let bet_already = game_state.current_round_player_bet(to_act_idx);
let to_call = game_state.current_round_bet();
let needed = to_call - bet_already;
if my_value < needed {
AgentAction::Fold
} else {
self.random_action(game_state, my_value)
}
}
fn random_action(&self, game_state: &GameState, max_value: f32) -> AgentAction {
let mut rng = rng();
let round_data = &game_state.round_data;
let raise_count = round_data.total_raise_count;
let call_idx = raise_count.min((self.percent_call.len() - 1) as u8) as usize;
let percent_call = self.percent_call.get(call_idx).map_or_else(|| 1.0, |v| *v);
if rng.random_bool(percent_call) {
AgentAction::Bet(round_data.bet)
} else {
let min_raise = round_data.min_raise;
let low = round_data.bet + min_raise;
let bet_value = rng.random_range(low..max_value.max(low + min_raise));
AgentAction::Bet(bet_value)
}
}
pub fn new(percent_call: Vec<f64>) -> Self {
Self { percent_call }
}
}
impl Agent for RandomPotControlAgent {
fn act(&mut self, _id: u128, game_state: &GameState) -> AgentAction {
let clean_hands = self.clean_hands(game_state);
if let Ok(monte) = MonteCarloGame::new(clean_hands) {
self.monte_carlo_based_action(game_state, monte)
} else {
AgentAction::Fold
}
}
}
#[cfg(test)]
mod tests {
use crate::{
arena::{
HoldemSimulationBuilder,
test_util::{assert_valid_game_state, assert_valid_round_data},
},
core::Deck,
};
use super::*;
#[test_log::test]
fn test_random_five_nl() {
let mut deck: Deck = Deck::default();
let mut rng = rand::rng();
let stacks = vec![100.0; 5];
let mut game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0);
let agents: Vec<Box<dyn Agent>> = vec![
Box::<RandomAgent>::default(),
Box::<RandomAgent>::default(),
Box::<RandomAgent>::default(),
Box::<RandomAgent>::default(),
Box::<RandomAgent>::default(),
];
for hand in game_state.hands.iter_mut() {
hand.insert(deck.deal(&mut rng).unwrap());
hand.insert(deck.deal(&mut rng).unwrap());
}
let mut sim = HoldemSimulationBuilder::default()
.game_state(game_state)
.agents(agents)
.deck(deck)
.build()
.unwrap();
sim.run(&mut rng);
let min_stack = sim
.game_state
.stacks
.clone()
.into_iter()
.reduce(f32::min)
.unwrap();
let max_stack = sim
.game_state
.stacks
.clone()
.into_iter()
.reduce(f32::max)
.unwrap();
assert_ne!(min_stack, max_stack, "There should have been some betting.");
assert_valid_round_data(&sim.game_state.round_data);
assert_valid_game_state(&sim.game_state);
}
#[test_log::test]
fn test_five_pot_control() {
let stacks = vec![100.0; 5];
let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0);
let agents: Vec<Box<dyn Agent>> = vec![
Box::new(RandomPotControlAgent::new(vec![0.3])),
Box::new(RandomPotControlAgent::new(vec![0.3])),
Box::new(RandomPotControlAgent::new(vec![0.3])),
Box::new(RandomPotControlAgent::new(vec![0.3])),
Box::new(RandomPotControlAgent::new(vec![0.3])),
];
let mut rng = rand::rng();
let mut sim = HoldemSimulationBuilder::default()
.game_state(game_state)
.agents(agents)
.build()
.unwrap();
sim.run(&mut rng);
let min_stack = sim
.game_state
.stacks
.clone()
.into_iter()
.reduce(f32::min)
.unwrap();
let max_stack = sim
.game_state
.stacks
.clone()
.into_iter()
.reduce(f32::max)
.unwrap();
assert_ne!(min_stack, max_stack, "There should have been some betting.");
assert_valid_round_data(&sim.game_state.round_data);
assert_valid_game_state(&sim.game_state);
}
#[test_log::test]
fn test_random_agents_no_fold_get_all_rounds() {
let stacks = vec![100.0; 5];
let game_state = GameState::new_starting(stacks, 10.0, 5.0, 0.0, 0);
let agents: Vec<Box<dyn Agent>> = vec![
Box::new(RandomAgent::new(vec![0.0], vec![0.75])),
Box::new(RandomAgent::new(vec![0.0], vec![0.75])),
Box::new(RandomAgent::new(vec![0.0], vec![0.75])),
Box::new(RandomAgent::new(vec![0.0], vec![0.75])),
Box::new(RandomAgent::new(vec![0.0], vec![0.75])),
];
let mut rng = rand::rng();
let mut sim = HoldemSimulationBuilder::default()
.agents(agents)
.game_state(game_state)
.build()
.unwrap();
sim.run(&mut rng);
assert!(sim.game_state.is_complete());
assert_valid_game_state(&sim.game_state);
}
}