use crate::{bitboard::{Bitboard, BitboardFns}, moves::{Move, MoveKind}, attacks::Attacks, square::Square, board::io::IO}; use self::{zobrist::{ZobristSeed, Zobrist}, piece::Piece, color::Color}; pub mod io; pub mod color; pub mod piece; mod zobrist; #[derive(Debug, Clone, Copy)] pub enum CastlingSide { King, Queen, } /// Chess board is an main interface to the internal game state. /// Board defines rules of the game and manages players actions. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Board { pub ply: u16, pub piece_sets: [Bitboard; 12], /// Castling rights indexed by Color and CastlingSide pub castling_rights: [[bool; 2]; 2], /// En passsant target square pub ep_target: Option, // Computed values pub occupancy: Bitboard, /// Zobrist hash of the current position pub hash: u64, zobrist_seed: ZobristSeed, attacks: Attacks, } impl Board { pub fn new() -> Self { let default_fen = String::from("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); Self::from_FEN(default_fen) } /// Color to move at this ply pub fn color(&self) -> Color { Color::from(self.ply as u8 % 2) } fn update_occupancy(&mut self) { self.occupancy = self.piece_sets.iter().fold(0, |acc, bitboard| acc | bitboard) } pub fn empty(&self) -> Bitboard { !self.occupancy } pub fn pieces_by_color(&self, color: Color) -> &[Bitboard] { match color { Color::White => &self.piece_sets[0..6], Color::Black => &self.piece_sets[6..12], } } pub fn color_occupancy(&self, color: Color) -> Bitboard { self.pieces_by_color(color).iter().fold(0, |acc, bitboard| acc | bitboard) } pub fn piece_by_square(&self, square: Square) -> Option { let square_bb = square.to_bitboard(); self.piece_sets .iter() .enumerate() .find(|(_, bitboard)| *bitboard & square_bb > 0) .and_then(|(pt, _)| Some(Piece::from(pt))) } pub fn ep_bitboard(&self) -> Bitboard { let color = self.color(); match self.ep_target { Some(square) => { let rank = square.rank(); if (rank == 2 && color == Color::Black) || (rank == 5 && color == Color::White) { square.to_bitboard() } else { 0 } } None => 0, } } /// *Blindlessly* apply a move without any validation /// Legality test should still be performed pub fn make_move(&mut self, mov: Move) -> Option { let move_source_bb = mov.source.to_bitboard(); let move_target_bb = mov.target.to_bitboard(); // Remove existing piece (if any) from target square let mut captured_piece = match self.piece_by_square(mov.target) { Some(target_piece) => { self.piece_sets[target_piece as usize] ^= move_target_bb; self.zobrist_toggle_piece(target_piece, mov.target); Some(target_piece) }, None => None, }; // En Passant captures diffirently if mov.kind == MoveKind::EnPassant { debug_assert!(captured_piece.is_none(), "No capture should be found at this point"); let captured_square = Square::from_coords(mov.source.rank(), mov.target.file()); captured_piece = match self.piece_by_square(captured_square) { Some(pawn_type) => { let captured_bb = captured_square.to_bitboard(); self.piece_sets[pawn_type as usize] ^= captured_bb; self.occupancy ^= captured_bb; self.zobrist_toggle_piece(pawn_type, captured_square); Some(pawn_type) }, None => panic!("Pawn captured by En Passant was not found"), } } // Move a piece from source square to target let source_piece = match self.piece_by_square(mov.source) { Some(source_piece) => { let source_id = source_piece as usize; match mov.kind { MoveKind::Promotion(promotion_piece) => { let promo_id = promotion_piece as usize; self.piece_sets[source_id] ^= move_source_bb; self.occupancy ^= move_source_bb; self.zobrist_toggle_piece(source_piece, mov.source); self.piece_sets[promo_id] |= move_target_bb; self.occupancy |= move_target_bb; self.zobrist_toggle_piece(promotion_piece, mov.target); }, _ => { self.piece_sets[source_id] ^= move_source_bb; self.occupancy ^= move_source_bb; self.zobrist_toggle_piece(source_piece, mov.source); self.piece_sets[source_id] |= move_target_bb; self.occupancy |= move_target_bb; self.zobrist_toggle_piece(source_piece, mov.target); } } Piece::from(source_piece) }, None => { self.print(); panic!("{:?} is malformed: source piece not found", mov); } }; // When castling, also move a rook if mov.kind == MoveKind::Castle { debug_assert!(mov.source.file() == 4, "Castle can only be done from E file"); let (rook_source_file, rook_target_file) = match mov.target.file() { 2 => (0, 3), 6 => (7, 5), _ => panic!("Malformed castle, target square invalid: {:?}", mov), }; let rook_source_square = Square::from_coords(mov.target.rank(), rook_source_file); let rook_source_bb = rook_source_square.to_bitboard(); let rook_target_square = Square::from_coords(mov.target.rank(), rook_target_file); let rook_target_bb = rook_target_square.to_bitboard(); match self.piece_by_square(rook_source_square) { Some(rook_type) => { let rook_id = rook_type as usize; self.piece_sets[rook_id] ^= rook_source_bb; self.occupancy ^= rook_source_bb; self.zobrist_toggle_piece(rook_type, rook_source_square); self.piece_sets[rook_id] |= rook_target_bb; self.occupancy |= rook_target_bb; self.zobrist_toggle_piece(rook_type, rook_target_square); }, None => panic!("Rook was not found when castling"), } } // Double push should set En Passant target square self.ep_target = if mov.kind == MoveKind::DoublePush { self.zobrist_toggle_ep_square(mov.source); match mov.source.rank() { 1 => Some(mov.source.nort_one()), 6 => Some(mov.source.sout_one()), rank => panic!("Double-push was used from invalid rank({}) when trying to make {:?}", rank, mov), } } else { None }; // Withdraw castling rights when moving rooks or king let source_color = Color::from_piece(source_piece); match source_piece.without_color() { Piece::King => { self.castling_rights[source_color as usize][CastlingSide::King as usize] = false; self.castling_rights[source_color as usize][CastlingSide::Queen as usize] = false; self.zobrist_toggle_castling_right(source_color, CastlingSide::King); self.zobrist_toggle_castling_right(source_color, CastlingSide::Queen); }, Piece::Rook => { match mov.source.file() { 0 => { self.castling_rights[source_color as usize][CastlingSide::Queen as usize] = false; self.zobrist_toggle_castling_right(source_color, CastlingSide::Queen); } 7 => { self.castling_rights[source_color as usize][CastlingSide::King as usize] = false; self.zobrist_toggle_castling_right(source_color, CastlingSide::King); } _ => {}, } }, _ => {}, } self.ply += 1; self.zobrist_toggle_color(); captured_piece } /// Completely reverse make_move as if it never happened pub fn unmake_move( &mut self, mov: Move, captured_piece: Option, previous_ep_target: Option, previous_castling_rights: [[bool; 2]; 2], previous_hash: u64, ) { let move_source_bb = mov.source.to_bitboard(); let move_target_bb = mov.target.to_bitboard(); // Move a piece from target square back to source square match self.piece_by_square(mov.target) { Some(source_piece) => { match mov.kind { MoveKind::Promotion(promotion_piece) => { let promo_id = promotion_piece as usize; self.piece_sets[promo_id] ^= move_target_bb; self.occupancy ^= move_target_bb; let source_id = match Color::from_piece(promotion_piece) { Color::White => Piece::Pawn, Color::Black => Piece::PawnBlack, } as usize; self.piece_sets[source_id] |= move_source_bb; self.occupancy |= move_source_bb; } _ => { let source_id = source_piece as usize; self.piece_sets[source_id] ^= move_target_bb; self.occupancy ^= move_target_bb; self.piece_sets[source_id] |= move_source_bb; self.occupancy |= move_source_bb; } } }, None => panic!("Trying to unmake move which was not made: no piece was found on target square"), }; // If unmaking castle, also return rook to its place if mov.kind == MoveKind::Castle { let (rook_source_file, rook_target_file) = match mov.target.file() { 2 => (0, 3), 6 => (7, 5), _ => panic!("Malformed castle, target square invalid: {:?}", mov), }; let rook_source_bb = Square::from_coords(mov.target.rank(), rook_source_file).to_bitboard(); let rook_target_bb = Square::from_coords(mov.target.rank(), rook_target_file).to_bitboard(); match self.piece_sets .iter() .enumerate() .find(|(_, bitboard)| *bitboard & rook_target_bb > 0) { Some((rook_type, _)) => { self.piece_sets[rook_type] |= rook_source_bb; self.occupancy |= rook_source_bb; self.piece_sets[rook_type] ^= rook_target_bb; self.occupancy ^= rook_target_bb; }, None => panic!("Rook was not found when castling"), } } // Return captured piece to target square match captured_piece { Some(target_piece) => { match mov.kind { // Return pawn captured by En Passant pawn if needed MoveKind::EnPassant => { let original_dead_pawn_bb = Square::from_coords(mov.source.rank(), mov.target.file()).to_bitboard(); self.piece_sets[target_piece as usize] |= original_dead_pawn_bb; self.occupancy |= original_dead_pawn_bb; }, _ => { self.piece_sets[target_piece as usize] |= move_target_bb; self.occupancy |= move_target_bb; }, } }, None => {} } self.ep_target = previous_ep_target; self.castling_rights = previous_castling_rights; self.hash = previous_hash; self.ply -= 1; } pub fn is_square_attacked(&self, square: Square, attacker_color: Color) -> bool { for (piece_type, piece) in self.pieces_by_color(attacker_color).iter().enumerate() { match Piece::from(piece_type) { Piece::Queen => { if self.attacks.queen(self.occupancy, square) & piece > 0 { return true } } Piece::Knight => { if self.attacks.knight[square as usize] & piece > 0 { return true } } Piece::Bishop => { if self.attacks.bishop(self.occupancy, square) & piece > 0 { return true } } Piece::Rook => { if self.attacks.rook(self.occupancy, square) & piece > 0 { return true } } Piece::Pawn => { if self.attacks.pawn[attacker_color.flip() as usize][square as usize] & piece > 0 { return true } } Piece::King => { if self.attacks.king[square as usize] & piece > 0 { return true } } _ => panic!("Unexpected piece type! Pieces by color should be considered white") } } false } pub fn is_king_in_check(&self, color: Color) -> bool { let king_bb = match color { Color::White => self.piece_sets[Piece::King as usize], Color::Black => self.piece_sets[Piece::KingBlack as usize], }; let square = king_bb.bitscan(); self.is_square_attacked(square, color.flip()) } } #[cfg(test)] mod tests { use super::*; use crate::{square::Square, board::zobrist::Zobrist}; #[test] fn square_enum() { assert_eq!(Square::A1 as u8, 0); assert_eq!(Square::F1 as u8, 5); assert_eq!(Square::H8 as u8, 63); } #[test] fn make_move() { let fen = String::from("q1b2k2/5p1p/4p1pb/pPPp4/3N4/3nPB2/P2QKnR1/1R6 w - - 0 25"); let mut board = Board::from_FEN(fen); let initial_board = board.clone(); board.print(); let black_move = Move { source: Square::F7, target: Square::F5, kind: MoveKind::Quiet }; println!("\n{:?}", black_move); match board.make_move(black_move) { Some(..) => panic!("No piece should be captured"), None => {}, }; board.print(); let hash = board.hash; board.compute_hash(); assert_eq!(hash, board.hash, "Hash should be correctly updated after move"); assert!(board.piece_sets[Piece::PawnBlack as usize] & Square::F7.to_bitboard() == 0); assert!(board.piece_sets[Piece::PawnBlack as usize] & Square::F5.to_bitboard() > 0); assert!(board.ply == 1); let white_move = Move { source: Square::D2, target: Square::A5, kind: MoveKind::Capture }; println!("\n{:?}", white_move); match board.make_move(white_move) { Some(captured) => assert!(captured == Piece::PawnBlack), None => panic!("A piece should be captured"), }; board.print(); assert!(board.piece_sets[Piece::PawnBlack as usize] & Square::A5.to_bitboard() == 0, "Target piece should be captured"); assert!(board.piece_sets[Piece::Queen as usize] & Square::D2.to_bitboard() == 0); assert!(board.piece_sets[Piece::Queen as usize] & Square::A5.to_bitboard() > 0); assert_ne!(board.occupancy, initial_board.occupancy, "Occupancy should change after make_move"); assert!(board.ply == 2); } #[test] fn unmake_move() { let fen = String::from("q1b2k2/5p1p/4p1pb/pPPp4/3N4/3nPB2/P2QKnR1/1R6 w - - 0 25"); let mut board = Board::from_FEN(fen); let initial_board = board.clone(); let mov = Move { source: Square::D2, target: Square::A5, kind: MoveKind::Capture }; board.print(); let captured_piece = board.make_move(mov); board.print(); board.unmake_move(mov, captured_piece, None, board.castling_rights, initial_board.hash); board.print(); assert_eq!(board, initial_board, "Board state after unmake_move should be the same as before make_move"); } #[test] fn is_square_attacked() { let board = Board::new(); assert_eq!(board.is_square_attacked(Square::E2, Color::White), true); assert_eq!(board.is_square_attacked(Square::E2, Color::Black), false); assert_eq!(board.is_square_attacked(Square::E4, Color::White), false); assert_eq!(board.is_square_attacked(Square::B6, Color::Black), true); } }