turing_machine_ai/
gametree.rs

1use std::fmt::Debug;
2
3use thiserror::Error;
4
5use crate::{
6    code::{Code, Set},
7    game::Game, verifier::VerifierOption,
8};
9
10/// A struct representing the current game state. It contains the possible
11/// solutions for the verifier selection, the currently selected code (if any),
12/// the currently selected verifier (if any), whether a verifier was tested, as
13/// well as how many tests were performed.
14#[derive(Copy, Clone, Debug)]
15pub struct State<'a> {
16    game: &'a Game,
17    /// All the codes that are still possible solutions.
18    possible_codes: Set,
19    currently_selected_code: Option<Code>,
20    currently_chosen_verifier: Option<ChosenVerifier>,
21    has_guessed_one_verifier_for_code: bool,
22    codes_guessed: u8,
23    verifiers_checked: u8,
24}
25
26/// A struct representing a score associated with a game state. The score
27/// represents how desirable it is for the guesser. A perfect score compares
28/// the highest whereas a game without a solution is the lowest possible score.
29///
30/// Internally, the score is inverted so that a perfect game is represented by a 0.
31#[derive(Copy, Clone, Eq, PartialEq, Debug)]
32struct StateScore(u16);
33
34#[derive(Copy, Clone, Eq, PartialEq, Debug)]
35pub struct GameScore {
36    pub codes_guessed: u8,
37    pub verifiers_checked: u8,
38}
39
40impl StateScore {
41    fn no_solution() -> Self {
42        // A state without a solution gives the best possible score, which is
43        // the worst possible score for the answer to the verifier. This
44        // ensures that they will never pick this result. Instead they will
45        // give `StateScore::useless_verifier_check()`, which is the worst
46        // possible outcome for the player.
47        StateScore(0)
48    }
49
50    fn solution(codes_guessed: u8, verifier_checks: u8) -> Self {
51        StateScore(u16::from(codes_guessed) << 8 | u16::from(verifier_checks))
52    }
53
54    /// This is represented by the worst possible outcome for the verifier.
55    /// This is actually a heuristic---because it is never a good idea to guess
56    /// without gaining information, these branches do not have to be explored.
57    fn useless_verifier_check() -> Self {
58        StateScore(u16::MAX)
59    }
60
61    /// Get how many codes and verifiers were checked for this game score. If
62    /// the game did not finish for whatever reason, this function will return
63    /// `None`.
64    pub fn codes_and_verifiers_checked(self) -> Option<GameScore> {
65        if self.0 == u16::MAX {
66            None
67        } else {
68            Some(GameScore {
69                codes_guessed: (self.0 >> 8) as u8,
70                verifiers_checked: (self.0 & 0b1111_1111) as u8,
71            })
72        }
73    }
74
75    fn min() -> Self {
76        StateScore(u16::MAX)
77    }
78
79    fn max() -> Self {
80        StateScore(u16::MIN)
81    }
82}
83
84/// Additional info that may be returned by the function `State::after_move`.
85#[derive(Copy, Clone, Eq, PartialEq, Debug)]
86pub enum AfterMoveInfo {
87    /// The move checked a verifier that did not provide additional
88    /// information about the game solution.
89    UselessVerifierCheck,
90}
91
92impl Ord for StateScore {
93    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
94        self.0.cmp(&other.0).reverse()
95    }
96}
97
98impl PartialOrd for StateScore {
99    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
100        Some(self.cmp(other))
101    }
102}
103
104/// A verifier answer, represented either by a cross or a check.
105#[derive(Eq, PartialEq, Clone, Copy, Debug)]
106pub enum VerifierSolution {
107    Cross,
108    Check,
109}
110
111#[derive(Eq, PartialEq, Copy, Clone)]
112pub struct ChosenVerifier(u8);
113
114impl From<u8> for ChosenVerifier {
115    fn from(value: u8) -> Self {
116        ChosenVerifier(value)
117    }
118}
119
120impl Debug for ChosenVerifier {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "{}", ('A'..).nth(self.0.into()).unwrap())
123    }
124}
125
126#[derive(Eq, PartialEq, Clone, Copy, Debug)]
127pub enum Move {
128    ChooseNewCode(Code),
129    VerifierSolution(VerifierSolution),
130    ChooseVerifier(ChosenVerifier),
131}
132
133#[derive(Copy, Clone, Eq, PartialEq, Error, Debug)]
134pub enum AfterMoveError {
135    #[error("invalid move for game state")]
136    InvalidMoveError,
137    #[error("there are no solutions left for this game state")]
138    NoCodesLeft,
139}
140
141impl<'a> State<'a> {
142    #[must_use]
143    pub fn new(game: &'a Game) -> Self {
144        State {
145            game,
146            possible_codes: game.possible_solutions(),
147            currently_selected_code: None,
148            currently_chosen_verifier: None,
149            has_guessed_one_verifier_for_code: false,
150            codes_guessed: 0,
151            verifiers_checked: 0,
152        }
153    }
154
155    pub fn possible_codes(self) -> Set {
156        self.possible_codes
157    }
158
159    #[must_use]
160    pub fn is_solved(self) -> bool {
161        self.possible_codes.size() == 1
162    }
163
164    /// If solved, returns the solution. Otherwise, it returns `None`.
165    #[must_use]
166    pub fn solution(self) -> Option<Code> {
167        self.possible_codes
168            .into_iter()
169            .next()
170            .filter(|_| self.is_solved())
171    }
172
173    /// Return the state after performing the given move. If the given move is
174    /// invalid, this function returns an error. In addition to the state
175    /// itself, this function will sometimes provide additional information
176    /// about the transition as the second argument of the tuple.
177    pub fn after_move(
178        mut self,
179        move_to_do: Move,
180    ) -> Result<(State<'a>, Option<AfterMoveInfo>), AfterMoveError> {
181        let mut info = None;
182        match move_to_do {
183            Move::ChooseNewCode(code) => {
184                if self.is_awaiting_result() {
185                    return Err(AfterMoveError::InvalidMoveError);
186                }
187                self.currently_selected_code = Some(code);
188                self.codes_guessed += 1;
189                self.has_guessed_one_verifier_for_code = false;
190            }
191            Move::ChooseVerifier(choose_verifier_option) => {
192                if self.is_awaiting_result() {
193                    return Err(AfterMoveError::InvalidMoveError);
194                }
195                self.currently_chosen_verifier = Some(choose_verifier_option);
196                self.verifiers_checked += 1;
197            }
198            Move::VerifierSolution(verifier_solution) => {
199                if let Some(chosen_verifier) = self.currently_chosen_verifier {
200                    // Get all codes that correspond to a verifier option giving the provided answer.
201                    let bitmask_for_solution = self
202                        .game
203                        .verfier(chosen_verifier.0)
204                        .options()
205                        .map(VerifierOption::code_set)
206                        .filter(|code_set| {
207                            let would_give_check =
208                                code_set.contains(self.currently_selected_code.unwrap());
209                            let gives_check = verifier_solution == VerifierSolution::Check;
210                            would_give_check == gives_check
211                        })
212                        .collect::<Set>();
213                    let possible_codes = self.possible_codes;
214                    let new_possible_codes = possible_codes.intersected_with(bitmask_for_solution);
215                    if new_possible_codes == possible_codes {
216                        info = Some(AfterMoveInfo::UselessVerifierCheck);
217                    } else if new_possible_codes.size() == 0 {
218                        return Err(AfterMoveError::NoCodesLeft);
219                    } else {
220                        self.possible_codes = new_possible_codes;
221                    }
222
223                    self.currently_chosen_verifier = None;
224                    if self.verifiers_checked == 3 {
225                        self.currently_selected_code = None;
226                    }
227                } else {
228                    return Err(AfterMoveError::InvalidMoveError);
229                }
230            }
231        }
232        Ok((self, info))
233    }
234
235    /// Returns true if the game is awaiting a verifier answer.
236    #[must_use]
237    pub fn is_awaiting_result(&self) -> bool {
238        self.currently_chosen_verifier.is_some()
239    }
240
241    /// Return all possible moves. Notably these are not verified in every way:
242    /// - Verifiers may return impossible results, leading to no solution.
243    /// - Codes or verifiers may be chosen that do not provide information to
244    ///   the player.
245    pub fn possible_moves(&self) -> impl Iterator<Item = Move> + '_ {
246        // This function looks messy to avoid allocating a Vec with moves.
247
248        // If awaiting result, return both results
249        [
250            Move::VerifierSolution(VerifierSolution::Check),
251            Move::VerifierSolution(VerifierSolution::Cross),
252        ]
253        .iter()
254        .copied()
255        .filter(|_| self.is_awaiting_result())
256        // Otherwise,
257        .chain(
258            // Otherwise, if a code is chosen, choose a verifier
259            (0..u8::try_from(self.game.verifier_count()).unwrap())
260                .map(|i| Move::ChooseVerifier(i.into()))
261                .filter(|_| self.currently_selected_code.is_some())
262                .chain(
263                    // If the code was used once, or if no code was selected, choose new code
264                    Set::all().into_iter().map(Move::ChooseNewCode).filter(|_| {
265                        self.currently_selected_code.is_none()
266                            || self.has_guessed_one_verifier_for_code
267                    }),
268                )
269                .filter(|_| !self.is_awaiting_result()),
270        )
271    }
272
273    /// Returns whether the state demands maximizing the score. This
274    /// corresponds to those states where the player must do a turn as opposed
275    /// to waiting for a verifier answer.
276    fn is_maximizing_score(self) -> bool {
277        !self.is_awaiting_result()
278    }
279
280    /// Perform minmax with alpha-beta pruning.
281    fn alphabeta(self, mut alpha: StateScore, mut beta: StateScore) -> (StateScore, Option<Move>) {
282        // If the game is solved, return the result.
283        if self.is_solved() {
284            (
285                StateScore::solution(self.codes_guessed, self.verifiers_checked),
286                None,
287            )
288        } else if self.is_maximizing_score() {
289            let mut highest_score = StateScore::min();
290            let mut best_move = None;
291            for move_to_do in self.possible_moves() {
292                let next_node = self.after_move(move_to_do);
293                let score = match next_node {
294                    Err(AfterMoveError::NoCodesLeft) => StateScore::no_solution(),
295                    Err(AfterMoveError::InvalidMoveError) => panic!("invalid move!"),
296                    Ok((_, Some(AfterMoveInfo::UselessVerifierCheck))) => {
297                        StateScore::useless_verifier_check()
298                    }
299                    Ok((state, None)) => state.alphabeta(alpha, beta).0,
300                };
301                if score > highest_score {
302                    highest_score = score;
303                    best_move = Some(move_to_do);
304                }
305                if score > beta {
306                    break;
307                }
308                if score > alpha {
309                    alpha = score;
310                }
311            }
312            (highest_score, best_move)
313        } else {
314            let mut lowest_score = StateScore::max();
315            for move_to_do in self.possible_moves() {
316                let next_node = self.after_move(move_to_do);
317                let score = match next_node {
318                    Err(AfterMoveError::NoCodesLeft) => StateScore::no_solution(),
319                    Err(AfterMoveError::InvalidMoveError) => panic!("invalid move"),
320                    Ok((_, Some(AfterMoveInfo::UselessVerifierCheck))) => {
321                        StateScore::useless_verifier_check()
322                    }
323                    Ok((state, None)) => state.alphabeta(alpha, beta).0,
324                };
325                if score < lowest_score {
326                    lowest_score = score;
327                }
328                if score < alpha {
329                    break;
330                }
331                if score < beta {
332                    beta = score;
333                }
334            }
335            // It doesn't make sense to return a move for the other player
336            (lowest_score, None)
337        }
338    }
339
340    /// Find the best possible move to minimize the maximum amount of codes and
341    /// verifier checks needed. The game must be at a state where the player
342    /// chooses a code or a verifier.
343    ///
344    /// # Panics
345    /// This function will panic if the state is currently awaiting a verifier
346    /// answer or if the game has already been solved.
347    pub fn find_best_move(self) -> (GameScore, Move) {
348        assert!(!self.is_awaiting_result() && !self.is_solved());
349        // The optimal possible game.
350        let alpha = StateScore::min();
351        // The worst possible game.
352        let beta = StateScore::max();
353        if let (score, Some(move_to_do)) = self.alphabeta(alpha, beta) {
354            (score.codes_and_verifiers_checked().unwrap(), move_to_do)
355        } else {
356            panic!("No move possible");
357        }
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use crate::gametree::GameScore;
364
365    use super::StateScore;
366
367    #[test]
368    fn test_game_score() {
369        for codes_guessed in 0..10 {
370            for verifiers_checked in codes_guessed..30 {
371                let state_score = StateScore::solution(codes_guessed, verifiers_checked);
372                assert_eq!(
373                    state_score.codes_and_verifiers_checked().unwrap(),
374                    GameScore {
375                        codes_guessed,
376                        verifiers_checked
377                    }
378                );
379            }
380        }
381    }
382}