rs_wordle_solver/
results.rs

1use std::error::Error;
2use std::fmt;
3
4/// A compressed form of [LetterResult]s. Can only store vectors of up to 10 results.
5#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
6pub struct CompressedGuessResult {
7    data: u32,
8}
9
10const NUM_BITS_PER_LETTER_RESULT: usize = 2;
11pub const MAX_LETTERS_IN_COMPRESSED_GUESS_RESULT: usize =
12    std::mem::size_of::<u32>() * 8 / NUM_BITS_PER_LETTER_RESULT;
13
14impl CompressedGuessResult {
15    /// Creates a compressed form of the given letter results.
16    ///
17    /// Returns a [`WordleError::WordLength`] error if `letter_results` has more than
18    /// [`MAX_LETTERS_IN_COMPRESSED_GUESS_RESULT`] values.
19    pub fn from_results(
20        letter_results: &[LetterResult],
21    ) -> std::result::Result<CompressedGuessResult, WordleError> {
22        if letter_results.len() > MAX_LETTERS_IN_COMPRESSED_GUESS_RESULT {
23            return Err(WordleError::WordLength(
24                MAX_LETTERS_IN_COMPRESSED_GUESS_RESULT,
25            ));
26        }
27        let mut data = 0;
28        let mut index = 0;
29        for letter in letter_results {
30            data |= (*letter as u32) << index;
31            index += NUM_BITS_PER_LETTER_RESULT;
32        }
33        Ok(Self { data })
34    }
35}
36
37/// The result of a given letter at a specific location. There is some complexity here when a
38/// letter appears in a word more than once. See [`GuessResult`] for more details.
39#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
40pub enum LetterResult {
41    /// This letter goes exactly here in the objective word.
42    Correct = 0b01,
43    /// This letter is in the objective word, but not here.
44    PresentNotHere = 0b10,
45    /// This letter is not in the objective word, or is only in the word as many times as it was
46    /// marked either `PresentNotHere` or `Correct`.
47    NotPresent = 0b11,
48}
49
50/// Indicates that an error occurred while trying to guess the objective word.
51#[derive(Debug)]
52pub enum WordleError {
53    /// Indicates that the word lengths differed, or words were too long for the chosen
54    /// implementation. The expected word length or max possible word length is provided.
55    WordLength(usize),
56    /// Indicates that the given `GuessResult`s are impossible due to some inconsistency.
57    InvalidResults,
58    /// An IO error occurred.
59    IoError(std::io::Error),
60}
61
62impl fmt::Display for WordleError {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            WordleError::WordLength(expected_length) => write!(f, "{:?}: all words and guesses in a Wordle game must have the same length, and must be less than or equal to the max word length: {}", self, expected_length),
66            WordleError::InvalidResults => write!(f, "{:?}: provided GuessResults led to an impossible set of WordRestrictions", self),
67            WordleError::IoError(io_err) => write!(f, "{:?}: {}", self, io_err),
68        }
69    }
70}
71
72impl Error for WordleError {
73    fn source(&self) -> Option<&(dyn Error + 'static)> {
74        match self {
75            WordleError::IoError(io_err) => io_err.source(),
76            _ => None,
77        }
78    }
79}
80
81impl From<std::io::Error> for WordleError {
82    fn from(io_err: std::io::Error) -> Self {
83        WordleError::IoError(io_err)
84    }
85}
86
87/// The result of a single word guess.
88///
89/// There is some complexity here when the guess has duplicate letters. Duplicate letters are
90/// matched to [`LetterResult`]s as follows:
91///
92/// 1. All letters in the correct location are marked `Correct`.
93/// 2. For any remaining letters, if the objective word has more letters than were marked correct,
94///    then these letters are marked as `PresentNotHere` starting from the beginning of the word,
95///    until all letters have been accounted for.
96/// 3. Any remaining letters are marked as `NotPresent`.
97///
98/// For example, if the guess was "sassy" for the objective word "mesas", then the results would
99/// be: `[PresentNotHere, PresentNotHere, Correct, NotPresent, NotPresent]`.
100#[derive(Clone, Debug, PartialEq, Hash)]
101pub struct GuessResult<'a> {
102    /// The guess that was made.
103    pub guess: &'a str,
104    /// The result of each letter, provided in the same leter order as in the guess.
105    pub results: Vec<LetterResult>,
106}
107
108/// Data about a single turn of a Wordle game.
109#[derive(Clone, Debug, PartialEq, Hash)]
110pub struct TurnData {
111    /// The guess that was made this turn.
112    pub guess: Box<str>,
113    /// The number of possible words that remained at the start of this turn.
114    pub num_possible_words_before_guess: usize,
115}
116
117/// The data from a game that was played.
118#[derive(Clone, Debug, PartialEq)]
119pub struct GameData {
120    /// Data for each turn that was played.
121    pub turns: Vec<TurnData>,
122}
123
124/// Whether the game was won or lost by the guesser.
125#[derive(Clone, Debug, PartialEq)]
126pub enum GameResult {
127    /// Indicates that the guesser won the game, and provides the guesses that were given.
128    Success(GameData),
129    /// Indicates that the guesser failed to guess the word under the guess limit, and provides the
130    /// guesses that were given.
131    Failure(GameData),
132    /// Indicates that the given word was not in the guesser's word bank.
133    UnknownWord,
134}
135
136/// Determines the result of the given `guess` when applied to the given `objective`.
137///
138/// ```
139/// use rs_wordle_solver::get_result_for_guess;
140/// use rs_wordle_solver::GuessResult;
141/// use rs_wordle_solver::LetterResult;
142///
143/// let result = get_result_for_guess("mesas", "sassy");
144/// assert!(
145///     matches!(
146///         result,
147///         Ok(GuessResult {
148///             guess: "sassy",
149///             results: _
150///         })
151///     )
152/// );
153/// assert_eq!(
154///     result.unwrap().results,
155///     vec![
156///         LetterResult::PresentNotHere,
157///         LetterResult::PresentNotHere,
158///         LetterResult::Correct,
159///         LetterResult::NotPresent,
160///         LetterResult::NotPresent
161///     ]
162/// );
163/// ```
164pub fn get_result_for_guess<'a>(
165    objective: &str,
166    guess: &'a str,
167) -> Result<GuessResult<'a>, WordleError> {
168    if objective.len() != guess.len() {
169        return Err(WordleError::WordLength(objective.len()));
170    }
171    let mut results = Vec::with_capacity(guess.len());
172    results.resize(guess.len(), LetterResult::NotPresent);
173    for (objective_index, objective_letter) in objective.char_indices() {
174        let mut set_index = None;
175        for (guess_index, guess_letter) in guess.char_indices() {
176            // Break if we're done and there is no chance of being correct.
177            if set_index.is_some() && guess_index > objective_index {
178                break;
179            }
180            // Continue if this letter doesn't match.
181            if guess_letter != objective_letter {
182                continue;
183            }
184            let existing_result = results[guess_index];
185            // This letter is correct.
186            if guess_index == objective_index {
187                results[guess_index] = LetterResult::Correct;
188                if set_index.is_some() {
189                    // Undo the previous set.
190                    results[set_index.unwrap()] = LetterResult::NotPresent;
191                    set_index = None;
192                }
193                // This result was previously unset and we're done with this letter.
194                if existing_result == LetterResult::NotPresent {
195                    break;
196                }
197                // This result was previously set to "LetterResult::PresentNotHere", so we need to
198                // forward that to the next matching letter.
199                continue;
200            }
201            // This result is already set to something, so skip.
202            if existing_result != LetterResult::NotPresent || set_index.is_some() {
203                continue;
204            }
205            // This result was previously unset and matches this letter.
206            results[guess_index] = LetterResult::PresentNotHere;
207            set_index = Some(guess_index);
208        }
209    }
210    Ok(GuessResult { guess, results })
211}