fast-tak 0.4.2

Implementation of the abstract strategy board game Tak
Documentation
use std::array;

use takparse::{Direction, Move, MoveKind, Square};

use crate::{board::Board, game::Game};

pub trait Symmetry<const N: usize>: Sized {
    fn symmetries(&self) -> [Self; 8];
}

impl<const N: usize> Symmetry<N> for Square {
    fn symmetries(&self) -> [Self; 8] {
        let n = u8::try_from(N).unwrap();
        [
            *self,
            self.rotate(n),
            self.rotate(n).rotate(n),
            self.rotate(n).rotate(n).rotate(n),
            self.mirror(n),
            self.mirror(n).rotate(n),
            self.mirror(n).rotate(n).rotate(n),
            self.mirror(n).rotate(n).rotate(n).rotate(n),
        ]
    }
}

impl<const N: usize> Symmetry<N> for Direction {
    fn symmetries(&self) -> [Self; 8] {
        [
            *self,
            self.rotate(),
            self.rotate().rotate(),
            self.rotate().rotate().rotate(),
            self.mirror(),
            self.mirror().rotate(),
            self.mirror().rotate().rotate(),
            self.mirror().rotate().rotate().rotate(),
        ]
    }
}

impl<const N: usize> Symmetry<N> for Move {
    fn symmetries(&self) -> [Self; 8] {
        let square = self.square();
        let kind = self.kind();
        match kind {
            MoveKind::Place(_) => {
                Symmetry::<N>::symmetries(&square).map(|square| Self::new(square, kind))
            }
            MoveKind::Spread(direction, pattern) => zip(
                Symmetry::<N>::symmetries(&square),
                Symmetry::<N>::symmetries(&direction),
            )
            .map(|(square, direction)| Self::new(square, MoveKind::Spread(direction, pattern))),
        }
    }
}

impl<const N: usize> Symmetry<N> for Board<N> {
    fn symmetries(&self) -> [Self; 8] {
        let n = u8::try_from(N).unwrap();
        array::from_fn(|i| {
            let mut board = Self::default();
            for x in 0..n {
                for y in 0..n {
                    let square = Square::new(y, x);
                    let target = Symmetry::<N>::symmetries(&square)[i];
                    *board.get_mut(target).unwrap() = *self.get(square).unwrap();
                }
            }
            board
        })
    }
}

impl<const N: usize, const HALF_KOMI: i8> Symmetry<N> for Game<N, HALF_KOMI> {
    fn symmetries(&self) -> [Self; 8] {
        let mut iter = self.board.symmetries().into_iter();
        array::from_fn(|_| {
            let mut game = self.clone();
            game.board = iter.next().unwrap();
            game
        })
    }
}

#[inline]
fn zip<const N: usize, A: Copy, B: Copy>(a: [A; N], b: [B; N]) -> [(A, B); N] {
    array::from_fn(|i| (a[i], b[i]))
}

impl<const N: usize, const HALF_KOMI: i8> Game<N, HALF_KOMI> {
    #[must_use]
    #[allow(clippy::missing_panics_doc)]
    pub fn canonical(mut self) -> Self {
        self.board = self.board.symmetries().into_iter().min().unwrap();
        self
    }
}

#[cfg(test)]
mod tests {
    use crate::{reserves::Reserves, symm::Symmetry, Game, GameResult, PlayError};

    fn symmetrical_boards<const N: usize>(seed: usize) -> Result<(), PlayError>
    where
        Reserves<N>: Default,
    {
        let mut game: Game<N, 0> = Game::default();
        let mut moves = Vec::new();
        while matches!(game.result(), GameResult::Ongoing) {
            moves.clear();
            game.possible_moves(&mut moves);
            let my_move = moves[seed % moves.len()];

            let mut game_syms = game.symmetries();
            let move_syms = Symmetry::<N>::symmetries(&my_move);

            game.play(my_move)?;
            game_syms
                .iter_mut()
                .zip(move_syms)
                .try_for_each(|(game, m)| game.play(m))?;

            let result = game.result();
            assert!(game_syms.iter().all(|game| game.result() == result));
        }
        Ok(())
    }

    macro_rules! symmetrical_boards_seeded {
        [$($name:ident $seed:literal),*] => {
            $(
                #[test]
                fn $name() {
                    symmetrical_boards::<3>($seed).unwrap();
                    symmetrical_boards::<4>($seed).unwrap();
                    symmetrical_boards::<5>($seed).unwrap();
                    symmetrical_boards::<6>($seed).unwrap();
                    symmetrical_boards::<7>($seed).unwrap();
                    symmetrical_boards::<8>($seed).unwrap();
                }
            )*
        };
    }

    symmetrical_boards_seeded![
        symmetrical_boards_5915587277 5_915_587_277,
        symmetrical_boards_1500450271 1_500_450_271,
        symmetrical_boards_3267000013 3_267_000_013,
        symmetrical_boards_5754853343 5_754_853_343,
        symmetrical_boards_4093082899 4_093_082_899,
        symmetrical_boards_9576890767 9_576_890_767,
        symmetrical_boards_3628273133 3_628_273_133,
        symmetrical_boards_2860486313 2_860_486_313,
        symmetrical_boards_5463458053 5_463_458_053,
        symmetrical_boards_3367900313 3_367_900_313
    ];
}