diff --git a/Cargo.toml b/Cargo.toml index 671297c..71fdfb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ clap = { version = "4.5.3", features = ["derive"], optional = true } colored = { version = "2.1.0", optional = false } libpt = "0.4.2" rand = "0.8.5" +regex = "1.10.3" serde = { version = "1.0.197", optional = true, features = ["serde_derive"] } serde_json = { version = "1.0.114", optional = true } thiserror = "1.0.58" diff --git a/src/bin/game/cli.rs b/src/bin/game/cli.rs index 9072152..a2c179f 100644 --- a/src/bin/game/cli.rs +++ b/src/bin/game/cli.rs @@ -54,8 +54,8 @@ fn main() -> anyhow::Result<()> { response = match game.guess(guess) { Ok(r) => r, Err(err) => match err { - GameError::GuessHasWrongLength => { - println!("word length: must be {} long", game.length()); + GameError::GuessHasWrongLength(len) => { + println!("word length: must be {} long but is {}", game.length(), len); continue; } _ => { diff --git a/src/error.rs b/src/error.rs index ab98955..4a287d5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,7 @@ use thiserror::Error; +use crate::wlist::word::Word; + pub type WResult = std::result::Result; pub type GameResult = std::result::Result; @@ -18,12 +20,19 @@ pub enum Error { // for `FromStr` of `BuiltinSolver` #[error("Unknown builtin solver")] UnknownBuiltinSolver, + #[error("pattern matching error")] + Regex{ + #[from] + source: regex::Error + } } #[derive(Debug, Clone, Error)] pub enum GameError { - #[error("The guess has the wrong length")] - GuessHasWrongLength, + #[error("The guess has the wrong length ({0})")] + GuessHasWrongLength(usize), #[error("The game is finished but a guess is being made")] TryingToPlayAFinishedGame, + #[error("Tried to guess a word that is not in the wordlist ({0})")] + WordNotInWordlist(Word), } diff --git a/src/game/mod.rs b/src/game/mod.rs index f40107c..f8ebcaa 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -78,11 +78,14 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// if the game is finished. pub fn guess(&mut self, guess: Word) -> GameResult { if guess.len() != self.length { - return Err(GameError::GuessHasWrongLength); + return Err(GameError::GuessHasWrongLength(guess.len())); } if self.finished || self.step > self.max_steps { return Err(GameError::TryingToPlayAFinishedGame); } + if self.wordlist.get_word(&guess).is_none() { + return Err(GameError::WordNotInWordlist(guess)); + } self.step += 1; let mut compare_solution = self.solution.0.clone(); @@ -101,7 +104,8 @@ impl<'wl, WL: WordList> Game<'wl, WL> { evaluation.push((c, status)); } - let mut response = GuessResponse::new(guess, evaluation, &self); + let response = GuessResponse::new(guess, evaluation, self); + self.responses.push(response.clone()); self.finished = response.finished(); Ok(response) } @@ -128,6 +132,13 @@ impl<'wl, WL: WordList> Game<'wl, WL> { pub fn responses(&self) -> &Vec { &self.responses } + pub fn last_response(&self) -> Option<&GuessResponse> { + self.responses().last() + } + + pub fn wordlist(&self) -> &WL { + self.wordlist + } } /// Build and Configure a [`Game`] diff --git a/src/game/response.rs b/src/game/response.rs index d14f02c..6966b87 100644 --- a/src/game/response.rs +++ b/src/game/response.rs @@ -62,6 +62,14 @@ impl GuessResponse { None } } + + pub fn evaluation(&self) -> &[(char, Status)] { + &self.evaluation + } + + pub fn guess(&self) -> &str { + &self.guess + } } impl Display for GuessResponse { diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index a8c98de..bc2f9e3 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -3,7 +3,7 @@ use libpt::log::info; use crate::wlist::word::Word; use crate::wlist::WordList; -use super::{AnyBuiltinSolver, Solver}; +use super::{AnyBuiltinSolver, Solver, Status}; #[derive(Debug, Clone)] pub struct NaiveSolver<'wl, WL> { @@ -16,7 +16,21 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { Ok(Self { wl: wordlist }) } fn guess_for(&self, game: &crate::game::Game) -> Word { - self.wl.rand_word().0 + // HACK: hardcoded length + let mut buf: Word = Word::from("....."); + let response = game.last_response(); + if response.is_some() { + for (idx, p) in response.unwrap().evaluation().iter().enumerate() { + if p.1 == Status::Matched { + buf.replace_range(idx..idx + 1, &p.0.to_string()); + } + } + } + game.wordlist() + .get_words_matching(buf) + .expect("the solution does not exist in the wordlist")[0] + .0 + .clone() } } diff --git a/src/wlist/builtin.rs b/src/wlist/builtin.rs index 2e5e855..3204376 100644 --- a/src/wlist/builtin.rs +++ b/src/wlist/builtin.rs @@ -19,7 +19,7 @@ impl super::WordList for BuiltinWList { &self.words } fn get_word(&self, word: &Word) -> Option { - self.words.get(&word) + self.words.get(word) } } diff --git a/src/wlist/mod.rs b/src/wlist/mod.rs index 2cd029c..ab795b2 100644 --- a/src/wlist/mod.rs +++ b/src/wlist/mod.rs @@ -1,5 +1,8 @@ +use libpt::log::debug; use rand::seq::IteratorRandom; +use regex::Regex; + use std::collections::HashMap; use std::ops::RangeBounds; @@ -14,13 +17,16 @@ pub type AnyWordlist = Box; pub trait WordList: Clone + std::fmt::Debug + Default { fn solutions(&self) -> ManyWordDatas { - let wmap = self.wordmap(); + let wmap = self.wordmap().clone(); let threshold = wmap.threshold(); - wmap.iter().filter(|i| *i.1 > threshold).collect() + wmap.iter() + .filter(|i| *i.1 > threshold) + .map(|p| (p.0.clone(), *p.1)) + .collect() } fn rand_solution(&self) -> WordData { let mut rng = rand::thread_rng(); - let sol = *self.solutions().iter().choose(&mut rng).unwrap(); + let sol = self.solutions().iter().choose(&mut rng).unwrap().clone(); (sol.0.to_owned(), sol.1.to_owned()) } fn rand_word(&self) -> WordData { @@ -62,4 +68,28 @@ pub trait WordList: Clone + std::fmt::Debug + Default { let n: f64 = cmap.keys().len() as f64; cmap.into_iter().map(|p| (p.0, p.1 as f64 / n)).collect() } + fn raw_wordlist(&self) -> String { + let mut buf = String::new(); + for w in self.wordmap().keys() { + buf += &w; + buf += "\n"; + } + buf + } + fn get_words_matching(&self, pattern: String) -> WResult { + let pattern = Regex::new(&pattern)?; + let hay = self.raw_wordlist(); + let keys = pattern.captures_iter(&hay); + let mut buf = ManyWordDatas::new(); + for k in keys { + debug!("match: {k:?}"); + let w: WordData = self.wordmap().get(&k[0]).unwrap(); + buf.push(w) + } + // sort by frequency + buf.sort_by(|a, b| { + a.1.partial_cmp(&b.1).unwrap() + }); + Ok(buf) + } } diff --git a/src/wlist/word.rs b/src/wlist/word.rs index 2385a2e..91128d4 100644 --- a/src/wlist/word.rs +++ b/src/wlist/word.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::fmt::write; +use std::hash::Hash; use libpt::log::debug; #[cfg(feature = "serde")] @@ -11,7 +12,7 @@ pub type Frequency = f64; pub type Word = String; pub type WordData = (Word, Frequency); pub type ManyWords<'a> = Vec<&'a Word>; -pub type ManyWordDatas<'a> = Vec<(&'a Word, &'a Frequency)>; +pub type ManyWordDatas = Vec<(Word, Frequency)>; #[derive(Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -66,9 +67,9 @@ impl WordMap { pub fn inner(&self) -> &HashMap { &self.inner } - pub fn get(&self, word: &Word) -> Option { - match self.inner.get(word) { - Some(f) => Some((word.clone(), *f)), + pub fn get(&self, word: I) -> Option { + match self.inner.get(&word.to_string()) { + Some(f) => Some((word.to_string(), *f)), None => None, } } @@ -104,7 +105,7 @@ impl From> for WordMap { } } -impl From for HashMap{ +impl From for HashMap { fn from(value: WordMap) -> Self { value.inner }