Compare commits
1 commit
main
...
chessfrien
Author | SHA1 | Date | |
---|---|---|---|
90ee3e416f |
37 changed files with 650 additions and 949 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -71,6 +71,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
[[package]]
|
||||
name = "chessfriend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chessfriend_core",
|
||||
"chessfriend_moves",
|
||||
"chessfriend_position",
|
||||
"clap",
|
||||
"shlex",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chessfriend_bitboard"
|
||||
|
|
|
@ -10,7 +10,3 @@ members = [
|
|||
"position",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[profile.release-debug]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
|
113
README.md
113
README.md
|
@ -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.
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -3,3 +3,10 @@ name = "chessfriend"
|
|||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chessfriend_core = { path = "../core" }
|
||||
chessfriend_moves = { path = "../moves" }
|
||||
chessfriend_position = { path = "../position" }
|
||||
clap = { version = "4.4.12", features = ["derive"] }
|
||||
shlex = "1.2.0"
|
||||
thiserror = "2"
|
||||
|
|
87
chessfriend/src/chessfriend.rs
Normal file
87
chessfriend/src/chessfriend.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{
|
||||
core::{Piece, Square},
|
||||
position::{MakeMoveError, Move, ValidateMove},
|
||||
threadpool::ThreadPool,
|
||||
};
|
||||
use chessfriend_core::random::RandomNumberGenerator;
|
||||
use chessfriend_position::{
|
||||
PlacePieceError, PlacePieceStrategy, Position, ZobristState, fen::FromFenStr,
|
||||
};
|
||||
use std::{num::NonZero, sync::Arc};
|
||||
|
||||
pub struct ChessFriend {
|
||||
/// A pool of worker threads over which tasks may be distributed.
|
||||
thread_pool: ThreadPool,
|
||||
|
||||
zobrist_state: Arc<ZobristState>,
|
||||
|
||||
/// A global Position for the engine.
|
||||
position: Position,
|
||||
}
|
||||
|
||||
impl ChessFriend {
|
||||
#[must_use]
|
||||
pub fn new(options: Options) -> Self {
|
||||
let mut rng = RandomNumberGenerator::default();
|
||||
|
||||
let zobrist_state = Arc::new(ZobristState::new(&mut rng));
|
||||
|
||||
let position = match options.initial_position {
|
||||
InitialPosition::Empty => Position::empty(Some(zobrist_state.clone())),
|
||||
InitialPosition::Starting => Position::starting(Some(zobrist_state.clone())),
|
||||
InitialPosition::Fen(fen) => {
|
||||
let mut position = Position::from_fen_str(fen).unwrap_or_default();
|
||||
position.set_zobrist_state(zobrist_state.clone());
|
||||
position
|
||||
}
|
||||
};
|
||||
|
||||
let thread_pool = ThreadPool::new(options.threading.0);
|
||||
|
||||
Self {
|
||||
thread_pool,
|
||||
zobrist_state,
|
||||
position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChessFriend {}
|
||||
|
||||
#[derive(Clone, Copy, Default, Eq, PartialEq)]
|
||||
pub struct Options<'a> {
|
||||
initial_position: InitialPosition<'a>,
|
||||
threading: Threading,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Eq, PartialEq)]
|
||||
pub enum InitialPosition<'a> {
|
||||
Empty,
|
||||
#[default]
|
||||
Starting,
|
||||
Fen(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub struct Threading(NonZero<usize>);
|
||||
|
||||
impl Threading {
|
||||
#[must_use]
|
||||
pub fn new(n: NonZero<usize>) -> Self {
|
||||
Self(n)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_with_available_parallelism() -> Self {
|
||||
const ONE: NonZero<usize> = NonZero::new(1).unwrap();
|
||||
Self(std::thread::available_parallelism().unwrap_or(ONE))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Threading {
|
||||
fn default() -> Self {
|
||||
Self::new_with_available_parallelism()
|
||||
}
|
||||
}
|
|
@ -1,2 +1,19 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod chessfriend;
|
||||
mod threadpool;
|
||||
mod uci;
|
||||
|
||||
pub use crate::chessfriend::ChessFriend;
|
||||
|
||||
pub mod options {
|
||||
pub use crate::chessfriend::{InitialPosition, Options, Threading};
|
||||
}
|
||||
|
||||
pub mod core {
|
||||
pub use chessfriend_core::{Color, Piece, Shape, Square};
|
||||
}
|
||||
|
||||
pub mod position {
|
||||
pub use chessfriend_moves::{MakeMoveError, Move, ValidateMove};
|
||||
}
|
||||
|
|
77
chessfriend/src/threadpool.rs
Normal file
77
chessfriend/src/threadpool.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use std::{
|
||||
num::NonZero,
|
||||
panic,
|
||||
sync::{Arc, Mutex, mpsc},
|
||||
thread,
|
||||
};
|
||||
|
||||
pub(crate) trait Job: FnOnce() + Send + 'static {}
|
||||
|
||||
pub(crate) struct ThreadPool {
|
||||
workers: Vec<Worker>,
|
||||
sender: Option<mpsc::Sender<Box<dyn Job>>>,
|
||||
}
|
||||
|
||||
impl ThreadPool {
|
||||
pub fn new(threads_count: NonZero<usize>) -> Self {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
|
||||
let receiver = Arc::new(Mutex::new(receiver));
|
||||
let workers: Vec<_> = (0..threads_count.into())
|
||||
.map(|i| Worker::new(i, receiver.clone()))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
workers,
|
||||
sender: Some(sender),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ThreadPool {
|
||||
fn drop(&mut self) {
|
||||
drop(self.sender.take());
|
||||
self.workers.drain(..).for_each(Worker::join);
|
||||
}
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
id: usize,
|
||||
handle: thread::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Box<dyn Job>>>>) -> Self {
|
||||
// TODO: A note from the Rust Programming Language
|
||||
//
|
||||
// Note: If the operating system can’t create a thread because there
|
||||
// aren’t enough system resources, thread::spawn will panic. That will
|
||||
// cause our whole server to panic, even though the creation of some
|
||||
// threads might succeed. For simplicity’s sake, this behavior is fine,
|
||||
// but in a production thread pool implementation, you’d likely want to
|
||||
// use std::thread::Builder and its spawn method that returns Result
|
||||
// instead.
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
loop {
|
||||
let job = {
|
||||
let receiver = receiver.lock().unwrap();
|
||||
receiver.recv().unwrap()
|
||||
};
|
||||
|
||||
job();
|
||||
}
|
||||
});
|
||||
|
||||
Self { id, handle }
|
||||
}
|
||||
|
||||
fn join(self) {
|
||||
match self.handle.join() {
|
||||
Ok(()) => {}
|
||||
Err(error) => panic::resume_unwind(error),
|
||||
}
|
||||
}
|
||||
}
|
148
chessfriend/src/uci.rs
Normal file
148
chessfriend/src/uci.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use clap::{Error as ClapError, Parser, Subcommand, ValueEnum};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{BufRead, Write},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(multicall = true)]
|
||||
pub struct Uci {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
/// Establish UCI (Universal Chess Interface) as the channel's exchange protocol.
|
||||
Uci,
|
||||
|
||||
/// Toggle debug state on or off.
|
||||
Debug { state: DebugState },
|
||||
|
||||
/// Synchronize the engine with the client. Can also be used as a 'ping'.
|
||||
IsReady,
|
||||
|
||||
/// Stop calculating as soon as possible.
|
||||
Stop,
|
||||
|
||||
/// Stop all processing and quit the program.
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
|
||||
enum DebugState {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
pub enum Response<'a> {
|
||||
/// Declares one aspect of the engine's identity.
|
||||
Id(IdValue<'a>),
|
||||
|
||||
/// Declares that communicating in UCI is acceptable.
|
||||
UciOk,
|
||||
|
||||
/// Declares that the engine is ready to receive commands from the client.
|
||||
ReadyOk,
|
||||
}
|
||||
|
||||
impl Display for Response<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Response::Id(value) => write!(f, "id {value}"),
|
||||
Response::UciOk => write!(f, "uciok"),
|
||||
Response::ReadyOk => write!(f, "readyok"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum IdValue<'a> {
|
||||
Name(&'a str),
|
||||
Author(&'a str),
|
||||
}
|
||||
|
||||
impl Display for IdValue<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
IdValue::Name(name) => write!(f, "name {name}"),
|
||||
IdValue::Author(author) => write!(f, "author {author}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("unable to parse command")]
|
||||
LexError,
|
||||
|
||||
#[error("{0}")]
|
||||
ClapError(#[from] ClapError),
|
||||
}
|
||||
|
||||
impl Uci {
|
||||
/// Respond to a command.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns an error if parsing the command string fails, otherwise returns an array of
|
||||
/// responses.
|
||||
///
|
||||
pub fn respond(line: &str) -> Result<Vec<Response>, Error> {
|
||||
let arguments = shlex::split(line).ok_or(Error::LexError)?;
|
||||
|
||||
let interface = Self::try_parse_from(arguments)?;
|
||||
|
||||
match interface.command {
|
||||
Command::Uci => {
|
||||
const IDENTITIES: [Response; 2] = [
|
||||
Response::Id(IdValue::Name("ChessFriend")),
|
||||
Response::Id(IdValue::Author("Eryn Wells")),
|
||||
];
|
||||
|
||||
let options: Vec<Response> = vec![];
|
||||
|
||||
Ok(IDENTITIES.into_iter().chain(options).collect())
|
||||
}
|
||||
Command::Debug { state: _ } => Ok(vec![]),
|
||||
Command::IsReady => Ok(vec![Response::ReadyOk]),
|
||||
Command::Stop => Ok(vec![]),
|
||||
Command::Quit => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UciInterface {}
|
||||
|
||||
impl UciInterface {
|
||||
pub fn read_until_quit(
|
||||
&self,
|
||||
input: impl BufRead,
|
||||
output: &mut impl Write,
|
||||
) -> Result<(), UciInterfaceError> {
|
||||
for line in input.lines() {
|
||||
let line = line?;
|
||||
|
||||
let responses = Uci::respond(line.as_str())?;
|
||||
|
||||
// TODO: Dispatch command to background processing thread.
|
||||
|
||||
for r in responses {
|
||||
write!(output, "{r}")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UciInterfaceError {
|
||||
#[error("io error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("uci error: {0}")]
|
||||
UciError(#[from] Error),
|
||||
}
|
|
@ -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,
|
||||
"{}",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -4,7 +4,6 @@ pub mod colors;
|
|||
pub mod coordinates;
|
||||
pub mod pieces;
|
||||
pub mod random;
|
||||
pub mod score;
|
||||
pub mod shapes;
|
||||
|
||||
mod macros;
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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? ");
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod evaluation;
|
||||
mod position;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
pub use chessfriend_board::{PlacePieceError, PlacePieceStrategy, fen};
|
||||
pub use chessfriend_moves::{GeneratedMove, ValidateMove};
|
||||
pub use chessfriend_board::{fen, PlacePieceError, PlacePieceStrategy, ZobristState};
|
||||
pub use chessfriend_moves::{GeneratedMove, Move, ValidateMove};
|
||||
pub use position::Position;
|
||||
|
||||
pub mod perft;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
style_edition = "2024"
|
||||
|
||||
imports_layout = "HorizontalVertical"
|
||||
group_imports = "StdExternalCrate"
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue