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}