Compare commits

..

No commits in common. "c140264a0cf485f4d64e8e495a8c4bc2b82584c7" and "5f32f8227fe551dee8d61fd0fdad72bf362034a0" have entirely different histories.

24 changed files with 167 additions and 216279 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "wordle-analyzer" name = "wordle-analyzer"
version = "0.1.0-alpha.0" version = "0.1.0"
edition = "2021" edition = "2021"
publish = false publish = false
authors = ["Christoph J. Scherr <software@cscherr.de>"] authors = ["Christoph J. Scherr <software@cscherr.de>"]
@ -27,17 +27,15 @@ 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 = { version = "0.6.0", features = ["cli"] } libpt = "0.6.0"
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"
@ -58,9 +56,3 @@ 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",
] }

View file

@ -25,6 +25,4 @@ have to guess words by slowly guessing the letters contained in it.
Included in this repository are the following wordlists: Included in this repository are the following wordlists:
<!-- TODO: make sure this is properly cited --> * [3Blue1Brown Top English words -- `./data/wordlists/en_US_3b1b_freq_map.json`](https://github.com/3b1b/videos/tree/master/_2022/wordle/data)
* [3Blue1Brown Top English words](./data/wordlists/german_SUBTLEX-DE.json) --- [`./data/wordlists/en_US_3b1b_freq_map.json`](https://github.com/3b1b/videos/tree/master/_2022/wordle/data)
* [~33.000 Common German Words](./data/wordlists/german_SUBTLEX-DE.json) --- [SUBTLEX-DE](https://osf.io/py9ba/files/osfstorage)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,18 +0,0 @@
import json
# Load the word frequency dictionary
with open('../data/wordlists/german_SUBTLEX-DE_full.json', 'r') as f:
word_freqs = json.load(f)
# Set a frequency threshold (e.g., 0.001)
freq_threshold = 0.000001
# Set a maximum word length (e.g., 10)
max_word_length = 10
# Filter out words with low frequency and long length
filtered_word_freqs = {word: freq for word, freq in word_freqs.items() if freq >= freq_threshold and len(word) <= max_word_length}
# Save the filtered word frequencies to a new JSON file
with open('../data/wordlists/german_SUBTLEX-DE_small.json', 'w') as f:
json.dump(filtered_word_freqs, f, indent=4)

View file

@ -1,10 +0,0 @@
import json
with open('../data/wordlists/german_SUBTLEX-DE_small.json', 'r') as f:
word_freqs: dict[str,float] = json.load(f)
nd: dict[str, float] = dict()
for k in word_freqs:
nd[k.lower()] = word_freqs[k]
with open('../data/wordlists/german_SUBTLEX-DE_small.json', 'w') as f:
json.dump(nd, f, indent=4)

View file

@ -45,6 +45,7 @@ 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,

View file

@ -2,7 +2,7 @@
// #![warn(missing_docs)] // #![warn(missing_docs)]
#![warn(missing_debug_implementations)] #![warn(missing_debug_implementations)]
use std::thread::sleep; use std::thread::sleep_ms;
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}; use wordle_analyzer::solve::{AnyBuiltinSolver, BuiltinSolverNames, Solver};
use wordle_analyzer::wlist::builtin::BuiltinWList; use wordle_analyzer::wlist::builtin::BuiltinWList;
use wordle_analyzer::{self, game}; use wordle_analyzer::{self, game};
@ -41,13 +41,6 @@ struct Cli {
/// Note that the application as the whole will use at least one more thread. /// Note that the application as the whole will use at least one more thread.
#[arg(short, long, default_value_t = num_cpus::get())] #[arg(short, long, default_value_t = num_cpus::get())]
threads: usize, threads: usize,
/// select a wordlist
///
/// 'ger' and 'eng' are special values bundled with this executable, if the value does not
/// match either of those, it will be assumed to be a file path.
#[arg(short, long, default_value_t = String::from("eng"))]
wordlist: String,
} }
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
@ -59,11 +52,7 @@ fn main() -> anyhow::Result<()> {
} }
trace!("dumping CLI: {:#?}", cli); trace!("dumping CLI: {:#?}", cli);
let wl = match cli.wordlist.as_str() { let wl = BuiltinWList::default();
"ger" => BuiltinWList::german(cli.length),
"eng" => BuiltinWList::english(cli.length),
_ => BuiltinWList::load(&cli.wordlist, cli.length)?,
};
let builder: GameBuilder<'_, BuiltinWList> = game::Game::builder(&wl) let builder: GameBuilder<'_, BuiltinWList> = game::Game::builder(&wl)
.length(cli.length) .length(cli.length)
.max_steps(cli.max_steps) .max_steps(cli.max_steps)
@ -72,10 +61,10 @@ fn main() -> anyhow::Result<()> {
let bench = BuiltinBenchmark::build(&wl, solver, builder, cli.threads)?; let bench = BuiltinBenchmark::build(&wl, solver, builder, cli.threads)?;
trace!("{bench:#?}"); trace!("{bench:#?}");
bench.start(cli.n, &bench.builder())?; bench.start(50, &bench.builder())?;
loop { loop {
sleep(std::time::Duration::from_secs(1)); sleep_ms(1000);
println!("{}", bench.report()); println!("{}", bench.report());
if bench.is_finished() { if bench.is_finished() {
break; break;

View file

@ -27,12 +27,6 @@ struct Cli {
/// more verbose logs /// more verbose logs
#[arg(short, long)] #[arg(short, long)]
verbose: bool, verbose: bool,
/// select a wordlist
///
/// 'ger' and 'eng' are special values bundled with this executable, if the value does not
/// match either of those, it will be assumed to be a file path.
#[arg(short, long, default_value_t = String::from("eng"))]
wordlist: String,
} }
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
@ -44,11 +38,7 @@ fn main() -> anyhow::Result<()> {
} }
debug!("dumping CLI: {:#?}", cli); debug!("dumping CLI: {:#?}", cli);
let wl = match cli.wordlist.as_str() { let wl = BuiltinWList::default();
"ger" => BuiltinWList::german(cli.length),
"eng" => BuiltinWList::english(cli.length),
_ => BuiltinWList::load(&cli.wordlist, cli.length)?,
};
let builder = game::Game::builder(&wl) let builder = game::Game::builder(&wl)
.length(cli.length) .length(cli.length)
.max_steps(cli.max_steps) .max_steps(cli.max_steps)
@ -61,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, None) { response = match game.guess(guess) {
Ok(r) => r, Ok(r) => r,
Err(err) => match err { Err(err) => match err {
GameError::GuessHasWrongLength(len) => { GameError::GuessHasWrongLength(len) => {

View file

@ -1,4 +1,5 @@
#![warn(clippy::all)] #![warn(clippy::all)]
#![warn(missing_docs)]
#![warn(missing_debug_implementations)] #![warn(missing_debug_implementations)]
fn main() { fn main() {
unimplemented!(); unimplemented!();

View file

@ -2,20 +2,14 @@
// #![warn(missing_docs)] // #![warn(missing_docs)]
#![warn(missing_debug_implementations)] #![warn(missing_debug_implementations)]
use clap::{Parser, Subcommand}; use clap::Parser;
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)]
@ -31,220 +25,36 @@ 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
#[command(flatten)] #[arg(short, long)]
verbose: libpt::cli::args::VerbosityLevel, verbose: bool,
/// which solver to use /// which solver to use
#[arg(long, default_value_t = BuiltinSolverNames::default())] #[arg(short, 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>,
/// select a wordlist
///
/// 'ger' and 'eng' are special values bundled with this executable, if the value does not
/// match either of those, it will be assumed to be a file path.
#[arg(short, long, default_value_t = String::from("eng"))]
wordlist: String,
}
#[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();
Logger::builder() if cli.verbose {
.set_level(cli.verbose.level()) Logger::builder().set_level(Level::DEBUG).build().unwrap();
.build() } else {
.unwrap(); Logger::builder().set_level(Level::INFO).build().unwrap();
trace!("dumping CLI: {:#?}", cli);
if cli.non_interactive {
play_native_non_interactive(cli)?;
std::process::exit(0);
}
help_guess_interactive(cli)
} }
debug!("dumping CLI: {:#?}", cli);
fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> { let wl = BuiltinWList::default();
let wl = match cli.wordlist.as_str() { let builder = game::Game::builder(&wl)
"ger" => BuiltinWList::german(cli.length),
"eng" => BuiltinWList::english(cli.length),
_ => BuiltinWList::load(&cli.wordlist, cli.length)?,
};
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!("{game:#?}");
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;
}
trace!("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());
trace!("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 = match cli.wordlist.as_str() {
"ger" => BuiltinWList::german(cli.length),
"eng" => BuiltinWList::english(cli.length),
_ => BuiltinWList::load(&cli.wordlist, cli.length)?,
};
trace!("wordlist: {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()?;
trace!("{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)?;
trace!("game state: {game:#?}");
println!("{}. guess: {response}", game.step() - 1); println!("{}. guess: {response}", game.step() - 1);
if response.finished() { if response.finished() {
@ -256,5 +66,6 @@ fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> {
} else { } else {
println!("You lose! The solution was {:?}.", game.solution()); println!("You lose! The solution was {:?}.", game.solution());
} }
Ok(()) Ok(())
} }

View file

@ -1,7 +1,7 @@
use thiserror::Error; use thiserror::Error;
use crate::bench::report::Report; use crate::bench::report::Report;
use crate::wlist::word::{Word, WordData}; use crate::wlist::word::Word;
pub type WResult<T> = std::result::Result<T, Error>; pub type WResult<T> = std::result::Result<T, Error>;
pub type GameResult<T> = std::result::Result<T, GameError>; pub type GameResult<T> = std::result::Result<T, GameError>;
@ -13,16 +13,6 @@ pub enum Error {
#[from] #[from]
source: GameError, source: GameError,
}, },
#[error("Wordlist Error")]
WordlistError {
#[from]
source: WordlistError,
},
#[error("Solver Error")]
SolverError {
#[from]
source: SolverError,
},
#[error("Benchmark Error")] #[error("Benchmark Error")]
BenchError { BenchError {
#[from] #[from]
@ -33,6 +23,14 @@ pub enum Error {
#[from] #[from]
source: anyhow::Error, source: anyhow::Error,
}, },
// for `FromStr` of `BuiltinSolver`
#[error("Unknown builtin solver")]
UnknownBuiltinSolver,
#[error("pattern matching error")]
Regex {
#[from]
source: regex::Error,
},
#[error("Error sharing the benchmark data over multiple threads")] #[error("Error sharing the benchmark data over multiple threads")]
Mutex { Mutex {
#[from] #[from]
@ -46,12 +44,8 @@ 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 or use a word that is not in the wordlist ({0})")] #[error("Tried to guess 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)]
@ -59,30 +53,3 @@ 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 (solution: {0:?})")]
NoMatches(Option<WordData>),
#[error("Unknown builtin solver")]
UnknownBuiltinSolver,
}
#[derive(Debug, Error)]
pub enum WordlistError {
#[error("Wordlist has no matches for the gamestate")]
BadFormat {
#[from]
source: serde_json::Error,
},
#[error("I/O error (probably filesystem related)")]
IO {
#[from]
source: std::io::Error,
},
#[error("pattern matching error")]
Regex {
#[from]
source: regex::Error,
},
}

View file

@ -1,110 +0,0 @@
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())
}
pub fn inner(&self) -> &Vec<EvaluationUnit> {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut Vec<EvaluationUnit> {
&mut self.inner
}
pub fn guess(&self) -> Word {
Word::from(self)
}
}
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.iter().map(|v| v.0).collect::<String>())
}
}
impl From<&Evaluation> for Word {
fn from(value: &Evaluation) -> Self {
Word::from(value.inner.iter().map(|v| v.0).collect::<String>())
}
}

View file

@ -1,8 +1,5 @@
use core::panic;
use std::fmt::Display;
use crate::error::*; use crate::error::*;
use crate::wlist::word::{Word, WordData}; use crate::wlist::word::{ManyWordsRef, Word, WordData};
use crate::wlist::WordList; use crate::wlist::WordList;
use libpt::log::{debug, trace}; use libpt::log::{debug, trace};
@ -10,11 +7,8 @@ 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)]
@ -26,8 +20,9 @@ where
precompute: bool, precompute: bool,
max_steps: usize, max_steps: usize,
step: usize, step: usize,
solution: Option<WordData>, solution: 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
} }
@ -35,7 +30,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, true) GameBuilder::new(wl)
} }
/// Create a [Game] of wordle /// Create a [Game] of wordle
/// ///
@ -48,166 +43,104 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
/// ///
/// # Errors /// # Errors
/// ///
/// No Errors /// This function will return an error if .
/// pub(crate) fn build(
/// # Parameters
///
/// `length` - how many chars the solution has
/// `precompute` - how many chars the solution has
/// `max_steps` - how many tries the player has
/// `precompute` - how many chars the solution has
/// `wlist` - which wordlist to use
/// `generate_solution` - should the game have a randomly generated solution?
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: if generate_solution { 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, eval: Option<Evaluation>) -> GameResult<GuessResponse> { pub fn guess(&mut self, guess: Word) -> 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() {
return Err(GameError::WordNotInWordlist(guess.to_string())); return Err(GameError::WordNotInWordlist(guess));
} }
self.step += 1; self.step += 1;
let response; let mut compare_solution = self.solution.0.clone();
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)
}
/// Generates an [Evaluation] for a given solution and guess.
pub(crate) 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;
let mut buf = solution.0.clone(); for (idx, c) in guess.chars().enumerate() {
for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.0.chars()) { if compare_solution.chars().nth(idx) == Some(c) {
if c_guess == c_sol {
status = Status::Matched; status = Status::Matched;
buf.replace_range(idx..idx + 1, "_"); compare_solution.replace_range(idx..idx + 1, "_");
} else if buf.contains(c_guess) { } else if compare_solution.contains(c) {
status = Status::Exists; status = Status::Exists;
buf = buf.replacen(c_guess, "_", 1); compare_solution = compare_solution.replacen(c, "_", 1);
} else { } else {
status = Status::None status = Status::None
} }
evaluation.push((c_guess, status)); evaluation.push((c, status));
}
evaluation.into()
} }
/// discard the last n responses let response = GuessResponse::new(guess, evaluation, self);
pub fn undo(&mut self, n: usize) -> WResult<()> { self.responses.push(response.clone());
self.responses self.finished = response.finished();
.drain(self.responses.len() - n..self.responses.len()); Ok(response)
Ok(())
} }
/// get how many characters the words have for this game
pub fn length(&self) -> usize { pub fn length(&self) -> usize {
self.length self.length
} }
/// get the solution for this game, if the game is aware of one. pub fn solution(&self) -> &WordData {
/// &self.solution
/// Consider that games may also be played on other platforms, so the game might not "know" the
/// solution yet.
pub fn solution(&self) -> Option<&WordData> {
self.solution.as_ref()
} }
/// get how many guesses have been made already
pub fn step(&self) -> usize { pub fn step(&self) -> usize {
self.step self.step
} }
/// true if the game has finished and no more guesses can be made
pub fn finished(&self) -> bool { pub fn finished(&self) -> bool {
if self.responses().is_empty() { self.finished
return false;
}
self.responses().last().unwrap().finished()
} }
/// true if the game has finished and the solution was found
pub fn won(&self) -> bool {
if !self.finished() || self.responses().is_empty() {
return false;
}
self.responses().last().unwrap().won()
}
/// get how many tries the player has
pub fn max_steps(&self) -> usize { pub fn max_steps(&self) -> usize {
self.max_steps self.max_steps
} }
/// get the responses that were already made
pub fn responses(&self) -> &Vec<GuessResponse> { pub fn responses(&self) -> &Vec<GuessResponse> {
&self.responses &self.responses
} }
/// get the most recent response
pub fn last_response(&self) -> Option<&GuessResponse> { pub fn last_response(&self) -> Option<&GuessResponse> {
self.responses().last() self.responses().last()
} }
/// get the [WordList] for this game
pub fn wordlist(&self) -> &WL { pub fn wordlist(&self) -> &WL {
self.wordlist self.wordlist
} }
/// get the [Words](Word) that have already been tried 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()
} }
} }
@ -227,7 +160,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, true) /// let game: Game<_> = GameBuilder::new(&wl)
/// .build()?; /// .build()?;
/// # Ok(()) /// # Ok(())
/// # } /// # }
@ -248,44 +181,32 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
/// # } /// # }
/// ``` /// ```
/// ///
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
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, generate_solution: bool) -> Self { pub fn new(wl: &'wl WL) -> 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 mut game: Game<WL> = Game::build( let game: Game<WL> =
self.length, Game::build(self.length, self.precompute, self.max_steps, self.wordlist)?;
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)
} }
@ -323,53 +244,4 @@ impl<'wl, WL: WordList> GameBuilder<'wl, WL> {
self.wordlist = wl; self.wordlist = wl;
self self
} }
/// Enable or disable Generation of a solution for this builder
///
/// Default is true
pub fn generate_solution(mut self, generate: bool) -> Self {
self.generate_solution = generate;
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(())
}
} }

View file

@ -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::{Evaluation, Game}; use super::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,13 +13,15 @@ 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,
solution: Option<WordData>, finish: bool,
solution: WordData,
step: usize, step: usize,
max_steps: usize, max_steps: usize,
} }
@ -32,48 +34,44 @@ 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>(guess: &Word, status: Evaluation, game: &Game<WL>) -> Self { pub(crate) fn new<WL: WordList>(
let new = Self { guess: Word,
guess: guess.to_owned(), status: Vec<(char, Status)>,
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,
solution: game.solution().cloned(), finish,
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.step() > self.max_steps() || self.won() self.finish
} }
pub fn won(&self) -> bool { pub fn won(&self) -> bool {
let mut ok = true; self.guess == self.solution.0
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> {
self.solution.clone() if self.won() {
Some(self.solution.clone())
} else {
None
}
} }
pub fn evaluation(&self) -> &Evaluation { pub fn evaluation(&self) -> &[(char, Status)] {
&self.evaluation &self.evaluation
} }
@ -92,7 +90,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.clone().into_iter() { for s in &self.evaluation {
write!( write!(
f, f,
"{}", "{}",

View file

@ -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

View file

@ -1,7 +1,7 @@
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr};
use crate::{ use crate::{
error::{SolverError, WResult}, error::{Error, WResult},
game::{response::*, Game}, game::{response::*, Game},
wlist::{ wlist::{
word::{Word, WordData}, word::{Word, WordData},
@ -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>) -> WResult<Word>; fn guess_for(&self, game: &Game<'wl, WL>) -> 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)?, None)?) Ok(game.guess(self.guess_for(game))?)
} }
/// Play a [Game] and return the last [GuessResponse]. /// Play a [Game] and return the last [GuessResponse].
/// ///
@ -59,7 +59,6 @@ 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)?;
@ -127,7 +126,7 @@ impl BuiltinSolverNames {
} }
impl FromStr for BuiltinSolverNames { impl FromStr for BuiltinSolverNames {
type Err = SolverError; type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"naive" => Ok(Self::Naive), "naive" => Ok(Self::Naive),
@ -147,10 +146,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>) -> WResult<Word> { fn guess_for(&self, game: &Game<'wl, WL>) -> Word {
Ok(match self { 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),
}) }
} }
} }

View file

@ -1,17 +1,10 @@
use std::collections::HashMap; use libpt::log::{info, trace};
use libpt::log::{debug, error, info, trace}; use crate::wlist::word::{ManyWordDatas, Word};
use crate::error::{SolverError, WResult};
use crate::game::evaluation::{Evaluation, EvaluationUnit};
use crate::wlist::word::{Word, WordData};
use crate::wlist::WordList; use crate::wlist::WordList;
use super::{AnyBuiltinSolver, Solver, Status}; use super::{AnyBuiltinSolver, Solver, Status};
mod states;
use states::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NaiveSolver<'wl, WL> { pub struct NaiveSolver<'wl, WL> {
wl: &'wl WL, wl: &'wl WL,
@ -22,94 +15,40 @@ 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 })
} }
/// Guess a word from the wordlist for the given game fn guess_for(&self, game: &crate::game::Game<WL>) -> Word {
/// // HACK: hardcoded length
/// ## Algorithm let mut pattern: String = String::from(".....");
/// let mut other_chars: Vec<char> = Vec::new();
/// * Look at the evaluation for the last response and keep the correct letters let response = game.last_response();
/// * Get all words that have these letters at the right position if response.is_some() {
/// * Discard words that have already been tried for (idx, p) in response.unwrap().evaluation().iter().enumerate() {
/// * Discard all words that don't have the chars that we know from the last guess are in the if p.1 == Status::Matched {
/// word, but don't know the position of.
fn guess_for(&self, game: &crate::game::Game<WL>) -> WResult<Word> {
let mut pattern: String = ".".repeat(game.length());
// indexes we tried for that char and the number of occurences
let mut state: SolverState = SolverState::new();
let responses = game.responses().iter().enumerate();
for (_idx, response) in responses {
let mut already_found_amounts: HashMap<char, usize> = HashMap::new();
let evaluation: &Evaluation = response.evaluation();
for (idx, p) in evaluation.clone().into_iter().enumerate() {
match 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 {
state other_chars.push(p.0)
.char_map_mut()
.entry(p.0)
.or_insert(CharInfo::new(game.length()))
.found_at(idx);
*already_found_amounts.entry(p.0).or_default() += 1;
}
Status::Exists => {
let cinfo = state
.char_map_mut()
.entry(p.0)
.or_insert(CharInfo::new(game.length()));
cinfo.tried_but_failed(idx);
*already_found_amounts.entry(p.0).or_default() += 1;
cinfo.min_occurences(already_found_amounts[&p.0]);
}
Status::None => state
.char_map_mut()
.entry(p.0)
.or_insert(CharInfo::new(game.length()))
.max_occurences(*already_found_amounts.entry(p.0).or_default()),
}
trace!("absolute frequencies: {already_found_amounts:?}");
} }
} }
debug!("built state from responses: {state:#?}");
// get all words that have the correct chars on the same positions
let mut matches: Vec<WordData> = game.wordlist().get_words_matching(&pattern)?;
if matches.is_empty() {
error!("no matches even when just considering the known good chars");
return Err(SolverError::NoMatches(game.solution().cloned()).into());
} else {
trace!("found {} basic matches", matches.len())
} }
matches = matches trace!("other chars: {:?}", other_chars);
let matches: ManyWordDatas = game
.wordlist()
.get_words_matching(pattern)
.expect("the solution does not exist in the wordlist")
.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))
.filter(|solution_candidate| { // only words that contain the letters we found earlier (that were not matched)
if !game.responses().is_empty() .filter(|p| {
&& !state.has_all_known_contained(&solution_candidate.0) // TODO: don't repeat unmatched contained chars on the same position twice #2
{ let mut fits = true;
trace!("known cont:{:#?}", state.get_all_known_contained()); for c in other_chars.iter() {
return false; fits &= p.0.contains(*c);
} }
for (idx, c) in solution_candidate.0.char_indices() { fits
let cinfo = state
.char_map_mut()
.entry(c)
.or_insert(CharInfo::new(game.length()));
if !cinfo.occurences_of_char_possible(&solution_candidate.0, c)
|| cinfo.has_been_tried(idx)
{
return false;
}
}
true
}) })
.map(|v| v.to_owned()) .map(|v| v.to_owned())
.collect(); .collect();
if matches.is_empty() { matches[0].0.to_owned()
return Err(SolverError::NoMatches(game.solution().cloned()).into());
}
Ok(matches[0].0.to_owned())
} }
} }

View file

@ -1,143 +0,0 @@
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::{Range, RangeBounds};
use crate::error::WResult;
use crate::wlist::word::Word;
pub(crate) type CharMap = HashMap<char, CharInfo>;
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct CharInfo {
confirmed_indexes: HashSet<usize>,
bad_indexes: HashSet<usize>,
occurences_amount: Range<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SolverState {
char_map: CharMap,
}
impl SolverState {
pub fn new() -> Self {
Self {
char_map: HashMap::new(),
}
}
pub fn char_map(&self) -> &CharMap {
&self.char_map
}
pub fn char_map_mut(&mut self) -> &mut CharMap {
&mut self.char_map
}
pub(crate) fn get_all_known_contained(&self) -> Vec<(&char, &CharInfo)> {
self.char_map
.iter()
.filter(|(key, value)| value.part_of_solution())
.collect()
}
pub(crate) fn has_all_known_contained(&self, guess: &Word) -> bool {
for needed_char in self.get_all_known_contained() {
if !guess.contains(*needed_char.0) {
return false;
}
}
true
}
}
impl CharInfo {
pub fn new(word_length: usize) -> Self {
Self {
confirmed_indexes: HashSet::new(),
bad_indexes: HashSet::new(),
occurences_amount: 0..word_length,
}
}
pub fn found_at(&mut self, idx: usize) {
self.confirmed_indexes.insert(idx);
if self.occurences_amount.start < 1 {
self.occurences_amount.start = 1;
}
}
/// tried to guess a char we know exists at this position, but it was incorrect
pub fn tried_but_failed(&mut self, idx: usize) {
self.bad_indexes.insert(idx);
}
pub fn has_been_tried(&self, idx: usize) -> bool {
self.bad_indexes.contains(&idx)
}
#[must_use]
pub fn part_of_solution(&self) -> bool {
self.occurences_amount.start > 0 && self.occurences_amount.end > 0
}
pub fn min_occurences(&mut self, min: usize) {
self.occurences_amount.start = min;
}
pub(crate) fn max_occurences(&mut self, max: usize) {
self.occurences_amount.end = max
}
pub(crate) fn confirmed_indexes(&self) -> &HashSet<usize> {
&self.confirmed_indexes
}
pub(crate) fn confirmed_indexes_mut(&mut self) -> &mut HashSet<usize> {
&mut self.confirmed_indexes
}
pub(crate) fn tried_indexes(&self) -> &HashSet<usize> {
&self.bad_indexes
}
pub(crate) fn tried_indexes_mut(&mut self) -> &mut HashSet<usize> {
&mut self.bad_indexes
}
pub(crate) fn occurences_amount(&self) -> &Range<usize> {
&self.occurences_amount
}
pub(crate) fn occurences_amount_mut(&mut self) -> &mut Range<usize> {
&mut self.occurences_amount
}
pub(crate) fn occurences_of_char_possible(
&self,
solution_candidate: &str,
character: char,
) -> bool {
let occ = solution_candidate
.chars()
.filter(|c| *c == character)
.count();
self.occurences_amount.start <= occ && occ <= self.occurences_amount.end
}
}
impl Debug for CharInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.part_of_solution() {
f.debug_struct("CharInfo")
.field("correct_idxs", &self.confirmed_indexes)
.field("amnt_occ", &self.occurences_amount)
.field("bad_idxs", &self.bad_indexes)
.finish()
} else {
write!(f, "(not in solution)")
}
}
}

View file

@ -1,6 +1,5 @@
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;
@ -16,8 +15,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>) -> WResult<Word> { fn guess_for(&self, game: &crate::game::Game<WL>) -> Word {
Ok(self.wl.rand_word().0) self.wl.rand_word().0
} }
} }

View file

@ -1,82 +1,14 @@
use std::fmt::{Debug, Display}; use std::fmt::{write, Debug};
use std::path::Path;
use serde_json; use serde_json;
use crate::error::WordlistError;
use super::{Word, WordList}; use super::{Word, WordList};
pub const RAW_WORDLIST_BUNDLED_ENGLISH: &str = const RAW_WORDLIST_FILE: &str = include_str!("../../data/wordlists/en_US_3b1b_freq_map.json");
include_str!("../../data/wordlists/en_US_3b1b_freq_map.json");
pub const RAW_WORDLIST_BUNDLED_GERMAN_SMALL: &str =
include_str!("../../data/wordlists/german_SUBTLEX-DE_small.json");
pub const RAW_WORDLIST_PATH_ENGLISH: &str = "../../data/wordlists/en_US_3b1b_freq_map.json";
pub const RAW_WORDLIST_PATH_GERMAN_FULL: &str = "../../data/wordlists/german_SUBTLEX-DE_full.json";
pub const RAW_WORDLIST_PATH_GERMAN_SMALL: &str = "../../data/wordlists/german_SUBTLEX-DE_full.json";
#[derive(Clone)] #[derive(Clone)]
pub struct BuiltinWList { pub struct BuiltinWList {
words: super::WordMap, words: super::WordMap,
name: String,
}
impl BuiltinWList {
/// load a wordlist from file
///
/// Wordlist files are expected to have the following format:
///
/// ```json
/// {
/// "word": 0.001
/// }
/// ```
///
/// Where the number is the frequency. Higher/Lower case is ignored.
///
/// Only words with the specified length will be included.
///
/// ## Errors
///
/// Will fail if the file path cannot be read or the format is wrong.
pub fn load<P: AsRef<std::path::Path>>(wl_path: P, len: usize) -> Result<Self, WordlistError> {
let path: &Path = wl_path.as_ref();
let file = std::fs::File::open(path)?;
// don't load the whole string into memory
let reader = std::io::BufReader::new(file);
let mut words: super::WordMap = serde_json::from_reader(reader)?;
words.only_words_with_len(len);
let name: String = if let Some(osstr) = path.file_name() {
osstr.to_str().unwrap_or("(no name)").to_string()
} else {
"(no name)".to_string()
};
Ok(Self { words, name })
}
pub fn english(len: usize) -> Self {
let mut words: super::WordMap = serde_json::from_str(RAW_WORDLIST_BUNDLED_ENGLISH).unwrap();
words.only_words_with_len(len);
Self {
words,
name: "(builtin english)".to_string(),
}
}
pub fn german(len: usize) -> Self {
let mut words: super::WordMap =
serde_json::from_str(RAW_WORDLIST_BUNDLED_GERMAN_SMALL).unwrap();
words.only_words_with_len(len);
Self {
words,
name: "(builtin german)".to_string(),
}
}
} }
impl super::WordList for BuiltinWList { impl super::WordList for BuiltinWList {
@ -93,27 +25,32 @@ impl super::WordList for BuiltinWList {
impl Default for BuiltinWList { impl Default for BuiltinWList {
fn default() -> Self { fn default() -> Self {
Self::english(5) let words: super::WordMap = serde_json::from_str(RAW_WORDLIST_FILE).unwrap();
Self { words }
} }
} }
impl Debug for BuiltinWList { impl Debug for BuiltinWList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BuiltinWList") write(
.field("name", &self.name)
.field("words", &self.words)
.finish()
}
}
impl Display for BuiltinWList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f, f,
"{}:\nwords:\t{}\ntop 5:\t{:?}", format_args!(
self.name, "BuiltinWList {{ \n\
self.len(), \tamount: {}, \n\
self.n_most_likely(5) \ttotal_freq: {}, \n\
\tcommon: {}, \n\
\tthreshold: {}, \n\
\tfreq_range: {:?}, \n\
\tover_threshold: {:#?}, \n\
}}",
self.amount(),
self.total_freq(),
self.wordmap().n_common(),
self.wordmap().threshold(),
self.wordmap().freq_range(),
self.over_threashold()
),
) )
} }
} }

View file

@ -3,7 +3,6 @@ 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")]
@ -11,12 +10,12 @@ pub mod builtin;
pub mod word; pub mod word;
use word::*; use word::*;
use crate::error::{WResult, WordlistError}; 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 + Display { pub trait WordList: Clone + std::fmt::Debug + Default + Sync {
fn solutions(&self) -> Vec<WordData> { fn solutions(&self) -> ManyWordDatas {
let wmap = self.wordmap().clone(); let wmap = self.wordmap().clone();
let threshold = wmap.threshold(); let threshold = wmap.threshold();
wmap.iter() wmap.iter()
@ -35,30 +34,13 @@ pub trait WordList: Clone + std::fmt::Debug + Default + Sync + Display {
(w.0.clone(), *w.1) (w.0.clone(), *w.1)
} }
fn length_range(&self) -> impl RangeBounds<usize>; fn length_range(&self) -> impl RangeBounds<usize>;
#[must_use] fn amount(&self) -> usize {
fn len(&self) -> usize {
self.solutions().len() self.solutions().len()
} }
#[must_use]
fn is_empty(&self) -> bool {
self.solutions().len() == 0
}
fn wordmap(&self) -> &WordMap; fn wordmap(&self) -> &WordMap;
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();
@ -93,11 +75,11 @@ pub trait WordList: Clone + std::fmt::Debug + Default + Sync + Display {
} }
buf buf
} }
fn get_words_matching(&self, pattern: &str) -> WResult<Vec<WordData>> { fn get_words_matching(&self, pattern: String) -> WResult<ManyWordDatas> {
let pattern = Regex::new(&pattern).map_err(WordlistError::from)?; 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 = Vec::new(); let mut buf = ManyWordDatas::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)

View file

@ -10,14 +10,14 @@ 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 WordDataRef<'wl> = (&'wl Word, &'wl Frequency); pub type ManyWordsRef<'a> = Vec<&'a Word>;
pub(crate) type WordMapInner = HashMap<Word, 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))]
pub struct WordMap { pub struct WordMap {
#[serde(flatten)] #[serde(flatten)]
inner: WordMapInner, inner: HashMap<Word, Frequency>,
} }
impl Default for WordMap { impl Default for WordMap {
@ -72,9 +72,6 @@ impl WordMap {
pub fn inner(&self) -> &HashMap<Word, Frequency> { pub fn inner(&self) -> &HashMap<Word, Frequency> {
&self.inner &self.inner
} }
pub fn inner_mut(&mut self) -> &mut HashMap<Word, Frequency> {
&mut self.inner
}
pub fn get<I: std::fmt::Display>(&self, word: I) -> Option<WordData> { pub fn get<I: std::fmt::Display>(&self, word: I) -> Option<WordData> {
self.inner self.inner
.get(&word.to_string()) .get(&word.to_string())
@ -86,14 +83,6 @@ impl WordMap {
abs.into_iter().map(|p| (p.0, p.1 as f64 / n)).collect(); abs.into_iter().map(|p| (p.0, p.1 as f64 / n)).collect();
relative.into() relative.into()
} }
pub fn only_words_with_len(&mut self, len: usize) {
self.inner = self
.inner
.iter()
.filter(|a| a.0.len() == len)
.map(|a| (a.0.to_owned(), *a.1))
.collect::<WordMapInner>();
}
} }
impl std::fmt::Debug for WordMap { impl std::fmt::Debug for WordMap {

View file

@ -1,139 +0,0 @@
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(())
}