use core::fmt; use crate::{ bitboard::{have_common_bit, square_to_bitboard}, board::{Board, Color, Kind, Piece}, square::{coords_to_square, square_to_algebraic, Square}, state::{Castle, State}, }; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub enum Promote { Knight, Rook, Bishop, Queen, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub enum MoveType { Quiet, Capture, DoublePush, Promotion(Promote), PromotionCapture(Promote), EnPassant, Castle, } #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub struct Move { pub src: usize, pub dst: usize, pub move_type: MoveType, } impl fmt::Debug for Move { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}{}", square_to_algebraic(self.src), square_to_algebraic(self.dst) )?; if let MoveType::Promotion(piece) | MoveType::PromotionCapture(piece) = &self.move_type { let promote_char = match piece { Promote::Knight => 'n', Promote::Bishop => 'b', Promote::Rook => 'r', Promote::Queen => 'q', }; write!(f, "{}", promote_char)?; } Ok(()) } } impl Move { pub const fn new(src: usize, dst: usize) -> Self { Self { src, dst, move_type: MoveType::Quiet, } } pub const fn new_with_type(src: usize, dst: usize, move_type: MoveType) -> Self { Self { src, dst, move_type, } } pub fn parse_from_str(mv: &str) -> Result { if mv.len() != 4 && mv.len() != 5 { return Err("Invalid move characters length".to_string()); } let mut mv_chars = mv.chars(); let src_file = (mv_chars.next().unwrap() as usize) - ('a' as usize); let src_rank = (mv_chars.next().unwrap() as usize) - ('1' as usize); let dst_file = (mv_chars.next().unwrap() as usize) - ('a' as usize); let dst_rank = (mv_chars.next().unwrap() as usize) - ('1' as usize); let src = coords_to_square(src_rank, src_file); let dst = coords_to_square(dst_rank, dst_file); if mv.len() == 5 { let promotion_move = match mv_chars.next().unwrap() { 'n' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Knight)), 'b' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Bishop)), 'r' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Rook)), 'q' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Queen)), _ => return Err("Unrecongisable character in promotion piece index".to_string()), }; return Ok(promotion_move); } Ok(Self::new(src, dst)) } pub fn parse_into_str(&self) -> String { format!("{:?}", self) } } impl Board { pub fn make_move(&mut self, mv: &Move, color: Color) { self.update_board_state(mv, color); } pub fn update_board_state(&mut self, mv: &Move, color: Color) { let (own_pieces, opponent_pieces, en_passant_square) = match color { Color::White => ( &mut self.white_pieces, &mut self.black_pieces, Some(mv.src + 8), ), Color::Black => ( &mut self.black_pieces, &mut self.white_pieces, Some(mv.src - 8), ), }; let pawn_move = Self::is_pawn_move(mv.src, &own_pieces[Kind::Pawn]); Self::update_game_state(&mut self.state, mv, color, pawn_move); match &mv.move_type { MoveType::Quiet => { Self::move_piece(mv.src, mv.dst, own_pieces); } MoveType::Capture => { Self::move_piece(mv.src, mv.dst, own_pieces); Self::remove_piece(mv.dst, opponent_pieces); } MoveType::EnPassant => { Self::move_piece(mv.src, mv.dst, own_pieces); Self::remove_pawn_enpassant(mv.dst, opponent_pieces, color); } MoveType::DoublePush => { Self::move_piece(mv.src, mv.dst, own_pieces); self.state.set_en_passant_target_square(en_passant_square); } MoveType::Promotion(promote) => { Self::remove_piece(mv.src, own_pieces); Self::promote_piece(mv.dst, own_pieces, promote); } MoveType::PromotionCapture(promote) => { Self::remove_piece(mv.src, own_pieces); Self::remove_piece(mv.dst, opponent_pieces); Self::promote_piece(mv.dst, own_pieces, promote); } MoveType::Castle => { Self::move_piece(mv.src, mv.dst, own_pieces); Self::move_rook_castle(mv.dst, own_pieces); self.state.set_castling_ability(color, Castle::None) } } } fn update_game_state(state: &mut State, mv: &Move, color: Color, pawn_move: bool) { state.set_en_passant_target_square(None); state.update_castling_state_quiet(mv.src, color); state.update_castling_state_capture(mv.dst, Color::opponent_color(color)); state.update_half_move(mv.move_type, pawn_move); state.update_full_move(color); state.change_side(); } fn move_piece(src: usize, dst: usize, pieces: &mut [Piece; 6]) { for p in pieces.iter_mut() { if have_common_bit(p.bitboard, square_to_bitboard(src)) { p.bitboard &= !square_to_bitboard(src); p.bitboard |= square_to_bitboard(dst); break; } } } fn move_rook_castle(king_dst: usize, pieces: &mut [Piece; 6]) { let (rook_src, rook_dst) = match king_dst { Square::C1 | Square::C8 => (king_dst - 2, king_dst + 1), Square::G1 | Square::G8 => (king_dst + 1, king_dst - 1), _ => return, }; Self::move_piece(rook_src, rook_dst, pieces); } fn promote_piece(square: usize, pieces: &mut [Piece; 6], promote: &Promote) { match promote { Promote::Knight => pieces[Kind::Knight].bitboard |= square_to_bitboard(square), Promote::Bishop => pieces[Kind::Bishop].bitboard |= square_to_bitboard(square), Promote::Rook => pieces[Kind::Rook].bitboard |= square_to_bitboard(square), Promote::Queen => pieces[Kind::Queen].bitboard |= square_to_bitboard(square), }; } fn remove_piece(square: usize, pieces: &mut [Piece; 6]) { for p in pieces.iter_mut() { if have_common_bit(p.bitboard, square_to_bitboard(square)) { p.bitboard &= !(square_to_bitboard(square)); break; } } } fn remove_pawn_enpassant(square: usize, pieces: &mut [Piece; 6], color: Color) { let piece_to_remove = match color { Color::White => square - 8, Color::Black => square + 8, }; Self::remove_piece(piece_to_remove, pieces); } const fn is_pawn_move(square: usize, pawns: &Piece) -> bool { have_common_bit(square_to_bitboard(square), pawns.bitboard) } } #[cfg(test)] mod tests { use crate::{ board::Color, fen::from_fen, r#move::{Move, MoveType, Promote}, square::Square, }; const FEN_QUIET: [&str; 2] = [ "r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R3K2R w KQkq - 0 1", "r3k2r/2p1p1qp/2npb3/1p3p2/pP2P1pP/P1PB1Q2/3PNPP1/R3K2R b KQkq b3 0 1", ]; #[test] fn test_make_move_quiet() -> Result<(), String> { let mut game = from_fen(FEN_QUIET[0])?; let mv = Move::new_with_type(9, 25, MoveType::DoublePush); game.board.make_move(&mv, Color::White); assert_eq!(game, from_fen(FEN_QUIET[1])?); Ok(()) } const FEN_CAPTURE: [&str; 2] = [ "r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R3K2R w KQkq - 0 1", "r3k2r/2p1p1qp/2npb3/1p3Q2/p3P1pP/P1PB4/1P1PNPP1/R3K2R b KQkq - 0 1", ]; #[test] fn test_make_move_capture() -> Result<(), String> { let mut game = from_fen(FEN_CAPTURE[0])?; let mv = Move::new_with_type(21, 37, MoveType::Capture); game.board.make_move(&mv, Color::White); assert_eq!(game, from_fen(FEN_CAPTURE[1])?); Ok(()) } const FEN_EN_PASSANT: [&str; 2] = [ "r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R3K2R b KQkq h3 0 1", "r3k2r/2p1p1qp/2npb3/1p3p2/p3P3/P1PB1Q1p/1P1PNPP1/R3K2R w KQkq - 0 2", ]; #[test] fn test_make_move_en_passant() -> Result<(), String> { let mut game = from_fen(FEN_EN_PASSANT[0])?; let mv = Move::new_with_type(30, 23, MoveType::EnPassant); game.board.make_move(&mv, Color::Black); assert_eq!(game, from_fen(FEN_EN_PASSANT[1])?); Ok(()) } const FEN_DOUBLE_PUSH: [&str; 2] = [ "rnbqkbnr/p1pppppp/8/8/1p5P/8/PPPPPPP1/RNBQKBNR w KQkq - 0 1", "rnbqkbnr/p1pppppp/8/8/1pP4P/8/PP1PPPP1/RNBQKBNR b KQkq c3 0 1", ]; #[test] fn test_make_move_double_push() -> Result<(), String> { let mut game = from_fen(FEN_DOUBLE_PUSH[0])?; let mv = Move::new_with_type(10, 26, MoveType::DoublePush); game.board.make_move(&mv, Color::White); assert_eq!(game, from_fen(FEN_DOUBLE_PUSH[1])?); Ok(()) } const FEN_PROMOTION: [&str; 2] = [ "8/6P1/4n2b/1p6/1Kp5/6n1/4b3/1k6 w - - 0 1", "6Q1/8/4n2b/1p6/1Kp5/6n1/4b3/1k6 b - - 0 1", ]; #[test] fn test_make_move_promotion() -> Result<(), String> { let mut game = from_fen(FEN_PROMOTION[0])?; let mv = Move::new_with_type(54, 62, MoveType::Promotion(Promote::Queen)); game.board.make_move(&mv, Color::White); assert_eq!(game, from_fen(FEN_PROMOTION[1])?); Ok(()) } const FEN_PROMOTION_CAPTURE: [&str; 2] = [ "5n2/6P1/7b/1p6/1Kp5/6n1/4b3/1k6 w - - 0 1", "5Q2/8/7b/1p6/1Kp5/6n1/4b3/1k6 b - - 0 1", ]; #[test] fn test_make_move_promotion_capture() -> Result<(), String> { let mut game = from_fen(FEN_PROMOTION_CAPTURE[0])?; let mv = Move::new_with_type(54, 61, MoveType::PromotionCapture(Promote::Queen)); game.board.make_move(&mv, Color::White); assert_eq!(game, from_fen(FEN_PROMOTION_CAPTURE[1])?); Ok(()) } const FEN_CASTLE: [&str; 2] = [ "r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R3K2R w KQkq - 0 1", "r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R4RK1 b kq - 1 1", ]; #[test] fn test_make_move_castle() -> Result<(), String> { let mut game = from_fen(FEN_CASTLE[0])?; let mv = Move::new_with_type(4, 6, MoveType::Castle); game.board.make_move(&mv, Color::White); assert_eq!(game, from_fen(FEN_CASTLE[1])?); Ok(()) } #[test] fn test_parse_from_str() -> Result<(), String> { let mv_str = "a1a8"; let actual = Move::parse_from_str(&mv_str)?; let expected = Move::new(Square::A1, Square::A8); assert_eq!(actual, expected); let mv_str = "b7b8q"; let actual = Move::parse_from_str(&mv_str)?; let expected = Move::new_with_type(Square::B7, Square::B8, MoveType::Promotion(Promote::Queen)); assert_eq!(actual, expected); Ok(()) } #[test] #[should_panic(expected = "Invalid move characters length")] fn test_parse_from_str_panic() -> () { Move::parse_from_str(&"a7a8qk").unwrap(); } }