arimaa-engine 0.1.0

A bitboard-based Arimaa game engine: move generation, game state, and Zobrist hashing.
Documentation
use anyhow::anyhow;
use std::fmt;
use std::str::FromStr;

use super::Direction;
use super::bits::{for_each_bit_idx, single_bit_index};
use super::constants::{ASCII_LETTER_A, BOARD_HEIGHT, BOARD_SIZE, BOARD_WIDTH};

#[derive(Hash, Eq, PartialEq, Clone, Copy, Default, Ord, PartialOrd)]
pub struct Square(u8);

impl Square {
    pub fn new(column: char, row: usize) -> Self {
        let index = (column as u8 - ASCII_LETTER_A) + (BOARD_HEIGHT - row) as u8 * 8;
        Square(index)
    }

    pub fn from_index(index: u8) -> Self {
        Square(index)
    }

    pub fn from_bit_board(board: u64) -> Self {
        Square(single_bit_index(board as u128) as u8)
    }

    pub fn from_multiple_bits(board: u64) -> Vec<Self> {
        let mut squares = Vec::with_capacity(board.count_ones() as usize);

        for_each_bit_idx(board, |idx| squares.push(Square::from_index(idx as u8)));

        squares
    }

    pub fn as_bit_board(&self) -> u64 {
        1 << self.0
    }

    pub fn get_index(&self) -> usize {
        self.0 as usize
    }

    pub fn invert(&self) -> Self {
        Square((BOARD_HEIGHT * BOARD_WIDTH) as u8 - 1 - self.0)
    }

    pub fn vertical_symmetry(&self) -> Self {
        Square(self.0 ^ 7)
    }

    pub fn column_char(&self) -> char {
        let index = self.0 as usize;
        let column = index % BOARD_WIDTH;

        (ASCII_LETTER_A + column as u8) as char
    }

    pub fn row(&self) -> u8 {
        let index = self.0 as usize;
        let row = BOARD_HEIGHT - (index / BOARD_WIDTH);
        row as u8
    }

    pub fn shift_in_direction(&self, direction: Direction) -> Self {
        let new_index = match direction {
            Direction::Up => self.0 - BOARD_WIDTH as u8,
            Direction::Right => self.0 + 1,
            Direction::Down => self.0 + BOARD_WIDTH as u8,
            Direction::Left => self.0 - 1,
        };

        Self::from_index(new_index)
    }

    pub fn can_shift_in_direction(&self, direction: Direction) -> bool {
        match direction {
            Direction::Up => self.0 >= BOARD_WIDTH as u8,
            Direction::Right => self.0 % BOARD_WIDTH as u8 != 7,
            Direction::Down => (self.0 + BOARD_WIDTH as u8) < (BOARD_SIZE as u8),
            Direction::Left => !self.0.is_multiple_of(BOARD_WIDTH as u8),
        }
    }

    pub fn is_adjacent(&self, square: &Self) -> bool {
        let column_diff = isize::abs(self.column_char() as isize - square.column_char() as isize);
        let row_diff = i8::abs(self.row() as i8 - square.row() as i8);

        column_diff == 1 && row_diff == 1
    }
}

impl fmt::Display for Square {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let column = self.column_char();
        let row = self.row();

        write!(f, "{column}{row}", column = column, row = row)
    }
}

impl fmt::Debug for Square {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self)
    }
}

impl FromStr for Square {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let chars: Vec<char> = s.chars().collect();

        if chars.len() == 2 {
            let column = chars[0];
            let row = chars[1];

            if let Ok(row) = row.to_string().parse() {
                let column_as_num = column as u8 - ASCII_LETTER_A + 1;
                if column_as_num >= 1 && column_as_num <= BOARD_WIDTH as u8 && (1..=BOARD_HEIGHT).contains(&row) {
                    return Ok(Square::new(column, row));
                }
            }
        }

        Err(anyhow!("Invalid value for square"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn assert_can_shift_in_direction(invalid_squares: &[&str], direction: Direction) {
        for idx in 0..64 {
            let square = Square::from_index(idx);
            if invalid_squares.contains(&square.to_string().as_str()) {
                assert!(!square.can_shift_in_direction(direction));
            } else {
                assert!(square.can_shift_in_direction(direction));
            }
        }
    }

    #[test]
    fn test_can_shift_in_direction_up() {
        let invalid_squares = ["a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8"];
        let direction = Direction::Up;

        assert_can_shift_in_direction(&invalid_squares, direction);
    }

    #[test]
    fn test_can_shift_in_direction_right() {
        let invalid_squares = ["h8", "h7", "h6", "h5", "h4", "h3", "h2", "h1"];
        let direction = Direction::Right;

        assert_can_shift_in_direction(&invalid_squares, direction);
    }

    #[test]
    fn test_can_shift_in_direction_down() {
        let invalid_squares = ["a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1"];
        let direction = Direction::Down;

        assert_can_shift_in_direction(&invalid_squares, direction);
    }

    #[test]
    fn test_can_shift_in_direction_left() {
        let invalid_squares = ["a8", "a7", "a6", "a5", "a4", "a3", "a2", "a1"];
        let direction = Direction::Left;

        assert_can_shift_in_direction(&invalid_squares, direction);
    }
}