rs_wordle_solver/
engine.rs

1use rayon::prelude::*;
2
3use crate::data::*;
4use crate::restrictions::WordRestrictions;
5use crate::results::*;
6use crate::scorers::WordScorer;
7use std::collections::HashMap;
8use std::num::NonZeroUsize;
9use std::result::Result;
10use std::sync::Arc;
11
12/// Indicates which set of words to guess from. See [`MaxScoreGuesser::new()`].
13#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum GuessFrom {
16    /// Choose the next guess from any unguessed word in the whole word list.
17    AllUnguessedWords,
18    /// Choose the next guess from any possible word based on the current restrictions.
19    PossibleWords,
20}
21
22/// Guesses words in order to solve a single Wordle.
23pub trait Guesser {
24    /// Updates this guesser with information about a word.
25    fn update(&mut self, result: &GuessResult) -> Result<(), WordleError>;
26
27    /// Selects a new guess for the Wordle.
28    ///
29    /// Returns `None` if no known words are possible given the known restrictions imposed by
30    /// previous calls to [`Self::update()`].
31    fn select_next_guess(&mut self) -> Option<Arc<str>>;
32
33    /// Selects a new guess for the Wordle using the requested set of possible words instead of
34    /// the default set for this guesser.
35    ///
36    /// Returns `None` if no known words are possible given the known restrictions imposed by
37    /// previous calls to [`Self::update()`].
38    fn select_next_guess_from(&mut self, from: GuessFrom) -> Option<Arc<str>>;
39
40    /// Provides read access to the remaining set of possible words in this guesser.
41    fn possible_words(&self) -> &[Arc<str>];
42}
43
44/// Attempts to guess the given word within the maximum number of guesses, using the given word
45/// guesser.
46///
47/// ```
48/// use rs_wordle_solver::GameResult;
49/// use rs_wordle_solver::RandomGuesser;
50/// use rs_wordle_solver::WordBank;
51/// use rs_wordle_solver::play_game_with_guesser;
52///
53/// let bank = WordBank::from_iterator(&["abc", "def", "ghi"]).unwrap();
54/// let mut guesser = RandomGuesser::new(bank);
55/// let result = play_game_with_guesser("def", 4, guesser.clone());
56///
57/// assert!(matches!(result, GameResult::Success(_guesses)));
58///
59/// let result = play_game_with_guesser("zzz", 4, guesser.clone());
60///
61/// assert!(matches!(result, GameResult::UnknownWord));
62///
63/// let result = play_game_with_guesser("other", 4, guesser);
64///
65/// assert!(matches!(result, GameResult::UnknownWord));
66/// ```
67pub fn play_game_with_guesser<G: Guesser>(
68    word_to_guess: &str,
69    max_num_guesses: u32,
70    mut guesser: G,
71) -> GameResult {
72    let mut turns: Vec<TurnData> = Vec::new();
73    for _ in 1..=max_num_guesses {
74        let maybe_guess = guesser.select_next_guess();
75        if maybe_guess.is_none() {
76            return GameResult::UnknownWord;
77        }
78        let guess = maybe_guess.unwrap();
79        let num_possible_words_before_guess = guesser.possible_words().len();
80        let result = get_result_for_guess(word_to_guess, guess.as_ref());
81        if result.is_err() {
82            return GameResult::UnknownWord;
83        }
84        let result = result.unwrap();
85        turns.push(TurnData {
86            num_possible_words_before_guess,
87            guess: Box::from(guess.as_ref()),
88        });
89        if result.results.iter().all(|lr| *lr == LetterResult::Correct) {
90            return GameResult::Success(GameData { turns });
91        }
92        guesser.update(&result).unwrap();
93    }
94    GameResult::Failure(GameData { turns })
95}
96
97/// Guesses at random from the possible words that meet the restrictions.
98///
99/// A sample benchmark against the `data/improved-words.txt` list performed as follows:
100///
101/// |Num guesses to win|Num games|
102/// |------------------|---------|
103/// |1|1|
104/// |2|106|
105/// |3|816|
106/// |4|1628|
107/// |5|1248|
108/// |6|518|
109/// |7|180|
110/// |8|67|
111/// |9|28|
112/// |10|7|
113/// |11|2|
114/// |12|1|
115///
116/// **Average number of guesses:** 4.49 +/- 1.26
117#[derive(Debug, Clone)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
119pub struct RandomGuesser {
120    words: GroupedWords,
121    restrictions: WordRestrictions,
122}
123
124impl RandomGuesser {
125    /// Constructs a new `RandomGuesser` using the given word bank.
126    ///
127    /// This guesser always uses the `GuessFrom::PossibleWords` strategy by default.
128    ///
129    /// ```
130    /// use rs_wordle_solver::RandomGuesser;
131    /// use rs_wordle_solver::WordBank;
132    ///
133    /// let bank = WordBank::from_iterator(&["abc", "def", "ghi"]).unwrap();
134    /// let guesser = RandomGuesser::new(bank);
135    /// ```
136    pub fn new(bank: WordBank) -> RandomGuesser {
137        let word_length = bank.word_length();
138        RandomGuesser {
139            words: GroupedWords::new(bank),
140            restrictions: WordRestrictions::new(word_length as u8),
141        }
142    }
143
144    fn select_random_word(words: &[Arc<str>]) -> Option<Arc<str>> {
145        if words.is_empty() {
146            return None;
147        }
148        let random: usize = rand::random();
149        words.get(random % words.len()).map(Arc::clone)
150    }
151}
152
153impl Guesser for RandomGuesser {
154    fn update(&mut self, result: &GuessResult) -> Result<(), WordleError> {
155        self.restrictions.update(result)?;
156        self.words
157            .filter_possible_words(|word| self.restrictions.is_satisfied_by(word));
158        Ok(())
159    }
160
161    fn select_next_guess(&mut self) -> Option<Arc<str>> {
162        self.select_next_guess_from(GuessFrom::PossibleWords)
163    }
164
165    fn select_next_guess_from(&mut self, from: GuessFrom) -> Option<Arc<str>> {
166        match from {
167            GuessFrom::AllUnguessedWords => {
168                RandomGuesser::select_random_word(self.words.unguessed_words())
169            }
170            GuessFrom::PossibleWords => RandomGuesser::select_random_word(self.possible_words()),
171        }
172    }
173
174    fn possible_words(&self) -> &[Arc<str>] {
175        self.words.possible_words()
176    }
177}
178
179/// Represents a guess with a 'score' estimating how useful the guess is. Higher scores are better.
180#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
181pub struct ScoredGuess {
182    pub score: i64,
183    pub guess: Arc<str>,
184}
185
186/// Selects the next guess that maximizes the score according to the owned scorer.
187///
188/// See [`WordScorer`] for more information about possible scoring algorithms.
189#[derive(Debug, Clone)]
190#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
191pub struct MaxScoreGuesser<T>
192where
193    T: WordScorer + Clone + Sync,
194{
195    default_guess_mode: GuessFrom,
196    grouped_words: GroupedWords,
197    restrictions: WordRestrictions,
198    scorer: T,
199    parallelisation_limit: usize,
200    all_unguessed_word_scores: Option<Vec<i64>>,
201    possible_word_scores: Option<Vec<i64>>,
202}
203
204impl<T> MaxScoreGuesser<T>
205where
206    T: WordScorer + Clone + Sync,
207{
208    /// Constructs a new `MaxScoreGuesser` that will guess the word with the maximum score
209    /// according to the given [`WordScorer`]. This will only score and guess from the words in the
210    /// `word_bank` according to the given `guess_from` strategy.
211    ///
212    /// If in doubt, you probably want to use `GuessFrom::AllUnguessedWords` for better performance.
213    ///
214    /// See [`WordScorer`] for more information about possible scoring algorithms.
215    ///
216    /// ```
217    /// use std::sync::Arc;
218    /// use rs_wordle_solver::GuessFrom;
219    /// use rs_wordle_solver::Guesser;
220    /// use rs_wordle_solver::MaxScoreGuesser;
221    /// use rs_wordle_solver::WordBank;
222    /// use rs_wordle_solver::scorers::MaxEliminationsScorer;
223    ///
224    /// let bank = WordBank::from_iterator(&["azz", "bzz", "czz", "abc"]).unwrap();
225    /// let scorer = MaxEliminationsScorer::new(bank.clone());
226    /// let mut guesser = MaxScoreGuesser::new(GuessFrom::AllUnguessedWords, bank, scorer);
227    ///
228    /// assert_eq!(guesser.select_next_guess(), Some(Arc::from("abc")));
229    /// ```
230    pub fn new(guess_mode: GuessFrom, word_bank: WordBank, scorer: T) -> MaxScoreGuesser<T> {
231        let word_length = word_bank.word_length();
232        Self {
233            default_guess_mode: guess_mode,
234            grouped_words: GroupedWords::new(word_bank),
235            restrictions: WordRestrictions::new(word_length as u8),
236            scorer,
237            parallelisation_limit: std::thread::available_parallelism()
238                .map(NonZeroUsize::get)
239                .unwrap_or(1),
240            all_unguessed_word_scores: None,
241            possible_word_scores: None,
242        }
243    }
244
245    /// Sets the parallelisation limit. Various internal operations may be parallelised if operating
246    /// on lists larger than this limit. The default setting is the result of
247    /// `std::thread::available_parallelism`.
248    pub fn with_parallelisation_limit(mut self, parallelisation_limit: usize) -> Self {
249        self.parallelisation_limit = parallelisation_limit;
250        self
251    }
252
253    /// Sets the precomputed word scores based on the provided map. If the map is missing scores
254    /// for any words, they will be computed to fill the gaps. These scores will be used until the
255    /// next call to [`Self::update()`].
256    ///
257    /// This is handy to initialise the guesser with precomputed scores for the first guess. You
258    /// can retrieve this data by calling [`Self::get_or_compute_scores()`] on a newly constructed
259    /// guesser.
260    pub fn with_scores(mut self, scores: &HashMap<Arc<str>, i64>) -> Self {
261        let words_to_score = self.words_to_score(self.default_guess_mode);
262        let ordered_scores: Vec<i64> = words_to_score
263            .iter()
264            .map(|word| {
265                scores
266                    .get(word)
267                    .copied()
268                    .unwrap_or_else(|| self.scorer.score_word(word))
269            })
270            .collect();
271        match self.default_guess_mode {
272            GuessFrom::AllUnguessedWords => {
273                self.all_unguessed_word_scores = Some(ordered_scores);
274            }
275            GuessFrom::PossibleWords => {
276                self.possible_word_scores = Some(ordered_scores);
277            }
278        }
279        self
280    }
281
282    /// Gets the score for each available guess, keyed by guess. This computes the scores if they
283    /// have not already been computed. The set of words is limited by the [`GuessFrom`] value used
284    /// in this guesser, and by the updates that have been provided so far.
285    pub fn get_or_compute_scores(&mut self) -> HashMap<Arc<str>, i64> {
286        self.compute_scores_if_unknown();
287        self.words_to_score(self.default_guess_mode)
288            .iter()
289            .zip(self.word_scores(self.default_guess_mode).unwrap().iter())
290            .map(|(word, score)| (Arc::clone(word), *score))
291            .collect()
292    }
293
294    /// Returns up-to the top `n` guesses for the wordle, based on the current state.
295    ///
296    /// Returns an empty vector if no known words are possible given the known restrictions imposed
297    /// by previous calls to [`Self::update()`].
298    pub fn select_top_n_guesses(&mut self, n: usize) -> Vec<ScoredGuess> {
299        self.select_top_n_guesses_from(n, self.default_guess_mode)
300    }
301
302    /// Returns up-to the top `n` guesses for the wordle, based on the current state and the
303    /// provided [`GuessFrom`] option.
304    ///
305    /// Returns an empty vector if no known words are possible given the known restrictions imposed
306    /// by previous calls to [`Self::update()`].
307    pub fn select_top_n_guesses_from(&mut self, n: usize, from: GuessFrom) -> Vec<ScoredGuess> {
308        self.compute_scores_if_needed_from(from);
309        let word_scores = self.word_scores(from).unwrap();
310        let mut scored_words: Vec<(&Arc<str>, i64)> = word_scores
311            .iter()
312            .zip(self.words_to_score(from).iter())
313            .map(|(score, word)| (word, *score))
314            .collect();
315
316        // Use a stable sort, because possible words come before impossible words, and we want to
317        // prioritise possible words if we're using GuessFrom::AllUnguessedWords.
318        if scored_words.len() >= self.parallelisation_limit {
319            scored_words.par_sort_by_key(|(_, score)| -score);
320        } else {
321            scored_words.sort_by_key(|(_, score)| -score);
322        }
323        scored_words
324            .iter()
325            .take(n)
326            .map(|(word, score)| ScoredGuess {
327                score: *score,
328                guess: Arc::clone(*word),
329            })
330            .collect()
331    }
332
333    /// Computes the word scores if they are not known, using the default [`GuessFrom`] provided on
334    /// construction. The result is cached into `Self` until the scorer's state changes.
335    ///
336    /// This can be useful to precompute the scores for the first guess in a base guesser, and then
337    /// clone that guesser for each use.
338    ///
339    /// ```
340    /// # use std::sync::Arc;
341    /// # use rs_wordle_solver::GuessFrom;
342    /// # use rs_wordle_solver::Guesser;
343    /// # use rs_wordle_solver::MaxScoreGuesser;
344    /// # use rs_wordle_solver::WordBank;
345    /// # use rs_wordle_solver::scorers::MaxEliminationsScorer;
346    ///
347    /// let bank = WordBank::from_iterator(&["azz", "bzz", "czz", "abc"]).unwrap();
348    /// let scorer = MaxEliminationsScorer::new(bank.clone());
349    /// let mut base_guesser = MaxScoreGuesser::new(GuessFrom::AllUnguessedWords, bank, scorer);
350    ///
351    /// // Precompute the first scores.
352    /// base_guesser.compute_scores_if_unknown();
353    ///
354    /// // Clone a new guesser for each use.
355    /// let guesser = base_guesser.clone();
356    /// ```
357    ///
358    /// You can also use the `serde` feature to serialise an instance after doing this
359    /// precomputation.
360    pub fn compute_scores_if_unknown(&mut self) {
361        self.compute_scores_if_needed_from(self.default_guess_mode);
362    }
363
364    /// Computes the word scores if they are not known. The result is cached into `Self` until the
365    /// scorer's state changes.
366    ///
367    /// This can be useful to precompute the scores for the first guess in a base guesser, and then
368    /// clone that guesser for each use.
369    ///
370    /// ```
371    /// # use std::sync::Arc;
372    /// # use rs_wordle_solver::GuessFrom;
373    /// # use rs_wordle_solver::Guesser;
374    /// # use rs_wordle_solver::MaxScoreGuesser;
375    /// # use rs_wordle_solver::WordBank;
376    /// # use rs_wordle_solver::scorers::MaxEliminationsScorer;
377    ///
378    /// let bank = WordBank::from_iterator(&["azz", "bzz", "czz", "abc"]).unwrap();
379    /// let scorer = MaxEliminationsScorer::new(bank.clone());
380    /// let mut base_guesser = MaxScoreGuesser::new(GuessFrom::AllUnguessedWords, bank, scorer);
381    ///
382    /// // Precompute the first scores.
383    /// base_guesser.compute_scores_if_needed_from(GuessFrom::PossibleWords);
384    ///
385    /// // Clone a new guesser for each use.
386    /// let guesser = base_guesser.clone();
387    /// ```
388    ///
389    /// You can also use the `serde` feature to serialise an instance after doing this
390    /// precomputation.
391    pub fn compute_scores_if_needed_from(&mut self, from: GuessFrom) {
392        let current_scores = self.word_scores(from);
393        if current_scores.is_some() {
394            return;
395        }
396        let computed_scores = Some(MaxScoreGuesser::score_words(
397            self.words_to_score(from),
398            &self.scorer,
399            self.parallelisation_limit,
400        ));
401
402        match from {
403            GuessFrom::AllUnguessedWords => {
404                self.all_unguessed_word_scores = computed_scores;
405            }
406            GuessFrom::PossibleWords => {
407                self.possible_word_scores = computed_scores;
408            }
409        }
410    }
411
412    fn score_words(
413        words_to_score: &[Arc<str>],
414        scorer: &T,
415        parallelisation_limit: usize,
416    ) -> Vec<i64> {
417        if words_to_score.len() >= parallelisation_limit {
418            words_to_score
419                .par_iter()
420                .map(|word| scorer.score_word(word))
421                .collect()
422        } else {
423            words_to_score
424                .iter()
425                .map(|word| scorer.score_word(word))
426                .collect()
427        }
428    }
429
430    /// Retrieves the requested set of word scores, if they have been precomputed, else `None`.
431    ///
432    /// If present, these are in the same order as the words returned by [`Self::words_to_score()`].
433    fn word_scores(&self, from: GuessFrom) -> Option<&[i64]> {
434        match from {
435            GuessFrom::PossibleWords => {
436                if let Some(scores) = self.possible_word_scores.as_ref() {
437                    Some(scores.as_slice())
438                } else if let Some(scores) = self.all_unguessed_word_scores.as_ref() {
439                    // The possible words are always at the start, so return those, except for
440                    // the special case where there is only one possible word, and it has
441                    // already been guessed.
442                    if self.grouped_words.num_possible_words()
443                        <= self.grouped_words.num_unguessed_words()
444                    {
445                        Some(scores.as_slice()[..self.grouped_words.num_possible_words()].as_ref())
446                    } else {
447                        None
448                    }
449                } else {
450                    None
451                }
452            }
453            GuessFrom::AllUnguessedWords => self.all_unguessed_word_scores.as_deref(),
454        }
455    }
456
457    /// Retrieves the words that need scoring, in the same order as the precomputed scores, if any.
458    ///
459    /// Note that if there are two or fewer possible words remaining, this will always return only
460    /// the possible words.
461    fn words_to_score(&self, from: GuessFrom) -> &[Arc<str>] {
462        match from {
463            // Only score possible words if we're down to the last two guesses.
464            _ if self.grouped_words.num_possible_words() <= 2 => {
465                self.grouped_words.possible_words()
466            }
467            GuessFrom::AllUnguessedWords => self.grouped_words.unguessed_words(),
468            GuessFrom::PossibleWords => self.grouped_words.possible_words(),
469        }
470    }
471}
472
473impl<T> Guesser for MaxScoreGuesser<T>
474where
475    T: WordScorer + Clone + Sync,
476{
477    fn update(&mut self, result: &GuessResult) -> Result<(), WordleError> {
478        self.all_unguessed_word_scores = None;
479        self.possible_word_scores = None;
480        self.grouped_words.remove_guess_if_present(result.guess);
481        self.restrictions.update(result)?;
482        self.grouped_words
483            .filter_possible_words(|word| self.restrictions.is_satisfied_by(word));
484        self.scorer.update(
485            result.guess,
486            &self.restrictions,
487            self.grouped_words.possible_words(),
488        )?;
489        Ok(())
490    }
491
492    fn select_next_guess(&mut self) -> Option<Arc<str>> {
493        self.select_next_guess_from(self.default_guess_mode)
494    }
495
496    fn select_next_guess_from(&mut self, from: GuessFrom) -> Option<Arc<str>> {
497        // Compute an owned instance of word_scores if the guess mode has changed.
498        self.compute_scores_if_needed_from(from);
499        let word_scores = self.word_scores(from).unwrap();
500        let words_to_score = self.words_to_score(from);
501        let (best_index, _) = if words_to_score.len() > self.parallelisation_limit {
502            word_scores.par_iter().enumerate().reduce(
503                || (usize::MAX, &i64::MIN),
504                |(best_index, best_score), (index, score)| {
505                    if *score > *best_score {
506                        return (index, score);
507                    }
508                    // Use the lower index, because it is more likely to be a possible word.
509                    if *score == *best_score && index < best_index {
510                        return (index, score);
511                    }
512                    (best_index, best_score)
513                },
514            )
515        } else {
516            let mut best_score = &i64::MIN;
517            let mut best_index = usize::MAX;
518            word_scores.iter().enumerate().for_each(|(i, score)| {
519                if *score > *best_score {
520                    best_score = score;
521                    best_index = i;
522                }
523            });
524            (best_index, best_score)
525        };
526        if best_index > words_to_score.len() {
527            None
528        } else {
529            Some(Arc::clone(&words_to_score[best_index]))
530        }
531    }
532
533    fn possible_words(&self) -> &[Arc<str>] {
534        self.grouped_words.possible_words()
535    }
536}