Compare commits

...

10 commits

Author SHA1 Message Date
1c94f157e6 [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/
2025-07-12 20:26:41 -07:00
5444d8ea3a [bitboard] Add a doc comment to BitBoard::first_occupied_square 2025-07-12 20:20:04 -07:00
d3c94356bd [bitboard] Implement BitBoard::occupied_squares_direction
Iterate a BitBoard in a direction (from leading or trailing edge) based on a
board direction.
2025-07-12 20:19:09 -07:00
6e8c0d3466 [core] Fix an incorrect assertion in the Score doc test
Negate with - instead of with !.
2025-07-12 17:12:34 -07:00
f658903b54 [core] Export Score::CENTIPAWNS_PER_POINT to the crate
This constant is a conversion factor of points to the internal fixed point unit
of centipawns. Points are more familiar to people because pawns are worth 1 pt.

Calculate the scores of the various piece shapes with this constant.
2025-07-12 17:11:52 -07:00
7d3fd4b20a [core] Make Shape::is_promotable() const 2025-07-12 17:09:55 -07:00
b4536ffbe3 [board] Remove a useless .into() call
Clippy pointed this out to me. This .into() call serves no purpose.
2025-07-12 17:09:15 -07:00
0a2dd7e09d [bitboard] Replace some references to BitBoard::full() and BitBoard::empty() with the const values
Two doc tests reference the methods instead of the const variables. Update them.
2025-07-12 17:08:25 -07:00
21a2a37e1f [board] Implement Board::is_king_attacked_from_square
Calculate whether the active color's king is attacked from the provided Square.
2025-07-02 18:30:13 -07:00
89a9588e69 [core] Implement Slider::ray_direction()
This method returns iterators over the directions each kind of slider can move.
2025-07-01 13:02:35 -07:00
5 changed files with 362 additions and 21 deletions

View file

@ -160,9 +160,9 @@ impl BitBoard {
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// assert_eq!(BitBoard::empty().population_count(), 0);
/// assert_eq!(BitBoard::EMPTY.population_count(), 0);
/// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6);
/// assert_eq!(BitBoard::full().population_count(), 64);
/// assert_eq!(BitBoard::FULL.population_count(), 64);
/// ```
#[must_use]
pub const fn population_count(&self) -> u32 {
@ -211,8 +211,8 @@ impl BitBoard {
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// assert!(!BitBoard::empty().is_single_square(), "Empty bitboards represent no squares");
/// assert!(!BitBoard::full().is_single_square(), "Full bitboards represent all the squares");
/// assert!(!BitBoard::EMPTY.is_single_square(), "Empty bitboards represent no squares");
/// assert!(!BitBoard::FULL.is_single_square(), "Full bitboards represent all the squares");
/// assert!(!BitBoard::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares");
/// assert!(BitBoard::new(0b10000000000000).is_single_square());
/// ```
@ -233,6 +233,38 @@ impl BitBoard {
}
}
/// Iterate through the occupied squares in a direction specified by a
/// compass direction. This method is mose useful for bitboards of slider
/// rays so that iteration proceeds in order along the ray's direction.
///
/// ## Examples
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// use chessfriend_core::{Direction, Square};
///
/// let ray = BitBoard::ray(Square::E4, Direction::North);
/// assert_eq!(
/// ray.occupied_squares_direction(Direction::North).collect::<Vec<Square>>(),
/// vec![Square::E5, Square::E6, Square::E7, Square::E8]
/// );
/// ```
///
#[must_use]
pub fn occupied_squares_direction(
&self,
direction: Direction,
) -> Box<dyn Iterator<Item = Square>> {
match direction {
Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => {
Box::new(self.occupied_squares_trailing())
}
Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => {
Box::new(self.occupied_squares_leading())
}
}
}
#[must_use]
pub fn occupied_squares_leading(&self) -> LeadingBitScanner {
LeadingBitScanner::new(self.0)
@ -255,6 +287,12 @@ impl BitBoard {
}
}
/// Get the first occupied square in the given direction.
///
/// ## To-Do
///
/// - Take `direction` by value instead of reference
///
#[must_use]
pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option<Square> {
match direction {

View file

@ -46,7 +46,7 @@ impl Board {
let color = self.unwrap_color(color);
if !self.has_castling_right_unwrapped(color, wing.into()) {
if !self.has_castling_right_unwrapped(color, wing) {
return Err(CastleEvaluationError::NoRights { color, wing });
}

View file

@ -2,27 +2,241 @@
use crate::Board;
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece};
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(color);
let king = self.kings(self.active_color());
let opposing_sight = self.opposing_sight(color);
(king & opposing_sight).is_populated()
}
fn king_bitboard(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::king(color))
/// 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());
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);
if (active_king & sight_from_square).is_populated() {
Ok(sight_from_square)
} else {
Err(())
}
}
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,
}
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
/// 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::Color;
use chessfriend_core::{Square, piece};
#[test]
fn active_color_is_in_check() {
@ -43,4 +257,43 @@ mod tests {
assert!(!board.is_in_check());
}
#[test]
fn king_is_attacked_from_square() {
let board = test_board!(
White King on D3,
Black Rook on H3,
Black Queen on F4
);
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);
}
}

View file

@ -23,7 +23,7 @@ impl Score {
///
/// ```
/// use chessfriend_core::score::Score;
/// assert_eq!(!Score::MIN, Score::MAX);
/// assert_eq!(-Score::MIN, Score::MAX);
/// ```
///
pub const MIN: Score = Score(Value::MIN + 1);
@ -31,7 +31,7 @@ impl Score {
/// The maximum possible value of a score.
pub const MAX: Score = Score(Value::MAX);
const CENTIPAWNS_PER_POINT: f32 = 100.0;
pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0;
#[must_use]
pub const fn new(value: Value) -> Self {

View file

@ -1,10 +1,9 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{Direction, score::Score};
use std::{array, fmt, slice, str::FromStr};
use thiserror::Error;
use crate::score::Score;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Shape {
Pawn = 0,
@ -70,18 +69,21 @@ impl Shape {
}
#[must_use]
pub fn is_promotable(&self) -> bool {
pub const fn is_promotable(&self) -> bool {
matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen)
}
#[must_use]
pub fn score(self) -> Score {
pub const fn score(self) -> Score {
#[allow(clippy::cast_possible_truncation)]
const CP_PER_PT: i32 = Score::CENTIPAWNS_PER_POINT as i32;
match self {
Shape::Pawn => Score::new(100),
Shape::Knight | Shape::Bishop => Score::new(300),
Shape::Rook => Score::new(500),
Shape::Queen => Score::new(900),
Shape::King => Score::new(20000),
Shape::Pawn => Score::new(CP_PER_PT),
Shape::Knight | Shape::Bishop => Score::new(3 * CP_PER_PT),
Shape::Rook => Score::new(5 * CP_PER_PT),
Shape::Queen => Score::new(9 * CP_PER_PT),
Shape::King => Score::new(200 * CP_PER_PT),
}
}
}
@ -93,6 +95,54 @@ pub enum Slider {
Queen,
}
impl Slider {
pub const NUM: usize = 3;
pub const ALL: [Self; Self::NUM] = [Slider::Bishop, Slider::Rook, Slider::Queen];
/// Return the set of directions a slider can move.
///
/// ## Examples
///
/// ```
/// use chessfriend_core::{Direction, Slider};
///
/// assert_eq!(
/// Slider::Bishop.ray_directions().collect::<Vec<Direction>>(),
/// vec![
/// Direction::NorthWest,
/// Direction::NorthEast,
/// Direction::SouthEast,
/// Direction::SouthWest
/// ]
/// );
/// ```
///
#[must_use]
pub fn ray_directions(self) -> Box<dyn Iterator<Item = Direction>> {
match self {
Slider::Bishop => Box::new(
[
Direction::NorthWest,
Direction::NorthEast,
Direction::SouthEast,
Direction::SouthWest,
]
.into_iter(),
),
Slider::Rook => Box::new(
[
Direction::North,
Direction::East,
Direction::South,
Direction::West,
]
.into_iter(),
),
Slider::Queen => Box::new(Direction::ALL.into_iter()),
}
}
}
impl From<Slider> for Shape {
fn from(value: Slider) -> Self {
match value {