board_game/
board.rs

1use std::fmt::{Debug, Display, Formatter};
2use std::hash::Hash;
3use std::marker::PhantomData;
4use std::ops::ControlFlow;
5
6use internal_iterator::InternalIterator;
7use rand::Rng;
8
9use crate::symmetry::Symmetry;
10
11// TODO consider adding pseudo-legal movegen and a play variant that can reject non-available moves
12
13/// One of the two players.
14#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
15pub enum Player {
16    A,
17    B,
18}
19
20/// The absolute outcome for a game.
21#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
22pub enum Outcome {
23    WonBy(Player),
24    Draw,
25}
26
27/// The error returned when a [Board] is done, causing the called function to not work.
28#[derive(Debug, Copy, Clone, Eq, PartialEq)]
29pub struct BoardDone;
30
31/// The error returned when [Board::play] fails.
32#[derive(Debug, Copy, Clone, Eq, PartialEq)]
33pub enum PlayError {
34    BoardDone,
35    UnavailableMove,
36}
37
38/// The main trait of this crate. Represents the state of a game.
39///
40/// Each game implementation is supposed to provide its own constructors to allow for customizable start positions.
41///
42/// Implementing [Board] also requires [BoardSymmetry] and [BoardMoves] to be implemented.
43///
44/// Additionally it can be useful to implement [Hash] for both the board and move types, some utility functions require those extra bounds.
45/// Do **not** implement [Copy] for the board, since the type is mutated by some of the functions and this can quickly lead to confusion.
46pub trait Board: 'static + Debug + Display + Clone + Eq + Send + Sync + BoardSymmetry<Self>
47where
48    for<'a> Self: BoardMoves<'a, Self>,
49{
50    /// The type used to represent moves on this board.
51    type Move: Debug + Display + Eq + Copy + Send + Sync;
52
53    /// Return the next player to make a move.
54    /// If the board is done this is the player that did not play the last move for consistency.
55    fn next_player(&self) -> Player;
56
57    /// Return whether the given move is available.
58    fn is_available_move(&self, mv: Self::Move) -> Result<bool, BoardDone>;
59
60    /// Pick a random move from the `available_moves` with a uniform distribution.
61    /// Can be overridden for better performance.
62    fn random_available_move(&self, rng: &mut impl Rng) -> Result<Self::Move, BoardDone> {
63        let count = self.available_moves()?.count();
64        let index = rng.gen_range(0..count);
65        // we can unwrap since we just picked the index to fall in the range
66        //   and we can assume the number of available moves did not change
67        Ok(self.available_moves()?.nth(index).unwrap())
68    }
69
70    // TODO is play with an invalid move allowed to leave the board in an inconsistent state?
71    /// Play the move `mv`, modifying this board.
72    fn play(&mut self, mv: Self::Move) -> Result<(), PlayError>;
73
74    /// Clone this board, play `mv` on it and return the new board.
75    /// Can be overridden for better performance.
76    fn clone_and_play(&self, mv: Self::Move) -> Result<Self, PlayError> {
77        let mut next = self.clone();
78        next.play(mv)?;
79        Ok(next)
80    }
81
82    /// The outcome of this board, is `None` when this games is not done yet.
83    fn outcome(&self) -> Option<Outcome>;
84
85    /// Whether this games is done.
86    fn is_done(&self) -> bool {
87        self.outcome().is_some()
88    }
89
90    /// Whether the player who plays a move can lose by playing that move.
91    /// Symbolically whether `b.won_by() == Some(Winner::Player(b.next_player()))` can ever be true.
92    /// This may be pessimistic, returning `true` is always correct.
93    fn can_lose_after_move() -> bool;
94
95    /// Returns
96    /// * `Err(BoardDone)` if `self.is_done()`
97    /// * `Ok(())` otherwise.
98    fn check_done(&self) -> Result<(), BoardDone> {
99        match self.is_done() {
100            true => Err(BoardDone),
101            false => Ok(()),
102        }
103    }
104
105    /// Returns
106    /// * `Err(BoardDone)` if `self.is_done()`
107    /// * `Err(UnavailableMove)` if `!self.is_available_move(mv)`
108    /// * `Ok(())` otherwise.
109    fn check_can_play(&self, mv: Self::Move) -> Result<(), PlayError> {
110        match self.is_available_move(mv)? {
111            true => Ok(()),
112            false => Err(PlayError::UnavailableMove),
113        }
114    }
115
116    /// The same as `self.play(self.random_available_move(rng))`, but needs less error handling.
117    /// Can be overridden for better performance by skipping the valid move check.
118    fn play_random_available_move(&mut self, rng: &mut impl Rng) -> Result<(), BoardDone> {
119        self.play(self.random_available_move(rng)?).unwrap();
120        Ok(())
121    }
122
123    /// The same as `self.available_moves().map(|mv| self.clone_and_play(mv))`, but needs less error handling.
124    /// Can be overridden for better performance by skipping the valid move check.
125    fn children(&self) -> Result<BoardChildrenIterator<Self>, BoardDone> {
126        BoardChildrenIterator::new(self)
127    }
128}
129
130/// A marker trait for boards which guarantee that [Board::next_player] flips after a move is played.
131pub trait Alternating {}
132
133/// Auto trait for [Board]s that also implement [Alternating].
134pub trait AltBoard: Board + Alternating {}
135
136impl<B: Board + Alternating> AltBoard for B {}
137
138/// A helper trait to get the correct lifetimes for [BoardMoves::available_moves].
139/// This is a workaround to get generic associated types, See <https://github.com/rust-lang/rust/issues/44265>.
140pub trait BoardMoves<'a, B: Board> {
141    type AllMovesIterator: InternalIterator<Item = B::Move> + Clone;
142    type AvailableMovesIterator: InternalIterator<Item = B::Move> + Clone;
143
144    /// All theoretically possible moves, for any possible board.
145    /// Moves returned by `available_moves` will always be a subset of these moves.
146    /// The order of these moves does not need to match the order from `available_moves`.
147    fn all_possible_moves() -> Self::AllMovesIterator;
148
149    /// Return an iterator over available moves, is always nonempty. No guarantees are made about the ordering except
150    /// that it stays consistent when the board is not modified.
151    /// Panics if this board is done.
152    fn available_moves(&'a self) -> Result<Self::AvailableMovesIterator, BoardDone>;
153}
154
155/// Utility macro to implement [BoardSymmetry] for boards with [UnitSymmetry](crate::symmetry::UnitSymmetry).
156#[macro_export]
157macro_rules! impl_unit_symmetry_board {
158    ($B:ty) => {
159        impl $crate::board::BoardSymmetry<$B> for $B {
160            type Symmetry = $crate::symmetry::UnitSymmetry;
161            type CanonicalKey = ();
162
163            fn map(&self, _: Self::Symmetry) -> Self {
164                self.clone()
165            }
166
167            fn map_move(
168                &self,
169                _: Self::Symmetry,
170                mv: <$B as $crate::board::Board>::Move,
171            ) -> <$B as $crate::board::Board>::Move {
172                mv
173            }
174
175            fn canonical_key(&self) -> Self::CanonicalKey {}
176        }
177    };
178}
179
180/// A helper trait that describes the ways in which a board is symmetric.
181/// For boards without any symmetry, the macro [impl_unit_symmetry_board] can be used to reduce boilerplate.
182/// This is a separate trait specifically to allow this trick to work.
183pub trait BoardSymmetry<B: Board>: Sized {
184    /// The type used to represent symmetries.
185    type Symmetry: Symmetry;
186
187    /// The type used by [Self::canonical_key].
188    type CanonicalKey: Ord;
189
190    /// Map this board under the given symmetry.
191    fn map(&self, sym: Self::Symmetry) -> Self;
192
193    /// Map a move under the given symmetry.
194    fn map_move(&self, sym: Self::Symmetry, mv: B::Move) -> B::Move;
195
196    /// Extract **all** of the state from this board that can potentially change when calling [Self::map].
197    /// This is used by [Self::canonicalize] to determine which symmetry ends up as the canonical one for the given board.
198    fn canonical_key(&self) -> Self::CanonicalKey;
199
200    /// Convert this board to a canonical version,
201    /// by mapping it with the symmetry that results in the smallest [Self::canonical_key].
202    ///
203    /// This implies that the returned board is the same for any symmetry of this board,
204    /// which can be useful for deduplication in things like transposition takes.
205    ///
206    /// Implementations are free to override this function if they can provide a faster one.
207    fn canonicalize(&self) -> Self {
208        Self::Symmetry::all()
209            .iter()
210            .map(|&sym| self.map(sym))
211            .min_by_key(|cand| cand.canonical_key())
212            .unwrap()
213    }
214}
215
216impl Player {
217    pub const BOTH: [Player; 2] = [Player::A, Player::B];
218
219    pub fn other(self) -> Player {
220        match self {
221            Player::A => Player::B,
222            Player::B => Player::A,
223        }
224    }
225
226    pub fn index(self) -> u8 {
227        match self {
228            Player::A => 0,
229            Player::B => 1,
230        }
231    }
232
233    pub fn to_char(self) -> char {
234        match self {
235            Player::A => 'A',
236            Player::B => 'B',
237        }
238    }
239
240    pub fn sign<V: num_traits::One + std::ops::Neg<Output = V>>(self, pov: Player) -> V {
241        if self == pov {
242            V::one()
243        } else {
244            -V::one()
245        }
246    }
247}
248
249/// A convenient type to use for the iterator returned by [BoardMoves::all_possible_moves].
250#[derive(Debug, Clone)]
251pub struct AllMovesIterator<B: Board>(PhantomData<B>);
252
253impl<B: Board> Default for AllMovesIterator<B> {
254    fn default() -> Self {
255        AllMovesIterator(PhantomData)
256    }
257}
258
259/// A convenient type to use for the iterator returned by [BoardMoves::available_moves].
260#[derive(Debug, Clone)]
261pub struct AvailableMovesIterator<'a, B: Board> {
262    board: &'a B,
263}
264
265/// A helper struct function can be used to implement [InternalIterator] for [AvailableMovesIterator].
266/// based on [BoardMoves::all_possible_moves] and [Board::is_available_move].
267/// This may be a lot slower then directly generating the available moves.
268#[derive(Debug, Clone)]
269pub struct BruteforceMoveIterator<'a, B: Board> {
270    board: &'a B,
271}
272
273impl<'a, B: Board> AvailableMovesIterator<'a, B> {
274    pub fn new(board: &'a B) -> Result<Self, BoardDone> {
275        board.check_done()?;
276        Ok(AvailableMovesIterator { board })
277    }
278
279    pub fn board(&self) -> &'a B {
280        self.board
281    }
282}
283
284impl<'a, B: Board> BruteforceMoveIterator<'a, B> {
285    pub fn new(board: &'a B) -> Result<Self, BoardDone> {
286        board.check_done()?;
287        Ok(BruteforceMoveIterator { board })
288    }
289}
290
291impl<'a, B: Board> InternalIterator for BruteforceMoveIterator<'a, B> {
292    type Item = B::Move;
293
294    fn try_for_each<R, F>(self, mut f: F) -> ControlFlow<R>
295    where
296        F: FnMut(Self::Item) -> ControlFlow<R>,
297    {
298        B::all_possible_moves().try_for_each(|mv: B::Move| {
299            // we can unwrap here because the constructor has already checked that the board is not done
300            if self.board.is_available_move(mv).unwrap() {
301                f(mv)
302            } else {
303                ControlFlow::Continue(())
304            }
305        })
306    }
307}
308
309pub struct BoardChildrenIterator<'a, B: Board> {
310    board: &'a B,
311    available: <B as BoardMoves<'a, B>>::AvailableMovesIterator,
312}
313
314impl<'a, B: Board> BoardChildrenIterator<'a, B> {
315    pub fn new(board: &'a B) -> Result<Self, BoardDone> {
316        let available = board.available_moves()?;
317        Ok(BoardChildrenIterator { board, available })
318    }
319}
320
321impl<'a, B: Board> InternalIterator for BoardChildrenIterator<'a, B> {
322    type Item = (B::Move, B);
323
324    fn try_for_each<R, F>(self, f: F) -> ControlFlow<R>
325    where
326        F: FnMut(Self::Item) -> ControlFlow<R>,
327    {
328        let board = self.board;
329        self.available
330            .map(|mv: B::Move| (mv, board.clone_and_play(mv).unwrap()))
331            .try_for_each(f)
332    }
333}
334
335impl<'a, B: Board> Debug for BoardChildrenIterator<'a, B> {
336    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
337        f.debug_struct("BoardChildrenIterator")
338            .field("board", self.board)
339            .finish_non_exhaustive()
340    }
341}
342
343impl<'a, B: Board> Clone for BoardChildrenIterator<'a, B>
344where
345    <B as BoardMoves<'a, B>>::AvailableMovesIterator: Clone,
346{
347    fn clone(&self) -> Self {
348        BoardChildrenIterator {
349            board: self.board,
350            available: self.available.clone(),
351        }
352    }
353}
354
355impl From<BoardDone> for PlayError {
356    fn from(_: BoardDone) -> Self {
357        PlayError::BoardDone
358    }
359}