diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 084db61..fdfa775 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -8,12 +8,13 @@ use libpt::cli::{repl::Repl, strum}; use libpt::log::*; use strum::EnumIter; +use wordle_analyzer::error::Error; use wordle_analyzer::game::evaluation::Evaluation; use wordle_analyzer::game::response::GuessResponse; use wordle_analyzer::solve::{BuiltinSolverNames, Solver}; use wordle_analyzer::wlist::builtin::BuiltinWList; -use wordle_analyzer::wlist::word::Word; +use wordle_analyzer::wlist::word::{Word, WordData}; use wordle_analyzer::wlist::WordList; use wordle_analyzer::{self, game}; @@ -33,12 +34,22 @@ struct Cli { #[command(flatten)] verbose: libpt::cli::args::VerbosityLevel, /// which solver to use - #[arg(short, long, default_value_t = BuiltinSolverNames::default())] + #[arg(long, default_value_t = BuiltinSolverNames::default())] solver: BuiltinSolverNames, /// set if the solver should play a full native game without interaction #[arg(short, long)] non_interactive: bool, + + // FIXME: line breaks don't work correctly in the cli help + // + /// Solution for the game + /// + /// This will only be used when non-interactive is used. You can use this option to see how the + /// selected solver behaves when trying to guess a specific solution, which can help reproduce + /// behavior. + #[arg(short, long)] + solution: Option, } #[derive(Subcommand, Debug, EnumIter, Clone)] @@ -184,10 +195,22 @@ fn wlcommand_handler(_cli: &Cli, cmd: &WlCommand, wl: &impl WordList) -> anyhow: fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> { let wl = BuiltinWList::default(); - let builder = game::Game::builder(&wl) + let mut builder = game::Game::builder(&wl) .length(cli.length) .max_steps(cli.max_steps) .precompute(cli.precompute); + if cli.solution.is_some() { + let solw: Word = cli.solution.unwrap(); + let sol = wl.get_word(&solw); + if sol.is_none() { + eprintln!("the requested solution \"{solw}\" is not in the wordlist"); + return Err(Error::GameError { + source: wordle_analyzer::error::GameError::WordNotInWordlist(solw), + } + .into()); + } + builder = builder.solution(sol); + } let solver = cli.solver.to_solver(&wl); let mut game = builder.build()?; diff --git a/src/error.rs b/src/error.rs index 88b34f9..5ab936a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -49,7 +49,7 @@ pub enum GameError { GuessHasWrongLength(usize), #[error("The game is finished but a guess is being made")] TryingToPlayAFinishedGame, - #[error("Tried to guess a word that is not in the wordlist ({0})")] + #[error("Tried to guess or use a word that is not in the wordlist ({0})")] WordNotInWordlist(Word), } diff --git a/src/game/mod.rs b/src/game/mod.rs index e1d8199..0074086 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -214,13 +214,14 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// # } /// ``` /// -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq)] pub struct GameBuilder<'wl, WL: WordList> { length: usize, precompute: bool, max_steps: usize, wordlist: &'wl WL, generate_solution: bool, + solution: Option, } impl<'wl, WL: WordList> GameBuilder<'wl, WL> { @@ -234,19 +235,23 @@ impl<'wl, WL: WordList> GameBuilder<'wl, WL> { max_steps: super::DEFAULT_MAX_STEPS, wordlist: wl, generate_solution, + solution: None, } } /// build a [`Game`] with the stored configuration pub fn build(&'wl self) -> GameResult> { trace!("{:#?}", self); - let game: Game = Game::build( + let mut game: Game = Game::build( self.length, self.precompute, self.max_steps, self.wordlist, self.generate_solution, )?; + if self.solution.is_some() { + game.set_solution(self.solution.clone()) + } Ok(game) } @@ -284,6 +289,22 @@ impl<'wl, WL: WordList> GameBuilder<'wl, WL> { self.wordlist = wl; self } + + /// Set the solution for the games built by the builder + /// + /// If this is [Some], then the solution generated by + /// [generate_solution](Self::generate_solution) will be overwritten (if it + /// is true). + /// + /// If [generate_solution](Self::generate_solution) is false and this method is not used, the + /// game will not have a predetermined solution and will not be able to generate evaluations + /// for guesses, so these will need to be added manually by the user. The intention is that + /// this can be used for use cases where the user plays wordle not within wordle-analyzer but + /// in another program (like their browser). It can also be used to test solvers. + pub fn solution(mut self, solution: Option) -> Self { + self.solution = solution; + self + } } impl<'wl, WL: WordList> Display for Game<'wl, WL> { diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 2de11b0..2bd8fa0 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -21,6 +21,10 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { let mut pattern: String = String::from("....."); let mut other_chars: Vec = Vec::new(); let response = game.last_response(); + trace!( + "guessing best guess for last response: {response:#?}\n{:#?}", + response.map(|a| a.evaluation()) + ); if response.is_some() { for (idx, p) in response .unwrap() @@ -37,10 +41,11 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { } } trace!("other chars: {:?}", other_chars); - let matches: Vec = game - .wordlist() - .get_words_matching(pattern) - .expect("the solution does not exist in the wordlist") + let mut matches: Vec = game.wordlist().get_words_matching(pattern)?; + if matches.is_empty() { + return Err(SolverError::NoMatches.into()); + } + matches = matches .iter() // only words that have not been guessed yet .filter(|p| !game.made_guesses().contains(&&p.0))