arimaa_engine_step 1.0.1

A step based engine for the board game Arimaa.
Documentation
use anyhow::Result;
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;

use super::zobrist::Zobrist;
use super::{GameState, Phase, PieceBoard, PieceBoardState, PlayPhase};
use super::{List, Piece, Square};
use super::{BOARD_HEIGHT, BOARD_WIDTH};

impl Display for GameState {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let move_number = self.move_number();
        let curr_player = if self.is_p1_turn_to_move() { "g" } else { "s" };
        let piece_board = &self.piece_board();
        writeln!(f, "{}{}", move_number, curr_player)?;

        writeln!(f, " +-----------------+")?;

        for row_idx in 0..BOARD_HEIGHT {
            write!(f, "{}|", BOARD_HEIGHT - row_idx)?;
            for col_idx in 0..BOARD_WIDTH {
                let idx = (row_idx * BOARD_WIDTH + col_idx) as u8;
                let square = Square::from_index(idx);
                let letter = if let Some(piece) = piece_board.piece_type_at_square(&square) {
                    let is_p1_piece = is_p1_piece(square.as_bit_board(), piece_board);
                    convert_piece_to_letter(&piece, is_p1_piece)
                } else if idx == 18 || idx == 21 || idx == 42 || idx == 45 {
                    "x".to_string()
                } else {
                    " ".to_string()
                };

                write!(f, " {}", letter)?;
            }
            writeln!(f, " |")?;
        }

        writeln!(f, " +-----------------+")?;
        writeln!(f, "   a b c d e f g h")?;

        Ok(())
    }
}

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

    fn from_str(s: &str) -> Result<Self> {
        let lines: Vec<_> = s
            .split('|')
            .enumerate()
            .filter(|(i, _)| i % 2 == 1)
            .map(|(_, s)| s)
            .collect();
        let regex = regex::Regex::new(r"^\s*(\d+)([gswb])").unwrap();

        let (move_number, p1_turn_to_move) = regex
            .captures(s.split('|').find(|_| true).unwrap())
            .map_or((2, true), |c| {
                (
                    c.get(1).unwrap().as_str().parse().unwrap(),
                    c.get(2).unwrap().as_str() != "s" && c.get(2).unwrap().as_str() != "b",
                )
            });

        let mut p1_pieces = 0;
        let mut elephants = 0;
        let mut camels = 0;
        let mut horses = 0;
        let mut dogs = 0;
        let mut cats = 0;
        let mut rabbits = 0;

        for (row_idx, line) in lines.iter().enumerate() {
            for (col_idx, charr) in line
                .chars()
                .enumerate()
                .filter(|(i, _)| i % 2 == 1)
                .map(|(_, s)| s)
                .enumerate()
            {
                let idx = (row_idx * BOARD_WIDTH + col_idx) as u8;
                let square = Square::from_index(idx);
                if let Some((piece, is_p1)) = convert_char_to_piece(charr) {
                    let square_bit = square.as_bit_board();

                    match piece {
                        Piece::Elephant => elephants |= square_bit,
                        Piece::Camel => camels |= square_bit,
                        Piece::Horse => horses |= square_bit,
                        Piece::Dog => dogs |= square_bit,
                        Piece::Cat => cats |= square_bit,
                        Piece::Rabbit => rabbits |= square_bit,
                    }

                    if is_p1 {
                        p1_pieces |= square_bit;
                    }
                }
            }
        }

        let piece_board =
            PieceBoard::new(p1_pieces, elephants, camels, horses, dogs, cats, rabbits);
        let hash = Zobrist::from_piece_board(piece_board.piece_board(), p1_turn_to_move, 0);
        let hash_history = List::new();
        let hash_history = hash_history.append(hash);

        Ok(GameState::new(
            p1_turn_to_move,
            move_number,
            Phase::PlayPhase(PlayPhase::initial(hash, hash_history)),
            piece_board,
            hash,
        ))
    }
}

fn convert_char_to_piece(c: char) -> Option<(Piece, bool)> {
    let is_p1 = c.is_uppercase();

    let piece = match c {
        'E' | 'e' => Some(Piece::Elephant),
        'M' | 'm' => Some(Piece::Camel),
        'H' | 'h' => Some(Piece::Horse),
        'D' | 'd' => Some(Piece::Dog),
        'C' | 'c' => Some(Piece::Cat),
        'R' | 'r' => Some(Piece::Rabbit),
        _ => None,
    };

    piece.map(|p| (p, is_p1))
}

fn is_p1_piece(square_bit: u64, piece_board: &PieceBoardState) -> bool {
    let p1_piece_mask = piece_board.player_piece_mask(true);
    (square_bit & p1_piece_mask) != 0
}

pub fn convert_piece_to_letter(piece: &Piece, is_p1: bool) -> String {
    let letter = match piece {
        Piece::Elephant => "E",
        Piece::Camel => "M",
        Piece::Horse => "H",
        Piece::Dog => "D",
        Piece::Cat => "C",
        Piece::Rabbit => "R",
    };

    if is_p1 {
        letter.to_string()
    } else {
        letter.to_lowercase()
    }
}

#[cfg(test)]
mod tests {
    use super::super::{take_actions, GameState, Piece};

    fn place_major_pieces(game_state: GameState) -> GameState {
        take_actions!(game_state => h, c, d, m, e, d, c, h)
    }

    fn place_8_rabbits(game_state: GameState) -> GameState {
        take_actions!(game_state => r, r, r, r, r, r, r, r)
    }

    fn initial_play_state() -> GameState {
        let game_state = GameState::initial();
        let game_state = place_8_rabbits(game_state);
        let game_state = place_major_pieces(game_state);

        let game_state = place_major_pieces(game_state);
        place_8_rabbits(game_state)
    }

    #[test]
    fn test_gamestate_fromstr() {
        let game_state: GameState = "
            +-----------------+
            8|   r   r r   r   |
            7| m   h     e   c |
            6|   r x r r x r   |
            5| h   d     c   d |
            4| E   H         M |
            3|   R x R R H R   |
            2| D   C     C   D |
            1|   R   R R   R   |
            +-----------------+
               a b c d e f g h"
            .parse()
            .unwrap();

        let piece_board = game_state.piece_board();
        assert_eq!(
            piece_board.bits_for_piece(Piece::Rabbit, true),
            0b__01011010__00000000__01011010__00000000__00000000__00000000__00000000__00000000
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Cat, true),
            0b__00000000__00100100__00000000__00000000__00000000__00000000__00000000__00000000
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Dog, true),
            0b__00000000__10000001__00000000__00000000__00000000__00000000__00000000__00000000
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Horse, true),
            0b__00000000__00000000__00100000__00000100__00000000__00000000__00000000__00000000
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Camel, true),
            0b__00000000__00000000__00000000__10000000__00000000__00000000__00000000__00000000
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Elephant, true),
            0b__00000000__00000000__00000000__00000001__00000000__00000000__00000000__00000000
        );

        assert_eq!(
            piece_board.bits_for_piece(Piece::Rabbit, false),
            0b__00000000__00000000__00000000__00000000__00000000__01011010__00000000__01011010
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Cat, false),
            0b__00000000__00000000__00000000__00000000__00100000__00000000__10000000__00000000
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Dog, false),
            0b__00000000__00000000__00000000__00000000__10000100__00000000__00000000__00000000
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Horse, false),
            0b__00000000__00000000__00000000__00000000__00000001__00000000__00000100__00000000
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Camel, false),
            0b__00000000__00000000__00000000__00000000__00000000__00000000__00000001__00000000
        );
        assert_eq!(
            piece_board.bits_for_piece(Piece::Elephant, false),
            0b__00000000__00000000__00000000__00000000__00000000__00000000__00100000__00000000
        );
    }

    #[test]
    fn test_gamestate_fromstr_move_number_default() {
        let game_state: GameState = "
            +-----------------+
            8|   r   r r   r   |
            7| m   h     e   c |
            6|   r x r r x r   |
            5| h   d     c   d |
            4| E   H         M |
            3|   R x R R H R   |
            2| D   C     C   D |
            1|   R   R R   R   |
            +-----------------+
               a b c d e f g h"
            .parse()
            .unwrap();

        assert_eq!(game_state.move_number(), 2);
        assert!(game_state.is_p1_turn_to_move());
    }

    #[test]
    fn test_gamestate_fromstr_move_number() {
        let game_state: GameState = "
            5s
            +-----------------+
            8|   r   r r   r   |
            7| m   h     e   c |
            6|   r x r r x r   |
            5| h   d     c   d |
            4| E   H         M |
            3|   R x R R H R   |
            2| D   C     C   D |
            1|   R   R R   R   |
            +-----------------+
               a b c d e f g h"
            .parse()
            .unwrap();

        assert_eq!(game_state.move_number(), 5);
        assert!(!game_state.is_p1_turn_to_move());
    }

    #[test]
    fn test_gamestate_fromstr_player() {
        let game_state: GameState = "
            176b
            +-----------------+
            8|   r   r r   r   |
            7| m   h     e   c |
            6|   r x r r x r   |
            5| h   d     c   d |
            4| E   H         M |
            3|   R x R R H R   |
            2| D   C     C   D |
            1|   R   R R   R   |
            +-----------------+
               a b c d e f g h"
            .parse()
            .unwrap();

        assert_eq!(game_state.move_number(), 176);
        assert!(!game_state.is_p1_turn_to_move());
    }

    #[test]
    fn test_gamestate_fromstr_player_w() {
        let game_state: GameState = "
            13w
            +-----------------+
            8|   r   r r   r   |
            7| m   h     e   c |
            6|   r x r r x r   |
            5| h   d     c   d |
            4| E   H         M |
            3|   R x R R H R   |
            2| D   C     C   D |
            1|   R   R R   R   |
            +-----------------+
               a b c d e f g h"
            .parse()
            .unwrap();

        assert_eq!(game_state.move_number(), 13);
        assert!(game_state.is_p1_turn_to_move());
    }

    #[test]
    fn test_gamestate_to_str() {
        let expected_output = "2g
 +-----------------+
8| h c d m e d c h |
7| r r r r r r r r |
6|     x     x     |
5|                 |
4|                 |
3|     x     x     |
2| R R R R R R R R |
1| H C D M E D C H |
 +-----------------+
   a b c d e f g h
";

        assert_eq!(format!("{}", initial_play_state()), expected_output);
    }

    #[test]
    fn test_gamestate_from_str_and_to_str() {
        let orig_str = "14g
 +-----------------+
8|   r   r r   r   |
7| m   h     e   c |
6|   r x r r x r   |
5| h   d     c   d |
4| E   H         M |
3|   R x R R x R   |
2| D   C     C   D |
1|   R   R R   R   |
 +-----------------+
   a b c d e f g h
";

        let game_state: GameState = orig_str.parse().unwrap();
        let new_str = format!("{}", game_state);

        assert_eq!(new_str, orig_str);
    }
}