ncpig 0.6.1

Non-Cooperative Perfect Information Games, and algorithms to play them.
Documentation
//! A [`SearchChoose`] strategy which allows the user to input the next [`Action`] via stdin.

use std::error::Error;
use std::fmt::Debug;
use std::io::{self, Write};
use std::str::FromStr;

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

/// Errors related to the [`UserInput`] [`SearchChoose`].
#[derive(Debug, thiserror::Error)]
pub enum UserInputError<const N: usize, G: Game<N>>
where
    G::Action: FromStr + Debug,
    <G::Action as FromStr>::Err: Error,
{
    /// Error returned in the stdin/stdout IO.
    #[error(transparent)]
    IoError(#[from] io::Error),

    /// Error parsing an [`Action`].
    #[error(transparent)]
    ParseError(<G::Action as FromStr>::Err),

    /// Error returned by the [`Game`].
    #[error(transparent)]
    GameError(G::Error),

    /// The provided action is not valid at the current [`Game`] [`State`].
    #[error("Action {0:?} is not valid at the current game state")]
    InvalidGameAction(G::Action),
}

impl<const N: usize, G: Game<N>> SearchError for UserInputError<N, G>
where
    G::Action: FromStr + Debug,
    <G::Action as FromStr>::Err: Error,
{
}

impl<const N: usize, G: Game<N, Error = E>, E: GameError> From<E> for UserInputError<N, G>
where
    G::Action: FromStr + Debug,
    <G::Action as FromStr>::Err: Error,
{
    fn from(value: G::Error) -> Self {
        Self::GameError(value)
    }
}

/// A "search" algorithm that prompts the user for input on which [`Action`] to take.
#[derive(Debug)]
pub struct UserInput {
    /// The prompt to show when the user is asked to select an [`Action`].
    prompt: String,
    /// The number of retries a user is given in case of an error.
    ///
    /// Retries are allowed if (1) there is an error parsing the string to an [`Action`]
    /// or (2) the parsed [`Action`] is not valid at the current [`Game`] [`State`].
    parse_retries: usize,
}

impl Default for UserInput {
    fn default() -> Self {
        Self {
            prompt: "User input".into(),
            parse_retries: 0,
        }
    }
}

impl UserInput {
    /// Initialize a new user input.
    pub fn new(prompt: impl ToString, parse_retries: usize) -> Self {
        Self {
            prompt: prompt.to_string(),
            parse_retries,
        }
    }

    fn parse_action<const N: usize, G: Game<N>>(
        &self,
        retry: usize,
        game: &G,
        state: &G::State,
    ) -> Result<<G as Game<N>>::Action, UserInputError<N, G>>
    where
        G::Action: FromUserInput + PartialEq + Debug,
        <G::Action as FromStr>::Err: Error,
    {
        let mut buffer = String::new();
        let stdin = io::stdin();
        let active_player = game.active_player(state)?;
        if let Some(hint) = <G::Action as FromUserInput>::HINT {
            print!("{} [{active_player:?}] ({}): ", self.prompt, hint);
        } else {
            print!("{} [{active_player:?}]: ", self.prompt);
        }
        io::stdout().flush()?;
        stdin.read_line(&mut buffer)?;
        let input = buffer.trim_end();

        let action = match input.parse() {
            Ok(action) => {
                if game.available_actions(state)?.contains(&action) {
                    Ok(action)
                } else {
                    log::warn!(
                        "Parsed action {:?}, but this is not a valid action at this game state",
                        action
                    );
                    Err(UserInputError::InvalidGameAction(action))
                }
            }
            Err(e) => {
                log::warn!("Unable to parse {} to expected action format", input);
                Err(UserInputError::ParseError(e))
            }
        };

        if action.is_err() && retry < self.parse_retries {
            log::info!("Giving the user another opportunity to input");
            self.parse_action::<N, G>(retry + 1, game, state)
        } else {
            action
        }
    }
}

impl<const N: usize, G: Game<N>> SearchChoose<N, G> for UserInput
where
    G::Action: FromUserInput + Debug + PartialEq,
    <G::Action as FromStr>::Err: Error,
{
    type Error = UserInputError<N, G>;

    fn choose_action(
        &self,
        game: &G,
        state: &G::State,
    ) -> Result<Option<G::Action>, UserInputError<N, G>> {
        let action = self.parse_action::<N, G>(0, game, state)?;
        Ok(Some(action))
    }
}

/// An [`Action`] must implement this for the [`Game`] to be usable with [`UserInput`].
pub trait FromUserInput: FromStr {
    /// An optional hint to provide the user with about the expected format for successful parsing.
    ///
    /// This will be included with the prompt if provided.
    const HINT: Option<&'static str> = None;
}