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;
#[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(transparent)]
IoError(#[from] io::Error),
#[error(transparent)]
ParseError(<G::Action as FromStr>::Err),
#[error(transparent)]
GameError(G::Error),
#[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)
}
}
#[derive(Debug)]
pub struct UserInput {
prompt: String,
parse_retries: usize,
}
impl Default for UserInput {
fn default() -> Self {
Self {
prompt: "User input".into(),
parse_retries: 0,
}
}
}
impl UserInput {
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))
}
}
pub trait FromUserInput: FromStr {
const HINT: Option<&'static str> = None;
}