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
9 changed files with 352 additions and 155 deletions

113
README.md
View file

@ -1,113 +0,0 @@
ChessFriend
===========
A chess engine written in Rust.
The project is divided into crates for major components of the engine. These
crates are collected in a Cargo workspace. All crates have the `chessfriend_`
naming prefix. The directory structure omits this prefix, and I also frequently
do when referring to them.
## Engine Crates
The engine is divided into several crates, each providing vital types and
functionality.
### `core`
A collection of types for representing core concepts in a chess game and the
engine. Types like `Color` (player or piece color), `Shape` (the shape of a
piece: knight, etc), `Piece` (a piece of a particular color and shape), and
`Score` (for scoring a board position) live here.
### `bitboard`
Implements an efficient BitBoard type. Bitboards use a 64-bit bit field to mark a
square on a board as occupied or free.
### `board`
Implements a `Board` type that represents a moment-in-time board position. FEN
parsing and production lives here.
### `moves`
The `Move` type lives here, along with routines for encoding moves in efficient
forms, parsing moves from algebraic notation, and recording moves in a game
context. Additionally, the move generators for each shape of piece are here.
### `position`
Exports the `Position` type, representing a board position within the context of
a game. As such, it also provides a move list, and methods to make and unmake
moves.
## Support Crates
These crates are for debugging and testing.
### `explorer`
This crate implements a small command-line application for "exploring" board
positions. I meant for this program to be a debugging utility so that I could
examine bitboards and other board structures live. It has grown over time to
also support more aspects of interacting with the engine. So you can also use it
to play a game!
### `perft`
A small Perft utility that executes perft to a given depth from some starting
position.
## Building
Build the engine in the usual Rusty way.
```sh
$ cargo build
```
## Testing
Test in the usual Rusty way.
```sh
$ cargo test
```
The engine has a fairly comprehensive unit test suite, as well as a decent pile
of integration tests.
## Authors
This engine is built entirely by me, Eryn Wells.

View file

@ -46,6 +46,16 @@ impl BitBoard {
pub const EMPTY: BitBoard = BitBoard(u64::MIN);
pub const FULL: BitBoard = BitBoard(u64::MAX);
#[deprecated(note = "Use BitBoard::EMPTY instead")]
pub const fn empty() -> BitBoard {
Self::EMPTY
}
#[deprecated(note = "Use BitBoard::FULL instead")]
pub const fn full() -> BitBoard {
Self::FULL
}
pub const fn new(bits: u64) -> BitBoard {
BitBoard(bits)
}
@ -99,7 +109,7 @@ impl BitBoard {
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// assert!(BitBoard::EMPTY.is_empty());
/// assert!(BitBoard::empty().is_empty());
/// assert!(!BitBoard::full().is_empty());
/// assert!(!BitBoard::new(0b1000).is_empty());
/// ```
@ -115,7 +125,7 @@ impl BitBoard {
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// assert!(!BitBoard::EMPTY.is_populated());
/// assert!(!BitBoard::empty().is_populated());
/// assert!(BitBoard::full().is_populated());
/// assert!(BitBoard::new(0b1).is_populated());
/// ```
@ -554,8 +564,8 @@ mod tests {
let b = bitboard![B5 G7 H3];
assert_eq!(a ^ b, bitboard![B5 C5 H3]);
assert_eq!(a ^ BitBoard::EMPTY, a);
assert_eq!(BitBoard::EMPTY ^ BitBoard::EMPTY, BitBoard::EMPTY);
assert_eq!(a ^ BitBoard::empty(), a);
assert_eq!(BitBoard::empty() ^ BitBoard::empty(), BitBoard::empty());
}
#[test]

View file

@ -110,14 +110,14 @@ pub(super) struct MoveLibrary {
impl MoveLibrary {
const fn new() -> MoveLibrary {
MoveLibrary {
rays: [[BitBoard::EMPTY; Direction::NUM]; Square::NUM],
pawn_attacks: [[BitBoard::EMPTY; Square::NUM]; Color::NUM],
pawn_pushes: [[BitBoard::EMPTY; Square::NUM]; Color::NUM],
knight_moves: [BitBoard::EMPTY; Square::NUM],
bishop_moves: [BitBoard::EMPTY; Square::NUM],
rook_moves: [BitBoard::EMPTY; Square::NUM],
queen_moves: [BitBoard::EMPTY; Square::NUM],
king_moves: [BitBoard::EMPTY; Square::NUM],
rays: [[BitBoard::empty(); Direction::NUM]; Square::NUM],
pawn_attacks: [[BitBoard::empty(); Square::NUM]; Color::NUM],
pawn_pushes: [[BitBoard::empty(); Square::NUM]; Color::NUM],
knight_moves: [BitBoard::empty(); Square::NUM],
bishop_moves: [BitBoard::empty(); Square::NUM],
rook_moves: [BitBoard::empty(); Square::NUM],
queen_moves: [BitBoard::empty(); Square::NUM],
king_moves: [BitBoard::empty(); Square::NUM],
}
}
@ -238,7 +238,7 @@ impl MoveLibrary {
}
fn _generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard {
let mut ray = BitBoard::EMPTY;
let mut ray = BitBoard::empty();
let mut iter = shift(&sq);
while !iter.is_empty() {

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

@ -41,7 +41,7 @@ impl Movement for Piece {
let parameters = Board::castling_parameters(Wing::KingSide, color);
parameters.target.king.into()
} else {
BitBoard::EMPTY
BitBoard::empty()
};
let queenside_target_square = if board
@ -51,7 +51,7 @@ impl Movement for Piece {
let parameters = Board::castling_parameters(Wing::QueenSide, color);
parameters.target.king.into()
} else {
BitBoard::EMPTY
BitBoard::empty()
};
self.sight(square, board) | kingside_target_square | queenside_target_square
@ -99,11 +99,11 @@ mod tests {
#[test]
fn white_pushes_empty_board() {
assert_eq!(
pawn_pushes(Square::E4.into(), Color::White, BitBoard::EMPTY),
pawn_pushes(Square::E4.into(), Color::White, BitBoard::empty()),
bitboard![E5]
);
assert_eq!(
pawn_pushes(Square::E2.into(), Color::White, BitBoard::EMPTY),
pawn_pushes(Square::E2.into(), Color::White, BitBoard::empty()),
bitboard![E3 E4]
);
}
@ -111,11 +111,11 @@ mod tests {
#[test]
fn black_pawn_empty_board() {
assert_eq!(
pawn_pushes(Square::A4.into(), Color::Black, BitBoard::EMPTY),
pawn_pushes(Square::A4.into(), Color::Black, BitBoard::empty()),
bitboard![A3]
);
assert_eq!(
pawn_pushes(Square::B7.into(), Color::Black, BitBoard::EMPTY),
pawn_pushes(Square::B7.into(), Color::Black, BitBoard::empty()),
bitboard![B6 B5]
);
}
@ -124,7 +124,7 @@ mod tests {
fn white_pushes_blocker() {
assert_eq!(
pawn_pushes(Square::C5.into(), Color::White, bitboard![C6]),
BitBoard::EMPTY
BitBoard::empty()
);
assert_eq!(
pawn_pushes(Square::D2.into(), Color::White, bitboard![D4]),
@ -132,7 +132,7 @@ mod tests {
);
assert_eq!(
pawn_pushes(Square::D2.into(), Color::White, bitboard![D3]),
BitBoard::EMPTY
BitBoard::empty()
);
}
@ -140,7 +140,7 @@ mod tests {
fn black_pushes_blocker() {
assert_eq!(
pawn_pushes(Square::C5.into(), Color::Black, bitboard![C4]),
BitBoard::EMPTY
BitBoard::empty()
);
assert_eq!(
pawn_pushes(Square::D7.into(), Color::Black, bitboard![D5]),
@ -148,7 +148,7 @@ mod tests {
);
assert_eq!(
pawn_pushes(Square::D7.into(), Color::Black, bitboard![D6]),
BitBoard::EMPTY
BitBoard::empty()
);
}
}

View file

@ -51,10 +51,11 @@ impl PieceSet {
color_occupancy[color_index] |= bitboard;
shape_occupancy[shape_index] |= bitboard;
counts.increment(color, shape);
for square in bitboard.occupied_squares(&IterationDirection::default()) {
let piece = Piece::new(color, shape);
mailbox.set(piece, square);
counts.increment(color, shape);
}
}
}

View file

@ -17,21 +17,20 @@ impl Counts {
const SQUARE_NUM: u8 = Square::NUM as u8;
let updated_value = self.0[color as usize][shape as usize] + 1;
if updated_value > SQUARE_NUM {
let shape_name = shape.name();
panic!("piece count for {color} {shape_name} overflowed");
if updated_value <= SQUARE_NUM {
self.0[color as usize][shape as usize] = updated_value;
} else {
unreachable!("piece count for {color} {shape} overflowed");
}
self.0[color as usize][shape as usize] = updated_value;
}
pub fn decrement(&mut self, color: Color, shape: Shape) {
let count = self.0[color as usize][shape as usize];
let updated_count = count.checked_sub(1).unwrap_or_else(|| {
let shape_name = shape.name();
panic!("piece count for {color} {shape_name} should not underflow");
});
self.0[color as usize][shape as usize] = updated_count;
if let Some(updated_count) = count.checked_sub(1) {
self.0[color as usize][shape as usize] = updated_count;
} else {
unreachable!("piece count for {color} {shape} underflowed");
}
}
#[cfg(test)]

View file

@ -305,7 +305,7 @@ mod tests {
let piece = piece!(White Pawn);
let sight = piece.sight(Square::E4, &pos);
assert_eq!(sight, BitBoard::EMPTY);
assert_eq!(sight, BitBoard::empty());
}
#[test]

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,
@ -96,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 {