From 05197181d045ef10fc775a2bf410e6a63cbf1b5e Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 2 Aug 2024 17:37:50 +0200 Subject: [PATCH 1/9] test(game): test the evaluation --- tests/game.rs | 57 +++++++++++++++++++++++++++++++++++++++++++ tests/solver.rs | 64 ++++++++++++++++++++++++------------------------- 2 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 tests/game.rs diff --git a/tests/game.rs b/tests/game.rs new file mode 100644 index 0000000..dd658b1 --- /dev/null +++ b/tests/game.rs @@ -0,0 +1,57 @@ +use wordle_analyzer::error::GameError; +use wordle_analyzer::game::evaluation::Evaluation; +use wordle_analyzer::game::response::GuessResponse; +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)?; + assert_eq!( + *game.last_response().unwrap().evaluation(), + Evaluation::build(&guess, "xxccc")? + ); + Ok(()) +} + +#[test] +fn test_eval_reoccuring_char() -> anyhow::Result<()> { + let wl = wordlist(); + let builder = game::Game::builder(&wl) + .solution(Some(wl.get_word(&Word::from("nines")).unwrap())) + .precompute(false); + + let mut game = builder.build()?; + let guess = Word::from("pines"); + game.guess(&guess, None)?; + assert_eq!( + *game.last_response().unwrap().evaluation(), + Evaluation::build(&guess, "xcccc")? + ); + + let mut game = builder.build()?; + let guess = Word::from("sides"); + game.guess(&guess, None)?; + assert_eq!( + *game.last_response().unwrap().evaluation(), + Evaluation::build(&guess, "xcxcc")? + ); + + Ok(()) +} diff --git a/tests/solver.rs b/tests/solver.rs index 3aa2634..ac93ef5 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -36,22 +36,17 @@ fn test_naive_play_predetermined_game() -> anyhow::Result<()> { sl.make_a_move(&mut game)?; assert_eq!( game.responses().last().unwrap().guess(), - &Word::from("their") + &Word::from("first") ); sl.make_a_move(&mut game)?; assert_eq!( game.responses().last().unwrap().guess(), - &Word::from("being") + &Word::from("lives") ); sl.make_a_move(&mut game)?; assert_eq!( game.responses().last().unwrap().guess(), - &Word::from("since") - ); - sl.make_a_move(&mut game)?; - assert_eq!( - game.responses().last().unwrap().guess(), - &Word::from("lines") + &Word::from("sides") ); sl.make_a_move(&mut game)?; assert_eq!( @@ -61,12 +56,16 @@ fn test_naive_play_predetermined_game() -> anyhow::Result<()> { sl.make_a_move(&mut game)?; assert_eq!( game.responses().last().unwrap().guess(), - &Word::from("wines") + &Word::from("pines") + ); + sl.make_a_move(&mut game)?; + assert_eq!( + game.responses().last().unwrap().guess(), + &Word::from("nines") ); - // naive is at the moment too bad to solve "nines" assert!(game.finished()); - assert!(!game.won()); + assert!(game.won()); Ok(()) } @@ -85,55 +84,54 @@ fn test_naive_play_predetermined_game_manually() -> anyhow::Result<()> { next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("which")); game.guess( - next_guess.clone(), + &next_guess.clone(), Some(Evaluation::build(&next_guess, "xxfxx")?), )?; next_guess = sl.guess_for(&game)?; - assert_eq!(next_guess, Word::from("their")); + assert_eq!(next_guess, Word::from("first")); game.guess( - next_guess.clone(), - Some(Evaluation::build(&next_guess, "xxffx")?), + &next_guess.clone(), + Some(Evaluation::build(&next_guess, "xcxfx")?), )?; next_guess = sl.guess_for(&game)?; - assert_eq!(next_guess, Word::from("being")); + assert_eq!(next_guess, Word::from("lives")); game.guess( - next_guess.clone(), - Some(Evaluation::build(&next_guess, "xfffx")?), + &next_guess.clone(), + Some(Evaluation::build(&next_guess, "xcxcc")?), )?; next_guess = sl.guess_for(&game)?; - assert_eq!(next_guess, Word::from("since")); + assert_eq!(next_guess, Word::from("sides")); 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.clone(), - Some(Evaluation::build(&next_guess, "xcccc")?), + &next_guess.clone(), + Some(Evaluation::build(&next_guess, "xcxcc")?), )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("mines")); game.guess( - next_guess.clone(), + &next_guess.clone(), Some(Evaluation::build(&next_guess, "xcccc")?), )?; next_guess = sl.guess_for(&game)?; - assert_eq!(next_guess, Word::from("wines")); + assert_eq!(next_guess, Word::from("pines")); game.guess( - next_guess.clone(), + &next_guess.clone(), Some(Evaluation::build(&next_guess, "xcccc")?), )?; - // naive is at the moment too bad to solve "nines" + next_guess = sl.guess_for(&game)?; + assert_eq!(next_guess, Word::from("nines")); + game.guess( + &next_guess.clone(), + Some(Evaluation::build(&next_guess, "ccccc")?), + )?; + assert!(game.finished()); - assert!(!game.won()); + assert!(game.won()); Ok(()) } -- 2.40.1 From af1a2ee36c41c99a073a9ea2a5b6dcfce2716a5e Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Sat, 3 Aug 2024 18:51:53 +0200 Subject: [PATCH 2/9] fix(evaluation): added more evaluation tests and fixed #21 --- src/game/evaluation.rs | 36 ++++++++++++--------- src/game/mod.rs | 31 ++++++++++-------- src/game/response.rs | 13 +------- tests/game.rs | 71 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 103 insertions(+), 48 deletions(-) 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 9b34f65..1841997 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> { @@ -126,15 +125,29 @@ impl<'wl, WL: WordList> Game<'wl, WL> { } /// Generates an [Evaluation] for a given solution and guess. - pub(crate) fn evaluate(mut solution: WordData, guess: &Word) -> Evaluation { + pub(crate) fn evaluate(solution: WordData, guess: &Word) -> Evaluation { let mut evaluation = Vec::new(); let mut status: Status; let mut buf = solution.0.clone(); + for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.0.chars()) { if c_guess == c_sol { status = Status::Matched; buf.replace_range(idx..idx + 1, "_"); - } else if buf.contains(c_guess) { + } else if buf.contains(c_guess) + && buf + .char_indices() + .filter(|c| c.1 == c_guess) + .filter(|c| { + guess + .chars() + .nth(c.0) + .expect("the evaluations are somehow different lengths") + == c.1 + }) + .count() + == 0 + { status = Status::Exists; buf = buf.replacen(c_guess, "_", 1); } else { @@ -359,16 +372,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/tests/game.rs b/tests/game.rs index dd658b1..31eef13 100644 --- a/tests/game.rs +++ b/tests/game.rs @@ -1,6 +1,7 @@ -use wordle_analyzer::error::GameError; +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::game::response::GuessResponse; use wordle_analyzer::wlist::builtin::BuiltinWList; use wordle_analyzer::wlist::WordList; @@ -23,10 +24,25 @@ fn test_eval_simple() -> anyhow::Result<()> { let mut game = builder.build()?; let guess = Word::from("slate"); game.guess(&guess, None)?; - assert_eq!( + let correct = Evaluation::build(&guess, "xxccc")?; + info!( + "{} =? {}", *game.last_response().unwrap().evaluation(), - Evaluation::build(&guess, "xxccc")? + 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(()) } @@ -40,18 +56,57 @@ fn test_eval_reoccuring_char() -> anyhow::Result<()> { let mut game = builder.build()?; let guess = Word::from("pines"); game.guess(&guess, None)?; - assert_eq!( + let correct = Evaluation::build(&guess, "xcccc")?; + info!( + "{} =? {}", *game.last_response().unwrap().evaluation(), - Evaluation::build(&guess, "xcccc")? + correct ); + assert_eq!(*game.last_response().unwrap().evaluation(), correct); let mut game = builder.build()?; let guess = Word::from("sides"); game.guess(&guess, None)?; - assert_eq!( + let correct = Evaluation::build(&guess, "xcxcc")?; + info!( + "{} =? {}", *game.last_response().unwrap().evaluation(), - Evaluation::build(&guess, "xcxcc")? + 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(()) } -- 2.40.1 From 6cc6694747bddc59a5818e01e4d534e7001dc93f Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Sat, 3 Aug 2024 19:09:07 +0200 Subject: [PATCH 3/9] fix(naive): solver works again after evaluation bugfixes, but is dumber than before --- src/solve/naive/mod.rs | 22 ++++++++++------------ src/solve/naive/states.rs | 24 +++++++++++++++++++++++- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index e824dde..5a4b2d6 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -4,14 +4,15 @@ use libpt::log::{debug, error, info, trace}; use crate::error::{SolverError, WResult}; 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,12 @@ 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 => state - .char_map_mut() - .entry(p.0) - .or_insert(CharInfo::new(game.length())) - .max_occurences(*already_found_amounts.entry(p.0).or_default()), + Status::None => (), } - trace!("absolute frequencies: {already_found_amounts:?}"); + trace!("absolute frequencies: {abs_freq:?}"); + state.finish_step(&abs_freq); } } diff --git a/src/solve/naive/states.rs b/src/solve/naive/states.rs index 486d7e7..a385b84 100644 --- a/src/solve/naive/states.rs +++ b/src/solve/naive/states.rs @@ -37,7 +37,7 @@ impl SolverState { 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.part_of_solution()) .collect() } @@ -49,6 +49,28 @@ 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); + } + } + } } impl CharInfo { -- 2.40.1 From 5c43363cf872069ca38f703d62273e7180f88a10 Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Sat, 3 Aug 2024 19:09:07 +0200 Subject: [PATCH 4/9] fix(naive): solver works again after evaluation bugfixes, but is dumber than before --- src/solve/naive/mod.rs | 28 ++++++++++++++++------------ src/solve/naive/states.rs | 24 +++++++++++++++++++++++- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index e824dde..3e70368 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -4,14 +4,15 @@ use libpt::log::{debug, error, info, trace}; use crate::error::{SolverError, WResult}; 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,18 @@ 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); } - 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); } } diff --git a/src/solve/naive/states.rs b/src/solve/naive/states.rs index 486d7e7..a385b84 100644 --- a/src/solve/naive/states.rs +++ b/src/solve/naive/states.rs @@ -37,7 +37,7 @@ impl SolverState { 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.part_of_solution()) .collect() } @@ -49,6 +49,28 @@ 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); + } + } + } } impl CharInfo { -- 2.40.1 From 5f8c92de4736a0ccee32032a31f493a7e698fedf Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Sat, 3 Aug 2024 20:05:46 +0200 Subject: [PATCH 5/9] refactor(evaluation): fix yet another evaluation bug by refactoring --- src/game/mod.rs | 72 +++++++++++++++++++++++++++++++++---------------- tests/game.rs | 25 ++++++++++++++++- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index 1841997..fedc34a 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -126,34 +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(); + let mut buf: Vec = solution.chars().collect(); - for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.0.chars()) { + #[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) - && buf - .char_indices() - .filter(|c| c.1 == c_guess) - .filter(|c| { - guess - .chars() - .nth(c.0) - .expect("the evaluations are somehow different lengths") - == c.1 - }) - .count() - == 0 - { - 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() } diff --git a/tests/game.rs b/tests/game.rs index 31eef13..bb8ca0e 100644 --- a/tests/game.rs +++ b/tests/game.rs @@ -47,11 +47,12 @@ fn test_eval_simple() -> anyhow::Result<()> { } #[test] -fn test_eval_reoccuring_char() -> anyhow::Result<()> { +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"); @@ -110,3 +111,25 @@ fn test_eval_reoccuring_char() -> anyhow::Result<()> { 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(()) +} -- 2.40.1 From 38d484fb5e832b8c2a1e214413664a39235bc056 Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Sat, 3 Aug 2024 20:16:26 +0200 Subject: [PATCH 6/9] test(naive): test that the naive solver should win it's games --- tests/solver.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/solver.rs b/tests/solver.rs index ac93ef5..85f56f7 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -7,6 +7,8 @@ use wordle_analyzer::wlist::builtin::BuiltinWList; use wordle_analyzer::wlist::word::{Word, WordData}; use wordle_analyzer::wlist::WordList; +use rayon::prelude::*; + fn wordlist() -> impl WordList { BuiltinWList::default() } @@ -135,3 +137,19 @@ fn test_naive_play_predetermined_game_manually() -> anyhow::Result<()> { Ok(()) } + +#[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(()) +} -- 2.40.1 From 38ae0337984b899773cfe58fb1bf7268033b9bf4 Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Wed, 7 Aug 2024 14:20:53 +0200 Subject: [PATCH 7/9] WIP: try fix naive solver --- src/solve/naive/mod.rs | 4 ++++ src/solve/naive/states.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 3e70368..51bcd9d 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -67,6 +67,7 @@ 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); + abs_freq.entry(p.0).or_default(); } } trace!("absolute frequencies: {abs_freq:?}"); @@ -91,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 a385b84..c5f9fd9 100644 --- a/src/solve/naive/states.rs +++ b/src/solve/naive/states.rs @@ -34,10 +34,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() } @@ -71,6 +78,15 @@ impl SolverState { } } } + + 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 { @@ -100,7 +116,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 } @@ -152,7 +173,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) -- 2.40.1 From 58f6646c6e7c0ccbb9f09d9b54248d15ed65bccf Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Wed, 7 Aug 2024 15:01:11 +0200 Subject: [PATCH 8/9] chore: add back a test wrongly removed in merge --- tests/solver.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/solver.rs b/tests/solver.rs index 4cad1c0..49ea963 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -21,3 +21,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(()) +} -- 2.40.1 From a9549aedc0e78d40722494c755eb8e4b17b99956 Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Wed, 7 Aug 2024 22:30:53 +0200 Subject: [PATCH 9/9] WIP: make the solver work with the fixed game evaluation --- src/game/mod.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index ee15947..5f857ff 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -131,14 +131,6 @@ impl<'wl, WL: WordList> Game<'wl, WL> { let mut status: Status; 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 { @@ -148,12 +140,6 @@ impl<'wl, WL: WordList> Game<'wl, WL> { } } - #[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" @@ -175,12 +161,6 @@ impl<'wl, WL: WordList> Game<'wl, WL> { } evaluation[idx] = (c_guess, status); } - - #[cfg(debug_assertions)] - { - assert_eq!(buflen, buf.len()); - assert_eq!(buflen, evaluation.len()); - } evaluation.into() } -- 2.40.1