Compare commits

...
Sign in to create a new pull request.

39 commits

Author SHA1 Message Date
dae5179947 Add a README 2025-08-15 17:06:07 -07:00
182bf81126 [board] Fix a counter underflow in the piece set
During perft runs, the PieceSet counter would occasionally underflow, causing
the whole program to crash. This is because, when building a Board from a list
of bitboards, Counts::increment() was only being called once, even when the
bitboard had more than one piece in it. Fix the bug by incrementing during the
loop that sets up the mailbox.

Additionally, refactor the increment() and decrement() methods to be a little
more succinct.
2025-08-15 16:15:09 -07:00
3d73760146 [bitboard, board] Remove BitBoard::empty() and BitBoard::full()
These have been deprecated for a while. Clean up the remaining uses and remove
the methods from BitBoard.
2025-08-15 16:14:34 -07:00
3a0541a2c3 [bitboard] Add a doc comment to BitBoard::first_occupied_square 2025-07-12 20:27:47 -07:00
b6d27356ac [bitboard] Implement BitBoard::occupied_squares_direction
Iterate a BitBoard in a direction (from leading or trailing edge) based on a
board direction.
2025-07-12 20:27:47 -07:00
146e4d34d3 [core] Fix an incorrect assertion in the Score doc test
Negate with - instead of with !.
2025-07-12 20:27:47 -07:00
b505606925 [core] Export Score::CENTIPAWNS_PER_POINT to the crate
This constant is a conversion factor of points to the internal fixed point unit
of centipawns. Points are more familiar to people because pawns are worth 1 pt.

Calculate the scores of the various piece shapes with this constant.
2025-07-12 20:27:47 -07:00
b3ff8dec49 [core] Make Shape::is_promotable() const 2025-07-12 20:27:47 -07:00
484fcf342e [board] Remove a useless .into() call
Clippy pointed this out to me. This .into() call serves no purpose.
2025-07-12 20:27:47 -07:00
45183c910c [bitboard] Replace some references to BitBoard::full() and BitBoard::empty() with the const values
Two doc tests reference the methods instead of the const variables. Update them.
2025-07-12 20:27:47 -07:00
a904e4a5bb [bitboard, board] Replace ray_in_direction! macro with a function
This is simpler than writing a macro, at the expense of some overhead for calling
a function. But the Rust compiler might inline it anyway!

To support this change, implement BitBoard::first_occupied_square_direction, which
iterates a bitboard in a direction (i.e. leading or trailing) depending on the
core::Direction value passed to it.
2025-06-30 15:37:35 -07:00
e3d17219ad [board, position] Simplify check methods
Only one check-testing method, Board::is_in_check(), that tests if the current
active player is in check. It doesn't make sense to test if the non-active player
is in check.
2025-06-29 09:25:08 -07:00
a30553503f [board, explorer, position] Clean up naming of sight and movement methods
These methods have a prefix, either `sight` or `movement`, and then follow the conventions
for other "trio" clusters where there's an un-suffixed method that takes an
Option<Color>, a _active method that uses the active color, and a _unwrapped
method that takes a bare Color.
2025-06-29 09:23:20 -07:00
e7fd65672d [bitboard, board] Make BitBoard::EMPTY and BitBoard::FULL public
Deprecate the methods.

I think I'm undoing a change I made earlier. 🙃
2025-06-29 09:18:44 -07:00
8db533cb52 [board] Use $crate in the fen! macro so you don't have to import Board to get one back 2025-06-27 08:44:56 -07:00
74c0e4144f [position] Remove the to_move_factor from symmetric evaluation
Just use material balance.
2025-06-24 20:04:41 -07:00
1ae6d5df48 [core, position] Rename the type of Score's inner value → Value 2025-06-24 20:01:05 -07:00
4e80cc36ca [core] Implement Display for Score 2025-06-24 15:20:31 -07:00
80ac8ea036 [core] Import std::fmt and remove std:: from Display symbol spelling 2025-06-24 15:18:49 -07:00
9f2dc3fa76 [position] Update import ordering in position.rs 2025-06-21 21:09:01 -07:00
54d9c3838d [position] Export Position::active_color()
Passes through to the Board method.
2025-06-21 21:08:32 -07:00
4b96db230d [board, moves] Derive Clone on several error types
- PlacePieceError
- MakeMoveError
- UnmakeMoveError
2025-06-21 21:08:04 -07:00
f84319272c [explorer, position] Make Position.board private to the crate
Export a Position::board() method that returns a reference to the internal Board.
2025-06-21 21:07:26 -07:00
4ae1fd62b7 [perft] Remove an unused Move import
This was causing a warning.
2025-06-20 14:25:27 -07:00
abaf277fb4 [core] Use the matches! macro to calculate the value of Shape::is_promotable
I learned about this macro a little while ago and it's better than writing out
a match block by hand, and also doesn't require static or const data, like the
previous implementation did.
2025-06-20 14:25:10 -07:00
a91bb8c983 [board] Remove the unused Mailbox::new method
Just use Mailbox::default().
2025-06-20 14:24:16 -07:00
7f25548335 [board, core, position] A simple static evaluation method for scoring positions
Implement a new Evaluator struct that evaluates a Board and returns a score. This
evaluation mechanism uses only a material balance function. It doesn't account
for anything else.

Supporting this, add a Counts struct to the internal piece set structure of a
Board. This struct is responsible for keeping counts of how many pieces of each
shape are on the board for each color. Export a count_piece() method on Board
that returns a count of the number of pieces of a particular color and shape.

Implement a newtype wrapper around i32 called Score that represents the score of
a position in centipawns, i.e. hundredths of a pawn. Add piece values to the
Shape enum.
2025-06-20 14:23:57 -07:00
481ae70698 [core] Directly index the array of Squares with a given index
In Square::from_index_unchecked, instead of using TryFrom to convert the index
to a square, just index directly into the Square::ALL array. This function is
already marked unsafe.
2025-06-19 14:32:07 -07:00
1d8a0dc3a4 Add a release-debug profile
This profile builds binaries for release, but includes debugging information.
Useful for profiling!
2025-06-19 14:27:52 -07:00
0f5a538f0a [explorer, perft, position] Move node count into a new PerftCounters struct 2025-06-19 11:34:59 -07:00
4ce7e89cdb [board, explorer, moves] Clean up the castling rights API
Reorganize castling rights API on Board into methods named according to
conventions applied to other API.

Board::has_castling_right
Board::has_castling_right_active
Board::has_castling_right_unwrapped

    These all check if a color has the right to castle on a particular side
    (wing) of the board. The first takes an Option<Color>, the latter two
    operate on bare Colors: the active color, or an explicit Color.

Board::grant_castling_right
Board::grant_castling_right_active
Board::grant_castling_right_unwrapped

    Grant castling rights to a color. Color arguments follow the pattern above.

Board::revoke_castling_right
Board::revoke_castling_right_active
Board::revoke_castling_right_unwrapped

    Revoke castling rights from a color. Color arguments follow the pattern
    above.

The latter two groups of methods take a new CastleRightsOption type that
specifies either a single Wing or All.

Rework the implementation of CastleRights to take a CastleRightsOption. Update
the unit tests and make sure everything builds.
2025-06-18 23:44:40 +00:00
933924d37a [board] When loading a FEN string, start with no castling rights
The default value of the castle::Rights struct is with all rights granted. When
loading a FEN string, start with none and add to it.
2025-06-18 08:26:29 -07:00
9972ce94d0 [moves] Revoke castling rights only for the player that moved
There was a bug in the code that revokes castling rights after a king move where
it revoked the rights for *all* players, rather than just the current player.
Fix it.
2025-06-18 08:25:41 -07:00
c5cc0646ef [perft] Add back the block on searching into seen positions
Check if the board position has been seen and stop recursion if so.
2025-06-18 08:22:12 -07:00
bf17017694 [explorer] Add several commands to help with debugging
flags
: Print flags for the current board position. This prints the castling rights
and whether the player can castle (regardless of whether they have the right).

make
: Finally reimplement the make command. Change the format so it takes a move in
the UCI long algebraic style.

perft
: Run perft to a given depth on the current board position.
2025-06-18 08:21:31 -07:00
6996cbeb15 [position] Remove the Perft trait
It wasn't serving a purpose.
2025-06-17 16:42:57 -07:00
f3b31d5514 [perft] Print the fen string of the board position 2025-06-17 16:42:35 -07:00
076cdfe66f Remove empty dependencies list from Cargo.lock 2025-06-17 16:42:17 -07:00
801e15fd5a Add style_edition to rustfmt.toml
Set style edition to 2024.
2025-06-17 16:24:46 -07:00
32 changed files with 948 additions and 307 deletions

2
Cargo.lock generated
View file

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

View file

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

113
README.md Normal file
View file

@ -0,0 +1,113 @@
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,16 +43,8 @@ macro_rules! moves_getter {
}
impl BitBoard {
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 EMPTY: BitBoard = BitBoard(u64::MIN);
pub const FULL: BitBoard = BitBoard(u64::MAX);
pub const fn new(bits: u64) -> BitBoard {
BitBoard(bits)
@ -107,7 +99,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());
/// ```
@ -123,7 +115,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());
/// ```
@ -158,9 +150,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 {
@ -209,8 +201,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());
/// ```
@ -231,6 +223,38 @@ impl BitBoard {
}
}
/// Iterate through the occupied squares in a direction specified by a
/// compass direction. This method is mose useful for bitboards of slider
/// rays so that iteration proceeds in order along the ray's direction.
///
/// ## Examples
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// use chessfriend_core::{Direction, Square};
///
/// let ray = BitBoard::ray(Square::E4, Direction::North);
/// assert_eq!(
/// ray.occupied_squares_direction(Direction::North).collect::<Vec<Square>>(),
/// vec![Square::E5, Square::E6, Square::E7, Square::E8]
/// );
/// ```
///
#[must_use]
pub fn occupied_squares_direction(
&self,
direction: Direction,
) -> Box<dyn Iterator<Item = Square>> {
match direction {
Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => {
Box::new(self.occupied_squares_trailing())
}
Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => {
Box::new(self.occupied_squares_leading())
}
}
}
#[must_use]
pub fn occupied_squares_leading(&self) -> LeadingBitScanner {
LeadingBitScanner::new(self.0)
@ -241,6 +265,24 @@ 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 {
@ -512,8 +554,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::{
PieceSet, castle,
CastleRights, PieceSet,
display::DiagramFormatter,
piece_sets::{PlacePieceError, PlacePieceStrategy},
piece_sets::{Counter, PlacePieceError, PlacePieceStrategy},
zobrist::{ZobristHash, ZobristState},
};
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Shape, Square, Wing};
use chessfriend_core::{Color, Piece, Shape, Square};
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: castle::Rights,
castling_rights: CastleRights,
en_passant_target: Option<Square>,
pub half_move_clock: HalfMoveClock,
pub full_move_number: FullMoveClock,
@ -92,59 +92,27 @@ impl Board {
impl Board {
#[must_use]
pub fn castling_rights(&self) -> castle::Rights {
self.castling_rights
pub fn castling_rights(&self) -> &CastleRights {
&self.castling_rights
}
pub fn set_castling_rights(&mut self, rights: castle::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) {
if rights == self.castling_rights {
return;
}
let old_rights = self.castling_rights;
self.castling_rights = rights;
self.update_zobrist_hash_castling_rights(old_rights);
}
#[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) {
pub(crate) fn update_zobrist_hash_castling_rights(&mut self, old_rights: CastleRights) {
let new_rights = self.castling_rights;
if old_rights == new_rights {
return;
@ -154,6 +122,18 @@ 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 {
@ -239,6 +219,11 @@ impl Board {
removed_piece
}
#[must_use]
pub fn count_piece(&self, piece: &Piece) -> Counter {
self.pieces.count(piece)
}
}
impl Board {

View file

@ -4,10 +4,10 @@ mod parameters;
mod rights;
pub use parameters::Parameters;
pub use rights::Rights;
pub use rights::{CastleRightsOption, Rights};
use crate::{Board, CastleParameters};
use chessfriend_core::{Color, Piece, Square, Wing};
use chessfriend_core::{Color, 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.color_has_castling_right_unwrapped(color, wing) {
if !self.has_castling_right_unwrapped(color, wing) {
return Err(CastleEvaluationError::NoRights { color, wing });
}
@ -76,17 +76,60 @@ impl Board {
Ok(parameters)
}
}
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())
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_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())
#[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);
}
}
@ -104,8 +147,8 @@ mod tests {
White Rook on H1
);
assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(board.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide));
assert!(board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(board.has_castling_right_unwrapped(Color::White, Wing::QueenSide));
}
#[test]

View file

@ -1,6 +1,14 @@
// 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);
@ -12,16 +20,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 color_has_right(self, color: Color, wing: Wing) -> bool {
(self.0 & (1 << Self::flag_offset(color, wing))) != 0
pub fn get(self, color: Color, option: CastleRightsOption) -> bool {
(self.0 & Self::flags(color, option)) != 0
}
pub fn grant(&mut self, color: Color, wing: Wing) {
self.0 |= 1 << Self::flag_offset(color, wing);
pub fn grant(&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));
pub fn revoke(&mut self, color: Color, option: CastleRightsOption) {
self.0 &= !Self::flags(color, option);
}
/// Revoke castling rights for all colors and all sides of the board.
@ -31,14 +39,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 {
fn flag_offset(color: Color, wing: Wing) -> usize {
((color as usize) << 1) + wing as usize
const fn flags(color: Color, option: CastleRightsOption) -> u8 {
option.as_flags() << (color as u8 * 2)
}
}
@ -54,36 +62,55 @@ 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::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);
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);
}
#[test]
fn default_rights() {
let mut rights = Rights::default();
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));
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.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.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));
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()));
}
}

View file

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

View file

@ -9,9 +9,10 @@ use thiserror::Error;
#[macro_export]
macro_rules! fen {
($fen_string:literal) => {
Board::from_fen_str($fen_string)
};
($fen_string:literal) => {{
use $crate::fen::FromFenStr;
$crate::Board::from_fen_str($fen_string)
}};
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
@ -24,12 +25,16 @@ 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),
}
@ -122,12 +127,12 @@ impl ToFenStr for Board {
(Color::Black, Wing::KingSide),
(Color::Black, Wing::QueenSide),
]
.map(|(color, castle)| {
if !self.color_has_castling_right_unwrapped(color, castle) {
.map(|(color, wing)| {
if !self.has_castling_right_unwrapped(color, wing) {
return "";
}
match (color, castle) {
match (color, wing) {
(Color::White, Wing::KingSide) => "K",
(Color::White, Wing::QueenSide) => "Q",
(Color::Black, Wing::KingSide) => "k",
@ -226,20 +231,23 @@ 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))?;
if castling_rights == "-" {
board.revoke_all_castling_rights();
} else {
board.revoke_all_castling_rights();
if castling_rights != "-" {
for ch in castling_rights.chars() {
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),
};
let (color, wing) =
color_wing_from_char(ch).ok_or(FromFenStrError::InvalidValue)?;
board.grant_castling_rights_unwrapped(color, wing.into());
}
}

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::{sight::Sight, Board};
use crate::{Board, sight::Sight};
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
impl Board {
pub fn movement(&self, square: Square) -> BitBoard {
pub fn movement_piece(&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,8 +1,9 @@
// Eryn Wells <eryn@erynwells.me>
mod counts;
mod mailbox;
use self::mailbox::Mailbox;
use self::{counts::Counts, mailbox::Mailbox};
use chessfriend_bitboard::{BitBoard, IterationDirection};
use chessfriend_core::{Color, Piece, Shape, Square};
use std::{
@ -11,6 +12,8 @@ use std::{
};
use thiserror::Error;
pub(crate) use counts::Counter;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum PlacePieceStrategy {
#[default]
@ -18,7 +21,7 @@ pub enum PlacePieceStrategy {
PreserveExisting,
}
#[derive(Debug, Error, Eq, PartialEq)]
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum PlacePieceError {
#[error("cannot place piece on {square} with existing {piece}")]
ExisitingPiece { piece: Piece, square: Square },
@ -29,6 +32,7 @@ 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],
}
@ -36,10 +40,11 @@ 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::iter().enumerate() {
for (color_index, color) in Color::into_iter().enumerate() {
for (shape_index, shape) in Shape::into_iter().enumerate() {
let bitboard = pieces[color_index][shape_index];
@ -47,14 +52,16 @@ 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,
}
@ -94,6 +101,10 @@ 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];
@ -120,6 +131,7 @@ 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)
@ -127,8 +139,12 @@ impl PieceSet {
pub(crate) fn remove(&mut self, square: Square) -> Option<Piece> {
if let Some(piece) = self.mailbox.get(square) {
self.color_occupancy[piece.color as usize].clear(square);
self.shape_occupancy[piece.shape as usize].clear(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.mailbox.remove(square);
Some(piece)

View file

@ -0,0 +1,61 @@
// 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,10 +7,6 @@ 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]
}
@ -46,7 +42,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::new(), |mut mailbox, (square, piece)| {
.fold(Self::default(), |mut mailbox, (square, piece)| {
mailbox.set(piece, square);
mailbox
})
@ -61,7 +57,7 @@ mod tests {
#[test]
fn iter_returns_all_pieces() {
let mut mailbox = Mailbox::new();
let mut mailbox = Mailbox::default();
mailbox.set(piece!(White Queen), Square::C3);
mailbox.set(piece!(White Rook), Square::H8);
mailbox.set(piece!(Black Bishop), Square::E4);

View file

@ -23,16 +23,31 @@ use std::ops::BitOr;
impl Board {
/// Compute sight of the piece on the given square.
pub fn sight(&self, square: Square) -> BitBoard {
pub fn sight_piece(&self, square: Square) -> BitBoard {
if let Some(piece) = self.get_piece(square) {
piece.sight(square, self)
} else {
BitBoard::empty()
BitBoard::EMPTY
}
}
pub fn active_sight(&self) -> BitBoard {
self.friendly_sight(self.active_color())
/// 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)
}
/// A [`BitBoard`] of all squares the given color can see.
@ -40,8 +55,8 @@ impl Board {
// TODO: Probably want to implement a caching layer here.
self.friendly_occupancy(color)
.occupied_squares(&IterationDirection::default())
.map(|square| self.sight(square))
.fold(BitBoard::empty(), BitOr::bitor)
.map(|square| self.sight_piece(square))
.fold(BitBoard::EMPTY, BitOr::bitor)
}
pub fn active_color_opposing_sight(&self) -> BitBoard {
@ -60,7 +75,7 @@ impl Board {
Some(self.friendly_sight(c))
}
})
.fold(BitBoard::empty(), BitOr::bitor)
.fold(BitBoard::EMPTY, BitOr::bitor)
}
}
@ -123,20 +138,6 @@ 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;
@ -160,15 +161,27 @@ 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;
#[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);
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);
sight
}
@ -177,11 +190,10 @@ fn rook_sight(info: &SightInfo) -> BitBoard {
let rook = info.square;
let occupancy = info.occupancy;
#[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);
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);
sight
}
@ -190,15 +202,14 @@ fn queen_sight(info: &SightInfo) -> BitBoard {
let queen = info.square;
let occupancy = info.occupancy;
#[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);
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);
sight
}
@ -244,7 +255,7 @@ mod tests {
White King on E4,
);
let sight = pos.active_sight();
let sight = pos.sight_active();
assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]);
}
@ -267,8 +278,8 @@ mod tests {
mod pawn {
use crate::{sight::Sight, test_board};
use chessfriend_bitboard::{bitboard, BitBoard};
use chessfriend_core::{piece, Square};
use chessfriend_bitboard::{BitBoard, bitboard};
use chessfriend_core::{Square, piece};
sight_test!(e4_pawn, piece!(White Pawn), Square::E4, bitboard![D5 F5]);
@ -294,7 +305,7 @@ mod tests {
let piece = piece!(White Pawn);
let sight = piece.sight(Square::E4, &pos);
assert_eq!(sight, BitBoard::empty());
assert_eq!(sight, BitBoard::EMPTY);
}
#[test]

View file

@ -1,6 +1,7 @@
// Eryn Wells <eryn@erynwells.me>
use crate::Direction;
use crate::{Direction, score};
use std::fmt;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
@ -56,10 +57,18 @@ impl Color {
Color::Black => "black",
}
}
#[must_use]
pub const fn score_factor(self) -> score::Value {
match self {
Color::White => 1,
Color::Black => -1,
}
}
}
impl std::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",

View file

@ -115,7 +115,9 @@ 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 {
@ -262,7 +264,7 @@ impl Square {
#[must_use]
pub unsafe fn from_index_unchecked(x: u8) -> Square {
debug_assert!((x as usize) < Self::NUM);
Self::try_from(x).unwrap_unchecked()
Self::ALL[x as usize]
}
#[inline]

View file

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

126
core/src/score.rs Normal file
View file

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

View file

@ -1,14 +1,11 @@
// Eryn Wells <eryn@erynwells.me>
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_board::{Board, ZobristState, fen::FromFenStr};
use chessfriend_core::{Color, Piece, Shape, Square, Wing, random::RandomNumberGenerator};
use chessfriend_moves::{GeneratedMove, ValidateMove, algebraic::AlgebraicMoveComponents};
use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr};
use clap::{Arg, Command};
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
use clap::{Arg, Command, value_parser};
use rustyline::{DefaultEditor, error::ReadlineError};
use std::sync::Arc;
use thiserror::Error;
@ -45,6 +42,7 @@ 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))
@ -53,8 +51,7 @@ fn command_line() -> Command {
)
.subcommand(
Command::new("make")
.arg(Arg::new("from").required(true))
.arg(Arg::new("to").required(true))
.arg(Arg::new("move").required(true))
.alias("m")
.about("Make a move"),
)
@ -68,7 +65,7 @@ fn command_line() -> Command {
)
.subcommand(
Command::new("sight")
.arg(Arg::new("square").required(true))
.arg(Arg::new("square").required(false))
.about("Show sight of a piece on a square"),
)
.subcommand(
@ -81,6 +78,14 @@ 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"))
@ -107,6 +112,12 @@ 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> {
@ -116,6 +127,7 @@ 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)) => {
@ -126,9 +138,8 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
println!("{}", state.position.to_fen_str()?);
result.should_print_position = false;
}
Some(("make", _matches)) => {
unimplemented!()
}
Some(("make", matches)) => result = do_make_command(state, matches)?,
Some(("perft", matches)) => result = do_perft_command(state, matches)?,
Some(("place", matches)) => {
let color = matches
.get_one::<String>("color")
@ -152,12 +163,12 @@ fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
.place_piece(piece, square, PlacePieceStrategy::default())?;
}
Some(("sight", matches)) => {
let square = matches
.get_one::<String>("square")
.ok_or(CommandHandlingError::MissingArgument("square"))?;
let square = square.parse::<Square>()?;
let sight = state.position.sight(square);
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 display = state.position.display().highlight(sight);
println!("\n{display}");
@ -175,6 +186,34 @@ 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")
@ -191,6 +230,26 @@ 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,
@ -272,7 +331,7 @@ fn do_movement_command(
.get_one::<Square>("square")
.ok_or(CommandHandlingError::MissingArgument("square"))?;
let movement = state.position.movement(square);
let movement = state.position.movement_piece(square);
let display = state.position.display().highlight(movement);
println!("\n{display}");
@ -282,6 +341,25 @@ 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}");
@ -312,7 +390,7 @@ fn main() -> Result<(), String> {
loop {
if should_print_position {
println!("{}", &state.position);
println!("{} to move.", state.position.board.active_color());
println!("{} to move.", state.position.active_color());
}
let readline = editor.readline("\n? ");

View file

@ -3,7 +3,8 @@
use crate::{Move, MoveRecord};
use chessfriend_board::{
Board, BoardProvider, CastleParameters, PlacePieceError, PlacePieceStrategy,
castle::CastleEvaluationError, movement::Movement,
castle::{CastleEvaluationError, CastleRightsOption},
movement::Movement,
};
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
use thiserror::Error;
@ -17,7 +18,7 @@ pub enum ValidateMove {
Yes,
}
#[derive(Debug, Error, Eq, PartialEq)]
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum MakeMoveError {
#[error("no piece on {0}")]
NoPiece(Square),
@ -251,7 +252,7 @@ impl<T: BoardProvider> MakeMoveInternal for T {
// original board state is preserved.
let record = MoveRecord::new(board, ply, None);
board.revoke_castling_right_unwrapped(active_color, wing);
board.revoke_castling_rights_active(wing.into());
self.advance_board_state(&ply, &king, None, HalfMoveClock::Advance);
@ -307,21 +308,23 @@ impl<T: BoardProvider> MakeMoveInternal for T {
Shape::Rook => {
let origin = ply.origin_square();
if board.color_has_castling_right(None, Wing::KingSide) {
if board.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_right(None, Wing::KingSide);
board.revoke_castling_rights(None, Wing::KingSide.into());
}
}
let queenside_parameters =
CastleParameters::get(board.active_color(), Wing::QueenSide);
if origin == queenside_parameters.origin.rook {
board.revoke_castling_right(None, Wing::QueenSide);
board.revoke_castling_rights(None, Wing::QueenSide.into());
}
}
Shape::King => board.revoke_all_castling_rights(),
Shape::King => {
board.revoke_castling_rights(None, CastleRightsOption::All);
}
_ => {}
}
@ -559,7 +562,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.color_has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
Ok(())
}
@ -579,7 +582,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.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide));
assert!(!board.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::HalfMoveClock, Board, CastleRights};
use chessfriend_board::{Board, CastleRights, board::HalfMoveClock};
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(Debug, Error, Eq, PartialEq)]
#[derive(Clone, 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.color_has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(!board.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.color_has_castling_right_unwrapped(Color::White, Wing::QueenSide));
assert!(!board.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,9 +1,13 @@
use chessfriend_position::{Position, fen::FromFenStr, perft::Perft};
use chessfriend_position::{
Position,
fen::{FromFenStr, ToFenStr},
};
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")]
@ -14,17 +18,18 @@ 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)
};
let nodes_searched = position.perft(depth);
println!("fen \"{}\"", position.to_fen_str().unwrap());
println!("depth {depth}");
println!("nodes {nodes_searched}");
let counters = position.perft(depth);
println!("\n{counters}");
Ok(())
}

View file

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

View file

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

View file

@ -6,25 +6,25 @@ use crate::fen::{FromFenStr, FromFenStrError};
use captures::CapturesList;
use chessfriend_bitboard::BitBoard;
use chessfriend_board::{
display::DiagramFormatter, fen::ToFenStr, Board, PlacePieceError, PlacePieceStrategy,
ZobristState,
Board, PlacePieceError, PlacePieceStrategy, ZobristState, display::DiagramFormatter,
fen::ToFenStr,
};
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 board: Board,
pub(crate) board: Board,
pub(crate) moves: Vec<MoveRecord>,
pub(crate) captures: CapturesList,
@ -48,6 +48,16 @@ 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 {
@ -76,12 +86,14 @@ impl Position {
}
impl Position {
pub fn sight(&self, square: Square) -> BitBoard {
self.board.sight(square)
/// Calculate sight of a piece on the provided [`Square`].
pub fn sight_piece(&self, square: Square) -> BitBoard {
self.board.sight_piece(square)
}
pub fn movement(&self, square: Square) -> BitBoard {
self.board.movement(square)
/// Calculate movement of a piece on the provided [`Square`].
pub fn movement_piece(&self, square: Square) -> BitBoard {
self.board.movement_piece(square)
}
}
@ -117,7 +129,7 @@ impl Position {
);
});
let move_is_legal = !test_board.color_is_in_check(Some(active_color_before_move));
let move_is_legal = !test_board.is_in_check();
test_board.unmake_move(&record).unwrap_or_else(|err| {
panic!(
@ -153,8 +165,8 @@ impl Position {
}
impl Position {
pub fn active_sight(&self) -> BitBoard {
self.board.active_sight()
pub fn sight_active(&self) -> BitBoard {
self.board.sight_active()
}
/// A [`BitBoard`] of all squares the given color can see.
@ -275,7 +287,7 @@ impl Position {
}
let target_bitboard: BitBoard = target.into();
if !(self.movement(origin) & target_bitboard).is_populated() {
if !(self.movement_piece(origin) & target_bitboard).is_populated() {
return None;
}
@ -344,7 +356,7 @@ impl fmt::Display for Position {
#[cfg(test)]
mod tests {
use super::*;
use crate::{test_position, Position};
use crate::{Position, test_position};
use chessfriend_core::piece;
#[test]

View file

@ -8,7 +8,7 @@
use chessfriend_core::Color;
use chessfriend_moves::{
assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move,
Move, assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply,
};
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.active_color_is_in_check());
assert!(pos.board().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.active_color_is_in_check());
assert!(pos.board().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.active_color_is_in_check());
assert!(!pos.board().is_in_check());
let rook_moves: HashSet<_> = pos.all_legal_moves(None).collect();

View file

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