ncpig 0.6.1

Non-Cooperative Perfect Information Games, and algorithms to play them.
Documentation
//! Have [`search`] algorithms compete against each other in a [`Game`].

use std::fmt::{Debug, Display};

#[cfg(doc)]
use crate::game::State;
use crate::game::{Game, GameError};
#[cfg(doc)]
use crate::search;
use crate::search::{SearchChoose, SearchError};

/// Errors that could occur in a [`Competition`].
#[derive(Debug, thiserror::Error)]
pub enum CompetitionError<const N: usize, G: Game<N>> {
    /// Error occured in the [`Game`].
    #[error(transparent)]
    GameError(G::Error),

    /// Error occured in one of the [`SearchChoose`] algorithms.
    #[error("{0}")]
    SearchError(Box<dyn SearchError + Send + Sync>),

    /// No actions to take
    #[error("No actions are available to take")]
    NoActions,
}

/// Manually implement the conversion from [`Game::Error`] to [`CompetitionError`].
impl<const N: usize, G: Game<N, Error = E>, E: GameError> From<E> for CompetitionError<N, G> {
    fn from(value: G::Error) -> Self {
        Self::GameError(value)
    }
}

/// Allow a [`SearchChoose`] (or something else) to be used in a [`Competition`].
pub trait CompetitionStrategy<const N: usize, G: Game<N>>: Debug {
    /// Take a turn given the current strategy.
    fn take_turn(&self, game: &G, state: G::State) -> Result<G::State, CompetitionError<N, G>>;
}

/// Blanket implement [`CompetitionStrategy`] for all [`SearchChoose`]es than can be.
impl<const N: usize, G: Game<N>, T: SearchChoose<N, G> + Debug> CompetitionStrategy<N, G> for T
where
    <T as SearchChoose<N, G>>::Error: Send + Sync + 'static,
    G::Action: Debug,
{
    fn take_turn(&self, game: &G, state: G::State) -> Result<G::State, CompetitionError<N, G>> {
        let action = self
            .choose_action(game, &state)
            .map_err(|err| CompetitionError::SearchError(Box::new(err)))?
            .ok_or(CompetitionError::NoActions)?;
        log::debug!("Strategy has determined action {:?}", action);
        Ok(game.do_action(&action, state)?)
    }
}

/// Pit different strategies against each other in an `N`-player [`Game`].
pub struct Competition<'a, const N: usize, G: Game<N>> {
    /// The game being played.
    game: &'a G,
    /// The strategies for each player, in the same order as [`Game.players`].
    strategies: [&'a dyn CompetitionStrategy<N, G>; N],
    /// Whether to display the [`State`] of the game at each turn.
    display_state: bool,
}

impl<'a, const N: usize, G: Game<N>> Competition<'a, N, G> {
    /// Create a new [`Competition`].
    pub fn new(
        game: &'a G,
        strategies: [&'a dyn CompetitionStrategy<N, G>; N],
        display_state: bool,
    ) -> Self {
        Self {
            game,
            strategies,
            display_state,
        }
    }

    /// Let the players play.
    ///
    /// The [`State`] will be mutated and can be inspected afterwards to determined the
    /// winner and/or other info.
    pub fn play(&'a self, state: G::State) -> Result<G::State, CompetitionError<N, G>>
    where
        G::State: Display,
    {
        let mut state = state;
        log::info!("Starting competition");
        while !self.game.is_complete(&state)? {
            if self.display_state {
                println!("{}\n", state);
            }
            let current_player = self.game.active_player(&state)?;

            let current_player_index = self.game.player_index(current_player)?;
            log::trace!(
                "Player {:?} has player index {}",
                current_player,
                current_player_index
            );

            let strategy = &self.strategies[current_player_index];
            log::info!(
                "Player {:?} taking turn with strategy {:?}",
                current_player,
                strategy
            );

            state = strategy.take_turn(self.game, state)?;
        }
        log::info!("Competition is complete because game is over");
        if self.display_state {
            println!("{}\n", state);
        }
        Ok(state)
    }
}