#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![warn(
bad_style,
const_err,
dead_code,
improper_ctypes,
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
no_mangle_generic_items,
non_shorthand_field_patterns,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
rust_2018_idioms,
trivial_casts,
trivial_numeric_casts,
unconditional_recursion,
unsafe_code,
unused_allocation,
unused_comparisons,
unused_crate_dependencies,
unused_extern_crates,
unused_import_braces,
unused_parens,
unused_qualifications,
unused_results,
unused,
while_true
)]
use fst::Automaton;
use std::num::NonZeroUsize;
pub mod prepare;
mod types;
pub use types::{Constraint, Letter, LetterList, LetterSet};
#[allow(missing_copy_implementations)]
#[derive(Clone, Debug)]
pub struct Wordle<const N: usize> {
never: LetterSet,
eventually: LetterList<N>,
positions: [Constraint; N],
}
impl<const N: usize> Wordle<N> {
#[must_use]
pub const fn new() -> Self {
Self {
never: LetterSet::new(),
eventually: LetterList::new(),
positions: [Constraint::new(); N],
}
}
#[must_use]
pub fn is_solved(&self) -> bool {
self.positions.iter().all(|p| !p.is_free())
}
#[must_use]
pub const fn has_solution_at(&self, pos: usize) -> bool {
!self.positions[pos].is_free()
}
#[must_use]
pub const fn solution_at(&self, pos: usize) -> Option<u8> {
let cons = self.positions[pos];
if cons.is_free() {
None
} else {
Some(cons.must_letter().into_byte())
}
}
pub fn decode(&self) -> impl Iterator<Item = u8> + '_ {
self.positions.iter().map(|cons| {
assert!(!cons.is_free(), "Trying to decode an unsolved wordle");
u8::from(cons.must_letter())
})
}
#[must_use]
pub fn decode_str(&self) -> String {
self.decode().map(char::from).collect()
}
#[must_use]
pub const fn decode_guess(&self, pos: usize, guess: u8) -> Guess {
let letter = Letter::new(guess);
if self.never.contains(letter) {
Guess::Absent
} else if self.positions[pos].accept(letter) {
Guess::Correct
} else {
Guess::Present
}
}
pub const fn try_decode_guess(&self, pos: usize, guess: u8) -> Result<Guess, GuessError> {
let guess = self.decode_guess(pos, guess);
match guess {
Guess::Correct if self.positions[pos].is_free() => Err(GuessError::NeverSeen),
Guess::Present if !self.positions[pos].is_free() => Err(GuessError::NeverSeen),
guess => Ok(guess),
}
}
}
#[derive(Clone, Debug)]
pub struct WordleBuilder<const N: usize>(Wordle<N>, LetterList<N>);
impl<const N: usize> WordleBuilder<N> {
#[must_use]
pub const fn new() -> Self {
Self(Wordle::new(), LetterList::new())
}
#[must_use]
pub fn from(mut wordle: Wordle<N>) -> Self {
let old_eventually = std::mem::replace(&mut wordle.eventually, LetterList::new());
Self(wordle, old_eventually)
}
pub fn never(&mut self, letter: u8) -> &mut Self {
self.0.never = self.0.never.add(Letter::new(letter));
self
}
pub fn never_all(&mut self, letters: impl AsRef<[u8]>) -> &mut Self {
self.0.never = letters
.as_ref()
.iter()
.copied()
.map(Letter::new)
.fold(self.0.never, LetterSet::add);
self
}
pub fn correct_pos(&mut self, pos: usize, letter: u8) -> &mut Self {
self.0.positions[pos] = self.0.positions[pos].must(Letter::new(letter));
self
}
pub fn wrong_pos(&mut self, pos: usize, letter: u8) -> &mut Self {
let letter = Letter::new(letter);
self.0.eventually.add(letter);
self.0.positions[pos] = self.0.positions[pos].must_not(letter);
self
}
pub fn eventually(&mut self, letter: u8) -> &mut Self {
let letter = Letter::new(letter);
self.0.never = self.0.never.remove(letter);
self.0.eventually.add(letter);
self
}
pub fn build(&mut self) -> Wordle<N> {
let mut wordle = std::mem::replace(&mut self.0, Wordle::new());
let mut never = wordle
.eventually
.iter()
.fold(wordle.never, |n, l| n.remove(*l));
let global_never = never;
let mut solved = LetterSet::new();
for cons in wordle.positions.iter_mut() {
if cons.is_free() {
never = never.remove_all(cons.must_not_letters());
*cons = cons.must_not_all(global_never);
} else {
never = never.remove(cons.must_letter());
solved = solved.add(cons.must_letter());
};
}
for &letter in self.1.iter() {
if never.contains(letter) {
never = LetterSet::full();
}
if !solved.contains(letter) {
wordle.eventually.add_if_absent(letter);
}
}
wordle.never = never;
wordle
}
#[must_use]
pub const fn current(&self) -> &Wordle<N> {
&self.0
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Guess {
Absent,
Present,
Correct,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum GuessError {
NeverSeen,
}
#[derive(Clone, Debug)]
#[doc(hidden)]
pub struct SolveState<const N: usize> {
pos: NonZeroUsize,
eventually: LetterList<N>,
}
const _: () = assert!(
std::mem::size_of::<Option<SolveState<5>>>() == std::mem::size_of::<SolveState<5>>(),
"SolveState should have a niche"
);
impl<const N: usize> Automaton for Wordle<N> {
type State = Option<SolveState<N>>;
fn start(&self) -> Self::State {
Some(SolveState {
pos: NonZeroUsize::new(1).unwrap(),
eventually: self.eventually.clone(),
})
}
fn is_match(&self, state: &Self::State) -> bool {
state.as_ref().map_or(false, |s| s.pos.get() - 1 == N)
}
fn accept(&self, state: &Self::State, byte: u8) -> Self::State {
let state = state.as_ref()?;
let pos = state.pos.get() - 1;
if pos == N {
return None;
}
let letter = Letter::try_new(byte)?;
if self.never.contains(letter) {
return None;
}
let constraint = self.positions[pos];
if !constraint.accept(letter) {
return None;
}
let mut state = state.clone();
let removed = state.eventually.remove(letter);
if !removed {
let need_to_fill = state.eventually.len();
let available = self.positions[pos + 1..]
.iter()
.map(|p| usize::from(p.is_free()))
.sum::<usize>();
if available < need_to_fill {
return None;
}
}
state.pos = NonZeroUsize::new(pos + 2).expect("Adding to non-zero value is non-zero");
Some(state)
}
fn can_match(&self, state: &Self::State) -> bool {
!self.never.is_full() && state.as_ref().map_or(false, |s| s.pos.get() <= N)
}
}
#[cfg(test)]
mod tests {
use super::*;
use fst::{IntoStreamer, Set};
#[test]
fn test_no_conditions() {
let wordle = WordleBuilder::new().build();
test_fst(wordle, ["abcd", "abcde", "abcdef"], &["abcde"]);
}
#[test]
fn test_never_accept_a() {
let wordle = WordleBuilder::new().never(b'a').build();
test_fst(wordle, ["abcde", "bcdef", "cdefa"], &["bcdef"]);
}
#[test]
fn test_never_accept_abcd() {
let wordle = WordleBuilder::new().never_all("abcd").build();
test_fst(wordle, ["abcde", "cdefg", "efghi"], &["efghi"]);
}
#[test]
fn test_require_a_in_third_pos() {
let wordle = WordleBuilder::new().correct_pos(2, b'a').build();
test_fst(
wordle,
["abcde", "bacde", "bcade", "bcdae", "bcdea"],
&["bcade"],
);
}
#[test]
fn test_dont_allow_a_in_third_pos() {
let wordle = WordleBuilder::new().wrong_pos(2, b'a').build();
test_fst(
wordle,
["abcde", "bacde", "bcade", "bcdae", "bcdea"],
&["abcde", "bacde", "bcdae", "bcdea"],
);
}
#[test]
fn test_use_partially_solved_letter() {
let wordle = WordleBuilder::new()
.correct_pos(0, b'a')
.correct_pos(1, b'b')
.correct_pos(2, b'c')
.wrong_pos(3, b'd')
.build();
test_fst(wordle, ["abcde", "abced", "abcef"], &["abced"]);
}
#[test]
fn test_guess_with_repeated_letters_as_yellow() {
let wordle = WordleBuilder::new()
.correct_pos(0, b'a')
.correct_pos(1, b'b')
.correct_pos(2, b'c')
.wrong_pos(3, b'e')
.correct_pos(4, b'e')
.build();
test_fst(wordle, ["abcde", "abcee"], &[]);
let wordle = WordleBuilder::new()
.correct_pos(0, b'a')
.correct_pos(1, b'b')
.correct_pos(2, b'c')
.wrong_pos(3, b'e')
.build();
test_fst(wordle, ["abcde", "abcee"], &["abcde"]);
}
#[test]
fn test_guess_with_repeated_letters_as_gray() {
let wordle = WordleBuilder::new()
.correct_pos(0, b'a')
.correct_pos(1, b'b')
.correct_pos(2, b'c')
.never(b'e')
.correct_pos(4, b'e')
.build();
test_fst(wordle, ["abcde", "abcee"], &["abcde"]);
}
#[test]
fn test_guess_with_repeated_correct_letters_in_wrong_pos() {
let wordle = WordleBuilder::new()
.correct_pos(0, b'a')
.wrong_pos(1, b'c')
.wrong_pos(2, b'e')
.wrong_pos(3, b'b')
.never(b'b')
.build();
test_fst(wordle, ["abcde", "acebb"], &["abcde"]);
}
fn test_fst<'a>(
wordle: Wordle<5>,
words: impl IntoIterator<Item = &'a str>,
expected: &[&'a str],
) {
fn inner_test<'a>(
wordle: Wordle<5>,
words: impl IntoIterator<Item = &'a str>,
expected: &[&'a str],
) -> fst::Result<()> {
let set = Set::from_iter(words)?;
let stream = set.search(wordle).into_stream();
let matches = stream.into_strs()?;
assert_eq!(&matches[..], expected);
Ok(())
}
inner_test(wordle, words, expected).unwrap();
}
}