rs_poker 5.0.0

A library to help with any Rust code dealing with poker. This includes card values, suits, hands, hand ranks, 5 card hand strength calculation, 7 card hand strength calulcation, and monte carlo game simulation helpers.
Documentation
use async_trait::async_trait;
use tracing::trace;

use crate::arena::{GameState, action::Action};

use super::{Historian, HistorianError};

/// This `Agent` is an implmentation that returns
/// random actions. However, it also takes in a function
/// that is called when an action is received. This is
/// useful for testing and debugging.
#[derive(Debug, Clone)]
pub struct FnHistorian<F> {
    func: F,
}

impl<F: Fn(u128, &GameState, &Action) -> Result<(), HistorianError>> FnHistorian<F> {
    /// Create a new `FnHistorian` with the provided function
    /// that will be called when an action is received on a simulation.
    pub fn new(f: F) -> Self {
        Self { func: f }
    }
}

#[async_trait]
impl<F: Clone + Send + Fn(u128, &GameState, &Action) -> Result<(), HistorianError>> Historian
    for FnHistorian<F>
{
    async fn record_action(
        &mut self,
        id: u128,
        game_state: &GameState,
        action: &Action,
    ) -> Result<(), HistorianError> {
        trace!(id, ?action, "FnHistorian invoking closure");
        // Call the function with the action that was received
        (self.func)(id, game_state, action)
    }
}

#[cfg(test)]
mod tests {
    use std::sync::{Arc, Mutex};

    use crate::arena::{Agent, HoldemSimulationBuilder, agent::RandomAgent, game_state::Round};

    use super::*;
    use crate::arena::GameStateBuilder;

    #[tokio::test]
    async fn test_can_record_actions_with_agents() {
        let last_action: Arc<Mutex<Option<Action>>> = Arc::new(Mutex::new(None));
        let count = Arc::new(Mutex::new(0));

        let agents: Vec<Box<dyn Agent>> = (0..2)
            .map(|_| Box::<RandomAgent>::default() as Box<dyn Agent>)
            .collect();
        let game_state = GameStateBuilder::new()
            .stacks(vec![100.0, 100.0])
            .blinds(10.0, 5.0)
            .build()
            .unwrap();

        let borrow_count = count.clone();
        let borrow_last_action = last_action.clone();

        let historian = Box::new(FnHistorian::new(move |_id, _game_state, action| {
            *borrow_count.lock().unwrap() += 1;
            *borrow_last_action.lock().unwrap() = Some(action.clone());
            Ok(())
        }));

        let mut sim = HoldemSimulationBuilder::default()
            .agents(agents)
            .game_state(game_state)
            .historians(vec![historian])
            .build()
            .unwrap();

        sim.run().await;

        assert_ne!(0, *count.lock().unwrap());

        let act = last_action.lock().unwrap().clone();

        assert!(act.is_some());

        assert_eq!(Some(Action::RoundAdvance(Round::Complete)), act);
    }

    #[tokio::test]
    async fn test_fn_historian_can_withstand_error() {
        // A test that adds a historian that always returns an error
        // This shows that the historian will be dropped from the simulation
        // if it returns an error but the simulation will continue to run.

        let agents: Vec<Box<dyn Agent>> = (0..2)
            .map(|_| Box::<RandomAgent>::default() as Box<dyn Agent>)
            .collect();

        let game_state = GameStateBuilder::new()
            .stacks(vec![100.0, 100.0])
            .blinds(10.0, 5.0)
            .build()
            .unwrap();
        let historian = Box::new(FnHistorian::new(|_, _, _| {
            Err(HistorianError::UnableToRecordAction)
        }));

        HoldemSimulationBuilder::default()
            .agents(agents)
            .game_state(game_state)
            .historians(vec![historian])
            .panic_on_historian_error(false)
            .build()
            .unwrap()
            .run()
            .await;
    }
}