generated from PlexSheep/rs-base
improve naive solver #13
|
@ -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<StyledObject<String>> {
|
||||
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::<String>())
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ where
|
|||
solution: Option<WordData>,
|
||||
wordlist: &'wl WL,
|
||||
responses: Vec<GuessResponse>,
|
||||
// TODO: keep track of the letters the user has tried
|
||||
}
|
||||
|
||||
impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||
|
@ -127,20 +126,40 @@ 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<char> = solution.chars().collect();
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
evaluation.into()
|
||||
}
|
||||
|
@ -359,16 +378,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(())
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<char, usize> = HashMap::new();
|
||||
let mut abs_freq: HashMap<char, usize> = 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 => state
|
||||
Status::None => {
|
||||
let cinfo = state
|
||||
.char_map_mut()
|
||||
.entry(p.0)
|
||||
.or_insert(CharInfo::new(game.length()))
|
||||
.max_occurences(*already_found_amounts.entry(p.0).or_default()),
|
||||
.or_insert(CharInfo::new(game.length()));
|
||||
cinfo.tried_but_failed(idx);
|
||||
abs_freq.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;
|
||||
|
|
|
@ -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<char, usize>) {
|
||||
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)
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue