//! Functions to determine legal moves. use crate::board::*; use crate::castling::*; use crate::fen; use crate::movement::Move; /// Characteristics of the state of a game. /// /// It does not include various parameters such as clocks that are /// more aimed for engine analysis than typical rules checking. /// /// - `color`: current player's turn /// - `castling`: which castling options are available; updated throughout the game. /// - `en_passant`: position of a pawn that can be taken using en passant attack. /// - `halfmove`: eh not sure /// - `fullmove`: same #[derive(Debug, PartialEq, Clone, Hash)] pub struct GameState { pub color: Color, pub castling: u8, pub en_passant: Option, pub halfmove: i32, pub fullmove: i32, } impl GameState { pub const fn new() -> GameState { GameState { color: WHITE, castling: CASTLE_MASK, en_passant: None, halfmove: 0, fullmove: 1, } } } impl std::fmt::Display for GameState { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!( f, "[color: {}, castling: {:04b}, en_passant: {}, halfmove: {}, fullmove: {}]", color_to_string(self.color), self.castling, fen::en_passant_to_string(self.en_passant), self.halfmove, self.fullmove ) } } /// Get a list of moves for all pieces of the playing color. /// /// If `pseudo_legal` is true, do not check for illegal moves. This is /// used to avoid endless recursion when checking if a P move is /// illegal, as it needs to check all possible following enemy moves, /// e.g. to see if P's king can be taken. Consider a call with true /// `pseudo_legal` as a collection of attacked squares instead of legal /// move collection. pub fn get_player_moves( board: &Board, game_state: &GameState, ) -> Vec { let attacked_bb = board.get_rays(opposite(game_state.color)); let mut moves = Vec::with_capacity(32); for r in 0..8 { for f in 0..8 { let square = sq(f, r); if board.is_empty(square) { continue } if board.get_color_on(square) == game_state.color { moves.append( &mut get_piece_moves(board, game_state, square, game_state.color, attacked_bb) ); } } } moves } /// Get a list of moves for the piece of `color` on `square`. /// /// Use `board` and `game_state` to get the moves. `color` is the color /// of the piece on `square`; it could technically be found from the /// board but that would require an additional lookup and this function /// is always called in a context where the piece color is known. fn get_piece_moves( board: &Board, game_state: &GameState, square: Square, color: Color, attacked_bb: Bitboard, ) -> Vec { let piece = board.get_piece_on(square); let mut moves = Vec::with_capacity(32); get_moves_from_bb( board, game_state, match piece { PAWN => { board.get_pawn_progresses(square, color) | board.get_pawn_captures(square, color) } KING => board.get_king_rays(square, color), BISHOP => board.get_bishop_rays(square, color), KNIGHT => board.get_knight_rays(square, color), ROOK => board.get_rook_rays(square, color), QUEEN => board.get_queen_rays(square, color), _ => { panic!("Invalid piece.") } }, square, color, piece, attacked_bb, &mut moves ); if piece == KING && sq_rank(square) == CASTLE_RANK_BY_COLOR[color] { get_king_castles(board, game_state, square, color, attacked_bb, &mut moves); } moves } /// Get moves from this ray bitboard. /// /// Inspect all moves from the bitboard and produce a Move for each /// legal move. Does not take castle into account. Pawns that reach /// the last rank are promoted as queens. fn get_moves_from_bb( board: &Board, game_state: &GameState, bitboard: Bitboard, square: Square, color: Color, piece: Piece, attacked_bb: Bitboard, moves: &mut Vec ) { for ray_square in 0..NUM_SQUARES { if ray_square == square || bitboard & bit_pos(ray_square) == 0 { continue } if let Some(mut m) = inspect_move(board, game_state, square, ray_square, attacked_bb) { // Automatic queen promotion for pawns moving to the opposite rank. if piece == PAWN && ( (color == WHITE && sq_rank(ray_square) == RANK_8) || (color == BLACK && sq_rank(ray_square) == RANK_1) ) { m.promotion = Some(QUEEN); } moves.push(m); } } } /// Accept or ignore a move from `square` to `ray_square`. /// /// This function checks that the move is legal. It assumes that /// `ray_square` is either empty or an enemy piece, but not a friend /// piece: they should have been filtered. /// /// This function does not set promotions for pawns reaching last rank. fn inspect_move( board: &Board, game_state: &GameState, square: Square, ray_square: Square, attacked_bb: Bitboard ) -> Option { let m = Move::new(square, ray_square); if !is_illegal(board, game_state, &m, attacked_bb) { Some(m) } else { None } } /// Check if a move is illegal. fn is_illegal( board: &Board, game_state: &GameState, m: &Move, attacked_bb: Bitboard ) -> bool { // A move is illegal if the king ends up in check. let king_square = if board.get_piece_on(m.source) == KING { m.dest } else { if let Some(king) = board.find_king(game_state.color) { king } else { return false } }; attacked_bb & bit_pos(king_square) != 0 } /// Get possible castles. /// /// Here are the rules that should ALL be respected: /// 1. The king and the chosen rook are on the player's first rank. /// 2. Neither the king nor the chosen rook has previously moved. /// 3. There are no pieces between the king and the chosen rook. /// 4. The king is not currently in check. /// 5. The king does not pass through a square that is attacked by an enemy piece. /// 6. The king does not end up in check. /// /// Rule 1 is NOT checked by this method to avoid creating empty vecs. /// Check it in the caller. fn get_king_castles( board: &Board, game_state: &GameState, square: Square, color: Color, attacked_bb: Bitboard, moves: &mut Vec ) { let combined_bb = board.combined(); // First get the required castling rank and color mask for the player. let castle_rank = CASTLE_RANK_BY_COLOR[color]; let castle_color_mask = CASTLE_MASK_BY_COLOR[color]; // Check for castling if the king is on its castling rank (R1) if sq_rank(square) == castle_rank { // Check for both castling sides. for castle_side_id in 0..NUM_CASTLE_SIDES { let castle_side_mask = CASTLE_SIDES[castle_side_id]; // Check for castling availability for this color and side (R2). if (game_state.castling & castle_color_mask & castle_side_mask) != 0 { // Check that squares in the king's path are not attacked (R4, R5, R6). let castle_legality_path = CASTLE_LEGALITY_PATHS[color][castle_side_id]; if attacked_bb & castle_legality_path != 0 { continue } // Check that squares in both the king and rook's path are empty. let castle_move_path = CASTLE_MOVE_PATHS[color][castle_side_id]; if combined_bb & castle_move_path != 0 { continue } moves.push(Move::get_castle_move(castle_side_mask & castle_color_mask)); } } } } #[cfg(test)] mod tests { use super::*; /// Like `get_piece_moves` but generate attacked bitboard. fn get_legal_piece_moves( board: &Board, game_state: &GameState, square: Square, color: Color ) -> Vec { let attacked_bb = board.get_rays(opposite(game_state.color)); get_piece_moves(board, game_state, square, color, attacked_bb) } #[test] fn test_get_player_moves() { let b = Board::new(); let gs = GameState::new(); // At first move, white has 16 pawn moves and 4 knight moves. let moves = get_player_moves(&b, &gs); assert_eq!(moves.len(), 20); } #[test] fn test_get_pawn_moves() { let mut b = Board::new_empty(); let gs = GameState::new(); // Check that a pawn (here white queen's pawn) can move forward if the road is free. b.set_square(D3, WHITE, PAWN); let moves = get_legal_piece_moves(&b, &gs, D3, WHITE); assert!(moves.len() == 1 && moves.contains(&Move::new(D3, D4))); // Check that a pawn (here white king's pawn) can move 2 square forward on first move. b.set_square(E2, WHITE, PAWN); let moves = get_legal_piece_moves(&b, &gs, E2, WHITE); assert_eq!(moves.len(), 2); assert!(moves.contains(&Move::new(E2, E3))); assert!(moves.contains(&Move::new(E2, E4))); // Check that a pawn cannot move forward if a piece is blocking its path. // 1. black pawn 2 square forward; only 1 square forward available from start pos. b.set_square(E4, BLACK, PAWN); let moves = get_legal_piece_moves(&b, &gs, E2, WHITE); assert!(moves.len() == 1 && moves.contains(&Move::new(E2, E3))); // 2. black pawn 1 square forward; no square available. b.set_square(E3, BLACK, PAWN); let moves = get_legal_piece_moves(&b, &gs, E2, WHITE); assert_eq!(moves.len(), 0); // 3. remove the e4 black pawn; the white pawn should not be able to jump above e3 pawn. b.clear_square(E4, BLACK, PAWN); let moves = get_legal_piece_moves(&b, &gs, E2, WHITE); assert_eq!(moves.len(), 0); // Check that a pawn can take a piece diagonally. b.set_square(F3, BLACK, PAWN); let moves = get_legal_piece_moves(&b, &gs, E2, WHITE); assert!(moves.len() == 1 && moves.contains(&Move::new(E2, F3))); b.set_square(D3, BLACK, PAWN); let moves = get_legal_piece_moves(&b, &gs, E2, WHITE); assert_eq!(moves.len(), 2); assert!(moves.contains( &Move::new(E2, F3) )); assert!(moves.contains( &Move::new(E2, D3) )); // Check that a pawn moving to the last rank leads to queen promotion. // 1. by simply moving forward. b.set_square(A7, WHITE, PAWN); let moves = get_legal_piece_moves(&b, &gs, A7, WHITE); assert!(moves.len() == 1 && moves.contains(&Move::new_promotion(A7, A8, QUEEN))); } #[test] fn test_get_bishop_moves() { let mut b = Board::new_empty(); let gs = GameState::new(); // A bishop has maximum range when it's in a center square. b.set_square(D4, WHITE, BISHOP); let moves = get_legal_piece_moves(&b, &gs, D4, WHITE); assert_eq!(moves.len(), 13); // Going top-right. assert!(moves.contains(&Move::new(D4, E5))); assert!(moves.contains(&Move::new(D4, F6))); assert!(moves.contains(&Move::new(D4, G7))); assert!(moves.contains(&Move::new(D4, H8))); // Going bottom-right. assert!(moves.contains(&Move::new(D4, E3))); assert!(moves.contains(&Move::new(D4, F2))); assert!(moves.contains(&Move::new(D4, G1))); // Going bottom-left. assert!(moves.contains(&Move::new(D4, C3))); assert!(moves.contains(&Move::new(D4, B2))); assert!(moves.contains(&Move::new(D4, A1))); // Going top-left. assert!(moves.contains(&Move::new(D4, C5))); assert!(moves.contains(&Move::new(D4, B6))); assert!(moves.contains(&Move::new(D4, A7))); // When blocking commit to one square with friendly piece, lose 2 moves. b.set_square(B2, WHITE, PAWN); assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 11); // When blocking commit to one square with enemy piece, lose only 1 move. b.set_square(B2, BLACK, PAWN); assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 12); } #[test] fn test_get_knight_moves() { let mut b = Board::new_empty(); let gs = GameState::new(); // A knight never has blocked commit; if it's in the center of the board, it can have up to // 8 moves. b.set_square(D4, WHITE, KNIGHT); assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 8); // If on a side if has only 4 moves. b.set_square(A4, WHITE, KNIGHT); assert_eq!(get_legal_piece_moves(&b, &gs, A4, WHITE).len(), 4); // And in a corner, only 2 moves. b.set_square(A1, WHITE, KNIGHT); assert_eq!(get_legal_piece_moves(&b, &gs, A1, WHITE).len(), 2); // Add 2 friendly pieces and it is totally blocked. b.set_square(B3, WHITE, PAWN); b.set_square(C2, WHITE, PAWN); assert_eq!(get_legal_piece_moves(&b, &gs, A1, WHITE).len(), 0); } #[test] fn test_get_rook_moves() { let mut b = Board::new_empty(); let gs = GameState::new(); b.set_square(D4, WHITE, ROOK); assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 14); b.set_square(D6, BLACK, PAWN); assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 12); b.set_square(D6, WHITE, PAWN); assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 11); } #[test] fn test_get_queen_moves() { let mut b = Board::new_empty(); let gs = GameState::new(); b.set_square(D4, WHITE, QUEEN); assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 14 + 13); } #[test] fn test_get_king_moves() { let mut gs = GameState::new(); // King can move 1 square in any direction. let mut b = Board::new_empty(); b.set_square(D4, WHITE, KING); assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 8); b.set_square(E5, WHITE, PAWN); assert_eq!(get_legal_piece_moves(&b, &gs, D4, WHITE).len(), 7); // If castling is available, other moves are possible: 5 moves + 2 castles. let mut b = Board::new_empty(); b.set_square(E1, WHITE, KING); b.set_square(A1, WHITE, ROOK); b.set_square(H1, WHITE, ROOK); assert_eq!(get_legal_piece_moves(&b, &gs, E1, WHITE).len(), 5 + 2); // Castling works as well for black. gs.color = BLACK; b.set_square(E8, BLACK, KING); b.set_square(A8, BLACK, ROOK); b.set_square(H8, BLACK, ROOK); assert_eq!(get_legal_piece_moves(&b, &gs, E8, BLACK).len(), 5 + 2); } #[test] fn test_filter_illegal_moves() { let mut b = Board::new_empty(); let mut gs = GameState::new(); // Place white's king on first rank. b.set_square(E1, WHITE, KING); // Place black rook in second rank: king can only move left or right. b.set_square(H2, BLACK, ROOK); // No castling available. gs.castling = 0; // 5 moves in absolute but only 2 are legal. let all_wh_moves = get_legal_piece_moves(&b, &gs, E1, WHITE); assert_eq!(all_wh_moves.len(), 2); } }