diff --git a/board/src/check.rs b/board/src/check.rs index e905498..ca7eca2 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -2,42 +2,241 @@ use crate::Board; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece, Square}; +use chessfriend_core::{Piece, Shape, Slider, Square}; +use std::{convert::Into, ops::BitOr}; impl Board { /// Return whether the active color is in check. #[must_use] pub fn is_in_check(&self) -> bool { let color = self.active_color(); - let king = self.king_bitboard_active(); + let king = self.kings(self.active_color()); let opposing_sight = self.opposing_sight(color); (king & opposing_sight).is_populated() } + /// Calculate checks on the board. + /// + /// ## Panics + /// + /// If the board has multiple kings for any color, this method will panic. + /// + #[must_use] + pub fn calculate_checks( + &self, + piece_moved: &Piece, + last_ply_origin: Square, + last_ply_target: Square, + ) -> CheckingPieces { + let active_king = self.kings(self.active_color()); - fn is_king_attacked_from_square(&self, square: Square) -> bool { - let active_king = self.king_bitboard_active(); + let active_king_square: Square = active_king + .try_into() + .expect("active color has more than one king on the board"); + + let mut checks: Vec = vec![]; + + // Calculate a direct check, where the piece that just moved attacks the + // king. + + if let Ok(sight) = self.is_king_attacked_from_square(last_ply_target) { + checks.push(CheckInfo { + square: last_ply_target, + piece: *piece_moved, + sight, + }); + } + + // Look for revealed checks, where moving a piece opens up an attacking + // ray from a slider to the king. + + let inactive_color = self.active_color().other(); + + for slider in Slider::ALL { + let shape: Shape = slider.into(); + let piece = Piece::new(inactive_color, shape); + + for square in self.find_pieces(piece).occupied_squares_trailing() { + if let Some(check) = + self.calculate_revealed_check(slider, square, active_king, last_ply_origin) + { + checks.push(check); + } + } + } + + let king_moves = BitBoard::king_moves(active_king_square); + // TODO: Bitwise OR with the sight mask of the opposing color. + + CheckingPieces::new(&checks, active_king_square, king_moves) + } + + fn is_king_attacked_from_square(&self, square: Square) -> Result { + let active_king = self.kings(self.active_color()); let sight_from_square = self.sight_piece(square); - (active_king & sight_from_square).is_populated() + if (active_king & sight_from_square).is_populated() { + Ok(sight_from_square) + } else { + Err(()) + } } - fn king_bitboard(&self, color: Option) -> BitBoard { - self.king_bitboard_unwrapped(self.unwrap_color(color)) + fn calculate_revealed_check( + &self, + slider: Slider, + square: Square, + active_king: BitBoard, + last_ply_origin: Square, + ) -> Option { + println!( + "Examining {} on {square} for checks", + Into::::into(slider).name() + ); + + let origin: BitBoard = last_ply_origin.into(); + + let sight = self.sight_piece(square); + let piece_is_attacking_origin = (sight & origin).is_populated(); + if !piece_is_attacking_origin { + println!("does not attack move origin"); + return None; + } + + let origin_and_king = active_king | origin; + println!("origin and king\n{origin_and_king}"); + + for direction in slider.ray_directions() { + let ray = BitBoard::ray(square, direction); + println!("ray\n{ray}"); + + let ray_attacks = ray & origin_and_king; + println!("ray attacks\n{ray_attacks}"); + + // When a check by a slider is revealed by moving another piece, the + // slider will first attack the square the piece was moved from + // (a.k.a. the origin), and also attack the king square. The + // attacked king square must immediately follow the attacked origin + // square, and the attacked origin square must be the first attacked + // square in the ray. + + let mut occupied_squares = ray_attacks.occupied_squares_direction(direction); + if let (Some(first_square), Some(second_square)) = + (occupied_squares.next(), occupied_squares.next()) + { + if first_square != last_ply_origin { + continue; + } + + if Into::::into(second_square) != active_king { + continue; + } + + println!("found check ray\n{ray}"); + + return Some(CheckInfo { + square, + piece: Piece::new(self.active_color(), slider.into()), + sight: ray, + }); + } + } + + None + } +} + +struct CheckInfo { + /// The square the checking piece is on in the current position. + square: Square, + + /// The piece checking the king. + piece: Piece, + + /// The complete sight or direct ray that attacks the king. + sight: BitBoard, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CheckingPieces { + /// Number of checking pieces + count: usize, + + /// A [`BitBoard`] representing the set of pieces to which the color in + /// check can move a piece to block the check. This bitboard includes + /// squares along sight rays of opposing slider pieces. + push_mask: BitBoard, + + /// A [`BitBoard`] representing the set of pieces that can be captured to + /// resolve the check. + capture_mask: BitBoard, + + /// A [`BitBoard`] representing the moves the king can make to get out of + /// check. + king_moves: BitBoard, +} + +impl CheckingPieces { + fn new(checks: &[CheckInfo], king: Square, king_moves: BitBoard) -> Self { + if checks.is_empty() { + return Self { + count: 0, + push_mask: BitBoard::FULL, + capture_mask: BitBoard::EMPTY, + king_moves, + }; + } + + let count = checks.len(); + let push_mask = CheckingPieces::calculate_push_mask(king, checks); + let capture_mask = CheckingPieces::calculate_capture_mask(checks); + + Self { + count, + push_mask, + capture_mask, + king_moves, + } } - fn king_bitboard_active(&self) -> BitBoard { - self.king_bitboard_unwrapped(self.active_color()) + pub fn is_empty(&self) -> bool { + self.count == 0 } - fn king_bitboard_unwrapped(&self, color: Color) -> BitBoard { - self.find_pieces(Piece::king(color)) + /// The number of checking pieces. + pub fn len(&self) -> usize { + self.count + } + + /// A [`BitBoard`] representing the set of pieces that must be captured to + /// resolve check. + fn calculate_capture_mask(checks: &[CheckInfo]) -> BitBoard { + if checks.is_empty() { + BitBoard::FULL + } else { + checks + .iter() + .map(|c| Into::::into(c.square)) + .fold(BitBoard::EMPTY, BitOr::bitor) + } + } + + /// A [`BitBoard`] representing the set of squares to which a player can + /// move a piece to block a checking piece. + fn calculate_push_mask(king: Square, checks: &[CheckInfo]) -> BitBoard { + let king_moves = BitBoard::king_moves(king); + let push_mask = checks + .iter() + .map(|c| c.sight) + .fold(BitBoard::EMPTY, BitOr::bitor); + + king_moves & !push_mask } } #[cfg(test)] mod tests { use crate::test_board; - use chessfriend_core::Square; + use chessfriend_core::{Square, piece}; #[test] fn active_color_is_in_check() { @@ -67,8 +266,34 @@ mod tests { Black Queen on F4 ); - assert!(board.is_king_attacked_from_square(Square::H3)); - assert!(!board.is_king_attacked_from_square(Square::F4)); - assert!(!board.is_king_attacked_from_square(Square::A3)); + assert!(board.is_king_attacked_from_square(Square::H3).is_ok()); + assert!(board.is_king_attacked_from_square(Square::F4).is_err()); + assert!(board.is_king_attacked_from_square(Square::A3).is_err()); + } + + #[test] + fn calculate_two_checks() { + let board = test_board!( + White King on D3, + Black Rook on H3, + Black Queen on F5 + ); + + let checks = board.calculate_checks(&piece!(Black Queen), Square::F3, Square::F5); + + assert!(!checks.is_empty()); + assert_eq!(checks.len(), 2); + } + + #[test] + fn calculate_en_passant_revealed_check() { + let board = test_board!(White, [ + White King on D4, + Black Pawn on E3, + Black Rook on H4 + ], E4); + + let checks = board.calculate_checks(&piece!(Black Pawn), Square::F4, Square::E3); + assert_eq!(checks.len(), 1); } }