fast_tak/
game.rs

1use std::ops::Not;
2
3use takparse::{Color, Direction, Move, MoveKind, Pattern, Piece, Square};
4
5use crate::{board::Board, error::PlayError, reserves::Reserves, stack::Stack};
6
7#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
8pub struct Game<const N: usize, const HALF_KOMI: i8> {
9    pub board: Board<N>,
10    pub to_move: Color,
11    pub white_reserves: Reserves<N>,
12    pub black_reserves: Reserves<N>,
13    pub ply: u16,
14    pub reversible_plies: u16,
15}
16
17// https://godbolt.org/z/434j733oW
18impl<const N: usize, const HALF_KOMI: i8> Clone for Game<N, HALF_KOMI> {
19    #[inline]
20    fn clone(&self) -> Self {
21        unsafe { std::mem::transmute_copy(self) }
22    }
23}
24
25impl<const N: usize, const HALF_KOMI: i8> Default for Game<N, HALF_KOMI>
26where
27    Reserves<N>: Default,
28{
29    fn default() -> Self {
30        Self {
31            board: Board::default(),
32            to_move: Color::White,
33            white_reserves: Reserves::default(),
34            black_reserves: Reserves::default(),
35            ply: u16::default(),
36            reversible_plies: u16::default(),
37        }
38    }
39}
40
41impl<const N: usize, const HALF_KOMI: i8> Game<N, HALF_KOMI> {
42    pub(crate) const fn is_swapped(&self) -> bool {
43        self.ply < 2
44    }
45
46    pub(crate) fn color_to_place(&self) -> Color {
47        if self.is_swapped() {
48            self.to_move.not()
49        } else {
50            self.to_move
51        }
52    }
53
54    pub(crate) fn get_reserves(&self) -> Reserves<N> {
55        match self.color_to_place() {
56            Color::White => self.white_reserves,
57            Color::Black => self.black_reserves,
58        }
59    }
60
61    fn dec_stones(&mut self) {
62        if (self.to_move == Color::White) ^ self.is_swapped() {
63            self.white_reserves.stones -= 1;
64        } else {
65            self.black_reserves.stones -= 1;
66        }
67    }
68
69    fn dec_caps(&mut self) {
70        match self.to_move {
71            Color::White => self.white_reserves.caps -= 1,
72            Color::Black => self.black_reserves.caps -= 1,
73        }
74    }
75
76    /// Play a move on the board.
77    ///
78    /// # Errors
79    ///
80    /// In case the move is invalid an error is returned and the game
81    /// might be in an invalid state.
82    pub fn play(&mut self, my_move: Move) -> Result<(), PlayError> {
83        match my_move.kind() {
84            MoveKind::Place(piece) => self.execute_place(my_move.square(), piece),
85            MoveKind::Spread(direction, pattern) => {
86                self.execute_spread(my_move.square(), direction, pattern)
87            }
88        }?;
89        self.update_reversible(my_move);
90        self.ply += 1;
91        self.to_move = self.to_move.not();
92        Ok(())
93    }
94
95    fn execute_place(&mut self, square: Square, piece: Piece) -> Result<(), PlayError> {
96        let Reserves { stones, caps } = self.get_reserves();
97        let is_swapped = self.is_swapped();
98        let color_to_place = self.color_to_place();
99        let stack = self.board.get_mut(square).ok_or(PlayError::OutOfBounds)?;
100        if !stack.is_empty() {
101            Err(PlayError::AlreadyOccupied)
102        } else if matches!(piece, Piece::Cap) && (caps == 0) {
103            Err(PlayError::NoCapstone)
104        } else if matches!(piece, Piece::Flat | Piece::Wall) && (stones == 0) {
105            Err(PlayError::NoStones)
106        } else if is_swapped && matches!(piece, Piece::Wall | Piece::Cap) {
107            Err(PlayError::OpeningNonFlat)
108        } else {
109            *stack = Stack::new(piece, color_to_place);
110            if matches!(piece, Piece::Flat | Piece::Wall) {
111                self.dec_stones();
112            } else {
113                self.dec_caps();
114            }
115            Ok(())
116        }
117    }
118
119    fn execute_spread(
120        &mut self,
121        square: Square,
122        direction: Direction,
123        pattern: Pattern,
124    ) -> Result<(), PlayError> {
125        let n = u8::try_from(N).unwrap();
126
127        let stack = self.board.get_mut(square).ok_or(PlayError::OutOfBounds)?;
128        let (_, top_color) = stack.top().ok_or(PlayError::EmptySquare)?;
129        if top_color != self.to_move {
130            return Err(PlayError::StackNotOwned);
131        }
132        let mut amount = pattern.count_pieces();
133        let (piece, colors) = stack.take::<N>(amount)?;
134        let mut carry = colors.into_iter();
135
136        // For each square in the spread, stack dropped pieces.
137        let mut pos = square;
138        for drop_count in pattern {
139            pos = pos
140                .checked_step(direction, n)
141                .ok_or(PlayError::SpreadOutOfBounds)?;
142
143            for color in carry.by_ref().take(drop_count as usize) {
144                amount -= 1;
145                // unwrap is sound since we checked whether it is on the board earlier
146                let Some(stack) = self.board.get_mut(pos) else {
147                    continue;
148                };
149                stack.stack(if amount > 0 { Piece::Flat } else { piece }, color)?;
150            }
151        }
152
153        assert_eq!(amount, 0);
154        assert_eq!(carry.next(), None);
155        Ok(())
156    }
157
158    fn update_reversible(&mut self, my_move: Move) {
159        // TODO detect smashes
160        if matches!(my_move.kind(), MoveKind::Place(_)) {
161            self.reversible_plies = 0;
162        } else {
163            self.reversible_plies += 1;
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use crate::{Game, PlayError, StackError, TakeError};
171
172    #[test]
173    fn square_out_of_bounds() {
174        let mut game = Game::<5, 0>::default();
175        let my_move = "a8".parse().unwrap();
176        assert_eq!(game.play(my_move), Err(PlayError::OutOfBounds));
177    }
178
179    #[test]
180    fn already_occupied() {
181        let mut game = Game::<5, 0>::from_ptn_moves(&["a2"]);
182        let my_move = "a2".parse().unwrap();
183        assert_eq!(game.play(my_move), Err(PlayError::AlreadyOccupied));
184    }
185
186    #[test]
187    fn no_capstone() {
188        let mut game = Game::<6, 0>::from_ptn_moves(&["a1", "b1", "Ca2", "b2"]);
189        let my_move = "Ca3".parse().unwrap();
190        assert_eq!(game.play(my_move), Err(PlayError::NoCapstone));
191    }
192
193    #[test]
194    fn no_stones() {
195        let mut game = Game::<3, 0>::from_ptn_moves(&[
196            "a1", "b1", "a2", "b2", "a2-", "a2", "b1<", "b1", "3a1>", "a1", "b1<", "c1", "3b1>",
197            "b1", "2a1>", "a1", "3b1<", "b1", "c2", "c3",
198        ]);
199        // technically the game is over so maybe this should be an error?
200        assert_eq!(game.play("c2<".parse().unwrap()), Ok(()));
201        let my_move = "c2".parse().unwrap();
202        assert_eq!(game.play(my_move), Err(PlayError::NoStones));
203    }
204
205    #[test]
206    fn opening_wall() {
207        let mut game = Game::<7, 0>::default();
208        let my_move = "Sa1".parse().unwrap();
209        assert_eq!(game.play(my_move), Err(PlayError::OpeningNonFlat));
210    }
211
212    #[test]
213    fn opening_capstone() {
214        let mut game = Game::<8, 0>::default();
215        let my_move = "Ca1".parse().unwrap();
216        assert_eq!(game.play(my_move), Err(PlayError::OpeningNonFlat));
217    }
218
219    #[test]
220    fn empty_square() {
221        let mut game = Game::<4, 0>::from_ptn_moves(&["a1", "b1", "a2"]);
222        let my_move = "b2<".parse().unwrap();
223        assert_eq!(game.play(my_move), Err(PlayError::EmptySquare));
224    }
225
226    #[test]
227    fn stack_not_owned() {
228        let mut game = Game::<5, 0>::from_ptn_moves(&[
229            "a1", "e5", "c3", "b2", "c2", "c1", "d1", "Cd2", "b1", "c1+", "Cc1", "e2", "c1+",
230        ]);
231        let my_move = "3c2<12".parse().unwrap();
232        assert_eq!(game.play(my_move), Err(PlayError::StackNotOwned));
233    }
234
235    #[test]
236    fn spread_out_of_bounds() {
237        let mut game = Game::<5, 0>::from_ptn_moves(&[
238            "a1", "e5", "c3", "b2", "c2", "c1", "d1", "Cd2", "b1", "c1+", "Cc1", "e2", "c1+", "a2",
239        ]);
240        let my_move = "3c2-111".parse().unwrap();
241        assert_eq!(game.play(my_move), Err(PlayError::SpreadOutOfBounds));
242    }
243
244    #[test]
245    fn stack_on_wall() {
246        let mut game = Game::<4, 0>::from_ptn_moves(&["a1", "a2", "Sb1"]);
247        let my_move = "a1>".parse().unwrap();
248        assert_eq!(
249            game.play(my_move),
250            Err(PlayError::StackError(StackError::Wall))
251        );
252    }
253
254    #[test]
255    fn stack_on_cap() {
256        let mut game = Game::<7, 0>::from_ptn_moves(&["a1", "a2", "Cb1"]);
257        let my_move = "a1>".parse().unwrap();
258        assert_eq!(
259            game.play(my_move),
260            Err(PlayError::StackError(StackError::Cap))
261        );
262    }
263
264    #[test]
265    fn carry_limit() {
266        let mut game =
267            Game::<3, 0>::from_ptn_moves(&["a1", "b1", "b1<", "b1", "2a1>", "a1", "3b1<", "b1"]);
268        let my_move = "4a1>".parse().unwrap();
269        assert_eq!(
270            game.play(my_move),
271            Err(PlayError::TakeError(TakeError::CarryLimit))
272        );
273    }
274
275    #[test]
276    fn take_more_than_stack() {
277        let mut game = Game::<3, 0>::from_ptn_moves(&["a1", "b1", "b1<", "b1"]);
278        let my_move = "3a1>".parse().unwrap();
279        assert_eq!(
280            game.play(my_move),
281            Err(PlayError::TakeError(TakeError::StackSize))
282        );
283    }
284}