board_game/ai/
solver.rs

1use std::cmp::Ordering;
2use std::fmt::{Debug, Formatter};
3use std::ops::Neg;
4
5use internal_iterator::InternalIterator;
6use rand::Rng;
7
8use crate::ai::minimax::{minimax, minimax_all_moves, minimax_value, Heuristic, MinimaxResult};
9use crate::ai::Bot;
10use crate::board::{Board, BoardDone, Outcome};
11use crate::pov::NonPov;
12use crate::wdl::OutcomeWDL;
13
14/// Minimax [Heuristic] that only looks at board outcomes.
15/// When there are multiple winning moves it picks the shortest one,
16/// and when there are only losing moves it picks the longest one.  
17#[derive(Debug)]
18pub struct SolverHeuristic;
19
20#[derive(Debug, Copy, Clone, Eq, PartialEq)]
21pub enum SolverValue {
22    WinIn(u32),
23    LossIn(u32),
24    Draw,
25    Unknown,
26}
27
28impl<B: Board> Heuristic<B> for SolverHeuristic {
29    type V = SolverValue;
30
31    fn value(&self, board: &B, length: u32) -> SolverValue {
32        board
33            .outcome()
34            .map_or(SolverValue::Unknown, |p| match p.pov(board.next_player()) {
35                OutcomeWDL::Win => SolverValue::WinIn(length),
36                OutcomeWDL::Draw => SolverValue::Draw,
37                OutcomeWDL::Loss => SolverValue::LossIn(length),
38            })
39    }
40
41    fn merge(old: SolverValue, new: SolverValue) -> (SolverValue, Ordering) {
42        SolverValue::merge(old, new)
43    }
44}
45
46impl SolverValue {
47    pub fn to_i32(self) -> i32 {
48        match self {
49            SolverValue::WinIn(n) => i32::MAX - n as i32,
50            SolverValue::LossIn(n) => -i32::MAX + n as i32,
51            SolverValue::Draw | SolverValue::Unknown => 0,
52        }
53    }
54
55    pub fn to_outcome_wdl(self) -> Option<OutcomeWDL> {
56        match self {
57            SolverValue::WinIn(_) => Some(OutcomeWDL::Win),
58            SolverValue::LossIn(_) => Some(OutcomeWDL::Loss),
59            SolverValue::Draw => Some(OutcomeWDL::Draw),
60            SolverValue::Unknown => None,
61        }
62    }
63
64    /// Return `(best_value, cmp(new, old))`,
65    /// where best_value properly accounts for shortest win, longest loss and draw vs unknown.
66    pub fn merge(old: SolverValue, new: SolverValue) -> (SolverValue, Ordering) {
67        use SolverValue::*;
68
69        match (old, new) {
70            // prefer shortest win and longest loss
71            (WinIn(old_n), WinIn(new_n)) => (if new_n <= old_n { new } else { old }, new_n.cmp(&old_n).reverse()),
72            (LossIn(old_n), LossIn(new_n)) => (if new_n >= old_n { new } else { old }, new_n.cmp(&old_n)),
73
74            // win/loss is better/worse then everything else
75            (WinIn(_), _) => (old, Ordering::Less),
76            (LossIn(_), _) => (new, Ordering::Greater),
77            (_, WinIn(_)) => (new, Ordering::Greater),
78            (_, LossIn(_)) => (old, Ordering::Less),
79
80            // draw and unknown are the same, but return unknown if either is unknown
81            (Draw, Draw) => (Draw, Ordering::Equal),
82            (Unknown | Draw, Unknown | Draw) => (Unknown, Ordering::Equal),
83        }
84    }
85
86    /// Return whether `child` could a child of the optimally combined `parent`.
87    pub fn could_be_optimal_child(parent: SolverValue, child: SolverValue) -> bool {
88        let best_child = match parent {
89            SolverValue::WinIn(n) => SolverValue::LossIn(n - 1),
90            SolverValue::LossIn(n) => SolverValue::WinIn(n - 1),
91            SolverValue::Draw => SolverValue::Draw,
92            SolverValue::Unknown => panic!("This function does not work for unknown values"),
93        };
94
95        // evaluate child >= best_child since this is from the other POV
96        SolverValue::merge(best_child, child).1.is_ge()
97    }
98}
99
100impl Neg for SolverValue {
101    type Output = SolverValue;
102
103    fn neg(self) -> Self::Output {
104        match self {
105            SolverValue::WinIn(n) => SolverValue::LossIn(n),
106            SolverValue::LossIn(n) => SolverValue::WinIn(n),
107            SolverValue::Draw => SolverValue::Draw,
108            SolverValue::Unknown => SolverValue::Unknown,
109        }
110    }
111}
112
113pub fn solve<B: Board>(board: &B, depth: u32, rng: &mut impl Rng) -> MinimaxResult<SolverValue, B::Move> {
114    minimax(board, &SolverHeuristic, depth, rng)
115}
116
117pub fn solve_all_moves<B: Board>(board: &B, depth: u32) -> MinimaxResult<SolverValue, Vec<B::Move>> {
118    minimax_all_moves(board, &SolverHeuristic, depth)
119}
120
121pub fn solve_value<B: Board>(board: &B, depth: u32) -> SolverValue {
122    minimax_value(board, &SolverHeuristic, depth)
123}
124
125/// Return whether this board is a double forced draw, ie. no matter what either player does the game can only end in a draw.
126/// Returns `None` if the result is unknown.
127pub fn is_double_forced_draw(board: &impl Board, depth: u32) -> Option<bool> {
128    if let Some(outcome) = board.outcome() {
129        return Some(outcome == Outcome::Draw);
130    }
131    if depth == 0 {
132        return None;
133    }
134
135    let mut unknown = false;
136    let draw_or_unknown = board
137        .children()
138        .unwrap()
139        .all(|(_, child)| match is_double_forced_draw(&child, depth - 1) {
140            Some(draw) => draw,
141            None => {
142                unknown = true;
143                true
144            }
145        });
146
147    if draw_or_unknown && unknown {
148        None
149    } else {
150        Some(draw_or_unknown)
151    }
152}
153
154pub struct SolverBot<R: Rng> {
155    depth: u32,
156    rng: R,
157}
158
159impl<R: Rng> Debug for SolverBot<R> {
160    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
161        write!(f, "SolverBot {{ depth: {} }}", self.depth)
162    }
163}
164
165impl<R: Rng> SolverBot<R> {
166    pub fn new(depth: u32, rng: R) -> Self {
167        assert!(depth > 0);
168        SolverBot { depth, rng }
169    }
170}
171
172impl<B: Board, R: Rng> Bot<B> for SolverBot<R> {
173    fn select_move(&mut self, board: &B) -> Result<B::Move, BoardDone> {
174        board.check_done()?;
175        Ok(minimax(board, &SolverHeuristic, self.depth, &mut self.rng)
176            .best_move
177            .unwrap())
178    }
179}