codebreaker_solver/
solver.rs

1use std::cmp::min;
2
3use rand::seq::SliceRandom as _;
4use rayon::iter::{IntoParallelRefIterator, ParallelIterator as _};
5
6use crate::{
7    code::{all_possible_codes, Code, NUMBER_OF_PEGS_IN_CODE},
8    hint::Hint,
9    peg::NUM_DIFFERENT_PEGS,
10};
11
12/// A solver for codebreaker
13pub struct Solver {
14    /// Codes which have not been guessed so far yet, but are also possible solutions. We may still
15    /// want to guess them though, because guessing them might reveal more information, than
16    /// guessing a potential solution.
17    remaining_unguessed_codes: Vec<Code>,
18    /// Solutions which are still possible.
19    potential_solutions: Vec<Code>,
20}
21
22impl Solver {
23    /// Creates a solver which deterministically solves the codebreaker game.
24    pub fn new() -> Self {
25        let mut potential_solutions =
26            Vec::with_capacity((NUM_DIFFERENT_PEGS as usize).pow(NUMBER_OF_PEGS_IN_CODE as u32));
27        potential_solutions.extend(all_possible_codes());
28        Solver {
29            remaining_unguessed_codes: Vec::new(),
30            potential_solutions,
31        }
32    }
33
34    /// Creates a solver which picks randomly one of the best guesses
35    pub fn with_sampled_guesses(rng: &mut impl rand::Rng) -> Self {
36        let mut solver = Solver::new();
37        solver.remaining_unguessed_codes.shuffle(rng);
38        solver
39    }
40
41    pub fn guess(&mut self) -> Code {
42        // Maximize guaranteed elimination of possiblities. We find a guess which maximizes the
43        // potential solutions elimintated. We would do so, even if the guess is not a potential
44        // solution itself, however, if two guesses eliminate the same number of possibilities, we
45        // prefer to guess a potential solution, so we might get lucky and finish the game right
46        // there.
47        let best_candidate = self
48            .potential_solutions
49            .par_iter()
50            .chain(self.remaining_unguessed_codes.par_iter())
51            .map(|&code| Candidate::new(code))
52            .reduce_with(|a, b| {
53                Candidate::better(a, b, |code, lower_bound| {
54                    self.min_possibilties_eliminated(code, lower_bound)
55                })
56            })
57            .expect("Solution must be possible");
58        best_candidate.code
59    }
60
61    pub fn update(&mut self, guess: Code, hint: Hint) {
62        self.potential_solutions.retain(|&code| {
63            let canidate_hint = Hint::new(guess, code);
64            if hint != canidate_hint && code != guess {
65                self.remaining_unguessed_codes.push(code);
66            }
67            hint == canidate_hint
68        });
69        self.remaining_unguessed_codes.retain(|&code| code != guess);
70    }
71
72    /// Minimum number of possiblities a guess would eliminate. If the minimum is larger than the
73    /// lower_bound result is exact, otherwise it is lower_bound. Setting a lower bound allows the
74    /// function to exit early if the result would be smaller than the a better maximum we already
75    /// know about.
76    fn min_possibilties_eliminated(&self, candidate_guess: Code, lower_bound: u32) -> u32 {
77        let mut current_min = u32::MAX;
78        for &possible_solution in &self.potential_solutions {
79            let hint = Hint::new(candidate_guess, possible_solution);
80            let eliminated = self.num_eliminated_possiblities(hint, candidate_guess, current_min);
81            current_min = min(current_min, eliminated);
82            if current_min <= lower_bound {
83                return lower_bound;
84            }
85        }
86        current_min
87    }
88
89    /// How many possible codes would be remaining by a guess with a certain hint. The result will
90    /// be exact if it is smaller or equal to upper bound. Otherwise it is larger than upper bound.
91    fn num_eliminated_possiblities(&self, hint: Hint, guess: Code, upper_bound: u32) -> u32 {
92        self.potential_solutions
93            .iter()
94            .filter(|&&code| hint != Hint::new(guess, code) || guess == code)
95            .take(upper_bound as usize)
96            .count() as u32
97    }
98}
99
100impl Default for Solver {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106/// Candidate for a guess
107struct Candidate {
108    /// Code the guess represents
109    code: Code,
110    /// Laziyly computed number of guaranteed possibilities eliminated by this guess
111    eliminated: Option<u32>,
112}
113
114impl Candidate {
115    fn new(code: Code) -> Self {
116        Candidate {
117            code,
118            eliminated: None,
119        }
120    }
121
122    /// Out of two candidates, picks the better one, or the first one if they are equal.
123    fn better(
124        mut a: Candidate,
125        mut b: Candidate,
126        min_eliminated_with_lower_bound: impl Fn(Code, u32) -> u32,
127    ) -> Candidate {
128        // Calculate the guaranteed number of eliminated possibilities, if missing, but using an
129        // existing number as lower bound, if available.
130        match (a.eliminated, b.eliminated) {
131            // Both values availabe, nothing to do
132            (Some(_), Some(_)) => a.replace_if_better(b),
133            (Some(a_eliminated), None) => {
134                b.eliminated = Some(min_eliminated_with_lower_bound(b.code, a_eliminated));
135                // Be careful, the order matters here. Due to short circuiting, above b might be
136                // worse, but seem just as good.
137                a.replace_if_better(b)
138            }
139            (None, Some(b_eliminated)) => {
140                a.eliminated = Some(min_eliminated_with_lower_bound(a.code, b_eliminated));
141                b.replace_if_better(a)
142            }
143            (None, None) => {
144                a.eliminated = Some(min_eliminated_with_lower_bound(a.code, 0));
145                b.eliminated = Some(min_eliminated_with_lower_bound(
146                    b.code,
147                    a.eliminated.unwrap(),
148                ));
149                a.replace_if_better(b)
150            }
151        }
152    }
153
154    /// Replaces the candidate with another one if the other one is better. If both are equal, self
155    /// is returned. Requires eliminated to be computed.
156    fn replace_if_better(self, other: Candidate) -> Candidate {
157        if other.eliminated.unwrap() > self.eliminated.unwrap() {
158            other
159        } else {
160            self
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167
168    use crate::{code::Code, hint::Hint};
169
170    use super::Solver;
171
172    #[test]
173    fn solve_5611() {
174        let code: Code = "5611".parse().unwrap();
175
176        let mut solver = Solver::new();
177        let guesses = play_out(code, &mut solver);
178
179        assert_eq!(&["2211", "4321", "5131", "5511", "5611"], &guesses[..]);
180    }
181
182    #[test]
183    fn solve_2231() {
184        let code: Code = "2231".parse().unwrap();
185
186        let mut solver = Solver::new();
187        let guesses = play_out(code, &mut solver);
188
189        assert_eq!(&["2211", "3221", "2231"], &guesses[..]);
190    }
191
192    fn play_out(code: Code, solver: &mut Solver) -> Vec<String> {
193        let mut guesses = Vec::new();
194        let mut count = 0;
195        loop {
196            assert!(count < 5, "Too many guesses");
197
198            let guess = solver.guess();
199            guesses.push(guess.to_string());
200            let hint = Hint::new(guess, code);
201            solver.update(guess, hint);
202            if hint.is_solution() {
203                break;
204            }
205            count += 1;
206        }
207        guesses
208    }
209}