add interactive solver #4

Merged
cscherrNT merged 29 commits from feat/interactive-solver into devel 2024-07-25 15:19:54 +02:00
9 changed files with 105 additions and 34 deletions
Showing only changes of commit 18a5125028 - Show all commits

View File

@ -37,6 +37,7 @@ serde_json = { version = "1.0.114", optional = true }
strum = "0.26.3" 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"
@ -57,3 +58,6 @@ required-features = ["solve", "cli", "builtin"]
name = "wordlebench" name = "wordlebench"
path = "src/bin/bench/cli.rs" path = "src/bin/bench/cli.rs"
required-features = ["solve", "cli", "bench", "builtin"] required-features = ["solve", "cli", "bench", "builtin"]
[dev-dependencies]
test-log = { version = "0.2.16", default-features = false, features = ["color", "trace"] }

View File

@ -45,7 +45,6 @@ where
// TODO: add some interface to get reports while the benchmark runs // TODO: add some interface to get reports while the benchmark runs
// TODO: make the benchmark optionally multithreaded // TODO: make the benchmark optionally multithreaded
// NOTE: This is blocking, use start to let it run in another thread // NOTE: This is blocking, use start to let it run in another thread
// FIXME: this never stops? Reports just keep getting printed
fn bench( fn bench(
&self, &self,
n: usize, n: usize,

View File

@ -75,7 +75,7 @@ enum ReplCommand {
/// is correct /// is correct
Guess { Guess {
your_guess: String, your_guess: String,
evalutation: Evaluation, evalutation: String,
}, },
/// Let the solver make a guess /// Let the solver make a guess
Solve, Solve,
@ -163,7 +163,9 @@ fn help_guess_interactive(cli: Cli) -> anyhow::Result<()> {
your_guess, your_guess,
evalutation, evalutation,
} => { } => {
let guess = game.guess(your_guess, Some(evalutation)); let evaluation_converted: Evaluation =
Evaluation::build(&your_guess, &evalutation)?;
let guess = game.guess(your_guess, Some(evaluation_converted));
debug!("your guess: {guess:?}"); 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());

View File

@ -51,6 +51,10 @@ pub enum GameError {
TryingToPlayAFinishedGame, TryingToPlayAFinishedGame,
#[error("Tried to guess or use a word that is not in the wordlist ({0})")] #[error("Tried to guess or use a word that is not in the wordlist ({0})")]
WordNotInWordlist(Word), WordNotInWordlist(Word),
#[error("Invalid syntax for manual evaluation creation")]
InvalidEvaluationSyntax(String),
#[error("The length of guess and evaluation must be the same")]
GuessAndEvalNotSameLen((String, String)),
} }
#[derive(Debug, Clone, Error)] #[derive(Debug, Clone, Error)]

View File

@ -1,25 +1,25 @@
use std::convert::Infallible;
use std::str::FromStr;
use libpt::cli::console::{style, StyledObject}; use libpt::cli::console::{style, StyledObject};
use crate::wlist::word::Word; use crate::wlist::word::Word;
use super::response::Status; use super::response::Status;
use super::{GameError, WResult};
/// the [char] of the guess and the [Status] associated with it
pub type EvaluationUnit = (char, Status); pub type EvaluationUnit = (char, Status);
/// Basically a [String] with extra information associated with each char
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct Evaluation { pub struct Evaluation {
inner: Vec<EvaluationUnit>, inner: Vec<EvaluationUnit>,
} }
impl Evaluation { impl Evaluation {
pub(crate) fn colorized_display(&self, guess: &Word) -> Vec<StyledObject<String>> { /// Display the evaluation color coded
assert_eq!(guess.len(), self.inner.len()); pub fn colorized_display(&self) -> Vec<StyledObject<String>> {
let mut buf = Vec::new(); let mut buf = Vec::new();
for (i, e) in self.inner.iter().enumerate() { for e in self.inner.iter() {
let mut c = style(guess.chars().nth(i).unwrap().to_string()); let mut c = style(e.0.to_string());
if e.1 == Status::Matched { if e.1 == Status::Matched {
c = c.green(); c = c.green();
} else if e.1 == Status::Exists { } else if e.1 == Status::Exists {
@ -29,6 +29,45 @@ impl Evaluation {
} }
buf buf
} }
/// The first string is the word the evaluation is for, The second string defines how the
/// characters of the first string match the solution.
///
///
/// ## Evaluation Format:
///
/// 'x' means wrong character
///
/// 'p' means present character
///
/// 'c' means correct character
///
/// ### Example:
///
/// 'xxxcc' --- means the first 3 chars are wrong but the second 2 chars are correct
///
/// 'xppxc' --- means the first character is wrong, the next two characters are present, the last
/// is correct
///
/// ## Example
///
/// "wordle xxxcff" --- the guess was wordle, the d is in the correct spot, the solution
/// contains 'l' and 'e', but on another index.
///
pub fn build(guess: &Word, eval_str: &str) -> WResult<Self> {
if guess.len() != eval_str.len() {
return Err(GameError::GuessAndEvalNotSameLen((
guess.to_string(),
eval_str.to_string(),
))
.into());
}
let mut v: Vec<EvaluationUnit> = Vec::new();
for (c, e) in guess.chars().zip(eval_str.chars()) {
v.push((c, Status::from(e)))
}
Ok(v.into())
}
} }
impl IntoIterator for Evaluation { impl IntoIterator for Evaluation {
@ -46,19 +85,8 @@ impl From<Vec<EvaluationUnit>> for Evaluation {
} }
} }
impl From<&str> for Evaluation { impl From<Evaluation> for Word {
fn from(value: &str) -> Self { fn from(value: Evaluation) -> Self {
Self::from_str(value).unwrap() Word::from(value.inner.into_iter().map(|v| v.0).collect::<String>())
}
}
impl FromStr for Evaluation {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut v: Vec<EvaluationUnit> = Vec::new();
for c in s.chars() {
v.push((c, Status::from(c)))
}
Ok(v.into())
} }
} }

View File

@ -320,7 +320,7 @@ impl<'wl, WL: WordList> Display for Game<'wl, WL> {
for s in self for s in self
.responses() .responses()
.iter() .iter()
.map(|v| v.evaluation().to_owned().colorized_display(v.guess())) .map(|v| v.evaluation().to_owned().colorized_display())
{ {
write!(f, "\"")?; write!(f, "\"")?;
for si in s { for si in s {

View File

@ -59,6 +59,7 @@ pub trait Solver<'wl, WL: WordList>: Clone + std::fmt::Debug + Sized + Sync {
/// ///
/// This function will return an error if [make_a_move](Solver::make_a_move) fails. /// This function will return an error if [make_a_move](Solver::make_a_move) fails.
fn play(&self, game: &mut Game<'wl, WL>) -> WResult<GuessResponse> { fn play(&self, game: &mut Game<'wl, WL>) -> WResult<GuessResponse> {
// TODO: check if the game is finished already and return an Err if so
let mut resp: GuessResponse; let mut resp: GuessResponse;
loop { loop {
resp = self.make_a_move(game)?; resp = self.make_a_move(game)?;

View File

@ -16,6 +16,15 @@ 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
///
/// ## Algorithm
///
/// * Look at the evaluation for the last response and keep the correct letters
/// * Get all words that have these letters at the right position
/// * Discard words that have already been tried
/// * Discard all words that don't have the chars that we know from the last guess are in the
/// word, but don't know the position of.
fn guess_for(&self, game: &crate::game::Game<WL>) -> WResult<Word> { fn guess_for(&self, game: &crate::game::Game<WL>) -> WResult<Word> {
// HACK: hardcoded length // HACK: hardcoded length
let mut pattern: String = String::from("....."); let mut pattern: String = String::from(".....");

View File

@ -1,3 +1,6 @@
use test_log::test; // set the log level with an envvar: `RUST_LOG=trace cargo test`
use wordle_analyzer::game::evaluation::Evaluation;
use wordle_analyzer::game::Game; use wordle_analyzer::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;
@ -69,7 +72,7 @@ fn test_naive_play_predetermined_game() -> anyhow::Result<()> {
} }
#[test] #[test]
fn test_naive_play_predetermined_game_by_manual_guess_and_eval() -> anyhow::Result<()> { fn test_naive_play_predetermined_game_manually() -> anyhow::Result<()> {
let wl = wordlist(); let wl = wordlist();
let sl = let sl =
AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver")); AnyBuiltinSolver::Naive(NaiveSolver::build(&wl).expect("could not build naive solver"));
@ -77,35 +80,56 @@ fn test_naive_play_predetermined_game_by_manual_guess_and_eval() -> anyhow::Resu
// pretend that a user inputs guesses manually // pretend that a user inputs guesses manually
let mut game = Game::build(5, false, 6, &wl, false)?; let mut game = Game::build(5, false, 6, &wl, false)?;
let _actual_solution: Option<WordData> = Some(("nines".into(), 0.002)); let _actual_solution: Option<WordData> = Some(("nines".into(), 0.002));
let mut next_guess; let mut next_guess: Word;
next_guess = sl.guess_for(&game)?; next_guess = sl.guess_for(&game)?;
assert_eq!(next_guess, Word::from("which")); assert_eq!(next_guess, Word::from("which"));
game.guess(next_guess, Some("xxfxx".into()))?; game.guess(
next_guess.clone(),
Some(Evaluation::build(&next_guess, "xxfxx")?),
)?;
next_guess = sl.guess_for(&game)?; next_guess = sl.guess_for(&game)?;
assert_eq!(next_guess, Word::from("their")); assert_eq!(next_guess, Word::from("their"));
game.guess(next_guess, Some("xxffx".into()))?; game.guess(
next_guess.clone(),
Some(Evaluation::build(&next_guess, "xxffx")?),
)?;
next_guess = sl.guess_for(&game)?; next_guess = sl.guess_for(&game)?;
assert_eq!(next_guess, Word::from("being")); assert_eq!(next_guess, Word::from("being"));
game.guess(next_guess, Some("xfffx".into()))?; game.guess(
next_guess.clone(),
Some(Evaluation::build(&next_guess, "xfffx")?),
)?;
next_guess = sl.guess_for(&game)?; next_guess = sl.guess_for(&game)?;
assert_eq!(next_guess, Word::from("since")); assert_eq!(next_guess, Word::from("since"));
game.guess(next_guess, Some("fcfxf".into()))?; game.guess(
next_guess.clone(),
Some(Evaluation::build(&next_guess, "fcfxf")?),
)?;
next_guess = sl.guess_for(&game)?; next_guess = sl.guess_for(&game)?;
assert_eq!(next_guess, Word::from("lines")); assert_eq!(next_guess, Word::from("lines"));
game.guess(next_guess, Some("xcccc".into()))?; game.guess(
next_guess.clone(),
Some(Evaluation::build(&next_guess, "xcccc")?),
)?;
next_guess = sl.guess_for(&game)?; next_guess = sl.guess_for(&game)?;
assert_eq!(next_guess, Word::from("mines")); assert_eq!(next_guess, Word::from("mines"));
game.guess(next_guess, Some("xcccc".into()))?; game.guess(
next_guess.clone(),
Some(Evaluation::build(&next_guess, "xcccc")?),
)?;
next_guess = sl.guess_for(&game)?; next_guess = sl.guess_for(&game)?;
assert_eq!(next_guess, Word::from("wines")); assert_eq!(next_guess, Word::from("wines"));
game.guess(next_guess, Some("xcccc".into()))?; game.guess(
next_guess.clone(),
Some(Evaluation::build(&next_guess, "xcccc")?),
)?;
// naive is at the moment too bad to solve "nines" // naive is at the moment too bad to solve "nines"
assert!(game.finished()); assert!(game.finished());