fast_tak/
symm.rs

1use std::array;
2
3use takparse::{Direction, Move, MoveKind, Square};
4
5use crate::{board::Board, game::Game};
6
7pub trait Symmetry<const N: usize>: Sized {
8    fn symmetries(&self) -> [Self; 8];
9}
10
11impl<const N: usize> Symmetry<N> for Square {
12    fn symmetries(&self) -> [Self; 8] {
13        let n = u8::try_from(N).unwrap();
14        [
15            *self,
16            self.rotate(n),
17            self.rotate(n).rotate(n),
18            self.rotate(n).rotate(n).rotate(n),
19            self.mirror(n),
20            self.mirror(n).rotate(n),
21            self.mirror(n).rotate(n).rotate(n),
22            self.mirror(n).rotate(n).rotate(n).rotate(n),
23        ]
24    }
25}
26
27impl<const N: usize> Symmetry<N> for Direction {
28    fn symmetries(&self) -> [Self; 8] {
29        [
30            *self,
31            self.rotate(),
32            self.rotate().rotate(),
33            self.rotate().rotate().rotate(),
34            self.mirror(),
35            self.mirror().rotate(),
36            self.mirror().rotate().rotate(),
37            self.mirror().rotate().rotate().rotate(),
38        ]
39    }
40}
41
42impl<const N: usize> Symmetry<N> for Move {
43    fn symmetries(&self) -> [Self; 8] {
44        let square = self.square();
45        let kind = self.kind();
46        match kind {
47            MoveKind::Place(_) => {
48                Symmetry::<N>::symmetries(&square).map(|square| Self::new(square, kind))
49            }
50            MoveKind::Spread(direction, pattern) => zip(
51                Symmetry::<N>::symmetries(&square),
52                Symmetry::<N>::symmetries(&direction),
53            )
54            .map(|(square, direction)| Self::new(square, MoveKind::Spread(direction, pattern))),
55        }
56    }
57}
58
59impl<const N: usize> Symmetry<N> for Board<N> {
60    fn symmetries(&self) -> [Self; 8] {
61        let n = u8::try_from(N).unwrap();
62        array::from_fn(|i| {
63            let mut board = Self::default();
64            for x in 0..n {
65                for y in 0..n {
66                    let square = Square::new(y, x);
67                    let target = Symmetry::<N>::symmetries(&square)[i];
68                    *board.get_mut(target).unwrap() = *self.get(square).unwrap();
69                }
70            }
71            board
72        })
73    }
74}
75
76impl<const N: usize, const HALF_KOMI: i8> Symmetry<N> for Game<N, HALF_KOMI> {
77    fn symmetries(&self) -> [Self; 8] {
78        let mut iter = self.board.symmetries().into_iter();
79        array::from_fn(|_| {
80            let mut game = self.clone();
81            game.board = iter.next().unwrap();
82            game
83        })
84    }
85}
86
87#[inline]
88fn zip<const N: usize, A: Copy, B: Copy>(a: [A; N], b: [B; N]) -> [(A, B); N] {
89    array::from_fn(|i| (a[i], b[i]))
90}
91
92impl<const N: usize, const HALF_KOMI: i8> Game<N, HALF_KOMI> {
93    #[must_use]
94    #[allow(clippy::missing_panics_doc)]
95    pub fn canonical(mut self) -> Self {
96        self.board = self.board.symmetries().into_iter().min().unwrap();
97        self
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use crate::{reserves::Reserves, symm::Symmetry, Game, GameResult, PlayError};
104
105    fn symmetrical_boards<const N: usize>(seed: usize) -> Result<(), PlayError>
106    where
107        Reserves<N>: Default,
108    {
109        let mut game: Game<N, 0> = Game::default();
110        let mut moves = Vec::new();
111        while matches!(game.result(), GameResult::Ongoing) {
112            moves.clear();
113            game.possible_moves(&mut moves);
114            let my_move = moves[seed % moves.len()];
115
116            let mut game_syms = game.symmetries();
117            let move_syms = Symmetry::<N>::symmetries(&my_move);
118
119            game.play(my_move)?;
120            game_syms
121                .iter_mut()
122                .zip(move_syms)
123                .try_for_each(|(game, m)| game.play(m))?;
124
125            let result = game.result();
126            assert!(game_syms.iter().all(|game| game.result() == result));
127        }
128        Ok(())
129    }
130
131    macro_rules! symmetrical_boards_seeded {
132        [$($name:ident $seed:literal),*] => {
133            $(
134                #[test]
135                fn $name() {
136                    symmetrical_boards::<3>($seed).unwrap();
137                    symmetrical_boards::<4>($seed).unwrap();
138                    symmetrical_boards::<5>($seed).unwrap();
139                    symmetrical_boards::<6>($seed).unwrap();
140                    symmetrical_boards::<7>($seed).unwrap();
141                    symmetrical_boards::<8>($seed).unwrap();
142                }
143            )*
144        };
145    }
146
147    symmetrical_boards_seeded![
148        symmetrical_boards_5915587277 5_915_587_277,
149        symmetrical_boards_1500450271 1_500_450_271,
150        symmetrical_boards_3267000013 3_267_000_013,
151        symmetrical_boards_5754853343 5_754_853_343,
152        symmetrical_boards_4093082899 4_093_082_899,
153        symmetrical_boards_9576890767 9_576_890_767,
154        symmetrical_boards_3628273133 3_628_273_133,
155        symmetrical_boards_2860486313 2_860_486_313,
156        symmetrical_boards_5463458053 5_463_458_053,
157        symmetrical_boards_3367900313 3_367_900_313
158    ];
159}