[board] First pass at computing checks

Calculate checks using a method described on a blog post I read.[1] This requires
knowing some details about the move that was just made: the origin and target
squares. The piece moved is also passed to the method, though it could look it up.

First look for a direct check, where the piece that just moved is now attacking
the king.

Second, look for revealed checks, where the moved piece reveals a ray from a
sliding piece that attacks the king.

This check information is collected into a CheckingPieces struct that describes
for the newly active player how they can get out of check. The push mask is a
BitBoard of the set of squares the player can move to in order to block the check.
The capture mask is the set of pieces that must be captured to resolve the check.
And the king moves BitBoard shows the squares the king can move to, accounting for
the opposing player's sight.

[1]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/
This commit is contained in:
Eryn Wells 2025-07-12 20:26:41 -07:00
parent 5444d8ea3a
commit 1c94f157e6

View file

@ -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<CheckInfo> = 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<BitBoard, ()> {
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<Color>) -> 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<CheckInfo> {
println!(
"Examining {} on {square} for checks",
Into::<Shape>::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::<BitBoard>::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::<BitBoard>::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);
}
}