From 94d16b345f2c37e3f645ead75948584ae5d64ed8 Mon Sep 17 00:00:00 2001 From: PlexSheep Date: Fri, 22 Mar 2024 00:01:40 +0100 Subject: [PATCH] workle --- Cargo.toml | 11 ++++--- src/bin/game/cli.rs | 69 ++++++++++++++++++++++++++++++++++++++------ src/error.rs | 26 +++++++++++++++++ src/game/mod.rs | 62 ++++++++++++++++++++++++++++++++------- src/game/response.rs | 68 ++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 1 + 6 files changed, 210 insertions(+), 27 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.toml b/Cargo.toml index 1e39de9..35b7248 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,22 +17,25 @@ default = ["game", "bench", "tui", "solvers", "builtin_wlist", "serde"] builtin_wlist = ["dep:serde_json", "serde"] game = ["builtin_wlist"] solvers = [] -tui = ["game"] +tui = ["cli"] +cli = ["dep:clap"] bench = [] serde = ["dep:serde"] [dependencies] anyhow = "1.0.81" -clap = { version = "4.5.3", features = ["derive"] } +clap = { version = "4.5.3", features = ["derive"], optional = true } +colored = { version = "2.1.0", optional = false } libpt = "0.4.2" rand = "0.8.5" serde = { version = "1.0.197", optional = true, features = ["serde_derive"] } -serde_json = {version = "1.0.114", optional = true} +serde_json = { version = "1.0.114", optional = true } +thiserror = "1.0.58" [[bin]] name = "wordlec" path = "src/bin/game/cli.rs" -required-features = ["game"] +required-features = ["game", "cli"] [[bin]] name = "wordlet" diff --git a/src/bin/game/cli.rs b/src/bin/game/cli.rs index 9d64210..4f9dd2b 100644 --- a/src/bin/game/cli.rs +++ b/src/bin/game/cli.rs @@ -1,12 +1,20 @@ #![warn(clippy::all)] -#![warn(missing_docs)] +// #![warn(missing_docs)] #![warn(missing_debug_implementations)] +use std::io::{Read, Write}; + +use anyhow::anyhow; 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::wlist::builtin::BuiltinWList; +use wordle_analyzer::wlist::word::Word; use wordle_analyzer::{self, game}; +use colored::Colorize; + #[derive(Parser, Clone, Debug)] #[command(version, about, long_about, author)] struct Cli { @@ -19,31 +27,74 @@ struct Cli { /// 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, } fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - Logger::build_mini(Some(Level::TRACE))?; + if cli.verbose { + Logger::build_mini(Some(Level::TRACE))?; + } else { + Logger::build_mini(Some(Level::INFO))?; + } debug!("dumping CLI: {:#?}", cli); - let game: Game = game::Game::builder() + let mut game = game::Game::::builder() .length(cli.length) .precompute(cli.precompute) .build()?; - debug!("game: {:#?}", game); + debug!("{game:#?}"); + + let mut response: GuessResponse; + let mut guess: Word; + loop { + guess = match get_word(&cli, game.step()) { + Ok(g) => g, + Err(err) => match err.downcast::() { + Ok(game_err) => match game_err { + GameError::GuessHasWrongLength => { + println!("wring length: must be {} long", game.length()); + continue; + } + _ => { + return Err(game_err.into()); + } + }, + Err(err) => return Err(anyhow!(err.to_string())), + }, + }; + response = game.guess(guess)?; + + 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(()) } -fn get_word(cli: &Cli) -> String { - let mut word = String::new(); +fn get_word(cli: &Cli, step: usize) -> anyhow::Result { + let mut word = Word::new(); + let stdin = std::io::stdin(); + let mut stdout = std::io::stdout(); // TODO: get user input // TODO: validate user input - todo!(); + print!("guess {step} > "); + stdout.flush()?; + stdin.read_line(&mut word)?; + word = word.replace('\n', ""); - assert_eq!(word.len(), cli.length); - word + Ok(word) } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..d7683fe --- /dev/null +++ b/src/error.rs @@ -0,0 +1,26 @@ +use thiserror::Error; + +pub type Result = std::result::Result; +pub type GameResult = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error("GameError")] + GameError { + #[from] + source: GameError, + }, + #[error(transparent)] + Other { + #[from] + source: anyhow::Error, + }, +} + +#[derive(Debug, Clone, Error)] +pub enum GameError { + #[error("The guess has the wrong length")] + GuessHasWrongLength, + #[error("The game is finished but a guess is being made")] + TryingToPlayAFinishedGame, +} diff --git a/src/game/mod.rs b/src/game/mod.rs index 2c76c4b..9060a03 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,9 +1,11 @@ -use crate::wlist::word::{Frequency, Solution, Word}; +use crate::error::*; +use crate::wlist::word::{Solution, Word}; use crate::wlist::WordList; -use self::response::GuessResponse; - pub mod response; +use response::GuessResponse; + +use self::response::Status; #[derive(Debug, Clone, PartialEq)] pub struct Game @@ -16,6 +18,7 @@ where step: usize, solution: Solution, wordlist: WL, + finished: bool, } impl Game { @@ -40,15 +43,16 @@ impl Game { precompute: bool, max_steps: usize, wlist: WL, - ) -> anyhow::Result { + ) -> GameResult { let solution = wlist.rand_solution(); - let mut game = Game { + let game = Game { length, precompute, max_steps, - step: 0, + step: 1, solution, wordlist: wlist, + finished: false, }; Ok(game) @@ -56,12 +60,50 @@ impl Game { pub fn reset(mut self) -> Self { self.solution = self.wordlist.rand_solution(); - self.step = 0; + self.step = 1; + self.finished = false; self } - pub fn guess(&mut self, word: Word) -> anyhow::Result { - todo!() + pub fn guess(&mut self, guess: Word) -> GameResult { + if guess.len() != self.length { + return Err(GameError::GuessHasWrongLength); + } + if self.finished || self.step > self.max_steps { + return Err(GameError::TryingToPlayAFinishedGame); + } + self.step += 1; + + let mut compare_solution = self.solution.0.clone(); + let mut evaluation = Vec::new(); + let mut status: Status; + for (idx, c) in guess.chars().enumerate() { + if compare_solution.chars().nth(idx) == Some(c) { + status = Status::Matched; + compare_solution = compare_solution.replace(c, "_"); + } else if compare_solution.contains(c) { + status = Status::Exists; + compare_solution = compare_solution.replacen(c, "_", 1); + } else { + status = Status::None + } + evaluation.push((c, status)); + } + + let mut response = GuessResponse::new(guess, evaluation, self.step, self.max_steps); + Ok(response) + } + + pub fn length(&self) -> usize { + self.length + } + + pub fn solution(&self) -> &Solution { + &self.solution + } + + pub fn step(&self) -> usize { + self.step } } @@ -107,7 +149,7 @@ pub struct GameBuilder { impl GameBuilder { /// build a [`Game`] with the stored configuration - pub fn build(self) -> anyhow::Result> { + pub fn build(self) -> GameResult> { let game: Game = Game::build(self.length, self.precompute, self.max_steps, WL::default())?; Ok(game) diff --git a/src/game/response.rs b/src/game/response.rs index 965892a..e1fb615 100644 --- a/src/game/response.rs +++ b/src/game/response.rs @@ -1,13 +1,73 @@ use crate::wlist::word::Word; +use anyhow::Ok; +use colored::{ColoredString, Colorize}; +use std::fmt::Display; +#[derive(Debug, Clone, PartialEq, Eq)] pub struct GuessResponse { guess: Word, - status: Vec<(char, Status)>, + evaluation: Vec<(char, Status)>, step: usize, + finish: bool, + win: bool, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Status { - None, - Exists, - Matched, + None = 0, + Exists = 1, + Matched = 2, +} + +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 { + true + } else { + let mut matched = true; + for p in &status { + matched &= p.1 == Status::Matched; + } + win = matched; + win + }; + Self { + guess, + evaluation: status, + step, + finish, + win + } + } + + pub fn finished(&self) -> bool { + self.finish + } + + pub fn won(&self) -> bool { + self.win + } +} + +impl Display for GuessResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for s in &self.evaluation { + write!( + f, + "{}", + match s.1 { + Status::None => s.0.to_string().into(), + Status::Exists => s.0.to_string().yellow(), + Status::Matched => s.0.to_string().green(), + } + )?; + } + std::fmt::Result::Ok(()) + } } diff --git a/src/lib.rs b/src/lib.rs index 22c9942..f5fc075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub const DEFAULT_WORD_LENGTH: usize = 5; /// Default amount of guesses per game pub const DEFAULT_MAX_STEPS: usize = 6; +pub mod error; #[cfg(feature = "bench")] pub mod bench; #[cfg(feature = "game")]