From 44ed7210c4b4f36596113607bb17c915e9b404c5 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 13:33:35 +0200 Subject: [PATCH 01/26] feat(solver): add verbosity flag --- Cargo.toml | 2 +- src/bin/solve/simple.rs | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 68d345b..aadc5a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ anyhow = "1.0.81" chrono = { version = "0.4.37" } clap = { version = "4.5.3", features = ["derive"], optional = true } colored = { version = "2.1.0", optional = false } -libpt = "0.6.0" +libpt = { version = "0.6.0", features = ["cli"] } num_cpus = "1.16.0" rand = "0.8.5" rayon = "1.10.0" diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index e235562..0219d70 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -2,8 +2,13 @@ // #![warn(missing_docs)] #![warn(missing_debug_implementations)] -use clap::Parser; +use clap::{Parser, Subcommand}; +use libpt::cli::{ + repl::{DefaultRepl, Repl}, + strum, +}; use libpt::log::*; +use strum::IntoEnumIterator; use wordle_analyzer::game::response::GuessResponse; @@ -25,8 +30,8 @@ struct Cli { #[arg(short, long, default_value_t = wordle_analyzer::DEFAULT_MAX_STEPS)] max_steps: usize, /// more verbose logs - #[arg(short, long)] - verbose: bool, + #[command(flatten)] + verbose: libpt::cli::args::VerbosityLevel, /// which solver to use #[arg(short, long, default_value_t = BuiltinSolverNames::default())] solver: BuiltinSolverNames, @@ -34,12 +39,13 @@ struct Cli { fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - if cli.verbose { - Logger::builder().set_level(Level::DEBUG).build().unwrap(); - } else { - Logger::builder().set_level(Level::INFO).build().unwrap(); - } - debug!("dumping CLI: {:#?}", cli); + Logger::builder() + .set_level(cli.verbose.level()) + .build() + .unwrap(); + trace!("dumping CLI: {:#?}", cli); + + // let repl = libpt::cli::repl::DefaultRepl::::default(); let wl = BuiltinWList::default(); let builder = game::Game::builder(&wl) -- 2.40.1 From a9bec80efad3b83d59598dccf363107e57f820d4 Mon Sep 17 00:00:00 2001 From: cscherrNT Date: Mon, 22 Jul 2024 11:35:25 +0000 Subject: [PATCH 02/26] automatic cargo CI changes --- src/bin/solve/simple.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 0219d70..b18f5a5 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -2,11 +2,8 @@ // #![warn(missing_docs)] #![warn(missing_debug_implementations)] -use clap::{Parser, Subcommand}; -use libpt::cli::{ - repl::{DefaultRepl, Repl}, - strum, -}; +use clap::Parser; +use libpt::cli::{repl::Repl, strum}; use libpt::log::*; use strum::IntoEnumIterator; -- 2.40.1 From 6a9a99aa5113776a9f9ed3a1666e769db0257c69 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 14:11:29 +0200 Subject: [PATCH 03/26] refactor(solve): try to hack the idea of "idk the solution yet" into the game --- Cargo.toml | 1 + src/bin/solve/simple.rs | 86 +++++++++++++++++++++++++++++++++++++++-- src/game/mod.rs | 44 +++++++++++---------- 3 files changed, 107 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aadc5a2..7f7dd9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ rayon = "1.10.0" regex = "1.10.3" serde = { version = "1.0.197", optional = true, features = ["serde_derive"] } serde_json = { version = "1.0.114", optional = true } +strum = "0.26.3" # serde_with = "3.7.0" thiserror = "1.0.58" diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 0219d70..17784fc 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -2,15 +2,19 @@ // #![warn(missing_docs)] #![warn(missing_debug_implementations)] +use std::process::exit; + use clap::{Parser, Subcommand}; use libpt::cli::{ + console::style, + indicatif, repl::{DefaultRepl, Repl}, strum, }; use libpt::log::*; -use strum::IntoEnumIterator; +use strum::{EnumIter, IntoEnumIterator}; -use wordle_analyzer::game::response::GuessResponse; +use wordle_analyzer::game::{response::GuessResponse, Game, GameBuilder}; use wordle_analyzer::solve::{BuiltinSolverNames, Solver}; use wordle_analyzer::wlist::builtin::BuiltinWList; @@ -35,6 +39,30 @@ struct Cli { /// which solver to use #[arg(short, long, default_value_t = BuiltinSolverNames::default())] solver: BuiltinSolverNames, + + /// set if the solver should play a full native game without interaction + #[arg(short, long)] + non_interactive: bool, +} + +#[derive(Subcommand, Debug, EnumIter, Clone)] +enum ReplCommand { + /// Let the user input the response to the last guess + /// + /// Format: + /// + /// 'x' means wrong character + /// + /// 'p' means present character + /// + /// 'c' means correct character + Response { encoded: String }, + /// Let the user input a word they guessed + Guess { your_guess: String }, + /// Let the solver make a guess + Solve, + /// Leave the Repl + Exit, } fn main() -> anyhow::Result<()> { @@ -45,8 +73,59 @@ fn main() -> anyhow::Result<()> { .unwrap(); trace!("dumping CLI: {:#?}", cli); - // let repl = libpt::cli::repl::DefaultRepl::::default(); + if cli.non_interactive { + play_native_non_interactive(cli)?; + exit(0); + } + help_guess_interactive(cli) +} +fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { + let wl = BuiltinWList::default(); + let builder = game::Game::builder(&wl) + .length(cli.length) + .max_steps(cli.max_steps) + .precompute(cli.precompute); + let solver = cli.solver.to_solver(&wl); + let mut game = builder.build()?; + + let mut repl = libpt::cli::repl::DefaultRepl::::default(); + + debug!("entering the repl"); + loop { + // repl.step() should be at the start of your loop + // It is here that the repl will get the user input, validate it, and so on + match repl.step() { + Ok(c) => c, + Err(e) => { + // if the user requested the help, print in blue, otherwise in red as it's just an + // error + if let libpt::cli::repl::error::Error::Parsing(e) = &e { + if e.kind() == clap::error::ErrorKind::DisplayHelp { + println!("{}", style(e).cyan()); + continue; + } + } + println!("{}", style(e).red().bold()); + continue; + } + }; + + // now we can match our defined commands + // + // only None if the repl has not stepped yet + match repl.command().to_owned().unwrap() { + ReplCommand::Exit => break, + ReplCommand::Guess { your_guess } => { + println!("{}", game.guess(your_guess)?) + } + _ => todo!(), + } + } + Ok(()) +} + +fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> { let wl = BuiltinWList::default(); let builder = game::Game::builder(&wl) .length(cli.length) @@ -72,6 +151,5 @@ fn main() -> anyhow::Result<()> { } else { println!("You lose! The solution was {:?}.", game.solution()); } - Ok(()) } diff --git a/src/game/mod.rs b/src/game/mod.rs index e61f1ce..cec0e30 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -20,7 +20,7 @@ where precompute: bool, max_steps: usize, step: usize, - solution: WordData, + solution: Option, wordlist: &'wl WL, finished: bool, responses: Vec, @@ -57,7 +57,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> { precompute, max_steps, step: 0, - solution, + solution: Some(solution), wordlist: wlist, finished: false, responses: Vec::new(), @@ -72,6 +72,8 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// A [GuessResponse] will be formulated, showing us which letters are correctly placed, in the /// solution, or just wrong. /// + /// Note that you do not need to use the [GuessResponse], it is appended to the game state. + /// /// # Errors /// /// This function will return an error if the length of the [Word] is wrong It will also error @@ -88,34 +90,36 @@ impl<'wl, WL: WordList> Game<'wl, WL> { } 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.replace_range(idx..idx + 1, "_"); - } 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 response = GuessResponse::new(guess, evaluation, self); self.responses.push(response.clone()); self.finished = response.finished(); Ok(response) } + pub fn evaluate(mut solution: WordData, guess: Word) -> Vec<()> { + let mut evaluation = Vec::new(); + let mut status: Status; + for (idx, c) in guess.chars().enumerate() { + if solution.0.chars().nth(idx) == Some(c) { + status = Status::Matched; + solution.0.replace_range(idx..idx + 1, "_"); + } else if solution.0.contains(c) { + status = Status::Exists; + solution.0 = solution.0.replacen(c, "_", 1); + } else { + status = Status::None + } + evaluation.push((c, status)); + } + todo!() + } + pub fn length(&self) -> usize { self.length } - pub fn solution(&self) -> &WordData { - &self.solution + pub fn solution(&self) -> Option<&WordData> { + self.solution.as_ref() } pub fn step(&self) -> usize { -- 2.40.1 From 1f8b85a152fffe3fc9da0db561ecd20abcc3a77d Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 14:47:11 +0200 Subject: [PATCH 04/26] refactor: game without solution seems to become possible? --- src/bin/game/cli.rs | 2 +- src/bin/solve/simple.rs | 20 +++++++---- src/game/mod.rs | 19 ++++++++--- src/game/response.rs | 73 ++++++++++++++++++++++++++--------------- src/solve/mod.rs | 2 +- src/solve/naive/mod.rs | 8 ++++- 6 files changed, 84 insertions(+), 40 deletions(-) diff --git a/src/bin/game/cli.rs b/src/bin/game/cli.rs index 1001e44..e8bbf14 100644 --- a/src/bin/game/cli.rs +++ b/src/bin/game/cli.rs @@ -51,7 +51,7 @@ fn main() -> anyhow::Result<()> { let mut guess: Word; loop { guess = get_word(&cli, game.step())?; - response = match game.guess(guess) { + response = match game.guess(guess, None) { Ok(r) => r, Err(err) => match err { GameError::GuessHasWrongLength(len) => { diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 6c0ab5c..3e80e8f 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -2,11 +2,13 @@ // #![warn(missing_docs)] #![warn(missing_debug_implementations)] -use clap::Parser; +use clap::{Parser, Subcommand}; +use libpt::cli::console::style; use libpt::cli::{repl::Repl, strum}; use libpt::log::*; use strum::{EnumIter, IntoEnumIterator}; +use wordle_analyzer::game::response::Evaluation; use wordle_analyzer::game::{response::GuessResponse, Game, GameBuilder}; use wordle_analyzer::solve::{BuiltinSolverNames, Solver}; @@ -50,8 +52,11 @@ enum ReplCommand { /// /// 'c' means correct character Response { encoded: String }, - /// Let the user input a word they guessed - Guess { your_guess: String }, + /// Let the user input a word and the response for that word + Guess { + your_guess: String, + evalutation: Evaluation, + }, /// Let the solver make a guess Solve, /// Leave the Repl @@ -68,7 +73,7 @@ fn main() -> anyhow::Result<()> { if cli.non_interactive { play_native_non_interactive(cli)?; - exit(0); + std::process::exit(0); } help_guess_interactive(cli) } @@ -109,8 +114,11 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { // only None if the repl has not stepped yet match repl.command().to_owned().unwrap() { ReplCommand::Exit => break, - ReplCommand::Guess { your_guess } => { - println!("{}", game.guess(your_guess)?) + ReplCommand::Guess { + your_guess, + evalutation, + } => { + println!("{}", game.guess(your_guess, Some(evalutation))?) } _ => todo!(), } diff --git a/src/game/mod.rs b/src/game/mod.rs index cec0e30..8ce9996 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,3 +1,5 @@ +use core::panic; + use crate::error::*; use crate::wlist::word::{ManyWordsRef, Word, WordData}; use crate::wlist::WordList; @@ -9,7 +11,7 @@ use response::GuessResponse; pub mod summary; -use self::response::Status; +use self::response::{Evaluation, Status}; #[derive(Debug, Clone, PartialEq)] pub struct Game<'wl, WL> @@ -78,7 +80,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// /// 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 { + pub fn guess(&mut self, guess: Word, eval: Option) -> GameResult { if guess.len() != self.length { return Err(GameError::GuessHasWrongLength(guess.len())); } @@ -90,13 +92,20 @@ impl<'wl, WL: WordList> Game<'wl, WL> { } self.step += 1; - let response = GuessResponse::new(guess, evaluation, self); + let response; + if eval.is_some() && self.solution.is_none() { + response = GuessResponse::new(&guess, eval.unwrap(), self); + } else if let Some(solution) = self.solution.clone() { + response = GuessResponse::new(&guess, Self::evaluate(solution, &guess), self); + } else { + panic!("there is neither an evaluation nor a predefined solution for this guess"); + } self.responses.push(response.clone()); self.finished = response.finished(); Ok(response) } - pub fn evaluate(mut solution: WordData, guess: Word) -> Vec<()> { + pub fn evaluate(mut solution: WordData, guess: &Word) -> Evaluation { let mut evaluation = Vec::new(); let mut status: Status; for (idx, c) in guess.chars().enumerate() { @@ -111,7 +120,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> { } evaluation.push((c, status)); } - todo!() + evaluation.into() } pub fn length(&self) -> usize { diff --git a/src/game/response.rs b/src/game/response.rs index 2395e78..966c9a0 100644 --- a/src/game/response.rs +++ b/src/game/response.rs @@ -3,7 +3,9 @@ use crate::wlist::WordList; use colored::Colorize; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use std::convert::Infallible; use std::fmt::Display; +use std::str::FromStr; use super::Game; @@ -13,15 +15,42 @@ pub struct AtomicEvaluation { char: char, status: Status, } -pub type Evaluation = Vec<(char, Status)>; +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Evaluation { + inner: Vec, +} +pub type EvaluationUnit = (char, Status); + +impl IntoIterator for Evaluation { + type Item = EvaluationUnit; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} + +impl From> for Evaluation { + fn from(value: Vec) -> Self { + Self { inner: value } + } +} + +impl FromStr for Evaluation { + type Err = Infallible; + fn from_str(s: &str) -> Result { + // TODO: make this proper + Ok(vec![('x', Status::None)].into()) + } +} #[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct GuessResponse { guess: Word, evaluation: Evaluation, finish: bool, - solution: WordData, + solution: Option, step: usize, max_steps: usize, } @@ -35,24 +64,16 @@ pub enum Status { } impl GuessResponse { - pub(crate) fn new( - guess: Word, - status: Vec<(char, Status)>, - game: &Game, - ) -> Self { - let finish: bool = if game.step() > game.max_steps() { - true - } else { - guess == game.solution().0 - }; - Self { - guess, + pub(crate) fn new(guess: &Word, status: Evaluation, game: &Game) -> Self { + let new = Self { + guess: guess.to_owned(), evaluation: status, - finish, - solution: game.solution().clone(), + finish: game.step() > game.max_steps(), + solution: game.solution().cloned(), step: game.step(), max_steps: game.max_steps(), - } + }; + new } pub fn finished(&self) -> bool { @@ -60,18 +81,18 @@ impl GuessResponse { } pub fn won(&self) -> bool { - self.guess == self.solution.0 + let mut ok = true; + for i in self.evaluation.clone().into_iter() { + ok &= i.1 == Status::Matched + } + ok } pub fn solution(&self) -> Option { - if self.won() { - Some(self.solution.clone()) - } else { - None - } + self.solution.clone() } - pub fn evaluation(&self) -> &[(char, Status)] { + pub fn evaluation(&self) -> &Evaluation { &self.evaluation } @@ -90,7 +111,7 @@ impl GuessResponse { impl Display for GuessResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for s in &self.evaluation { + for s in self.evaluation.clone().into_iter() { write!( f, "{}", diff --git a/src/solve/mod.rs b/src/solve/mod.rs index d398f4f..5ef34c9 100644 --- a/src/solve/mod.rs +++ b/src/solve/mod.rs @@ -51,7 +51,7 @@ pub trait Solver<'wl, WL: WordList>: Clone + std::fmt::Debug + Sized + Sync { /// /// This function will return an error if [guess_for](Solver::guess_for) fails. fn make_a_move(&self, game: &mut Game<'wl, WL>) -> WResult { - Ok(game.guess(self.guess_for(game))?) + Ok(game.guess(self.guess_for(game), None)?) } /// Play a [Game] and return the last [GuessResponse]. /// diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index bfe7625..4a01e9a 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -21,7 +21,13 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { let mut other_chars: Vec = Vec::new(); let response = game.last_response(); if response.is_some() { - for (idx, p) in response.unwrap().evaluation().iter().enumerate() { + for (idx, p) in response + .unwrap() + .evaluation() + .clone() + .into_iter() + .enumerate() + { if p.1 == Status::Matched { pattern.replace_range(idx..idx + 1, &p.0.to_string()); } else if p.1 == Status::Exists { -- 2.40.1 From 8a48498182411317bbcc7cc1d41f28837a8b2022 Mon Sep 17 00:00:00 2001 From: cscherrNT Date: Mon, 22 Jul 2024 12:49:03 +0000 Subject: [PATCH 05/26] automatic cargo CI changes --- src/bin/solve/simple.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 3e80e8f..450cdc9 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -9,7 +9,7 @@ use libpt::log::*; use strum::{EnumIter, IntoEnumIterator}; use wordle_analyzer::game::response::Evaluation; -use wordle_analyzer::game::{response::GuessResponse, Game, GameBuilder}; +use wordle_analyzer::game::response::GuessResponse; use wordle_analyzer::solve::{BuiltinSolverNames, Solver}; use wordle_analyzer::wlist::builtin::BuiltinWList; -- 2.40.1 From 36347d16615a1569fe5dbf26ae8726fc1caa8afa Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 14:54:22 +0200 Subject: [PATCH 06/26] refactor(evaluation): move evaluation to a new module --- src/game/evaluation.rs | 33 +++++++++++++++++++++++++++++++++ src/game/mod.rs | 3 +++ 2 files changed, 36 insertions(+) create mode 100644 src/game/evaluation.rs diff --git a/src/game/evaluation.rs b/src/game/evaluation.rs new file mode 100644 index 0000000..d99cb4d --- /dev/null +++ b/src/game/evaluation.rs @@ -0,0 +1,33 @@ +use std::convert::Infallible; +use std::str::FromStr; + +use super::response::Status; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Evaluation { + inner: Vec, +} +pub type EvaluationUnit = (char, Status); + +impl IntoIterator for Evaluation { + type Item = EvaluationUnit; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} + +impl From> for Evaluation { + fn from(value: Vec) -> Self { + Self { inner: value } + } +} + +impl FromStr for Evaluation { + type Err = Infallible; + fn from_str(s: &str) -> Result { + // TODO: make this proper + Ok(vec![('x', Status::None)].into()) + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs index 8ce9996..a1f36d4 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -9,6 +9,9 @@ use libpt::log::{debug, trace}; pub mod response; use response::GuessResponse; +pub mod evaluation; +use evaluation::*; + pub mod summary; use self::response::{Evaluation, Status}; -- 2.40.1 From 79715e5374259ffdb7e7f8aa4f5cdb6131314d59 Mon Sep 17 00:00:00 2001 From: cscherrNT Date: Mon, 22 Jul 2024 12:56:07 +0000 Subject: [PATCH 07/26] automatic cargo CI changes --- src/game/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index a1f36d4..117edd5 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -10,7 +10,6 @@ pub mod response; use response::GuessResponse; pub mod evaluation; -use evaluation::*; pub mod summary; -- 2.40.1 From 0b83b26b2092d412c01918b4a439e3ff6858c3eb Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 15:15:05 +0200 Subject: [PATCH 08/26] feat(solver): interactive input of responses #3 --- src/bin/solve/simple.rs | 4 ++-- src/game/evaluation.rs | 10 +++++++--- src/game/mod.rs | 25 +++++++++++++++++------- src/game/response.rs | 43 ++++++++++++++--------------------------- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 450cdc9..c5573b1 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -8,7 +8,7 @@ use libpt::cli::{repl::Repl, strum}; use libpt::log::*; use strum::{EnumIter, IntoEnumIterator}; -use wordle_analyzer::game::response::Evaluation; +use wordle_analyzer::game::evaluation::Evaluation; use wordle_analyzer::game::response::GuessResponse; use wordle_analyzer::solve::{BuiltinSolverNames, Solver}; @@ -80,7 +80,7 @@ fn main() -> anyhow::Result<()> { fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { let wl = BuiltinWList::default(); - let builder = game::Game::builder(&wl) + let builder = game::GameBuilder::new(&wl, false) .length(cli.length) .max_steps(cli.max_steps) .precompute(cli.precompute); diff --git a/src/game/evaluation.rs b/src/game/evaluation.rs index d99cb4d..5f3b778 100644 --- a/src/game/evaluation.rs +++ b/src/game/evaluation.rs @@ -3,11 +3,12 @@ use std::str::FromStr; use super::response::Status; +pub type EvaluationUnit = (char, Status); + #[derive(Debug, Clone, PartialEq, Default)] pub struct Evaluation { inner: Vec, } -pub type EvaluationUnit = (char, Status); impl IntoIterator for Evaluation { type Item = EvaluationUnit; @@ -27,7 +28,10 @@ impl From> for Evaluation { impl FromStr for Evaluation { type Err = Infallible; fn from_str(s: &str) -> Result { - // TODO: make this proper - Ok(vec![('x', Status::None)].into()) + let mut v: Vec = Vec::new(); + for c in s.chars() { + v.push((c, Status::from(c))) + } + Ok(v.into()) } } diff --git a/src/game/mod.rs b/src/game/mod.rs index a1f36d4..f977362 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -14,7 +14,7 @@ use evaluation::*; pub mod summary; -use self::response::{Evaluation, Status}; +use self::response::Status; #[derive(Debug, Clone, PartialEq)] pub struct Game<'wl, WL> @@ -35,7 +35,7 @@ where impl<'wl, WL: WordList> Game<'wl, WL> { /// get a new [`GameBuilder`] pub fn builder(wl: &'wl WL) -> GameBuilder<'wl, WL> { - GameBuilder::new(wl) + GameBuilder::new(wl, true) } /// Create a [Game] of wordle /// @@ -54,15 +54,19 @@ impl<'wl, WL: WordList> Game<'wl, WL> { precompute: bool, max_steps: usize, wlist: &'wl WL, + generate_solution: bool, ) -> GameResult { // TODO: check if the length is in the range bounds of the wordlist - let solution = wlist.rand_solution(); let game: Game<'wl, WL> = Game { length, precompute, max_steps, step: 0, - solution: Some(solution), + solution: if generate_solution { + Some(wlist.rand_solution()) + } else { + None + }, wordlist: wlist, finished: false, responses: Vec::new(), @@ -203,26 +207,33 @@ pub struct GameBuilder<'wl, WL: WordList> { precompute: bool, max_steps: usize, wordlist: &'wl WL, + generate_solution: bool, } 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 { + pub fn new(wl: &'wl WL, generate_solution: bool) -> Self { Self { length: super::DEFAULT_WORD_LENGTH, precompute: false, max_steps: super::DEFAULT_MAX_STEPS, wordlist: wl, + generate_solution, } } /// build a [`Game`] with the stored configuration pub fn build(&'wl self) -> GameResult> { trace!("{:#?}", self); - let game: Game = - Game::build(self.length, self.precompute, self.max_steps, self.wordlist)?; + let game: Game = Game::build( + self.length, + self.precompute, + self.max_steps, + self.wordlist, + self.generate_solution, + )?; Ok(game) } diff --git a/src/game/response.rs b/src/game/response.rs index 966c9a0..58a9915 100644 --- a/src/game/response.rs +++ b/src/game/response.rs @@ -7,7 +7,7 @@ use std::convert::Infallible; use std::fmt::Display; use std::str::FromStr; -use super::Game; +use super::{Evaluation, Game}; #[derive(Debug, Clone, PartialEq, Copy, Eq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -15,34 +15,6 @@ pub struct AtomicEvaluation { char: char, status: Status, } -#[derive(Debug, Clone, PartialEq, Default)] -pub struct Evaluation { - inner: Vec, -} -pub type EvaluationUnit = (char, Status); - -impl IntoIterator for Evaluation { - type Item = EvaluationUnit; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.inner.into_iter() - } -} - -impl From> for Evaluation { - fn from(value: Vec) -> Self { - Self { inner: value } - } -} - -impl FromStr for Evaluation { - type Err = Infallible; - fn from_str(s: &str) -> Result { - // TODO: make this proper - Ok(vec![('x', Status::None)].into()) - } -} #[derive(Debug, Clone, PartialEq)] // #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -63,6 +35,19 @@ pub enum Status { Matched = 2, } +impl From for Status { + fn from(value: char) -> Self { + let value = value.to_ascii_lowercase(); // let's not deal with unicode here, wordle is + // ASCII centric anyway + match value { + 'x' => Self::None, + 'f' | 'e' => Self::Exists, + 'c' | 'm' => Self::Matched, + _ => Self::None, + } + } +} + impl GuessResponse { pub(crate) fn new(guess: &Word, status: Evaluation, game: &Game) -> Self { let new = Self { -- 2.40.1 From 42323410f54ce4f5cc645044325f198c3318f33d Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 16:06:41 +0200 Subject: [PATCH 09/26] feat(solver): show the gamestate --- src/bin/solve/simple.rs | 31 +++++++++++++++++++++++++++---- src/game/evaluation.rs | 22 ++++++++++++++++++++++ src/game/mod.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index c5573b1..d0d13cf 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -44,21 +44,31 @@ struct Cli { enum ReplCommand { /// Let the user input the response to the last guess /// - /// Format: + Response { encoded: String }, + /// Let the user input a word and the response for that word + /// + /// Evaluation Format: /// /// 'x' means wrong character /// /// 'p' means present character /// /// 'c' means correct character - Response { encoded: String }, - /// Let the user input a word and the response for that word + /// + /// Example: + /// + /// 'xxxcc' means the first 3 chars are wrong but the second 2 chars are correct + /// + /// 'xppxc' means the first character is wrong, the next two characters are present, the last + /// is correct Guess { your_guess: String, evalutation: Evaluation, }, /// Let the solver make a guess Solve, + /// Show the current state of the game + Show, /// Leave the Repl Exit, } @@ -114,11 +124,24 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { // only None if the repl has not stepped yet match repl.command().to_owned().unwrap() { ReplCommand::Exit => break, + ReplCommand::Show => { + println!("{}", game); + } + ReplCommand::Solve => { + let best_guess = solver.guess_for(&game); + println!("best guess: {best_guess}"); + } ReplCommand::Guess { your_guess, evalutation, } => { - println!("{}", game.guess(your_guess, Some(evalutation))?) + let guess = game.guess(your_guess, Some(evalutation)); + if guess.is_err() { + eprintln!("{}", style(guess.unwrap_err()).red().bold()); + continue; + } + println!("{}", guess.unwrap()); + debug!("current gamestate: {game:#?}"); } _ => todo!(), } diff --git a/src/game/evaluation.rs b/src/game/evaluation.rs index 5f3b778..1fa7603 100644 --- a/src/game/evaluation.rs +++ b/src/game/evaluation.rs @@ -1,6 +1,11 @@ use std::convert::Infallible; +use std::fmt::Display; use std::str::FromStr; +use libpt::cli::console::{style, StyledObject}; + +use crate::wlist::word::Word; + use super::response::Status; pub type EvaluationUnit = (char, Status); @@ -10,6 +15,23 @@ pub struct Evaluation { inner: Vec, } +impl Evaluation { + pub(crate) fn colorized_display(&self, guess: &Word) -> Vec> { + assert_eq!(guess.len(), self.inner.len()); + let mut buf = Vec::new(); + for (i, e) in self.inner.iter().enumerate() { + let mut c = style(guess.chars().nth(i).unwrap().to_string()); + if e.1 == Status::Matched { + c = c.green(); + } else if e.1 == Status::Exists { + c = c.yellow(); + } + buf.push(c); + } + buf + } +} + impl IntoIterator for Evaluation { type Item = EvaluationUnit; type IntoIter = std::vec::IntoIter; diff --git a/src/game/mod.rs b/src/game/mod.rs index ec3f626..bcbfdb3 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,9 +1,11 @@ use core::panic; +use std::fmt::Display; use crate::error::*; use crate::wlist::word::{ManyWordsRef, Word, WordData}; use crate::wlist::WordList; +use libpt::cli::console::StyledObject; use libpt::log::{debug, trace}; pub mod response; @@ -13,6 +15,7 @@ pub mod evaluation; pub mod summary; +use self::evaluation::Evaluation; use self::response::Status; #[derive(Debug, Clone, PartialEq)] @@ -271,3 +274,28 @@ impl<'wl, WL: WordList> GameBuilder<'wl, WL> { self } } + +impl<'wl, WL: WordList> Display for Game<'wl, WL> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // TODO: make this actually useful + // TODO: make this actually fancy + write!( + f, + "turn:\t\t{}\nsolution:\t{:?}\nguesses:\t", + self.step(), + self.solution(), + )?; + for s in self + .responses() + .iter() + .map(|v| v.evaluation().to_owned().colorized_display(v.guess())) + { + write!(f, "\"")?; + for si in s { + write!(f, "{si}")?; + } + write!(f, "\", ")?; + } + Ok(()) + } +} -- 2.40.1 From f0afcbe252eaa1de28713b082121a7c4720b82df Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 16:10:50 +0200 Subject: [PATCH 10/26] chore: add fixme comment for #5 --- src/solve/naive/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 4a01e9a..d70b752 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -54,7 +54,9 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { }) .map(|v| v.to_owned()) .collect(); - matches[0].0.to_owned() + matches[0].0.to_owned() // FIXME: panicks in interactive solve, when I insert bullshit and + // pretend that there is a solution #5 It also crashes in + // non-interactive mode } } -- 2.40.1 From 610b404ab7fb86b442ce30b1735eb1efa9491312 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 16:34:35 +0200 Subject: [PATCH 11/26] fix(game): fix #6 and #5 --- src/bin/solve/simple.rs | 2 +- src/error.rs | 11 +++++++++++ src/game/response.rs | 6 ++---- src/solve/mod.rs | 16 +++++++++------- src/solve/naive/mod.rs | 10 ++++++---- src/solve/stupid/mod.rs | 5 +++-- 6 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index d0d13cf..3c8673d 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -128,7 +128,7 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { println!("{}", game); } ReplCommand::Solve => { - let best_guess = solver.guess_for(&game); + let best_guess = solver.guess_for(&game)?; println!("best guess: {best_guess}"); } ReplCommand::Guess { diff --git a/src/error.rs b/src/error.rs index dbed13d..88b34f9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,11 @@ pub enum Error { #[from] source: GameError, }, + #[error("Solver Error")] + SolverError { + #[from] + source: SolverError, + }, #[error("Benchmark Error")] BenchError { #[from] @@ -53,3 +58,9 @@ pub enum BenchError { #[error("Trying to modify a finished report")] ModifyFinishedReport, } + +#[derive(Debug, Clone, Error)] +pub enum SolverError { + #[error("Wordlist has no matches for the gamestate")] + NoMatches, +} diff --git a/src/game/response.rs b/src/game/response.rs index 58a9915..280b0f0 100644 --- a/src/game/response.rs +++ b/src/game/response.rs @@ -21,7 +21,6 @@ pub struct AtomicEvaluation { pub struct GuessResponse { guess: Word, evaluation: Evaluation, - finish: bool, solution: Option, step: usize, max_steps: usize, @@ -50,10 +49,9 @@ impl From for Status { impl GuessResponse { pub(crate) fn new(guess: &Word, status: Evaluation, game: &Game) -> Self { - let new = Self { + let mut new = Self { guess: guess.to_owned(), evaluation: status, - finish: game.step() > game.max_steps(), solution: game.solution().cloned(), step: game.step(), max_steps: game.max_steps(), @@ -62,7 +60,7 @@ impl GuessResponse { } pub fn finished(&self) -> bool { - self.finish + self.step() > self.max_steps() || self.won() } pub fn won(&self) -> bool { diff --git a/src/solve/mod.rs b/src/solve/mod.rs index 5ef34c9..456911f 100644 --- a/src/solve/mod.rs +++ b/src/solve/mod.rs @@ -11,6 +11,7 @@ use crate::{ #[cfg(feature = "builtin")] pub mod naive; +use libpt::log::debug; #[cfg(feature = "builtin")] pub use naive::NaiveSolver; #[cfg(feature = "builtin")] @@ -44,14 +45,14 @@ pub trait Solver<'wl, WL: WordList>: Clone + std::fmt::Debug + Sized + Sync { /// /// Each [Solver] needs to implement this method themselves, many other methods rely on this to /// play the [Game], such as [play](Solver::play) or [solve](Solver::solve). - fn guess_for(&self, game: &Game<'wl, WL>) -> Word; + fn guess_for(&self, game: &Game<'wl, WL>) -> WResult; /// Make a singular step for a [Game] /// /// # Errors /// /// This function will return an error if [guess_for](Solver::guess_for) fails. fn make_a_move(&self, game: &mut Game<'wl, WL>) -> WResult { - Ok(game.guess(self.guess_for(game), None)?) + Ok(game.guess(self.guess_for(game)?, None)?) } /// Play a [Game] and return the last [GuessResponse]. /// @@ -146,10 +147,11 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for AnyBuiltinSolver<'wl, WL> { fn build(wordlist: &'wl WL) -> WResult { Ok(Self::Naive(NaiveSolver::build(wordlist)?)) } - fn guess_for(&self, game: &Game<'wl, WL>) -> Word { - match self { - Self::Naive(solver) => solver.guess_for(game), - Self::Stupid(solver) => solver.guess_for(game), - } + fn guess_for(&self, game: &Game<'wl, WL>) -> WResult { + debug!("solver: {self:?}"); + Ok(match self { + Self::Naive(solver) => solver.guess_for(game)?, + Self::Stupid(solver) => solver.guess_for(game)?, + }) } } diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index d70b752..0cb9313 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -1,5 +1,6 @@ use libpt::log::{info, trace}; +use crate::error::{Error, SolverError, WResult}; use crate::wlist::word::{ManyWordDatas, Word}; use crate::wlist::WordList; @@ -15,7 +16,7 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { info!("using naive solver"); Ok(Self { wl: wordlist }) } - fn guess_for(&self, game: &crate::game::Game) -> Word { + fn guess_for(&self, game: &crate::game::Game) -> WResult { // HACK: hardcoded length let mut pattern: String = String::from("....."); let mut other_chars: Vec = Vec::new(); @@ -54,9 +55,10 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { }) .map(|v| v.to_owned()) .collect(); - matches[0].0.to_owned() // FIXME: panicks in interactive solve, when I insert bullshit and - // pretend that there is a solution #5 It also crashes in - // non-interactive mode + if matches.is_empty() { + return Err(SolverError::NoMatches.into()); + } + Ok(matches[0].0.to_owned()) } } diff --git a/src/solve/stupid/mod.rs b/src/solve/stupid/mod.rs index 2aa1d61..e4df9e7 100644 --- a/src/solve/stupid/mod.rs +++ b/src/solve/stupid/mod.rs @@ -1,5 +1,6 @@ use libpt::log::info; +use crate::error::WResult; use crate::wlist::word::Word; use crate::wlist::WordList; @@ -15,8 +16,8 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for StupidSolver<'wl, WL> { info!("using stupid solver"); Ok(Self { wl: wordlist }) } - fn guess_for(&self, game: &crate::game::Game) -> Word { - self.wl.rand_word().0 + fn guess_for(&self, game: &crate::game::Game) -> WResult { + Ok(self.wl.rand_word().0) } } -- 2.40.1 From e4431ca688055113d631c8dcdfd3cce6147e009e Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 16:40:27 +0200 Subject: [PATCH 12/26] chore: allow missing docs the project is not nearly at a state where thats a real problem --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 05b16ec..9893c0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![warn(clippy::all)] -#![warn(missing_docs)] +// #![warn(missing_docs)] #![warn(missing_debug_implementations)] /// Default letters of a solution word -- 2.40.1 From 70711ad33485b87bcc3a670a034dbd97901bc31c Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Mon, 22 Jul 2024 16:52:08 +0200 Subject: [PATCH 13/26] refactor(solver): debugging interactive --- src/bin/solve/simple.rs | 5 ++++- src/solve/mod.rs | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 3c8673d..622fcc0 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -129,6 +129,7 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { } ReplCommand::Solve => { let best_guess = solver.guess_for(&game)?; + debug!("game state: {game:?}"); println!("best guess: {best_guess}"); } ReplCommand::Guess { @@ -136,12 +137,13 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { evalutation, } => { let guess = game.guess(your_guess, Some(evalutation)); + debug!("your guess: {guess:?}"); if guess.is_err() { eprintln!("{}", style(guess.unwrap_err()).red().bold()); continue; } println!("{}", guess.unwrap()); - debug!("current gamestate: {game:#?}"); + debug!("game state: {game:#?}"); } _ => todo!(), } @@ -164,6 +166,7 @@ fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> { let mut _guess: Word; loop { response = solver.make_a_move(&mut game)?; + debug!("game state: {game:#?}"); println!("{}. guess: {response}", game.step() - 1); if response.finished() { diff --git a/src/solve/mod.rs b/src/solve/mod.rs index 456911f..9a9368c 100644 --- a/src/solve/mod.rs +++ b/src/solve/mod.rs @@ -148,7 +148,6 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for AnyBuiltinSolver<'wl, WL> { Ok(Self::Naive(NaiveSolver::build(wordlist)?)) } fn guess_for(&self, game: &Game<'wl, WL>) -> WResult { - debug!("solver: {self:?}"); Ok(match self { Self::Naive(solver) => solver.guess_for(game)?, Self::Stupid(solver) => solver.guess_for(game)?, -- 2.40.1 From d9d84f8a11044cf55ed1d2a440dd1cdb88eafdca Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Tue, 23 Jul 2024 11:22:12 +0200 Subject: [PATCH 14/26] feat(solve): add wl commands for show and top words --- src/bin/solve/simple.rs | 31 +++++++++++++++++++++++++++++++ src/wlist/builtin.rs | 8 +++++++- src/wlist/mod.rs | 15 ++++++++++++++- src/wlist/word.rs | 1 + 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 622fcc0..8da8854 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -14,6 +14,7 @@ use wordle_analyzer::game::response::GuessResponse; use wordle_analyzer::solve::{BuiltinSolverNames, Solver}; use wordle_analyzer::wlist::builtin::BuiltinWList; use wordle_analyzer::wlist::word::Word; +use wordle_analyzer::wlist::WordList; use wordle_analyzer::{self, game}; #[derive(Parser, Clone, Debug)] @@ -69,10 +70,24 @@ enum ReplCommand { Solve, /// Show the current state of the game Show, + /// Display data about the wordlist + Wl { + #[command(subcommand)] + cmd: WlCommand, + }, /// Leave the Repl Exit, } +#[derive(Subcommand, Debug, EnumIter, Clone, Default)] +enum WlCommand { + #[default] + Stats, + Top { + amount: usize, + }, +} + fn main() -> anyhow::Result<()> { let cli = Cli::parse(); Logger::builder() @@ -124,6 +139,7 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { // only None if the repl has not stepped yet match repl.command().to_owned().unwrap() { ReplCommand::Exit => break, + ReplCommand::Wl { cmd } => wlcommand_handler(&cli, &cmd, &wl)?, ReplCommand::Show => { println!("{}", game); } @@ -151,6 +167,21 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { Ok(()) } +fn wlcommand_handler(cli: &Cli, cmd: &WlCommand, wl: &impl WordList) -> anyhow::Result<()> { + match cmd { + WlCommand::Stats => { + println!("{wl}") + } + WlCommand::Top { amount } => { + println!(); + for s in wl.n_most_likely(*amount).iter() { + println!("\t\"{}\":\t{:.08}%", s.0, s.1 * 100.0); + } + } + } + Ok(()) +} + fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> { let wl = BuiltinWList::default(); let builder = game::Game::builder(&wl) diff --git a/src/wlist/builtin.rs b/src/wlist/builtin.rs index 3204376..116e66f 100644 --- a/src/wlist/builtin.rs +++ b/src/wlist/builtin.rs @@ -1,4 +1,4 @@ -use std::fmt::{write, Debug}; +use std::fmt::{write, Debug, Display}; use serde_json; @@ -54,3 +54,9 @@ impl Debug for BuiltinWList { ) } } + +impl Display for BuiltinWList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:#?}") + } +} diff --git a/src/wlist/mod.rs b/src/wlist/mod.rs index 155ec24..0e7a28e 100644 --- a/src/wlist/mod.rs +++ b/src/wlist/mod.rs @@ -3,6 +3,7 @@ use rand::seq::IteratorRandom; use regex::Regex; use std::collections::HashMap; +use std::fmt::Display; use std::ops::RangeBounds; #[cfg(feature = "builtin")] @@ -14,7 +15,7 @@ use crate::error::WResult; pub type AnyWordlist = Box; -pub trait WordList: Clone + std::fmt::Debug + Default + Sync { +pub trait WordList: Clone + std::fmt::Debug + Default + Sync + Display { fn solutions(&self) -> ManyWordDatas { let wmap = self.wordmap().clone(); let threshold = wmap.threshold(); @@ -41,6 +42,18 @@ pub trait WordList: Clone + std::fmt::Debug + Default + Sync { fn total_freq(&self) -> Frequency { self.wordmap().values().map(|a| a.to_owned()).sum() } + fn sort_likelihood(&self) -> Vec { + let wmap = self.wordmap(); + let mut wpairs: Vec<(_, _)> = wmap.iter().collect(); + wpairs.sort_by(|a, b| a.1.partial_cmp(b.1).unwrap().reverse()); + wpairs + .iter() + .map(|v| (v.0.to_owned(), v.1.to_owned())) + .collect() + } + fn n_most_likely(&self, n: usize) -> Vec { + self.sort_likelihood().into_iter().take(n).collect() + } fn over_threashold(&self) -> WordMap { let wmap = self.wordmap(); let threshold = wmap.threshold(); diff --git a/src/wlist/word.rs b/src/wlist/word.rs index 1defc1f..b4f55ce 100644 --- a/src/wlist/word.rs +++ b/src/wlist/word.rs @@ -10,6 +10,7 @@ pub type Frequency = f64; // PERF: Hash for String is probably a bottleneck pub type Word = String; pub type WordData = (Word, Frequency); +pub type WordDataRef<'wl> = (&'wl Word, &'wl Frequency); pub type ManyWordsRef<'a> = Vec<&'a Word>; pub type ManyWordDatas = Vec<(Word, Frequency)>; -- 2.40.1 From 0c4adba682202868253ecbc043370ffd6dda67d2 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Tue, 23 Jul 2024 11:22:50 +0200 Subject: [PATCH 15/26] test(solve): add test cases for naive and stupid solver --- src/game/mod.rs | 28 ++++++++++++++------ tests/solver.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 tests/solver.rs diff --git a/src/game/mod.rs b/src/game/mod.rs index bcbfdb3..b778469 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -29,7 +29,6 @@ where step: usize, solution: Option, wordlist: &'wl WL, - finished: bool, responses: Vec, // TODO: keep track of the letters the user has tried } @@ -50,8 +49,8 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// /// # Errors /// - /// This function will return an error if . - pub(crate) fn build( + /// No Errors + pub fn build( length: usize, precompute: bool, max_steps: usize, @@ -70,13 +69,17 @@ impl<'wl, WL: WordList> Game<'wl, WL> { None }, wordlist: wlist, - finished: false, responses: Vec::new(), }; Ok(game) } + /// set a solution, can be used for testing + pub fn set_solution(&mut self, sol: Option) { + self.solution = sol; + } + /// Make a new guess /// /// The word will be evaluated against the [solution](Game::solution) of the [Game]. @@ -93,7 +96,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> { if guess.len() != self.length { return Err(GameError::GuessHasWrongLength(guess.len())); } - if self.finished || self.step > self.max_steps { + if self.finished() || self.step > self.max_steps { return Err(GameError::TryingToPlayAFinishedGame); } if self.wordlist.get_word(&guess).is_none() { @@ -110,7 +113,6 @@ impl<'wl, WL: WordList> Game<'wl, WL> { panic!("there is neither an evaluation nor a predefined solution for this guess"); } self.responses.push(response.clone()); - self.finished = response.finished(); Ok(response) } @@ -145,7 +147,17 @@ impl<'wl, WL: WordList> Game<'wl, WL> { } pub fn finished(&self) -> bool { - self.finished + if self.responses().is_empty() { + return false; + } + self.responses().last().unwrap().finished() + } + + pub fn won(&self) -> bool { + if self.responses().is_empty() { + return false; + } + self.responses().last().unwrap().won() } pub fn max_steps(&self) -> usize { @@ -182,7 +194,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// # use anyhow::Result; /// # fn main() -> Result<()> { /// let wl = BuiltinWList::default(); -/// let game: Game<_> = GameBuilder::new(&wl) +/// let game: Game<_> = GameBuilder::new(&wl, true) /// .build()?; /// # Ok(()) /// # } diff --git a/tests/solver.rs b/tests/solver.rs new file mode 100644 index 0000000..580ad97 --- /dev/null +++ b/tests/solver.rs @@ -0,0 +1,69 @@ +use wordle_analyzer::game::Game; +use wordle_analyzer::solve::{AnyBuiltinSolver, NaiveSolver, Solver, StupidSolver}; +use wordle_analyzer::wlist::builtin::BuiltinWList; +use wordle_analyzer::wlist::word::Word; +use wordle_analyzer::wlist::WordList; + +fn wordlist() -> impl WordList { + BuiltinWList::default() +} + +#[test] +fn test_build_builtin_solvers() { + let wl = wordlist(); + let stupid_solver = + AnyBuiltinSolver::Stupid(StupidSolver::build(&wl).expect("could not build naive solver")); + let naive_solver = + AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); +} + +#[test] +fn test_naive_play_predetermined_game() -> anyhow::Result<()> { + let wl = wordlist(); + let sl = + AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); + let mut game = Game::build(5, false, 6, &wl, false)?; + game.set_solution(Some(("nines".into(), 0.002))); // The accuracy is made up but shouldn't + // matter + sl.make_a_move(&mut game)?; + assert_eq!( + game.responses().last().unwrap().guess(), + &Word::from("which") + ); + sl.make_a_move(&mut game)?; + assert_eq!( + game.responses().last().unwrap().guess(), + &Word::from("their") + ); + sl.make_a_move(&mut game)?; + assert_eq!( + game.responses().last().unwrap().guess(), + &Word::from("being") + ); + sl.make_a_move(&mut game)?; + assert_eq!( + game.responses().last().unwrap().guess(), + &Word::from("since") + ); + sl.make_a_move(&mut game)?; + assert_eq!( + game.responses().last().unwrap().guess(), + &Word::from("lines") + ); + sl.make_a_move(&mut game)?; + assert_eq!( + game.responses().last().unwrap().guess(), + &Word::from("mines") + ); + sl.make_a_move(&mut game)?; + assert_eq!( + game.responses().last().unwrap().guess(), + &Word::from("wines") + ); + + // naive is at the moment too bad to solve "nines" + assert!(game.finished()); + assert!(!game.won()); + + Ok(()) +} -- 2.40.1 From 226ad3ea6efc34c9f80800cbd4dbc8affd61ecd4 Mon Sep 17 00:00:00 2001 From: cscherrNT Date: Tue, 23 Jul 2024 09:25:12 +0000 Subject: [PATCH 16/26] automatic cargo CI changes --- src/game/evaluation.rs | 1 - src/game/mod.rs | 1 - src/game/response.rs | 4 +--- src/solve/mod.rs | 1 - src/solve/naive/mod.rs | 2 +- 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/game/evaluation.rs b/src/game/evaluation.rs index 1fa7603..83a790b 100644 --- a/src/game/evaluation.rs +++ b/src/game/evaluation.rs @@ -1,5 +1,4 @@ use std::convert::Infallible; -use std::fmt::Display; use std::str::FromStr; use libpt::cli::console::{style, StyledObject}; diff --git a/src/game/mod.rs b/src/game/mod.rs index b778469..95796ad 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -5,7 +5,6 @@ use crate::error::*; use crate::wlist::word::{ManyWordsRef, Word, WordData}; use crate::wlist::WordList; -use libpt::cli::console::StyledObject; use libpt::log::{debug, trace}; pub mod response; diff --git a/src/game/response.rs b/src/game/response.rs index 280b0f0..8b4bd00 100644 --- a/src/game/response.rs +++ b/src/game/response.rs @@ -3,9 +3,7 @@ use crate::wlist::WordList; use colored::Colorize; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use std::convert::Infallible; use std::fmt::Display; -use std::str::FromStr; use super::{Evaluation, Game}; @@ -49,7 +47,7 @@ impl From for Status { impl GuessResponse { pub(crate) fn new(guess: &Word, status: Evaluation, game: &Game) -> Self { - let mut new = Self { + let new = Self { guess: guess.to_owned(), evaluation: status, solution: game.solution().cloned(), diff --git a/src/solve/mod.rs b/src/solve/mod.rs index 9a9368c..54842e7 100644 --- a/src/solve/mod.rs +++ b/src/solve/mod.rs @@ -11,7 +11,6 @@ use crate::{ #[cfg(feature = "builtin")] pub mod naive; -use libpt::log::debug; #[cfg(feature = "builtin")] pub use naive::NaiveSolver; #[cfg(feature = "builtin")] diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 0cb9313..b5c6581 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -1,6 +1,6 @@ use libpt::log::{info, trace}; -use crate::error::{Error, SolverError, WResult}; +use crate::error::{SolverError, WResult}; use crate::wlist::word::{ManyWordDatas, Word}; use crate::wlist::WordList; -- 2.40.1 From b159cb4439a54cefc9220358068f57e72b335bba Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Tue, 23 Jul 2024 11:27:24 +0200 Subject: [PATCH 17/26] refactor: remove ManyWordData ad ManyWords and so on, useless type aliases --- src/game/mod.rs | 5 ++--- src/solve/naive/mod.rs | 6 +++--- src/wlist/mod.rs | 6 +++--- src/wlist/word.rs | 2 -- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index b778469..497b827 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -2,10 +2,9 @@ use core::panic; use std::fmt::Display; use crate::error::*; -use crate::wlist::word::{ManyWordsRef, Word, WordData}; +use crate::wlist::word::{Word, WordData, WordDataRef}; use crate::wlist::WordList; -use libpt::cli::console::StyledObject; use libpt::log::{debug, trace}; pub mod response; @@ -174,7 +173,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> { self.wordlist } - pub(crate) fn made_guesses(&self) -> ManyWordsRef { + pub(crate) fn made_guesses(&self) -> Vec<&Word> { self.responses.iter().map(|r| r.guess()).collect() } } diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 0cb9313..2de11b0 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -1,7 +1,7 @@ use libpt::log::{info, trace}; -use crate::error::{Error, SolverError, WResult}; -use crate::wlist::word::{ManyWordDatas, Word}; +use crate::error::{SolverError, WResult}; +use crate::wlist::word::{Word, WordData}; use crate::wlist::WordList; use super::{AnyBuiltinSolver, Solver, Status}; @@ -37,7 +37,7 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { } } trace!("other chars: {:?}", other_chars); - let matches: ManyWordDatas = game + let matches: Vec = game .wordlist() .get_words_matching(pattern) .expect("the solution does not exist in the wordlist") diff --git a/src/wlist/mod.rs b/src/wlist/mod.rs index 0e7a28e..214c082 100644 --- a/src/wlist/mod.rs +++ b/src/wlist/mod.rs @@ -16,7 +16,7 @@ use crate::error::WResult; pub type AnyWordlist = Box; pub trait WordList: Clone + std::fmt::Debug + Default + Sync + Display { - fn solutions(&self) -> ManyWordDatas { + fn solutions(&self) -> Vec { let wmap = self.wordmap().clone(); let threshold = wmap.threshold(); wmap.iter() @@ -88,11 +88,11 @@ pub trait WordList: Clone + std::fmt::Debug + Default + Sync + Display { } buf } - fn get_words_matching(&self, pattern: String) -> WResult { + 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(); + let mut buf = Vec::new(); for k in keys { let w: WordData = self.wordmap().get(&k[0]).unwrap(); buf.push(w) diff --git a/src/wlist/word.rs b/src/wlist/word.rs index b4f55ce..22d666b 100644 --- a/src/wlist/word.rs +++ b/src/wlist/word.rs @@ -11,8 +11,6 @@ pub type Frequency = f64; pub type Word = String; pub type WordData = (Word, Frequency); pub type WordDataRef<'wl> = (&'wl Word, &'wl Frequency); -pub type ManyWordsRef<'a> = Vec<&'a Word>; -pub type ManyWordDatas = Vec<(Word, Frequency)>; #[derive(Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -- 2.40.1 From 1d839e14ac3bc9959dc6d2137ab61f178a5abbac Mon Sep 17 00:00:00 2001 From: cscherrNT Date: Tue, 23 Jul 2024 09:29:15 +0000 Subject: [PATCH 18/26] automatic cargo CI changes --- src/game/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index 497b827..e1d8199 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -2,7 +2,7 @@ use core::panic; use std::fmt::Display; use crate::error::*; -use crate::wlist::word::{Word, WordData, WordDataRef}; +use crate::wlist::word::{Word, WordData}; use crate::wlist::WordList; use libpt::log::{debug, trace}; -- 2.40.1 From 0c90f874b347b00fed0617cc35567596e8d94ada Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Wed, 24 Jul 2024 11:54:02 +0200 Subject: [PATCH 19/26] test: add test to check manual guessing --- src/game/evaluation.rs | 6 ++++++ tests/solver.rs | 48 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/game/evaluation.rs b/src/game/evaluation.rs index 83a790b..1587e4a 100644 --- a/src/game/evaluation.rs +++ b/src/game/evaluation.rs @@ -46,6 +46,12 @@ impl From> for Evaluation { } } +impl From<&str> for Evaluation { + fn from(value: &str) -> Self { + Self::from_str(value).unwrap() + } +} + impl FromStr for Evaluation { type Err = Infallible; fn from_str(s: &str) -> Result { diff --git a/tests/solver.rs b/tests/solver.rs index 580ad97..e8a9e1c 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -1,7 +1,7 @@ use wordle_analyzer::game::Game; use wordle_analyzer::solve::{AnyBuiltinSolver, NaiveSolver, Solver, StupidSolver}; use wordle_analyzer::wlist::builtin::BuiltinWList; -use wordle_analyzer::wlist::word::Word; +use wordle_analyzer::wlist::word::{Word, WordData}; use wordle_analyzer::wlist::WordList; fn wordlist() -> impl WordList { @@ -67,3 +67,49 @@ fn test_naive_play_predetermined_game() -> anyhow::Result<()> { Ok(()) } + +#[test] +fn test_naive_play_predetermined_game_by_manual_guess_and_evak() -> anyhow::Result<()> { + let wl = wordlist(); + let sl = + AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); + // we don't insert the solution yet, + // pretend that a user inputs guesses manually + let mut game = Game::build(5, false, 6, &wl, false)?; + let _actual_solution: Option = Some(("nines".into(), 0.002)); + let mut next_guess; + + next_guess = sl.guess_for(&game)?; + assert_eq!(next_guess, Word::from("which")); + game.guess(next_guess, Some("xxfxx".into()))?; + + next_guess = sl.guess_for(&game)?; + assert_eq!(next_guess, Word::from("their")); + game.guess(next_guess, Some("xxffx".into()))?; + + next_guess = sl.guess_for(&game)?; + assert_eq!(next_guess, Word::from("being")); + game.guess(next_guess, Some("xfffx".into()))?; + + next_guess = sl.guess_for(&game)?; + assert_eq!(next_guess, Word::from("since")); + game.guess(next_guess, Some("fcfxf".into()))?; + + next_guess = sl.guess_for(&game)?; + assert_eq!(next_guess, Word::from("lines")); + game.guess(next_guess, Some("xcccc".into()))?; + + next_guess = sl.guess_for(&game)?; + assert_eq!(next_guess, Word::from("mines")); + game.guess(next_guess, Some("xcccc".into()))?; + + next_guess = sl.guess_for(&game)?; + assert_eq!(next_guess, Word::from("wines")); + game.guess(next_guess, Some("xcccc".into()))?; + + // naive is at the moment too bad to solve "nines" + assert!(game.finished()); + assert!(!game.won()); + + Ok(()) +} -- 2.40.1 From 5a5757b07117feb4ba77c3179a53877a4e2bed51 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Wed, 24 Jul 2024 12:00:43 +0200 Subject: [PATCH 20/26] chore: fix warnings in tests --- tests/solver.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/solver.rs b/tests/solver.rs index e8a9e1c..474042c 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -11,9 +11,9 @@ fn wordlist() -> impl WordList { #[test] fn test_build_builtin_solvers() { let wl = wordlist(); - let stupid_solver = + let _stupid_solver = AnyBuiltinSolver::Stupid(StupidSolver::build(&wl).expect("could not build naive solver")); - let naive_solver = + let _naive_solver = AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); } -- 2.40.1 From 87a41d66038faab83f00d5c92fce2721e551ff49 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Wed, 24 Jul 2024 12:01:14 +0200 Subject: [PATCH 21/26] chore: fix warning --- src/bin/bench/cli.rs | 6 +++--- src/bin/game/tui.rs | 1 - src/bin/solve/simple.rs | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/bin/bench/cli.rs b/src/bin/bench/cli.rs index c153eb6..cda7541 100644 --- a/src/bin/bench/cli.rs +++ b/src/bin/bench/cli.rs @@ -2,7 +2,7 @@ // #![warn(missing_docs)] #![warn(missing_debug_implementations)] -use std::thread::sleep_ms; +use std::thread::sleep; use clap::Parser; use libpt::log::*; @@ -10,7 +10,7 @@ use libpt::log::*; use wordle_analyzer::bench::builtin::BuiltinBenchmark; use wordle_analyzer::bench::{Benchmark, DEFAULT_N}; use wordle_analyzer::game::GameBuilder; -use wordle_analyzer::solve::{AnyBuiltinSolver, BuiltinSolverNames, Solver}; +use wordle_analyzer::solve::{AnyBuiltinSolver, BuiltinSolverNames}; use wordle_analyzer::wlist::builtin::BuiltinWList; use wordle_analyzer::{self, game}; @@ -64,7 +64,7 @@ fn main() -> anyhow::Result<()> { bench.start(50, &bench.builder())?; loop { - sleep_ms(1000); + sleep(std::time::Duration::from_secs(1)); println!("{}", bench.report()); if bench.is_finished() { break; diff --git a/src/bin/game/tui.rs b/src/bin/game/tui.rs index f2829d1..a1c6413 100644 --- a/src/bin/game/tui.rs +++ b/src/bin/game/tui.rs @@ -1,5 +1,4 @@ #![warn(clippy::all)] -#![warn(missing_docs)] #![warn(missing_debug_implementations)] fn main() { unimplemented!(); diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 8da8854..084db61 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -6,7 +6,7 @@ use clap::{Parser, Subcommand}; use libpt::cli::console::style; use libpt::cli::{repl::Repl, strum}; use libpt::log::*; -use strum::{EnumIter, IntoEnumIterator}; +use strum::EnumIter; use wordle_analyzer::game::evaluation::Evaluation; use wordle_analyzer::game::response::GuessResponse; @@ -167,7 +167,7 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { Ok(()) } -fn wlcommand_handler(cli: &Cli, cmd: &WlCommand, wl: &impl WordList) -> anyhow::Result<()> { +fn wlcommand_handler(_cli: &Cli, cmd: &WlCommand, wl: &impl WordList) -> anyhow::Result<()> { match cmd { WlCommand::Stats => { println!("{wl}") -- 2.40.1 From 500e99d8eef804a0a93fd3041b173534c6e1e15a Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Thu, 25 Jul 2024 13:24:01 +0200 Subject: [PATCH 22/26] chore: fix typo in test name --- tests/solver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/solver.rs b/tests/solver.rs index 474042c..339dc36 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -69,7 +69,7 @@ fn test_naive_play_predetermined_game() -> anyhow::Result<()> { } #[test] -fn test_naive_play_predetermined_game_by_manual_guess_and_evak() -> anyhow::Result<()> { +fn test_naive_play_predetermined_game_by_manual_guess_and_eval() -> anyhow::Result<()> { let wl = wordlist(); let sl = AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); -- 2.40.1 From 713a661cc502650fcfc7b27a2640db3823b65657 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Thu, 25 Jul 2024 13:24:56 +0200 Subject: [PATCH 23/26] refactor: improve error handling --- src/bin/solve/simple.rs | 29 ++++++++++++++++++++++++++--- src/error.rs | 2 +- src/game/mod.rs | 25 +++++++++++++++++++++++-- src/solve/naive/mod.rs | 13 +++++++++---- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 084db61..fdfa775 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -8,12 +8,13 @@ use libpt::cli::{repl::Repl, strum}; use libpt::log::*; use strum::EnumIter; +use wordle_analyzer::error::Error; use wordle_analyzer::game::evaluation::Evaluation; use wordle_analyzer::game::response::GuessResponse; use wordle_analyzer::solve::{BuiltinSolverNames, Solver}; use wordle_analyzer::wlist::builtin::BuiltinWList; -use wordle_analyzer::wlist::word::Word; +use wordle_analyzer::wlist::word::{Word, WordData}; use wordle_analyzer::wlist::WordList; use wordle_analyzer::{self, game}; @@ -33,12 +34,22 @@ struct Cli { #[command(flatten)] verbose: libpt::cli::args::VerbosityLevel, /// which solver to use - #[arg(short, long, default_value_t = BuiltinSolverNames::default())] + #[arg(long, default_value_t = BuiltinSolverNames::default())] solver: BuiltinSolverNames, /// set if the solver should play a full native game without interaction #[arg(short, long)] non_interactive: bool, + + // FIXME: line breaks don't work correctly in the cli help + // + /// Solution for the game + /// + /// This will only be used when non-interactive is used. You can use this option to see how the + /// selected solver behaves when trying to guess a specific solution, which can help reproduce + /// behavior. + #[arg(short, long)] + solution: Option, } #[derive(Subcommand, Debug, EnumIter, Clone)] @@ -184,10 +195,22 @@ fn wlcommand_handler(_cli: &Cli, cmd: &WlCommand, wl: &impl WordList) -> anyhow: fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> { let wl = BuiltinWList::default(); - let builder = game::Game::builder(&wl) + let mut builder = game::Game::builder(&wl) .length(cli.length) .max_steps(cli.max_steps) .precompute(cli.precompute); + if cli.solution.is_some() { + let solw: Word = cli.solution.unwrap(); + let sol = wl.get_word(&solw); + if sol.is_none() { + eprintln!("the requested solution \"{solw}\" is not in the wordlist"); + return Err(Error::GameError { + source: wordle_analyzer::error::GameError::WordNotInWordlist(solw), + } + .into()); + } + builder = builder.solution(sol); + } let solver = cli.solver.to_solver(&wl); let mut game = builder.build()?; diff --git a/src/error.rs b/src/error.rs index 88b34f9..5ab936a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -49,7 +49,7 @@ pub enum GameError { 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})")] + #[error("Tried to guess or use a word that is not in the wordlist ({0})")] WordNotInWordlist(Word), } diff --git a/src/game/mod.rs b/src/game/mod.rs index e1d8199..0074086 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -214,13 +214,14 @@ impl<'wl, WL: WordList> Game<'wl, WL> { /// # } /// ``` /// -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq)] pub struct GameBuilder<'wl, WL: WordList> { length: usize, precompute: bool, max_steps: usize, wordlist: &'wl WL, generate_solution: bool, + solution: Option, } impl<'wl, WL: WordList> GameBuilder<'wl, WL> { @@ -234,19 +235,23 @@ impl<'wl, WL: WordList> GameBuilder<'wl, WL> { max_steps: super::DEFAULT_MAX_STEPS, wordlist: wl, generate_solution, + solution: None, } } /// build a [`Game`] with the stored configuration pub fn build(&'wl self) -> GameResult> { trace!("{:#?}", self); - let game: Game = Game::build( + let mut game: Game = Game::build( self.length, self.precompute, self.max_steps, self.wordlist, self.generate_solution, )?; + if self.solution.is_some() { + game.set_solution(self.solution.clone()) + } Ok(game) } @@ -284,6 +289,22 @@ impl<'wl, WL: WordList> GameBuilder<'wl, WL> { self.wordlist = wl; self } + + /// Set the solution for the games built by the builder + /// + /// If this is [Some], then the solution generated by + /// [generate_solution](Self::generate_solution) will be overwritten (if it + /// is true). + /// + /// If [generate_solution](Self::generate_solution) is false and this method is not used, the + /// game will not have a predetermined solution and will not be able to generate evaluations + /// for guesses, so these will need to be added manually by the user. The intention is that + /// this can be used for use cases where the user plays wordle not within wordle-analyzer but + /// in another program (like their browser). It can also be used to test solvers. + pub fn solution(mut self, solution: Option) -> Self { + self.solution = solution; + self + } } impl<'wl, WL: WordList> Display for Game<'wl, WL> { diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 2de11b0..2bd8fa0 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -21,6 +21,10 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { let mut pattern: String = String::from("....."); let mut other_chars: Vec = Vec::new(); let response = game.last_response(); + trace!( + "guessing best guess for last response: {response:#?}\n{:#?}", + response.map(|a| a.evaluation()) + ); if response.is_some() { for (idx, p) in response .unwrap() @@ -37,10 +41,11 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { } } trace!("other chars: {:?}", other_chars); - let matches: Vec = game - .wordlist() - .get_words_matching(pattern) - .expect("the solution does not exist in the wordlist") + let mut matches: Vec = game.wordlist().get_words_matching(pattern)?; + if matches.is_empty() { + return Err(SolverError::NoMatches.into()); + } + matches = matches .iter() // only words that have not been guessed yet .filter(|p| !game.made_guesses().contains(&&p.0)) -- 2.40.1 From 18a512502884b2b42f583ace37ac1c134ae69259 Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Thu, 25 Jul 2024 14:46:20 +0200 Subject: [PATCH 24/26] fix(evaluation): evaluation did not get built from string correctly #9 --- Cargo.toml | 4 +++ src/bench/mod.rs | 1 - src/bin/solve/simple.rs | 6 ++-- src/error.rs | 4 +++ src/game/evaluation.rs | 70 ++++++++++++++++++++++++++++------------- src/game/mod.rs | 2 +- src/solve/mod.rs | 1 + src/solve/naive/mod.rs | 9 ++++++ tests/solver.rs | 42 +++++++++++++++++++------ 9 files changed, 105 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7f7dd9b..44cc4a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ serde_json = { version = "1.0.114", optional = true } strum = "0.26.3" # serde_with = "3.7.0" thiserror = "1.0.58" +tracing-test = "0.2.5" [[bin]] name = "wordlec" @@ -57,3 +58,6 @@ required-features = ["solve", "cli", "builtin"] name = "wordlebench" path = "src/bin/bench/cli.rs" required-features = ["solve", "cli", "bench", "builtin"] + +[dev-dependencies] +test-log = { version = "0.2.16", default-features = false, features = ["color", "trace"] } diff --git a/src/bench/mod.rs b/src/bench/mod.rs index 1b251d3..be2dea4 100644 --- a/src/bench/mod.rs +++ b/src/bench/mod.rs @@ -45,7 +45,6 @@ where // TODO: add some interface to get reports while the benchmark runs // TODO: make the benchmark optionally multithreaded // NOTE: This is blocking, use start to let it run in another thread - // FIXME: this never stops? Reports just keep getting printed fn bench( &self, n: usize, diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index fdfa775..50d694b 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -75,7 +75,7 @@ enum ReplCommand { /// is correct Guess { your_guess: String, - evalutation: Evaluation, + evalutation: String, }, /// Let the solver make a guess Solve, @@ -163,7 +163,9 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { your_guess, evalutation, } => { - let guess = game.guess(your_guess, Some(evalutation)); + let evaluation_converted: Evaluation = + Evaluation::build(&your_guess, &evalutation)?; + let guess = game.guess(your_guess, Some(evaluation_converted)); debug!("your guess: {guess:?}"); if guess.is_err() { eprintln!("{}", style(guess.unwrap_err()).red().bold()); diff --git a/src/error.rs b/src/error.rs index 5ab936a..0b10b8c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -51,6 +51,10 @@ pub enum GameError { TryingToPlayAFinishedGame, #[error("Tried to guess or use a word that is not in the wordlist ({0})")] WordNotInWordlist(Word), + #[error("Invalid syntax for manual evaluation creation")] + InvalidEvaluationSyntax(String), + #[error("The length of guess and evaluation must be the same")] + GuessAndEvalNotSameLen((String, String)), } #[derive(Debug, Clone, Error)] diff --git a/src/game/evaluation.rs b/src/game/evaluation.rs index 1587e4a..fa4b1a0 100644 --- a/src/game/evaluation.rs +++ b/src/game/evaluation.rs @@ -1,25 +1,25 @@ -use std::convert::Infallible; -use std::str::FromStr; - use libpt::cli::console::{style, StyledObject}; use crate::wlist::word::Word; use super::response::Status; +use super::{GameError, WResult}; +/// the [char] of the guess and the [Status] associated with it pub type EvaluationUnit = (char, Status); +/// Basically a [String] with extra information associated with each char #[derive(Debug, Clone, PartialEq, Default)] pub struct Evaluation { inner: Vec, } impl Evaluation { - pub(crate) fn colorized_display(&self, guess: &Word) -> Vec> { - assert_eq!(guess.len(), self.inner.len()); + /// Display the evaluation color coded + pub fn colorized_display(&self) -> Vec> { let mut buf = Vec::new(); - for (i, e) in self.inner.iter().enumerate() { - let mut c = style(guess.chars().nth(i).unwrap().to_string()); + for e in self.inner.iter() { + let mut c = style(e.0.to_string()); if e.1 == Status::Matched { c = c.green(); } else if e.1 == Status::Exists { @@ -29,6 +29,45 @@ impl Evaluation { } buf } + + /// The first string is the word the evaluation is for, The second string defines how the + /// characters of the first string match the solution. + /// + /// + /// ## Evaluation Format: + /// + /// 'x' means wrong character + /// + /// 'p' means present character + /// + /// 'c' means correct character + /// + /// ### Example: + /// + /// 'xxxcc' --- means the first 3 chars are wrong but the second 2 chars are correct + /// + /// 'xppxc' --- means the first character is wrong, the next two characters are present, the last + /// is correct + /// + /// ## Example + /// + /// "wordle xxxcff" --- the guess was wordle, the d is in the correct spot, the solution + /// contains 'l' and 'e', but on another index. + /// + pub fn build(guess: &Word, eval_str: &str) -> WResult { + if guess.len() != eval_str.len() { + return Err(GameError::GuessAndEvalNotSameLen(( + guess.to_string(), + eval_str.to_string(), + )) + .into()); + } + let mut v: Vec = Vec::new(); + for (c, e) in guess.chars().zip(eval_str.chars()) { + v.push((c, Status::from(e))) + } + Ok(v.into()) + } } impl IntoIterator for Evaluation { @@ -46,19 +85,8 @@ impl From> for Evaluation { } } -impl From<&str> for Evaluation { - fn from(value: &str) -> Self { - Self::from_str(value).unwrap() - } -} - -impl FromStr for Evaluation { - type Err = Infallible; - fn from_str(s: &str) -> Result { - let mut v: Vec = Vec::new(); - for c in s.chars() { - v.push((c, Status::from(c))) - } - Ok(v.into()) +impl From for Word { + fn from(value: Evaluation) -> Self { + Word::from(value.inner.into_iter().map(|v| v.0).collect::()) } } diff --git a/src/game/mod.rs b/src/game/mod.rs index 0074086..d3dbde7 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -320,7 +320,7 @@ impl<'wl, WL: WordList> Display for Game<'wl, WL> { for s in self .responses() .iter() - .map(|v| v.evaluation().to_owned().colorized_display(v.guess())) + .map(|v| v.evaluation().to_owned().colorized_display()) { write!(f, "\"")?; for si in s { diff --git a/src/solve/mod.rs b/src/solve/mod.rs index 54842e7..9e34dbc 100644 --- a/src/solve/mod.rs +++ b/src/solve/mod.rs @@ -59,6 +59,7 @@ pub trait Solver<'wl, WL: WordList>: Clone + std::fmt::Debug + Sized + Sync { /// /// This function will return an error if [make_a_move](Solver::make_a_move) fails. fn play(&self, game: &mut Game<'wl, WL>) -> WResult { + // TODO: check if the game is finished already and return an Err if so let mut resp: GuessResponse; loop { resp = self.make_a_move(game)?; diff --git a/src/solve/naive/mod.rs b/src/solve/naive/mod.rs index 2bd8fa0..f8cdee0 100644 --- a/src/solve/naive/mod.rs +++ b/src/solve/naive/mod.rs @@ -16,6 +16,15 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> { info!("using naive solver"); Ok(Self { wl: wordlist }) } + /// Guess a word from the wordlist for the given game + /// + /// ## Algorithm + /// + /// * Look at the evaluation for the last response and keep the correct letters + /// * Get all words that have these letters at the right position + /// * Discard words that have already been tried + /// * 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. fn guess_for(&self, game: &crate::game::Game) -> WResult { // HACK: hardcoded length let mut pattern: String = String::from("....."); diff --git a/tests/solver.rs b/tests/solver.rs index 339dc36..3aa2634 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -1,3 +1,6 @@ +use test_log::test; // set the log level with an envvar: `RUST_LOG=trace cargo test` + +use wordle_analyzer::game::evaluation::Evaluation; use wordle_analyzer::game::Game; use wordle_analyzer::solve::{AnyBuiltinSolver, NaiveSolver, Solver, StupidSolver}; use wordle_analyzer::wlist::builtin::BuiltinWList; @@ -69,7 +72,7 @@ fn test_naive_play_predetermined_game() -> anyhow::Result<()> { } #[test] -fn test_naive_play_predetermined_game_by_manual_guess_and_eval() -> anyhow::Result<()> { +fn test_naive_play_predetermined_game_manually() -> anyhow::Result<()> { let wl = wordlist(); let sl = AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); @@ -77,35 +80,56 @@ fn test_naive_play_predetermined_game_by_manual_guess_and_eval() -> anyhow::Resu // pretend that a user inputs guesses manually let mut game = Game::build(5, false, 6, &wl, false)?; let _actual_solution: Option = Some(("nines".into(), 0.002)); - let mut next_guess; + let mut next_guess: Word; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("which")); - game.guess(next_guess, Some("xxfxx".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xxfxx")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("their")); - game.guess(next_guess, Some("xxffx".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xxffx")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("being")); - game.guess(next_guess, Some("xfffx".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xfffx")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("since")); - game.guess(next_guess, Some("fcfxf".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "fcfxf")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("lines")); - game.guess(next_guess, Some("xcccc".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xcccc")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("mines")); - game.guess(next_guess, Some("xcccc".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xcccc")?), + )?; next_guess = sl.guess_for(&game)?; assert_eq!(next_guess, Word::from("wines")); - game.guess(next_guess, Some("xcccc".into()))?; + game.guess( + next_guess.clone(), + Some(Evaluation::build(&next_guess, "xcccc")?), + )?; // naive is at the moment too bad to solve "nines" assert!(game.finished()); -- 2.40.1 From 3800b9068bc2585b0325aed3b949d22b4dc9411b Mon Sep 17 00:00:00 2001 From: cscherrNT Date: Thu, 25 Jul 2024 12:50:09 +0000 Subject: [PATCH 25/26] automatic cargo CI changes --- src/bin/solve/simple.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index 50d694b..a5fca3c 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -14,7 +14,7 @@ use wordle_analyzer::game::response::GuessResponse; use wordle_analyzer::solve::{BuiltinSolverNames, Solver}; use wordle_analyzer::wlist::builtin::BuiltinWList; -use wordle_analyzer::wlist::word::{Word, WordData}; +use wordle_analyzer::wlist::word::Word; use wordle_analyzer::wlist::WordList; use wordle_analyzer::{self, game}; -- 2.40.1 From 740f2fbdc13873728c8a891af89170e607980d9d Mon Sep 17 00:00:00 2001 From: "Christoph J. Scherr" Date: Thu, 25 Jul 2024 15:17:54 +0200 Subject: [PATCH 26/26] feat(solve): new and undo #7 --- src/bin/solve/simple.rs | 18 ++++++++++++------ src/game/mod.rs | 7 +++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/bin/solve/simple.rs b/src/bin/solve/simple.rs index a5fca3c..19e274a 100644 --- a/src/bin/solve/simple.rs +++ b/src/bin/solve/simple.rs @@ -54,9 +54,6 @@ struct Cli { #[derive(Subcommand, Debug, EnumIter, Clone)] enum ReplCommand { - /// Let the user input the response to the last guess - /// - Response { encoded: String }, /// Let the user input a word and the response for that word /// /// Evaluation Format: @@ -86,6 +83,10 @@ enum ReplCommand { #[command(subcommand)] cmd: WlCommand, }, + /// Start a new game + New, + /// Undo the last n operations + Undo { n: usize }, /// Leave the Repl Exit, } @@ -155,9 +156,13 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { println!("{}", game); } ReplCommand::Solve => { - let best_guess = solver.guess_for(&game)?; + let best_guess = solver.guess_for(&game); + if best_guess.is_err() { + eprintln!("{}", style(best_guess.unwrap_err()).red().bold()); + continue; + } debug!("game state: {game:?}"); - println!("best guess: {best_guess}"); + println!("best guess: {}", best_guess.unwrap()); } ReplCommand::Guess { your_guess, @@ -174,7 +179,8 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { println!("{}", guess.unwrap()); debug!("game state: {game:#?}"); } - _ => todo!(), + ReplCommand::New => game = builder.build()?, + ReplCommand::Undo { n } => game.undo(n)?, } } Ok(()) diff --git a/src/game/mod.rs b/src/game/mod.rs index d3dbde7..6873311 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -133,6 +133,13 @@ impl<'wl, WL: WordList> Game<'wl, WL> { evaluation.into() } + /// discard the last n responses + pub fn undo(&mut self, n: usize) -> WResult<()> { + self.responses + .drain(self.responses.len() - n..self.responses.len()); + Ok(()) + } + pub fn length(&self) -> usize { self.length } -- 2.40.1