Compare commits

..

1 commit

Author SHA1 Message Date
d1950def00 [core] Add conversions between Score and f32
These conversions assume the float value you want is a point value where 1 point
equals 1 pawn.
2025-06-30 15:32:38 -07:00
10 changed files with 119 additions and 244 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());
/// ```
@ -150,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 {
@ -201,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());
/// ```
@ -223,38 +233,6 @@ 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)
@ -265,24 +243,6 @@ impl BitBoard {
TrailingBitScanner::new(self.0)
}
#[must_use]
pub fn first_occupied_square_direction(&self, direction: Direction) -> Option<Square> {
match direction {
Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => {
self.first_occupied_square_trailing()
}
Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => {
self.first_occupied_square_leading()
}
}
}
/// 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 {
@ -554,8 +514,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

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

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

@ -138,6 +138,20 @@ struct SightInfo {
friendly_occupancy: BitBoard,
}
macro_rules! ray_in_direction {
($square:expr, $blockers:expr, $direction:ident, $first_occupied_square:tt) => {{
let ray = BitBoard::ray($square, Direction::$direction);
let ray_blockers = ray & $blockers;
if let Some(first_occupied_square) = ray_blockers.$first_occupied_square() {
let remainder = BitBoard::ray(first_occupied_square, Direction::$direction);
let attack_ray = ray & !remainder;
attack_ray
} else {
ray
}
}};
}
/// Compute sight of a white pawn.
fn white_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard {
let possible_squares = !info.friendly_occupancy | en_passant_square;
@ -161,27 +175,15 @@ fn knight_sight(info: &SightInfo) -> BitBoard {
BitBoard::knight_moves(info.square)
}
fn ray_in_direction(square: Square, blockers: BitBoard, direction: Direction) -> BitBoard {
let ray = BitBoard::ray(square, direction);
let ray_blockers = ray & blockers;
if let Some(first_occupied_square) = ray_blockers.first_occupied_square_direction(direction) {
let remainder = BitBoard::ray(first_occupied_square, direction);
let attack_ray = ray & !remainder;
attack_ray
} else {
ray
}
}
fn bishop_sight(info: &SightInfo) -> BitBoard {
let bishop = info.square;
let occupancy = info.occupancy;
let sight = ray_in_direction(bishop, occupancy, Direction::NorthEast)
| ray_in_direction(bishop, occupancy, Direction::SouthEast)
| ray_in_direction(bishop, occupancy, Direction::SouthWest)
| ray_in_direction(bishop, occupancy, Direction::NorthWest);
#[rustfmt::skip]
let sight = ray_in_direction!(bishop, occupancy, NorthEast, first_occupied_square_trailing)
| ray_in_direction!(bishop, occupancy, SouthEast, first_occupied_square_leading)
| ray_in_direction!(bishop, occupancy, SouthWest, first_occupied_square_leading)
| ray_in_direction!(bishop, occupancy, NorthWest, first_occupied_square_trailing);
sight
}
@ -190,10 +192,11 @@ fn rook_sight(info: &SightInfo) -> BitBoard {
let rook = info.square;
let occupancy = info.occupancy;
let sight = ray_in_direction(rook, occupancy, Direction::North)
| ray_in_direction(rook, occupancy, Direction::East)
| ray_in_direction(rook, occupancy, Direction::South)
| ray_in_direction(rook, occupancy, Direction::West);
#[rustfmt::skip]
let sight = ray_in_direction!(rook, occupancy, North, first_occupied_square_trailing)
| ray_in_direction!(rook, occupancy, East, first_occupied_square_trailing)
| ray_in_direction!(rook, occupancy, South, first_occupied_square_leading)
| ray_in_direction!(rook, occupancy, West, first_occupied_square_leading);
sight
}
@ -202,14 +205,15 @@ fn queen_sight(info: &SightInfo) -> BitBoard {
let queen = info.square;
let occupancy = info.occupancy;
let sight = ray_in_direction(queen, occupancy, Direction::NorthWest)
| ray_in_direction(queen, occupancy, Direction::North)
| ray_in_direction(queen, occupancy, Direction::NorthEast)
| ray_in_direction(queen, occupancy, Direction::East)
| ray_in_direction(queen, occupancy, Direction::SouthEast)
| ray_in_direction(queen, occupancy, Direction::South)
| ray_in_direction(queen, occupancy, Direction::SouthWest)
| ray_in_direction(queen, occupancy, Direction::West);
#[rustfmt::skip]
let sight = ray_in_direction!(queen, occupancy, NorthWest, first_occupied_square_trailing)
| ray_in_direction!(queen, occupancy, North, first_occupied_square_trailing)
| ray_in_direction!(queen, occupancy, NorthEast, first_occupied_square_trailing)
| ray_in_direction!(queen, occupancy, East, first_occupied_square_trailing)
| ray_in_direction!(queen, occupancy, SouthEast, first_occupied_square_leading)
| ray_in_direction!(queen, occupancy, South, first_occupied_square_leading)
| ray_in_direction!(queen, occupancy, SouthWest, first_occupied_square_leading)
| ray_in_direction!(queen, occupancy, West, first_occupied_square_leading);
sight
}
@ -305,7 +309,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

@ -6,6 +6,7 @@ use std::{
};
pub(crate) type Value = i32;
pub(crate) type FloatValue = f32;
/// A score for a position in centipawns.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
@ -23,7 +24,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,13 +32,31 @@ impl Score {
/// The maximum possible value of a score.
pub const MAX: Score = Score(Value::MAX);
pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0;
const CENTIPAWNS_PER_POINT: f32 = 100.0;
#[must_use]
pub const fn new(value: Value) -> Self {
Self(value)
}
/// Create a [`Score`] from a floating point value. This method assumes the
/// value is a point value where 1 point = 1 pawn. The floating point value
/// will be truncated as part of this conversion.
///
/// ## Examples
///
/// ```
/// use chessfriend_core::score::Score;
/// assert_eq!(Score::from_float(3.1415926), Score(314));
/// assert_ne!(Score::from_float(2.7182818).to_float(), 2.7182818);
/// ```
///
#[must_use]
pub const fn from_float(value: FloatValue) -> Self {
#[allow(clippy::cast_possible_truncation)]
Self((value * Self::CENTIPAWNS_PER_POINT) as Value)
}
/// Returns `true` if this [`Score`] is zero.
///
/// ## Examples
@ -52,6 +71,14 @@ impl Score {
pub const fn is_zero(&self) -> bool {
self.0 == 0
}
/// Return a floating point value in points where 1 point = 1 pawn. This
/// conversion loses precision.
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub const fn to_float(&self) -> FloatValue {
self.0 as f32 / Self::CENTIPAWNS_PER_POINT
}
}
impl Add for Score {

View file

@ -70,21 +70,18 @@ impl Shape {
}
#[must_use]
pub const fn is_promotable(&self) -> bool {
pub fn is_promotable(&self) -> bool {
matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen)
}
#[must_use]
pub const fn score(self) -> Score {
#[allow(clippy::cast_possible_truncation)]
const CP_PER_PT: i32 = Score::CENTIPAWNS_PER_POINT as i32;
pub fn score(self) -> Score {
match self {
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),
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),
}
}
}