feat(naive): don't reuse bad chars #12
cargo devel CI / cargo CI (push) Failing after 1m53s Details

This commit is contained in:
Christoph J. Scherr 2024-07-31 10:16:51 +02:00
parent 1b354299d4
commit 6f72fda4ff
4 changed files with 58 additions and 38 deletions

View File

@ -172,7 +172,7 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> {
eprintln!("{}", style(best_guess.unwrap_err()).red().bold()); eprintln!("{}", style(best_guess.unwrap_err()).red().bold());
continue; continue;
} }
debug!("game state: {game:?}"); trace!("game state: {game:?}");
println!("best guess: {}", best_guess.unwrap()); println!("best guess: {}", best_guess.unwrap());
} }
ReplCommand::Guess { ReplCommand::Guess {
@ -188,7 +188,7 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> {
continue; continue;
} }
println!("{}", guess.unwrap()); println!("{}", guess.unwrap());
debug!("game state: {game:#?}"); trace!("game state: {game:#?}");
} }
ReplCommand::New => game = builder.build()?, ReplCommand::New => game = builder.build()?,
ReplCommand::Undo { n } => game.undo(n)?, ReplCommand::Undo { n } => game.undo(n)?,
@ -218,7 +218,7 @@ fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> {
"eng" => BuiltinWList::english(cli.length), "eng" => BuiltinWList::english(cli.length),
_ => BuiltinWList::load(&cli.wordlist, cli.length)?, _ => BuiltinWList::load(&cli.wordlist, cli.length)?,
}; };
debug!("wordlist: {wl}"); trace!("wordlist: {wl}");
let mut builder = game::Game::builder(&wl) let mut builder = game::Game::builder(&wl)
.length(cli.length) .length(cli.length)
.max_steps(cli.max_steps) .max_steps(cli.max_steps)
@ -238,13 +238,13 @@ fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> {
let solver = cli.solver.to_solver(&wl); let solver = cli.solver.to_solver(&wl);
let mut game = builder.build()?; let mut game = builder.build()?;
debug!("{game:#?}"); trace!("{game:#?}");
let mut response: GuessResponse; let mut response: GuessResponse;
let mut _guess: Word; let mut _guess: Word;
loop { loop {
response = solver.make_a_move(&mut game)?; response = solver.make_a_move(&mut game)?;
debug!("game state: {game:#?}"); trace!("game state: {game:#?}");
println!("{}. guess: {response}", game.step() - 1); println!("{}. guess: {response}", game.step() - 1);
if response.finished() { if response.finished() {

View File

@ -1,7 +1,7 @@
use thiserror::Error; use thiserror::Error;
use crate::bench::report::Report; use crate::bench::report::Report;
use crate::wlist::word::Word; use crate::wlist::word::{Word, WordData};
pub type WResult<T> = std::result::Result<T, Error>; pub type WResult<T> = std::result::Result<T, Error>;
pub type GameResult<T> = std::result::Result<T, GameError>; pub type GameResult<T> = std::result::Result<T, GameError>;
@ -62,8 +62,8 @@ pub enum BenchError {
#[derive(Debug, Clone, Error)] #[derive(Debug, Clone, Error)]
pub enum SolverError { pub enum SolverError {
#[error("Wordlist has no matches for the gamestate")] #[error("Wordlist has no matches for the gamestate (solution: {0:?})")]
NoMatches, NoMatches(Option<WordData>),
#[error("Unknown builtin solver")] #[error("Unknown builtin solver")]
UnknownBuiltinSolver, UnknownBuiltinSolver,
} }

View File

@ -1,6 +1,10 @@
use libpt::log::{info, trace}; use core::panic;
use std::collections::HashMap;
use libpt::log::{debug, info, trace};
use crate::error::{SolverError, WResult}; use crate::error::{SolverError, WResult};
use crate::game::response;
use crate::wlist::word::{Word, WordData}; use crate::wlist::word::{Word, WordData};
use crate::wlist::WordList; use crate::wlist::WordList;
@ -26,51 +30,67 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
/// * Discard all words that don't have the chars that we know from the last guess are in the /// * 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. /// word, but don't know the position of.
fn guess_for(&self, game: &crate::game::Game<WL>) -> WResult<Word> { fn guess_for(&self, game: &crate::game::Game<WL>) -> WResult<Word> {
// HACK: hardcoded length
let mut pattern: String = ".".repeat(game.length()); let mut pattern: String = ".".repeat(game.length());
let mut other_chars: Vec<char> = Vec::new(); let mut other_chars: Vec<char> = Vec::new();
let response = game.last_response(); // a hash map telling how many of the characters may be in a correct word (+1)
trace!( // if the value for a char is 2 => it may be in the solution 1 time
"guessing best guess for last response: {response:#?}\n{:#?}", // if the value for a char is 1 => it may not be in the solution
response.map(|a| a.evaluation()) let mut wrong_chars: Vec<char> = Vec::new();
); let responses = game.responses().iter().enumerate();
if response.is_some() { for (_idx, response) in responses {
for (idx, p) in response for (idx, p) in response.evaluation().clone().into_iter().enumerate() {
.unwrap() match p.1 {
.evaluation() Status::Matched => {
.clone() pattern.replace_range(idx..idx + 1, &p.0.to_string());
.into_iter() }
.enumerate() Status::Exists => other_chars.push(p.0),
{ Status::None => wrong_chars.push(p.0),
if p.1 == Status::Matched {
pattern.replace_range(idx..idx + 1, &p.0.to_string());
} else if p.1 == Status::Exists {
other_chars.push(p.0)
} }
} }
} }
trace!("other chars: {:?}", other_chars); debug!("other chars: {:?}", other_chars);
let mut matches: Vec<WordData> = game.wordlist().get_words_matching(pattern)?; debug!("wrong chars: {:?}", wrong_chars);
let mut matches: Vec<WordData> = game.wordlist().get_words_matching(&pattern)?;
if matches.is_empty() { if matches.is_empty() {
return Err(SolverError::NoMatches.into()); return Err(SolverError::NoMatches(game.solution().cloned()).into());
} }
matches = matches matches = matches
.iter() .iter()
// only words that have not been guessed yet // only words that have not been guessed yet
.filter(|p| !game.made_guesses().contains(&&p.0)) .filter(|p| !game.made_guesses().contains(&&p.0))
// only words that contain the letters we found earlier (that were not matched) // only words that do contain the chars we know exist
.filter(|p| { .filter(|p| {
// TODO: don't repeat unmatched contained chars on the same position twice #2 for other in &other_chars {
let mut fits = true; if p.0.contains(*other) {
for c in other_chars.iter() { // TODO: account for chars that occur multiple times
fits &= p.0.contains(*c); continue;
} else {
return false;
}
} }
fits true
})
// only words that do not contain the letters we know are wrong
.filter(|p| {
for wrong in &wrong_chars {
if p.0.contains(*wrong) {
let mut tmp = 0;
let in_other = other_chars.iter().filter(|v| **v == *wrong).count()
+ pattern.chars().filter(|v| *v == *wrong).count();
// TODO: account for chars that occur multiple times
if in_other > tmp {
tmp += 1;
continue;
}
return false;
}
}
true
}) })
.map(|v| v.to_owned()) .map(|v| v.to_owned())
.collect(); .collect();
if matches.is_empty() { if matches.is_empty() {
return Err(SolverError::NoMatches.into()); return Err(SolverError::NoMatches(game.solution().cloned()).into());
} }
Ok(matches[0].0.to_owned()) Ok(matches[0].0.to_owned())
} }

View File

@ -93,7 +93,7 @@ pub trait WordList: Clone + std::fmt::Debug + Default + Sync + Display {
} }
buf buf
} }
fn get_words_matching(&self, pattern: String) -> WResult<Vec<WordData>> { fn get_words_matching(&self, pattern: &str) -> WResult<Vec<WordData>> {
let pattern = Regex::new(&pattern).map_err(WordlistError::from)?; let pattern = Regex::new(&pattern).map_err(WordlistError::from)?;
let hay = self.raw_wordlist(); let hay = self.raw_wordlist();
let keys = pattern.captures_iter(&hay); let keys = pattern.captures_iter(&hay);