use crate::{bitboard::{Bitboard, serialize_bitboard}, moves::Move, attacks::Attacks}; pub static DEFAULT_FEN: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Board { pub pieces: [Bitboard; 12], pub occupancy: Bitboard, pub ply: u16, attacks: Attacks, } #[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive)] #[repr(usize)] pub enum PieceType { #[default] Pawn, Knight, Bishop, Rook, Queen, King, PawnBlack, KnightBlack, BishopBlack, RookBlack, QueenBlack, KingBlack, } const PIECE_CHARS: [&str; 12] = [ "♟︎", "♞", "♝", "♜", "♛", "♚", "♙", "♘", "♗", "♖", "♕", "♔", ]; #[allow(unused)] impl Board { #[allow(non_snake_case)] pub fn from_FEN(fen: String) -> Self { let mut pieces = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; let mut rank = 7; let mut file = 0i32; for character in fen.chars() { let index = rank * 8 + file; let position = 1 << index.clamp(0, 63); if character.is_numeric() { let digit = match character.to_digit(10) { None => todo!("What to do here?"), Some(digit) => digit, }; if digit > 0 && digit <= 8 { file += digit as i32; } } else { match character { 'P' => pieces[PieceType::Pawn as usize] |= position, 'N' => pieces[PieceType::Knight as usize] |= position, 'B' => pieces[PieceType::Bishop as usize] |= position, 'R' => pieces[PieceType::Rook as usize] |= position, 'Q' => pieces[PieceType::Queen as usize] |= position, 'K' => pieces[PieceType::King as usize] |= position, 'p' => pieces[PieceType::PawnBlack as usize] |= position, 'n' => pieces[PieceType::KnightBlack as usize] |= position, 'b' => pieces[PieceType::BishopBlack as usize] |= position, 'r' => pieces[PieceType::RookBlack as usize] |= position, 'q' => pieces[PieceType::QueenBlack as usize] |= position, 'k' => pieces[PieceType::KingBlack as usize] |= position, '/' => { rank -= 1; file = -1; // So it becomes 0 }, ' ' => { break }, // TODO: break for now, parse everything else later '-' => {}, // TODO 'w' => {}, // TODO _ => todo!("Unexpected character!"), } file += 1; } } let mut board = Self { pieces, occupancy: 0, ply: 0, attacks: Attacks::new() }; board.update_occupancy(); board } fn update_occupancy(&mut self) { self.occupancy = 0; // TODO: reduce for piece in self.pieces { self.occupancy |= piece; } } fn empty(&self) -> Bitboard { !self.occupancy } fn pieces_by_color(&self, color: Color) -> [Bitboard; 6] { let mut pieces = [0; 6]; for piece_type in 0..6 { pieces[piece_type] = self.pieces[piece_type + 6 * color as usize]; } pieces } fn color_occupancy(&self, color: Color) -> Bitboard { let mut occupancy = 0; for piece in self.pieces_by_color(color) { occupancy |= piece; } occupancy } pub fn print(&self) { println!(); for rank in (0..8).rev() { print!("{}|", rank + 1); for file in 0..8 { let index = rank * 8 + file; let position: Bitboard = 1 << index; let mut found = false; for (piece_type, piece_bitboard) in self.pieces.iter().enumerate() { if (piece_bitboard & position) > 0 { found = true; print!("{} ", PIECE_CHARS[piece_type]); } } if !found { print!(". "); } } println!(); } println!(" a b c d e f g h"); } pub fn generate_moves(&self, color: Color) -> Vec { let mut moves = Vec::with_capacity(1024); let opponent_occupancy = self.color_occupancy(Color::from(1 - color as u8)); let available_targets = opponent_occupancy | self.empty(); for (piece_type, piece) in self.pieces_by_color(color).iter().enumerate() { match PieceType::from(piece_type) { PieceType::Pawn => { for source in serialize_bitboard(*piece) { for target in serialize_bitboard(self.attacks.pawn[color as usize][source as usize] & opponent_occupancy) { moves.push(Move { source, target }); }; for target in serialize_bitboard(self.attacks.pawn_pushes[color as usize][source as usize] & available_targets) { moves.push(Move { source, target }); }; } } PieceType::King => { for source in serialize_bitboard(*piece) { for target in serialize_bitboard(self.attacks.king[source as usize] & available_targets) { moves.push(Move { source, target }); }; } } PieceType::Knight => { for source in serialize_bitboard(*piece) { for target in serialize_bitboard(self.attacks.knight[source as usize] & available_targets) { moves.push(Move { source, target }); }; } } PieceType::Bishop => { for source in serialize_bitboard(*piece) { for target in serialize_bitboard(self.attacks.bishop(self.occupancy, source) & available_targets) { moves.push(Move { source, target }); }; } } PieceType::Rook => { for source in serialize_bitboard(*piece) { for target in serialize_bitboard(self.attacks.rook(self.occupancy, source) & available_targets) { moves.push(Move { source, target }); }; } } PieceType::Queen => { for source in serialize_bitboard(*piece) { for target in serialize_bitboard(self.attacks.queen(self.occupancy, source) & available_targets) { moves.push(Move { source, target }); }; } } _ => todo!("Incorrect piece type") } } moves } /// *Blindlessly* apply a move without any validation /// Move should be validated beforehand 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 captured_piece = match self.pieces .iter() .enumerate() .find(|(piece_type, bitboard)| *bitboard & mov.target.to_bitboard() > 0) { Some((target_piece, _)) => { self.pieces[target_piece] ^= move_target_bb; Some(PieceType::from(target_piece)) }, None => None, }; // Move a piece from source square to target match self.pieces .iter() .enumerate() .find(|(piece_type, bitboard)| *bitboard & mov.source.to_bitboard() > 0) { Some((source_piece, _)) => { self.pieces[source_piece] ^= move_source_bb; self.occupancy ^= move_source_bb; self.pieces[source_piece] |= move_target_bb; self.occupancy |= move_target_bb; }, None => panic!("Move is malformed: source piece not found"), }; self.ply += 1; captured_piece } /// Completely reverse make_move as if it never happened pub fn unmake_move(&mut self, mov: Move, captured_piece: Option) { 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.pieces .iter() .enumerate() .find(|(piece_type, bitboard)| *bitboard & mov.target.to_bitboard() > 0) { Some((source_piece, _)) => { self.pieces[source_piece] ^= move_target_bb; self.occupancy ^= move_target_bb; self.pieces[source_piece] |= 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"), }; // Return captured piece to target square match captured_piece { Some(target_piece) => { self.pieces[target_piece as usize] |= move_target_bb; self.occupancy |= move_target_bb; }, None => {} } self.ply -= 1; } } #[derive(Debug, Clone, Copy, PartialEq, num_enum::FromPrimitive)] #[repr(u8)] pub enum Color { #[default] White, Black, } #[cfg(test)] mod tests { use super::*; use crate::{bitboard::{pop_count, bitscan, print}, square::Square}; #[test] fn test_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 test_from_fen() { let fen = String::from(DEFAULT_FEN); let board = Board::from_FEN(fen); board.print(); print(board.empty(), "Empty squares"); assert_eq!(pop_count(board.pieces[PieceType::Pawn as usize]), 8); assert_eq!(pop_count(board.pieces[PieceType::Knight as usize]), 2); assert_eq!(pop_count(board.pieces[PieceType::Bishop as usize]), 2); assert_eq!(pop_count(board.pieces[PieceType::Rook as usize]), 2); assert_eq!(pop_count(board.pieces[PieceType::Queen as usize]), 1); assert_eq!(pop_count(board.pieces[PieceType::King as usize]), 1); assert_eq!(pop_count(board.pieces[PieceType::PawnBlack as usize]), 8); assert_eq!(pop_count(board.pieces[PieceType::KnightBlack as usize]), 2); assert_eq!(pop_count(board.pieces[PieceType::BishopBlack as usize]), 2); assert_eq!(pop_count(board.pieces[PieceType::RookBlack as usize]), 2); assert_eq!(pop_count(board.pieces[PieceType::QueenBlack as usize]), 1); assert_eq!(pop_count(board.pieces[PieceType::KingBlack as usize]), 1); assert_eq!(bitscan(board.pieces[PieceType::King as usize]), Square::E1); assert_eq!(bitscan(board.pieces[PieceType::QueenBlack as usize]), Square::D8); assert_eq!(pop_count(board.occupancy), 32); assert_eq!(pop_count(board.empty()), 32); assert_eq!(pop_count(board.color_occupancy(Color::White)), 16); assert_eq!(pop_count(board.color_occupancy(Color::Black)), 16); } #[test] fn test_generate_moves_starting_position() { let fen = String::from(DEFAULT_FEN); let board = Board::from_FEN(fen); let moves = board.generate_moves(Color::White); let black_moves = board.generate_moves(Color::Black); assert_eq!(moves.len(), 20); assert_eq!(black_moves.len(), 20); for mov in moves { mov.print(); } } #[test] fn test_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 }; println!("\n{:?}", black_move); match board.make_move(black_move) { Some(..) => panic!("No piece should be captured"), None => {}, }; board.print(); assert!(board.pieces[PieceType::PawnBlack as usize] & Square::F7.to_bitboard() == 0); assert!(board.pieces[PieceType::PawnBlack as usize] & Square::F5.to_bitboard() > 0); assert!(board.ply == 1); let white_move = Move { source: Square::D2, target: Square::A5 }; println!("\n{:?}", white_move); match board.make_move(white_move) { Some(captured) => assert!(captured == PieceType::PawnBlack), None => panic!("A piece should be captured"), }; board.print(); assert!(board.pieces[PieceType::PawnBlack as usize] & Square::A5.to_bitboard() == 0, "Target piece should be captured"); assert!(board.pieces[PieceType::Queen as usize] & Square::D2.to_bitboard() == 0); assert!(board.pieces[PieceType::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 test_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 }; board.print(); let captured_piece = board.make_move(mov); board.print(); board.unmake_move(mov, captured_piece); board.print(); assert_eq!(board, initial_board, "Board state after unmake_move should be the same as before make_move"); } }