generated from PlexSheep/rs-base
Merge pull request 'add interactive solver' (#4) from feat/interactive-solver into devel
cargo devel CI / cargo CI (push) Successful in 1m46s
Details
cargo devel CI / cargo CI (push) Successful in 1m46s
Details
Reviewed-on: #4
This commit is contained in:
commit
f3fd03e20b
|
@ -27,15 +27,17 @@ anyhow = "1.0.81"
|
||||||
chrono = { version = "0.4.37" }
|
chrono = { version = "0.4.37" }
|
||||||
clap = { version = "4.5.3", features = ["derive"], optional = true }
|
clap = { version = "4.5.3", features = ["derive"], optional = true }
|
||||||
colored = { version = "2.1.0", optional = false }
|
colored = { version = "2.1.0", optional = false }
|
||||||
libpt = "0.6.0"
|
libpt = { version = "0.6.0", features = ["cli"] }
|
||||||
num_cpus = "1.16.0"
|
num_cpus = "1.16.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
regex = "1.10.3"
|
regex = "1.10.3"
|
||||||
serde = { version = "1.0.197", optional = true, features = ["serde_derive"] }
|
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 }
|
||||||
|
strum = "0.26.3"
|
||||||
# serde_with = "3.7.0"
|
# serde_with = "3.7.0"
|
||||||
thiserror = "1.0.58"
|
thiserror = "1.0.58"
|
||||||
|
tracing-test = "0.2.5"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "wordlec"
|
name = "wordlec"
|
||||||
|
@ -56,3 +58,6 @@ required-features = ["solve", "cli", "builtin"]
|
||||||
name = "wordlebench"
|
name = "wordlebench"
|
||||||
path = "src/bin/bench/cli.rs"
|
path = "src/bin/bench/cli.rs"
|
||||||
required-features = ["solve", "cli", "bench", "builtin"]
|
required-features = ["solve", "cli", "bench", "builtin"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
test-log = { version = "0.2.16", default-features = false, features = ["color", "trace"] }
|
||||||
|
|
|
@ -45,7 +45,6 @@ where
|
||||||
// TODO: add some interface to get reports while the benchmark runs
|
// TODO: add some interface to get reports while the benchmark runs
|
||||||
// TODO: make the benchmark optionally multithreaded
|
// TODO: make the benchmark optionally multithreaded
|
||||||
// NOTE: This is blocking, use start to let it run in another thread
|
// NOTE: This is blocking, use start to let it run in another thread
|
||||||
// FIXME: this never stops? Reports just keep getting printed
|
|
||||||
fn bench(
|
fn bench(
|
||||||
&self,
|
&self,
|
||||||
n: usize,
|
n: usize,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// #![warn(missing_docs)]
|
// #![warn(missing_docs)]
|
||||||
#![warn(missing_debug_implementations)]
|
#![warn(missing_debug_implementations)]
|
||||||
|
|
||||||
use std::thread::sleep_ms;
|
use std::thread::sleep;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use libpt::log::*;
|
use libpt::log::*;
|
||||||
|
@ -10,7 +10,7 @@ use libpt::log::*;
|
||||||
use wordle_analyzer::bench::builtin::BuiltinBenchmark;
|
use wordle_analyzer::bench::builtin::BuiltinBenchmark;
|
||||||
use wordle_analyzer::bench::{Benchmark, DEFAULT_N};
|
use wordle_analyzer::bench::{Benchmark, DEFAULT_N};
|
||||||
use wordle_analyzer::game::GameBuilder;
|
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::wlist::builtin::BuiltinWList;
|
||||||
|
|
||||||
use wordle_analyzer::{self, game};
|
use wordle_analyzer::{self, game};
|
||||||
|
@ -64,7 +64,7 @@ fn main() -> anyhow::Result<()> {
|
||||||
bench.start(50, &bench.builder())?;
|
bench.start(50, &bench.builder())?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
sleep_ms(1000);
|
sleep(std::time::Duration::from_secs(1));
|
||||||
println!("{}", bench.report());
|
println!("{}", bench.report());
|
||||||
if bench.is_finished() {
|
if bench.is_finished() {
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -51,7 +51,7 @@ fn main() -> anyhow::Result<()> {
|
||||||
let mut guess: Word;
|
let mut guess: Word;
|
||||||
loop {
|
loop {
|
||||||
guess = get_word(&cli, game.step())?;
|
guess = get_word(&cli, game.step())?;
|
||||||
response = match game.guess(guess) {
|
response = match game.guess(guess, None) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
GameError::GuessHasWrongLength(len) => {
|
GameError::GuessHasWrongLength(len) => {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#![warn(clippy::all)]
|
#![warn(clippy::all)]
|
||||||
#![warn(missing_docs)]
|
|
||||||
#![warn(missing_debug_implementations)]
|
#![warn(missing_debug_implementations)]
|
||||||
fn main() {
|
fn main() {
|
||||||
unimplemented!();
|
unimplemented!();
|
||||||
|
|
|
@ -2,14 +2,20 @@
|
||||||
// #![warn(missing_docs)]
|
// #![warn(missing_docs)]
|
||||||
#![warn(missing_debug_implementations)]
|
#![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 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::game::response::GuessResponse;
|
||||||
|
|
||||||
use wordle_analyzer::solve::{BuiltinSolverNames, Solver};
|
use wordle_analyzer::solve::{BuiltinSolverNames, Solver};
|
||||||
use wordle_analyzer::wlist::builtin::BuiltinWList;
|
use wordle_analyzer::wlist::builtin::BuiltinWList;
|
||||||
use wordle_analyzer::wlist::word::Word;
|
use wordle_analyzer::wlist::word::Word;
|
||||||
|
use wordle_analyzer::wlist::WordList;
|
||||||
use wordle_analyzer::{self, game};
|
use wordle_analyzer::{self, game};
|
||||||
|
|
||||||
#[derive(Parser, Clone, Debug)]
|
#[derive(Parser, Clone, Debug)]
|
||||||
|
@ -25,36 +31,204 @@ struct Cli {
|
||||||
#[arg(short, long, default_value_t = wordle_analyzer::DEFAULT_MAX_STEPS)]
|
#[arg(short, long, default_value_t = wordle_analyzer::DEFAULT_MAX_STEPS)]
|
||||||
max_steps: usize,
|
max_steps: usize,
|
||||||
/// more verbose logs
|
/// more verbose logs
|
||||||
#[arg(short, long)]
|
#[command(flatten)]
|
||||||
verbose: bool,
|
verbose: libpt::cli::args::VerbosityLevel,
|
||||||
/// which solver to use
|
/// which solver to use
|
||||||
#[arg(short, long, default_value_t = BuiltinSolverNames::default())]
|
#[arg(long, default_value_t = BuiltinSolverNames::default())]
|
||||||
solver: BuiltinSolverNames,
|
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<Word>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug, EnumIter, Clone)]
|
||||||
|
enum ReplCommand {
|
||||||
|
/// 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
|
||||||
|
///
|
||||||
|
/// 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: String,
|
||||||
|
},
|
||||||
|
/// Let the solver make a guess
|
||||||
|
Solve,
|
||||||
|
/// Show the current state of the game
|
||||||
|
Show,
|
||||||
|
/// Display data about the wordlist
|
||||||
|
Wl {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: WlCommand,
|
||||||
|
},
|
||||||
|
/// Start a new game
|
||||||
|
New,
|
||||||
|
/// Undo the last n operations
|
||||||
|
Undo { n: usize },
|
||||||
|
/// Leave the Repl
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug, EnumIter, Clone, Default)]
|
||||||
|
enum WlCommand {
|
||||||
|
#[default]
|
||||||
|
Stats,
|
||||||
|
Top {
|
||||||
|
amount: usize,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
if cli.verbose {
|
Logger::builder()
|
||||||
Logger::builder().set_level(Level::DEBUG).build().unwrap();
|
.set_level(cli.verbose.level())
|
||||||
} else {
|
.build()
|
||||||
Logger::builder().set_level(Level::INFO).build().unwrap();
|
.unwrap();
|
||||||
}
|
trace!("dumping CLI: {:#?}", cli);
|
||||||
debug!("dumping CLI: {:#?}", cli);
|
|
||||||
|
|
||||||
|
if cli.non_interactive {
|
||||||
|
play_native_non_interactive(cli)?;
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
help_guess_interactive(cli)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> {
|
||||||
let wl = BuiltinWList::default();
|
let wl = BuiltinWList::default();
|
||||||
let builder = game::Game::builder(&wl)
|
let builder = game::GameBuilder::new(&wl, false)
|
||||||
.length(cli.length)
|
.length(cli.length)
|
||||||
.max_steps(cli.max_steps)
|
.max_steps(cli.max_steps)
|
||||||
.precompute(cli.precompute);
|
.precompute(cli.precompute);
|
||||||
let solver = cli.solver.to_solver(&wl);
|
let solver = cli.solver.to_solver(&wl);
|
||||||
let mut game = builder.build()?;
|
let mut game = builder.build()?;
|
||||||
|
|
||||||
|
let mut repl = libpt::cli::repl::DefaultRepl::<ReplCommand>::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::Wl { cmd } => wlcommand_handler(&cli, &cmd, &wl)?,
|
||||||
|
ReplCommand::Show => {
|
||||||
|
println!("{}", game);
|
||||||
|
}
|
||||||
|
ReplCommand::Solve => {
|
||||||
|
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.unwrap());
|
||||||
|
}
|
||||||
|
ReplCommand::Guess {
|
||||||
|
your_guess,
|
||||||
|
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());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println!("{}", guess.unwrap());
|
||||||
|
debug!("game state: {game:#?}");
|
||||||
|
}
|
||||||
|
ReplCommand::New => game = builder.build()?,
|
||||||
|
ReplCommand::Undo { n } => game.undo(n)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 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()?;
|
||||||
|
|
||||||
debug!("{game:#?}");
|
debug!("{game:#?}");
|
||||||
|
|
||||||
let mut response: GuessResponse;
|
let mut response: GuessResponse;
|
||||||
let mut _guess: Word;
|
let mut _guess: Word;
|
||||||
loop {
|
loop {
|
||||||
response = solver.make_a_move(&mut game)?;
|
response = solver.make_a_move(&mut game)?;
|
||||||
|
debug!("game state: {game:#?}");
|
||||||
println!("{}. guess: {response}", game.step() - 1);
|
println!("{}. guess: {response}", game.step() - 1);
|
||||||
|
|
||||||
if response.finished() {
|
if response.finished() {
|
||||||
|
@ -66,6 +240,5 @@ fn main() -> anyhow::Result<()> {
|
||||||
} else {
|
} else {
|
||||||
println!("You lose! The solution was {:?}.", game.solution());
|
println!("You lose! The solution was {:?}.", game.solution());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
17
src/error.rs
17
src/error.rs
|
@ -13,6 +13,11 @@ pub enum Error {
|
||||||
#[from]
|
#[from]
|
||||||
source: GameError,
|
source: GameError,
|
||||||
},
|
},
|
||||||
|
#[error("Solver Error")]
|
||||||
|
SolverError {
|
||||||
|
#[from]
|
||||||
|
source: SolverError,
|
||||||
|
},
|
||||||
#[error("Benchmark Error")]
|
#[error("Benchmark Error")]
|
||||||
BenchError {
|
BenchError {
|
||||||
#[from]
|
#[from]
|
||||||
|
@ -44,8 +49,12 @@ pub enum GameError {
|
||||||
GuessHasWrongLength(usize),
|
GuessHasWrongLength(usize),
|
||||||
#[error("The game is finished but a guess is being made")]
|
#[error("The game is finished but a guess is being made")]
|
||||||
TryingToPlayAFinishedGame,
|
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),
|
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)]
|
#[derive(Debug, Clone, Error)]
|
||||||
|
@ -53,3 +62,9 @@ pub enum BenchError {
|
||||||
#[error("Trying to modify a finished report")]
|
#[error("Trying to modify a finished report")]
|
||||||
ModifyFinishedReport,
|
ModifyFinishedReport,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum SolverError {
|
||||||
|
#[error("Wordlist has no matches for the gamestate")]
|
||||||
|
NoMatches,
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
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<EvaluationUnit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Evaluation {
|
||||||
|
/// Display the evaluation color coded
|
||||||
|
pub fn colorized_display(&self) -> Vec<StyledObject<String>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
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 {
|
||||||
|
c = c.yellow();
|
||||||
|
}
|
||||||
|
buf.push(c);
|
||||||
|
}
|
||||||
|
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<Self> {
|
||||||
|
if guess.len() != eval_str.len() {
|
||||||
|
return Err(GameError::GuessAndEvalNotSameLen((
|
||||||
|
guess.to_string(),
|
||||||
|
eval_str.to_string(),
|
||||||
|
))
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
let mut v: Vec<EvaluationUnit> = 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 {
|
||||||
|
type Item = EvaluationUnit;
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.inner.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<EvaluationUnit>> for Evaluation {
|
||||||
|
fn from(value: Vec<EvaluationUnit>) -> Self {
|
||||||
|
Self { inner: value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Evaluation> for Word {
|
||||||
|
fn from(value: Evaluation) -> Self {
|
||||||
|
Word::from(value.inner.into_iter().map(|v| v.0).collect::<String>())
|
||||||
|
}
|
||||||
|
}
|
151
src/game/mod.rs
151
src/game/mod.rs
|
@ -1,5 +1,8 @@
|
||||||
|
use core::panic;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::wlist::word::{ManyWordsRef, Word, WordData};
|
use crate::wlist::word::{Word, WordData};
|
||||||
use crate::wlist::WordList;
|
use crate::wlist::WordList;
|
||||||
|
|
||||||
use libpt::log::{debug, trace};
|
use libpt::log::{debug, trace};
|
||||||
|
@ -7,8 +10,11 @@ use libpt::log::{debug, trace};
|
||||||
pub mod response;
|
pub mod response;
|
||||||
use response::GuessResponse;
|
use response::GuessResponse;
|
||||||
|
|
||||||
|
pub mod evaluation;
|
||||||
|
|
||||||
pub mod summary;
|
pub mod summary;
|
||||||
|
|
||||||
|
use self::evaluation::Evaluation;
|
||||||
use self::response::Status;
|
use self::response::Status;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
@ -20,9 +26,8 @@ where
|
||||||
precompute: bool,
|
precompute: bool,
|
||||||
max_steps: usize,
|
max_steps: usize,
|
||||||
step: usize,
|
step: usize,
|
||||||
solution: WordData,
|
solution: Option<WordData>,
|
||||||
wordlist: &'wl WL,
|
wordlist: &'wl WL,
|
||||||
finished: bool,
|
|
||||||
responses: Vec<GuessResponse>,
|
responses: Vec<GuessResponse>,
|
||||||
// TODO: keep track of the letters the user has tried
|
// TODO: keep track of the letters the user has tried
|
||||||
}
|
}
|
||||||
|
@ -30,7 +35,7 @@ where
|
||||||
impl<'wl, WL: WordList> Game<'wl, WL> {
|
impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
/// get a new [`GameBuilder`]
|
/// get a new [`GameBuilder`]
|
||||||
pub fn builder(wl: &'wl WL) -> GameBuilder<'wl, WL> {
|
pub fn builder(wl: &'wl WL) -> GameBuilder<'wl, WL> {
|
||||||
GameBuilder::new(wl)
|
GameBuilder::new(wl, true)
|
||||||
}
|
}
|
||||||
/// Create a [Game] of wordle
|
/// Create a [Game] of wordle
|
||||||
///
|
///
|
||||||
|
@ -43,44 +48,54 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// This function will return an error if .
|
/// No Errors
|
||||||
pub(crate) fn build(
|
pub fn build(
|
||||||
length: usize,
|
length: usize,
|
||||||
precompute: bool,
|
precompute: bool,
|
||||||
max_steps: usize,
|
max_steps: usize,
|
||||||
wlist: &'wl WL,
|
wlist: &'wl WL,
|
||||||
|
generate_solution: bool,
|
||||||
) -> GameResult<Self> {
|
) -> GameResult<Self> {
|
||||||
// TODO: check if the length is in the range bounds of the wordlist
|
// TODO: check if the length is in the range bounds of the wordlist
|
||||||
let solution = wlist.rand_solution();
|
|
||||||
let game: Game<'wl, WL> = Game {
|
let game: Game<'wl, WL> = Game {
|
||||||
length,
|
length,
|
||||||
precompute,
|
precompute,
|
||||||
max_steps,
|
max_steps,
|
||||||
step: 0,
|
step: 0,
|
||||||
solution,
|
solution: if generate_solution {
|
||||||
|
Some(wlist.rand_solution())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
wordlist: wlist,
|
wordlist: wlist,
|
||||||
finished: false,
|
|
||||||
responses: Vec::new(),
|
responses: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(game)
|
Ok(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// set a solution, can be used for testing
|
||||||
|
pub fn set_solution(&mut self, sol: Option<WordData>) {
|
||||||
|
self.solution = sol;
|
||||||
|
}
|
||||||
|
|
||||||
/// Make a new guess
|
/// Make a new guess
|
||||||
///
|
///
|
||||||
/// The word will be evaluated against the [solution](Game::solution) of the [Game].
|
/// The word will be evaluated against the [solution](Game::solution) of the [Game].
|
||||||
/// A [GuessResponse] will be formulated, showing us which letters are correctly placed, in the
|
/// A [GuessResponse] will be formulated, showing us which letters are correctly placed, in the
|
||||||
/// solution, or just wrong.
|
/// solution, or just wrong.
|
||||||
///
|
///
|
||||||
|
/// Note that you do not need to use the [GuessResponse], it is appended to the game state.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// This function will return an error if the length of the [Word] is wrong It will also error
|
/// This function will return an error if the length of the [Word] is wrong It will also error
|
||||||
/// if the game is finished.
|
/// if the game is finished.
|
||||||
pub fn guess(&mut self, guess: Word) -> GameResult<GuessResponse> {
|
pub fn guess(&mut self, guess: Word, eval: Option<Evaluation>) -> GameResult<GuessResponse> {
|
||||||
if guess.len() != self.length {
|
if guess.len() != self.length {
|
||||||
return Err(GameError::GuessHasWrongLength(guess.len()));
|
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);
|
return Err(GameError::TryingToPlayAFinishedGame);
|
||||||
}
|
}
|
||||||
if self.wordlist.get_word(&guess).is_none() {
|
if self.wordlist.get_word(&guess).is_none() {
|
||||||
|
@ -88,34 +103,49 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
}
|
}
|
||||||
self.step += 1;
|
self.step += 1;
|
||||||
|
|
||||||
let mut compare_solution = self.solution.0.clone();
|
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());
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(mut solution: WordData, guess: &Word) -> Evaluation {
|
||||||
let mut evaluation = Vec::new();
|
let mut evaluation = Vec::new();
|
||||||
let mut status: Status;
|
let mut status: Status;
|
||||||
for (idx, c) in guess.chars().enumerate() {
|
for (idx, c) in guess.chars().enumerate() {
|
||||||
if compare_solution.chars().nth(idx) == Some(c) {
|
if solution.0.chars().nth(idx) == Some(c) {
|
||||||
status = Status::Matched;
|
status = Status::Matched;
|
||||||
compare_solution.replace_range(idx..idx + 1, "_");
|
solution.0.replace_range(idx..idx + 1, "_");
|
||||||
} else if compare_solution.contains(c) {
|
} else if solution.0.contains(c) {
|
||||||
status = Status::Exists;
|
status = Status::Exists;
|
||||||
compare_solution = compare_solution.replacen(c, "_", 1);
|
solution.0 = solution.0.replacen(c, "_", 1);
|
||||||
} else {
|
} else {
|
||||||
status = Status::None
|
status = Status::None
|
||||||
}
|
}
|
||||||
evaluation.push((c, status));
|
evaluation.push((c, status));
|
||||||
}
|
}
|
||||||
|
evaluation.into()
|
||||||
|
}
|
||||||
|
|
||||||
let response = GuessResponse::new(guess, evaluation, self);
|
/// discard the last n responses
|
||||||
self.responses.push(response.clone());
|
pub fn undo(&mut self, n: usize) -> WResult<()> {
|
||||||
self.finished = response.finished();
|
self.responses
|
||||||
Ok(response)
|
.drain(self.responses.len() - n..self.responses.len());
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn length(&self) -> usize {
|
pub fn length(&self) -> usize {
|
||||||
self.length
|
self.length
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn solution(&self) -> &WordData {
|
pub fn solution(&self) -> Option<&WordData> {
|
||||||
&self.solution
|
self.solution.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn step(&self) -> usize {
|
pub fn step(&self) -> usize {
|
||||||
|
@ -123,7 +153,17 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finished(&self) -> bool {
|
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 {
|
pub fn max_steps(&self) -> usize {
|
||||||
|
@ -140,7 +180,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
self.wordlist
|
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()
|
self.responses.iter().map(|r| r.guess()).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,7 +200,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
/// # use anyhow::Result;
|
/// # use anyhow::Result;
|
||||||
/// # fn main() -> Result<()> {
|
/// # fn main() -> Result<()> {
|
||||||
/// let wl = BuiltinWList::default();
|
/// let wl = BuiltinWList::default();
|
||||||
/// let game: Game<_> = GameBuilder::new(&wl)
|
/// let game: Game<_> = GameBuilder::new(&wl, true)
|
||||||
/// .build()?;
|
/// .build()?;
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
|
@ -181,32 +221,44 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct GameBuilder<'wl, WL: WordList> {
|
pub struct GameBuilder<'wl, WL: WordList> {
|
||||||
length: usize,
|
length: usize,
|
||||||
precompute: bool,
|
precompute: bool,
|
||||||
max_steps: usize,
|
max_steps: usize,
|
||||||
wordlist: &'wl WL,
|
wordlist: &'wl WL,
|
||||||
|
generate_solution: bool,
|
||||||
|
solution: Option<WordData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'wl, WL: WordList> GameBuilder<'wl, WL> {
|
impl<'wl, WL: WordList> GameBuilder<'wl, WL> {
|
||||||
/// make a new [GameBuilder]
|
/// make a new [GameBuilder]
|
||||||
///
|
///
|
||||||
/// We need a [WordList], so provide one here.
|
/// 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 {
|
Self {
|
||||||
length: super::DEFAULT_WORD_LENGTH,
|
length: super::DEFAULT_WORD_LENGTH,
|
||||||
precompute: false,
|
precompute: false,
|
||||||
max_steps: super::DEFAULT_MAX_STEPS,
|
max_steps: super::DEFAULT_MAX_STEPS,
|
||||||
wordlist: wl,
|
wordlist: wl,
|
||||||
|
generate_solution,
|
||||||
|
solution: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// build a [`Game`] with the stored configuration
|
/// build a [`Game`] with the stored configuration
|
||||||
pub fn build(&'wl self) -> GameResult<Game<'wl, WL>> {
|
pub fn build(&'wl self) -> GameResult<Game<'wl, WL>> {
|
||||||
trace!("{:#?}", self);
|
trace!("{:#?}", self);
|
||||||
let game: Game<WL> =
|
let mut game: Game<WL> = Game::build(
|
||||||
Game::build(self.length, self.precompute, self.max_steps, self.wordlist)?;
|
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)
|
Ok(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,4 +296,45 @@ impl<'wl, WL: WordList> GameBuilder<'wl, WL> {
|
||||||
self.wordlist = wl;
|
self.wordlist = wl;
|
||||||
self
|
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<WordData>) -> Self {
|
||||||
|
self.solution = solution;
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
write!(f, "\"")?;
|
||||||
|
for si in s {
|
||||||
|
write!(f, "{si}")?;
|
||||||
|
}
|
||||||
|
write!(f, "\", ")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use colored::Colorize;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use super::Game;
|
use super::{Evaluation, Game};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Copy, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Copy, Eq, PartialOrd, Ord)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
@ -13,15 +13,13 @@ pub struct AtomicEvaluation {
|
||||||
char: char,
|
char: char,
|
||||||
status: Status,
|
status: Status,
|
||||||
}
|
}
|
||||||
pub type Evaluation = Vec<(char, Status)>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
// #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct GuessResponse {
|
pub struct GuessResponse {
|
||||||
guess: Word,
|
guess: Word,
|
||||||
evaluation: Evaluation,
|
evaluation: Evaluation,
|
||||||
finish: bool,
|
solution: Option<WordData>,
|
||||||
solution: WordData,
|
|
||||||
step: usize,
|
step: usize,
|
||||||
max_steps: usize,
|
max_steps: usize,
|
||||||
}
|
}
|
||||||
|
@ -34,44 +32,48 @@ pub enum Status {
|
||||||
Matched = 2,
|
Matched = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<char> 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 {
|
impl GuessResponse {
|
||||||
pub(crate) fn new<WL: WordList>(
|
pub(crate) fn new<WL: WordList>(guess: &Word, status: Evaluation, game: &Game<WL>) -> Self {
|
||||||
guess: Word,
|
let new = Self {
|
||||||
status: Vec<(char, Status)>,
|
guess: guess.to_owned(),
|
||||||
game: &Game<WL>,
|
|
||||||
) -> Self {
|
|
||||||
let finish: bool = if game.step() > game.max_steps() {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
guess == game.solution().0
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
guess,
|
|
||||||
evaluation: status,
|
evaluation: status,
|
||||||
finish,
|
solution: game.solution().cloned(),
|
||||||
solution: game.solution().clone(),
|
|
||||||
step: game.step(),
|
step: game.step(),
|
||||||
max_steps: game.max_steps(),
|
max_steps: game.max_steps(),
|
||||||
}
|
};
|
||||||
|
new
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finished(&self) -> bool {
|
pub fn finished(&self) -> bool {
|
||||||
self.finish
|
self.step() > self.max_steps() || self.won()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn won(&self) -> bool {
|
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<WordData> {
|
pub fn solution(&self) -> Option<WordData> {
|
||||||
if self.won() {
|
self.solution.clone()
|
||||||
Some(self.solution.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn evaluation(&self) -> &[(char, Status)] {
|
pub fn evaluation(&self) -> &Evaluation {
|
||||||
&self.evaluation
|
&self.evaluation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +92,7 @@ impl GuessResponse {
|
||||||
|
|
||||||
impl Display for GuessResponse {
|
impl Display for GuessResponse {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
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!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{}",
|
"{}",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#![warn(clippy::all)]
|
#![warn(clippy::all)]
|
||||||
#![warn(missing_docs)]
|
// #![warn(missing_docs)]
|
||||||
#![warn(missing_debug_implementations)]
|
#![warn(missing_debug_implementations)]
|
||||||
|
|
||||||
/// Default letters of a solution word
|
/// Default letters of a solution word
|
||||||
|
|
|
@ -44,14 +44,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
|
/// 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).
|
/// 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<Word>;
|
||||||
/// Make a singular step for a [Game]
|
/// Make a singular step for a [Game]
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// This function will return an error if [guess_for](Solver::guess_for) fails.
|
/// This function will return an error if [guess_for](Solver::guess_for) fails.
|
||||||
fn make_a_move(&self, game: &mut Game<'wl, WL>) -> WResult<GuessResponse> {
|
fn make_a_move(&self, game: &mut Game<'wl, WL>) -> WResult<GuessResponse> {
|
||||||
Ok(game.guess(self.guess_for(game))?)
|
Ok(game.guess(self.guess_for(game)?, None)?)
|
||||||
}
|
}
|
||||||
/// Play a [Game] and return the last [GuessResponse].
|
/// Play a [Game] and return the last [GuessResponse].
|
||||||
///
|
///
|
||||||
|
@ -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.
|
/// This function will return an error if [make_a_move](Solver::make_a_move) fails.
|
||||||
fn play(&self, game: &mut Game<'wl, WL>) -> WResult<GuessResponse> {
|
fn play(&self, game: &mut Game<'wl, WL>) -> WResult<GuessResponse> {
|
||||||
|
// TODO: check if the game is finished already and return an Err if so
|
||||||
let mut resp: GuessResponse;
|
let mut resp: GuessResponse;
|
||||||
loop {
|
loop {
|
||||||
resp = self.make_a_move(game)?;
|
resp = self.make_a_move(game)?;
|
||||||
|
@ -146,10 +147,10 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for AnyBuiltinSolver<'wl, WL> {
|
||||||
fn build(wordlist: &'wl WL) -> WResult<Self> {
|
fn build(wordlist: &'wl WL) -> WResult<Self> {
|
||||||
Ok(Self::Naive(NaiveSolver::build(wordlist)?))
|
Ok(Self::Naive(NaiveSolver::build(wordlist)?))
|
||||||
}
|
}
|
||||||
fn guess_for(&self, game: &Game<'wl, WL>) -> Word {
|
fn guess_for(&self, game: &Game<'wl, WL>) -> WResult<Word> {
|
||||||
match self {
|
Ok(match self {
|
||||||
Self::Naive(solver) => solver.guess_for(game),
|
Self::Naive(solver) => solver.guess_for(game)?,
|
||||||
Self::Stupid(solver) => solver.guess_for(game),
|
Self::Stupid(solver) => solver.guess_for(game)?,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use libpt::log::{info, trace};
|
use libpt::log::{info, trace};
|
||||||
|
|
||||||
use crate::wlist::word::{ManyWordDatas, Word};
|
use crate::error::{SolverError, WResult};
|
||||||
|
use crate::wlist::word::{Word, WordData};
|
||||||
use crate::wlist::WordList;
|
use crate::wlist::WordList;
|
||||||
|
|
||||||
use super::{AnyBuiltinSolver, Solver, Status};
|
use super::{AnyBuiltinSolver, Solver, Status};
|
||||||
|
@ -15,13 +16,32 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
|
||||||
info!("using naive solver");
|
info!("using naive solver");
|
||||||
Ok(Self { wl: wordlist })
|
Ok(Self { wl: wordlist })
|
||||||
}
|
}
|
||||||
fn guess_for(&self, game: &crate::game::Game<WL>) -> Word {
|
/// 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<WL>) -> WResult<Word> {
|
||||||
// HACK: hardcoded length
|
// HACK: hardcoded length
|
||||||
let mut pattern: String = String::from(".....");
|
let mut pattern: String = String::from(".....");
|
||||||
let mut other_chars: Vec<char> = Vec::new();
|
let mut other_chars: Vec<char> = Vec::new();
|
||||||
let response = game.last_response();
|
let response = game.last_response();
|
||||||
|
trace!(
|
||||||
|
"guessing best guess for last response: {response:#?}\n{:#?}",
|
||||||
|
response.map(|a| a.evaluation())
|
||||||
|
);
|
||||||
if response.is_some() {
|
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 {
|
if p.1 == Status::Matched {
|
||||||
pattern.replace_range(idx..idx + 1, &p.0.to_string());
|
pattern.replace_range(idx..idx + 1, &p.0.to_string());
|
||||||
} else if p.1 == Status::Exists {
|
} else if p.1 == Status::Exists {
|
||||||
|
@ -30,10 +50,11 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trace!("other chars: {:?}", other_chars);
|
trace!("other chars: {:?}", other_chars);
|
||||||
let matches: ManyWordDatas = game
|
let mut matches: Vec<WordData> = game.wordlist().get_words_matching(pattern)?;
|
||||||
.wordlist()
|
if matches.is_empty() {
|
||||||
.get_words_matching(pattern)
|
return Err(SolverError::NoMatches.into());
|
||||||
.expect("the solution does not exist in the wordlist")
|
}
|
||||||
|
matches = matches
|
||||||
.iter()
|
.iter()
|
||||||
// only words that have not been guessed yet
|
// only words that have not been guessed yet
|
||||||
.filter(|p| !game.made_guesses().contains(&&p.0))
|
.filter(|p| !game.made_guesses().contains(&&p.0))
|
||||||
|
@ -48,7 +69,10 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
|
||||||
})
|
})
|
||||||
.map(|v| v.to_owned())
|
.map(|v| v.to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
matches[0].0.to_owned()
|
if matches.is_empty() {
|
||||||
|
return Err(SolverError::NoMatches.into());
|
||||||
|
}
|
||||||
|
Ok(matches[0].0.to_owned())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use libpt::log::info;
|
use libpt::log::info;
|
||||||
|
|
||||||
|
use crate::error::WResult;
|
||||||
use crate::wlist::word::Word;
|
use crate::wlist::word::Word;
|
||||||
use crate::wlist::WordList;
|
use crate::wlist::WordList;
|
||||||
|
|
||||||
|
@ -15,8 +16,8 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for StupidSolver<'wl, WL> {
|
||||||
info!("using stupid solver");
|
info!("using stupid solver");
|
||||||
Ok(Self { wl: wordlist })
|
Ok(Self { wl: wordlist })
|
||||||
}
|
}
|
||||||
fn guess_for(&self, game: &crate::game::Game<WL>) -> Word {
|
fn guess_for(&self, game: &crate::game::Game<WL>) -> WResult<Word> {
|
||||||
self.wl.rand_word().0
|
Ok(self.wl.rand_word().0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::fmt::{write, Debug};
|
use std::fmt::{write, Debug, Display};
|
||||||
|
|
||||||
use serde_json;
|
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:#?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use rand::seq::IteratorRandom;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::ops::RangeBounds;
|
use std::ops::RangeBounds;
|
||||||
|
|
||||||
#[cfg(feature = "builtin")]
|
#[cfg(feature = "builtin")]
|
||||||
|
@ -14,8 +15,8 @@ use crate::error::WResult;
|
||||||
|
|
||||||
pub type AnyWordlist = Box<dyn WordList>;
|
pub type AnyWordlist = Box<dyn WordList>;
|
||||||
|
|
||||||
pub trait WordList: Clone + std::fmt::Debug + Default + Sync {
|
pub trait WordList: Clone + std::fmt::Debug + Default + Sync + Display {
|
||||||
fn solutions(&self) -> ManyWordDatas {
|
fn solutions(&self) -> Vec<WordData> {
|
||||||
let wmap = self.wordmap().clone();
|
let wmap = self.wordmap().clone();
|
||||||
let threshold = wmap.threshold();
|
let threshold = wmap.threshold();
|
||||||
wmap.iter()
|
wmap.iter()
|
||||||
|
@ -41,6 +42,18 @@ pub trait WordList: Clone + std::fmt::Debug + Default + Sync {
|
||||||
fn total_freq(&self) -> Frequency {
|
fn total_freq(&self) -> Frequency {
|
||||||
self.wordmap().values().map(|a| a.to_owned()).sum()
|
self.wordmap().values().map(|a| a.to_owned()).sum()
|
||||||
}
|
}
|
||||||
|
fn sort_likelihood(&self) -> Vec<WordData> {
|
||||||
|
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<WordData> {
|
||||||
|
self.sort_likelihood().into_iter().take(n).collect()
|
||||||
|
}
|
||||||
fn over_threashold(&self) -> WordMap {
|
fn over_threashold(&self) -> WordMap {
|
||||||
let wmap = self.wordmap();
|
let wmap = self.wordmap();
|
||||||
let threshold = wmap.threshold();
|
let threshold = wmap.threshold();
|
||||||
|
@ -75,11 +88,11 @@ pub trait WordList: Clone + std::fmt::Debug + Default + Sync {
|
||||||
}
|
}
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
fn get_words_matching(&self, pattern: String) -> WResult<ManyWordDatas> {
|
fn get_words_matching(&self, pattern: String) -> WResult<Vec<WordData>> {
|
||||||
let pattern = Regex::new(&pattern)?;
|
let pattern = Regex::new(&pattern)?;
|
||||||
let hay = self.raw_wordlist();
|
let hay = self.raw_wordlist();
|
||||||
let keys = pattern.captures_iter(&hay);
|
let keys = pattern.captures_iter(&hay);
|
||||||
let mut buf = ManyWordDatas::new();
|
let mut buf = Vec::new();
|
||||||
for k in keys {
|
for k in keys {
|
||||||
let w: WordData = self.wordmap().get(&k[0]).unwrap();
|
let w: WordData = self.wordmap().get(&k[0]).unwrap();
|
||||||
buf.push(w)
|
buf.push(w)
|
||||||
|
|
|
@ -10,8 +10,7 @@ pub type Frequency = f64;
|
||||||
// PERF: Hash for String is probably a bottleneck
|
// PERF: Hash for String is probably a bottleneck
|
||||||
pub type Word = String;
|
pub type Word = String;
|
||||||
pub type WordData = (Word, Frequency);
|
pub type WordData = (Word, Frequency);
|
||||||
pub type ManyWordsRef<'a> = Vec<&'a Word>;
|
pub type WordDataRef<'wl> = (&'wl Word, &'wl Frequency);
|
||||||
pub type ManyWordDatas = Vec<(Word, Frequency)>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
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;
|
||||||
|
use wordle_analyzer::wlist::word::{Word, WordData};
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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"));
|
||||||
|
// 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<WordData> = Some(("nines".into(), 0.002));
|
||||||
|
let mut next_guess: Word;
|
||||||
|
|
||||||
|
next_guess = sl.guess_for(&game)?;
|
||||||
|
assert_eq!(next_guess, Word::from("which"));
|
||||||
|
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.clone(),
|
||||||
|
Some(Evaluation::build(&next_guess, "xxffx")?),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
next_guess = sl.guess_for(&game)?;
|
||||||
|
assert_eq!(next_guess, Word::from("being"));
|
||||||
|
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.clone(),
|
||||||
|
Some(Evaluation::build(&next_guess, "fcfxf")?),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
next_guess = sl.guess_for(&game)?;
|
||||||
|
assert_eq!(next_guess, Word::from("lines"));
|
||||||
|
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.clone(),
|
||||||
|
Some(Evaluation::build(&next_guess, "xcccc")?),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
next_guess = sl.guess_for(&game)?;
|
||||||
|
assert_eq!(next_guess, Word::from("wines"));
|
||||||
|
game.guess(
|
||||||
|
next_guess.clone(),
|
||||||
|
Some(Evaluation::build(&next_guess, "xcccc")?),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// naive is at the moment too bad to solve "nines"
|
||||||
|
assert!(game.finished());
|
||||||
|
assert!(!game.won());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue