From 3cd20d775e84a7e73841d9e5fa98f11fa2d49daf Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Fri, 2 Aug 2024 15:11:46 +0200 Subject: [PATCH] feat(naive): start moving to state machine for solver internals --- src/solve/naive/mod.rs | 67 +++++++++++------------ src/solve/naive/states.rs | 110 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 35 deletions(-) create mode 100644 src/solve/naive/states.rs diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 3ab6637..13203b0 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -3,12 +3,15 @@ use std::collections::HashMap; use libpt::log::{debug, info, trace}; use crate::error::{SolverError, WResult}; -use crate::game::evaluation::Evaluation; +use crate::game::evaluation::{Evaluation, EvaluationUnit}; use crate::wlist::word::{Word, WordData}; use crate::wlist::WordList; use super::{AnyBuiltinSolver, Solver, Status}; +mod states; +use states::*; + #[derive(Debug, Clone)] pub struct NaiveSolver<'wl, WL> { wl: &'wl WL, @@ -31,7 +34,7 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { fn guess_for(&self, game: &crate::game::Game) -> WResult { let mut pattern: String = ".".repeat(game.length()); // indexes we tried for that char and the number of occurences - let mut other_chars: HashMap, usize)> = HashMap::new(); + let mut state: SolverState = SolverState::new(); let responses = game.responses().iter().enumerate(); for (_idx, response) in responses { let evaluation: &Evaluation = response.evaluation(); @@ -40,25 +43,23 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { Status::Matched => { pattern.replace_range(idx..idx + 1, &p.0.to_string()); - other_chars.entry(p.0).or_default(); - let v = other_chars.get_mut(&p.0).unwrap(); - v.1 += 1; - } - Status::Exists => { - other_chars.entry(p.0).or_default(); - let v = other_chars.get_mut(&p.0).unwrap(); - v.0.push(idx); - - // TODO: count how many times the char occurs - v.1 += 1; - } - Status::None => { - other_chars.entry(p.0).or_default(); + state.char_map_mut().entry(p.0).or_default().found_at(idx); } + Status::Exists => state + .char_map_mut() + .entry(p.0) + .or_default() + .at_least_n_occurences( + response.guess().chars().filter(|c| *c == p.0).count(), + ), + Status::None => state + .char_map_mut() + .entry(p.0) + .or_default() + .not_in_solution(), } } } - debug!("other chars: {:?}", other_chars); // get all words that have the correct chars on the same positions let mut matches: Vec = game.wordlist().get_words_matching(&pattern)?; @@ -69,24 +70,14 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { .iter() // only words that have not been guessed yet .filter(|p| !game.made_guesses().contains(&&p.0)) - // only words that do contain the chars we know exist - .filter(|p| { - for other in other_chars.iter() { - if p.0.contains(*other.0) { - let mut already_tried: Vec<(_, _)> = Vec::new(); - for spot in &other.1 .0 { - already_tried.push((spot, *other.0)); - } - - if p.0.chars().filter(|c| *c == *other.0).count() > other.1 .1 { - return false; // the char occurs too often in that word - } - for c in p.0.char_indices() { - if c.1 == *other.0 && other.1 .0.contains(&c.0) { - return false; - } - } - } else if other.1 .1 != 0 { + .filter(|solution_candidate| { + for (idx, c) in solution_candidate.0.char_indices() { + let cinfo = state.char_map_mut().entry(c).or_default(); + // bad word if it uses a char thats not in the solution + if !cinfo.part_of_solution() + || cinfo.has_been_tried(idx) + || cinfo.occurences_of_char_possible(&solution_candidate.0, c) + { return false; } } @@ -101,6 +92,12 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { } } +impl<'wl, WL: WordList> NaiveSolver<'wl, WL> { + fn get_highest_possible_abs_freq(indexes: &[usize], game: &crate::game::Game) -> usize { + game.length() - indexes.len() + } +} + impl<'wl, WL: WordList> From> for AnyBuiltinSolver<'wl, WL> { fn from(value: NaiveSolver<'wl, WL>) -> Self { Self::Naive(value) diff --git a/src/solve/naive/states.rs b/src/solve/naive/states.rs new file mode 100644 index 0000000..a69b535 --- /dev/null +++ b/src/solve/naive/states.rs @@ -0,0 +1,110 @@ +use std::collections::{HashMap, HashSet}; +use std::ops::{Range, RangeBounds}; + +use crate::error::WResult; + +pub(crate) type CharMap = HashMap; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct CharInfo { + confirmed_indexes: HashSet, + tried_indexes: HashSet, + occurences_amount: Range, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SolverState { + char_map: CharMap, +} + +impl SolverState { + pub fn new() -> Self { + Self { + char_map: HashMap::new(), + } + } + + pub fn char_map(&self) -> &CharMap { + &self.char_map + } + + pub fn char_map_mut(&mut self) -> &mut CharMap { + &mut self.char_map + } +} + +impl CharInfo { + pub fn new() -> Self { + Self::default() + } + + pub fn found_at(&mut self, idx: usize) { + self.tried_indexes.insert(idx); + self.confirmed_indexes.insert(idx); + + if self.occurences_amount.start < 1 { + self.occurences_amount.start = 1; + } + } + + /// tried to guess a char we know exists at this position, but it was incorrect + pub fn tried_but_failed(&mut self, idx: usize) { + self.tried_indexes.insert(idx); + } + + /// char is not in the solution + pub fn not_in_solution(&mut self) { + self.occurences_amount.start = 0; + self.occurences_amount.end = 0; + } + + pub fn has_been_tried(&self, idx: usize) -> bool { + self.tried_indexes.contains(&idx) + } + + #[must_use] + pub fn part_of_solution(&self) -> bool { + self.occurences_amount.end > 0 + } + + pub fn at_least_n_occurences(&mut self, n: usize) { + self.occurences_amount.start = n; + } + + pub(crate) fn confirmed_indexes(&self) -> &HashSet { + &self.confirmed_indexes + } + + pub(crate) fn confirmed_indexes_mut(&mut self) -> &mut HashSet { + &mut self.confirmed_indexes + } + + pub(crate) fn tried_indexes(&self) -> &HashSet { + &self.tried_indexes + } + + pub(crate) fn tried_indexes_mut(&mut self) -> &mut HashSet { + &mut self.tried_indexes + } + + pub(crate) fn occurences_amount(&self) -> &Range { + &self.occurences_amount + } + + pub(crate) fn occurences_amount_mut(&mut self) -> &mut Range { + &mut self.occurences_amount + } + + pub(crate) fn occurences_of_char_possible( + &self, + solution_candidate: &str, + character: char, + ) -> bool { + self.occurences_amount.contains( + &solution_candidate + .chars() + .filter(|c| *c == character) + .count(), + ) + } +}