diff --git a/Cargo.toml b/Cargo.toml index 85c19fd..671297c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ default-run = "wordlec" default = ["game", "bench", "tui", "solve", "builtin_wlist", "serde"] builtin_wlist = ["dep:serde_json", "serde"] game = ["builtin_wlist"] -solve = [] +solve = ["game"] tui = ["cli"] cli = ["dep:clap"] bench = [] @@ -45,4 +45,4 @@ required-features = ["tui", "game"] [[bin]] name = "wordlesolve" path = "src/bin/solve/simple.rs" -required-features = ["game", "solve", "cli"] +required-features = ["solve", "cli"] diff --git a/src/bin/game/cli.rs b/src/bin/game/cli.rs index 17e99bd..2ef14b6 100644 --- a/src/bin/game/cli.rs +++ b/src/bin/game/cli.rs @@ -39,11 +39,10 @@ fn main() -> anyhow::Result<()> { debug!("dumping CLI: {:#?}", cli); let wl = BuiltinWList::default(); - let builder = game::Game::builder() + let builder = game::Game::builder(&wl) .length(cli.length) .max_steps(cli.max_steps) - .precompute(cli.precompute) - .wordlist(wl); + .precompute(cli.precompute); let mut game = builder.build()?; debug!("{game:#?}"); diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index ee80563..7b1c2aa 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -1,3 +1,78 @@ -fn main() { - unimplemented!(); +#![warn(clippy::all)] +// #![warn(missing_docs)] +#![warn(missing_debug_implementations)] +use std::io::Write; + +use clap::Parser; +use libpt::log::*; +use wordle_analyzer::error::GameError; +use wordle_analyzer::game::response::GuessResponse; + +use wordle_analyzer::game::Game; +use wordle_analyzer::solve::{stupid, BuiltinSolvers, Solver}; +use wordle_analyzer::wlist::builtin::BuiltinWList; +use wordle_analyzer::wlist::word::Word; +use wordle_analyzer::{self, game}; + +#[derive(Parser, Clone, Debug)] +#[command(version, about, long_about, author)] +struct Cli { + /// precompute all possibilities for better performance at runtime + #[arg(short, long)] + precompute: bool, + /// how long should the word be? + #[arg(short, long, default_value_t = wordle_analyzer::DEFAULT_WORD_LENGTH)] + length: usize, + /// how many times can we guess? + #[arg(short, long, default_value_t = wordle_analyzer::DEFAULT_MAX_STEPS)] + max_steps: usize, + /// more verbose logs + #[arg(short, long)] + verbose: bool, + /// which solver to use + #[arg(short, long, default_value_t = BuiltinSolvers::default())] + solver: BuiltinSolvers, +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + if cli.verbose { + Logger::build_mini(Some(Level::TRACE))?; + } else { + Logger::build_mini(Some(Level::INFO))?; + } + debug!("dumping CLI: {:#?}", cli); + + let wl = BuiltinWList::default(); + let builder = game::Game::builder(&wl) + .length(cli.length) + .max_steps(cli.max_steps) + .precompute(cli.precompute); + let solver = match cli.solver { + BuiltinSolvers::Naive => { + stupid::StupidSolver::build(&wl)? + }, + _ => todo!() + }; + let mut game = builder.build()?; + + debug!("{game:#?}"); + + let mut response: GuessResponse; + let mut guess: Word; + loop { + response = solver.play(&mut game)?; + println!("{response}"); + + if response.finished() { + break; + } + } + if response.won() { + println!("You win! You took {} guesses.", game.step() - 1); + } else { + println!("You lose! The solution was {:?}.", game.solution()); + } + + Ok(()) } diff --git a/src/error.rs b/src/error.rs index 74c92f6..8b1b880 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,9 @@ pub enum Error { #[from] source: anyhow::Error, }, + // for `FromStr` of `BuiltinSolver` + #[error("Unknown builtin solver")] + UnknownBuiltinSolver } #[derive(Debug, Clone, Error)] diff --git a/src/game/mod.rs b/src/game/mod.rs index 70ee679..f81e9da 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -7,6 +7,8 @@ use libpt::log::debug; pub mod response; use response::GuessResponse; +pub mod summary; + use self::response::Status; #[derive(Debug, Clone, PartialEq)] @@ -26,8 +28,8 @@ where impl<'wl, WL: WordList> Game<'wl, WL> { /// get a new [`GameBuilder`] - pub fn builder() -> GameBuilder { - GameBuilder::default() + pub fn builder(wl: &'wl WL) -> GameBuilder<'wl, WL> { + GameBuilder::new(wl) } /// Create a [Game] of wordle /// @@ -97,7 +99,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> { evaluation.push((c, status)); } - let mut response = GuessResponse::new(guess, evaluation, self.step, self.max_steps); + let mut response = GuessResponse::new(guess, evaluation, &self); self.finished = response.finished(); Ok(response) } @@ -113,6 +115,14 @@ impl<'wl, WL: WordList> Game<'wl, WL> { pub fn step(&self) -> usize { self.step } + + pub fn finished(&self) -> bool { + self.finished + } + + pub fn max_steps(&self) -> usize { + self.max_steps + } } /// Build and Configure a [`Game`] @@ -126,9 +136,11 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// /// ``` /// # use wordle_analyzer::game::*; +/// # use wordle_analyzer::wlist::builtin::BuiltinWList; /// # use anyhow::Result; /// # fn main() -> Result<()> { -/// let game: Game = GameBuilder::default() +/// let wl = BuiltinWList::default(); +/// let game: Game<_> = GameBuilder::new(&wl) /// .build()?; /// # Ok(()) /// # } @@ -136,9 +148,11 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// /// ``` /// # use wordle_analyzer::game::*; +/// # use wordle_analyzer::wlist::builtin::BuiltinWList; /// # use anyhow::Result; /// # fn main() -> Result<()> { -/// let game: Game = Game::builder() +/// let wl = BuiltinWList::default(); +/// let game: Game<_> = Game::builder(&wl) /// .length(5) /// .precompute(false) /// .max_steps(6) @@ -148,19 +162,31 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// ``` /// #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct GameBuilder { +pub struct GameBuilder<'wl, WL: WordList> { length: usize, precompute: bool, max_steps: usize, - wordlist: WL, + wordlist: &'wl WL, } -impl GameBuilder { +impl<'wl, WL: WordList> GameBuilder<'wl, WL> { + /// make a new [GameBuilder] + /// + /// We need a [WordList], so provide one here. + pub fn new(wl: &'wl WL) -> Self { + Self { + length: super::DEFAULT_WORD_LENGTH, + precompute: false, + max_steps: super::DEFAULT_MAX_STEPS, + wordlist: wl + } + } + /// 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, &self.wordlist)?; + Game::build(self.length, self.precompute, self.max_steps, self.wordlist)?; Ok(game) } @@ -194,19 +220,8 @@ impl GameBuilder { /// /// The builder can be used multiple times. Each [`Game`] will have a immutable reference to /// `wl`. - pub fn wordlist(mut self, wl: WL) -> Self { + pub fn wordlist(mut self, wl: &'wl WL) -> Self { self.wordlist = wl; self } } - -impl Default for GameBuilder { - fn default() -> Self { - Self { - length: super::DEFAULT_WORD_LENGTH, - precompute: false, - max_steps: super::DEFAULT_MAX_STEPS, - wordlist: WL::default(), - } - } -} diff --git a/src/game/response.rs b/src/game/response.rs index a41176a..a74303f 100644 --- a/src/game/response.rs +++ b/src/game/response.rs @@ -1,18 +1,20 @@ -use crate::wlist::word::Word; +use crate::wlist::word::{Word, WordData}; +use crate::wlist::WordList; use anyhow::Ok; use colored::{ColoredString, Colorize}; use libpt::log::debug; use std::fmt::Display; +use super::Game; + pub type Evaluation = Vec<(char, Status)>; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct GuessResponse { guess: Word, evaluation: Evaluation, - step: usize, finish: bool, - win: bool, + solution: WordData, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -23,29 +25,21 @@ pub enum Status { } impl GuessResponse { - pub(crate) fn new( - guess: Word, - status: Vec<(char, Status)>, - step: usize, - max_step: usize, - ) -> Self { - let mut win = false; - let mut finish: bool = if step > max_step { + pub(crate) fn new(guess: Word, status: Vec<(char, Status)>, game: &Game) -> Self { + let finish: bool = if game.step() > game.max_steps() { true } else { let mut matched = true; for p in &status { matched &= p.1 == Status::Matched; } - win = matched; - win + matched }; Self { guess, evaluation: status, - step, finish, - win, + solution: game.solution().clone(), } } @@ -54,7 +48,15 @@ impl GuessResponse { } pub fn won(&self) -> bool { - self.win + self.guess == self.solution.0 + } + + pub fn solution(&self) -> Option { + if self.won() { + Some(self.solution.clone()) + } else { + None + } } } diff --git a/src/game/summary.rs b/src/game/summary.rs new file mode 100644 index 0000000..05d7d12 --- /dev/null +++ b/src/game/summary.rs @@ -0,0 +1,19 @@ +use crate::wlist::WordList; + +use super::Game; + +pub struct Summary<'wl, WL: WordList> { + data: &'wl Vec>, +} + +impl<'wl, WL: WordList> From<&'wl Vec>> for Summary<'wl, WL> { + fn from(value: &'wl Vec>) -> Self { + Summary { data: value } + } +} + +impl<'wl, WL: WordList> From<&'wl mut Vec>> for Summary<'wl, WL> { + fn from(value: &'wl mut Vec>) -> Self { + Summary { data: value } + } +} diff --git a/src/lib.rs b/src/lib.rs index 9d567e0..05b16ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,6 @@ pub mod bench; pub mod error; #[cfg(feature = "game")] pub mod game; -#[cfg(feature = "solvers")] -pub mod solvers; +#[cfg(feature = "solve")] +pub mod solve; pub mod wlist; diff --git a/src/solve/mod.rs b/src/solve/mod.rs new file mode 100644 index 0000000..eb4f1c2 --- /dev/null +++ b/src/solve/mod.rs @@ -0,0 +1,56 @@ +use std::{fmt::Display, str::FromStr}; + +use crate::{ + error::{Error, WResult}, + game::{response::*, summary::Summary, Game}, + wlist::{word::WordData, WordList}, +}; + +pub mod naive; +pub mod stupid; + +pub trait Solver<'wl, WL: WordList>: Clone + std::fmt::Debug { + fn build(wordlist: &'wl WL) -> WResult; + fn play(&self, game: &mut Game<'wl, WL>) -> WResult; + fn solve(&self, game: &mut Game<'wl, WL>) -> WResult> { + let mut resp: GuessResponse; + loop { + resp = self.play(game)?; + if game.finished() { + break; + } + } + Ok(resp.solution()) + } + fn play_n(&self, games: &'wl mut Vec>) -> WResult> { + for game in games.iter_mut() { + self.play(game)?; + } + Ok(Summary::from(games)) + } +} + +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum BuiltinSolvers { + #[default] + Naive, + Stupid, +} + +impl FromStr for BuiltinSolvers { + type Err = Error; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "naive" => Ok(Self::Naive), + "stupid" => Ok(Self::Stupid), + _ => Err(Self::Err::UnknownBuiltinSolver), + } + } +} + +impl Display for BuiltinSolvers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/src/solvers/naive/mod.rs b/src/solve/naive/mod.rs similarity index 100% rename from src/solvers/naive/mod.rs rename to src/solve/naive/mod.rs diff --git a/src/solve/stupid/mod.rs b/src/solve/stupid/mod.rs new file mode 100644 index 0000000..17f6c33 --- /dev/null +++ b/src/solve/stupid/mod.rs @@ -0,0 +1,20 @@ +use crate::wlist::WordList; + +use super::Solver; + +#[derive(Debug, Clone)] +pub struct StupidSolver<'wl, WL> { + wl: &'wl WL, +} + +impl<'wl, WL: WordList> Solver<'wl, WL> for StupidSolver<'wl, WL> { + fn build(wordlist: &'wl WL) -> crate::error::WResult { + Ok(Self { wl: wordlist }) + } + fn play( + &self, + game: &mut crate::game::Game<'wl, WL>, + ) -> crate::error::WResult { + todo!() + } +} diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs deleted file mode 100644 index e6a1f52..0000000 --- a/src/solvers/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index e69de29..0000000