workle
cargo devel CI / cargo CI (push) Successful in 44s Details

This commit is contained in:
Christoph J. Scherr 2024-03-22 00:01:40 +01:00
parent 8ee32759ee
commit 94d16b345f
Signed by: PlexSheep
GPG Key ID: 7CDD0B14851A08EF
6 changed files with 210 additions and 27 deletions

View File

@ -17,22 +17,25 @@ default = ["game", "bench", "tui", "solvers", "builtin_wlist", "serde"]
builtin_wlist = ["dep:serde_json", "serde"]
game = ["builtin_wlist"]
solvers = []
tui = ["game"]
tui = ["cli"]
cli = ["dep:clap"]
bench = []
serde = ["dep:serde"]
[dependencies]
anyhow = "1.0.81"
clap = { version = "4.5.3", features = ["derive"] }
clap = { version = "4.5.3", features = ["derive"], optional = true }
colored = { version = "2.1.0", optional = false }
libpt = "0.4.2"
rand = "0.8.5"
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 }
thiserror = "1.0.58"
[[bin]]
name = "wordlec"
path = "src/bin/game/cli.rs"
required-features = ["game"]
required-features = ["game", "cli"]
[[bin]]
name = "wordlet"

View File

@ -1,12 +1,20 @@
#![warn(clippy::all)]
#![warn(missing_docs)]
// #![warn(missing_docs)]
#![warn(missing_debug_implementations)]
use std::io::{Read, Write};
use anyhow::anyhow;
use clap::Parser;
use libpt::log::*;
use wordle_analyzer::error::GameError;
use wordle_analyzer::game::response::GuessResponse;
use wordle_analyzer::game::Game;
use wordle_analyzer::wlist::builtin::BuiltinWList;
use wordle_analyzer::wlist::word::Word;
use wordle_analyzer::{self, game};
use colored::Colorize;
#[derive(Parser, Clone, Debug)]
#[command(version, about, long_about, author)]
struct Cli {
@ -19,31 +27,74 @@ struct Cli {
/// how many times can we guess?
#[arg(short, long, default_value_t = wordle_analyzer::DEFAULT_MAX_STEPS)]
max_steps: usize,
/// more verbose logs
#[arg(short, long)]
verbose: bool,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
if cli.verbose {
Logger::build_mini(Some(Level::TRACE))?;
} else {
Logger::build_mini(Some(Level::INFO))?;
}
debug!("dumping CLI: {:#?}", cli);
let game: Game<BuiltinWList> = game::Game::builder()
let mut game = game::Game::<BuiltinWList>::builder()
.length(cli.length)
.precompute(cli.precompute)
.build()?;
debug!("game: {:#?}", game);
debug!("{game:#?}");
let mut response: GuessResponse;
let mut guess: Word;
loop {
guess = match get_word(&cli, game.step()) {
Ok(g) => g,
Err(err) => match err.downcast::<GameError>() {
Ok(game_err) => match game_err {
GameError::GuessHasWrongLength => {
println!("wring length: must be {} long", game.length());
continue;
}
_ => {
return Err(game_err.into());
}
},
Err(err) => return Err(anyhow!(err.to_string())),
},
};
response = game.guess(guess)?;
println!("{response}");
if response.finished() {
break;
}
}
if response.won() {
println!("You win! You took {} guesses.", game.step() - 1);
} else {
println!("You lose! The solution was {:?}.", game.solution());
}
Ok(())
}
fn get_word(cli: &Cli) -> String {
let mut word = String::new();
fn get_word(cli: &Cli, step: usize) -> anyhow::Result<Word> {
let mut word = Word::new();
let stdin = std::io::stdin();
let mut stdout = std::io::stdout();
// TODO: get user input
// TODO: validate user input
todo!();
print!("guess {step} > ");
stdout.flush()?;
stdin.read_line(&mut word)?;
word = word.replace('\n', "");
assert_eq!(word.len(), cli.length);
word
Ok(word)
}

26
src/error.rs Normal file
View File

@ -0,0 +1,26 @@
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
pub type GameResult<T> = std::result::Result<T, GameError>;
#[derive(Debug, Error)]
pub enum Error {
#[error("GameError")]
GameError {
#[from]
source: GameError,
},
#[error(transparent)]
Other {
#[from]
source: anyhow::Error,
},
}
#[derive(Debug, Clone, Error)]
pub enum GameError {
#[error("The guess has the wrong length")]
GuessHasWrongLength,
#[error("The game is finished but a guess is being made")]
TryingToPlayAFinishedGame,
}

View File

@ -1,9 +1,11 @@
use crate::wlist::word::{Frequency, Solution, Word};
use crate::error::*;
use crate::wlist::word::{Solution, Word};
use crate::wlist::WordList;
use self::response::GuessResponse;
pub mod response;
use response::GuessResponse;
use self::response::Status;
#[derive(Debug, Clone, PartialEq)]
pub struct Game<WL>
@ -16,6 +18,7 @@ where
step: usize,
solution: Solution,
wordlist: WL,
finished: bool,
}
impl<WL: WordList> Game<WL> {
@ -40,15 +43,16 @@ impl<WL: WordList> Game<WL> {
precompute: bool,
max_steps: usize,
wlist: WL,
) -> anyhow::Result<Self> {
) -> GameResult<Self> {
let solution = wlist.rand_solution();
let mut game = Game {
let game = Game {
length,
precompute,
max_steps,
step: 0,
step: 1,
solution,
wordlist: wlist,
finished: false,
};
Ok(game)
@ -56,12 +60,50 @@ impl<WL: WordList> Game<WL> {
pub fn reset(mut self) -> Self {
self.solution = self.wordlist.rand_solution();
self.step = 0;
self.step = 1;
self.finished = false;
self
}
pub fn guess(&mut self, word: Word) -> anyhow::Result<GuessResponse> {
todo!()
pub fn guess(&mut self, guess: Word) -> GameResult<GuessResponse> {
if guess.len() != self.length {
return Err(GameError::GuessHasWrongLength);
}
if self.finished || self.step > self.max_steps {
return Err(GameError::TryingToPlayAFinishedGame);
}
self.step += 1;
let mut compare_solution = self.solution.0.clone();
let mut evaluation = Vec::new();
let mut status: Status;
for (idx, c) in guess.chars().enumerate() {
if compare_solution.chars().nth(idx) == Some(c) {
status = Status::Matched;
compare_solution = compare_solution.replace(c, "_");
} else if compare_solution.contains(c) {
status = Status::Exists;
compare_solution = compare_solution.replacen(c, "_", 1);
} else {
status = Status::None
}
evaluation.push((c, status));
}
let mut response = GuessResponse::new(guess, evaluation, self.step, self.max_steps);
Ok(response)
}
pub fn length(&self) -> usize {
self.length
}
pub fn solution(&self) -> &Solution {
&self.solution
}
pub fn step(&self) -> usize {
self.step
}
}
@ -107,7 +149,7 @@ pub struct GameBuilder<WL: WordList> {
impl<WL: WordList> GameBuilder<WL> {
/// build a [`Game`] with the stored configuration
pub fn build(self) -> anyhow::Result<Game<WL>> {
pub fn build(self) -> GameResult<Game<WL>> {
let game: Game<WL> =
Game::build(self.length, self.precompute, self.max_steps, WL::default())?;
Ok(game)

View File

@ -1,13 +1,73 @@
use crate::wlist::word::Word;
use anyhow::Ok;
use colored::{ColoredString, Colorize};
use std::fmt::Display;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GuessResponse {
guess: Word,
evaluation: Vec<(char, Status)>,
step: usize,
finish: bool,
win: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Status {
None = 0,
Exists = 1,
Matched = 2,
}
impl GuessResponse {
pub(crate) fn new(
guess: Word,
status: Vec<(char, Status)>,
step: usize,
max_step: usize,
) -> Self {
let mut win = false;
let mut finish: bool = if step >= max_step {
true
} else {
let mut matched = true;
for p in &status {
matched &= p.1 == Status::Matched;
}
win = matched;
win
};
Self {
guess,
evaluation: status,
step,
finish,
win
}
}
pub fn finished(&self) -> bool {
self.finish
}
pub fn won(&self) -> bool {
self.win
}
}
pub enum Status {
None,
Exists,
Matched,
impl Display for GuessResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for s in &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

@ -7,6 +7,7 @@ pub const DEFAULT_WORD_LENGTH: usize = 5;
/// Default amount of guesses per game
pub const DEFAULT_MAX_STEPS: usize = 6;
pub mod error;
#[cfg(feature = "bench")]
pub mod bench;
#[cfg(feature = "game")]