use std::{
fmt::{Debug, Display},
ops::Deref,
};
use itertools::Itertools;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{
words::GUESSES,
{PuzzleError, Result},
};
pub mod stupid;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_crate")
)]
pub struct Word {
pub(crate) index: usize,
}
impl Word {
pub fn from_index(index: usize) -> Result<Self> {
if index < GUESSES.len() {
Ok(Word { index })
} else {
Err(PuzzleError::InvalidIndex(index).into())
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(word: &str) -> Result<Self> {
GUESSES
.binary_search(&word)
.map(|index| Word { index })
.map_err(|_| PuzzleError::NotInWordlist(word.to_string()).into())
}
}
impl Deref for Word {
type Target = str;
fn deref(&self) -> &Self::Target {
crate::words::GUESSES[self.index]
}
}
impl Display for Word {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.deref())
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Puzzle {
word: Word,
pub(crate) poisoned: bool,
}
impl Puzzle {
pub fn new(word: Word) -> Self {
Puzzle {
word,
poisoned: false,
}
}
pub fn check(&mut self, guess: &Word, attempts: &mut Attempts) -> Result<([Grade; 5], bool)> {
if attempts.cheat {
self.poisoned = true;
}
if attempts.hard {
for previous in attempts.inner().iter().rev() {
let (previous_grades, _) = self.check_inner(previous);
self.hardmode_guard(previous, &previous_grades, guess)?;
}
}
if attempts.push(*guess).is_err() {
return Err(PuzzleError::OutOfGuesses.into());
}
Ok(self.check_inner(guess))
}
fn check_inner(&self, guess: &Word) -> ([Grade; 5], bool) {
use std::cmp::Ordering;
let mut used = String::new();
let mut res = [Grade::Incorrect; 5];
let mut correct = true;
for (i, (guess, answer)) in guess
.chars()
.zip(self.word.chars())
.enumerate()
.sorted_unstable_by(|&(a_i, (a_guess, a_answer)), &(b_i, (b_guess, b_answer))| {
let a_correct = a_guess == a_answer;
let b_correct = b_guess == b_answer;
match a_correct.cmp(&b_correct).reverse() {
Ordering::Equal => a_i.cmp(&b_i),
other => other,
}
})
{
if guess == answer {
used.push(guess);
res[i] = Grade::Correct;
} else {
match self.word.chars().filter(|&c| c == guess).count() {
0 => correct = false,
n if n >= 1 && used.chars().filter(|&c| c == guess).count() < n => {
used.push(guess);
res[i] = Grade::Almost;
correct = false;
}
_ => correct = false,
}
}
}
(res, correct)
}
fn hardmode_guard(&self, previous: &Word, grades: &[Grade], guess: &Word) -> Result<()> {
let mut almost_lookup = [0_u8; 26];
const A_ASCII: usize = 0x61;
let i = |c: char| c as usize - A_ASCII;
for (prev, grade, new) in previous
.chars()
.zip(grades.iter())
.zip(guess.chars())
.map(|c| (c.0 .0, c.0 .1, c.1))
.sorted_unstable_by_key(|c| c.1)
{
match grade {
Grade::Correct => {
if prev != new {
return Err(PuzzleError::InvalidHardmodeGuess.into());
}
}
Grade::Incorrect => {}
Grade::Almost => {
almost_lookup[i(prev)] += 1;
if guess.chars().filter(|&c| c == prev).count()
< almost_lookup[i(prev)] as usize
{
return Err(PuzzleError::InvalidHardmodeGuess.into());
}
}
}
}
Ok(())
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub enum Grade {
Correct,
Almost,
Incorrect,
}
pub struct AttemptsKey {
hard: bool,
cheat: bool,
}
impl AttemptsKey {
pub(crate) fn new(hard: bool) -> AttemptsKey {
AttemptsKey { hard, cheat: false }
}
pub fn new_cheat(hard: bool) -> AttemptsKey {
AttemptsKey { hard, cheat: true }
}
pub fn unlock(self) -> Attempts {
Attempts::new(self.hard, self.cheat)
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct Attempts {
inner: Vec<Word>,
pub(crate) hard: bool,
pub(crate) cheat: bool,
}
impl Attempts {
pub(crate) fn new(hard: bool, cheat: bool) -> Self {
Attempts {
hard,
cheat,
..Self::default()
}
}
pub fn cheat(hard: bool) -> Self {
Attempts {
hard,
cheat: true,
..Self::default()
}
}
pub(crate) fn push(&mut self, word: Word) -> Result<usize> {
if self.inner.len() < 6 {
self.inner.push(word);
Ok(self.inner.len() - 1)
} else {
Err(PuzzleError::OutOfGuesses.into())
}
}
pub fn inner(&self) -> &[Word] {
self.inner.as_slice()
}
pub fn finished(&self) -> bool {
self.inner.len() >= 6
}
pub(crate) fn solved(&self, word: &Word) -> bool {
matches!(self.inner().last(), Some(s) if s == word)
}
}
impl Display for Attempts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some((last, rest)) = self.inner.split_last() {
for word in rest {
writeln!(f, "{}", word)?;
}
write!(f, "{}", last)?;
}
Ok(())
}
}
pub trait Strategy: Display + Debug + Sync {
fn solve(&self, puzzle: &mut Puzzle, key: AttemptsKey) -> Attempts;
fn version(&self) -> &'static str;
fn hardmode(&self) -> bool;
}
#[cfg(test)]
mod test {
use super::*;
use crate::{words::GUESSES, WordleError};
#[test]
fn word_from_index() -> Result<()> {
Word::from_index(GUESSES.len() - 1)?;
Word::from_index(5574)?;
Word::from_index(GUESSES.len()).unwrap_err();
Ok(())
}
#[test]
fn word_from_str() -> Result<()> {
assert_eq!("tithe", Word::from_str("tithe")?.deref());
Word::from_str("doubt")?;
for word in &GUESSES[4452..4460] {
assert_eq!(*word, Word::from_str(word)?.deref());
}
Word::from_str("aaaaa").unwrap_err();
Word::from_str("tithes").unwrap_err();
Ok(())
}
#[test]
fn fmt_word() {
assert_eq!("tithe", format!("{}", Word::from_str("tithe").unwrap()));
for word in &GUESSES[3141..3149] {
assert_eq!(*word, format!("{}", Word::from_str(word).unwrap().deref()));
}
}
#[test]
fn puzzle_poisoning() -> Result<()> {
let mut puzzle = Puzzle::new(Word::from_str("nerds")?);
assert!(!puzzle.poisoned);
let safe_key = AttemptsKey::new(false);
let mut safe_attempts = safe_key.unlock();
let (safe_grades, safe_correct) =
puzzle.check(&Word::from_str("doubt")?, &mut safe_attempts)?;
assert!(!puzzle.poisoned);
let cheat_key = AttemptsKey::new_cheat(false);
let mut cheat_attempts = cheat_key.unlock();
let (cheat_grades, cheat_correct) =
puzzle.check(&Word::from_str("doubt")?, &mut cheat_attempts)?;
assert!(puzzle.poisoned);
assert_eq!(safe_grades, cheat_grades);
assert_eq!(safe_correct, cheat_correct);
let _ = puzzle.check(&Word::from_str("gorge")?, &mut safe_attempts)?;
assert!(puzzle.poisoned);
Ok(())
}
#[test]
fn puzzle_out_of_guesses() -> Result<()> {
let mut puzzle = Puzzle::new(Word::from_str("nerds")?);
let key = AttemptsKey::new(false);
let mut attempts = key.unlock();
for &word in &GUESSES[100..106] {
let _ = puzzle.check(&Word::from_str(word)?, &mut attempts)?;
}
assert!(attempts.finished());
assert!(puzzle
.check(&Word::from_str("mount")?, &mut attempts)
.is_err());
assert_eq!(
format!(
"{}\n{}\n{}\n{}\n{}\n{}",
GUESSES[100], GUESSES[101], GUESSES[102], GUESSES[103], GUESSES[104], GUESSES[105]
),
format!("{}", attempts)
);
Ok(())
}
fn str_to_grades(input: &str) -> [Grade; 5] {
let mut res = [Grade::Incorrect; 5];
for (i, c) in input.chars().enumerate() {
match c {
'c' => res[i] = Grade::Correct,
'a' => res[i] = Grade::Almost,
_ => {}
}
}
res
}
macro_rules! puzzle_test {
(I $answer:expr; $puzzle:ident, $attempts:ident, $count:ident; $guess:expr, $works:expr, $res:expr) => {{
if $works {
let (grades, correct) = $puzzle
.check(&Word::from_str($guess)?, &mut $attempts)
.unwrap();
$count += 1;
assert_eq!($attempts.inner().len(), $count);
assert_eq!(correct, $answer == $guess);
assert_eq!(grades, str_to_grades($res));
} else {
assert!($puzzle
.check(&Word::from_str($guess)?, &mut $attempts)
.is_err());
}
}};
($fn_name:ident[hard = $hard:expr, $answer:expr => $( [$guess:expr, $works:expr, $res:expr] );*]) => {
puzzle_test! { $fn_name [hard = $hard, $answer => $( [$guess, $works, $res] );*] {} }
};
($fn_name:ident[hard = $hard:expr, $answer:expr => $( [$guess:expr, $works:expr, $res:expr] );*] $other:block) => {
#[test]
fn $fn_name() -> Result<()> {
let mut attempts = Attempts::cheat($hard);
let mut puzzle = Puzzle::new(Word::from_str($answer)?);
let mut count = 0;
$(puzzle_test!(I $answer; puzzle, attempts, count; $guess, $works, $res);)*
$other
Ok(())
}
};
}
puzzle_test! { repeat_letter_guesses [hard = true, "sober" =>
["spool", true, "ciaii"];
["soaks", true, "cciii"]]
}
puzzle_test! { repeat_letter_guesses_before [hard = true, "tills" =>
["pines", true, "iciic"];
["sills", true, "icccc"]]
}
puzzle_test! { repeat_letter_answer [hard = true, "spoon" =>
["odors", true, "aicia"]]
}
puzzle_test! { wordle_crimp_props_primp [hard = true, "crimp" =>
["props", true, "aciii"];
["pinup", false, ""];
["primp", true, "icccc"];
["crimp", true, "ccccc"]]
}
puzzle_test! { wordle_crimp_error_order_trier [hard = true, "crimp" =>
["error", true, "iciii"];
["order", true, "iciii"];
["right", false, ""];
["trier", true, "iccii"];
["crimp", true, "ccccc"]]
}
puzzle_test! { wordle_crimp_lints_limit_minis [hard = true, "crimp" =>
["lints", true, "iaiii"];
["limit", true, "iaaii"];
["lipid", false, ""];
["minis", true, "aaiii"];
["crimp", true, "ccccc"]]
}
puzzle_test! { wordle_crimp_bolts_prick [hard = true, "crimp" =>
["bolts", true, "iiiii"];
["prick", true, "accai"];
["crimp", true, "ccccc"]]
}
puzzle_test! { filling_up [hard = true, "right" =>
["allay", true, "iiiii"];
["tough", true, "aiiaa"];
["spits", false, ""];
["might", true, "icccc"];
["night", true, "icccc"];
["fight", true, "icccc"];
["sight", true, "icccc"]]
}
puzzle_test! { repeat_hardmode_almost[hard = true, "spill" =>
["alloy", true, "iaaii"];
["limes", false, ""];
["spilt", false, ""];
["level", true, "aiiic"];
["petal", false, ""];
["spill", true, "ccccc"]]
}
puzzle_test! { more_are_allowed[hard = true, "earth" =>
["alloy", true, "aiiii"];
["drama", true, "iaaii"]]
}
puzzle_test! { hardmode_correct[hard = true, "tills" =>
["pines", true, "iciic"];
["butts", false, ""];
["right", false, ""];
["earth", false, ""];
["mills", true, "icccc"];
["tight", false, ""];
["tails", false, ""];
["sills", true, "icccc"];
["tills", true, "ccccc"]]
}
puzzle_test! { hardmode_almost[hard = true, "spots" =>
["crass", true, "iiiac"];
["wisps", true, "iiaac"];
["slots", false, ""];
["spots", true, "ccccc"]]
}
}