use std::collections::HashMap; use libpt::log::{debug, error, info, trace}; use crate::error::{SolverError, WResult}; use crate::game::evaluation::Evaluation; 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, } impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { fn build(wordlist: &'wl WL) -> crate::error::WResult { 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 { let mut pattern: String = ".".repeat(game.length()); // indexes we tried for that char and the number of occurences 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 evaluation: &Evaluation = response.evaluation(); for (idx, p) in evaluation.clone().into_iter().enumerate() { match p.1 { Status::Matched => { pattern.replace_range(idx..idx + 1, &p.0.to_string()); state .char_map_mut() .entry(p.0) .or_insert(CharInfo::new(game.length())) .found_at(idx); *already_found_amounts.entry(p.0).or_default() += 1; } Status::Exists => { let cinfo = state .char_map_mut() .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]); } 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:?}"); } } debug!("built state from responses: {state:#?}"); // get all words that have the correct chars on the same positions let mut matches: Vec = game.wordlist().get_words_matching(&pattern)?; if matches.is_empty() { error!("no matches even when just considering the known good chars"); return Err(SolverError::NoMatches(game.solution().cloned()).into()); } else { trace!("found {} basic matches", matches.len()) } matches = matches .iter() // only words that have not been guessed yet .filter(|p| !game.made_guesses().contains(&&p.0)) .filter(|solution_candidate| { if !game.responses().is_empty() && !state.has_all_known_contained(&solution_candidate.0) { trace!("known cont:{:#?}", state.get_all_known_contained()); return false; } for (idx, c) in solution_candidate.0.char_indices() { let cinfo = state .char_map_mut() .entry(c) .or_insert(CharInfo::new(game.length())); if !cinfo.occurences_of_char_possible(&solution_candidate.0, c) || cinfo.has_been_tried(idx) { return false; } } true }) .map(|v| v.to_owned()) .collect(); if matches.is_empty() { return Err(SolverError::NoMatches(game.solution().cloned()).into()); } Ok(matches[0].0.to_owned()) } } impl<'wl, WL: WordList> From> for AnyBuiltinSolver<'wl, WL> { fn from(value: NaiveSolver<'wl, WL>) -> Self { Self::Naive(value) } }