diff --git a/Cargo.toml b/Cargo.toml index 35b7248..85c19fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,10 @@ keywords = ["wordle", "benchmark"] default-run = "wordlec" [features] -default = ["game", "bench", "tui", "solvers", "builtin_wlist", "serde"] +default = ["game", "bench", "tui", "solve", "builtin_wlist", "serde"] builtin_wlist = ["dep:serde_json", "serde"] game = ["builtin_wlist"] -solvers = [] +solve = [] tui = ["cli"] cli = ["dep:clap"] bench = [] @@ -41,3 +41,8 @@ required-features = ["game", "cli"] name = "wordlet" path = "src/bin/game/tui.rs" required-features = ["tui", "game"] + +[[bin]] +name = "wordlesolve" +path = "src/bin/solve/simple.rs" +required-features = ["game", "solve", "cli"] diff --git a/src/bin/game/cli.rs b/src/bin/game/cli.rs index 17d70b2..58949fa 100644 --- a/src/bin/game/cli.rs +++ b/src/bin/game/cli.rs @@ -39,11 +39,13 @@ fn main() -> anyhow::Result<()> { } debug!("dumping CLI: {:#?}", cli); - let mut game = game::Game::::builder() + let wl = BuiltinWList::default(); + let builder = game::Game::builder() .length(cli.length) .max_steps(cli.max_steps) .precompute(cli.precompute) - .build()?; + .wordlist(wl); + let mut game = builder.build()?; debug!("{game:#?}"); diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs new file mode 100644 index 0000000..ee80563 --- /dev/null +++ b/src/bin/solve/simple.rs @@ -0,0 +1,3 @@ +fn main() { + unimplemented!(); +} diff --git a/src/error.rs b/src/error.rs index d7683fe..74c92f6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ use thiserror::Error; -pub type Result = std::result::Result; +pub type WResult = std::result::Result; pub type GameResult = std::result::Result; #[derive(Debug, Error)] diff --git a/src/game/mod.rs b/src/game/mod.rs index 8faf465..70ee679 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,15 +1,16 @@ use crate::error::*; -use crate::wlist::word::{Solution, Word}; +use crate::wlist::word::{WordData, Word}; use crate::wlist::WordList; -pub mod response; use libpt::log::debug; + +pub mod response; use response::GuessResponse; use self::response::Status; #[derive(Debug, Clone, PartialEq)] -pub struct Game +pub struct Game<'wl, WL> where WL: WordList, { @@ -17,13 +18,13 @@ where precompute: bool, max_steps: usize, step: usize, - solution: Solution, - wordlist: WL, + solution: WordData, + wordlist: &'wl WL, finished: bool, // TODO: keep track of the letters the user has tried } -impl Game { +impl<'wl, WL: WordList> Game<'wl, WL> { /// get a new [`GameBuilder`] pub fn builder() -> GameBuilder { GameBuilder::default() @@ -44,30 +45,33 @@ impl Game { length: usize, precompute: bool, max_steps: usize, - wlist: WL, + wlist: &'wl WL, ) -> GameResult { // TODO: check if the length is in the range bounds of the wordlist let solution = wlist.rand_solution(); - let game = Game { + let game: Game<'wl, WL> = Game { length, precompute, max_steps, step: 1, solution, - wordlist: wlist, + wordlist: &wlist, finished: false, }; Ok(game) } - pub fn reset(mut self) -> Self { - self.solution = self.wordlist.rand_solution(); - self.step = 1; - self.finished = false; - self - } - + /// Make a new guess + /// + /// The word will be evaluated against the [solution](Game::solution) of the [Game]. + /// A [GuessResponse] will be formulated, showing us which letters are correctly placed, in the + /// solution, or just wrong. + /// + /// # Errors + /// + /// This function will return an error if the length of the [Word] is wrong It will also error + /// if the game is finished. pub fn guess(&mut self, guess: Word) -> GameResult { if guess.len() != self.length { return Err(GameError::GuessHasWrongLength); @@ -102,7 +106,7 @@ impl Game { self.length } - pub fn solution(&self) -> &Solution { + pub fn solution(&self) -> &WordData { &self.solution } @@ -153,10 +157,10 @@ pub struct GameBuilder { impl GameBuilder { /// build a [`Game`] with the stored configuration - pub fn build(self) -> GameResult> { + pub fn build(&self) -> GameResult> { debug!("{:#?}", self); let game: Game = - Game::build(self.length, self.precompute, self.max_steps, WL::default())?; + Game::build(self.length, self.precompute, self.max_steps, &self.wordlist)?; Ok(game) } @@ -185,6 +189,15 @@ impl GameBuilder { debug!("max steps: {:#?}", self.max_steps); self } + + /// Set the wordlist for the builder + /// + /// The builder can be used multiple times. Each [`Game`] will have a immutable reference to + /// `wl`. + pub fn wordlist(mut self, wl: WL) -> Self { + self.wordlist = wl; + self + } } impl Default for GameBuilder { diff --git a/src/game/response.rs b/src/game/response.rs index df685cb..a41176a 100644 --- a/src/game/response.rs +++ b/src/game/response.rs @@ -4,10 +4,12 @@ use colored::{ColoredString, Colorize}; use libpt::log::debug; use std::fmt::Display; +pub type Evaluation = Vec<(char, Status)>; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct GuessResponse { guess: Word, - evaluation: Vec<(char, Status)>, + evaluation: Evaluation, step: usize, finish: bool, win: bool, diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 8b13789..e6a1f52 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -1 +1,12 @@ +use crate::{ + error::WResult, + game::{response::*, Game}, + wlist::{word::WordData, WordList}, +}; +pub trait Solver<'wl, WL: WordList>: Clone + Default { + fn build(wordlist: WL) -> WResult; + fn build_game(&self) -> Game<'wl, WL>; + fn play(game: &mut Game<'wl, WL>) -> Game<'wl, WL>; + fn solve(game: &mut Game<'wl, WL>) -> WResult; +} diff --git a/src/solvers/stupid/mod.rs b/src/solvers/stupid/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/wlist/builtin.rs b/src/wlist/builtin.rs index 030f7fb..2e5e855 100644 --- a/src/wlist/builtin.rs +++ b/src/wlist/builtin.rs @@ -18,6 +18,9 @@ impl super::WordList for BuiltinWList { fn wordmap(&self) -> &super::WordMap { &self.words } + fn get_word(&self, word: &Word) -> Option { + self.words.get(&word) + } } impl Default for BuiltinWList { diff --git a/src/wlist/mod.rs b/src/wlist/mod.rs index 6969480..ebc0c26 100644 --- a/src/wlist/mod.rs +++ b/src/wlist/mod.rs @@ -8,15 +8,17 @@ pub mod builtin; pub mod word; use word::*; +use crate::error::WResult; + pub type AnyWordlist = Box; pub trait WordList: Clone + std::fmt::Debug + Default { - fn solutions(&self) -> ManySolutions { + fn solutions(&self) -> ManyWordDatas { let wmap = self.wordmap(); let threshold = wmap.threshold(); wmap.iter().filter(|i| *i.1 > threshold).collect() } - fn rand_solution(&self) -> Solution { + fn rand_solution(&self) -> WordData { let mut rng = rand::thread_rng(); let sol = *self.solutions().iter().choose(&mut rng).unwrap(); (sol.0.to_owned(), sol.1.to_owned()) @@ -39,4 +41,5 @@ pub trait WordList: Clone + std::fmt::Debug + Default { } WordMap::new(hm) } + fn get_word(&self, word: &Word) -> Option; } diff --git a/src/wlist/word.rs b/src/wlist/word.rs index 7513a6f..72a8f82 100644 --- a/src/wlist/word.rs +++ b/src/wlist/word.rs @@ -1,7 +1,5 @@ use std::collections::HashMap; -use std::fmt::{write, Display}; -use std::iter::Sum; -use std::ops::RangeFull; +use std::fmt::write; use libpt::log::debug; #[cfg(feature = "serde")] @@ -11,8 +9,9 @@ pub type Frequency = f64; // PERF: Hash for String is probably a bottleneck pub type Word = String; -pub type ManySolutions<'a> = Vec<(&'a Word, &'a Frequency)>; -pub type Solution = (Word, Frequency); +pub type WordData = (Word, Frequency); +pub type ManyWords<'a> = Vec<&'a Word>; +pub type ManyWordDatas<'a> = Vec<(&'a Word, &'a Frequency)>; #[derive(Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -35,6 +34,7 @@ impl WordMap { self.inner.iter() } pub fn freq_range(&self) -> std::ops::Range { + // TODO: calculate this instead of estimating like this return 0.1e-10..1e-6; let lowest: Frequency = todo!(); let highest: Frequency = todo!(); @@ -64,6 +64,12 @@ 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)), + None => None, + } + } } impl std::fmt::Debug for WordMap {