ncpig 0.6.1

Non-Cooperative Perfect Information Games, and algorithms to play them.
Documentation
//! Build a game that is compatible with the ncpig search algorithms.
//!
//! See [`ncpig_testing`](https://git.sr.ht/~ryguy/ncpig/tree/main/item/ncpig-testing) for numerous
//! examples.

use std::error::Error;
use std::fmt::Debug;

/// Errors that are game-agnostic.
#[derive(Debug, thiserror::Error)]
pub enum GeneralGameError {
    /// An unknown player was provided.
    #[error("Unknown player: {0}")]
    UnknownPlayer(String),
}

impl GameError for GeneralGameError {}

/// The trait that makes something a "game".
///
/// The game holds things that are constant about the game
/// i.e. the "rules".
///
/// It is generic over [`usize`] `N` representing the number
/// of players playing the game.
pub trait Game<const N: usize>: Debug {
    /// The [`State`] of the [`Game`] to be mutated.
    type State: State<N, Self>;
    /// A [`Player`] of the [`Game`].
    type Player: Player<N, Self> + Eq + Debug;
    /// An [`Action'] of the [`Game`].
    type Action: Action<N, Self>;
    /// The value that represents the score of a [`Game`].
    type Score: Debug;
    /// The error type returned throught tha [`Game`] methods.
    type Error: GameError;

    /// The players of the game.
    ///
    /// The order of the players should be constant on every call of this method.
    fn players(&self) -> &[Self::Player; N];

    /// Get the index of a player (i.e. the position a player is in [`Game.players`].
    fn player_index(&self, player: &Self::Player) -> Result<usize, Self::Error> {
        Ok(self
            .players()
            .iter()
            .position(|p| p == player)
            .ok_or_else(|| GeneralGameError::UnknownPlayer(format!("{:?}", player)))?)
    }

    /// Get the active player, given the state of the game.
    fn active_player(&self, state: &Self::State) -> Result<&Self::Player, Self::Error>;

    /// Get the index associated with the active player, at a given state.
    fn active_player_index(&self, state: &Self::State) -> Result<usize, Self::Error> {
        self.player_index(self.active_player(state)?)
    }

    /// Get the actions that the [`self.active_player`] has to choose from.
    fn available_actions(&self, state: &Self::State) -> Result<Vec<Self::Action>, Self::Error>;

    /// Perform an action by the [`self.active_player`] and mutate the [`State`] appropriately.
    fn do_action(
        &self,
        action: &Self::Action,
        state: Self::State,
    ) -> Result<Self::State, Self::Error>;

    /// The [`Score`](Game::Score) of a certain [`Player`] at a certain [`State`].
    fn score(&self, player: &Self::Player, state: &Self::State)
        -> Result<Self::Score, Self::Error>;

    /// Check if the game is over.
    fn is_complete(&self, state: &Self::State) -> Result<bool, Self::Error>;
}

/// Errors returned by [`Game`] methods are required to implement this.
pub trait GameError: Error + From<GeneralGameError> {}

/// The state of a [`Game`].
///
/// This is what changes as [`Player`]s perform [`Action`]s.
///
/// It is dependent on the game, but this might hold where pieces
/// are on the board, which cards have been played and are available
/// or a log of all actions taken bu all players up to this point.
pub trait State<const N: usize, G: Game<N> + ?Sized>: Debug {}

/// A player of a [`Game`].
pub trait Player<const N: usize, G: Game<N, Player = Self> + ?Sized> {}

/// A [`Player`]'s [`Heuristic`] is used to estimate the quality of an intermediate [`Game`] [`State`].
pub trait Heuristic<const N: usize, G: Game<N, Player = Self> + ?Sized>: Player<N, G> {
    /// The error type returned by the [`Heuristic`] method.
    type Error: Error;

    /// A function that indicates the quality of a certain [`State`] for this [`Player`].
    ///
    /// This is used by algorithms that cannot search the entire tree and need to evaluate a
    /// [`State`] before the [`Game`] is complete.
    fn heuristic(
        &self,
        game: &G,
        state: &<G as Game<N>>::State,
    ) -> Result<<G as Game<N>>::Score, Self::Error>;
}

/// An action that can be taken in a [`Game`].
///
/// This might be an enum representing all cards available in a [`Game`]
/// or an (x, y) tuple representing a space on a rectangular board.
pub trait Action<const N: usize, G: Game<N, Action = Self> + ?Sized> {}