From 18a512502884b2b42f583ace37ac1c134ae69259 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Thu, 25 Jul 2024 14:46:20 +0200 Subject: [PATCH] fix(evaluation): evaluation did not get built from string correctly #9 --- Cargo.toml | 4 +++ src/bench/mod.rs | 1 - src/bin/solve/simple.rs | 6 ++-- src/error.rs | 4 +++ src/game/evaluation.rs | 70 ++++++++++++++++++++++++++++------------- src/game/mod.rs | 2 +- src/solve/mod.rs | 1 + src/solve/naive/mod.rs | 9 ++++++ tests/solver.rs | 42 +++++++++++++++++++------ 9 files changed, 105 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7f7dd9b..44cc4a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ serde_json = { version = "1.0.114", optional = true } strum = "0.26.3" # serde_with = "3.7.0" thiserror = "1.0.58" +tracing-test = "0.2.5" [[bin]] name = "wordlec" @@ -57,3 +58,6 @@ required-features = ["solve", "cli", "builtin"] name = "wordlebench" path = "src/bin/bench/cli.rs" required-features = ["solve", "cli", "bench", "builtin"] + +[dev-dependencies] +test-log = { version = "0.2.16", default-features = false, features = ["color", "trace"] } diff --git a/src/bench/mod.rs b/src/bench/mod.rs index 1b251d3..be2dea4 100644 --- a/src/bench/mod.rs +++ b/src/bench/mod.rs @@ -45,7 +45,6 @@ where // TODO: add some interface to get reports while the benchmark runs // TODO: make the benchmark optionally multithreaded // NOTE: This is blocking, use start to let it run in another thread - // FIXME: this never stops? Reports just keep getting printed fn bench( &self, n: usize, diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index fdfa775..50d694b 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -75,7 +75,7 @@ enum ReplCommand { /// is correct Guess { your_guess: String, - evalutation: Evaluation, + evalutation: String, }, /// Let the solver make a guess Solve, @@ -163,7 +163,9 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { your_guess, evalutation, } => { - let guess = game.guess(your_guess, Some(evalutation)); + let evaluation_converted: Evaluation = + Evaluation::build(&your_guess, &evalutation)?; + let guess = game.guess(your_guess, Some(evaluation_converted)); debug!("your guess: {guess:?}"); if guess.is_err() { eprintln!("{}", style(guess.unwrap_err()).red().bold()); diff --git a/src/error.rs b/src/error.rs index 5ab936a..0b10b8c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -51,6 +51,10 @@ pub enum GameError { TryingToPlayAFinishedGame, #[error("Tried to guess or use a word that is not in the wordlist ({0})")] WordNotInWordlist(Word), + #[error("Invalid syntax for manual evaluation creation")] + InvalidEvaluationSyntax(String), + #[error("The length of guess and evaluation must be the same")] + GuessAndEvalNotSameLen((String, String)), } #[derive(Debug, Clone, Error)] diff --git a/src/game/evaluation.rs b/src/game/evaluation.rs index 1587e4a..fa4b1a0 100644 --- a/src/game/evaluation.rs +++ b/src/game/evaluation.rs @@ -1,25 +1,25 @@ -use std::convert::Infallible; -use std::str::FromStr; - use libpt::cli::console::{style, StyledObject}; use crate::wlist::word::Word; use super::response::Status; +use super::{GameError, WResult}; +/// the [char] of the guess and the [Status] associated with it pub type EvaluationUnit = (char, Status); +/// Basically a [String] with extra information associated with each char #[derive(Debug, Clone, PartialEq, Default)] pub struct Evaluation { inner: Vec, } impl Evaluation { - pub(crate) fn colorized_display(&self, guess: &Word) -> Vec> { - assert_eq!(guess.len(), self.inner.len()); + /// Display the evaluation color coded + pub fn colorized_display(&self) -> Vec> { let mut buf = Vec::new(); - for (i, e) in self.inner.iter().enumerate() { - let mut c = style(guess.chars().nth(i).unwrap().to_string()); + for e in self.inner.iter() { + let mut c = style(e.0.to_string()); if e.1 == Status::Matched { c = c.green(); } else if e.1 == Status::Exists { @@ -29,6 +29,45 @@ impl Evaluation { } buf } + + /// The first string is the word the evaluation is for, The second string defines how the + /// characters of the first string match the solution. + /// + /// + /// ## Evaluation Format: + /// + /// 'x' means wrong character + /// + /// 'p' means present character + /// + /// 'c' means correct character + /// + /// ### Example: + /// + /// 'xxxcc' --- means the first 3 chars are wrong but the second 2 chars are correct + /// + /// 'xppxc' --- means the first character is wrong, the next two characters are present, the last + /// is correct + /// + /// ## Example + /// + /// "wordle xxxcff" --- the guess was wordle, the d is in the correct spot, the solution + /// contains 'l' and 'e', but on another index. + /// + pub fn build(guess: &Word, eval_str: &str) -> WResult { + if guess.len() != eval_str.len() { + return Err(GameError::GuessAndEvalNotSameLen(( + guess.to_string(), + eval_str.to_string(), + )) + .into()); + } + let mut v: Vec = Vec::new(); + for (c, e) in guess.chars().zip(eval_str.chars()) { + v.push((c, Status::from(e))) + } + Ok(v.into()) + } } impl IntoIterator for Evaluation { @@ -46,19 +85,8 @@ impl From> for Evaluation { } } -impl From<&str> for Evaluation { - fn from(value: &str) -> Self { - Self::from_str(value).unwrap() - } -} - -impl FromStr for Evaluation { - type Err = Infallible; - fn from_str(s: &str) -> Result { - let mut v: Vec = Vec::new(); - for c in s.chars() { - v.push((c, Status::from(c))) - } - Ok(v.into()) +impl From for Word { + fn from(value: Evaluation) -> Self { + Word::from(value.inner.into_iter().map(|v| v.0).collect::()) } } diff --git a/src/game/mod.rs b/src/game/mod.rs index 0074086..d3dbde7 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -320,7 +320,7 @@ impl<'wl, WL: WordList> Display for Game<'wl, WL> { for s in self .responses() .iter() - .map(|v| v.evaluation().to_owned().colorized_display(v.guess())) + .map(|v| v.evaluation().to_owned().colorized_display()) { write!(f, "\"")?; for si in s { diff --git a/src/solve/mod.rs b/src/solve/mod.rs index 54842e7..9e34dbc 100644 --- a/src/solve/mod.rs +++ b/src/solve/mod.rs @@ -59,6 +59,7 @@ pub trait Solver<'wl, WL: WordList>: Clone + std::fmt::Debug + Sized + Sync { /// /// This function will return an error if [make_a_move](Solver::make_a_move) fails. fn play(&self, game: &mut Game<'wl, WL>) -> WResult { + // TODO: check if the game is finished already and return an Err if so let mut resp: GuessResponse; loop { resp = self.make_a_move(game)?; diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 2bd8fa0..f8cdee0 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -16,6 +16,15 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { info!("using naive solver"); Ok(Self { wl: wordlist }) } + /// Guess a word from the wordlist for the given game + /// + /// ## Algorithm + /// + /// * Look at the evaluation for the last response and keep the correct letters + /// * Get all words that have these letters at the right position + /// * Discard words that have already been tried + /// * Discard all words that don't have the chars that we know from the last guess are in the + /// word, but don't know the position of. fn guess_for(&self, game: &crate::game::Game) -> WResult { // HACK: hardcoded length let mut pattern: String = String::from("....."); diff --git a/tests/solver.rs b/tests/solver.rs index 339dc36..3aa2634 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -1,3 +1,6 @@ +use test_log::test; // set the log level with an envvar: `RUST_LOG=trace cargo test` + +use wordle_analyzer::game::evaluation::Evaluation; use wordle_analyzer::game::Game; use wordle_analyzer::solve::{AnyBuiltinSolver, NaiveSolver, Solver, StupidSolver}; use wordle_analyzer::wlist::builtin::BuiltinWList; @@ -69,7 +72,7 @@ fn test_naive_play_predetermined_game() -> anyhow::Result<()> { } #[test] -fn test_naive_play_predetermined_game_by_manual_guess_and_eval() -> anyhow::Result<()> { +fn test_naive_play_predetermined_game_manually() -> anyhow::Result<()> { let wl = wordlist(); let sl = AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); @@ -77,35 +80,56 @@ fn test_naive_play_predetermined_game_by_manual_guess_and_eval() -> anyhow::Resu // pretend that a user inputs guesses manually let mut game = Game::build(5, false, 6, &wl, false)?; let _actual_solution: Option = Some(("nines".into(), 0.002)); - let mut next_guess; + let mut next_guess: Word; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("which")); - game.guess(next_guess, Some("xxfxx".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xxfxx")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("their")); - game.guess(next_guess, Some("xxffx".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xxffx")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("being")); - game.guess(next_guess, Some("xfffx".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xfffx")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("since")); - game.guess(next_guess, Some("fcfxf".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "fcfxf")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("lines")); - game.guess(next_guess, Some("xcccc".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xcccc")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("mines")); - game.guess(next_guess, Some("xcccc".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xcccc")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("wines")); - game.guess(next_guess, Some("xcccc".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xcccc")?), + )?; // naive is at the moment too bad to solve "nines" assert!(game.finished());