generated from PlexSheep/rs-base
Compare commits
16 commits
2fcdc062c8
...
c140264a0c
Author | SHA1 | Date | |
---|---|---|---|
c140264a0c | |||
567d02d48b | |||
b6f639da67 | |||
1b63e4d9cc | |||
d5cf04d89b | |||
3c44760a2c | |||
f511e158cd | |||
29ec8bf219 | |||
3cd20d775e | |||
714aac0d86 | |||
c76c1d2d78 | |||
845fd73040 | |||
4b7e9433b9 | |||
970c1be143 | |||
6f72fda4ff | |||
1b354299d4 |
11 changed files with 294 additions and 58 deletions
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "wordle-analyzer"
|
name = "wordle-analyzer"
|
||||||
version = "0.1.0"
|
version = "0.1.0-alpha.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
authors = ["Christoph J. Scherr <software@cscherr.de>"]
|
authors = ["Christoph J. Scherr <software@cscherr.de>"]
|
||||||
|
@ -60,4 +60,7 @@ path = "src/bin/bench/cli.rs"
|
||||||
required-features = ["solve", "cli", "bench", "builtin"]
|
required-features = ["solve", "cli", "bench", "builtin"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
test-log = { version = "0.2.16", default-features = false, features = ["color", "trace"] }
|
test-log = { version = "0.2.16", default-features = false, features = [
|
||||||
|
"color",
|
||||||
|
"trace",
|
||||||
|
] }
|
||||||
|
|
|
@ -72,7 +72,7 @@ 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(50, &bench.builder())?;
|
bench.start(cli.n, &bench.builder())?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
sleep(std::time::Duration::from_secs(1));
|
sleep(std::time::Duration::from_secs(1));
|
||||||
|
|
|
@ -61,7 +61,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, None) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
GameError::GuessHasWrongLength(len) => {
|
GameError::GuessHasWrongLength(len) => {
|
||||||
|
|
|
@ -172,7 +172,7 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> {
|
||||||
eprintln!("{}", style(best_guess.unwrap_err()).red().bold());
|
eprintln!("{}", style(best_guess.unwrap_err()).red().bold());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
debug!("game state: {game:?}");
|
trace!("game state: {game:?}");
|
||||||
println!("best guess: {}", best_guess.unwrap());
|
println!("best guess: {}", best_guess.unwrap());
|
||||||
}
|
}
|
||||||
ReplCommand::Guess {
|
ReplCommand::Guess {
|
||||||
|
@ -181,14 +181,14 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> {
|
||||||
} => {
|
} => {
|
||||||
let evaluation_converted: Evaluation =
|
let evaluation_converted: Evaluation =
|
||||||
Evaluation::build(&your_guess, &evalutation)?;
|
Evaluation::build(&your_guess, &evalutation)?;
|
||||||
let guess = game.guess(your_guess, Some(evaluation_converted));
|
let guess = game.guess(&your_guess, Some(evaluation_converted));
|
||||||
debug!("your guess: {guess:?}");
|
debug!("your guess: {guess:?}");
|
||||||
if guess.is_err() {
|
if guess.is_err() {
|
||||||
eprintln!("{}", style(guess.unwrap_err()).red().bold());
|
eprintln!("{}", style(guess.unwrap_err()).red().bold());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
println!("{}", guess.unwrap());
|
println!("{}", guess.unwrap());
|
||||||
debug!("game state: {game:#?}");
|
trace!("game state: {game:#?}");
|
||||||
}
|
}
|
||||||
ReplCommand::New => game = builder.build()?,
|
ReplCommand::New => game = builder.build()?,
|
||||||
ReplCommand::Undo { n } => game.undo(n)?,
|
ReplCommand::Undo { n } => game.undo(n)?,
|
||||||
|
@ -218,7 +218,7 @@ fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> {
|
||||||
"eng" => BuiltinWList::english(cli.length),
|
"eng" => BuiltinWList::english(cli.length),
|
||||||
_ => BuiltinWList::load(&cli.wordlist, cli.length)?,
|
_ => BuiltinWList::load(&cli.wordlist, cli.length)?,
|
||||||
};
|
};
|
||||||
debug!("wordlist: {wl}");
|
trace!("wordlist: {wl}");
|
||||||
let mut builder = game::Game::builder(&wl)
|
let mut builder = game::Game::builder(&wl)
|
||||||
.length(cli.length)
|
.length(cli.length)
|
||||||
.max_steps(cli.max_steps)
|
.max_steps(cli.max_steps)
|
||||||
|
@ -238,13 +238,13 @@ fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> {
|
||||||
let solver = cli.solver.to_solver(&wl);
|
let solver = cli.solver.to_solver(&wl);
|
||||||
let mut game = builder.build()?;
|
let mut game = builder.build()?;
|
||||||
|
|
||||||
debug!("{game:#?}");
|
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)?;
|
||||||
debug!("game state: {game:#?}");
|
trace!("game state: {game:#?}");
|
||||||
println!("{}. guess: {response}", game.step() - 1);
|
println!("{}. guess: {response}", game.step() - 1);
|
||||||
|
|
||||||
if response.finished() {
|
if response.finished() {
|
||||||
|
|
|
@ -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;
|
use crate::wlist::word::{Word, WordData};
|
||||||
|
|
||||||
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>;
|
||||||
|
@ -62,8 +62,8 @@ pub enum BenchError {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error)]
|
#[derive(Debug, Clone, Error)]
|
||||||
pub enum SolverError {
|
pub enum SolverError {
|
||||||
#[error("Wordlist has no matches for the gamestate")]
|
#[error("Wordlist has no matches for the gamestate (solution: {0:?})")]
|
||||||
NoMatches,
|
NoMatches(Option<WordData>),
|
||||||
#[error("Unknown builtin solver")]
|
#[error("Unknown builtin solver")]
|
||||||
UnknownBuiltinSolver,
|
UnknownBuiltinSolver,
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,18 @@ impl Evaluation {
|
||||||
}
|
}
|
||||||
Ok(v.into())
|
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 {
|
impl IntoIterator for Evaluation {
|
||||||
|
@ -87,6 +99,12 @@ impl From<Vec<EvaluationUnit>> for Evaluation {
|
||||||
|
|
||||||
impl From<Evaluation> for Word {
|
impl From<Evaluation> for Word {
|
||||||
fn from(value: Evaluation) -> Self {
|
fn from(value: Evaluation) -> Self {
|
||||||
Word::from(value.inner.into_iter().map(|v| v.0).collect::<String>())
|
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>())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,16 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// No Errors
|
/// No Errors
|
||||||
|
///
|
||||||
|
/// # 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(
|
pub fn build(
|
||||||
length: usize,
|
length: usize,
|
||||||
precompute: bool,
|
precompute: bool,
|
||||||
|
@ -91,7 +101,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
///
|
///
|
||||||
/// This function will return an error if the length of the [Word] is wrong It will also error
|
/// 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, 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()));
|
||||||
}
|
}
|
||||||
|
@ -99,7 +109,7 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
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));
|
return Err(GameError::WordNotInWordlist(guess.to_string()));
|
||||||
}
|
}
|
||||||
self.step += 1;
|
self.step += 1;
|
||||||
|
|
||||||
|
@ -115,20 +125,22 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn evaluate(mut solution: WordData, guess: &Word) -> Evaluation {
|
/// 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;
|
||||||
for (idx, c) in guess.chars().enumerate() {
|
let mut buf = solution.0.clone();
|
||||||
if solution.0.chars().nth(idx) == Some(c) {
|
for ((idx, c_guess), c_sol) in guess.chars().enumerate().zip(solution.0.chars()) {
|
||||||
|
if c_guess == c_sol {
|
||||||
status = Status::Matched;
|
status = Status::Matched;
|
||||||
solution.0.replace_range(idx..idx + 1, "_");
|
buf.replace_range(idx..idx + 1, "_");
|
||||||
} else if solution.0.contains(c) {
|
} else if buf.contains(c_guess) {
|
||||||
status = Status::Exists;
|
status = Status::Exists;
|
||||||
solution.0 = solution.0.replacen(c, "_", 1);
|
buf = buf.replacen(c_guess, "_", 1);
|
||||||
} else {
|
} else {
|
||||||
status = Status::None
|
status = Status::None
|
||||||
}
|
}
|
||||||
evaluation.push((c, status));
|
evaluation.push((c_guess, status));
|
||||||
}
|
}
|
||||||
evaluation.into()
|
evaluation.into()
|
||||||
}
|
}
|
||||||
|
@ -140,18 +152,25 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
Ok(())
|
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.
|
||||||
|
///
|
||||||
|
/// 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> {
|
pub fn solution(&self) -> Option<&WordData> {
|
||||||
self.solution.as_ref()
|
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() {
|
if self.responses().is_empty() {
|
||||||
return false;
|
return false;
|
||||||
|
@ -159,27 +178,35 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
|
||||||
self.responses().last().unwrap().finished()
|
self.responses().last().unwrap().finished()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// true if the game has finished and the solution was found
|
||||||
pub fn won(&self) -> bool {
|
pub fn won(&self) -> bool {
|
||||||
if self.responses().is_empty() {
|
if !self.finished() || self.responses().is_empty() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.responses().last().unwrap().won()
|
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) -> Vec<&Word> {
|
pub(crate) fn made_guesses(&self) -> Vec<&Word> {
|
||||||
self.responses.iter().map(|r| r.guess()).collect()
|
self.responses.iter().map(|r| r.guess()).collect()
|
||||||
}
|
}
|
||||||
|
@ -297,6 +324,14 @@ impl<'wl, WL: WordList> GameBuilder<'wl, 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
|
/// Set the solution for the games built by the builder
|
||||||
///
|
///
|
||||||
/// If this is [Some], then the solution generated by
|
/// If this is [Some], then the solution generated by
|
||||||
|
|
|
@ -51,7 +51,7 @@ pub trait Solver<'wl, WL: WordList>: Clone + std::fmt::Debug + Sized + Sync {
|
||||||
///
|
///
|
||||||
/// This function will return an error if [guess_for](Solver::guess_for) fails.
|
/// 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)?, None)?)
|
||||||
}
|
}
|
||||||
/// Play a [Game] and return the last [GuessResponse].
|
/// Play a [Game] and return the last [GuessResponse].
|
||||||
///
|
///
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
use libpt::log::{info, trace};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use libpt::log::{debug, error, info, trace};
|
||||||
|
|
||||||
use crate::error::{SolverError, WResult};
|
use crate::error::{SolverError, WResult};
|
||||||
|
use crate::game::evaluation::{Evaluation, EvaluationUnit};
|
||||||
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};
|
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,
|
||||||
|
@ -26,51 +32,82 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
|
||||||
/// * Discard all words that don't have the chars that we know from the last guess are in the
|
/// * 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.
|
/// word, but don't know the position of.
|
||||||
fn guess_for(&self, game: &crate::game::Game<WL>) -> WResult<Word> {
|
fn guess_for(&self, game: &crate::game::Game<WL>) -> WResult<Word> {
|
||||||
// HACK: hardcoded length
|
let mut pattern: String = ".".repeat(game.length());
|
||||||
let mut pattern: String = String::from(".....");
|
// indexes we tried for that char and the number of occurences
|
||||||
let mut other_chars: Vec<char> = Vec::new();
|
let mut state: SolverState = SolverState::new();
|
||||||
let response = game.last_response();
|
let responses = game.responses().iter().enumerate();
|
||||||
trace!(
|
for (_idx, response) in responses {
|
||||||
"guessing best guess for last response: {response:#?}\n{:#?}",
|
let mut already_found_amounts: HashMap<char, usize> = HashMap::new();
|
||||||
response.map(|a| a.evaluation())
|
let evaluation: &Evaluation = response.evaluation();
|
||||||
);
|
for (idx, p) in evaluation.clone().into_iter().enumerate() {
|
||||||
if response.is_some() {
|
match p.1 {
|
||||||
for (idx, p) in response
|
Status::Matched => {
|
||||||
.unwrap()
|
pattern.replace_range(idx..idx + 1, &p.0.to_string());
|
||||||
.evaluation()
|
|
||||||
.clone()
|
state
|
||||||
.into_iter()
|
.char_map_mut()
|
||||||
.enumerate()
|
.entry(p.0)
|
||||||
{
|
.or_insert(CharInfo::new(game.length()))
|
||||||
if p.1 == Status::Matched {
|
.found_at(idx);
|
||||||
pattern.replace_range(idx..idx + 1, &p.0.to_string());
|
*already_found_amounts.entry(p.0).or_default() += 1;
|
||||||
} else if p.1 == Status::Exists {
|
}
|
||||||
other_chars.push(p.0)
|
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:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trace!("other chars: {:?}", other_chars);
|
|
||||||
let mut matches: Vec<WordData> = game.wordlist().get_words_matching(pattern)?;
|
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() {
|
if matches.is_empty() {
|
||||||
return Err(SolverError::NoMatches.into());
|
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
|
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))
|
||||||
// only words that contain the letters we found earlier (that were not matched)
|
.filter(|solution_candidate| {
|
||||||
.filter(|p| {
|
if !game.responses().is_empty()
|
||||||
// TODO: don't repeat unmatched contained chars on the same position twice #2
|
&& !state.has_all_known_contained(&solution_candidate.0)
|
||||||
let mut fits = true;
|
{
|
||||||
for c in other_chars.iter() {
|
trace!("known cont:{:#?}", state.get_all_known_contained());
|
||||||
fits &= p.0.contains(*c);
|
return false;
|
||||||
}
|
}
|
||||||
fits
|
for (idx, c) in solution_candidate.0.char_indices() {
|
||||||
|
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() {
|
if matches.is_empty() {
|
||||||
return Err(SolverError::NoMatches.into());
|
return Err(SolverError::NoMatches(game.solution().cloned()).into());
|
||||||
}
|
}
|
||||||
Ok(matches[0].0.to_owned())
|
Ok(matches[0].0.to_owned())
|
||||||
}
|
}
|
||||||
|
|
143
src/solve/naive/states.rs
Normal file
143
src/solve/naive/states.rs
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -93,7 +93,7 @@ pub trait WordList: Clone + std::fmt::Debug + Default + Sync + Display {
|
||||||
}
|
}
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
fn get_words_matching(&self, pattern: String) -> WResult<Vec<WordData>> {
|
fn get_words_matching(&self, pattern: &str) -> WResult<Vec<WordData>> {
|
||||||
let pattern = Regex::new(&pattern).map_err(WordlistError::from)?;
|
let pattern = Regex::new(&pattern).map_err(WordlistError::from)?;
|
||||||
let hay = self.raw_wordlist();
|
let hay = self.raw_wordlist();
|
||||||
let keys = pattern.captures_iter(&hay);
|
let keys = pattern.captures_iter(&hay);
|
||||||
|
|
Loading…
Add table
Reference in a new issue