Compare commits

...

16 Commits

Author SHA1 Message Date
Christoph J. Scherr a9549aedc0 WIP: make the solver work with the fixed game evaluation
cargo devel CI / cargo CI (push) Failing after 1m52s Details
2024-08-07 22:32:25 +02:00
Christoph J. Scherr c8043dce34 Merge branch 'devel' into feat/naive
cargo devel CI / cargo CI (push) Failing after 3m25s Details
2024-08-07 22:13:04 +02:00
Christoph J. Scherr c7b068ef31 feat(bench): move benching to actual benches, primitively
cargo devel CI / cargo CI (push) Successful in 6m12s Details
2024-08-07 22:10:17 +02:00
Christoph J. Scherr 7644970d1f docs(readme): make wordlist attributions clearer
cargo devel CI / cargo CI (push) Successful in 1m41s Details
2024-08-07 15:09:42 +02:00
PlexSheep 813fff8647 automatic cargo CI changes 2024-08-07 13:01:35 +00:00
Christoph J. Scherr 58f6646c6e chore: add back a test wrongly removed in merge
cargo devel CI / cargo CI (push) Failing after 3m11s Details
2024-08-07 15:01:11 +02:00
Christoph J. Scherr f63c3d0449 Merge branch 'devel' into feat/naive
cargo devel CI / cargo CI (push) Has been cancelled Details
2024-08-07 15:00:32 +02:00
Christoph J. Scherr 530f51c9cc Merge branch 'devel' into feat/naive
cargo devel CI / cargo CI (push) Failing after 1m54s Details
2024-08-07 14:29:18 +02:00
Christoph J. Scherr 38ae033798 WIP: try fix naive solver 2024-08-07 14:20:53 +02:00
Christoph J. Scherr 38d484fb5e test(naive): test that the naive solver should win it's games
cargo devel CI / cargo CI (push) Failing after 1m55s Details
2024-08-03 20:16:26 +02:00
Christoph J. Scherr 5f8c92de47 refactor(evaluation): fix yet another evaluation bug by refactoring
cargo devel CI / cargo CI (push) Failing after 1m49s Details
2024-08-03 20:07:15 +02:00
Christoph J. Scherr cd48322e89 Merge branch 'feat/naive' of https://git.cscherr.de/PlexSheep/wordle-analyzer into feat/naive
cargo devel CI / cargo CI (push) Failing after 1m51s Details
2024-08-03 19:17:18 +02:00
Christoph J. Scherr 5c43363cf8 fix(naive): solver works again after evaluation bugfixes, but is dumber than before 2024-08-03 19:17:11 +02:00
Christoph J. Scherr 6cc6694747 fix(naive): solver works again after evaluation bugfixes, but is dumber than before
cargo devel CI / cargo CI (push) Failing after 1m54s Details
2024-08-03 19:09:07 +02:00
Christoph J. Scherr af1a2ee36c fix(evaluation): added more evaluation tests and fixed #21
cargo devel CI / cargo CI (push) Failing after 2m12s Details
2024-08-03 18:51:53 +02:00
Christoph J. Scherr 05197181d0 test(game): test the evaluation
cargo devel CI / cargo CI (push) Failing after 1m50s Details
2024-08-02 17:37:50 +02:00
11 changed files with 322 additions and 68 deletions

View File

@ -64,3 +64,11 @@ test-log = { version = "0.2.16", default-features = false, features = [
"color", "color",
"trace", "trace",
] } ] }
[[bench]]
name = "solver_naive"
harness = false
[[bench]]
name = "solver_stupid"
harness = false

View File

@ -26,5 +26,6 @@ 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 --> <!-- TODO: make sure this is properly cited -->
* [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) * [`./data/wordlists/en_US_3b1b_freq_map.json`](./data/wordlists/en_US_3b1b_freq_map.json) --- [3Blue1Brown Top English words](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) * [`./data/wordlists/german_SUBTLEX-DE_full.json`](./data/wordlists/german_SUBTLEX-DE_full.json) --- [SUBTLEX-DE ~33.000 Common German Words](https://osf.io/py9ba/files/osfstorage)
* [`./data/wordlists/german_SUBTLEX-DE_small.json`](./data/wordlists/german_SUBTLEX-DE_small.json) --- [SUBTLEX-DE ~33.000 Common German Words (reduced to most common)](https://osf.io/py9ba/files/osfstorage)

18
benches/solver_naive.rs Normal file
View File

@ -0,0 +1,18 @@
use wordle_analyzer::bench::builtin::BuiltinBenchmark;
use wordle_analyzer::bench::Benchmark;
use wordle_analyzer::game::{self, GameBuilder};
use wordle_analyzer::solve::{NaiveSolver, Solver};
use wordle_analyzer::wlist::builtin::BuiltinWList;
fn main() -> anyhow::Result<()> {
let wl = BuiltinWList::english(5);
let builder: GameBuilder<'_, BuiltinWList> = game::Game::builder(&wl)
.length(5)
.max_steps(6)
.precompute(true);
let solver: NaiveSolver<_> = NaiveSolver::build(&wl)?;
let bench = BuiltinBenchmark::build(&wl, solver, builder, 16)?;
bench.start(2000, &bench.builder())?;
println!("{}", bench.report());
Ok(())
}

19
benches/solver_stupid.rs Normal file
View File

@ -0,0 +1,19 @@
use wordle_analyzer::bench::builtin::BuiltinBenchmark;
use wordle_analyzer::bench::Benchmark;
use wordle_analyzer::game::{self, GameBuilder};
use wordle_analyzer::solve::Solver;
use wordle_analyzer::solve::StupidSolver;
use wordle_analyzer::wlist::builtin::BuiltinWList;
fn main() -> anyhow::Result<()> {
let wl = BuiltinWList::english(5);
let builder: GameBuilder<'_, BuiltinWList> = game::Game::builder(&wl)
.length(5)
.max_steps(6)
.precompute(true);
let solver: StupidSolver<_> = StupidSolver::build(&wl)?;
let bench = BuiltinBenchmark::build(&wl, solver, builder, 16)?;
bench.start(2000, &bench.builder())?;
println!("{}", bench.report());
Ok(())
}

View File

@ -1,3 +1,7 @@
use std::fmt::Display;
use std::io::Write;
use colored::Colorize;
use libpt::cli::console::{style, StyledObject}; use libpt::cli::console::{style, StyledObject};
use crate::wlist::word::Word; use crate::wlist::word::Word;
@ -15,21 +19,6 @@ pub struct Evaluation {
} }
impl Evaluation { 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 /// The first string is the word the evaluation is for, The second string defines how the
/// characters of the first string match the solution. /// characters of the first string match the solution.
/// ///
@ -108,3 +97,20 @@ impl From<&Evaluation> for Word {
Word::from(value.inner.iter().map(|v| v.0).collect::<String>()) Word::from(value.inner.iter().map(|v| v.0).collect::<String>())
} }
} }
impl Display for Evaluation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for s in &self.inner {
write!(
f,
"{}",
match s.1 {
Status::None => s.0.to_string().into(),
Status::Exists => s.0.to_string().yellow(),
Status::Matched => s.0.to_string().green(),
}
)?;
}
std::fmt::Result::Ok(())
}
}

View File

@ -29,7 +29,6 @@ where
solution: Option<WordData>, solution: Option<WordData>,
wordlist: &'wl WL, wordlist: &'wl WL,
responses: Vec<GuessResponse>, responses: Vec<GuessResponse>,
// TODO: keep track of the letters the user has tried
} }
impl<'wl, WL: WordList> Game<'wl, WL> { impl<'wl, WL: WordList> Game<'wl, WL> {
@ -127,20 +126,40 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
/// Generates an [Evaluation] for a given solution and guess. /// Generates an [Evaluation] for a given solution and guess.
pub(crate) fn evaluate(solution: WordData, guess: &Word) -> Evaluation { pub(crate) fn evaluate(solution: WordData, guess: &Word) -> Evaluation {
let mut evaluation = Vec::new(); let solution = solution.0;
let mut evaluation: Vec<(char, Status)> = vec![('!', Status::None); solution.len()];
let mut status: Status; let mut status: Status;
let mut buf = solution.0.clone(); let mut buf: Vec<char> = solution.chars().collect();
for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.0.chars()) {
// first the correct solutions
for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.chars()) {
if c_guess == c_sol { if c_guess == c_sol {
status = Status::Matched; status = Status::Matched;
buf.replace_range(idx..idx + 1, "_"); buf[idx] = '!';
} else if buf.contains(c_guess) { evaluation[idx] = (c_guess, status);
status = Status::Exists;
buf = buf.replacen(c_guess, "_", 1);
} else {
status = Status::None
} }
evaluation.push((c_guess, status)); }
// then check if the char exists, but was not guessed to be at the correct position
//
// We split this up, because finding the "exists" chars at the same time as the "correct"
// chars causes bugs
for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.chars()) {
if c_guess == c_sol {
continue;
} else if buf.contains(&c_guess) {
status = Status::Exists;
// replace that char in the buffer to signal that is has been paired with the
// current char
let idx_of_a_match = buf
.iter()
.position(|c| *c == c_guess)
.expect("did not find a character in a string even though we know it exists");
buf[idx_of_a_match] = '!';
} else {
status = Status::None;
}
evaluation[idx] = (c_guess, status);
} }
evaluation.into() evaluation.into()
} }
@ -359,16 +378,8 @@ impl<'wl, WL: WordList> Display for Game<'wl, WL> {
self.step(), self.step(),
self.solution(), self.solution(),
)?; )?;
for s in self for s in self.responses() {
.responses() write!(f, "\"{s}\",")?;
.iter()
.map(|v| v.evaluation().to_owned().colorized_display())
{
write!(f, "\"")?;
for si in s {
write!(f, "{si}")?;
}
write!(f, "\", ")?;
} }
Ok(()) Ok(())
} }

View File

@ -92,17 +92,6 @@ 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() { write!(f, "{}", self.evaluation())
write!(
f,
"{}",
match s.1 {
Status::None => s.0.to_string().into(),
Status::Exists => s.0.to_string().yellow(),
Status::Matched => s.0.to_string().green(),
}
)?;
}
std::fmt::Result::Ok(())
} }
} }

View File

@ -3,15 +3,16 @@ use std::collections::HashMap;
use libpt::log::{debug, error, info, trace}; use libpt::log::{debug, error, info, trace};
use crate::error::{SolverError, WResult}; use crate::error::{SolverError, WResult};
use crate::game::evaluation::{Evaluation}; use crate::game::evaluation::{Evaluation, EvaluationUnit};
use crate::game::response::Status;
use crate::wlist::word::{Word, WordData}; use crate::wlist::word::{Word, WordData};
use crate::wlist::WordList; use crate::wlist::WordList;
use super::{AnyBuiltinSolver, Solver, Status};
mod states; mod states;
use states::*; use states::*;
use super::{AnyBuiltinSolver, Solver};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct NaiveSolver<'wl, WL> { pub struct NaiveSolver<'wl, WL> {
wl: &'wl WL, wl: &'wl WL,
@ -37,9 +38,10 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
let mut state: SolverState = SolverState::new(); let mut state: SolverState = SolverState::new();
let responses = game.responses().iter().enumerate(); let responses = game.responses().iter().enumerate();
for (_idx, response) in responses { for (_idx, response) in responses {
let mut already_found_amounts: HashMap<char, usize> = HashMap::new(); let mut abs_freq: HashMap<char, usize> = HashMap::new();
let evaluation: &Evaluation = response.evaluation(); let evaluation: &Evaluation = response.evaluation();
for (idx, p) in evaluation.clone().into_iter().enumerate() { for (idx, p) in evaluation.clone().into_iter().enumerate() {
state.start_step();
match p.1 { match p.1 {
Status::Matched => { Status::Matched => {
pattern.replace_range(idx..idx + 1, &p.0.to_string()); pattern.replace_range(idx..idx + 1, &p.0.to_string());
@ -49,7 +51,7 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
.entry(p.0) .entry(p.0)
.or_insert(CharInfo::new(game.length())) .or_insert(CharInfo::new(game.length()))
.found_at(idx); .found_at(idx);
*already_found_amounts.entry(p.0).or_default() += 1; *abs_freq.entry(p.0).or_default() += 1;
} }
Status::Exists => { Status::Exists => {
let cinfo = state let cinfo = state
@ -57,16 +59,19 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
.entry(p.0) .entry(p.0)
.or_insert(CharInfo::new(game.length())); .or_insert(CharInfo::new(game.length()));
cinfo.tried_but_failed(idx); cinfo.tried_but_failed(idx);
*already_found_amounts.entry(p.0).or_default() += 1; *abs_freq.entry(p.0).or_default() += 1;
cinfo.min_occurences(already_found_amounts[&p.0]);
} }
Status::None => state Status::None => {
let cinfo = state
.char_map_mut() .char_map_mut()
.entry(p.0) .entry(p.0)
.or_insert(CharInfo::new(game.length())) .or_insert(CharInfo::new(game.length()));
.max_occurences(*already_found_amounts.entry(p.0).or_default()), cinfo.tried_but_failed(idx);
abs_freq.entry(p.0).or_default();
} }
trace!("absolute frequencies: {already_found_amounts:?}"); }
trace!("absolute frequencies: {abs_freq:?}");
state.finish_step(&abs_freq);
} }
} }
@ -87,6 +92,9 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
.filter(|solution_candidate| { .filter(|solution_candidate| {
if !game.responses().is_empty() if !game.responses().is_empty()
&& !state.has_all_known_contained(&solution_candidate.0) && !state.has_all_known_contained(&solution_candidate.0)
// we need these sometimes,
// because we can't just input gibberish
//&& !state.has_wrong_chars(&solution_candidate.0)
{ {
trace!("known cont:{:#?}", state.get_all_known_contained()); trace!("known cont:{:#?}", state.get_all_known_contained());
return false; return false;

View File

@ -33,10 +33,17 @@ impl SolverState {
&mut self.char_map &mut self.char_map
} }
pub(crate) fn get_all_known_bad(&self) -> Vec<(&char, &CharInfo)> {
self.char_map
.iter()
.filter(|(_key, value)| value.not_in_solution())
.collect()
}
pub(crate) fn get_all_known_contained(&self) -> Vec<(&char, &CharInfo)> { pub(crate) fn get_all_known_contained(&self) -> Vec<(&char, &CharInfo)> {
self.char_map self.char_map
.iter() .iter()
.filter(|(key, value)| value.part_of_solution()) .filter(|(_key, value)| value.known_part_of_solution())
.collect() .collect()
} }
@ -48,6 +55,37 @@ impl SolverState {
} }
true true
} }
pub(crate) fn start_step(&mut self) {}
pub(crate) fn finish_step(&mut self, abs_freq: &HashMap<char, usize>) {
for (k, v) in abs_freq {
if *v == 0 {
self.char_map
.get_mut(k)
.expect(
"char in abs_freq was not added to the char_map before finalizing the step",
)
.max_occurences(0);
} else {
self.char_map
.get_mut(k)
.expect(
"char in abs_freq was not added to the char_map before finalizing the step",
)
.min_occurences(*v);
}
}
}
pub(crate) fn has_wrong_chars(&self, guess: &Word) -> bool {
for needed_char in self.get_all_known_bad() {
if guess.contains(*needed_char.0) {
return false;
}
}
true
}
} }
impl CharInfo { impl CharInfo {
@ -77,7 +115,12 @@ impl CharInfo {
} }
#[must_use] #[must_use]
pub fn part_of_solution(&self) -> bool { pub fn not_in_solution(&self) -> bool {
self.occurences_amount.end == 0
}
#[must_use]
pub fn known_part_of_solution(&self) -> bool {
self.occurences_amount.start > 0 && self.occurences_amount.end > 0 self.occurences_amount.start > 0 && self.occurences_amount.end > 0
} }
@ -129,7 +172,7 @@ impl CharInfo {
impl Debug for CharInfo { impl Debug for CharInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.part_of_solution() { if !self.not_in_solution() {
f.debug_struct("CharInfo") f.debug_struct("CharInfo")
.field("correct_idxs", &self.confirmed_indexes) .field("correct_idxs", &self.confirmed_indexes)
.field("amnt_occ", &self.occurences_amount) .field("amnt_occ", &self.occurences_amount)

135
tests/game.rs Normal file
View File

@ -0,0 +1,135 @@
use test_log::test; // set the log level with an envvar: `RUST_LOG=trace cargo test`
use libpt::log::info;
use wordle_analyzer::game::evaluation::Evaluation;
use wordle_analyzer::wlist::builtin::BuiltinWList;
use wordle_analyzer::wlist::WordList;
use wordle_analyzer::wlist::word::Word;
use wordle_analyzer::{self, game};
fn wordlist() -> impl WordList {
BuiltinWList::default()
}
#[test]
fn test_eval_simple() -> anyhow::Result<()> {
let wl = wordlist();
let builder = game::Game::builder(&wl)
.length(5)
.max_steps(6)
.solution(Some(wl.get_word(&Word::from("crate")).unwrap()))
.precompute(false);
let mut game = builder.build()?;
let guess = Word::from("slate");
game.guess(&guess, None)?;
let correct = Evaluation::build(&guess, "xxccc")?;
info!(
"{} =? {}",
*game.last_response().unwrap().evaluation(),
correct
);
assert_eq!(*game.last_response().unwrap().evaluation(), correct);
let mut game = builder.build()?;
let guess = Word::from("about");
game.guess(&guess, None)?;
let correct = Evaluation::build(&guess, "fxxxf")?;
info!(
"{} =? {}",
*game.last_response().unwrap().evaluation(),
correct
);
assert_eq!(*game.last_response().unwrap().evaluation(), correct);
Ok(())
}
#[test]
fn test_eval_reoccuring_char0() -> anyhow::Result<()> {
let wl = wordlist();
let builder = game::Game::builder(&wl)
.solution(Some(wl.get_word(&Word::from("nines")).unwrap()))
.precompute(false);
info!("solution=nines");
let mut game = builder.build()?;
let guess = Word::from("pines");
game.guess(&guess, None)?;
let correct = Evaluation::build(&guess, "xcccc")?;
info!(
"{} =? {}",
*game.last_response().unwrap().evaluation(),
correct
);
assert_eq!(*game.last_response().unwrap().evaluation(), correct);
let mut game = builder.build()?;
let guess = Word::from("sides");
game.guess(&guess, None)?;
let correct = Evaluation::build(&guess, "xcxcc")?;
info!(
"{} =? {}",
*game.last_response().unwrap().evaluation(),
correct
);
assert_eq!(*game.last_response().unwrap().evaluation(), correct);
let mut game = builder.build()?;
let guess = Word::from("ninja");
game.guess(&guess, None)?;
let correct = Evaluation::build(&guess, "cccxx")?;
info!(
"{} =? {}",
*game.last_response().unwrap().evaluation(),
correct
);
assert_eq!(*game.last_response().unwrap().evaluation(), correct);
let mut game = builder.build()?;
let guess = Word::from("which");
game.guess(&guess, None)?;
let correct = Evaluation::build(&guess, "xxfxx")?;
info!(
"{} =? {}",
*game.last_response().unwrap().evaluation(),
correct
);
assert_eq!(*game.last_response().unwrap().evaluation(), correct);
let mut game = builder.build()?;
let guess = Word::from("indie");
game.guess(&guess, None)?;
let correct = Evaluation::build(&guess, "ffxxf")?;
info!(
"{} =? {}",
*game.last_response().unwrap().evaluation(),
correct
);
assert_eq!(*game.last_response().unwrap().evaluation(), correct);
Ok(())
}
#[test]
fn test_eval_reoccuring_char1() -> anyhow::Result<()> {
let wl = wordlist();
let builder = game::Game::builder(&wl)
.solution(Some(wl.get_word(&Word::from("fatty")).unwrap()))
.precompute(false);
info!("solution=fatty");
let mut game = builder.build()?;
let guess = Word::from("state");
game.guess(&guess, None)?;
let correct = Evaluation::build(&guess, "xffcx")?;
info!(
"{} =? {}",
*game.last_response().unwrap().evaluation(),
correct
);
assert_eq!(*game.last_response().unwrap().evaluation(), correct);
Ok(())
}

View File

@ -1,12 +1,12 @@
use test_log::test; // set the log level with an envvar: `RUST_LOG=trace cargo test` 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::game::Game;
use wordle_analyzer::solve::{AnyBuiltinSolver, NaiveSolver, Solver, StupidSolver}; use wordle_analyzer::solve::{AnyBuiltinSolver, NaiveSolver, Solver, StupidSolver};
use wordle_analyzer::wlist::builtin::BuiltinWList; use wordle_analyzer::wlist::builtin::BuiltinWList;
use wordle_analyzer::wlist::word::{Word, WordData};
use wordle_analyzer::wlist::WordList; use wordle_analyzer::wlist::WordList;
use rayon::prelude::*;
fn wordlist() -> impl WordList { fn wordlist() -> impl WordList {
BuiltinWList::default() BuiltinWList::default()
} }
@ -19,3 +19,19 @@ fn test_build_builtin_solvers() {
let _naive_solver = let _naive_solver =
AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver"));
} }
#[test]
fn test_naive_win_games() -> anyhow::Result<()> {
let wl = wordlist();
let sl =
AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver"));
let builder = Game::builder(&wl);
{ 0..50 }.into_par_iter().for_each(|_round| {
let mut game = builder.build().expect("could not make game");
sl.play(&mut game).expect("could not play game");
assert!(game.finished());
assert!(game.won());
});
Ok(())
}