add interactive solver #4

Merged
cscherrNT merged 29 commits from feat/interactive-solver into devel 2024-07-25 15:19:54 +02:00
4 changed files with 59 additions and 10 deletions
Showing only changes of commit 713a661cc5 - Show all commits

View File

@ -8,12 +8,13 @@ use libpt::cli::{repl::Repl, strum};
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::solve::{BuiltinSolverNames, Solver};
use wordle_analyzer::wlist::builtin::BuiltinWList;
use wordle_analyzer::wlist::word::Word;
use wordle_analyzer::wlist::word::{Word, WordData};
use wordle_analyzer::wlist::WordList;
use wordle_analyzer::{self, game};
@ -33,12 +34,22 @@ struct Cli {
#[command(flatten)]
verbose: libpt::cli::args::VerbosityLevel,
/// which solver to use
#[arg(short, long, default_value_t = BuiltinSolverNames::default())]
#[arg(long, default_value_t = BuiltinSolverNames::default())]
solver: BuiltinSolverNames,
/// set if the solver should play a full native game without interaction
#[arg(short, long)]
non_interactive: bool,
// FIXME: line breaks don't work correctly in the cli help
//
/// Solution for the game
///
/// This will only be used when non-interactive is used. You can use this option to see how the
/// selected solver behaves when trying to guess a specific solution, which can help reproduce
/// behavior.
#[arg(short, long)]
solution: Option<Word>,
}
#[derive(Subcommand, Debug, EnumIter, Clone)]
@ -184,10 +195,22 @@ fn wlcommand_handler(_cli: &Cli, cmd: &WlCommand, wl: &impl WordList) -> anyhow:
fn play_native_non_interactive(cli: Cli) -> anyhow::Result<()> {
let wl = BuiltinWList::default();
let builder = game::Game::builder(&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()?;

View File

@ -49,7 +49,7 @@ pub enum GameError {
GuessHasWrongLength(usize),
#[error("The game is finished but a guess is being made")]
TryingToPlayAFinishedGame,
#[error("Tried to guess a word that is not in the wordlist ({0})")]
#[error("Tried to guess or use a word that is not in the wordlist ({0})")]
WordNotInWordlist(Word),
}

View File

@ -214,13 +214,14 @@ impl<'wl, WL: WordList> Game<'wl, WL> {
/// # }
/// ```
///
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq)]
pub struct GameBuilder<'wl, WL: WordList> {
length: usize,
precompute: bool,
max_steps: usize,
wordlist: &'wl WL,
generate_solution: bool,
solution: Option<WordData>,
}
impl<'wl, WL: WordList> GameBuilder<'wl, WL> {
@ -234,19 +235,23 @@ impl<'wl, WL: WordList> GameBuilder<'wl, WL> {
max_steps: super::DEFAULT_MAX_STEPS,
wordlist: wl,
generate_solution,
solution: None,
}
}
/// build a [`Game`] with the stored configuration
pub fn build(&'wl self) -> GameResult<Game<'wl, WL>> {
trace!("{:#?}", self);
let game: Game<WL> = Game::build(
let mut game: Game<WL> = Game::build(
self.length,
self.precompute,
self.max_steps,
self.wordlist,
self.generate_solution,
)?;
if self.solution.is_some() {
game.set_solution(self.solution.clone())
}
Ok(game)
}
@ -284,6 +289,22 @@ impl<'wl, WL: WordList> GameBuilder<'wl, WL> {
self.wordlist = wl;
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> {

View File

@ -21,6 +21,10 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
let mut pattern: String = String::from(".....");
let mut other_chars: Vec<char> = Vec::new();
let response = game.last_response();
trace!(
"guessing best guess for last response: {response:#?}\n{:#?}",
response.map(|a| a.evaluation())
);
if response.is_some() {
for (idx, p) in response
.unwrap()
@ -37,10 +41,11 @@ impl<'wl, WL: WordList> Solver<'wl, WL> for NaiveSolver<'wl, WL> {
}
}
trace!("other chars: {:?}", other_chars);
let matches: Vec<WordData> = game
.wordlist()
.get_words_matching(pattern)
.expect("the solution does not exist in the wordlist")
let mut matches: Vec<WordData> = game.wordlist().get_words_matching(pattern)?;
if matches.is_empty() {
return Err(SolverError::NoMatches.into());
}
matches = matches
.iter()
// only words that have not been guessed yet
.filter(|p| !game.made_guesses().contains(&&p.0))