diff --git a/src/game/evaluation.rs b/src/game/evaluation.rs index eddff1e..3e5fdc7 100644 --- a/src/game/evaluation.rs +++ b/src/game/evaluation.rs @@ -1,3 +1,7 @@ +use std::fmt::Display; +use std::io::Write; + +use colored::Colorize; use libpt::cli::console::{style, StyledObject}; use crate::wlist::word::Word; @@ -15,21 +19,6 @@ pub struct Evaluation { } impl Evaluation { - /// Display the evaluation color coded - pub fn colorized_display(&self) -> Vec> { - let mut buf = Vec::new(); - 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 { - c = c.yellow(); - } - buf.push(c); - } - 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. /// @@ -108,3 +97,20 @@ impl From<&Evaluation> for Word { Word::from(value.inner.iter().map(|v| v.0).collect::()) } } + +impl Display for Evaluation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for s in &self.inner { + write!( + f, + "{}", + match s.1 { + Status::None => s.0.to_string().into(), + Status::Exists => s.0.to_string().yellow(), + Status::Matched => s.0.to_string().green(), + } + )?; + } + std::fmt::Result::Ok(()) + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs index ead7ee9..ee15947 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -29,7 +29,6 @@ where solution: Option, wordlist: &'wl WL, responses: Vec, - // TODO: keep track of the letters the user has tried } impl<'wl, WL: WordList> Game<'wl, WL> { @@ -127,20 +126,60 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// Generates an [Evaluation] for a given solution and guess. pub(crate) fn evaluate(solution: WordData, guess: &Word) -> Evaluation { - let mut evaluation = Vec::new(); + let solution = solution.0; + let mut evaluation: Vec<(char, Status)> = vec![('!', Status::None); solution.len()]; let mut status: Status; - let mut buf = solution.0.clone(); - for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.0.chars()) { + let mut buf: Vec = solution.chars().collect(); + + #[cfg(debug_assertions)] + let buflen = solution.len(); + #[cfg(debug_assertions)] + { + assert_eq!(buflen, buf.len()); + assert_eq!(buflen, evaluation.len()); + } + + // first the correct solutions + for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.chars()) { if c_guess == c_sol { status = Status::Matched; - buf.replace_range(idx..idx + 1, "_"); - } else if buf.contains(c_guess) { - status = Status::Exists; - buf = buf.replacen(c_guess, "_", 1); - } else { - status = Status::None + buf[idx] = '!'; + evaluation[idx] = (c_guess, status); } - evaluation.push((c_guess, status)); + } + + #[cfg(debug_assertions)] + { + assert_eq!(buflen, buf.len()); + assert_eq!(buflen, evaluation.len()); + } + + // then check if the char exists, but was not guessed to be at the correct position + // + // We split this up, because finding the "exists" chars at the same time as the "correct" + // chars causes bugs + for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.chars()) { + if c_guess == c_sol { + continue; + } else if buf.contains(&c_guess) { + status = Status::Exists; + // replace that char in the buffer to signal that is has been paired with the + // current char + let idx_of_a_match = buf + .iter() + .position(|c| *c == c_guess) + .expect("did not find a character in a string even though we know it exists"); + buf[idx_of_a_match] = '!'; + } else { + status = Status::None; + } + evaluation[idx] = (c_guess, status); + } + + #[cfg(debug_assertions)] + { + assert_eq!(buflen, buf.len()); + assert_eq!(buflen, evaluation.len()); } evaluation.into() } @@ -359,16 +398,8 @@ impl<'wl, WL: WordList> Display for Game<'wl, WL> { self.step(), self.solution(), )?; - for s in self - .responses() - .iter() - .map(|v| v.evaluation().to_owned().colorized_display()) - { - write!(f, "\"")?; - for si in s { - write!(f, "{si}")?; - } - write!(f, "\", ")?; + for s in self.responses() { + write!(f, "\"{s}\",")?; } Ok(()) } diff --git a/src/game/response.rs b/src/game/response.rs index 8b4bd00..01aac99 100644 --- a/src/game/response.rs +++ b/src/game/response.rs @@ -92,17 +92,6 @@ impl GuessResponse { impl Display for GuessResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for s in self.evaluation.clone().into_iter() { - write!( - f, - "{}", - match s.1 { - Status::None => s.0.to_string().into(), - Status::Exists => s.0.to_string().yellow(), - Status::Matched => s.0.to_string().green(), - } - )?; - } - std::fmt::Result::Ok(()) + write!(f, "{}", self.evaluation()) } } diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 7d72f33..51bcd9d 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -3,15 +3,16 @@ use std::collections::HashMap; use libpt::log::{debug, error, info, trace}; use crate::error::{SolverError, WResult}; -use crate::game::evaluation::Evaluation; +use crate::game::evaluation::{Evaluation, EvaluationUnit}; +use crate::game::response::Status; use crate::wlist::word::{Word, WordData}; use crate::wlist::WordList; -use super::{AnyBuiltinSolver, Solver, Status}; - mod states; use states::*; +use super::{AnyBuiltinSolver, Solver}; + #[derive(Debug, Clone)] pub struct NaiveSolver<'wl, WL> { wl: &'wl WL, @@ -37,9 +38,10 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { let mut state: SolverState = SolverState::new(); let responses = game.responses().iter().enumerate(); for (_idx, response) in responses { - let mut already_found_amounts: HashMap = HashMap::new(); + let mut abs_freq: HashMap = HashMap::new(); let evaluation: &Evaluation = response.evaluation(); for (idx, p) in evaluation.clone().into_iter().enumerate() { + state.start_step(); match p.1 { Status::Matched => { pattern.replace_range(idx..idx + 1, &p.0.to_string()); @@ -49,7 +51,7 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { .entry(p.0) .or_insert(CharInfo::new(game.length())) .found_at(idx); - *already_found_amounts.entry(p.0).or_default() += 1; + *abs_freq.entry(p.0).or_default() += 1; } Status::Exists => { let cinfo = state @@ -57,16 +59,19 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { .entry(p.0) .or_insert(CharInfo::new(game.length())); cinfo.tried_but_failed(idx); - *already_found_amounts.entry(p.0).or_default() += 1; - cinfo.min_occurences(already_found_amounts[&p.0]); + *abs_freq.entry(p.0).or_default() += 1; + } + Status::None => { + let cinfo = state + .char_map_mut() + .entry(p.0) + .or_insert(CharInfo::new(game.length())); + cinfo.tried_but_failed(idx); + abs_freq.entry(p.0).or_default(); } - Status::None => state - .char_map_mut() - .entry(p.0) - .or_insert(CharInfo::new(game.length())) - .max_occurences(*already_found_amounts.entry(p.0).or_default()), } - trace!("absolute frequencies: {already_found_amounts:?}"); + trace!("absolute frequencies: {abs_freq:?}"); + state.finish_step(&abs_freq); } } @@ -87,6 +92,9 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { .filter(|solution_candidate| { if !game.responses().is_empty() && !state.has_all_known_contained(&solution_candidate.0) + // we need these sometimes, + // because we can't just input gibberish + //&& !state.has_wrong_chars(&solution_candidate.0) { trace!("known cont:{:#?}", state.get_all_known_contained()); return false; diff --git a/src/solve/naive/states.rs b/src/solve/naive/states.rs index f7ee00d..6b92c89 100644 --- a/src/solve/naive/states.rs +++ b/src/solve/naive/states.rs @@ -33,10 +33,17 @@ impl SolverState { &mut self.char_map } + pub(crate) fn get_all_known_bad(&self) -> Vec<(&char, &CharInfo)> { + self.char_map + .iter() + .filter(|(_key, value)| value.not_in_solution()) + .collect() + } + pub(crate) fn get_all_known_contained(&self) -> Vec<(&char, &CharInfo)> { self.char_map .iter() - .filter(|(key, value)| value.part_of_solution()) + .filter(|(_key, value)| value.known_part_of_solution()) .collect() } @@ -48,6 +55,37 @@ impl SolverState { } true } + + pub(crate) fn start_step(&mut self) {} + + pub(crate) fn finish_step(&mut self, abs_freq: &HashMap) { + for (k, v) in abs_freq { + if *v == 0 { + self.char_map + .get_mut(k) + .expect( + "char in abs_freq was not added to the char_map before finalizing the step", + ) + .max_occurences(0); + } else { + self.char_map + .get_mut(k) + .expect( + "char in abs_freq was not added to the char_map before finalizing the step", + ) + .min_occurences(*v); + } + } + } + + pub(crate) fn has_wrong_chars(&self, guess: &Word) -> bool { + for needed_char in self.get_all_known_bad() { + if guess.contains(*needed_char.0) { + return false; + } + } + true + } } impl CharInfo { @@ -77,7 +115,12 @@ impl CharInfo { } #[must_use] - pub fn part_of_solution(&self) -> bool { + pub fn not_in_solution(&self) -> bool { + self.occurences_amount.end == 0 + } + + #[must_use] + pub fn known_part_of_solution(&self) -> bool { self.occurences_amount.start > 0 && self.occurences_amount.end > 0 } @@ -129,7 +172,7 @@ impl CharInfo { impl Debug for CharInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.part_of_solution() { + if !self.not_in_solution() { f.debug_struct("CharInfo") .field("correct_idxs", &self.confirmed_indexes) .field("amnt_occ", &self.occurences_amount) diff --git a/tests/game.rs b/tests/game.rs new file mode 100644 index 0000000..bb8ca0e --- /dev/null +++ b/tests/game.rs @@ -0,0 +1,135 @@ +use test_log::test; // set the log level with an envvar: `RUST_LOG=trace cargo test` + +use libpt::log::info; +use wordle_analyzer::game::evaluation::Evaluation; +use wordle_analyzer::wlist::builtin::BuiltinWList; +use wordle_analyzer::wlist::WordList; + +use wordle_analyzer::wlist::word::Word; +use wordle_analyzer::{self, game}; + +fn wordlist() -> impl WordList { + BuiltinWList::default() +} + +#[test] +fn test_eval_simple() -> anyhow::Result<()> { + let wl = wordlist(); + let builder = game::Game::builder(&wl) + .length(5) + .max_steps(6) + .solution(Some(wl.get_word(&Word::from("crate")).unwrap())) + .precompute(false); + + let mut game = builder.build()?; + let guess = Word::from("slate"); + game.guess(&guess, None)?; + let correct = Evaluation::build(&guess, "xxccc")?; + info!( + "{} =? {}", + *game.last_response().unwrap().evaluation(), + correct + ); + assert_eq!(*game.last_response().unwrap().evaluation(), correct); + + let mut game = builder.build()?; + let guess = Word::from("about"); + game.guess(&guess, None)?; + let correct = Evaluation::build(&guess, "fxxxf")?; + info!( + "{} =? {}", + *game.last_response().unwrap().evaluation(), + correct + ); + assert_eq!(*game.last_response().unwrap().evaluation(), correct); + + Ok(()) +} + +#[test] +fn test_eval_reoccuring_char0() -> anyhow::Result<()> { + let wl = wordlist(); + let builder = game::Game::builder(&wl) + .solution(Some(wl.get_word(&Word::from("nines")).unwrap())) + .precompute(false); + info!("solution=nines"); + + let mut game = builder.build()?; + let guess = Word::from("pines"); + game.guess(&guess, None)?; + let correct = Evaluation::build(&guess, "xcccc")?; + info!( + "{} =? {}", + *game.last_response().unwrap().evaluation(), + correct + ); + assert_eq!(*game.last_response().unwrap().evaluation(), correct); + + let mut game = builder.build()?; + let guess = Word::from("sides"); + game.guess(&guess, None)?; + let correct = Evaluation::build(&guess, "xcxcc")?; + info!( + "{} =? {}", + *game.last_response().unwrap().evaluation(), + correct + ); + assert_eq!(*game.last_response().unwrap().evaluation(), correct); + + let mut game = builder.build()?; + let guess = Word::from("ninja"); + game.guess(&guess, None)?; + let correct = Evaluation::build(&guess, "cccxx")?; + info!( + "{} =? {}", + *game.last_response().unwrap().evaluation(), + correct + ); + assert_eq!(*game.last_response().unwrap().evaluation(), correct); + + let mut game = builder.build()?; + let guess = Word::from("which"); + game.guess(&guess, None)?; + let correct = Evaluation::build(&guess, "xxfxx")?; + info!( + "{} =? {}", + *game.last_response().unwrap().evaluation(), + correct + ); + assert_eq!(*game.last_response().unwrap().evaluation(), correct); + + let mut game = builder.build()?; + let guess = Word::from("indie"); + game.guess(&guess, None)?; + let correct = Evaluation::build(&guess, "ffxxf")?; + info!( + "{} =? {}", + *game.last_response().unwrap().evaluation(), + correct + ); + assert_eq!(*game.last_response().unwrap().evaluation(), correct); + + Ok(()) +} + +#[test] +fn test_eval_reoccuring_char1() -> anyhow::Result<()> { + let wl = wordlist(); + let builder = game::Game::builder(&wl) + .solution(Some(wl.get_word(&Word::from("fatty")).unwrap())) + .precompute(false); + info!("solution=fatty"); + + let mut game = builder.build()?; + let guess = Word::from("state"); + game.guess(&guess, None)?; + let correct = Evaluation::build(&guess, "xffcx")?; + info!( + "{} =? {}", + *game.last_response().unwrap().evaluation(), + correct + ); + assert_eq!(*game.last_response().unwrap().evaluation(), correct); + + Ok(()) +} diff --git a/tests/solver.rs b/tests/solver.rs index 2b9ef82..18c27b8 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -1,9 +1,12 @@ use test_log::test; // set the log level with an envvar: `RUST_LOG=trace cargo test` +use wordle_analyzer::game::Game; use wordle_analyzer::solve::{AnyBuiltinSolver, NaiveSolver, Solver, StupidSolver}; use wordle_analyzer::wlist::builtin::BuiltinWList; use wordle_analyzer::wlist::WordList; +use rayon::prelude::*; + fn wordlist() -> impl WordList { BuiltinWList::default() } @@ -16,3 +19,19 @@ fn test_build_builtin_solvers() { let _naive_solver = AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); } + +#[test] +fn test_naive_win_games() -> anyhow::Result<()> { + let wl = wordlist(); + let sl = + AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); + let builder = Game::builder(&wl); + + { 0..50 }.into_par_iter().for_each(|_round| { + let mut game = builder.build().expect("could not make game"); + sl.play(&mut game).expect("could not play game"); + assert!(game.finished()); + assert!(game.won()); + }); + Ok(()) +}