Compare commits

..

1 commit

Author SHA1 Message Date
12cebf1f2b WIP 2025-06-17 16:24:28 -07:00
32 changed files with 346 additions and 948 deletions

2
Cargo.lock generated
View file

@ -71,6 +71,8 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chessfriend"
version = "0.1.0"
dependencies = [
]
[[package]]
name = "chessfriend_bitboard"

View file

@ -10,7 +10,3 @@ members = [
"position",
]
resolver = "3"
[profile.release-debug]
inherits = "release"
debug = true

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

@ -43,8 +43,16 @@ macro_rules! moves_getter {
}
impl BitBoard {
pub const EMPTY: BitBoard = BitBoard(u64::MIN);
pub const FULL: BitBoard = BitBoard(u64::MAX);
const EMPTY: BitBoard = BitBoard(u64::MIN);
const FULL: BitBoard = BitBoard(u64::MAX);
pub const fn empty() -> BitBoard {
Self::EMPTY
}
pub const fn full() -> BitBoard {
Self::FULL
}
pub const fn new(bits: u64) -> BitBoard {
BitBoard(bits)
@ -99,7 +107,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 +123,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 +158,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 +209,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 +231,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 +241,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 +512,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

@ -14,7 +14,7 @@ pub use direction::IterationDirection;
macro_rules! bitboard {
($($sq:ident)* $(,)?) => {
{
let mut bitboard = $crate::BitBoard::EMPTY;
let mut bitboard = $crate::BitBoard::empty();
$(bitboard.set(chessfriend_core::Square::$sq);)*
bitboard
}

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

@ -1,13 +1,13 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{
CastleRights, PieceSet,
PieceSet, castle,
display::DiagramFormatter,
piece_sets::{Counter, PlacePieceError, PlacePieceStrategy},
piece_sets::{PlacePieceError, PlacePieceStrategy},
zobrist::{ZobristHash, ZobristState},
};
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Shape, Square};
use chessfriend_core::{Color, Piece, Shape, Square, Wing};
use std::sync::Arc;
pub type HalfMoveClock = u32;
@ -17,7 +17,7 @@ pub type FullMoveClock = u32;
pub struct Board {
active_color: Color,
pieces: PieceSet,
castling_rights: CastleRights,
castling_rights: castle::Rights,
en_passant_target: Option<Square>,
pub half_move_clock: HalfMoveClock,
pub full_move_number: FullMoveClock,
@ -92,27 +92,59 @@ impl Board {
impl Board {
#[must_use]
pub fn castling_rights(&self) -> &CastleRights {
&self.castling_rights
pub fn castling_rights(&self) -> castle::Rights {
self.castling_rights
}
pub(crate) fn castling_rights_mut(&mut self) -> &mut CastleRights {
&mut self.castling_rights
}
/// Replace castling rights with new rights wholesale.
pub fn set_castling_rights(&mut self, rights: CastleRights) {
pub fn set_castling_rights(&mut self, rights: castle::Rights) {
if rights == self.castling_rights {
return;
}
let old_rights = self.castling_rights;
self.castling_rights = rights;
self.update_zobrist_hash_castling_rights(old_rights);
}
pub(crate) fn update_zobrist_hash_castling_rights(&mut self, old_rights: CastleRights) {
#[must_use]
pub fn active_color_has_castling_right(&self, wing: Wing) -> bool {
self.color_has_castling_right_unwrapped(self.active_color, wing)
}
#[must_use]
pub fn color_has_castling_right(&self, color: Option<Color>, wing: Wing) -> bool {
self.color_has_castling_right_unwrapped(self.unwrap_color(color), wing)
}
#[must_use]
pub fn color_has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool {
self.castling_rights.color_has_right(color, wing)
}
pub fn grant_castling_right(&mut self, color: Color, wing: Wing) {
let old_rights = self.castling_rights;
self.castling_rights.grant(color, wing);
self.update_zobrist_hash_castling_rights(old_rights);
}
pub fn revoke_all_castling_rights(&mut self) {
let old_rights = self.castling_rights;
self.castling_rights.revoke_all();
self.update_zobrist_hash_castling_rights(old_rights);
}
pub fn revoke_castling_right(&mut self, color: Option<Color>, wing: Wing) {
let color = self.unwrap_color(color);
self.revoke_castling_right_unwrapped(color, wing);
}
pub fn revoke_castling_right_unwrapped(&mut self, color: Color, wing: Wing) {
let old_rights = self.castling_rights;
self.castling_rights.revoke(color, wing);
self.update_zobrist_hash_castling_rights(old_rights);
}
fn update_zobrist_hash_castling_rights(&mut self, old_rights: castle::Rights) {
let new_rights = self.castling_rights;
if old_rights == new_rights {
return;
@ -122,18 +154,6 @@ impl Board {
zobrist.update_modifying_castling_rights(new_rights, old_rights);
}
}
pub(crate) fn castling_king(&self, square: Square) -> Option<Piece> {
let active_color = self.active_color();
self.get_piece(square)
.filter(|piece| piece.color == active_color && piece.is_king())
}
pub(crate) fn castling_rook(&self, square: Square) -> Option<Piece> {
let active_color = self.active_color();
self.get_piece(square)
.filter(|piece| piece.color == active_color && piece.is_rook())
}
}
impl Board {
@ -219,11 +239,6 @@ impl Board {
removed_piece
}
#[must_use]
pub fn count_piece(&self, piece: &Piece) -> Counter {
self.pieces.count(piece)
}
}
impl Board {
@ -311,6 +326,45 @@ impl Board {
}
}
impl Board {
#[must_use]
pub fn is_valid(&self) -> bool {
true
}
/// Determines whether `self` is a repetition of the given [`Board`]
/// according to the standard chess rules. A position is considered
/// equal or a repetition of another position if:
///
/// 1. The placement of all pieces of all colors is the same, i.e. same
/// pieces on same squares
/// 2. Castling rights are equal
/// 3. The target position does not have an en passant capture available
///
/// ## See Also
///
/// * The [Chess Programming][1] wiki page on repetitions
///
/// [1]: https://www.chessprogramming.org/Repetitions
///
#[must_use]
pub fn is_repetition(&self, other: &Board) -> bool {
if other.en_passant_target.is_some() {
return false;
}
if self.castling_rights != other.castling_rights {
return false;
}
if self.pieces != other.pieces {
return false;
}
true
}
}
impl Board {
pub fn display(&self) -> DiagramFormatter<'_> {
DiagramFormatter::new(self)

View file

@ -4,10 +4,10 @@ mod parameters;
mod rights;
pub use parameters::Parameters;
pub use rights::{CastleRightsOption, Rights};
pub use rights::Rights;
use crate::{Board, CastleParameters};
use chessfriend_core::{Color, Wing};
use chessfriend_core::{Color, Piece, Square, Wing};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
@ -46,7 +46,7 @@ impl Board {
let color = self.unwrap_color(color);
if !self.has_castling_right_unwrapped(color, wing) {
if !self.color_has_castling_right_unwrapped(color, wing) {
return Err(CastleEvaluationError::NoRights { color, wing });
}
@ -76,60 +76,17 @@ impl Board {
Ok(parameters)
}
}
impl Board {
#[must_use]
pub fn has_castling_right(&self, color: Option<Color>, wing: Wing) -> bool {
self.has_castling_right_unwrapped(self.unwrap_color(color), wing)
pub(crate) fn castling_king(&self, square: Square) -> Option<Piece> {
let active_color = self.active_color();
self.get_piece(square)
.filter(|piece| piece.color == active_color && piece.is_king())
}
#[must_use]
pub fn has_castling_right_active(&self, wing: Wing) -> bool {
self.has_castling_right_unwrapped(self.active_color(), wing)
}
#[must_use]
pub fn has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool {
self.castling_rights().get(color, wing.into())
}
}
impl Board {
pub fn grant_castling_rights(&mut self, color: Option<Color>, rights: CastleRightsOption) {
let color = self.unwrap_color(color);
self.grant_castling_rights_unwrapped(color, rights);
}
pub fn grant_castling_rights_active(&mut self, rights: CastleRightsOption) {
self.grant_castling_rights_unwrapped(self.active_color(), rights);
}
pub fn grant_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) {
let old_rights = *self.castling_rights();
self.castling_rights_mut().grant(color, rights);
self.update_zobrist_hash_castling_rights(old_rights);
}
}
impl Board {
pub fn revoke_all_castling_rights(&mut self) {
self.castling_rights_mut().revoke_all();
}
pub fn revoke_castling_rights(&mut self, color: Option<Color>, rights: CastleRightsOption) {
let color = self.unwrap_color(color);
self.revoke_castling_rights_unwrapped(color, rights);
}
pub fn revoke_castling_rights_active(&mut self, rights: CastleRightsOption) {
self.revoke_castling_rights_unwrapped(self.active_color(), rights);
}
pub fn revoke_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) {
let old_rights = *self.castling_rights();
self.castling_rights_mut().revoke(color, rights);
self.update_zobrist_hash_castling_rights(old_rights);
pub(crate) fn castling_rook(&self, square: Square) -> Option<Piece> {
let active_color = self.active_color();
self.get_piece(square)
.filter(|piece| piece.color == active_color && piece.is_rook())
}
}
@ -147,8 +104,8 @@ mod tests {
White Rook on H1
);
assert!(board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(board.has_castling_right_unwrapped(Color::White, Wing::QueenSide));
assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide));
}
#[test]

View file

@ -1,14 +1,6 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_core::{Color, Wing};
use std::fmt;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CastleRightsOption {
Wing(Wing),
All,
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Rights(u8);
@ -20,16 +12,16 @@ impl Rights {
/// as long as they have not moved their king, or the rook on that side of
/// the board.
#[must_use]
pub fn get(self, color: Color, option: CastleRightsOption) -> bool {
(self.0 & Self::flags(color, option)) != 0
pub fn color_has_right(self, color: Color, wing: Wing) -> bool {
(self.0 & (1 << Self::flag_offset(color, wing))) != 0
}
pub fn grant(&mut self, color: Color, option: CastleRightsOption) {
self.0 |= Self::flags(color, option);
pub fn grant(&mut self, color: Color, wing: Wing) {
self.0 |= 1 << Self::flag_offset(color, wing);
}
pub fn revoke(&mut self, color: Color, option: CastleRightsOption) {
self.0 &= !Self::flags(color, option);
pub fn revoke(&mut self, color: Color, wing: Wing) {
self.0 &= !(1 << Self::flag_offset(color, wing));
}
/// Revoke castling rights for all colors and all sides of the board.
@ -39,14 +31,14 @@ impl Rights {
}
impl Rights {
pub(crate) fn as_index(self) -> usize {
pub(crate) fn as_index(&self) -> usize {
self.0 as usize
}
}
impl Rights {
const fn flags(color: Color, option: CastleRightsOption) -> u8 {
option.as_flags() << (color as u8 * 2)
fn flag_offset(color: Color, wing: Wing) -> usize {
((color as usize) << 1) + wing as usize
}
}
@ -62,55 +54,36 @@ impl Default for Rights {
}
}
impl CastleRightsOption {
#[must_use]
pub const fn as_flags(self) -> u8 {
match self {
Self::Wing(wing) => 1 << (wing as u8),
Self::All => (1 << Wing::KingSide as u8) | (1 << Wing::QueenSide as u8),
}
}
}
impl From<Wing> for CastleRightsOption {
fn from(value: Wing) -> Self {
Self::Wing(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bitfield_offsets() {
assert_eq!(Rights::flags(Color::White, Wing::KingSide.into()), 1);
assert_eq!(Rights::flags(Color::White, Wing::QueenSide.into()), 1 << 1);
assert_eq!(Rights::flags(Color::Black, Wing::KingSide.into()), 1 << 2);
assert_eq!(Rights::flags(Color::Black, Wing::QueenSide.into()), 1 << 3);
assert_eq!(Rights::flags(Color::White, CastleRightsOption::All), 0b11);
assert_eq!(Rights::flags(Color::Black, CastleRightsOption::All), 0b1100);
assert_eq!(Rights::flag_offset(Color::White, Wing::KingSide), 0);
assert_eq!(Rights::flag_offset(Color::White, Wing::QueenSide), 1);
assert_eq!(Rights::flag_offset(Color::Black, Wing::KingSide), 2);
assert_eq!(Rights::flag_offset(Color::Black, Wing::QueenSide), 3);
}
#[test]
fn default_rights() {
let mut rights = Rights::default();
assert!(rights.get(Color::White, Wing::KingSide.into()));
assert!(rights.get(Color::White, Wing::QueenSide.into()));
assert!(rights.get(Color::Black, Wing::KingSide.into()));
assert!(rights.get(Color::Black, Wing::QueenSide.into()));
assert!(rights.color_has_right(Color::White, Wing::KingSide));
assert!(rights.color_has_right(Color::White, Wing::QueenSide));
assert!(rights.color_has_right(Color::Black, Wing::KingSide));
assert!(rights.color_has_right(Color::Black, Wing::QueenSide));
rights.revoke(Color::White, Wing::QueenSide.into());
assert!(rights.get(Color::White, Wing::KingSide.into()));
assert!(!rights.get(Color::White, Wing::QueenSide.into()));
assert!(rights.get(Color::Black, Wing::KingSide.into()));
assert!(rights.get(Color::Black, Wing::QueenSide.into()));
rights.revoke(Color::White, Wing::QueenSide);
assert!(rights.color_has_right(Color::White, Wing::KingSide));
assert!(!rights.color_has_right(Color::White, Wing::QueenSide));
assert!(rights.color_has_right(Color::Black, Wing::KingSide));
assert!(rights.color_has_right(Color::Black, Wing::QueenSide));
rights.grant(Color::White, Wing::QueenSide.into());
assert!(rights.get(Color::White, Wing::KingSide.into()));
assert!(rights.get(Color::White, Wing::QueenSide.into()));
assert!(rights.get(Color::Black, Wing::KingSide.into()));
assert!(rights.get(Color::Black, Wing::QueenSide.into()));
rights.grant(Color::White, Wing::QueenSide);
assert!(rights.color_has_right(Color::White, Wing::KingSide));
assert!(rights.color_has_right(Color::White, Wing::QueenSide));
assert!(rights.color_has_right(Color::Black, Wing::KingSide));
assert!(rights.color_has_right(Color::Black, Wing::QueenSide));
}
}

View file

@ -5,10 +5,18 @@ use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece};
impl Board {
/// Return whether the active color is in check.
#[must_use]
pub fn is_in_check(&self) -> bool {
let color = self.active_color();
pub fn active_color_is_in_check(&self) -> bool {
self.unwrapped_color_is_in_check(self.active_color())
}
#[must_use]
pub fn color_is_in_check(&self, color: Option<Color>) -> bool {
self.unwrapped_color_is_in_check(self.unwrap_color(color))
}
#[must_use]
pub fn unwrapped_color_is_in_check(&self, color: Color) -> bool {
let king = self.king_bitboard(color);
let opposing_sight = self.opposing_sight(color);
(king & opposing_sight).is_populated()
@ -31,7 +39,7 @@ mod tests {
Black Rook on F3,
);
assert!(board.is_in_check());
assert!(board.unwrapped_color_is_in_check(Color::White));
}
#[test]
@ -41,6 +49,6 @@ mod tests {
Black Rook on B4,
);
assert!(!board.is_in_check());
assert!(!board.unwrapped_color_is_in_check(Color::White));
}
}

View file

@ -9,10 +9,9 @@ use thiserror::Error;
#[macro_export]
macro_rules! fen {
($fen_string:literal) => {{
use $crate::fen::FromFenStr;
$crate::Board::from_fen_str($fen_string)
}};
($fen_string:literal) => {
Board::from_fen_str($fen_string)
};
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
@ -25,16 +24,12 @@ pub enum ToFenStrError {
pub enum FromFenStrError {
#[error("missing {0} field")]
MissingField(Field),
#[error("missing piece placement")]
MissingPlacement,
#[error("invalid value")]
InvalidValue,
#[error("{0}")]
ParseIntError(#[from] std::num::ParseIntError),
#[error("{0}")]
ParseSquareError(#[from] ParseSquareError),
}
@ -127,12 +122,12 @@ impl ToFenStr for Board {
(Color::Black, Wing::KingSide),
(Color::Black, Wing::QueenSide),
]
.map(|(color, wing)| {
if !self.has_castling_right_unwrapped(color, wing) {
.map(|(color, castle)| {
if !self.color_has_castling_right_unwrapped(color, castle) {
return "";
}
match (color, wing) {
match (color, castle) {
(Color::White, Wing::KingSide) => "K",
(Color::White, Wing::QueenSide) => "Q",
(Color::Black, Wing::KingSide) => "k",
@ -231,23 +226,20 @@ impl FromFenStr for Board {
)?;
board.set_active_color(active_color);
let color_wing_from_char = |ch| match ch {
'K' => Some((Color::White, Wing::KingSide)),
'Q' => Some((Color::White, Wing::QueenSide)),
'k' => Some((Color::Black, Wing::KingSide)),
'q' => Some((Color::Black, Wing::QueenSide)),
_ => None,
};
let castling_rights = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::CastlingRights))?;
board.revoke_all_castling_rights();
if castling_rights != "-" {
if castling_rights == "-" {
board.revoke_all_castling_rights();
} else {
for ch in castling_rights.chars() {
let (color, wing) =
color_wing_from_char(ch).ok_or(FromFenStrError::InvalidValue)?;
board.grant_castling_rights_unwrapped(color, wing.into());
match ch {
'K' => board.grant_castling_right(Color::White, Wing::KingSide),
'Q' => board.grant_castling_right(Color::White, Wing::QueenSide),
'k' => board.grant_castling_right(Color::Black, Wing::KingSide),
'q' => board.grant_castling_right(Color::Black, Wing::QueenSide),
_ => return Err(FromFenStrError::InvalidValue),
};
}
}

View file

@ -4,16 +4,16 @@
//! of squares a piece can move to. For all pieces except pawns, the Movement
//! set is equal to the Sight set.
use crate::{Board, sight::Sight};
use crate::{sight::Sight, Board};
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
impl Board {
pub fn movement_piece(&self, square: Square) -> BitBoard {
pub fn movement(&self, square: Square) -> BitBoard {
if let Some(piece) = self.get_piece(square) {
piece.movement(square, self)
} else {
BitBoard::EMPTY
BitBoard::empty()
}
}
}
@ -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
@ -93,17 +93,17 @@ fn pawn_pushes(pawn: BitBoard, color: Color, occupancy: BitBoard) -> BitBoard {
#[cfg(test)]
mod tests {
use super::pawn_pushes;
use chessfriend_bitboard::{BitBoard, bitboard};
use chessfriend_bitboard::{bitboard, BitBoard};
use chessfriend_core::{Color, Square};
#[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

@ -1,9 +1,8 @@
// Eryn Wells <eryn@erynwells.me>
mod counts;
mod mailbox;
use self::{counts::Counts, mailbox::Mailbox};
use self::mailbox::Mailbox;
use chessfriend_bitboard::{BitBoard, IterationDirection};
use chessfriend_core::{Color, Piece, Shape, Square};
use std::{
@ -12,8 +11,6 @@ use std::{
};
use thiserror::Error;
pub(crate) use counts::Counter;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum PlacePieceStrategy {
#[default]
@ -21,7 +18,7 @@ pub enum PlacePieceStrategy {
PreserveExisting,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[derive(Debug, Error, Eq, PartialEq)]
pub enum PlacePieceError {
#[error("cannot place piece on {square} with existing {piece}")]
ExisitingPiece { piece: Piece, square: Square },
@ -32,7 +29,6 @@ pub enum PlacePieceError {
#[derive(Clone, Debug, Default, Eq)]
pub struct PieceSet {
mailbox: Mailbox,
counts: Counts,
color_occupancy: [BitBoard; Color::NUM],
shape_occupancy: [BitBoard; Shape::NUM],
}
@ -40,11 +36,10 @@ pub struct PieceSet {
impl PieceSet {
pub(crate) fn new(pieces: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self {
let mut mailbox = Mailbox::default();
let mut counts = Counts::default();
let mut color_occupancy: [BitBoard; Color::NUM] = Default::default();
let mut shape_occupancy: [BitBoard; Shape::NUM] = Default::default();
for (color_index, color) in Color::into_iter().enumerate() {
for (color_index, color) in Color::iter().enumerate() {
for (shape_index, shape) in Shape::into_iter().enumerate() {
let bitboard = pieces[color_index][shape_index];
@ -52,16 +47,14 @@ impl PieceSet {
shape_occupancy[shape_index] |= bitboard;
for square in bitboard.occupied_squares(&IterationDirection::default()) {
let piece = Piece::new(color, shape);
let piece = Piece::new(*color, shape);
mailbox.set(piece, square);
counts.increment(color, shape);
}
}
}
Self {
mailbox,
counts,
color_occupancy,
shape_occupancy,
}
@ -101,10 +94,6 @@ impl PieceSet {
self.mailbox.get(square)
}
pub(crate) fn count(&self, piece: &Piece) -> Counter {
self.counts.get(piece.color, piece.shape)
}
// TODO: Rename this. Maybe get_all() is better?
pub(crate) fn find_pieces(&self, piece: Piece) -> BitBoard {
let color_occupancy = self.color_occupancy[piece.color as usize];
@ -131,7 +120,6 @@ impl PieceSet {
self.color_occupancy[color as usize].set(square);
self.shape_occupancy[shape as usize].set(square);
self.counts.increment(color, shape);
self.mailbox.set(piece, square);
Ok(existing_piece)
@ -139,12 +127,8 @@ impl PieceSet {
pub(crate) fn remove(&mut self, square: Square) -> Option<Piece> {
if let Some(piece) = self.mailbox.get(square) {
let color_index = piece.color as usize;
let shape_index = piece.shape as usize;
self.color_occupancy[color_index].clear(square);
self.shape_occupancy[shape_index].clear(square);
self.counts.decrement(piece.color, piece.shape);
self.color_occupancy[piece.color as usize].clear(square);
self.shape_occupancy[piece.shape as usize].clear(square);
self.mailbox.remove(square);
Some(piece)

View file

@ -1,61 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_core::{Color, Shape, Square};
pub(crate) type Counter = u8;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(super) struct Counts([[Counter; Shape::NUM]; Color::NUM]);
impl Counts {
pub fn get(&self, color: Color, shape: Shape) -> Counter {
self.0[color as usize][shape as usize]
}
pub fn increment(&mut self, color: Color, shape: Shape) {
#[allow(clippy::cast_possible_truncation)]
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");
}
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;
}
#[cfg(test)]
fn set(&mut self, color: Color, shape: Shape, value: u8) {
self.0[color as usize][shape as usize] = value;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "underflowed")]
fn underflow() {
let mut counts = Counts::default();
counts.decrement(Color::White, Shape::Queen);
}
#[test]
#[should_panic(expected = "overflowed")]
fn overflow() {
let mut counts = Counts::default();
counts.set(Color::White, Shape::Queen, 64);
counts.increment(Color::White, Shape::Queen);
}
}

View file

@ -7,6 +7,10 @@ use std::iter::FromIterator;
pub(crate) struct Mailbox([Option<Piece>; Square::NUM]);
impl Mailbox {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, square: Square) -> Option<Piece> {
self.0[square as usize]
}
@ -42,7 +46,7 @@ impl From<[Option<Piece>; Square::NUM]> for Mailbox {
impl FromIterator<(Square, Piece)> for Mailbox {
fn from_iter<T: IntoIterator<Item = (Square, Piece)>>(iter: T) -> Self {
iter.into_iter()
.fold(Self::default(), |mut mailbox, (square, piece)| {
.fold(Self::new(), |mut mailbox, (square, piece)| {
mailbox.set(piece, square);
mailbox
})
@ -57,7 +61,7 @@ mod tests {
#[test]
fn iter_returns_all_pieces() {
let mut mailbox = Mailbox::default();
let mut mailbox = Mailbox::new();
mailbox.set(piece!(White Queen), Square::C3);
mailbox.set(piece!(White Rook), Square::H8);
mailbox.set(piece!(Black Bishop), Square::E4);

View file

@ -23,31 +23,16 @@ use std::ops::BitOr;
impl Board {
/// Compute sight of the piece on the given square.
pub fn sight_piece(&self, square: Square) -> BitBoard {
pub fn sight(&self, square: Square) -> BitBoard {
if let Some(piece) = self.get_piece(square) {
piece.sight(square, self)
} else {
BitBoard::EMPTY
BitBoard::empty()
}
}
/// Calculate sight of all pieces of the given [`Color`]. If `color` is
/// `None`, calculate sight of the active color.
pub fn sight(&self, color: Option<Color>) -> BitBoard {
self.sight_unwrapped(self.unwrap_color(color))
}
/// Calculate sight of all pieces of the active color.
pub fn sight_active(&self) -> BitBoard {
self.sight_unwrapped(self.active_color())
}
/// Calculate sight of all pieces of the given [`Color`].
pub fn sight_unwrapped(&self, color: Color) -> BitBoard {
self.friendly_occupancy(color)
.occupied_squares_leading()
.map(|square| self.sight_piece(square))
.fold(BitBoard::EMPTY, BitOr::bitor)
pub fn active_sight(&self) -> BitBoard {
self.friendly_sight(self.active_color())
}
/// A [`BitBoard`] of all squares the given color can see.
@ -55,8 +40,8 @@ impl Board {
// TODO: Probably want to implement a caching layer here.
self.friendly_occupancy(color)
.occupied_squares(&IterationDirection::default())
.map(|square| self.sight_piece(square))
.fold(BitBoard::EMPTY, BitOr::bitor)
.map(|square| self.sight(square))
.fold(BitBoard::empty(), BitOr::bitor)
}
pub fn active_color_opposing_sight(&self) -> BitBoard {
@ -75,7 +60,7 @@ impl Board {
Some(self.friendly_sight(c))
}
})
.fold(BitBoard::EMPTY, BitOr::bitor)
.fold(BitBoard::empty(), BitOr::bitor)
}
}
@ -138,6 +123,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 +160,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 +177,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 +190,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
}
@ -255,7 +244,7 @@ mod tests {
White King on E4,
);
let sight = pos.sight_active();
let sight = pos.active_sight();
assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]);
}
@ -278,8 +267,8 @@ mod tests {
mod pawn {
use crate::{sight::Sight, test_board};
use chessfriend_bitboard::{BitBoard, bitboard};
use chessfriend_core::{Square, piece};
use chessfriend_bitboard::{bitboard, BitBoard};
use chessfriend_core::{piece, Square};
sight_test!(e4_pawn, piece!(White Pawn), Square::E4, bitboard![D5 F5]);
@ -305,7 +294,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,7 +1,6 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{Direction, score};
use std::fmt;
use crate::Direction;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
@ -57,18 +56,10 @@ impl Color {
Color::Black => "black",
}
}
#[must_use]
pub const fn score_factor(self) -> score::Value {
match self {
Color::White => 1,
Color::Black => -1,
}
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
impl std::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",

View file

@ -115,9 +115,7 @@ macro_rules! range_bound_struct {
coordinate_enum!(
Direction,
[
North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest
]
[North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest]
);
impl Direction {
@ -264,7 +262,7 @@ impl Square {
#[must_use]
pub unsafe fn from_index_unchecked(x: u8) -> Square {
debug_assert!((x as usize) < Self::NUM);
Self::ALL[x as usize]
Self::try_from(x).unwrap_unchecked()
}
#[inline]

View file

@ -4,7 +4,6 @@ pub mod colors;
pub mod coordinates;
pub mod pieces;
pub mod random;
pub mod score;
pub mod shapes;
mod macros;

View file

@ -1,126 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use std::{
fmt,
ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign},
};
pub(crate) type Value = i32;
/// A score for a position in centipawns.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Score(Value);
impl Score {
pub const ZERO: Score = Score(0);
/// The minimum possible value of a score. Notably, this is *not* the
/// minimum value for the inner integer value so negation works correctly.
/// This property is important during search, which relies on being able to
/// negate "infinity".
///
/// ## Examples
///
/// ```
/// use chessfriend_core::score::Score;
/// assert_eq!(-Score::MIN, Score::MAX);
/// ```
///
pub const MIN: Score = Score(Value::MIN + 1);
/// The maximum possible value of a score.
pub const MAX: Score = Score(Value::MAX);
pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0;
#[must_use]
pub const fn new(value: Value) -> Self {
Self(value)
}
/// Returns `true` if this [`Score`] is zero.
///
/// ## Examples
///
/// ```
/// use chessfriend_core::score::Score;
/// assert!(Score::ZERO.is_zero());
/// assert!(Score::new(0).is_zero());
/// ```
///
#[must_use]
pub const fn is_zero(&self) -> bool {
self.0 == 0
}
}
impl Add for Score {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Score(self.0 + rhs.0)
}
}
impl AddAssign for Score {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl Sub for Score {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Score(self.0 - rhs.0)
}
}
impl SubAssign for Score {
fn sub_assign(&mut self, rhs: Self) {
self.0 -= rhs.0;
}
}
impl Mul<Value> for Score {
type Output = Self;
fn mul(self, rhs: Value) -> Self::Output {
Score(self.0 * rhs)
}
}
impl Mul<Score> for Value {
type Output = Score;
fn mul(self, rhs: Score) -> Self::Output {
Score(self * rhs.0)
}
}
impl Neg for Score {
type Output = Self;
fn neg(self) -> Self::Output {
Score(-self.0)
}
}
impl From<Value> for Score {
fn from(value: Value) -> Self {
Score(value)
}
}
impl fmt::Display for Score {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = self.0;
if *self == Self::MAX {
write!(f, "INF")
} else if *self == Self::MIN {
write!(f, "-INF")
} else {
write!(f, "{value}cp")
}
}
}

View file

@ -3,8 +3,6 @@
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,22 +68,8 @@ impl Shape {
}
#[must_use]
pub const 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;
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),
}
pub fn is_promotable(&self) -> bool {
Self::PROMOTABLE_SHAPES.contains(self)
}
}

View file

@ -1,11 +1,14 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_board::{Board, ZobristState, fen::FromFenStr};
use chessfriend_core::{Color, Piece, Shape, Square, Wing, random::RandomNumberGenerator};
use chessfriend_moves::{GeneratedMove, ValidateMove, algebraic::AlgebraicMoveComponents};
use chessfriend_board::ZobristState;
use chessfriend_board::{Board, fen::FromFenStr};
use chessfriend_core::random::RandomNumberGenerator;
use chessfriend_core::{Color, Piece, Shape, Square};
use chessfriend_moves::GeneratedMove;
use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr};
use clap::{Arg, Command, value_parser};
use rustyline::{DefaultEditor, error::ReadlineError};
use clap::{Arg, Command};
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
use std::sync::Arc;
use thiserror::Error;
@ -42,7 +45,6 @@ fn command_line() -> Command {
.subcommand_help_heading("COMMANDS")
.help_template(PARSER_TEMPLATE)
.subcommand(Command::new("fen").about("Print the current position as a FEN string"))
.subcommand(Command::new("flags").about("Print flags for the current position"))
.subcommand(
Command::new("load")
.arg(Arg::new("fen").required(true))
@ -51,7 +53,8 @@ fn command_line() -> Command {
)
.subcommand(
Command::new("make")
.arg(Arg::new("move").required(true))
.arg(Arg::new("from").required(true))
.arg(Arg::new("to").required(true))
.alias("m")
.about("Make a move"),
)
@ -65,7 +68,7 @@ fn command_line() -> Command {
)
.subcommand(
Command::new("sight")
.arg(Arg::new("square").required(false))
.arg(Arg::new("square").required(true))
.about("Show sight of a piece on a square"),
)
.subcommand(
@ -78,14 +81,6 @@ fn command_line() -> Command {
.arg(Arg::new("square").required(true))
.about("Show moves of a piece on a square"),
)
.subcommand(
Command::new("perft")
.arg(Arg::new("depth")
.required(true)
.value_parser(value_parser!(usize))
)
.about("Run Perft on the current position to the given depth")
)
.subcommand(
Command::new("reset")
.subcommand(Command::new("clear").about("Reset to a cleared board"))
@ -112,12 +107,6 @@ enum CommandHandlingError<'a> {
#[error("no piece on {0}")]
NoPiece(Square),
#[error("{value:?} is not a valid value for {argument_name:?}")]
ValueError {
argument_name: &'static str,
value: String,
},
}
fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
@ -127,7 +116,6 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
let mut result = CommandResult::default();
match matches.subcommand() {
Some(("flags", matches)) => result = do_flags_command(state, matches),
Some(("load", matches)) => result = do_load_command(state, matches)?,
Some(("print", _matches)) => {}
Some(("quit", _matches)) => {
@ -138,8 +126,9 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
println!("{}", state.position.to_fen_str()?);
result.should_print_position = false;
}
Some(("make", matches)) => result = do_make_command(state, matches)?,
Some(("perft", matches)) => result = do_perft_command(state, matches)?,
Some(("make", _matches)) => {
unimplemented!()
}
Some(("place", matches)) => {
let color = matches
.get_one::<String>("color")
@ -163,12 +152,12 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
.place_piece(piece, square, PlacePieceStrategy::default())?;
}
Some(("sight", matches)) => {
let sight = if let Some(square) = matches.get_one::<String>("square") {
let square: Square = square.parse()?;
state.position.sight_piece(square)
} else {
state.position.sight_active()
};
let square = matches
.get_one::<String>("square")
.ok_or(CommandHandlingError::MissingArgument("square"))?;
let square = square.parse::<Square>()?;
let sight = state.position.sight(square);
let display = state.position.display().highlight(sight);
println!("\n{display}");
@ -186,34 +175,6 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
Ok(result)
}
fn do_flags_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult {
let board = state.position.board();
println!("Castling:");
for (color, wing) in [
(Color::White, Wing::KingSide),
(Color::White, Wing::QueenSide),
(Color::Black, Wing::KingSide),
(Color::Black, Wing::QueenSide),
] {
let has_right = board.has_castling_right_unwrapped(color, wing.into());
let can_castle = board.color_can_castle(wing, Some(color));
let can_castle_message = match can_castle {
Ok(_) => "ok".to_string(),
Err(error) => format!("{error}"),
};
println!(" {color} {wing}: {has_right}, {can_castle_message}");
}
CommandResult {
should_continue: true,
should_print_position: false,
}
}
fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result<CommandResult> {
let fen_string = matches
.get_one::<String>("fen")
@ -230,26 +191,6 @@ fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Res
})
}
fn do_make_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result<CommandResult> {
let move_string = matches
.get_one::<String>("move")
.ok_or(CommandHandlingError::MissingArgument("move"))?;
let algebraic_move: AlgebraicMoveComponents = move_string.parse()?;
let encoded_move = state
.position
.move_from_algebraic_components(algebraic_move)
.ok_or(CommandHandlingError::ValueError {
argument_name: "move",
value: move_string.to_string(),
})?;
state.position.make_move(encoded_move, ValidateMove::Yes)?;
Ok(CommandResult::default())
}
fn do_reset_command(
state: &mut State,
matches: &clap::ArgMatches,
@ -331,7 +272,7 @@ fn do_movement_command(
.get_one::<Square>("square")
.ok_or(CommandHandlingError::MissingArgument("square"))?;
let movement = state.position.movement_piece(square);
let movement = state.position.movement(square);
let display = state.position.display().highlight(movement);
println!("\n{display}");
@ -341,25 +282,6 @@ fn do_movement_command(
})
}
fn do_perft_command(
state: &mut State,
matches: &clap::ArgMatches,
) -> anyhow::Result<CommandResult> {
let depth = *matches
.get_one::<usize>("depth")
.ok_or(CommandHandlingError::MissingArgument("depth"))?;
let mut position = state.position.clone();
let counters = position.perft(depth);
println!("{counters}");
Ok(CommandResult {
should_continue: true,
should_print_position: false,
})
}
fn do_zobrist_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult {
if let Some(hash) = state.position.zobrist_hash() {
println!("hash:{hash}");
@ -390,7 +312,7 @@ fn main() -> Result<(), String> {
loop {
if should_print_position {
println!("{}", &state.position);
println!("{} to move.", state.position.active_color());
println!("{} to move.", state.position.board.active_color());
}
let readline = editor.readline("\n? ");

View file

@ -3,8 +3,7 @@
use crate::{Move, MoveRecord};
use chessfriend_board::{
Board, BoardProvider, CastleParameters, PlacePieceError, PlacePieceStrategy,
castle::{CastleEvaluationError, CastleRightsOption},
movement::Movement,
castle::CastleEvaluationError, movement::Movement,
};
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
use thiserror::Error;
@ -18,7 +17,7 @@ pub enum ValidateMove {
Yes,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[derive(Debug, Error, Eq, PartialEq)]
pub enum MakeMoveError {
#[error("no piece on {0}")]
NoPiece(Square),
@ -252,7 +251,7 @@ impl<T: BoardProvider> MakeMoveInternal for T {
// original board state is preserved.
let record = MoveRecord::new(board, ply, None);
board.revoke_castling_rights_active(wing.into());
board.revoke_castling_right_unwrapped(active_color, wing);
self.advance_board_state(&ply, &king, None, HalfMoveClock::Advance);
@ -308,23 +307,21 @@ impl<T: BoardProvider> MakeMoveInternal for T {
Shape::Rook => {
let origin = ply.origin_square();
if board.has_castling_right(None, Wing::KingSide) {
if board.color_has_castling_right(None, Wing::KingSide) {
let kingside_parameters =
CastleParameters::get(board.active_color(), Wing::KingSide);
if origin == kingside_parameters.origin.rook {
board.revoke_castling_rights(None, Wing::KingSide.into());
board.revoke_castling_right(None, Wing::KingSide);
}
}
let queenside_parameters =
CastleParameters::get(board.active_color(), Wing::QueenSide);
if origin == queenside_parameters.origin.rook {
board.revoke_castling_rights(None, Wing::QueenSide.into());
board.revoke_castling_right(None, Wing::QueenSide);
}
}
Shape::King => {
board.revoke_castling_rights(None, CastleRightsOption::All);
}
Shape::King => board.revoke_all_castling_rights(),
_ => {}
}
@ -562,7 +559,7 @@ mod tests {
assert_eq!(board.get_piece(Square::H1), None);
assert_eq!(board.get_piece(Square::G1), Some(piece!(White King)));
assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook)));
assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide));
Ok(())
}
@ -582,7 +579,7 @@ mod tests {
assert_eq!(board.get_piece(Square::A1), None);
assert_eq!(board.get_piece(Square::C1), Some(piece!(White King)));
assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook)));
assert!(!board.has_castling_right_unwrapped(Color::White, Wing::QueenSide));
assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide));
Ok(())
}

View file

@ -1,7 +1,7 @@
// Eryn Wells <eryn@erynwells.me>
use crate::Move;
use chessfriend_board::{Board, CastleRights, board::HalfMoveClock};
use chessfriend_board::{board::HalfMoveClock, Board, CastleRights};
use chessfriend_core::{Color, Piece, Square};
/// A record of a move made on a board. This struct contains all the information
@ -35,7 +35,7 @@ impl MoveRecord {
color: board.active_color(),
ply,
en_passant_target: board.en_passant_target(),
castling_rights: *board.castling_rights(),
castling_rights: board.castling_rights(),
half_move_clock: board.half_move_clock,
captured_piece: capture,
}

View file

@ -7,7 +7,7 @@ use thiserror::Error;
pub type UnmakeMoveResult = Result<(), UnmakeMoveError>;
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[derive(Debug, Error, Eq, PartialEq)]
pub enum UnmakeMoveError {
#[error("no move to unmake")]
NoMove,
@ -396,7 +396,7 @@ mod tests {
White Rook on H1,
];
let original_castling_rights = *board.castling_rights();
let original_castling_rights = board.castling_rights();
let ply = Move::castle(Color::White, Wing::KingSide);
let record = board.make_move(ply, ValidateMove::Yes)?;
@ -406,7 +406,7 @@ mod tests {
assert_eq!(board.get_piece(Square::H1), None);
assert_eq!(board.get_piece(Square::G1), Some(piece!(White King)));
assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook)));
assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide));
board.unmake_move(&record)?;
@ -415,7 +415,7 @@ mod tests {
assert_eq!(board.get_piece(Square::H1), Some(piece!(White Rook)));
assert_eq!(board.get_piece(Square::G1), None);
assert_eq!(board.get_piece(Square::F1), None);
assert_eq!(*board.castling_rights(), original_castling_rights);
assert_eq!(board.castling_rights(), original_castling_rights);
assert_eq!(board.active_color(), Color::White);
Ok(())
@ -428,7 +428,7 @@ mod tests {
White Rook on A1,
];
let original_castling_rights = *board.castling_rights();
let original_castling_rights = board.castling_rights();
let ply = Move::castle(Color::White, Wing::QueenSide);
let record = board.make_move(ply, ValidateMove::Yes)?;
@ -438,7 +438,7 @@ mod tests {
assert_eq!(board.get_piece(Square::A1), None);
assert_eq!(board.get_piece(Square::C1), Some(piece!(White King)));
assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook)));
assert!(!board.has_castling_right_unwrapped(Color::White, Wing::QueenSide));
assert!(!board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide));
board.unmake_move(&record)?;
@ -447,7 +447,7 @@ mod tests {
assert_eq!(board.get_piece(Square::A1), Some(piece!(White Rook)));
assert_eq!(board.get_piece(Square::C1), None);
assert_eq!(board.get_piece(Square::D1), None);
assert_eq!(*board.castling_rights(), original_castling_rights);
assert_eq!(board.castling_rights(), original_castling_rights);
assert_eq!(board.active_color(), Color::White);
Ok(())
@ -460,7 +460,7 @@ mod tests {
Black Rook on H8,
]);
let original_castling_rights = *board.castling_rights();
let original_castling_rights = board.castling_rights();
let ply = Move::castle(Color::Black, Wing::KingSide);
let record = board.make_move(ply, ValidateMove::Yes)?;
@ -478,7 +478,7 @@ mod tests {
assert_eq!(board.get_piece(Square::H8), Some(piece!(Black Rook)));
assert_eq!(board.get_piece(Square::G8), None);
assert_eq!(board.get_piece(Square::F8), None);
assert_eq!(*board.castling_rights(), original_castling_rights);
assert_eq!(board.castling_rights(), original_castling_rights);
assert_eq!(board.active_color(), Color::Black);
Ok(())

View file

@ -1,13 +1,9 @@
use chessfriend_position::{
Position,
fen::{FromFenStr, ToFenStr},
};
use chessfriend_position::{Position, fen::FromFenStr, perft::Perft};
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "Perft")]
struct Arguments {
#[arg(long, short, value_name = "INT")]
depth: usize,
#[arg(long, short, value_name = "FEN")]
@ -18,18 +14,17 @@ fn main() -> anyhow::Result<()> {
let args = Arguments::parse();
let depth = args.depth;
println!("depth {depth}");
let mut position = if let Some(fen) = args.fen {
Position::from_fen_str(&fen)?
} else {
Position::starting(None)
};
println!("fen \"{}\"", position.to_fen_str().unwrap());
println!("depth {depth}");
let nodes_searched = position.perft(depth);
let counters = position.perft(depth);
println!("\n{counters}");
println!("nodes {nodes_searched}");
Ok(())
}

View file

@ -1,65 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::Position;
use chessfriend_board::Board;
use chessfriend_core::{Color, Piece, Shape, score::Score};
struct Evaluator;
impl Evaluator {
pub fn evaluate_symmetric_unwrapped(position: &Position, color: Color) -> Score {
let board = &position.board;
let material_balance = Self::material_balance(board, color);
let score = material_balance;
score
}
/// Evaluate a board using the symmetric evaluation algorithm defined by
/// Claude Shannon.
fn material_balance(board: &Board, color: Color) -> Score {
let other_color = color.other();
Shape::into_iter().fold(Score::ZERO, |acc, shape| {
let (active_pieces, other_pieces) = (
i32::from(board.count_piece(&Piece::new(color, shape))),
i32::from(board.count_piece(&Piece::new(other_color, shape))),
);
let factor = shape.score() * (active_pieces - other_pieces);
acc + factor
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use chessfriend_board::fen;
#[test]
fn pawn_material_balance() -> Result<(), Box<dyn std::error::Error>> {
let board = fen!("8/8/8/8/8/3P4/8/8 w - - 0 1")?;
assert_eq!(
Evaluator::material_balance(&board, Color::White),
100i32.into()
);
let board = fen!("8/8/3p4/8/8/3P4/8/8 w - - 0 1")?;
assert_eq!(Evaluator::material_balance(&board, Color::White), 0.into());
Ok(())
}
#[test]
fn starting_position_is_even() {
let position = Position::new(Board::starting(None));
assert_eq!(
Evaluator::evaluate_symmetric_unwrapped(&position, Color::White),
Evaluator::evaluate_symmetric_unwrapped(&position, Color::Black)
);
}
}

View file

@ -1,12 +1,11 @@
// Eryn Wells <eryn@erynwells.me>
mod evaluation;
mod position;
#[macro_use]
mod macros;
pub use chessfriend_board::{PlacePieceError, PlacePieceStrategy, fen};
pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy};
pub use chessfriend_moves::{GeneratedMove, ValidateMove};
pub use position::Position;

View file

@ -1,70 +1,45 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{GeneratedMove, Position, ValidateMove};
use std::fmt;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PerftCounters {
nodes: u64,
pub trait Perft {
fn perft(&mut self, depth: usize) -> u64;
}
impl Position {
pub fn perft(&mut self, depth: usize) -> PerftCounters {
self.perft_recursive(0, depth)
impl Perft for Position {
fn perft(&mut self, depth: usize) -> u64 {
self.perft_recursive(depth, depth)
}
}
impl Position {
fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> PerftCounters {
let mut counters = PerftCounters::default();
if depth == max_depth {
counters.count_node();
return counters;
fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> u64 {
if depth == 0 {
return 1;
}
let mut total_nodes_counted = 0u64;
let legal_moves: Vec<GeneratedMove> = self.all_legal_moves(None).collect();
for generated_ply in legal_moves {
let ply = generated_ply.ply();
for ply in legal_moves {
let ply = ply.ply();
let has_seen_position = self
let _has_seen_position = self
.make_move(ply, ValidateMove::No)
.expect("unable to make generated move");
let recursive_counters = if has_seen_position {
let mut counters = PerftCounters::default();
counters.count_node();
counters
} else {
self.perft_recursive(depth + 1, max_depth)
};
let nodes_counted = self.perft_recursive(depth - 1, depth);
total_nodes_counted += nodes_counted;
self.unmake_last_move().expect("unable to unmake last move");
counters.fold(&recursive_counters);
if depth == 0 {
println!(" {ply}: {}", recursive_counters.nodes);
if depth == max_depth {
println!(" {ply} {nodes_counted}");
}
}
counters
}
}
impl PerftCounters {
fn count_node(&mut self) {
self.nodes += 1;
}
fn fold(&mut self, results: &Self) {
self.nodes += results.nodes;
}
}
impl fmt::Display for PerftCounters {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Perft Results")?;
write!(f, " Nodes: {}", self.nodes)
total_nodes_counted
}
}

View file

@ -6,25 +6,25 @@ use crate::fen::{FromFenStr, FromFenStrError};
use captures::CapturesList;
use chessfriend_bitboard::BitBoard;
use chessfriend_board::{
Board, PlacePieceError, PlacePieceStrategy, ZobristState, display::DiagramFormatter,
fen::ToFenStr,
display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy,
ZobristState,
};
use chessfriend_core::{Color, Piece, Shape, Square};
use chessfriend_moves::{
GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError,
UnmakeMoveResult, ValidateMove,
algebraic::AlgebraicMoveComponents,
generators::{
AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator,
PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator,
},
GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError,
UnmakeMoveResult, ValidateMove,
};
use std::{collections::HashSet, fmt, sync::Arc};
#[must_use]
#[derive(Clone, Debug, Default, Eq)]
pub struct Position {
pub(crate) board: Board,
pub board: Board,
pub(crate) moves: Vec<MoveRecord>,
pub(crate) captures: CapturesList,
@ -48,16 +48,6 @@ impl Position {
..Default::default()
}
}
#[must_use]
pub fn board(&self) -> &Board {
&self.board
}
#[must_use]
pub fn active_color(&self) -> Color {
self.board.active_color()
}
}
impl Position {
@ -86,14 +76,12 @@ impl Position {
}
impl Position {
/// Calculate sight of a piece on the provided [`Square`].
pub fn sight_piece(&self, square: Square) -> BitBoard {
self.board.sight_piece(square)
pub fn sight(&self, square: Square) -> BitBoard {
self.board.sight(square)
}
/// Calculate movement of a piece on the provided [`Square`].
pub fn movement_piece(&self, square: Square) -> BitBoard {
self.board.movement_piece(square)
pub fn movement(&self, square: Square) -> BitBoard {
self.board.movement(square)
}
}
@ -129,7 +117,7 @@ impl Position {
);
});
let move_is_legal = !test_board.is_in_check();
let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move));
test_board.unmake_move(&record).unwrap_or_else(|err| {
panic!(
@ -165,8 +153,8 @@ impl Position {
}
impl Position {
pub fn sight_active(&self) -> BitBoard {
self.board.sight_active()
pub fn active_sight(&self) -> BitBoard {
self.board.active_sight()
}
/// A [`BitBoard`] of all squares the given color can see.
@ -287,7 +275,7 @@ impl Position {
}
let target_bitboard: BitBoard = target.into();
if !(self.movement_piece(origin) & target_bitboard).is_populated() {
if !(self.movement(origin) & target_bitboard).is_populated() {
return None;
}
@ -356,7 +344,7 @@ impl fmt::Display for Position {
#[cfg(test)]
mod tests {
use super::*;
use crate::{Position, test_position};
use crate::{test_position, Position};
use chessfriend_core::piece;
#[test]

View file

@ -8,7 +8,7 @@
use chessfriend_core::Color;
use chessfriend_moves::{
Move, assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply,
assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move,
};
use chessfriend_position::test_position;
use std::collections::HashSet;
@ -107,7 +107,7 @@ fn en_passant_check_capture() {
White Pawn on D4,
], D3);
assert!(pos.board().is_in_check());
assert!(pos.board.active_color_is_in_check());
let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect();
@ -123,7 +123,7 @@ fn en_passant_check_block() {
White Queen on F1,
], D3);
assert!(pos.board().is_in_check());
assert!(pos.board.active_color_is_in_check());
let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect();
@ -139,7 +139,7 @@ fn pinned_pieces_rook_cannot_move_out_of_pin() {
White King on C1,
]);
assert!(!pos.board().is_in_check());
assert!(!pos.board.active_color_is_in_check());
let rook_moves: HashSet<_> = pos.all_legal_moves(None).collect();

View file

@ -1,5 +1,3 @@
style_edition = "2024"
imports_layout = "HorizontalVertical"
group_imports = "StdExternalCrate"