Compare commits

..

1 commit

Author SHA1 Message Date
0d8653894a An attempt at making unit structs for Color and piece Shape
My idea was to implement traits on Piece that return sight lines, etc. This has
turned out to be much more complex than I thought it would be. Ultimately, I
don't think it's worth the effort.
2024-01-14 10:23:35 -08:00
106 changed files with 3423 additions and 11325 deletions

617
Cargo.lock generated
View file

@ -1,617 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "bitflags"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chessfriend"
version = "0.1.0"
[[package]]
name = "chessfriend_bitboard"
version = "0.1.0"
dependencies = [
"chessfriend_core",
"forward_ref",
]
[[package]]
name = "chessfriend_board"
version = "0.1.0"
dependencies = [
"chessfriend_bitboard",
"chessfriend_core",
"rand",
"thiserror",
]
[[package]]
name = "chessfriend_core"
version = "0.1.0"
dependencies = [
"rand",
"thiserror",
]
[[package]]
name = "chessfriend_moves"
version = "0.1.0"
dependencies = [
"chessfriend_bitboard",
"chessfriend_board",
"chessfriend_core",
"thiserror",
]
[[package]]
name = "chessfriend_position"
version = "0.1.0"
dependencies = [
"chessfriend_bitboard",
"chessfriend_board",
"chessfriend_core",
"chessfriend_moves",
"thiserror",
]
[[package]]
name = "clap"
version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "clipboard-win"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c57002a5d9be777c1ef967e33674dac9ebd310d8893e4e3437b14d5f0f6372cc"
dependencies = [
"error-code",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "error-code"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc"
[[package]]
name = "explorer"
version = "0.1.0"
dependencies = [
"anyhow",
"chessfriend_board",
"chessfriend_core",
"chessfriend_moves",
"chessfriend_position",
"clap",
"rustyline",
"shlex",
"thiserror",
]
[[package]]
name = "fd-lock"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
dependencies = [
"cfg-if",
"rustix",
"windows-sys",
]
[[package]]
name = "forward_ref"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e"
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "nibble_vec"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "perft"
version = "0.1.0"
dependencies = [
"anyhow",
"chessfriend_board",
"chessfriend_position",
"clap",
"thiserror",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "radix_trie"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
dependencies = [
"endian-type",
"nibble_vec",
]
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "rustix"
version = "0.38.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "rustyline"
version = "13.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86"
dependencies = [
"bitflags",
"cfg-if",
"clipboard-win",
"fd-lock",
"home",
"libc",
"log",
"memchr",
"nix",
"radix_trie",
"unicode-segmentation",
"unicode-width",
"utf8parse",
"winapi",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View file

@ -1,16 +0,0 @@
[workspace]
members = [
"bitboard",
"board",
"chessfriend",
"core",
"explorer",
"moves",
"perft",
"position",
]
resolver = "3"
[profile.release-debug]
inherits = "release"
debug = true

View file

@ -1,7 +0,0 @@
{
"folders": [
{
"path": "."
}
]
}

View file

@ -1,4 +0,0 @@
all:
cargo build

113
README.md
View file

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

View file

@ -1,10 +0,0 @@
[package]
name = "chessfriend_bitboard"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chessfriend_core = { path = "../core" }
forward_ref = "1.0.0"

View file

@ -1,596 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::bit_scanner::{LeadingBitScanner, TrailingBitScanner};
use crate::direction::IterationDirection;
use crate::library;
use chessfriend_core::{Color, Direction, File, Rank, Square};
use forward_ref::{forward_ref_binop, forward_ref_op_assign, forward_ref_unop};
use std::fmt;
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not};
#[allow(clippy::cast_possible_truncation)]
const SQUARES_NUM: u8 = Square::NUM as u8;
/// A bitfield representation of a chess board that uses the bits of a 64-bit
/// unsigned integer to represent whether a square on the board is occupied.
/// Squares are laid out as follows, starting at the bottom left, going row-wise,
/// and ending at the top right corner:
///
/// ```text
/// +-------------------------+
/// 8 | 56 57 58 59 60 61 62 63 |
/// 7 | 48 49 50 51 52 53 54 55 |
/// 6 | 40 41 42 43 44 45 46 47 |
/// 5 | 32 33 34 35 36 37 38 39 |
/// 4 | 24 25 26 27 28 29 30 31 |
/// 3 | 16 17 18 19 20 21 22 23 |
/// 2 | 8 9 10 11 12 13 14 15 |
/// 1 | 0 1 2 3 4 5 6 7 |
/// +-------------------------+
/// A B C D E F G H
/// ```
///
#[must_use]
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct BitBoard(pub(crate) u64);
macro_rules! moves_getter {
($getter_name:ident) => {
pub fn $getter_name(sq: Square) -> BitBoard {
library::library().$getter_name(sq)
}
};
}
impl BitBoard {
pub const EMPTY: BitBoard = BitBoard(u64::MIN);
pub const FULL: BitBoard = BitBoard(u64::MAX);
pub const fn new(bits: u64) -> BitBoard {
BitBoard(bits)
}
pub const fn rank(rank: Rank) -> BitBoard {
library::RANKS[rank.as_index()]
}
pub const fn file(file: File) -> BitBoard {
library::FILES[file.as_index()]
}
pub fn ray(sq: Square, dir: Direction) -> BitBoard {
library::library().ray(sq, dir)
}
pub fn pawn_attacks(sq: Square, color: Color) -> BitBoard {
library::library().pawn_attacks(sq, color)
}
pub fn pawn_pushes(sq: Square, color: Color) -> BitBoard {
library::library().pawn_pushes(sq, color)
}
moves_getter!(knight_moves);
moves_getter!(bishop_moves);
moves_getter!(rook_moves);
moves_getter!(queen_moves);
moves_getter!(king_moves);
pub const fn kingside(color: Color) -> &'static BitBoard {
&library::KINGSIDES[color as usize]
}
pub const fn queenside(color: Color) -> &'static BitBoard {
&library::QUEENSIDES[color as usize]
}
}
impl BitBoard {
/// Converts this [`BitBoard`] to an unsigned 64-bit integer.
#[must_use]
pub const fn as_bits(&self) -> u64 {
self.0
}
/// Returns `true` if this [`BitBoard`] has no bits set. This is the opposite
/// of [`BitBoard::is_populated`].
///
/// ## Examples
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// assert!(BitBoard::EMPTY.is_empty());
/// assert!(!BitBoard::full().is_empty());
/// assert!(!BitBoard::new(0b1000).is_empty());
/// ```
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0 == 0
}
/// Returns `true` if the [`BitBoard`] has at least one bit set. This is the
/// opposite of [`BitBoard::is_empty`].
///
/// ## Examples
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// assert!(!BitBoard::EMPTY.is_populated());
/// assert!(BitBoard::full().is_populated());
/// assert!(BitBoard::new(0b1).is_populated());
/// ```
#[must_use]
pub const fn is_populated(&self) -> bool {
self.0 != 0
}
/// Returns `true` if this [`BitBoard`] has the bit corresponding to `square` set.
///
/// ## Examples
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// use chessfriend_core::Square;
///
/// let square = Square::E4;
/// let mut bitboard = BitBoard::new(0b1001100);
///
/// assert!(bitboard.contains(Square::C1));
/// assert!(!bitboard.contains(Square::B1));
/// ```
#[must_use]
pub fn contains(self, square: Square) -> bool {
let square_bitboard: BitBoard = square.into();
!(self & square_bitboard).is_empty()
}
/// Counts the number of set squares (1 bits) in this [`BitBoard`].
///
/// ## Examples
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// assert_eq!(BitBoard::EMPTY.population_count(), 0);
/// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6);
/// assert_eq!(BitBoard::FULL.population_count(), 64);
/// ```
#[must_use]
pub const fn population_count(&self) -> u32 {
self.0.count_ones()
}
/// Set a square in this [`BitBoard`] by toggling the corresponding bit to 1.
/// This always succeeds, even if the bit was already set.
///
/// ## Examples
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// use chessfriend_core::Square;
///
/// let mut bitboard = BitBoard::new(0b1001100);
/// bitboard.set(Square::E4);
/// assert!(bitboard.contains(Square::E4));
/// ```
pub fn set(&mut self, square: Square) {
let square_bitboard: BitBoard = square.into();
self.0 |= square_bitboard.0;
}
/// Clear a square (set it to 0) in this [`BitBoard`]. This always succeeds
/// even if the bit is not set.
///
/// ## Examples
///
/// ```
/// use chessfriend_bitboard::BitBoard;
/// use chessfriend_core::Square;
///
/// let mut bitboard = BitBoard::new(0b1001100);
/// bitboard.clear(Square::C1);
/// assert!(!bitboard.contains(Square::C1));
/// ```
pub fn clear(&mut self, square: Square) {
let square_bitboard: BitBoard = square.into();
self.0 &= !square_bitboard.0;
}
/// Returns `true` if this [`BitBoard`] represents a single square.
///
/// ## Examples
///
/// ```
/// 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::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares");
/// assert!(BitBoard::new(0b10000000000000).is_single_square());
/// ```
#[must_use]
pub fn is_single_square(&self) -> bool {
self.0.is_power_of_two()
}
/// Return an Iterator over the occupied squares.
#[must_use]
pub fn occupied_squares(
&self,
direction: &IterationDirection,
) -> Box<dyn Iterator<Item = Square>> {
match direction {
IterationDirection::Leading => Box::new(self.occupied_squares_leading()),
IterationDirection::Trailing => Box::new(self.occupied_squares_trailing()),
}
}
/// 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)
}
#[must_use]
pub fn occupied_squares_trailing(&self) -> TrailingBitScanner {
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 {
IterationDirection::Leading => self.first_occupied_square_leading(),
IterationDirection::Trailing => self.first_occupied_square_trailing(),
}
}
/// If the board is not empty, returns the first occupied square on the
/// board, starting at the leading (most-significant) end of the board. If
/// the board is empty, returns `None`.
#[must_use]
pub fn first_occupied_square_leading(self) -> Option<Square> {
let leading_zeros = self.leading_zeros();
if leading_zeros < SQUARES_NUM {
unsafe {
Some(Square::from_index_unchecked(
SQUARES_NUM - leading_zeros - 1,
))
}
} else {
None
}
}
/// If the board is not empty, returns the first occupied square on the
/// board, starting at the trailing (least-significant) end of the board.
/// If the board is empty, returns `None`.
#[must_use]
pub fn first_occupied_square_trailing(self) -> Option<Square> {
let trailing_zeros = self.trailing_zeros();
if trailing_zeros < SQUARES_NUM {
unsafe { Some(Square::from_index_unchecked(trailing_zeros)) }
} else {
None
}
}
}
impl BitBoard {
#[must_use]
#[allow(clippy::cast_possible_truncation)]
fn leading_zeros(self) -> u8 {
self.0.leading_zeros() as u8
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
fn trailing_zeros(self) -> u8 {
self.0.trailing_zeros() as u8
}
}
impl Default for BitBoard {
fn default() -> Self {
BitBoard::EMPTY
}
}
impl From<BitBoard> for u64 {
fn from(value: BitBoard) -> Self {
value.as_bits()
}
}
impl From<File> for BitBoard {
fn from(value: File) -> Self {
library::FILES[value.as_index()]
}
}
impl From<Option<Square>> for BitBoard {
fn from(value: Option<Square>) -> Self {
value.map_or(BitBoard::EMPTY, Into::<BitBoard>::into)
}
}
impl From<Rank> for BitBoard {
fn from(value: Rank) -> Self {
library::FILES[value.as_index()]
}
}
impl From<Square> for BitBoard {
fn from(value: Square) -> Self {
BitBoard(1u64 << value as u32)
}
}
impl FromIterator<Square> for BitBoard {
fn from_iter<T: IntoIterator<Item = Square>>(iter: T) -> Self {
iter.into_iter().fold(BitBoard::EMPTY, |mut acc, sq| {
acc.set(sq);
acc
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TryFromBitBoardError {
NotSingleSquare,
}
impl TryFrom<BitBoard> for Square {
type Error = TryFromBitBoardError;
fn try_from(value: BitBoard) -> Result<Self, Self::Error> {
if !value.is_single_square() {
return Err(TryFromBitBoardError::NotSingleSquare);
}
unsafe { Ok(Square::from_index_unchecked(value.trailing_zeros())) }
}
}
impl fmt::Binary for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Delegate to u64's implementation of Binary.
fmt::Binary::fmt(&self.0, f)
}
}
impl fmt::LowerHex for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Delegate to u64's implementation of LowerHex.
fmt::LowerHex::fmt(&self.0, f)
}
}
impl fmt::UpperHex for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Delegate to u64's implementation of UpperHex.
fmt::UpperHex::fmt(&self.0, f)
}
}
impl fmt::Display for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let binary_ranks = format!("{:064b}", self.0)
.chars()
.rev()
.map(String::from)
.collect::<Vec<String>>();
let mut ranks_written = 0;
for rank in binary_ranks.chunks(8).rev() {
write!(f, "{}", rank.join(" "))?;
ranks_written += 1;
if ranks_written < 8 {
writeln!(f)?;
}
}
Ok(())
}
}
impl fmt::Debug for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "BitBoard({:064b})", self.0)
}
}
macro_rules! infix_op {
($trait_type:ident, $func_name:ident, $left_type:ty, $right_type:ty) => {
impl std::ops::$trait_type<$right_type> for $left_type {
type Output = Self;
#[inline]
fn $func_name(self, rhs: $right_type) -> Self::Output {
BitBoard(std::ops::$trait_type::$func_name(self.0, rhs.0))
}
}
forward_ref_binop!(impl $trait_type, $func_name for $left_type, $right_type);
};
}
macro_rules! assign_op {
($trait_type:ident, $func_name:ident, $type:ty) => {
impl $trait_type for $type {
#[inline]
fn $func_name(&mut self, rhs: $type) {
$trait_type::$func_name(&mut self.0, rhs.0)
}
}
forward_ref_op_assign!(impl $trait_type, $func_name for $type, $type);
};
}
infix_op!(BitAnd, bitand, BitBoard, BitBoard);
infix_op!(BitOr, bitor, BitBoard, BitBoard);
infix_op!(BitXor, bitxor, BitBoard, BitBoard);
assign_op!(BitAndAssign, bitand_assign, BitBoard);
assign_op!(BitOrAssign, bitor_assign, BitBoard);
assign_op!(BitXorAssign, bitxor_assign, BitBoard);
impl Not for BitBoard {
type Output = Self;
#[inline]
fn not(self) -> Self::Output {
BitBoard(!self.0)
}
}
forward_ref_unop!(impl Not, not for BitBoard);
#[cfg(test)]
mod tests {
use super::*;
use crate::bitboard;
use chessfriend_core::Square;
#[test]
#[ignore]
fn display_and_debug() {
let bb = BitBoard::file(File::A)
| BitBoard::file(File::D)
| BitBoard::rank(Rank::FIVE)
| BitBoard::rank(Rank::EIGHT);
println!("{}", &bb);
}
#[test]
#[allow(clippy::unreadable_literal)]
fn rank() {
assert_eq!(BitBoard::rank(Rank::ONE).0, 0xFF, "Rank 1");
assert_eq!(BitBoard::rank(Rank::TWO).0, 0xFF00, "Rank 2");
assert_eq!(BitBoard::rank(Rank::THREE).0, 0xFF0000, "Rank 3");
assert_eq!(BitBoard::rank(Rank::FOUR).0, 0xFF000000, "Rank 4");
assert_eq!(BitBoard::rank(Rank::FIVE).0, 0xFF00000000, "Rank 5");
assert_eq!(BitBoard::rank(Rank::SIX).0, 0xFF0000000000, "Rank 6");
assert_eq!(BitBoard::rank(Rank::SEVEN).0, 0xFF000000000000, "Rank 7");
assert_eq!(BitBoard::rank(Rank::EIGHT).0, 0xFF00000000000000, "Rank 8");
}
#[test]
fn single_rank_occupancy() {
#[allow(clippy::unreadable_literal)]
let bb = BitBoard(0b01010100);
let expected_squares = [Square::G1, Square::E1, Square::C1];
bb.occupied_squares(&IterationDirection::Leading)
.zip(expected_squares)
.for_each(|(a, b)| assert_eq!(a, b));
}
#[test]
fn occupancy_spot_check() {
#[allow(clippy::unreadable_literal)]
let bb =
BitBoard(0b10000000_00000000_00100000_00000100_00000000_00000000_00010000_00001000);
let expected_squares = [Square::H8, Square::F6, Square::C5, Square::E2, Square::D1];
bb.occupied_squares(&IterationDirection::Leading)
.zip(expected_squares)
.for_each(|(a, b)| assert_eq!(a, b));
}
#[test]
fn xor() {
let a = bitboard![C5 G7];
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);
}
#[test]
fn bitand_assign() {
let mut a = bitboard![C5 G7];
let b = bitboard![B5 G7 H3];
a &= b;
assert_eq!(a, bitboard![G7]);
}
#[test]
fn bitor_assign() {
let mut a = bitboard![C5 G7];
let b = bitboard![B5 G7 H3];
a |= b;
assert_eq!(a, bitboard![B5 C5 G7 H3]);
}
#[test]
fn from_square() {
assert_eq!(BitBoard::from(Square::A1), BitBoard(0b1));
assert_eq!(BitBoard::from(Square::H8), BitBoard(1 << 63));
}
#[test]
fn first_occupied_squares() {
let bb = bitboard![A8 E1];
assert_eq!(bb.first_occupied_square_leading(), Some(Square::A8));
assert_eq!(bb.first_occupied_square_trailing(), Some(Square::E1));
let bb = bitboard![D6 E7 F8];
assert_eq!(bb.first_occupied_square_trailing(), Some(Square::D6));
}
}

View file

@ -1,6 +0,0 @@
#[derive(Default)]
pub enum IterationDirection {
#[default]
Leading,
Trailing,
}

View file

@ -1,22 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
pub mod bit_scanner;
mod bitboard;
mod direction;
mod library;
mod shifts;
pub use bitboard::BitBoard;
pub use direction::IterationDirection;
#[macro_export]
macro_rules! bitboard {
($($sq:ident)* $(,)?) => {
{
let mut bitboard = $crate::BitBoard::EMPTY;
$(bitboard.set(chessfriend_core::Square::$sq);)*
bitboard
}
};
}

View file

@ -1,269 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
//! # The Bitboard Library
//!
//! This module implements a collection of commonly used bitboards that can be
//! looked up efficiently.
//!
//! The `library()` method returns a static instance of a `Library`, which
//! provides getters for all available bitboards.
use crate::BitBoard;
use chessfriend_core::{Color, Direction, File, Rank, Square};
use std::sync::OnceLock;
#[allow(clippy::identity_op)]
#[allow(clippy::erasing_op)]
pub(crate) const RANKS: [BitBoard; Rank::NUM] = [
BitBoard(0xFF << (0 * 8)),
BitBoard(0xFF << (1 * 8)),
BitBoard(0xFF << (2 * 8)),
BitBoard(0xFF << (3 * 8)),
BitBoard(0xFF << (4 * 8)),
BitBoard(0xFF << (5 * 8)),
BitBoard(0xFF << (6 * 8)),
BitBoard(0xFF << (7 * 8)),
];
#[allow(clippy::identity_op)]
#[allow(clippy::erasing_op)]
pub(crate) const FILES: [BitBoard; File::NUM] = [
BitBoard(0x0101_0101_0101_0101 << 0),
BitBoard(0x0101_0101_0101_0101 << 1),
BitBoard(0x0101_0101_0101_0101 << 2),
BitBoard(0x0101_0101_0101_0101 << 3),
BitBoard(0x0101_0101_0101_0101 << 4),
BitBoard(0x0101_0101_0101_0101 << 5),
BitBoard(0x0101_0101_0101_0101 << 6),
BitBoard(0x0101_0101_0101_0101 << 7),
];
/// Bitboards representing the kingside of the board, per color.
pub(crate) const KINGSIDES: [BitBoard; Color::NUM] = [
BitBoard(0xF0F0_F0F0_F0F0_F0F0),
BitBoard(0x0F0F_0F0F_0F0F_0F0F),
];
/// Bitboards representing the queenside of the board, per color.
pub(crate) const QUEENSIDES: [BitBoard; Color::NUM] = [
BitBoard(0x0F0F_0F0F_0F0F_0F0F),
BitBoard(0xF0F0_F0F0_F0F0_F0F0),
];
/// A bitboard representing the light squares.
#[allow(dead_code)]
pub(crate) const LIGHT_SQUARES: BitBoard =
BitBoard(0x5555 | 0x5555 << 16 | 0x5555 << 32 | 0x5555 << 48);
/// A bitboad representing the dark squares
#[allow(dead_code)]
pub(crate) const DARK_SQUARES: BitBoard = BitBoard(!LIGHT_SQUARES.0);
pub(super) fn library() -> &'static MoveLibrary {
static MOVE_LIBRARY: MoveLibraryWrapper = MoveLibraryWrapper::new();
MOVE_LIBRARY.library()
}
macro_rules! library_getter {
($name:ident) => {
pub(super) const fn $name(&self, sq: Square) -> BitBoard {
self.$name[sq as usize]
}
};
}
struct MoveLibraryWrapper {
library: OnceLock<MoveLibrary>,
}
impl MoveLibraryWrapper {
const fn new() -> Self {
Self {
library: OnceLock::<MoveLibrary>::new(),
}
}
fn library(&self) -> &MoveLibrary {
self.library.get_or_init(|| {
let mut library = MoveLibrary::new();
library.init();
library
})
}
}
#[derive(Debug)]
pub(super) struct MoveLibrary {
// Rays
rays: [[BitBoard; Direction::NUM]; Square::NUM],
// Piecewise move tables
pawn_attacks: [[BitBoard; Square::NUM]; Color::NUM],
pawn_pushes: [[BitBoard; Square::NUM]; Color::NUM],
knight_moves: [BitBoard; Square::NUM],
bishop_moves: [BitBoard; Square::NUM],
rook_moves: [BitBoard; Square::NUM],
queen_moves: [BitBoard; Square::NUM],
king_moves: [BitBoard; Square::NUM],
}
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],
}
}
fn init(&mut self) {
for sq in Square::ALL {
self.init_pawn_moves(sq);
self.init_orthogonal_rays(sq);
self.init_diagonal_rays(sq);
self.init_knight_moves(sq as usize);
self.init_bishop_moves(sq);
self.init_rook_moves(sq);
self.init_queen_moves(sq);
self.init_king_moves(sq as usize);
}
}
fn init_orthogonal_rays(&mut self, sq: Square) {
let sq_bb: BitBoard = sq.into();
let rays = &mut self.rays[sq as usize];
rays[Direction::North as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_north_one);
rays[Direction::South as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_south_one);
rays[Direction::East as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_east_one);
rays[Direction::West as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_west_one);
}
fn init_diagonal_rays(&mut self, sq: Square) {
let sq_bb: BitBoard = sq.into();
let rays = &mut self.rays[sq as usize];
rays[Direction::NorthEast as usize] =
Self::_generate_ray(sq_bb, BitBoard::shift_north_east_one);
rays[Direction::NorthWest as usize] =
Self::_generate_ray(sq_bb, BitBoard::shift_north_west_one);
rays[Direction::SouthWest as usize] =
Self::_generate_ray(sq_bb, BitBoard::shift_south_west_one);
rays[Direction::SouthEast as usize] =
Self::_generate_ray(sq_bb, BitBoard::shift_south_east_one);
}
fn init_king_moves(&mut self, idx: usize) {
let king = BitBoard::new(1 << idx);
let mut attacks = king.shift_east_one() | king.shift_west_one();
let king = king | attacks;
attacks |= king.shift_north_one() | king.shift_south_one();
self.king_moves[idx] = attacks;
}
/// Calculate bitboards representing knight moves from each square on the
/// board. The algorithm is described on the [Chess Programming Wiki][cpw].
///
/// [cpw]: https://www.chessprogramming.org/Knight_Pattern
fn init_knight_moves(&mut self, idx: usize) {
let knight = BitBoard::new(1 << idx);
let east = knight.shift_east_one();
let west = knight.shift_west_one();
let mut attacks = (east | west).shift_north(2);
attacks |= (east | west).shift_south(2);
let east = east.shift_east_one();
let west = west.shift_west_one();
attacks |= (east | west).shift_north_one();
attacks |= (east | west).shift_south_one();
self.knight_moves[idx] = attacks;
}
fn init_bishop_moves(&mut self, sq: Square) {
let rays = self.rays[sq as usize];
self.bishop_moves[sq as usize] = rays[Direction::NorthWest as usize]
| rays[Direction::NorthEast as usize]
| rays[Direction::SouthEast as usize]
| rays[Direction::SouthWest as usize];
}
fn init_rook_moves(&mut self, sq: Square) {
let rays = self.rays[sq as usize];
self.rook_moves[sq as usize] = rays[Direction::North as usize]
| rays[Direction::East as usize]
| rays[Direction::South as usize]
| rays[Direction::West as usize];
}
fn init_queen_moves(&mut self, sq: Square) {
let rook_moves = self.rook_moves[sq as usize];
let bishop_moves = self.bishop_moves[sq as usize];
self.queen_moves[sq as usize] = rook_moves | bishop_moves;
}
fn init_pawn_moves(&mut self, sq: Square) {
let bitboard: BitBoard = sq.into();
self.pawn_attacks[Color::White as usize][sq as usize] =
bitboard.shift_north_west_one() | bitboard.shift_north_east_one();
self.pawn_attacks[Color::Black as usize][sq as usize] =
bitboard.shift_south_west_one() | bitboard.shift_south_east_one();
self.pawn_pushes[Color::White as usize][sq as usize] = {
let mut push = bitboard.shift_north_one();
if !(bitboard & RANKS[1]).is_empty() {
push |= push.shift_north_one();
}
push
};
self.pawn_pushes[Color::Black as usize][sq as usize] = {
let mut push = bitboard.shift_south_one();
if !(bitboard & RANKS[6]).is_empty() {
push |= push.shift_south_one();
}
push
};
}
fn _generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard {
let mut ray = BitBoard::EMPTY;
let mut iter = shift(&sq);
while !iter.is_empty() {
ray |= iter;
iter = shift(&iter);
}
ray
}
pub(super) const fn ray(&self, sq: Square, dir: Direction) -> BitBoard {
self.rays[sq as usize][dir as usize]
}
pub(super) const fn pawn_pushes(&self, sq: Square, color: Color) -> BitBoard {
self.pawn_pushes[color as usize][sq as usize]
}
pub(super) const fn pawn_attacks(&self, sq: Square, color: Color) -> BitBoard {
self.pawn_attacks[color as usize][sq as usize]
}
library_getter!(knight_moves);
library_getter!(bishop_moves);
library_getter!(rook_moves);
library_getter!(queen_moves);
library_getter!(king_moves);
}

View file

@ -1,12 +1,8 @@
[package]
name = "chessfriend_board"
name = "board"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chessfriend_bitboard = { path = "../bitboard" }
chessfriend_core = { path = "../core" }
rand = "0.9.1"
thiserror = "2"

View file

@ -1,11 +1,8 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_core::Square;
macro_rules! bit_scanner {
($name:ident) => {
#[derive(Clone, Debug)]
pub struct $name {
pub(crate) struct $name {
bits: u64,
shift: usize,
}
@ -21,16 +18,8 @@ macro_rules! bit_scanner {
bit_scanner!(LeadingBitScanner);
bit_scanner!(TrailingBitScanner);
fn index_to_square(index: usize) -> Square {
debug_assert!(index < Square::NUM);
unsafe {
#[allow(clippy::cast_possible_truncation)]
Square::from_index_unchecked(index as u8)
}
}
impl Iterator for LeadingBitScanner {
type Item = Square;
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
let u64bits = u64::BITS as usize;
@ -51,12 +40,12 @@ impl Iterator for LeadingBitScanner {
// Shift 1 additional place to account for the 1 that `leading_zeros` found.
self.shift += leading_zeros + 1;
Some(index_to_square(position))
Some(position)
}
}
impl Iterator for TrailingBitScanner {
type Item = Square;
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
let u64bits = u64::BITS as usize;
@ -77,7 +66,7 @@ impl Iterator for TrailingBitScanner {
// Shift 1 additional place to account for the 1 that `leading_zeros` found.
self.shift += trailing_zeros + 1;
Some(index_to_square(position))
Some(position)
}
}
@ -94,17 +83,17 @@ mod tests {
#[test]
fn leading_one() {
let mut scanner = LeadingBitScanner::new(1);
assert_eq!(scanner.next(), Some(Square::A1));
assert_eq!(scanner.next(), Some(0));
assert_eq!(scanner.next(), None);
}
#[test]
fn leading_complex() {
let mut scanner = LeadingBitScanner::new(0b_1100_0101);
assert_eq!(scanner.next(), Some(Square::H1));
assert_eq!(scanner.next(), Some(Square::G1));
assert_eq!(scanner.next(), Some(Square::C1));
assert_eq!(scanner.next(), Some(Square::A1));
let mut scanner = LeadingBitScanner::new(0b11000101);
assert_eq!(scanner.next(), Some(7));
assert_eq!(scanner.next(), Some(6));
assert_eq!(scanner.next(), Some(2));
assert_eq!(scanner.next(), Some(0));
assert_eq!(scanner.next(), None);
}
@ -117,17 +106,17 @@ mod tests {
#[test]
fn trailing_one() {
let mut scanner = TrailingBitScanner::new(1);
assert_eq!(scanner.next(), Some(Square::A1));
assert_eq!(scanner.next(), Some(0));
assert_eq!(scanner.next(), None);
}
#[test]
fn trailing_complex() {
let mut scanner = TrailingBitScanner::new(0b_1100_0101);
assert_eq!(scanner.next(), Some(Square::A1));
assert_eq!(scanner.next(), Some(Square::C1));
assert_eq!(scanner.next(), Some(Square::G1));
assert_eq!(scanner.next(), Some(Square::H1));
let mut scanner = TrailingBitScanner::new(0b11000101);
assert_eq!(scanner.next(), Some(0));
assert_eq!(scanner.next(), Some(2));
assert_eq!(scanner.next(), Some(6));
assert_eq!(scanner.next(), Some(7));
assert_eq!(scanner.next(), None);
}
}

View file

@ -0,0 +1,262 @@
// Eryn Wells <eryn@erynwells.me>
use super::library::{library, FILES, RANKS};
use super::LeadingBitScanner;
use crate::{square::Direction, Square};
use std::fmt;
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not};
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub(crate) struct BitBoard(pub(super) u64);
macro_rules! moves_getter {
($getter_name:ident) => {
pub fn $getter_name(sq: Square) -> BitBoard {
library().$getter_name(sq)
}
};
}
impl BitBoard {
pub const fn empty() -> BitBoard {
BitBoard(0)
}
pub fn new(bits: u64) -> BitBoard {
BitBoard(bits)
}
pub fn rank(rank: usize) -> BitBoard {
assert!(rank < 8);
RANKS[rank]
}
pub fn file(file: usize) -> BitBoard {
assert!(file < 8);
FILES[file]
}
pub fn ray(sq: Square, dir: Direction) -> BitBoard {
library().ray(sq, dir)
}
moves_getter!(knight_moves);
moves_getter!(bishop_moves);
moves_getter!(rook_moves);
moves_getter!(queen_moves);
moves_getter!(king_moves);
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn has_piece_at(self, sq: Square) -> bool {
!(self & sq.into()).is_empty()
}
pub fn place_piece_at(&mut self, sq: Square) {
let sq_bb: BitBoard = sq.into();
*self |= sq_bb
}
fn remove_piece_at(&mut self, sq: Square) {
let sq_bb: BitBoard = sq.into();
*self &= !sq_bb
}
}
impl BitBoard {
/// Return an Iterator over the occupied squares, starting from the leading
/// (most-significant bit) end of the field.
pub(crate) fn occupied_squares(&self) -> impl Iterator<Item = Square> {
LeadingBitScanner::new(self.0).map(Square::from_index)
}
/// Return an Iterator over the occupied squares, starting from the trailing
/// (least-significant bit) end of the field.
pub(crate) fn occupied_squares_trailing(&self) -> impl Iterator<Item = Square> {
LeadingBitScanner::new(self.0).map(Square::from_index)
}
}
impl From<Square> for BitBoard {
fn from(value: Square) -> Self {
BitBoard(1 << value as u64)
}
}
impl fmt::Binary for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Delegate to u64's implementation of Binary.
fmt::Binary::fmt(&self.0, f)
}
}
impl fmt::LowerHex for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Delegate to u64's implementation of LowerHex.
fmt::LowerHex::fmt(&self.0, f)
}
}
impl fmt::UpperHex for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Delegate to u64's implementation of UpperHex.
fmt::UpperHex::fmt(&self.0, f)
}
}
impl fmt::Display for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let binary_ranks = format!("{:064b}", self.0)
.chars()
.rev()
.map(|c| String::from(c))
.collect::<Vec<String>>();
let mut ranks_written = 0;
for rank in binary_ranks.chunks(8).rev() {
let joined_rank = rank.join(" ");
write!(f, "{}", joined_rank)?;
ranks_written += 1;
if ranks_written < 8 {
write!(f, "\n")?;
}
}
Ok(())
}
}
impl fmt::Debug for BitBoard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
f.debug_tuple("BitBoard")
.field(&format_args!("{:064b}", self.0))
.finish()
}
}
macro_rules! infix_op {
($trait_type:ident, $func_name:ident, $left_type:ty, $right_type:ty) => {
impl $trait_type<$right_type> for $left_type {
type Output = BitBoard;
#[inline]
fn $func_name(self, rhs: $right_type) -> Self::Output {
BitBoard($trait_type::$func_name(self.0, rhs.0))
}
}
};
}
macro_rules! assign_op {
($trait_type:ident, $func_name:ident, $left_type:ty) => {
impl $trait_type for $left_type {
#[inline]
fn $func_name(&mut self, rhs: Self) {
$trait_type::$func_name(&mut self.0, rhs.0)
}
}
};
}
infix_op!(BitAnd, bitand, BitBoard, BitBoard);
assign_op!(BitAndAssign, bitand_assign, BitBoard);
assign_op!(BitOrAssign, bitor_assign, BitBoard);
infix_op!(BitOr, bitor, BitBoard, BitBoard);
impl Not for BitBoard {
type Output = BitBoard;
#[inline]
fn not(self) -> Self::Output {
BitBoard(!self.0)
}
}
impl Not for &BitBoard {
type Output = BitBoard;
#[inline]
fn not(self) -> Self::Output {
BitBoard(!self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Square;
#[test]
fn display_and_debug() {
let bb = BitBoard::file(0) | BitBoard::file(3) | BitBoard::rank(7) | BitBoard::rank(4);
println!("{}", &bb);
println!("{:?}", &bb);
}
#[test]
fn rank() {
assert_eq!(BitBoard::rank(0).0, 0xFF, "Rank 1");
assert_eq!(BitBoard::rank(1).0, 0xFF00, "Rank 2");
assert_eq!(BitBoard::rank(2).0, 0xFF0000, "Rank 3");
assert_eq!(BitBoard::rank(3).0, 0xFF000000, "Rank 4");
assert_eq!(BitBoard::rank(4).0, 0xFF00000000, "Rank 5");
assert_eq!(BitBoard::rank(5).0, 0xFF0000000000, "Rank 6");
assert_eq!(BitBoard::rank(6).0, 0xFF000000000000, "Rank 7");
assert_eq!(BitBoard::rank(7).0, 0xFF00000000000000, "Rank 8");
}
#[test]
fn is_empty() {
assert!(BitBoard(0).is_empty());
assert!(!BitBoard(0xFF).is_empty());
}
#[test]
fn has_piece_at() {
let bb = BitBoard(0b1001100);
assert!(bb.has_piece_at(Square::C1));
assert!(!bb.has_piece_at(Square::B1));
}
#[test]
fn place_piece_at() {
let sq = Square::E4;
let mut bb = BitBoard(0b1001100);
bb.place_piece_at(sq);
assert!(bb.has_piece_at(sq));
}
#[test]
fn remove_piece_at() {
let sq = Square::A3;
let mut bb = BitBoard(0b1001100);
bb.remove_piece_at(sq);
assert!(!bb.has_piece_at(sq));
}
#[test]
fn single_rank_occupancy() {
let bb = BitBoard(0b01010100);
let expected_squares = [Square::G1, Square::E1, Square::C1];
for (a, b) in bb.occupied_squares().zip(expected_squares.iter().cloned()) {
assert_eq!(a, b);
}
}
#[test]
fn occupancy_spot_check() {
let bb =
BitBoard(0b10000000_00000000_00100000_00000100_00000000_00000000_00010000_00001000);
let expected_squares = [Square::H8, Square::F6, Square::C5, Square::E2, Square::D1];
for (a, b) in bb.occupied_squares().zip(expected_squares.iter().cloned()) {
assert_eq!(a, b);
}
}
}

View file

@ -0,0 +1,189 @@
// Eryn Wells <eryn@erynwells.me>
use super::BitBoard;
use crate::{square::Direction, Square};
use std::sync::Once;
pub(super) const RANKS: [BitBoard; 8] = [
BitBoard(0xFF << 0 * 8),
BitBoard(0xFF << 1 * 8),
BitBoard(0xFF << 2 * 8),
BitBoard(0xFF << 3 * 8),
BitBoard(0xFF << 4 * 8),
BitBoard(0xFF << 5 * 8),
BitBoard(0xFF << 6 * 8),
BitBoard(0xFF << 7 * 8),
];
pub(super) const FILES: [BitBoard; 8] = [
BitBoard(0x0101010101010101 << 0),
BitBoard(0x0101010101010101 << 1),
BitBoard(0x0101010101010101 << 2),
BitBoard(0x0101010101010101 << 3),
BitBoard(0x0101010101010101 << 4),
BitBoard(0x0101010101010101 << 5),
BitBoard(0x0101010101010101 << 6),
BitBoard(0x0101010101010101 << 7),
];
pub(super) const LIGHT_SQUARES: BitBoard =
BitBoard(0x5555 | 0x5555 << 16 | 0x5555 << 32 | 0x5555 << 48);
pub(super) const DARK_SQUARES: BitBoard = BitBoard(!LIGHT_SQUARES.0);
pub(super) fn library() -> &'static MoveLibrary {
static MOVE_LIBRARY_INIT: Once = Once::new();
static mut MOVE_LIBRARY: MoveLibrary = MoveLibrary::new();
unsafe {
MOVE_LIBRARY_INIT.call_once(|| {
MOVE_LIBRARY.init();
});
&MOVE_LIBRARY
}
}
macro_rules! library_getter {
($name:ident) => {
pub(super) fn $name(&self, sq: Square) -> BitBoard {
self.$name[sq as usize]
}
};
}
#[derive(Debug)]
pub(super) struct MoveLibrary {
// Rays
rays: [[BitBoard; 8]; Square::NUM],
// Piecewise move tables
knight_moves: [BitBoard; 64],
bishop_moves: [BitBoard; 64],
rook_moves: [BitBoard; 64],
queen_moves: [BitBoard; 64],
king_moves: [BitBoard; 64],
}
impl MoveLibrary {
const fn new() -> MoveLibrary {
MoveLibrary {
rays: [[BitBoard::empty(); 8]; Square::NUM],
knight_moves: [BitBoard::empty(); 64],
bishop_moves: [BitBoard::empty(); 64],
rook_moves: [BitBoard::empty(); 64],
queen_moves: [BitBoard::empty(); 64],
king_moves: [BitBoard::empty(); 64],
}
}
fn init(&mut self) {
for sq in Square::ALL {
self.init_orthogonal_rays(sq);
self.init_diagonal_rays(sq);
self.init_knight_moves(sq as usize);
self.init_bishop_moves(sq);
self.init_rook_moves(sq);
self.init_queen_moves(sq);
self.init_king_moves(sq as usize);
}
}
fn init_orthogonal_rays(&mut self, sq: Square) {
let sq_bb: BitBoard = sq.into();
let rays = &mut self.rays[sq as usize];
rays[Direction::North as usize] = Self::generate_ray(sq_bb, BitBoard::shift_north_one);
rays[Direction::South as usize] = Self::generate_ray(sq_bb, BitBoard::shift_south_one);
rays[Direction::East as usize] = Self::generate_ray(sq_bb, BitBoard::shift_east_one);
rays[Direction::West as usize] = Self::generate_ray(sq_bb, BitBoard::shift_west_one);
}
fn init_diagonal_rays(&mut self, sq: Square) {
let sq_bb: BitBoard = sq.into();
let rays = &mut self.rays[sq as usize];
rays[Direction::NorthEast as usize] =
Self::generate_ray(sq_bb, BitBoard::shift_north_east_one);
rays[Direction::NorthWest as usize] =
Self::generate_ray(sq_bb, BitBoard::shift_north_west_one);
rays[Direction::SouthWest as usize] =
Self::generate_ray(sq_bb, BitBoard::shift_south_west_one);
rays[Direction::SouthEast as usize] =
Self::generate_ray(sq_bb, BitBoard::shift_south_east_one);
}
fn init_king_moves(&mut self, idx: usize) {
let king = BitBoard::new(1 << idx);
let mut attacks = king.shift_east_one() | king.shift_west_one();
let king = king | attacks;
attacks |= king.shift_north_one() | king.shift_south_one();
self.king_moves[idx] = attacks;
}
/// Calculate bitboards representing knight moves from each square on the
/// board. The algorithm is described on the [Chess Programming Wiki][cpw].
///
/// [cpw]: https://www.chessprogramming.org/Knight_Pattern
fn init_knight_moves(&mut self, idx: usize) {
let knight = BitBoard::new(1 << idx);
let east = knight.shift_east_one();
let west = knight.shift_west_one();
let mut attacks = (east | west).shift_north(2);
attacks |= (east | west).shift_south(2);
let east = east.shift_east_one();
let west = west.shift_west_one();
attacks |= (east | west).shift_north_one();
attacks |= (east | west).shift_south_one();
self.knight_moves[idx] = attacks;
}
fn init_bishop_moves(&mut self, sq: Square) {
let rays = self.rays[sq as usize];
self.bishop_moves[sq as usize] = rays[Direction::NorthWest as usize]
| rays[Direction::NorthEast as usize]
| rays[Direction::SouthEast as usize]
| rays[Direction::SouthWest as usize];
}
fn init_rook_moves(&mut self, sq: Square) {
let rays = self.rays[sq as usize];
self.rook_moves[sq as usize] = rays[Direction::North as usize]
| rays[Direction::East as usize]
| rays[Direction::South as usize]
| rays[Direction::West as usize];
}
fn init_queen_moves(&mut self, sq: Square) {
let rook_moves = self.rook_moves[sq as usize];
let bishop_moves = self.bishop_moves[sq as usize];
self.queen_moves[sq as usize] = rook_moves | bishop_moves;
}
#[inline]
fn generate_ray(sq: BitBoard, shift: fn(BitBoard) -> BitBoard) -> BitBoard {
let mut ray = BitBoard::empty();
let mut iter = shift(sq);
while !iter.is_empty() {
ray |= iter;
iter = shift(iter);
}
ray
}
pub(super) fn ray(&self, sq: Square, dir: Direction) -> BitBoard {
self.rays[sq as usize][dir as usize]
}
library_getter!(knight_moves);
library_getter!(bishop_moves);
library_getter!(rook_moves);
library_getter!(queen_moves);
library_getter!(king_moves);
}

View file

@ -0,0 +1,7 @@
mod bit_scanner;
mod bitboard;
mod library;
mod shifts;
pub(crate) use self::bit_scanner::{LeadingBitScanner, TrailingBitScanner};
pub(crate) use self::bitboard::BitBoard;

View file

@ -3,62 +3,62 @@
use super::BitBoard;
impl BitBoard {
const NOT_A_FILE: u64 = 0xfefe_fefe_fefe_fefe;
const NOT_H_FILE: u64 = 0x7f7f_7f7f_7f7f_7f7f;
const NOT_A_FILE: u64 = 0xfefefefefefefefe;
const NOT_H_FILE: u64 = 0x7f7f7f7f7f7f7f7f;
#[inline]
pub fn shift_north(&self, n: u8) -> BitBoard {
pub fn shift_north(self, n: u8) -> BitBoard {
BitBoard(self.0 << (8 * n))
}
#[inline]
pub fn shift_north_one(&self) -> BitBoard {
pub fn shift_north_one(self) -> BitBoard {
BitBoard(self.0 << 8)
}
#[inline]
pub fn shift_north_east_one(&self) -> BitBoard {
pub fn shift_north_east_one(self) -> BitBoard {
BitBoard(self.0 << 9 & BitBoard::NOT_A_FILE)
}
#[inline]
pub fn shift_east(&self, n: u8) -> BitBoard {
pub fn shift_east(self, n: u8) -> BitBoard {
// TODO: Implement a bounds check here.
BitBoard(self.0 << n)
}
#[inline]
pub fn shift_east_one(&self) -> BitBoard {
pub fn shift_east_one(self) -> BitBoard {
BitBoard(self.0 << 1 & BitBoard::NOT_A_FILE)
}
#[inline]
pub fn shift_south_east_one(&self) -> BitBoard {
pub fn shift_south_east_one(self) -> BitBoard {
BitBoard(self.0 >> 7 & BitBoard::NOT_A_FILE)
}
#[inline]
pub fn shift_south(&self, n: u8) -> BitBoard {
pub fn shift_south(self, n: u8) -> BitBoard {
BitBoard(self.0 >> (8 * n))
}
#[inline]
pub fn shift_south_one(&self) -> BitBoard {
pub fn shift_south_one(self) -> BitBoard {
BitBoard(self.0 >> 8)
}
#[inline]
pub fn shift_south_west_one(&self) -> BitBoard {
pub fn shift_south_west_one(self) -> BitBoard {
BitBoard(self.0 >> 9 & BitBoard::NOT_H_FILE)
}
#[inline]
pub fn shift_west_one(&self) -> BitBoard {
pub fn shift_west_one(self) -> BitBoard {
BitBoard(self.0 >> 1 & BitBoard::NOT_H_FILE)
}
#[inline]
pub fn shift_north_west_one(&self) -> BitBoard {
pub fn shift_north_west_one(self) -> BitBoard {
BitBoard(self.0 << 7 & BitBoard::NOT_H_FILE)
}
}

View file

@ -1,392 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{
CastleRights, PieceSet,
display::DiagramFormatter,
piece_sets::{Counter, PlacePieceError, PlacePieceStrategy},
zobrist::{ZobristHash, ZobristState},
};
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Shape, Square};
use std::sync::Arc;
pub type HalfMoveClock = u32;
pub type FullMoveClock = u32;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Board {
active_color: Color,
pieces: PieceSet,
castling_rights: CastleRights,
en_passant_target: Option<Square>,
pub half_move_clock: HalfMoveClock,
pub full_move_number: FullMoveClock,
zobrist_hash: Option<ZobristHash>,
}
impl Board {
/// An empty board
#[must_use]
pub fn empty(zobrist: Option<Arc<ZobristState>>) -> Self {
let mut board = Self {
zobrist_hash: zobrist.map(ZobristHash::new),
..Default::default()
};
board.recompute_zobrist_hash();
board
}
/// The starting position
#[must_use]
pub fn starting(zobrist: Option<Arc<ZobristState>>) -> Self {
const BLACK_PIECES: [BitBoard; Shape::NUM] = [
BitBoard::new(0b0000_0000_1111_1111 << 48),
BitBoard::new(0b0100_0010_0000_0000 << 48),
BitBoard::new(0b0010_0100_0000_0000 << 48),
BitBoard::new(0b1000_0001_0000_0000 << 48),
BitBoard::new(0b0000_1000_0000_0000 << 48),
BitBoard::new(0b0001_0000_0000_0000 << 48),
];
const WHITE_PIECES: [BitBoard; Shape::NUM] = [
BitBoard::new(0b1111_1111_0000_0000),
BitBoard::new(0b0000_0000_0100_0010),
BitBoard::new(0b0000_0000_0010_0100),
BitBoard::new(0b0000_0000_1000_0001),
BitBoard::new(0b0000_0000_0000_1000),
BitBoard::new(0b0000_0000_0001_0000),
];
let mut board = Self {
pieces: PieceSet::new([WHITE_PIECES, BLACK_PIECES]),
zobrist_hash: zobrist.map(ZobristHash::new),
..Default::default()
};
board.recompute_zobrist_hash();
board
}
}
impl Board {
#[must_use]
pub fn active_color(&self) -> Color {
self.active_color
}
pub fn set_active_color(&mut self, color: Color) {
if color == self.active_color {
return;
}
self.active_color = color;
if let Some(zobrist) = self.zobrist_hash.as_mut() {
zobrist.update_setting_active_color(color);
}
}
}
impl Board {
#[must_use]
pub fn castling_rights(&self) -> &CastleRights {
&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) {
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) {
let new_rights = self.castling_rights;
if old_rights == new_rights {
return;
}
if let Some(zobrist) = self.zobrist_hash.as_mut() {
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 {
/// Returns a copy of the current en passant square, if one exists.
#[must_use]
pub fn en_passant_target(&self) -> Option<Square> {
self.en_passant_target
}
pub fn set_en_passant_target(&mut self, square: Square) {
self.set_en_passant_target_option(Some(square));
}
pub fn set_en_passant_target_option(&mut self, square: Option<Square>) {
let old_target = self.en_passant_target;
self.en_passant_target = square;
self.update_zobrist_hash_en_passant_target(old_target);
}
pub fn clear_en_passant_target(&mut self) {
let old_target = self.en_passant_target;
self.en_passant_target = None;
self.update_zobrist_hash_en_passant_target(old_target);
}
fn update_zobrist_hash_en_passant_target(&mut self, old_target: Option<Square>) {
let new_target = self.en_passant_target;
if old_target == new_target {
return;
}
if let Some(zobrist) = self.zobrist_hash.as_mut() {
zobrist.update_setting_en_passant_target(old_target, new_target);
}
}
}
impl Board {
#[must_use]
pub fn get_piece(&self, square: Square) -> Option<Piece> {
self.pieces.get(square)
}
pub fn find_pieces(&self, piece: Piece) -> BitBoard {
self.pieces.find_pieces(piece)
}
/// Place a piece on the board.
///
/// ## Errors
///
/// When is called with [`PlacePieceStrategy::PreserveExisting`], and a
/// piece already exists on `square`, this method returns a
/// [`PlacePieceError::ExistingPiece`] error.
///
pub fn place_piece(
&mut self,
piece: Piece,
square: Square,
strategy: PlacePieceStrategy,
) -> Result<Option<Piece>, PlacePieceError> {
let place_result = self.pieces.place(piece, square, strategy);
if let Ok(Some(existing_piece)) = place_result.as_ref() {
if let Some(zobrist) = self.zobrist_hash.as_mut() {
zobrist.update_removing_piece(square, *existing_piece);
zobrist.update_adding_piece(square, piece);
}
}
place_result
}
pub fn remove_piece(&mut self, square: Square) -> Option<Piece> {
let removed_piece = self.pieces.remove(square);
if let Some(piece) = removed_piece {
if let Some(zobrist) = self.zobrist_hash.as_mut() {
zobrist.update_removing_piece(square, piece);
}
}
removed_piece
}
#[must_use]
pub fn count_piece(&self, piece: &Piece) -> Counter {
self.pieces.count(piece)
}
}
impl Board {
pub fn iter(&self) -> impl Iterator<Item = (Square, Piece)> {
self.pieces.iter()
}
/// A [`BitBoard`] of squares occupied by pieces of all colors.
pub fn occupancy(&self) -> BitBoard {
self.pieces.occpuancy()
}
/// A [`BitBoard`] of squares that are vacant.
pub fn vacancy(&self) -> BitBoard {
!self.occupancy()
}
pub fn friendly_occupancy(&self, color: Color) -> BitBoard {
self.pieces.friendly_occupancy(color)
}
pub fn opposing_occupancy(&self, color: Color) -> BitBoard {
self.pieces.opposing_occupancy(color)
}
pub fn enemies(&self, color: Color) -> BitBoard {
self.pieces.opposing_occupancy(color)
}
/// Return a [`BitBoard`] of all pawns of a given color.
pub fn pawns(&self, color: Color) -> BitBoard {
self.pieces.find_pieces(Piece::pawn(color))
}
pub fn knights(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::knight(color))
}
pub fn bishops(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::bishop(color))
}
pub fn rooks(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::rook(color))
}
pub fn queens(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::queen(color))
}
pub fn kings(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::king(color))
}
}
impl Board {
pub fn zobrist_hash(&self) -> Option<u64> {
self.zobrist_hash.as_ref().map(ZobristHash::hash_value)
}
pub fn recompute_zobrist_hash(&mut self) {
// Avoid overlapping borrows when borrowing zobrist_hash.as_mut() and
// then also borrowing self to update the board hash by computing the
// hash with the static function first, and then setting the hash value
// on the zobrist instance. Unfortuantely this requires unwrapping
// self.zobrist_hash twice. C'est la vie.
let new_hash = self.zobrist_hash.as_ref().map(|zobrist| {
let state = zobrist.state();
ZobristHash::compute_board_hash(self, state.as_ref())
});
if let (Some(new_hash), Some(zobrist)) = (new_hash, self.zobrist_hash.as_mut()) {
zobrist.set_hash_value(new_hash);
}
}
pub fn zobrist_state(&self) -> Option<Arc<ZobristState>> {
self.zobrist_hash.as_ref().map(ZobristHash::state)
}
pub fn set_zobrist_state(&mut self, state: Arc<ZobristState>) {
self.zobrist_hash = Some(ZobristHash::new(state));
self.recompute_zobrist_hash();
}
}
impl Board {
pub fn display(&self) -> DiagramFormatter<'_> {
DiagramFormatter::new(self)
}
}
impl Board {
#[must_use]
pub fn unwrap_color(&self, color: Option<Color>) -> Color {
color.unwrap_or(self.active_color)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_board;
use chessfriend_core::{piece, random::RandomNumberGenerator};
#[test]
fn get_piece_on_square() {
let board = test_board![
Black Bishop on F7,
];
assert_eq!(board.get_piece(Square::F7), Some(piece!(Black Bishop)));
}
// MARK: - Zobrist Hashing
fn test_state() -> ZobristState {
let mut rng = RandomNumberGenerator::default();
ZobristState::new(&mut rng)
}
#[test]
fn zobrist_hash_set_for_empty_board() {
let state = Arc::new(test_state());
let board = Board::empty(Some(state.clone()));
let hash = board.zobrist_hash();
assert_eq!(hash, Some(0));
}
#[test]
fn zobrist_hash_set_for_starting_position_board() {
let state = Arc::new(test_state());
let board = Board::starting(Some(state.clone()));
let hash = board.zobrist_hash();
assert!(hash.is_some());
}
#[test]
fn zobrist_hash_updated_when_changing_active_color() {
let state = Arc::new(test_state());
let mut board = Board::empty(Some(state.clone()));
board.set_active_color(Color::Black);
// Just verify that the value is real and has changed. The actual value
// computation is covered by the tests in zobrist.rs.
let hash = board.zobrist_hash();
assert!(hash.is_some());
assert_ne!(hash, Some(0));
}
#[test]
fn zobrist_hash_updated_when_changing_en_passant_target() {
let state = Arc::new(test_state());
let mut board = Board::empty(Some(state.clone()));
board.set_en_passant_target(Square::C3);
// Just verify that the value is real and has changed. The actual value
// computation is covered by the tests in zobrist.rs.
let hash = board.zobrist_hash();
assert!(hash.is_some());
assert_ne!(hash, Some(0));
}
}

View file

@ -1,18 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::Board;
pub trait BoardProvider {
fn board(&self) -> &Board;
fn board_mut(&mut self) -> &mut Board;
}
impl BoardProvider for Board {
fn board(&self) -> &Board {
self
}
fn board_mut(&mut self) -> &mut Board {
self
}
}

View file

@ -1,286 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
mod parameters;
mod rights;
pub use parameters::Parameters;
pub use rights::{CastleRightsOption, Rights};
use crate::{Board, CastleParameters};
use chessfriend_core::{Color, Wing};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum CastleEvaluationError {
#[error("{color} does not have the right to castle {wing}")]
NoRights { color: Color, wing: Wing },
#[error("no king")]
NoKing,
#[error("no rook")]
NoRook,
#[error("castling path is not clear")]
ObstructingPieces,
#[error("opposing pieces check castling path")]
CheckingPieces,
}
impl Board {
#[must_use]
pub fn castling_parameters(wing: Wing, color: Color) -> &'static CastleParameters {
&CastleParameters::BY_COLOR[color as usize][wing as usize]
}
/// Evaluates whether the active color can castle toward the given wing of the board in the
/// current position.
///
/// ## Errors
///
/// Returns an error indicating why the active color cannot castle.
pub fn color_can_castle(
&self,
wing: Wing,
color: Option<Color>,
) -> Result<&'static CastleParameters, CastleEvaluationError> {
// TODO: Cache this result. It's expensive!
// TODO: Does this actually need to rely on internal state, i.e. active_color?
let color = self.unwrap_color(color);
if !self.has_castling_right_unwrapped(color, wing) {
return Err(CastleEvaluationError::NoRights { color, wing });
}
let parameters = Self::castling_parameters(wing, color);
if self.castling_king(parameters.origin.king).is_none() {
return Err(CastleEvaluationError::NoKing);
}
if self.castling_rook(parameters.origin.rook).is_none() {
return Err(CastleEvaluationError::NoRook);
}
// All squares must be clear.
let has_obstructing_pieces = (self.occupancy() & parameters.clear).is_populated();
if has_obstructing_pieces {
return Err(CastleEvaluationError::ObstructingPieces);
}
// King cannot pass through check.
let opposing_sight = self.opposing_sight(color);
let opposing_pieces_can_see_castling_path =
(parameters.check & opposing_sight).is_populated();
if opposing_pieces_can_see_castling_path {
return Err(CastleEvaluationError::CheckingPieces);
}
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)
}
#[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);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_board;
use chessfriend_core::{Color, Wing, piece};
#[test]
fn king_on_starting_square_can_castle() {
let board = test_board!(
White King on E1,
White Rook on A1,
White Rook on H1
);
assert!(board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
assert!(board.has_castling_right_unwrapped(Color::White, Wing::QueenSide));
}
#[test]
fn king_for_castle() {
let pos = test_board![
White King on E1,
White Rook on H1,
White Rook on A1,
];
let kingside_parameters = Board::castling_parameters(Wing::KingSide, Color::White);
assert_eq!(
pos.castling_king(kingside_parameters.origin.king),
Some(piece!(White King))
);
let queenside_parameters = Board::castling_parameters(Wing::QueenSide, Color::White);
assert_eq!(
pos.castling_king(queenside_parameters.origin.king),
Some(piece!(White King))
);
}
#[test]
fn rook_for_castle() {
let pos = test_board![
White King on E1,
White Rook on H1,
];
let kingside_parameters = Board::castling_parameters(Wing::KingSide, Color::White);
assert_eq!(
pos.castling_rook(kingside_parameters.origin.rook),
Some(piece!(White Rook))
);
let pos = test_board![
White King on E1,
White Rook on A1,
];
let queenside_parameters = Board::castling_parameters(Wing::QueenSide, Color::White);
assert_eq!(
pos.castling_rook(queenside_parameters.origin.rook),
Some(piece!(White Rook))
);
}
#[test]
fn white_can_castle() -> Result<(), CastleEvaluationError> {
let pos = test_board![
White King on E1,
White Rook on H1,
White Rook on A1,
];
pos.color_can_castle(Wing::KingSide, None)?;
pos.color_can_castle(Wing::QueenSide, None)?;
Ok(())
}
#[test]
fn white_cannot_castle_missing_king() {
let pos = test_board![
White King on E2,
White Rook on H1,
White Rook on A1,
];
assert_eq!(
pos.color_can_castle(Wing::KingSide, None),
Err(CastleEvaluationError::NoKing)
);
assert_eq!(
pos.color_can_castle(Wing::QueenSide, None),
Err(CastleEvaluationError::NoKing)
);
}
#[test]
fn white_cannot_castle_missing_rook() {
let pos = test_board![
White King on E1,
White Rook on A1,
];
assert_eq!(
pos.color_can_castle(Wing::KingSide, None),
Err(CastleEvaluationError::NoRook)
);
let pos = test_board![
White King on E1,
White Rook on H1,
];
assert_eq!(
pos.color_can_castle(Wing::QueenSide, None),
Err(CastleEvaluationError::NoRook)
);
}
#[test]
fn white_cannot_castle_obstructing_piece() {
let pos = test_board![
White King on E1,
White Bishop on F1,
White Rook on H1,
White Rook on A1,
];
assert_eq!(
pos.color_can_castle(Wing::KingSide, None),
Err(CastleEvaluationError::ObstructingPieces)
);
assert!(pos.color_can_castle(Wing::QueenSide, None).is_ok());
}
#[test]
fn white_cannot_castle_checking_pieces() {
let pos = test_board![
White King on E1,
White Rook on H1,
White Rook on A1,
Black Queen on C6,
];
assert!(pos.color_can_castle(Wing::KingSide, None).is_ok());
assert_eq!(
pos.color_can_castle(Wing::QueenSide, None),
Err(CastleEvaluationError::CheckingPieces)
);
}
}

View file

@ -1,88 +0,0 @@
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Square, Wing};
#[derive(Debug, Eq, PartialEq)]
pub struct Parameters {
/// Origin squares of the king and rook.
pub origin: Squares,
/// Target or destination squares for the king and rook.
pub target: Squares,
/// The set of squares that must be clear of any pieces in order to perform
/// this castle.
pub clear: BitBoard,
/// The set of squares that must not be attacked (i.e. visible to opposing
/// pieces) in order to perform this castle.
pub check: BitBoard,
}
#[derive(Debug, Eq, PartialEq)]
pub struct Squares {
pub king: Square,
pub rook: Square,
}
impl Parameters {
/// Parameters for each castling move, organized by color and board-side.
pub(crate) const BY_COLOR: [[Self; Wing::NUM]; Color::NUM] = [
[
Parameters {
origin: Squares {
king: Square::E1,
rook: Square::H1,
},
target: Squares {
king: Square::G1,
rook: Square::F1,
},
clear: BitBoard::new(0b0110_0000),
check: BitBoard::new(0b0111_0000),
},
Parameters {
origin: Squares {
king: Square::E1,
rook: Square::A1,
},
target: Squares {
king: Square::C1,
rook: Square::D1,
},
clear: BitBoard::new(0b0000_1110),
check: BitBoard::new(0b0001_1100),
},
],
[
Parameters {
origin: Squares {
king: Square::E8,
rook: Square::H8,
},
target: Squares {
king: Square::G8,
rook: Square::F8,
},
clear: BitBoard::new(0b0110_0000 << (8 * 7)),
check: BitBoard::new(0b0111_0000 << (8 * 7)),
},
Parameters {
origin: Squares {
king: Square::E8,
rook: Square::A8,
},
target: Squares {
king: Square::C8,
rook: Square::D8,
},
clear: BitBoard::new(0b0000_1110 << (8 * 7)),
check: BitBoard::new(0b0001_1100 << (8 * 7)),
},
],
];
#[must_use]
pub const fn get(color: Color, wing: Wing) -> &'static Parameters {
&Self::BY_COLOR[color as usize][wing as usize]
}
}

View file

@ -1,116 +0,0 @@
// 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);
impl Rights {
/// Returns `true` if the player has the right to castle on the given side
/// of the board.
///
/// A player retains the right to castle on a particular side of the board
/// 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 grant(&mut self, color: Color, option: CastleRightsOption) {
self.0 |= Self::flags(color, option);
}
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.
pub fn revoke_all(&mut self) {
self.0 = 0;
}
}
impl Rights {
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)
}
}
impl fmt::Debug for Rights {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Flags({:08b})", self.0)
}
}
impl Default for Rights {
fn default() -> Self {
Self(0b0000_1111)
}
}
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);
}
#[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()));
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.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

@ -1,46 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::Board;
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();
let king = self.king_bitboard(color);
let opposing_sight = self.opposing_sight(color);
(king & opposing_sight).is_populated()
}
fn king_bitboard(&self, color: Color) -> BitBoard {
self.find_pieces(Piece::king(color))
}
}
#[cfg(test)]
mod tests {
use crate::test_board;
use chessfriend_core::Color;
#[test]
fn active_color_is_in_check() {
let board = test_board!(
White King on A3,
Black Rook on F3,
);
assert!(board.is_in_check());
}
#[test]
fn active_color_is_not_in_check() {
let board = test_board!(
White King on A3,
Black Rook on B4,
);
assert!(!board.is_in_check());
}
}

View file

@ -1,119 +1,9 @@
// Eryn Wells <eryn@erynwells.me>
use crate::Board;
use chessfriend_bitboard::BitBoard;
use chessfriend_core::{File, Rank, Square};
use std::fmt;
#[derive(Default)]
pub enum PieceDisplayStyle {
#[default]
Unicode,
ASCII,
pub trait ASCIIDisplay {
fn fmt(&self, &mut f: fmt::Formatter) -> fmt::Result;
}
#[must_use]
pub struct DiagramFormatter<'a> {
board: &'a Board,
piece_style: PieceDisplayStyle,
marked_squares: BitBoard,
highlighted_squares: BitBoard,
}
impl<'a> DiagramFormatter<'a> {
pub fn new(board: &'a Board) -> Self {
Self {
board,
piece_style: PieceDisplayStyle::default(),
marked_squares: BitBoard::default(),
highlighted_squares: BitBoard::default(),
}
}
pub fn mark(mut self, bitboard: BitBoard) -> Self {
self.marked_squares = bitboard;
self
}
pub fn highlight(mut self, bitboard: BitBoard) -> Self {
self.highlighted_squares = bitboard;
self
}
}
impl fmt::Display for DiagramFormatter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, " ╔═════════════════╗")?;
for rank in Rank::ALL.into_iter().rev() {
write!(f, "{rank} ║ ")?;
for file in File::ALL {
let square = Square::from_file_rank(file, rank);
let should_highlight = self.highlighted_squares.contains(square);
if should_highlight {
write!(f, "\x1b[7m")?;
}
if let Some(piece) = self.board.get_piece(square) {
let ch = match self.piece_style {
PieceDisplayStyle::Unicode => piece.to_unicode(),
PieceDisplayStyle::ASCII => piece.to_ascii(),
};
write!(f, "{ch}")?;
} else {
let ch = if self.marked_squares.contains(square) {
'*'
} else {
'·'
};
write!(f, "{ch}")?;
}
if should_highlight {
write!(f, "\x1b[0m")?;
}
write!(f, " ")?;
}
writeln!(f, "")?;
}
writeln!(f, " ╚═════════════════╝")?;
write!(f, " a b c d e f g h")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{test_board, Board};
#[test]
#[ignore]
fn empty() {
let board = test_board!(empty);
let diagram = DiagramFormatter::new(&board);
println!("{diagram}");
}
#[test]
#[ignore]
fn one_king() {
let board = test_board![Black King on H3];
let diagram = DiagramFormatter::new(&board);
println!("{diagram}");
}
#[test]
#[ignore]
fn starting() {
let board = test_board!(starting);
let diagram = DiagramFormatter::new(&board);
println!("{diagram}");
}
pub trait UnicodeDisplay {
fn fmt(&self, &mut f: fmt::Formatter) -> fmt::Result;
}

View file

@ -1,48 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_core::{Rank, Square};
/// En passant information.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EnPassant {
target: Square,
capture: Square,
}
impl EnPassant {
fn _capture_square(target: Square) -> Option<Square> {
let (file, rank) = target.file_rank();
match rank {
Rank::THREE => Some(Square::from_file_rank(file, Rank::FOUR)),
Rank::SIX => Some(Square::from_file_rank(file, Rank::FIVE)),
_ => None,
}
}
/// Return en passant information for a particular target square. The target
/// square is the square a pawn capturing en passant will move to.
///
/// Return `None` if the square is not eligible for en passant.
///
/// ## Examples
///
/// ```
/// use chessfriend_board::en_passant::EnPassant;
/// use chessfriend_core::Square;
/// assert!(EnPassant::from_target_square(Square::E3).is_some());
/// assert!(EnPassant::from_target_square(Square::B4).is_none());
/// ```
pub fn from_target_square(target: Square) -> Option<Self> {
Self::_capture_square(target).map(|capture| Self { target, capture })
}
/// The square the capturing piece will move to.
pub fn target_square(self) -> Square {
self.target
}
/// The square on which the captured pawn sits.
pub fn capture_square(self) -> Square {
self.capture
}
}

View file

@ -1,348 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{Board, piece_sets::PlacePieceStrategy};
use chessfriend_core::{
Color, File, Piece, Rank, Square, Wing, coordinates::ParseSquareError, piece,
};
use std::fmt::Write;
use thiserror::Error;
#[macro_export]
macro_rules! fen {
($fen_string:literal) => {{
use $crate::fen::FromFenStr;
$crate::Board::from_fen_str($fen_string)
}};
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum ToFenStrError {
#[error("{0}")]
FmtError(#[from] std::fmt::Error),
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
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),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Field {
Placements,
PlayerToMove,
CastlingRights,
EnPassantSquare,
HalfMoveClock,
FullMoveCounter,
}
impl std::fmt::Display for Field {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Field::Placements => write!(f, "Placements"),
Field::PlayerToMove => write!(f, "Player To Move"),
Field::CastlingRights => write!(f, "Castling Rights"),
Field::EnPassantSquare => write!(f, "En Passant Square"),
Field::HalfMoveClock => write!(f, "Half Move Clock"),
Field::FullMoveCounter => write!(f, "Full move Counter"),
}
}
}
pub trait ToFenStr {
type Error;
/// Create a FEN string from `Self`.
///
/// # Errors
///
///
fn to_fen_str(&self) -> Result<String, Self::Error>;
}
pub trait FromFenStr: Sized {
type Error;
/// Create a `Self` from a FEN string.
///
/// # Errors
///
///
fn from_fen_str(string: &str) -> Result<Self, Self::Error>;
}
impl ToFenStr for Board {
type Error = ToFenStrError;
fn to_fen_str(&self) -> Result<String, Self::Error> {
let mut fen_string = String::new();
let mut empty_squares: u8 = 0;
for rank in Rank::ALL.into_iter().rev() {
for file in File::ALL {
let square = Square::from_file_rank(file, rank);
match self.get_piece(square) {
Some(piece) => {
if empty_squares > 0 {
write!(fen_string, "{empty_squares}")
.map_err(ToFenStrError::FmtError)?;
empty_squares = 0;
}
write!(fen_string, "{}", piece.to_fen_str()?)
.map_err(ToFenStrError::FmtError)?;
}
None => empty_squares += 1,
}
}
if empty_squares > 0 {
write!(fen_string, "{empty_squares}").map_err(ToFenStrError::FmtError)?;
empty_squares = 0;
}
if rank != Rank::ONE {
write!(fen_string, "/").map_err(ToFenStrError::FmtError)?;
}
}
write!(fen_string, " {}", self.active_color().to_fen_str()?)
.map_err(ToFenStrError::FmtError)?;
let castling = [
(Color::White, Wing::KingSide),
(Color::White, Wing::QueenSide),
(Color::Black, Wing::KingSide),
(Color::Black, Wing::QueenSide),
]
.map(|(color, wing)| {
if !self.has_castling_right_unwrapped(color, wing) {
return "";
}
match (color, wing) {
(Color::White, Wing::KingSide) => "K",
(Color::White, Wing::QueenSide) => "Q",
(Color::Black, Wing::KingSide) => "k",
(Color::Black, Wing::QueenSide) => "q",
}
})
.concat();
write!(
fen_string,
" {}",
if castling.is_empty() { "-" } else { &castling }
)
.map_err(ToFenStrError::FmtError)?;
write!(
fen_string,
" {}",
self.en_passant_target()
.map_or("-".to_string(), |square| square.to_string())
)
.map_err(ToFenStrError::FmtError)?;
write!(fen_string, " {}", self.half_move_clock).map_err(ToFenStrError::FmtError)?;
write!(fen_string, " {}", self.full_move_number).map_err(ToFenStrError::FmtError)?;
Ok(fen_string)
}
}
impl ToFenStr for Color {
type Error = ToFenStrError;
fn to_fen_str(&self) -> Result<String, Self::Error> {
match self {
Color::White => Ok("w".to_string()),
Color::Black => Ok("b".to_string()),
}
}
}
impl ToFenStr for Piece {
type Error = ToFenStrError;
fn to_fen_str(&self) -> Result<String, Self::Error> {
let ascii: char = self.to_ascii();
Ok(String::from(match self.color {
Color::White => ascii.to_ascii_uppercase(),
Color::Black => ascii.to_ascii_lowercase(),
}))
}
}
impl FromFenStr for Board {
type Error = FromFenStrError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
let mut board = Board::empty(None);
let mut fields = string.split(' ');
let placements = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::Placements))?;
let ranks = placements.split('/');
for (rank, pieces) in Rank::ALL.iter().rev().zip(ranks) {
let mut files = File::ALL.iter();
for ch in pieces.chars() {
if let Some(skip) = ch.to_digit(10) {
// TODO: Use advance_by() when it's available.
for _ in 0..skip {
files.next();
}
continue;
}
let file = files.next().ok_or(FromFenStrError::MissingPlacement)?;
let piece = Piece::from_fen_str(&ch.to_string())?;
let _ = board.place_piece(
piece,
Square::from_file_rank(*file, *rank),
PlacePieceStrategy::default(),
);
}
debug_assert_eq!(files.next(), None);
}
let active_color = Color::from_fen_str(
fields
.next()
.ok_or(FromFenStrError::MissingField(Field::PlayerToMove))?,
)?;
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 != "-" {
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());
}
}
let en_passant_square = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::EnPassantSquare))?;
if en_passant_square != "-" {
let square = Square::from_algebraic_str(en_passant_square)
.map_err(FromFenStrError::ParseSquareError)?;
board.set_en_passant_target(square);
}
let half_move_clock = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::HalfMoveClock))?;
let half_move_clock: u32 = half_move_clock
.parse()
.map_err(FromFenStrError::ParseIntError)?;
board.half_move_clock = half_move_clock;
let full_move_counter = fields
.next()
.ok_or(FromFenStrError::MissingField(Field::FullMoveCounter))?;
let full_move_counter: u32 = full_move_counter
.parse()
.map_err(FromFenStrError::ParseIntError)?;
board.full_move_number = full_move_counter;
debug_assert_eq!(fields.next(), None);
Ok(board)
}
}
impl FromFenStr for Color {
type Error = FromFenStrError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
if string.len() != 1 {
return Err(FromFenStrError::InvalidValue);
}
match string.chars().take(1).next().unwrap() {
'w' => Ok(Color::White),
'b' => Ok(Color::Black),
_ => Err(FromFenStrError::InvalidValue),
}
}
}
impl FromFenStr for Piece {
type Error = FromFenStrError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
if string.len() != 1 {
return Err(FromFenStrError::InvalidValue);
}
match string.chars().take(1).next().unwrap() {
'P' => Ok(piece!(White Pawn)),
'N' => Ok(piece!(White Knight)),
'B' => Ok(piece!(White Bishop)),
'R' => Ok(piece!(White Rook)),
'Q' => Ok(piece!(White Queen)),
'K' => Ok(piece!(White King)),
'p' => Ok(piece!(Black Pawn)),
'n' => Ok(piece!(Black Knight)),
'b' => Ok(piece!(Black Bishop)),
'r' => Ok(piece!(Black Rook)),
'q' => Ok(piece!(Black Queen)),
'k' => Ok(piece!(Black King)),
_ => Err(FromFenStrError::InvalidValue),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_board;
#[test]
fn starting_position() {
let pos = test_board!(starting);
assert_eq!(
pos.to_fen_str().unwrap(),
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0"
);
}
#[test]
fn from_starting_fen() {
let board = fen!("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0").unwrap();
let expected = Board::starting(None);
assert_eq!(board, expected, "{board:#?}\n{expected:#?}");
}
}

View file

@ -1,24 +1,15 @@
// Eryn Wells <eryn@erynwells.me>
pub mod board;
pub mod castle;
pub mod display;
pub mod en_passant;
pub mod fen;
pub mod macros;
pub mod movement;
pub mod sight;
pub mod zobrist;
mod bitboard;
//mod moves;
#[macro_use]
pub mod piece;
#[macro_use]
pub mod position;
mod square;
mod board_provider;
mod check;
mod piece_sets;
pub use board::Board;
pub use board_provider::BoardProvider;
pub use castle::Parameters as CastleParameters;
pub use castle::Rights as CastleRights;
pub use piece_sets::{PlacePieceError, PlacePieceStrategy};
pub use zobrist::ZobristState;
use piece_sets::PieceSet;
pub(crate) use bitboard::BitBoard;
//pub use moves::Move;
pub use piece::Color;
pub use position::Position;
pub use square::{File, Rank, Square};

View file

@ -1,82 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
#[macro_export]
macro_rules! test_board {
($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => {
{
let mut board = $crate::Board::empty(Some($crate::test_zobrist!()));
$(let _ = board.place_piece(
chessfriend_core::Piece::new(
chessfriend_core::Color::$color,
chessfriend_core::Shape::$shape
),
chessfriend_core::Square::$square,
$crate::PlacePieceStrategy::default());
)*
board.set_active_color(chessfriend_core::Color::$to_move);
board.set_en_passant_target(chessfriend_core::Square::$en_passant);
println!("{}", board.display());
board
}
};
($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => {
{
let mut board = $crate::Board::empty(Some($crate::test_zobrist!()));
$(let _ = board.place_piece(
chessfriend_core::Piece::new(
chessfriend_core::Color::$color,
chessfriend_core::Shape::$shape
),
chessfriend_core::Square::$square,
$crate::PlacePieceStrategy::default());
)*
board.set_active_color(chessfriend_core::Color::$to_move);
println!("{}", board.display());
board
}
};
($($color:ident $shape:ident on $square:ident),* $(,)?) => {
{
let mut board = $crate::Board::empty(Some($crate::test_zobrist!()));
$(let _ = board.place_piece(
chessfriend_core::Piece::new(
chessfriend_core::Color::$color,
chessfriend_core::Shape::$shape
),
chessfriend_core::Square::$square,
$crate::PlacePieceStrategy::default());
)*
println!("{}", board.display());
board
}
};
(empty) => {
{
let board = Board::empty(Some($crate::test_zobrist!()));
println!("{}", board.display());
board
}
};
(starting) => {
{
let board = Board::starting(Some($crate::test_zobrist!()));
println!("{}", board.display());
board
}
};
}
#[macro_export]
macro_rules! test_zobrist {
() => {{
let mut rng = chessfriend_core::random::RandomNumberGenerator::default();
let state = $crate::zobrist::ZobristState::new(&mut rng);
std::sync::Arc::new(state)
}};
}

View file

@ -1,154 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
//! Defines routines for computing the movement of a piece. Movement is the set
//! 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 chessfriend_bitboard::BitBoard;
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
impl Board {
pub fn movement_piece(&self, square: Square) -> BitBoard {
if let Some(piece) = self.get_piece(square) {
piece.movement(square, self)
} else {
BitBoard::EMPTY
}
}
}
pub trait Movement {
fn movement(&self, square: Square, board: &Board) -> BitBoard;
}
impl Movement for Piece {
fn movement(&self, square: Square, board: &Board) -> BitBoard {
let color = self.color;
let opposing_occupancy = board.opposing_occupancy(color);
match self.shape {
Shape::Pawn => {
let en_passant_square: BitBoard = board.en_passant_target().into();
// Pawns can only move to squares they can see to capture.
let sight = self.sight(square, board) & (opposing_occupancy | en_passant_square);
let pushes = pawn_pushes(square.into(), self.color, board.occupancy());
sight | pushes
}
Shape::King => {
let kingside_target_square =
if board.color_can_castle(Wing::KingSide, Some(color)).is_ok() {
let parameters = Board::castling_parameters(Wing::KingSide, color);
parameters.target.king.into()
} else {
BitBoard::EMPTY
};
let queenside_target_square = if board
.color_can_castle(Wing::QueenSide, Some(self.color))
.is_ok()
{
let parameters = Board::castling_parameters(Wing::QueenSide, color);
parameters.target.king.into()
} else {
BitBoard::EMPTY
};
self.sight(square, board) | kingside_target_square | queenside_target_square
}
_ => self.sight(square, board),
}
}
}
fn pawn_pushes(pawn: BitBoard, color: Color, occupancy: BitBoard) -> BitBoard {
let vacancy = !occupancy;
match color {
Color::White => {
const SECOND_RANK: BitBoard = BitBoard::rank(Rank::TWO);
let mut pushes = pawn.shift_north_one() & vacancy;
if !(pawn & SECOND_RANK).is_empty() {
// Double push
pushes = pushes | (pushes.shift_north_one() & vacancy);
}
pushes
}
Color::Black => {
const SEVENTH_RANK: BitBoard = BitBoard::rank(Rank::SEVEN);
let mut pushes = pawn.shift_south_one() & vacancy;
if !(pawn & SEVENTH_RANK).is_empty() {
// Double push
pushes = pushes | (pushes.shift_south_one() & vacancy);
}
pushes
}
}
}
#[cfg(test)]
mod tests {
use super::pawn_pushes;
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),
bitboard![E5]
);
assert_eq!(
pawn_pushes(Square::E2.into(), Color::White, BitBoard::EMPTY),
bitboard![E3 E4]
);
}
#[test]
fn black_pawn_empty_board() {
assert_eq!(
pawn_pushes(Square::A4.into(), Color::Black, BitBoard::EMPTY),
bitboard![A3]
);
assert_eq!(
pawn_pushes(Square::B7.into(), Color::Black, BitBoard::EMPTY),
bitboard![B6 B5]
);
}
#[test]
fn white_pushes_blocker() {
assert_eq!(
pawn_pushes(Square::C5.into(), Color::White, bitboard![C6]),
BitBoard::EMPTY
);
assert_eq!(
pawn_pushes(Square::D2.into(), Color::White, bitboard![D4]),
bitboard![D3]
);
assert_eq!(
pawn_pushes(Square::D2.into(), Color::White, bitboard![D3]),
BitBoard::EMPTY
);
}
#[test]
fn black_pushes_blocker() {
assert_eq!(
pawn_pushes(Square::C5.into(), Color::Black, bitboard![C4]),
BitBoard::EMPTY
);
assert_eq!(
pawn_pushes(Square::D7.into(), Color::Black, bitboard![D5]),
bitboard![D6]
);
assert_eq!(
pawn_pushes(Square::D7.into(), Color::Black, bitboard![D6]),
BitBoard::EMPTY
);
}
}

144
board/src/moves/bishop.rs Normal file
View file

@ -0,0 +1,144 @@
// Eryn Wells <eryn@erynwells.me>
use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight};
use crate::{
piece::{Color, Piece, PlacedPiece},
Move, Position,
};
move_generator_declaration!(ClassicalMoveGenerator);
impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> {
fn piece(color: Color) -> Piece {
Piece::bishop(color)
}
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
let piece = placed_piece.piece();
let color = piece.color();
let square = placed_piece.square();
let empty_squares = position.empty_squares();
let friendly_pieces = position.bitboard_for_color(color);
let opposing_pieces = position.bitboard_for_color(color.other());
let sight = classical::BishopSight.sight(square, position);
let quiet_moves_bb = sight & (empty_squares | !friendly_pieces);
let capture_moves_bb = sight & opposing_pieces;
let map_to_move = |sq| Move::new(piece, square, sq);
let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move);
let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move);
MoveSet::new(placed_piece)
.quiet_moves(quiet_moves_bb, quiet_moves)
.capture_moves(capture_moves_bb, capture_moves)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
bitboard::BitBoard,
piece::{Color, Piece},
position::DiagramFormatter,
Position, Square,
};
#[test]
fn classical_single_bishop_bitboard() {
let mut pos = Position::empty();
[(Piece::bishop(Color::White), Square::A1)]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {:?} on {}", &p, &sq))
});
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
assert_eq!(
generator.bitboard(),
BitBoard::new(
0b10000000_01000000_00100000_00010000_00001000_00000100_00000010_00000000
)
);
}
/// Test that a bishop can move up to, but not onto, a friendly piece.
#[test]
fn classical_single_bishop_with_same_color_blocker_bitboard() {
let mut pos = Position::empty();
[
(Piece::bishop(Color::White), Square::A1),
(Piece::knight(Color::White), Square::E5),
]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {} on {}", p, sq))
});
println!("{}", DiagramFormatter::new(&pos));
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
assert_eq!(
generator.bitboard(),
BitBoard::new(
0b00000000_00000000_00000000_00000000_00001000_00000100_00000010_00000000
)
);
}
/// Test that a rook can move up to, and then capture, an enemy piece.
#[test]
fn classical_single_bishop_with_opposing_color_blocker_bitboard() {
let mut pos = Position::empty();
[
(Piece::bishop(Color::White), Square::A1),
(Piece::knight(Color::Black), Square::C3),
]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {} on {}", p, sq))
});
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
assert_eq!(
generator.bitboard(),
BitBoard::new(
0b00000000_00000000_00000000_00000000_00000000_00000100_00000010_00000000
)
);
}
#[test]
fn classical_single_bishop_in_center() {
let mut pos = Position::empty();
[(Piece::bishop(Color::White), Square::E4)]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {} on {}", p, sq))
});
println!("{}", DiagramFormatter::new(&pos));
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
let bitboard = generator.bitboard();
let expected = BitBoard::new(
0b00000001_10000010_01000100_00101000_00000000_00101000_01000100_10000010,
);
assert_eq!(
bitboard, expected,
"actual:\n{}\nexpected:\n{}",
bitboard, expected
);
}
}

View file

@ -1,2 +1 @@
// Eryn Wells <eryn@erynwells.me>

165
board/src/moves/king.rs Normal file
View file

@ -0,0 +1,165 @@
// Eryn Wells <eryn@erynwells.me>
//! Declares the KingMoveGenerator type. This struct is responsible for
//! generating the possible moves for the king in the given position.
use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight};
use crate::{
bitboard::BitBoard,
piece::{Color, Piece, PlacedPiece},
position::BoardSide,
Move, Position,
};
move_generator_declaration!(KingMoveGenerator, struct);
move_generator_declaration!(KingMoveGenerator, new);
move_generator_declaration!(KingMoveGenerator, getters);
impl<'a> KingMoveGenerator<'a> {
#[allow(unused_variables)]
fn king_side_castle(position: &Position, color: Color) -> Option<Move> {
if !position.player_has_right_to_castle(color, BoardSide::King) {
return None;
}
// TODO: Implement king side castle.
None
}
#[allow(unused_variables)]
fn queen_side_castle(position: &Position, color: Color) -> Option<Move> {
if !position.player_has_right_to_castle(color, BoardSide::Queen) {
return None;
}
// TODO: Implement queen side castle.
None
}
}
impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> {
fn piece(color: Color) -> Piece {
Piece::king(color)
}
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
let piece = placed_piece.piece();
let color = piece.color();
let square = placed_piece.square();
let empty_squares = position.empty_squares();
let opposing_pieces = position.bitboard_for_color(color.other());
let all_moves = classical::KingSight.sight(square, position);
let quiet_moves_bb = all_moves & empty_squares;
let capture_moves_bb = all_moves & opposing_pieces;
// TODO: Handle checks. Prevent moving a king to a square attacked by a
// piece of the opposite color.
let map_to_move = |sq| Move::new(piece, square, sq);
let king_side_castle = Self::king_side_castle(position, color);
let queen_side_castle = Self::queen_side_castle(position, color);
let quiet_moves = quiet_moves_bb
.occupied_squares()
.map(map_to_move)
.chain(king_side_castle.iter().cloned())
.chain(queen_side_castle.iter().cloned());
let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move);
MoveSet::new(placed_piece)
.quiet_moves(quiet_moves_bb, quiet_moves)
.capture_moves(capture_moves_bb, capture_moves)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{piece::Piece, Position, Square};
use std::collections::HashSet;
#[test]
fn one_king() {
let mut pos = Position::empty();
pos.place_piece(Piece::king(Color::White), Square::E4)
.expect("Failed to place king on e4");
let generator = KingMoveGenerator::new(&pos, Color::White);
let bb = generator.bitboard();
assert_eq!(
bb,
BitBoard::new(
0b00000000_00000000_00000000_00111000_00101000_00111000_00000000_00000000
)
);
let expected_moves = [
Move::new(Piece::king(Color::White), Square::E4, Square::D5),
Move::new(Piece::king(Color::White), Square::E4, Square::E5),
Move::new(Piece::king(Color::White), Square::E4, Square::F5),
Move::new(Piece::king(Color::White), Square::E4, Square::F4),
Move::new(Piece::king(Color::White), Square::E4, Square::F3),
Move::new(Piece::king(Color::White), Square::E4, Square::E3),
Move::new(Piece::king(Color::White), Square::E4, Square::D3),
Move::new(Piece::king(Color::White), Square::E4, Square::D4),
];
let mut generated_moves: HashSet<Move> = generator.iter().cloned().collect();
for ex_move in expected_moves {
assert!(
generated_moves.remove(&ex_move),
"{:#?} was not generated",
&ex_move
);
}
assert!(
generated_moves.is_empty(),
"Moves unexpectedly present: {:#?}",
generated_moves
);
}
#[test]
fn one_king_corner() {
let mut pos = Position::empty();
pos.place_piece(Piece::king(Color::White), Square::A1)
.expect("Failed to place king on a1");
let generator = KingMoveGenerator::new(&pos, Color::White);
let bb = generator.bitboard();
assert_eq!(
bb,
BitBoard::new(
0b00000000_00000000_00000000_00000000_00000000_00000000_00000011_00000010
)
);
let expected_moves = [
Move::new(Piece::king(Color::White), Square::A1, Square::A2),
Move::new(Piece::king(Color::White), Square::A1, Square::B1),
Move::new(Piece::king(Color::White), Square::A1, Square::B2),
];
let mut generated_moves: HashSet<Move> = generator.iter().cloned().collect();
for ex_move in expected_moves {
assert!(
generated_moves.remove(&ex_move),
"{:#?} was not generated",
&ex_move
);
}
assert!(
generated_moves.is_empty(),
"Moves unexpectedly present: {:#?}",
generated_moves
);
}
}

90
board/src/moves/knight.rs Normal file
View file

@ -0,0 +1,90 @@
// Eryn Wells <eryn@erynwells.me>
use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet};
use crate::{
bitboard::BitBoard,
piece::{Color, Piece, PlacedPiece},
Move, Position,
};
move_generator_declaration!(KnightMoveGenerator);
impl<'pos> MoveGeneratorInternal for KnightMoveGenerator<'pos> {
fn piece(color: Color) -> Piece {
Piece::knight(color)
}
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
let opposing_pieces = position.bitboard_for_color(placed_piece.piece().color().other());
let empty_squares = position.empty_squares();
let knight_moves = BitBoard::knight_moves(placed_piece.square());
let quiet_moves_bb = knight_moves & empty_squares;
let capture_moves_bb = knight_moves & opposing_pieces;
let quiet_moves = quiet_moves_bb
.occupied_squares()
.map(|to_sq| Move::new(placed_piece.piece(), placed_piece.square(), to_sq));
let capture_moves = capture_moves_bb.occupied_squares().map(|to_sq| {
let captured_piece = position.piece_on_square(to_sq).unwrap();
Move::new(placed_piece.piece(), placed_piece.square(), to_sq).capturing(captured_piece)
});
MoveSet::new(placed_piece)
.quiet_moves(quiet_moves_bb, quiet_moves)
.capture_moves(capture_moves_bb, capture_moves)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Square;
use std::collections::HashSet;
#[test]
fn one_knight() {
let mut pos = Position::empty();
pos.place_piece(Piece::knight(Color::White), Square::E4)
.expect("Failed to place knight on e4");
let generator = KnightMoveGenerator::new(&pos, Color::White);
/*
let bb = generator.bitboard();
assert_eq!(
bb,
BitBoard::new(
0b00000000_00000000_00000000_00111000_00101000_00111000_00000000_00000000
)
);
*/
let expected_moves = [
Move::new(Piece::knight(Color::White), Square::E4, Square::C3),
Move::new(Piece::knight(Color::White), Square::E4, Square::D2),
Move::new(Piece::knight(Color::White), Square::E4, Square::F2),
Move::new(Piece::knight(Color::White), Square::E4, Square::G3),
Move::new(Piece::knight(Color::White), Square::E4, Square::C5),
Move::new(Piece::knight(Color::White), Square::E4, Square::D6),
Move::new(Piece::knight(Color::White), Square::E4, Square::G5),
Move::new(Piece::knight(Color::White), Square::E4, Square::F6),
];
let mut generated_moves: HashSet<Move> = generator.iter().cloned().collect();
for ex_move in expected_moves {
assert!(
generated_moves.remove(&ex_move),
"{:#?} was not generated",
&ex_move
);
}
assert!(
generated_moves.is_empty(),
"Moves unexpectedly present: {:#?}",
generated_moves
);
}
}

94
board/src/moves/mod.rs Normal file
View file

@ -0,0 +1,94 @@
// Eryn Wells <eryn@erynwells.me>
mod bishop;
mod classical;
mod king;
mod knight;
mod r#move;
mod move_generator;
mod move_set;
mod pawn;
mod queen;
mod rook;
pub(crate) mod sight;
pub use move_generator::Moves;
pub use r#move::Move;
pub(self) use move_set::MoveSet;
use crate::{
piece::{Color, Piece, PlacedPiece},
Position, Square,
};
use std::collections::BTreeMap;
trait MoveGenerator {
fn iter(&self) -> dyn Iterator<Item = Move>;
fn moves(&self, color: Color) -> dyn Iterator<Item = Move>;
fn attacks(&self, color: Color) -> dyn Iterator<Item = Move>;
}
macro_rules! move_generator_declaration {
($name:ident) => {
move_generator_declaration!($name, struct);
move_generator_declaration!($name, new);
move_generator_declaration!($name, getters);
};
($name:ident, struct) => {
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct $name<'pos> {
position: &'pos crate::Position,
color: crate::piece::Color,
move_sets: std::collections::BTreeMap<crate::Square, crate::moves::MoveSet>,
}
};
($name:ident, new) => {
impl<'pos> $name<'pos> {
pub(super) fn new(position: &Position, color: crate::piece::Color) -> $name {
$name {
position,
color,
move_sets: Self::move_sets(position, color),
}
}
}
};
($name:ident, getters) => {
impl<'pos> $name<'pos> {
pub(super) fn iter(&self) -> impl Iterator<Item = &crate::Move> + '_ {
self.move_sets.values().map(|set| set.moves()).flatten()
}
fn bitboard(&self) -> crate::bitboard::BitBoard {
self.move_sets
.values()
.fold(crate::bitboard::BitBoard::empty(), |partial, mv_set| {
partial | mv_set.bitboard()
})
}
}
};
}
pub(self) use move_generator_declaration;
trait MoveGeneratorInternal {
fn piece(color: Color) -> Piece;
fn move_sets(position: &Position, color: Color) -> BTreeMap<Square, MoveSet> {
let piece = Self::piece(color);
BTreeMap::from_iter(
position
.bitboard_for_piece(piece)
.occupied_squares()
.map(|sq| {
let placed_piece = PlacedPiece::new(piece, sq);
let move_set = Self::move_set_for_piece(position, placed_piece);
(sq, move_set)
}),
)
}
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet;
}

255
board/src/moves/move.rs Normal file
View file

@ -0,0 +1,255 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{piece::*, position::BoardSide, Square};
/// A move that transfers a piece from one square to another.
trait Move<C: Color, S: Shape>: Sized {
fn piece(&self) -> Piece<C, S>;
fn from_square(&self) -> Square;
fn to_square(&self) -> Square;
}
/// A move that captures an opposing piece.
trait Capturing<C: Color, S: Shape, CapturedS: Shape>: Move<C, S> {
type CaptureMove;
fn capturing(self, capturing: CapturedS) -> Self::CaptureMove;
fn captured_piece(&self) -> Piece<C::Other, CapturedS>;
}
/// A move that promotes a pawn to another piece.
trait Promoting<C: Color, S: Shape, PromotingS: Shape>: Move<C, Pawn> {
type PromotionMove;
fn promoting_to(self, shape: PromotingS) -> Self::PromotionMove;
fn promoting_piece(&self) -> Piece<C, S>;
}
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct SimpleMove<C, S> {
piece: Piece<C, S>,
from_square: Square,
to_square: Square,
}
impl<C: Color, S: Shape> SimpleMove<C, S> {
fn new(piece: Piece<C, S>, from_square: Square, to_square: Square) -> Self {
SimpleMove {
piece,
from_square,
to_square,
}
}
fn capturing<CapturedS: Shape>(
self,
captured_piece: Piece<C::Other, CapturedS>,
) -> Capture<C, S, CapturedS> {
Capture {
r#move: self,
captured_piece,
}
}
}
impl<C, S> Move<C, S> for Piece<C, S> {
fn piece(&self) -> Piece<C, S> {
self.piece
}
fn from_square(&self) -> Square {
self.from_square
}
fn to_square(&self) -> Square {
self.to_square
}
}
pub struct Capture<C: Color, S: Shape, CapturedS: Shape> {
r#move: dyn Move<C, S>,
captured_piece: Piece<C::Other, CapturedS>,
}
pub struct Promotion<C: Color, PromS: Shape> {
r#move: dyn Move<C, Pawn>,
promoting_to_shape: PromS,
}
pub struct CapturingMove<C: Color, S: Shape, CapturingShape: Shape> {
capturing: PlacedPiece<C::Other, CapturingShape>,
}
impl<C: Color, S: Shape> Move<C, S> {
pub fn is_castle(&self) -> bool {
let color = self.piece.color();
self.piece.shape() == Shape::King
&& self.from == Square::KING_STARTING_SQUARES[color as usize]
&& Square::KING_CASTLE_TARGET_SQUARES[color as usize].contains(&self.to)
}
pub fn is_kingside_castle(&self) -> bool {
let color = self.piece.color();
self.piece.shape() == Shape::King
&& self.from == Square::KING_STARTING_SQUARES[color as usize]
&& self.to
== Square::KING_CASTLE_TARGET_SQUARES[color as usize][BoardSide::King as usize]
}
pub fn is_queenside_castle(&self) -> bool {
let color = self.piece.color();
self.piece.shape() == Shape::King
&& self.from == Square::KING_STARTING_SQUARES[color as usize]
&& self.to
== Square::KING_CASTLE_TARGET_SQUARES[color as usize][BoardSide::Queen as usize]
}
pub fn is_capture(&self) -> bool {
self.capturing.is_some()
}
pub fn is_promotion(&self) -> bool {
self.promoting_to.is_some()
}
}
mod move_formatter {
use super::Move;
use crate::{piece::Shape, Position};
use std::fmt;
enum Style {
Short,
Long,
}
pub(crate) struct AlgebraicMoveFormatter<'m> {
r#move: &'m Move,
style: Style,
}
impl<'pos, 'm> AlgebraicMoveFormatter<'m> {
pub(crate) fn new(mv: &'m Move) -> AlgebraicMoveFormatter<'m> {
AlgebraicMoveFormatter {
r#move: mv,
style: Style::Short,
}
}
fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
fn fmt_kingside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0-0")
}
fn fmt_queenside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0-0-0")
}
fn fmt_short(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
unimplemented!()
}
fn fmt_long(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO: Figure out how to write the short algebraic form, where a
// disambiguating coordiate is specified when two of the same piece
// cam move to the same square.
// TODO: Write better pawn moves.
let mv = self.r#move;
let shape = mv.piece.shape();
if shape != Shape::Pawn {
write!(f, "{}", shape)?;
}
write!(
f,
"{}{}{}",
mv.from,
if mv.is_capture() { 'x' } else { '-' },
mv.to,
)?;
if let Some(promoting_to) = mv.promoting_to {
write!(f, "={}", promoting_to)?;
}
// TODO: Write check (+) and checkmate (#) symbols
Ok(())
}
}
impl<'pos, 'mv> fmt::Display for AlgebraicMoveFormatter<'mv> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.r#move.is_kingside_castle() {
return self.fmt_kingside_castle(f);
} else if self.r#move.is_queenside_castle() {
return self.fmt_queenside_castle(f);
}
match self.style {
Style::Short => self.fmt_short(f),
Style::Long => self.fmt_long(f),
}
}
}
#[cfg(test)]
mod tests {
use super::{AlgebraicMoveFormatter, Style};
macro_rules! chess_move {
($color:ident $shape:ident $from_square:ident - $to_square:ident) => {
crate::Move::new(
crate::piece::Piece::new(crate::piece::Color::$color, crate::piece::Shape::$shape),
crate::Square::$from_square,
crate::Square::$to_square,
)
};
($color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident) => {
chess_move!($color $shape $from_square - $to_square)
.capturing(crate::piece::PlacedPiece::new(
crate::piece::Piece::new(
crate::piece::Color::$captured_color,
crate::piece::Shape::$captured_shape,
),
crate::Square::$to_square,
))
};
}
macro_rules! test_algebraic_formatter {
($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident, $output:expr) => {
#[test]
fn $test_name() {
let mv = chess_move!(
$color $shape $from_square x $to_square,
$captured_color $captured_shape
);
let formatter = AlgebraicMoveFormatter::new(&mv).style(Style::$style);
assert_eq!(format!("{}", formatter), $output);
}
};
($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident - $to_square:ident, $output:expr) => {
#[test]
fn $test_name() {
let mv = chess_move!($color $shape $from_square-$to_square);
let formatter = AlgebraicMoveFormatter::new(&mv).style(Style::$style);
assert_eq!(format!("{}", formatter), $output);
}
};
}
test_algebraic_formatter!(long_pawn_move, Long, White Pawn E4 - E5, "e4-e5");
test_algebraic_formatter!(long_bishop_move, Long, White Bishop A4 - D7, "Ba4-d7");
test_algebraic_formatter!(long_bishop_capture, Long, White Bishop A2 x E6, Black Knight, "Ba2xe6");
}
}

View file

@ -0,0 +1,44 @@
// Eryn Wells <eryn@erynwells.me>
use super::{
bishop::ClassicalMoveGenerator as BishopMoveGenerator, king::KingMoveGenerator,
knight::KnightMoveGenerator, pawn::PawnMoveGenerator,
queen::ClassicalMoveGenerator as QueenMoveGenerator,
rook::ClassicalMoveGenerator as RookMoveGenerator, Move,
};
use crate::piece::Color;
use crate::Position;
#[derive(Clone, Eq, PartialEq)]
pub struct Moves<'pos> {
pawn_moves: PawnMoveGenerator<'pos>,
knight_moves: KnightMoveGenerator<'pos>,
bishop_moves: BishopMoveGenerator<'pos>,
rook_moves: RookMoveGenerator<'pos>,
queen_moves: QueenMoveGenerator<'pos>,
king_moves: KingMoveGenerator<'pos>,
}
impl<'a> Moves<'a> {
pub fn new(position: &Position, color: Color) -> Moves {
Moves {
pawn_moves: PawnMoveGenerator::new(position, color),
knight_moves: KnightMoveGenerator::new(position, color),
bishop_moves: BishopMoveGenerator::new(position, color),
rook_moves: RookMoveGenerator::new(position, color),
queen_moves: QueenMoveGenerator::new(position, color),
king_moves: KingMoveGenerator::new(position, color),
}
}
fn iter(&self) -> impl Iterator<Item = Move> + '_ {
self.pawn_moves
.iter()
.chain(self.knight_moves.iter())
.chain(self.bishop_moves.iter())
.chain(self.rook_moves.iter())
.chain(self.queen_moves.iter())
.chain(self.king_moves.iter())
.cloned()
}
}

View file

@ -0,0 +1,71 @@
use crate::{bitboard::BitBoard, piece::PlacedPiece, Move};
#[derive(Clone, Debug, Eq, PartialEq)]
struct BitBoardSet {
quiet: BitBoard,
captures: BitBoard,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct MoveListSet {
quiet: Vec<Move>,
captures: Vec<Move>,
}
/// A set of moves for a piece on the board.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct MoveSet {
piece: PlacedPiece,
bitboards: BitBoardSet,
move_lists: MoveListSet,
}
impl MoveSet {
pub(super) fn new(piece: PlacedPiece) -> MoveSet {
MoveSet {
piece,
bitboards: BitBoardSet {
quiet: BitBoard::empty(),
captures: BitBoard::empty(),
},
move_lists: MoveListSet {
quiet: Vec::new(),
captures: Vec::new(),
},
}
}
pub(super) fn quiet_moves(
mut self,
bitboard: BitBoard,
move_list: impl Iterator<Item = Move>,
) -> MoveSet {
self.bitboards.quiet = bitboard;
self.move_lists.quiet = move_list.collect();
self
}
pub(super) fn capture_moves(
mut self,
bitboard: BitBoard,
move_list: impl Iterator<Item = Move>,
) -> MoveSet {
self.bitboards.captures = bitboard;
self.move_lists.captures = move_list.collect();
self
}
/// Return a BitBoard representing all possible moves.
pub(super) fn bitboard(&self) -> BitBoard {
self.bitboards.captures | self.bitboards.quiet
}
pub(super) fn moves(&self) -> impl Iterator<Item = &Move> {
self.move_lists
.captures
.iter()
.chain(self.move_lists.quiet.iter())
}
}

394
board/src/moves/pawn.rs Normal file
View file

@ -0,0 +1,394 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{
bitboard::BitBoard,
piece::{Color, Piece, Shape},
Move, Position,
};
enum MoveList {
Quiet = 0,
Promotions = 1,
Captures = 2,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct MoveIterator(usize, usize);
struct MoveGenerationParameters {
starting_rank: BitBoard,
promotion_rank: BitBoard,
push_shift: fn(BitBoard) -> BitBoard,
left_capture_shift: fn(BitBoard) -> BitBoard,
right_capture_shift: fn(BitBoard) -> BitBoard,
}
#[derive(Clone, Eq, PartialEq)]
pub(super) struct PawnMoveGenerator<'pos> {
color: Color,
position: &'pos Position,
did_populate_move_lists: bool,
pushes: BitBoard,
attacks: BitBoard,
move_lists: [Vec<Move>; 3],
move_iterator: MoveIterator,
}
impl<'pos> PawnMoveGenerator<'pos> {
pub(super) fn new(position: &Position, color: Color) -> PawnMoveGenerator {
PawnMoveGenerator {
position,
color,
did_populate_move_lists: false,
pushes: BitBoard::empty(),
attacks: BitBoard::empty(),
move_lists: [Vec::new(), Vec::new(), Vec::new()],
move_iterator: MoveIterator(0, 0),
}
}
pub(super) fn iter(&self) -> impl Iterator<Item = &Move> + '_ {
self.move_lists.iter().flatten()
}
fn generate_moves(&mut self) {
self.generate_move_bitboards();
self.populate_move_lists();
}
fn move_generation_parameters(&self) -> MoveGenerationParameters {
match self.color {
Color::White => MoveGenerationParameters {
starting_rank: BitBoard::rank(1),
promotion_rank: BitBoard::rank(7),
push_shift: BitBoard::shift_north_one,
left_capture_shift: BitBoard::shift_north_west_one,
right_capture_shift: BitBoard::shift_north_east_one,
},
Color::Black => MoveGenerationParameters {
starting_rank: BitBoard::rank(6),
promotion_rank: BitBoard::rank(0),
push_shift: BitBoard::shift_south_one,
left_capture_shift: BitBoard::shift_south_east_one,
right_capture_shift: BitBoard::shift_south_west_one,
},
}
}
#[inline]
fn quiet_move_list(&mut self) -> &mut Vec<Move> {
&mut self.move_lists[MoveList::Quiet as usize]
}
#[inline]
fn promotion_move_list(&mut self) -> &mut Vec<Move> {
&mut self.move_lists[MoveList::Promotions as usize]
}
#[inline]
fn capture_move_list(&mut self) -> &mut Vec<Move> {
&mut self.move_lists[MoveList::Captures as usize]
}
}
impl<'pos> PawnMoveGenerator<'pos> {
fn generate_move_bitboards(&mut self) {
let parameters = self.move_generation_parameters();
self.generate_pushes_bitboard(&parameters);
self.generate_attacks_bitboard(&parameters);
}
fn generate_pushes_bitboard(&mut self, parameters: &MoveGenerationParameters) {
let empty_squares = self.position.empty_squares();
let bb = self.position.bitboard_for_piece(Piece::pawn(self.color));
let legal_1square_pushes = (parameters.push_shift)(bb) & empty_squares;
let legal_2square_pushes =
(parameters.push_shift)(legal_1square_pushes & BitBoard::rank(2)) & empty_squares;
self.pushes = legal_1square_pushes | legal_2square_pushes;
}
fn generate_attacks_bitboard(&mut self, parameters: &MoveGenerationParameters) {
let opponent_pieces = self.position.bitboard_for_color(self.color.other());
let bb = self.position.bitboard_for_piece(Piece::pawn(Color::White));
self.attacks = ((parameters.left_capture_shift)(bb) | (parameters.right_capture_shift)(bb))
& opponent_pieces;
#[allow(unused_variables)]
if let Some(en_passant) = self.en_passant() {
// TODO: Add en passant move to the attacks bitboard.
}
}
}
impl<'pos> PawnMoveGenerator<'pos> {
fn populate_move_lists(&mut self) {
let parameters = self.move_generation_parameters();
self._populate_move_lists(&parameters);
self.did_populate_move_lists = true;
}
fn _populate_move_lists(&mut self, parameters: &MoveGenerationParameters) {
let piece = Piece::pawn(self.color);
let bb = self.position.bitboard_for_piece(piece);
if bb.is_empty() {
return;
}
let empty_squares = self.position.empty_squares();
let black_pieces = self.position.bitboard_for_color(self.color.other());
for from_sq in bb.occupied_squares() {
let pawn: BitBoard = from_sq.into();
let push = (parameters.push_shift)(pawn);
if !(push & empty_squares).is_empty() {
let to_sq = push.occupied_squares().next().unwrap();
let r#move = Move::new(piece, from_sq, to_sq);
if !(push & parameters.promotion_rank).is_empty() {
for shape in Shape::promotable() {
self.promotion_move_list()
.push(r#move.clone().promoting_to(*shape));
}
} else {
self.quiet_move_list().push(r#move);
}
if !(pawn & parameters.starting_rank).is_empty() {
let push = (parameters.push_shift)(push);
if !(push & empty_squares).is_empty() {
let to_sq = push.occupied_squares().next().unwrap();
self.quiet_move_list()
.push(Move::new(piece, from_sq, to_sq));
}
}
}
for attack in [
(parameters.left_capture_shift)(pawn),
(parameters.right_capture_shift)(pawn),
] {
if !(attack & black_pieces).is_empty() {
let to_sq = attack.occupied_squares().next().unwrap();
let captured_piece = self.position.piece_on_square(to_sq).unwrap();
let r#move = Move::new(piece, from_sq, to_sq).capturing(captured_piece);
if !(attack & parameters.promotion_rank).is_empty() {
for shape in Shape::promotable() {
self.capture_move_list()
.push(r#move.clone().promoting_to(*shape));
}
} else {
self.capture_move_list().push(r#move);
}
}
}
if let Some(en_passant) = self.en_passant() {
self.capture_move_list().push(en_passant);
}
}
}
fn en_passant(&self) -> Option<Move> {
// TODO: En passant. I think the way to do this is to have the position mark
// an en passant square when the conditions are correct, i.e. when the
// opposing player has pushed a pawn two squares off the initial rank, and
// then check in these routines if a pawn of this color attacks that square.
None
}
}
impl<'pos> Iterator for PawnMoveGenerator<'pos> {
type Item = Move;
fn next(&mut self) -> Option<Self::Item> {
if !self.did_populate_move_lists {
self.generate_moves();
}
let iter = &mut self.move_iterator;
// Find the next non-empty list.
loop {
if iter.0 >= self.move_lists.len() || !self.move_lists[iter.0].is_empty() {
break;
}
iter.0 += 1;
}
if let Some(move_list) = self.move_lists.get(iter.0) {
let next_move = move_list[iter.1].clone();
iter.1 += 1;
if iter.1 >= move_list.len() {
// Increment the list index here. On the next iteration, find the next non-empty list.
iter.0 += 1;
iter.1 = 0;
}
Some(next_move)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::piece::PlacedPiece;
use crate::position::DiagramFormatter;
use crate::{Position, Square};
use std::collections::HashSet;
#[test]
fn one_2square_push() {
let mut pos = Position::empty();
pos.place_piece(Piece::pawn(Color::White), Square::E2)
.expect("Failed to place pawn on e2");
let generator = PawnMoveGenerator::new(&pos, Color::White);
let expected_moves = HashSet::from_iter(
[
Move::new(Piece::pawn(Color::White), Square::E2, Square::E3),
Move::new(Piece::pawn(Color::White), Square::E2, Square::E4),
]
.into_iter(),
);
let generated_moves: HashSet<Move> = generator.collect();
assert_eq!(generated_moves, expected_moves);
}
#[test]
fn one_1square_push() {
let mut pos = Position::empty();
pos.place_piece(Piece::pawn(Color::White), Square::E3)
.expect("Failed to place pawn on e3");
let generator = PawnMoveGenerator::new(&pos, Color::White);
let expected_moves = HashSet::from_iter(
[Move::new(Piece::pawn(Color::White), Square::E3, Square::E4)].into_iter(),
);
let generated_moves: HashSet<Move> = generator.collect();
assert_eq!(generated_moves, expected_moves);
}
#[test]
fn one_obstructed_2square_push() {
let mut pos = Position::empty();
pos.place_piece(Piece::pawn(Color::White), Square::E2)
.expect("Failed to place pawn on e2");
pos.place_piece(Piece::knight(Color::White), Square::E4)
.expect("Failed to place knight on e4");
println!("{}", DiagramFormatter::new(&pos));
let generator = PawnMoveGenerator::new(&pos, Color::White);
let expected_moves = HashSet::from_iter(
[Move::new(Piece::pawn(Color::White), Square::E2, Square::E3)].into_iter(),
);
let generated_moves: HashSet<Move> = generator.collect();
assert_eq!(generated_moves, expected_moves);
}
#[test]
fn one_obstructed_1square_push() {
let mut pos = Position::empty();
pos.place_piece(Piece::pawn(Color::White), Square::E2)
.expect("Failed to place pawn on e2");
pos.place_piece(Piece::knight(Color::White), Square::E3)
.expect("Failed to place knight on e4");
println!("{}", DiagramFormatter::new(&pos));
let generator = PawnMoveGenerator::new(&pos, Color::White);
let generated_moves: HashSet<Move> = generator.collect();
assert_eq!(generated_moves, HashSet::new());
}
#[test]
fn one_attack() {
let mut pos = Position::empty();
pos.place_piece(Piece::pawn(Color::White), Square::E4)
.expect("Failed to place pawn on e4");
pos.place_piece(Piece::bishop(Color::White), Square::E5)
.expect("Failed to place pawn on e4");
pos.place_piece(Piece::knight(Color::Black), Square::D5)
.expect("Failed to place knight on d5");
println!("{}", DiagramFormatter::new(&pos));
let generator = PawnMoveGenerator::new(&pos, Color::White);
let expected_moves = HashSet::from_iter(
[Move::new(Piece::pawn(Color::White), Square::E4, Square::D5)
.capturing(PlacedPiece::new(Piece::knight(Color::Black), Square::D5))]
.into_iter(),
);
let generated_moves: HashSet<Move> = generator.collect();
assert_eq!(generated_moves, expected_moves);
}
#[test]
fn one_double_attack() {
let mut pos = Position::empty();
pos.place_piece(Piece::pawn(Color::White), Square::E4)
.expect("Failed to place pawn on e4");
pos.place_piece(Piece::bishop(Color::White), Square::E5)
.expect("Failed to place pawn on e4");
pos.place_piece(Piece::knight(Color::Black), Square::D5)
.expect("Failed to place knight on d5");
pos.place_piece(Piece::queen(Color::Black), Square::F5)
.expect("Failed to place knight on f5");
println!("{}", DiagramFormatter::new(&pos));
let generator = PawnMoveGenerator::new(&pos, Color::White);
let expected_moves = HashSet::from_iter(
[
Move::new(Piece::pawn(Color::White), Square::E4, Square::D5)
.capturing(PlacedPiece::new(Piece::knight(Color::Black), Square::D5)),
Move::new(Piece::pawn(Color::White), Square::E4, Square::F5)
.capturing(PlacedPiece::new(Piece::queen(Color::Black), Square::F5)),
]
.into_iter(),
);
let generated_moves: HashSet<Move> = generator.collect();
assert_eq!(
generated_moves, expected_moves,
"generated: {:#?}\nexpected: {:#?}",
generated_moves, expected_moves
);
}
}

146
board/src/moves/queen.rs Normal file
View file

@ -0,0 +1,146 @@
// Eryn Wells <eryn@erynwells.me>
use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight};
use crate::{
piece::{Color, Piece, PlacedPiece},
Move, Position,
};
move_generator_declaration!(ClassicalMoveGenerator);
impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> {
fn piece(color: Color) -> Piece {
Piece::queen(color)
}
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
let piece = placed_piece.piece();
let color = piece.color();
let square = placed_piece.square();
let blockers = position.occupied_squares();
let empty_squares = !blockers;
let friendly_pieces = position.bitboard_for_color(color);
let opposing_pieces = position.bitboard_for_color(color.other());
let mut all_moves = classical::QueenSight.sight(square, position);
let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces);
let capture_moves_bb = all_moves & opposing_pieces;
let map_to_move = |sq| Move::new(piece, square, sq);
let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move);
let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move);
MoveSet::new(placed_piece)
.quiet_moves(quiet_moves_bb, quiet_moves)
.capture_moves(capture_moves_bb, capture_moves)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
bitboard::BitBoard,
piece::{Color, Piece},
position::DiagramFormatter,
Position, Square,
};
#[test]
fn classical_single_queen_bitboard() {
let mut pos = Position::empty();
[(Piece::queen(Color::White), Square::A1)]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {:?} on {}", &p, &sq))
});
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
let bitboard = generator.bitboard();
let expected = BitBoard::new(
0b10000001_01000001_00100001_00010001_00001001_00000101_00000011_11111110,
);
assert_eq!(
bitboard, expected,
"actual:\n{}\nexpected:\n{}",
bitboard, expected
);
}
/// Test that a rook can move up to, but not onto, a friendly piece.
#[test]
fn classical_single_queen_with_same_color_blocker_bitboard() {
let mut pos = Position::empty();
[
(Piece::queen(Color::White), Square::A1),
(Piece::knight(Color::White), Square::E1),
]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {} on {}", p, sq))
});
println!("{}", DiagramFormatter::new(&pos));
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
let bitboard = generator.bitboard();
let expected = BitBoard::new(
0b10000001_01000001_00100001_00010001_00001001_00000101_00000011_00001110,
);
assert_eq!(
bitboard, expected,
"actual:\n{}\nexpected:\n{}",
bitboard, expected
);
}
/// Test that a rook can move up to, and then capture, an enemy piece.
#[test]
fn classical_single_queen_with_opposing_color_blocker_bitboard() {
let mut pos = Position::empty();
[
(Piece::queen(Color::White), Square::A1),
(Piece::knight(Color::Black), Square::E5),
]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {} on {}", p, sq))
});
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
assert_eq!(
generator.bitboard(),
BitBoard::new(
0b00000001_00000001_00000001_00010001_00001001_00000101_00000011_11111110,
)
);
}
#[test]
fn classical_single_queen_in_center() {
let mut pos = Position::empty();
[(Piece::queen(Color::White), Square::E4)]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {} on {}", p, sq))
});
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
assert_eq!(
generator.bitboard(),
BitBoard::new(
0b00010001_10010010_01010100_00111000_11101111_00111000_01010100_10010010
)
);
}
}

140
board/src/moves/rook.rs Normal file
View file

@ -0,0 +1,140 @@
// Eryn Wells <eryn@erynwells.me>
use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight};
use crate::{
piece::{Color, Piece, PlacedPiece},
Move, Position,
};
move_generator_declaration!(ClassicalMoveGenerator);
impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> {
fn piece(color: Color) -> Piece {
Piece::rook(color)
}
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
let piece = placed_piece.piece();
let color = piece.color();
let square = placed_piece.square();
let blockers = position.occupied_squares();
let empty_squares = !blockers;
let friendly_pieces = position.bitboard_for_color(color);
let opposing_pieces = position.bitboard_for_color(color.other());
let all_moves = classical::RookSight.sight(square, position);
let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces);
let capture_moves_bb = all_moves & opposing_pieces;
let map_to_move = |sq| Move::new(piece, square, sq);
let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move);
let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move);
MoveSet::new(placed_piece)
.quiet_moves(quiet_moves_bb, quiet_moves)
.capture_moves(capture_moves_bb, capture_moves)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
bitboard::BitBoard,
piece::{Color, Piece},
position::DiagramFormatter,
Position, Square,
};
#[test]
fn classical_single_rook_bitboard() {
let mut pos = Position::empty();
[(Piece::rook(Color::White), Square::A1)]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {:?} on {}", &p, &sq))
});
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
assert_eq!(
generator.bitboard(),
BitBoard::new(
0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_11111110
)
);
}
/// Test that a rook can move up to, but not onto, a friendly piece.
#[test]
fn classical_single_rook_with_same_color_blocker_bitboard() {
let mut pos = Position::empty();
[
(Piece::rook(Color::White), Square::A1),
(Piece::knight(Color::White), Square::E1),
]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {} on {}", p, sq))
});
println!("{}", DiagramFormatter::new(&pos));
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
assert_eq!(
generator.bitboard(),
BitBoard::new(
0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00001110
)
);
}
/// Test that a rook can move up to, and then capture, an enemy piece.
#[test]
fn classical_single_rook_with_opposing_color_blocker_bitboard() {
let mut pos = Position::empty();
[
(Piece::rook(Color::White), Square::A1),
(Piece::knight(Color::Black), Square::E1),
]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {} on {}", p, sq))
});
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
assert_eq!(
generator.bitboard(),
BitBoard::new(
0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00011110
)
);
}
#[test]
fn classical_single_rook_in_center() {
let mut pos = Position::empty();
[(Piece::rook(Color::White), Square::E4)]
.into_iter()
.for_each(|(p, sq)| {
pos.place_piece(p, sq)
.expect(&format!("Unable to place {} on {}", p, sq))
});
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
assert_eq!(
generator.bitboard(),
BitBoard::new(
0b00010000_00010000_00010000_00010000_11101111_00010000_00010000_00010000
)
);
}
}

165
board/src/moves/sight.rs Normal file
View file

@ -0,0 +1,165 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{piece::*, square::Direction, BitBoard, Position, Square};
pub(crate) trait Sight {
fn sight_on_empty_board(self, square: Square) -> BitBoard;
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard;
}
impl Sight for Piece<White, Pawn> {
fn sight_on_empty_board(self, square: Square) -> BitBoard {
let pawn: BitBoard = square.into();
pawn.shift_north_west_one() | pawn.shift_north_east_one()
}
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
let mut possible_squares = position.empty_squares() | position.opposing_pieces();
if let Some(en_passant) = position.en_passant_square() {
possible_squares |= en_passant.into();
}
self.sight_on_empty_board(square) & possible_squares
}
}
impl Sight for Piece<Black, Pawn> {
fn sight_on_empty_board(self, square: Square) -> BitBoard {
let pawn: BitBoard = square.into();
pawn.shift_south_west_one() | pawn.shift_south_east_one()
}
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
let mut possible_squares = position.empty_squares() | position.opposing_pieces();
if let Some(en_passant) = position.en_passant_square() {
possible_squares |= en_passant.into();
}
self.sight_on_empty_board(square) & possible_squares
}
}
impl<C> Sight for Piece<C, Knight> {
fn sight_on_empty_board(self, square: Square) -> BitBoard {
BitBoard::knight_moves(square)
}
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
self.sight_on_empty_board(square) & !position.friendly_pieces()
}
}
impl<C> Sight for Piece<C, Bishop> {
fn sight_on_empty_board(self, square: Square) -> BitBoard {
BitBoard::bishop_moves(square)
}
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
let mut sight = BitBoard::empty();
let blockers = position.occupied_squares();
macro_rules! update_moves_with_ray {
($direction:ident, $occupied_squares:tt) => {
let ray = BitBoard::ray(square, Direction::$direction);
if let Some(first_occupied_square) =
BitBoard::$occupied_squares(&(ray & blockers)).next()
{
let remainder = BitBoard::ray(first_occupied_square, Direction::$direction);
let attack_ray = ray & !remainder;
sight |= attack_ray;
} else {
sight |= ray;
}
};
}
update_moves_with_ray!(NorthEast, occupied_squares_trailing);
update_moves_with_ray!(NorthWest, occupied_squares_trailing);
update_moves_with_ray!(SouthEast, occupied_squares);
update_moves_with_ray!(SouthWest, occupied_squares);
sight
}
}
impl<C> Sight for Piece<C, Rook> {
fn sight_on_empty_board(self, square: Square) -> BitBoard {
BitBoard::rook_moves(square)
}
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
let mut sight = BitBoard::empty();
let blockers = position.occupied_squares();
macro_rules! update_moves_with_ray {
($direction:ident, $occupied_squares:tt) => {
let ray = BitBoard::ray(square, Direction::$direction);
if let Some(first_occupied_square) =
BitBoard::$occupied_squares(&(ray & blockers)).next()
{
let remainder = BitBoard::ray(first_occupied_square, Direction::$direction);
let attack_ray = ray & !remainder;
sight |= attack_ray;
} else {
sight |= ray;
}
};
}
update_moves_with_ray!(North, occupied_squares_trailing);
update_moves_with_ray!(East, occupied_squares_trailing);
update_moves_with_ray!(South, occupied_squares);
update_moves_with_ray!(West, occupied_squares);
sight
}
}
impl<C> Sight for Piece<C, Queen> {
fn sight_on_empty_board(self, square: Square) -> BitBoard {
BitBoard::queen_moves(square)
}
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
let mut sight = BitBoard::empty();
let blockers = position.occupied_squares();
macro_rules! update_moves_with_ray {
($direction:ident, $occupied_squares:tt) => {
let ray = BitBoard::ray(square, Direction::$direction);
if let Some(first_occupied_square) =
BitBoard::$occupied_squares(&(ray & blockers)).next()
{
let remainder = BitBoard::ray(first_occupied_square, Direction::$direction);
let attack_ray = ray & !remainder;
sight |= attack_ray;
} else {
sight |= ray;
}
};
}
update_moves_with_ray!(NorthWest, occupied_squares_trailing);
update_moves_with_ray!(North, occupied_squares_trailing);
update_moves_with_ray!(NorthEast, occupied_squares_trailing);
update_moves_with_ray!(East, occupied_squares_trailing);
update_moves_with_ray!(SouthEast, occupied_squares);
update_moves_with_ray!(South, occupied_squares);
update_moves_with_ray!(SouthWest, occupied_squares);
update_moves_with_ray!(West, occupied_squares);
sight
}
}
impl<C> Sight for Piece<C, King> {
fn sight_on_empty_board(self, square: Square) -> BitBoard {
BitBoard::king_moves(square)
}
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
self.sight_on_empty_board(square) & !position.friendly_pieces()
}
}

177
board/src/piece.rs Normal file
View file

@ -0,0 +1,177 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{bitboard::BitBoard, Square};
use std::fmt;
pub trait Color: Sized {
type Other: Color;
}
macro_rules! into_int_type {
($name:ident, $int_type:ident, $value:expr) => {
impl Into<$int_type> for $name {
fn into(self) -> $int_type {
$value
}
}
};
}
macro_rules! color_struct {
($name:ident, $index:expr, $other_color:ident) => {
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct $name;
impl Color for $name {
type Other = $other_color;
}
into_int_type!($name, u8, $index);
into_int_type!($name, usize, $index);
};
}
color_struct!(White, 0, Black);
color_struct!(Black, 1, White);
macro_rules! shape_struct {
($name:ident, $index:expr, $char:expr, $white_char:expr, $black_char:expr) => {
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct $name;
impl Shape for $name {}
into_int_type!($name, u8, $index);
into_int_type!($name, usize, $index);
impl Into<char> for $name {
fn into(self) -> char {
$char
}
}
impl fmt::Display for Piece<$name, White> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", $white_char)
}
}
impl fmt::Display for Piece<$name, Black> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", $black_char)
}
}
};
}
pub trait Shape: Sized {}
shape_struct!(Pawn, 0, 'P', '♙', '♟');
shape_struct!(Knight, 1, 'N', '♘', '♞');
shape_struct!(Bishop, 2, 'B', '♗', '♝');
shape_struct!(Rook, 3, 'R', '♖', '♜');
shape_struct!(Queen, 4, 'Q', '♕', '♛');
shape_struct!(King, 5, 'K', '♔', '♚');
#[derive(Debug, Eq, PartialEq)]
pub struct TryFromError;
#[derive(Debug, Eq, PartialEq)]
pub enum PiecePlacementError {
ExistsOnSquare,
}
#[macro_export]
macro_rules! piece {
($color:ident $shape:ident) => {
Piece::new(Color::$color, Shape::$shape)
};
($color:ident $shape:ident on $square:ident) => {
PlacedPiece::new(piece!($color $shape), Square::$square)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Piece<C: Color, S: Shape> {
color: C,
shape: S,
}
macro_rules! piece_constructor {
($func_name:ident, $shape:tt) => {
pub fn $func_name(color: C) -> Piece<C, S> {
Piece {
color,
shape: $shape,
}
}
};
}
impl<C, S> Piece<C, S>
where
C: Color,
S: Shape,
{
pub fn new(color: C, shape: S) -> Piece<C, S> {
Piece { color, shape }
}
piece_constructor!(pawn, Pawn);
piece_constructor!(knight, Knight);
piece_constructor!(bishop, Bishop);
piece_constructor!(rook, Rook);
piece_constructor!(queen, Queen);
piece_constructor!(king, King);
pub fn color(&self) -> C {
self.color
}
pub fn shape(&self) -> S {
self.shape
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct PlacedPiece<C, S> {
piece: Piece<C, S>,
square: Square,
}
impl<C, S> PlacedPiece<C, S> {
pub const fn new(piece: Piece<C, S>, square: Square) -> PlacedPiece<C, S> {
PlacedPiece { piece, square }
}
#[inline]
pub fn piece(&self) -> Piece<C, S> {
self.piece
}
#[inline]
pub fn square(&self) -> Square {
self.square
}
#[inline]
pub(crate) fn bitboard(&self) -> BitBoard {
self.square.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_try_from() {
assert_eq!(Shape::try_from('p'), Ok(Shape::Pawn));
assert_eq!(Shape::try_from("p"), Ok(Shape::Pawn));
}
#[test]
fn shape_into_char() {
assert_eq!(<Shape as Into<char>>::into(Shape::Pawn) as char, 'p');
}
}

View file

@ -1,169 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
mod counts;
mod mailbox;
use self::{counts::Counts, mailbox::Mailbox};
use chessfriend_bitboard::{BitBoard, IterationDirection};
use chessfriend_core::{Color, Piece, Shape, Square};
use std::{
hash::{Hash, Hasher},
ops::BitOr,
};
use thiserror::Error;
pub(crate) use counts::Counter;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum PlacePieceStrategy {
#[default]
Replace,
PreserveExisting,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum PlacePieceError {
#[error("cannot place piece on {square} with existing {piece}")]
ExisitingPiece { piece: Piece, square: Square },
}
/// The internal data structure of a [`Board`] that efficiently manages the
/// placement of pieces on the board.
#[derive(Clone, Debug, Default, Eq)]
pub struct PieceSet {
mailbox: Mailbox,
counts: Counts,
color_occupancy: [BitBoard; Color::NUM],
shape_occupancy: [BitBoard; Shape::NUM],
}
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 (shape_index, shape) in Shape::into_iter().enumerate() {
let bitboard = pieces[color_index][shape_index];
color_occupancy[color_index] |= bitboard;
shape_occupancy[shape_index] |= bitboard;
for square in bitboard.occupied_squares(&IterationDirection::default()) {
let piece = Piece::new(color, shape);
mailbox.set(piece, square);
counts.increment(color, shape);
}
}
}
Self {
mailbox,
counts,
color_occupancy,
shape_occupancy,
}
}
pub fn iter(&self) -> impl Iterator<Item = (Square, Piece)> {
self.mailbox.iter()
}
/// A [`BitBoard`] representing all the pieces on the board.
pub fn occpuancy(&self) -> BitBoard {
self.color_occupancy
.iter()
.fold(BitBoard::default(), BitOr::bitor)
}
pub fn friendly_occupancy(&self, color: Color) -> BitBoard {
self.color_occupancy[color as usize]
}
pub fn opposing_occupancy(&self, color: Color) -> BitBoard {
let color_index = color as usize;
self.color_occupancy.iter().enumerate().fold(
BitBoard::default(),
|acc, (index, occupancy)| {
if index == color_index {
acc
} else {
acc | occupancy
}
},
)
}
pub(crate) fn get(&self, square: Square) -> Option<Piece> {
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];
let shape_occupancy = self.shape_occupancy[piece.shape as usize];
color_occupancy & shape_occupancy
}
pub(crate) fn place(
&mut self,
piece: Piece,
square: Square,
strategy: PlacePieceStrategy,
) -> Result<Option<Piece>, PlacePieceError> {
let existing_piece = self.mailbox.get(square);
if strategy == PlacePieceStrategy::PreserveExisting {
if let Some(piece) = existing_piece {
return Err(PlacePieceError::ExisitingPiece { piece, square });
}
}
let color = piece.color;
let shape = piece.shape;
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)
}
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.mailbox.remove(square);
Some(piece)
} else {
None
}
}
}
impl Hash for PieceSet {
fn hash<H: Hasher>(&self, state: &mut H) {
self.color_occupancy.hash(state);
self.shape_occupancy.hash(state);
}
}
impl PartialEq for PieceSet {
fn eq(&self, other: &Self) -> bool {
self.color_occupancy == other.color_occupancy
&& self.shape_occupancy == other.shape_occupancy
}
}

View file

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

View file

@ -1,77 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_core::{Piece, Square};
use std::iter::FromIterator;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct Mailbox([Option<Piece>; Square::NUM]);
impl Mailbox {
pub fn get(&self, square: Square) -> Option<Piece> {
self.0[square as usize]
}
pub fn set(&mut self, piece: Piece, square: Square) {
self.0[square as usize] = Some(piece);
}
pub fn remove(&mut self, square: Square) {
self.0[square as usize] = None;
}
pub fn iter(&self) -> impl Iterator<Item = (Square, Piece)> {
Square::ALL
.into_iter()
.zip(self.0)
.filter_map(|(square, piece)| piece.map(|piece| (square, piece)))
}
}
impl Default for Mailbox {
fn default() -> Self {
Self([None; Square::NUM])
}
}
impl From<[Option<Piece>; Square::NUM]> for Mailbox {
fn from(value: [Option<Piece>; Square::NUM]) -> Self {
Mailbox(value)
}
}
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)| {
mailbox.set(piece, square);
mailbox
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use chessfriend_core::piece;
use std::collections::HashSet;
#[test]
fn iter_returns_all_pieces() {
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);
mailbox.set(piece!(Black Pawn), Square::F6);
let pieces = mailbox.iter().collect::<HashSet<_>>();
assert_eq!(
pieces,
HashSet::from([
(Square::C3, piece!(White Queen)),
(Square::H8, piece!(White Rook)),
(Square::E4, piece!(Black Bishop)),
(Square::F6, piece!(Black Pawn)),
])
);
}
}

View file

@ -0,0 +1,73 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{File, Position, Rank, Square};
use std::{fmt, fmt::Write};
pub struct DiagramFormatter<'a, PosC>(&'a Position<PosC>);
impl<'a, PosC> DiagramFormatter<'a, PosC> {
pub fn new(position: &'a Position<PosC>) -> DiagramFormatter<PosC> {
DiagramFormatter(position)
}
}
impl<'a, PosC> fmt::Display for DiagramFormatter<'a, PosC> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut output = String::new();
output.push_str(" +-----------------+\n");
for rank in Rank::ALL.iter().rev() {
write!(output, "{} | ", rank)?;
for file in File::ALL.iter() {
let square = Square::from_file_rank(*file, *rank);
match self.0.piece_on_square(square) {
Some(placed_piece) => write!(output, "{} ", placed_piece.piece())?,
None => output.push_str(". "),
}
}
output.push_str("|\n");
}
output.push_str(" +-----------------+\n");
output.push_str(" a b c d e f g h\n");
write!(f, "{}", output)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::piece::{Color, Piece};
use crate::Position;
#[test]
fn empty() {
let pos = Position::empty();
let diagram = DiagramFormatter(&pos);
println!("{}", diagram);
}
#[test]
fn one_king() {
let mut pos = Position::empty();
pos.place_piece(
Piece::king(Color::Black),
Square::from_algebraic_str("h3").expect("h3"),
)
.expect("Unable to place piece");
let diagram = DiagramFormatter(&pos);
println!("{}", diagram);
}
#[test]
fn starting() {
let pos = Position::starting();
let diagram = DiagramFormatter(&pos);
println!("{}", diagram);
}
}

View file

@ -0,0 +1,86 @@
// Eryn Wells <eryn@erynwells.me>
use super::position::BoardSide;
use crate::piece::Color;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(super) struct Flags(u8);
impl Flags {
#[inline]
pub(super) fn player_has_right_to_castle_flag_offset<C: Color>(
color: C,
side: BoardSide,
) -> u8 {
((color as u8) << 1) + side as u8
}
pub(super) fn player_has_right_to_castle<C: Color>(&self, color: C, side: BoardSide) -> bool {
(self.0 & (1 << Self::player_has_right_to_castle_flag_offset(color, side))) != 0
}
#[cfg(test)]
pub(super) fn set_player_has_right_to_castle_flag<C: Color>(
&mut self,
color: C,
side: BoardSide,
) {
self.0 |= 1 << Self::player_has_right_to_castle_flag_offset(color, side);
}
#[cfg(test)]
pub(super) fn clear_player_has_right_to_castle_flag<C: Color>(
&mut self,
color: C,
side: BoardSide,
) {
self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, side));
}
}
impl Default for Flags {
fn default() -> Self {
Flags(0b00001111)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Color;
#[test]
fn castle_flags() {
assert_eq!(
Flags::player_has_right_to_castle_flag_offset(Color::White, BoardSide::King),
0
);
assert_eq!(
Flags::player_has_right_to_castle_flag_offset(Color::White, BoardSide::Queen),
1
);
assert_eq!(
Flags::player_has_right_to_castle_flag_offset(Color::Black, BoardSide::King),
2
);
assert_eq!(
Flags::player_has_right_to_castle_flag_offset(Color::Black, BoardSide::Queen),
3
);
}
#[test]
fn defaults() {
let mut flags: Flags = Default::default();
assert!(flags.player_has_right_to_castle(Color::White, BoardSide::King));
assert!(flags.player_has_right_to_castle(Color::White, BoardSide::Queen));
assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::King));
assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::Queen));
flags.clear_player_has_right_to_castle_flag(Color::White, BoardSide::Queen);
assert!(flags.player_has_right_to_castle(Color::White, BoardSide::King));
assert!(!flags.player_has_right_to_castle(Color::White, BoardSide::Queen));
assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::King));
assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::Queen));
}
}

12
board/src/position/mod.rs Normal file
View file

@ -0,0 +1,12 @@
// Eryn Wells <eryn@erynwells.me>
mod diagram_formatter;
mod flags;
mod pieces;
mod position;
pub use diagram_formatter::DiagramFormatter;
pub use pieces::Pieces;
pub use position::Position;
pub(crate) use position::BoardSide;

View file

@ -0,0 +1,141 @@
// Eryn Wells <eryn@erynwells.me>
use super::Position;
use crate::bitboard::BitBoard;
use crate::piece::*;
use crate::Square;
pub struct Pieces<'a, PosC, C: Color> {
color: C,
position: &'a Position<PosC>,
current_shape: Option<dyn Shape>,
shape_iterator: std::array::IntoIter<dyn Shape, 6>,
square_iterator: Option<Box<dyn Iterator<Item = Square>>>,
}
impl<'a, PosC, C> Pieces<'a, PosC, C> {
pub(crate) fn new(position: &Position<PosC>, color: C) -> Pieces<PosC, C> {
Pieces {
color,
position,
current_shape: None,
shape_iterator: [Pawn, Knight, Bishop, Rook, Queen, King].into_iter(),
square_iterator: None,
}
}
}
impl<'a, PosC, C, S> Iterator for Pieces<'a, PosC, C>
where
C: Color,
S: Shape,
{
type Item = PlacedPiece<C, S>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(square_iterator) = &mut self.square_iterator {
if let (Some(square), Some(shape)) = (square_iterator.next(), self.current_shape) {
return Some(PlacedPiece::new(Piece::new(self.color, shape), square));
}
}
let mut current_shape: Option<Shape> = None;
let mut next_nonempty_bitboard: Option<BitBoard> = None;
while let Some(shape) = self.shape_iterator.next() {
let piece = Piece::new(self.color, *shape);
let bitboard = self.position.bitboard_for_piece(piece);
if bitboard.is_empty() {
continue;
}
next_nonempty_bitboard = Some(bitboard);
current_shape = Some(*shape);
break;
}
if let (Some(bitboard), Some(shape)) = (next_nonempty_bitboard, current_shape) {
let mut square_iterator = bitboard.occupied_squares();
let mut next_placed_piece: Option<PlacedPiece> = None;
if let Some(square) = square_iterator.next() {
next_placed_piece = Some(PlacedPiece::new(Piece::new(self.color, shape), square));
}
self.square_iterator = Some(Box::new(square_iterator));
self.current_shape = Some(shape);
return next_placed_piece;
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::piece::{Color, Piece, Shape};
use crate::Position;
use crate::Square;
use std::collections::HashSet;
fn place_piece_in_position<PosC, C, S>(
pos: &mut Position<PosC>,
piece: Piece<C, S>,
sq: Square,
) {
pos.place_piece(piece, sq)
.expect("Unable to place {piece:?} queen on {sq}");
}
#[test]
fn empty() {
let pos = Position::empty();
let mut pieces = pos.pieces(Color::White);
assert_eq!(pieces.next(), None);
}
#[test]
fn one() {
let sq = Square::E4;
let mut pos = Position::empty();
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Queen), Square::E4);
println!("{:?}", &pos);
let mut pieces = pos.pieces(Color::White);
assert_eq!(
pieces.next(),
Some(PlacedPiece::new(Piece::new(Color::White, Shape::Queen), sq))
);
assert_eq!(pieces.next(), None);
}
#[test]
fn multiple_pieces() {
let mut pos = Position::empty();
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Queen), Square::E4);
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::King), Square::E1);
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Pawn), Square::B2);
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Pawn), Square::C2);
println!("{:?}", &pos);
let expected_placed_pieces = HashSet::from([
PlacedPiece::new(Piece::new(Color::White, Shape::Queen), Square::E4),
PlacedPiece::new(Piece::new(Color::White, Shape::King), Square::E1),
PlacedPiece::new(Piece::new(Color::White, Shape::Pawn), Square::B2),
PlacedPiece::new(Piece::new(Color::White, Shape::Pawn), Square::C2),
]);
let placed_pieces = HashSet::from_iter(pos.pieces(Color::White));
assert_eq!(placed_pieces, expected_placed_pieces);
}
}

View file

@ -0,0 +1,332 @@
// Eryn Wells <eryn@erynwells.me>
use super::{flags::Flags, Pieces};
use crate::{
//moves::Moves,
piece::{Color, Piece, PiecePlacementError, PlacedPiece, Shape},
BitBoard,
Square,
};
use std::fmt;
use std::fmt::Write;
/// A lateral side of the board relative to the player. Kingside is the side the
/// player's king starts on. Queenside is the side of the board the player's
/// queen starts on.
pub(crate) enum BoardSide {
King,
Queen,
}
#[macro_export]
macro_rules! position {
[$($color:ident $shape:ident on $square:ident,)*] => {
Position::from_iter([
$(piece!($color $shape on $square)),*
].into_iter())
};
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct Position<ColorToMove> {
color_to_move: ColorToMove,
flags: Flags,
/// Composite bitboards for all the pieces of a particular color.
pieces_per_color: [BitBoard; 2],
/// Bitboards representing positions of particular piece types per color.
pieces_per_type: [[BitBoard; 6]; 2],
en_passant_square: Option<Square>,
}
impl<ColorToMove> Position<ColorToMove> {
pub fn empty() -> Position<ColorToMove> {
Position {
color_to_move: crate::piece::White,
flags: Default::default(),
pieces_per_color: [BitBoard::empty(); 2],
pieces_per_type: [
[
BitBoard::empty(),
BitBoard::empty(),
BitBoard::empty(),
BitBoard::empty(),
BitBoard::empty(),
BitBoard::empty(),
],
[
BitBoard::empty(),
BitBoard::empty(),
BitBoard::empty(),
BitBoard::empty(),
BitBoard::empty(),
BitBoard::empty(),
],
],
en_passant_square: None,
}
}
/// Return a starting position.
pub fn starting() -> Position<ColorToMove> {
let white_pieces = [
BitBoard::new(0x00FF000000000000),
BitBoard::new(0x4200000000000000),
BitBoard::new(0x2400000000000000),
BitBoard::new(0x8100000000000000),
BitBoard::new(0x1000000000000000),
BitBoard::new(0x0800000000000000),
];
let black_pieces = [
BitBoard::new(0xFF00),
BitBoard::new(0x0042),
BitBoard::new(0x0024),
BitBoard::new(0x0081),
BitBoard::new(0x0010),
BitBoard::new(0x0008),
];
Position {
color_to_move: crate::piece::White,
flags: Default::default(),
pieces_per_color: [
white_pieces.iter().fold(BitBoard::empty(), |a, b| a | *b),
black_pieces.iter().fold(BitBoard::empty(), |a, b| a | *b),
],
pieces_per_type: [white_pieces, black_pieces],
en_passant_square: None,
}
}
/// Returns true if the player has the right to castle on the given side of
/// the board.
///
/// The right to castle on a particular side of the board is retained as
/// long as the player has not moved their king, or the rook on that side of
/// the board.
///
/// The following requirements must also be met:
///
/// 1. The spaces between the king and rook must be clear
/// 2. The king must not be in check
/// 3. In the course of castling on that side, the king must not pass
/// through a square that an enemy piece can see
pub(crate) fn player_has_right_to_castle<C>(&self, color: C, side: BoardSide) -> bool {
self.flags.player_has_right_to_castle(color, side)
}
pub fn place_piece<C, S>(
&mut self,
piece: Piece<C, S>,
square: Square,
) -> Result<(), PiecePlacementError> {
let type_bb = self.bitboard_for_piece_mut(piece);
if type_bb.has_piece_at(square) {
return Err(PiecePlacementError::ExistsOnSquare);
}
type_bb.place_piece_at(square);
let color_bb = &mut self.bitboard_for_color_mut(piece.color());
color_bb.place_piece_at(square);
Ok(())
}
// pub fn move_generator<C>(&self, color: C) -> Moves {
// Moves::new(self, color)
// }
pub(crate) fn bitboard_for_piece<C, S>(&self, piece: Piece<C, S>) -> BitBoard {
self.pieces_per_type[piece.color() as usize][piece.shape() as usize]
}
fn bitboard_for_piece_mut<C, S>(&mut self, piece: Piece<C, S>) -> &mut BitBoard {
&mut self.pieces_per_type[piece.color() as usize][piece.shape() as usize]
}
pub(crate) fn bitboard_for_color<C>(&self, color: C) -> BitBoard {
self.pieces_per_color[color as usize]
}
fn bitboard_for_color_mut<C>(&mut self, color: C) -> &mut BitBoard {
&mut self.pieces_per_color[color as usize]
}
pub fn piece_on_square<C, S>(&self, sq: Square) -> Option<PlacedPiece<C, S>> {
for color in Color::iter() {
for shape in Shape::iter() {
let piece = Piece::new(color, *shape);
let bb = self.bitboard_for_piece(piece);
if bb.has_piece_at(sq) {
return Some(PlacedPiece::new(piece, sq));
}
}
}
None
}
pub fn pieces<C>(&self, color: C) -> Pieces<ColorToMove, C> {
Pieces::new(&self, color)
}
pub fn king<C>(&self, color: C) -> PlacedPiece<C, crate::piece::King> {
let king = Piece::king(color);
let bitboard = self.bitboard_for_piece(king);
let square = bitboard.occupied_squares().next().unwrap();
PlacedPiece::new(king, square)
}
pub fn is_king_in_check(&self) -> bool {
false
}
}
impl<ColorToMove> Position<ColorToMove> {
/// Return a BitBoard representing the set of squares containing a piece.
#[inline]
pub(crate) fn occupied_squares(&self) -> BitBoard {
self.pieces_per_color[Color::White as usize] | self.pieces_per_color[Color::Black as usize]
}
/// A BitBoard representing the set of empty squares. This set is the
/// inverse of `occupied_squares`.
#[inline]
pub(crate) fn empty_squares(&self) -> BitBoard {
!self.occupied_squares()
}
/// A BitBoard representing squares occupied by the current player's pieces.
pub(crate) fn friendly_pieces(&self) -> BitBoard {
self.bitboard_for_color(self.color_to_move)
}
/// A BitBoard representing squares occupied by the opposing player's pieces.
pub(crate) fn opposing_pieces(&self) -> BitBoard {
self.bitboard_for_color(self.color_to_move.other())
}
/// If the previous move create an en passant elibile square, return `Some`.
/// Otherwise, return `None`.
pub(crate) fn en_passant_square(&self) -> Option<Square> {
self.en_passant_square
}
}
impl<ColorToMove> Position<ColorToMove> {
fn sight_of_pieces_of_color<C>(&self, color: C) -> BitBoard {
self.pieces(color)
.map(|placed_piece| {
self.sight_of_piece(placed_piece)
.sight(placed_piece.square(), self)
})
.fold(BitBoard::empty(), std::ops::BitOr::bitor)
}
fn sight_of_piece<C, S>(&self, placed_piece: PlacedPiece<C, S>) -> BitBoard {
placed_piece.piece().sight()
}
}
impl<ColorToMove> Default for Position<ColorToMove> {
fn default() -> Self {
Self::empty()
}
}
impl<ColorToMove, C, S> FromIterator<PlacedPiece<C, S>> for Position<ColorToMove> {
fn from_iter<T: IntoIterator<Item = PlacedPiece<C, S>>>(iter: T) -> Self {
let mut position = Position::empty();
for placed_piece in iter {
_ = position.place_piece(placed_piece.piece(), placed_piece.square());
}
position
}
}
impl<ColorToMove> fmt::Debug for Position<ColorToMove> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut output = String::new();
output.push_str("Position {\n");
write!(output, " color_to_move: {:?},\n", self.color_to_move)?;
output.push_str(" pieces_per_color: [\n");
for bb in self.pieces_per_color {
write!(output, " {bb:?},\n")?;
}
output.push_str(" ],\n");
output.push_str(" pieces_per_type: [\n");
for color_bbs in self.pieces_per_type {
output.push_str(" [\n");
for bb in color_bbs {
write!(output, " {bb:?},\n")?;
}
output.push_str(" ],\n");
}
output.push_str(" ],\n}");
write!(f, "{}", output)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{piece::Shape, position::DiagramFormatter};
#[test]
fn place_piece() {
let mut position = Position::empty();
let piece = Piece::new(Color::White, Shape::Queen);
let square = Square::E4;
position
.place_piece(piece, square)
.expect("Unable to place white queen on e4");
assert_eq!(
position.bitboard_for_color(piece.color()).clone(),
BitBoard::new(1 << 28)
);
assert_eq!(
position.bitboard_for_piece(piece).clone(),
BitBoard::new(1 << 28)
);
position
.place_piece(piece, square)
.expect_err("Placed white queen on e4 a second time?!");
}
#[test]
fn king_is_not_in_check() {
let position = position![
White King on E4,
Black Rook on D8,
];
println!("{}", DiagramFormatter::new(&position));
assert!(!position.is_king_in_check());
}
#[test]
fn king_is_in_check() {
let position = position![
White King on E4,
Black Rook on E8,
];
println!("{}", DiagramFormatter::new(&position));
assert!(position.is_king_in_check());
}
}

View file

@ -1,423 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
//! Defines routines for computing sight of a piece. Sight is the set of squares
//! that a piece can see. In other words, it's the set of squares attacked or
//! controled by a piece.
//!
//! These functions use some common terms to describe arguments.
//!
//! occupancy
//! : The set of occupied squares on the board.
//!
//! vacancy
//! : The set of empty squares on the board, `!occpuancy`.
//!
//! blockers
//! : The set of squares occupied by friendly pieces that block moves to that
//! square and beyond.
use crate::Board;
use chessfriend_bitboard::{BitBoard, IterationDirection};
use chessfriend_core::{Color, Direction, Piece, Shape, Square};
use std::ops::BitOr;
impl Board {
/// Compute sight of the piece on the given square.
pub fn sight_piece(&self, square: Square) -> BitBoard {
if let Some(piece) = self.get_piece(square) {
piece.sight(square, self)
} else {
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)
}
/// A [`BitBoard`] of all squares the given color can see.
pub fn friendly_sight(&self, color: Color) -> BitBoard {
// 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)
}
pub fn active_color_opposing_sight(&self) -> BitBoard {
self.opposing_sight(self.active_color())
}
/// A [`BitBoard`] of all squares visible by colors that oppose the given color.
pub fn opposing_sight(&self, color: Color) -> BitBoard {
// TODO: Probably want to implement a caching layer here.
Color::ALL
.into_iter()
.filter_map(|c| {
if c == color {
None
} else {
Some(self.friendly_sight(c))
}
})
.fold(BitBoard::EMPTY, BitOr::bitor)
}
}
pub trait Sight {
fn sight(&self, square: Square, board: &Board) -> BitBoard;
}
impl Sight for Piece {
fn sight(&self, square: Square, board: &Board) -> BitBoard {
let occupancy = board.occupancy();
let info = SightInfo {
square,
occupancy,
friendly_occupancy: board.friendly_occupancy(self.color),
};
match self.shape {
Shape::Pawn => {
let en_passant_square: BitBoard = board.en_passant_target().into();
match self.color {
Color::White => white_pawn_sight(&info, en_passant_square),
Color::Black => black_pawn_sight(&info, en_passant_square),
}
}
Shape::Knight => knight_sight(&info),
Shape::Bishop => bishop_sight(&info),
Shape::Rook => rook_sight(&info),
Shape::Queen => queen_sight(&info),
Shape::King => king_sight(&info),
}
}
}
macro_rules! sight_method {
($name:ident) => {
pub fn $name(&self, square: Square, color: Option<Color>) -> BitBoard {
let color = self.unwrap_color(color);
let info = SightInfo {
square,
occupancy: self.occupancy(),
friendly_occupancy: self.friendly_occupancy(color),
};
$name(&info)
}
};
}
impl Board {
sight_method!(bishop_sight);
sight_method!(rook_sight);
sight_method!(queen_sight);
sight_method!(king_sight);
}
struct SightInfo {
square: Square,
occupancy: BitBoard,
friendly_occupancy: BitBoard,
}
/// 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;
let pawn: BitBoard = info.square.into();
let pawn = pawn.shift_north_west_one() | pawn.shift_north_east_one();
pawn & possible_squares
}
fn black_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard {
let possible_squares = !info.friendly_occupancy | en_passant_square;
let pawn: BitBoard = info.square.into();
let pawn = pawn.shift_south_west_one() | pawn.shift_south_east_one();
pawn & possible_squares
}
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);
sight
}
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);
sight
}
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);
sight
}
fn king_sight(info: &SightInfo) -> BitBoard {
BitBoard::king_moves(info.square)
}
#[cfg(test)]
mod tests {
use crate::test_board;
use chessfriend_bitboard::bitboard;
use chessfriend_core::piece;
macro_rules! sight_test {
($test_name:ident, $position:expr, $piece:expr, $square:expr, $bitboard:expr) => {
#[test]
fn $test_name() {
use chessfriend_core::Square;
use $crate::sight::Sight;
let pos = $position;
let piece = $piece;
let sight = piece.sight($square, &pos);
assert_eq!(sight, $bitboard);
}
};
($test_name:ident, $piece:expr, $square:expr, $bitboard:expr) => {
sight_test! {
$test_name,
$crate::Board::empty(Some($crate::test_zobrist!())),
$piece,
$square,
$bitboard
}
};
}
#[test]
fn friendly_sight() {
let pos = test_board!(
White King on E4,
);
let sight = pos.sight_active();
assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]);
}
#[test]
fn opposing_sight() {
let pos = test_board!(
White King on E4,
Black Rook on E7,
);
let sight = pos.active_color_opposing_sight();
assert_eq!(sight, bitboard![A7 B7 C7 D7 F7 G7 H7 E8 E6 E5 E4]);
}
// #[test]
// fn pawns_and_knights_cannot_make_rays() {
// assert_eq!(Shape::Pawn.ray_to_square(Square::F7, Square::E8), None);
// assert_eq!(Shape::Knight.ray_to_square(Square::F6, Square::E8), None);
// }
mod pawn {
use crate::{sight::Sight, test_board};
use chessfriend_bitboard::{BitBoard, bitboard};
use chessfriend_core::{Square, piece};
sight_test!(e4_pawn, piece!(White Pawn), Square::E4, bitboard![D5 F5]);
sight_test!(
e4_pawn_one_blocker,
test_board![
White Bishop on D5,
White Pawn on E4,
],
piece!(White Pawn),
Square::E4,
bitboard!(F5)
);
#[test]
fn e4_pawn_two_blocker() {
let pos = test_board!(
White Bishop on D5,
White Queen on F5,
White Pawn on E4,
);
let piece = piece!(White Pawn);
let sight = piece.sight(Square::E4, &pos);
assert_eq!(sight, BitBoard::EMPTY);
}
#[test]
fn e4_pawn_capturable() {
let pos = test_board!(
Black Bishop on D5,
White Queen on F5,
White Pawn on E4,
);
let piece = piece!(White Pawn);
let sight = piece.sight(Square::E4, &pos);
assert_eq!(sight, bitboard![D5]);
}
#[test]
fn e5_en_passant() {
let pos = test_board!(White, [
White Pawn on E5,
Black Pawn on D5,
], D6);
let piece = piece!(White Pawn);
let sight = piece.sight(Square::E5, &pos);
assert_eq!(sight, bitboard!(D6 F6));
}
}
#[macro_use]
mod knight {
use chessfriend_bitboard::bitboard;
use chessfriend_core::piece;
sight_test!(
f6_knight,
piece!(Black Knight),
Square::F6,
bitboard![H7 G8 E8 D7 D5 E4 G4 H5]
);
}
mod bishop {
use super::*;
sight_test!(
c2_bishop,
piece!(Black Bishop),
Square::C2,
bitboard![D1 B3 A4 B1 D3 E4 F5 G6 H7]
);
// #[test]
// fn ray_to_square() {
// let generated_ray = Shape::Bishop.ray_to_square(Square::C5, Square::E7);
// let expected_ray = bitboard![D6 E7];
// assert_eq!(generated_ray, Some(expected_ray));
// }
}
mod rook {
use super::*;
use crate::test_board;
sight_test!(
g3_rook,
piece!(White Rook),
Square::G3,
bitboard![G1 G2 G4 G5 G6 G7 G8 A3 B3 C3 D3 E3 F3 H3]
);
sight_test!(
e4_rook_with_e1_white_king_e7_black_king,
test_board![
White Rook on E4,
White King on E2,
Black King on E7,
],
piece!(White Rook),
Square::E4,
bitboard![A4 B4 C4 D4 F4 G4 H4 E2 E3 E5 E6 E7]
);
// #[test]
// fn ray_to_square() {
// let generated_ray = Shape::Rook.ray_to_square(Square::C2, Square::C6);
// let expected_ray = bitboard![C3 C4 C5 C6];
// assert_eq!(generated_ray, Some(expected_ray));
// let generated_ray = Shape::Rook.ray_to_square(Square::D2, Square::H2);
// let expected_ray = bitboard![E2 F2 G2 H2];
// assert_eq!(generated_ray, Some(expected_ray));
// let generated_ray = Shape::Rook.ray_to_square(Square::G6, Square::B6);
// let expected_ray = bitboard![B6 C6 D6 E6 F6];
// assert_eq!(generated_ray, Some(expected_ray));
// let generated_ray = Shape::Rook.ray_to_square(Square::A6, Square::A3);
// let expected_ray = bitboard![A5 A4 A3];
// assert_eq!(generated_ray, Some(expected_ray));
// }
}
mod king {
use chessfriend_bitboard::bitboard;
use chessfriend_core::piece;
sight_test!(
e1_king,
piece!(White King),
Square::E1,
bitboard![D1 D2 E2 F2 F1]
);
}
}

334
board/src/square.rs Normal file
View file

@ -0,0 +1,334 @@
// Eryn Wells <eryn@erynwells.me>
use std::{fmt, str::FromStr};
pub enum Direction {
North,
NorthWest,
West,
SouthWest,
South,
SouthEast,
East,
NorthEast,
}
#[derive(Debug)]
pub struct ParseFileError;
#[derive(Debug)]
pub struct ParseSquareError;
#[derive(Debug)]
pub struct SquareOutOfBoundsError;
macro_rules! coordinate_enum {
($name: ident, $($variant:ident),*) => {
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum $name {
$($variant), *
}
impl $name {
pub const NUM: usize = [$(Self::$variant), *].len();
pub const ALL: [Self; Self::NUM] = [$(Self::$variant), *];
#[inline]
pub(crate) fn from_index(index: usize) -> Self {
assert!(
index < Self::NUM,
"Index {} out of bounds for {}.",
index,
stringify!($name)
);
Self::try_index(index).unwrap()
}
pub fn try_index(index: usize) -> Option<Self> {
$(
#[allow(non_upper_case_globals)]
const $variant: usize = $name::$variant as usize;
)*
#[allow(non_upper_case_globals)]
match index {
$($variant => Some($name::$variant),)*
_ => None,
}
}
}
}
}
#[rustfmt::skip]
coordinate_enum!(Rank,
One, Two, Three, Four, Five, Six, Seven, Eight
);
#[rustfmt::skip]
coordinate_enum!(File,
A, B, C, D, E, F, G, H
);
#[rustfmt::skip]
coordinate_enum!(Square,
A1, B1, C1, D1, E1, F1, G1, H1,
A2, B2, C2, D2, E2, F2, G2, H2,
A3, B3, C3, D3, E3, F3, G3, H3,
A4, B4, C4, D4, E4, F4, G4, H4,
A5, B5, C5, D5, E5, F5, G5, H5,
A6, B6, C6, D6, E6, F6, G6, H6,
A7, B7, C7, D7, E7, F7, G7, H7,
A8, B8, C8, D8, E8, F8, G8, H8
);
impl Into<char> for File {
fn into(self) -> char {
('a' as u8 + self as u8) as char
}
}
impl TryFrom<char> for File {
type Error = ParseFileError;
fn try_from(value: char) -> Result<Self, Self::Error> {
let lowercase_value = value.to_ascii_lowercase();
for file in File::ALL.iter() {
if lowercase_value == (*file).into() {
return Ok(*file);
}
}
Err(ParseFileError)
}
}
impl fmt::Display for File {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Into::<char>::into(*self).to_uppercase())
}
}
impl Into<char> for Rank {
fn into(self) -> char {
('1' as u8 + self as u8) as char
}
}
impl TryFrom<char> for Rank {
type Error = ParseFileError;
fn try_from(value: char) -> Result<Self, Self::Error> {
let lowercase_value = value.to_ascii_lowercase();
for rank in Self::ALL.iter().cloned() {
if lowercase_value == rank.into() {
return Ok(rank);
}
}
Err(ParseFileError)
}
}
impl fmt::Display for Rank {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Into::<char>::into(*self))
}
}
impl Square {
#[inline]
pub fn from_file_rank(file: File, rank: Rank) -> Square {
Self::from_index((rank as usize) << 3 | file as usize)
}
#[inline]
pub fn file(self) -> File {
File::from_index(self as usize & 0b000111)
}
#[inline]
pub fn rank(self) -> Rank {
Rank::from_index(self as usize >> 3)
}
}
impl Square {
pub(crate) const KING_STARTING_SQUARES: [Square; 2] = [Square::E1, Square::E8];
pub(crate) const KING_CASTLE_TARGET_SQUARES: [[Square; 2]; 2] =
[[Square::C1, Square::G1], [Square::C8, Square::G8]];
pub fn from_algebraic_str(s: &str) -> Result<Square, ParseSquareError> {
s.parse()
}
pub fn neighbor(self, direction: Direction) -> Option<Square> {
match direction {
Direction::North => Square::try_index(self as usize + 8),
Direction::NorthWest => {
if self.rank() != Rank::Eight {
Square::try_index(self as usize + 7)
} else {
None
}
}
Direction::West => {
if self.file() != File::A {
Square::try_index(self as usize - 1)
} else {
None
}
}
Direction::SouthWest => {
if self.rank() != Rank::One {
Square::try_index(self as usize - 9)
} else {
None
}
}
Direction::South => {
if self.rank() != Rank::One {
Square::try_index(self as usize - 8)
} else {
None
}
}
Direction::SouthEast => {
if self.rank() != Rank::One {
Square::try_index(self as usize - 7)
} else {
None
}
}
Direction::East => {
if self.file() != File::H {
Square::try_index(self as usize + 1)
} else {
None
}
}
Direction::NorthEast => Square::try_index(self as usize + 9),
}
}
}
impl FromStr for Square {
type Err = ParseSquareError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
let file: File = chars
.next()
.and_then(|c| c.try_into().ok())
.ok_or(ParseSquareError)?;
let rank: Rank = chars
.next()
.and_then(|c| c.try_into().ok())
.ok_or(ParseSquareError)?;
if !chars.next().is_none() {
return Err(ParseSquareError);
}
Ok(Square::from_file_rank(file, rank))
}
}
impl fmt::Display for Square {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}",
('a' as u8 + self.file() as u8) as char,
self.rank() as usize + 1
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn good_algebraic_input() {
let sq = Square::from_algebraic_str("a4").expect("Failed to parse 'a4' square");
assert_eq!(sq.file(), File::A);
assert_eq!(sq.rank(), Rank::Four);
let sq = Square::from_algebraic_str("B8").expect("Failed to parse 'B8' square");
assert_eq!(sq.file(), File::B);
assert_eq!(sq.rank(), Rank::Eight);
let sq = Square::from_algebraic_str("e4").expect("Failed to parse 'B8' square");
assert_eq!(sq.file(), File::E);
assert_eq!(sq.rank(), Rank::Four);
}
#[test]
fn bad_algebraic_input() {
Square::from_algebraic_str("a0").expect_err("Got valid Square for 'a0'");
Square::from_algebraic_str("j3").expect_err("Got valid Square for 'j3'");
Square::from_algebraic_str("a11").expect_err("Got valid Square for 'a11'");
Square::from_algebraic_str("b-1").expect_err("Got valid Square for 'b-1'");
Square::from_algebraic_str("a 1").expect_err("Got valid Square for 'a 1'");
Square::from_algebraic_str("").expect_err("Got valid Square for ''");
}
#[test]
fn from_index() {
let sq = Square::try_index(4).expect("Unable to get Square from index");
assert_eq!(sq.file(), File::E);
assert_eq!(sq.rank(), Rank::One);
let sq = Square::try_index(28).expect("Unable to get Square from index");
assert_eq!(sq.file(), File::E);
assert_eq!(sq.rank(), Rank::Four);
}
#[test]
fn valid_neighbors() {
let sq = Square::E4;
assert_eq!(sq.neighbor(Direction::North), Some(Square::E5));
assert_eq!(sq.neighbor(Direction::NorthEast), Some(Square::F5));
assert_eq!(sq.neighbor(Direction::East), Some(Square::F4));
assert_eq!(sq.neighbor(Direction::SouthEast), Some(Square::F3));
assert_eq!(sq.neighbor(Direction::South), Some(Square::E3));
assert_eq!(sq.neighbor(Direction::SouthWest), Some(Square::D3));
assert_eq!(sq.neighbor(Direction::West), Some(Square::D4));
assert_eq!(sq.neighbor(Direction::NorthWest), Some(Square::D5));
}
#[test]
fn invalid_neighbors() {
let sq = Square::A1;
assert!(sq.neighbor(Direction::West).is_none());
assert!(sq.neighbor(Direction::SouthWest).is_none());
assert!(sq.neighbor(Direction::South).is_none());
let sq = Square::H1;
assert!(sq.neighbor(Direction::East).is_none());
assert!(sq.neighbor(Direction::SouthEast).is_none());
assert!(sq.neighbor(Direction::South).is_none());
let sq = Square::A8;
assert!(sq.neighbor(Direction::North).is_none());
assert!(sq.neighbor(Direction::NorthWest).is_none());
assert!(sq.neighbor(Direction::West).is_none());
let sq = Square::H8;
assert!(sq.neighbor(Direction::North).is_none());
assert!(sq.neighbor(Direction::NorthEast).is_none());
assert!(sq.neighbor(Direction::East).is_none());
}
#[test]
fn display() {
assert_eq!(format!("{}", Square::C5), "c5");
assert_eq!(format!("{}", Square::A1), "a1");
assert_eq!(format!("{}", Square::H8), "h8");
}
}

View file

@ -1,234 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
//! This module implements facilities for computing hash values of board
//! positions via the Zobrist hashing algorithm.
//!
//! ## See Also
//!
//! * The Chess Programming Wiki page on [Zobrist Hashing][1]
//!
//! [1]: https://www.chessprogramming.org/Zobrist_Hashing
use crate::{castle, Board};
use chessfriend_core::{random::RandomNumberGenerator, Color, File, Piece, Shape, Square};
use rand::Fill;
use std::sync::Arc;
const NUM_CASTLING_RIGHTS_VALUES: usize = 16;
type HashValue = u64;
type SquarePieceValues = [[[HashValue; Color::NUM]; Shape::NUM]; Square::NUM];
type CastlingRightsValues = [HashValue; NUM_CASTLING_RIGHTS_VALUES];
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ZobristHash {
// TODO: Keep this field in mind if ChessFriend grows threads. It may also
// need a Mutex<>.
state: Arc<ZobristState>,
/// The current hash value.
hash_value: HashValue,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ZobristState {
square_piece_values: SquarePieceValues,
black_to_move_value: HashValue,
castling_rights_values: CastlingRightsValues,
en_passant_file_values: [HashValue; File::NUM],
}
impl ZobristState {
#[must_use]
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
let square_piece_values = {
let mut values = [[[0; Color::NUM]; Shape::NUM]; Square::NUM];
for square in Square::ALL {
for shape in Shape::ALL {
for color in Color::ALL {
values[square as usize][shape as usize][color as usize] = rng.next_u64();
}
}
}
values
};
let mut castling_rights_values: CastlingRightsValues = [0; NUM_CASTLING_RIGHTS_VALUES];
castling_rights_values.fill(rng.rand());
let mut en_passant_file_values = [0; File::NUM];
en_passant_file_values.fill(rng.rand());
Self {
square_piece_values,
black_to_move_value: rng.next_u64(),
castling_rights_values,
en_passant_file_values,
}
}
}
impl ZobristHash {
#[must_use]
pub fn new(state: Arc<ZobristState>) -> Self {
Self {
state,
hash_value: 0,
}
}
#[must_use]
pub fn hash_value(&self) -> HashValue {
self.hash_value
}
pub(crate) fn set_hash_value(&mut self, value: HashValue) {
self.hash_value = value;
}
#[must_use]
pub fn state(&self) -> Arc<ZobristState> {
self.state.clone()
}
#[must_use]
pub fn compute_board_hash(board: &Board, state: &ZobristState) -> HashValue {
let mut hash_value: HashValue = 0;
for (square, piece) in board.iter() {
hash_value ^= state.square_piece_values[square as usize][piece.shape as usize]
[piece.color as usize];
}
if board.active_color() == Color::Black {
hash_value ^= state.black_to_move_value;
}
if let Some(square) = board.en_passant_target() {
hash_value ^= state.en_passant_file_values[square.file().as_index()];
}
hash_value
}
pub fn recompute_hash(&mut self, board: &Board) -> HashValue {
self.hash_value = Self::compute_board_hash(board, self.state.as_ref());
self.hash_value
}
pub fn update_adding_piece(&mut self, square: Square, piece: Piece) -> HashValue {
self.hash_value ^= self.xor_piece_square_operand(square, piece.shape, piece.color);
self.hash_value
}
pub fn update_removing_piece(&mut self, square: Square, piece: Piece) -> HashValue {
self.hash_value ^= self.xor_piece_square_operand(square, piece.shape, piece.color);
self.hash_value
}
pub fn update_setting_active_color(&mut self, _color: Color) -> HashValue {
self.hash_value ^= self.state.black_to_move_value;
self.hash_value
}
pub fn update_modifying_castling_rights(
&mut self,
new_rights: castle::Rights,
old_rights: castle::Rights,
) -> HashValue {
let state = self.state.as_ref();
self.hash_value ^= state.castling_rights_values[new_rights.as_index()];
self.hash_value ^= state.castling_rights_values[old_rights.as_index()];
self.hash_value
}
pub fn update_setting_en_passant_target(
&mut self,
old_target: Option<Square>,
new_target: Option<Square>,
) -> HashValue {
let state = self.state.as_ref();
if let Some(old_target) = old_target {
self.hash_value ^= state.en_passant_file_values[old_target.file().as_index()];
}
if let Some(new_target) = new_target {
self.hash_value ^= state.en_passant_file_values[new_target.file().as_index()];
}
self.hash_value
}
fn xor_piece_square_operand(&self, square: Square, shape: Shape, color: Color) -> HashValue {
let square = square as usize;
let shape = shape as usize;
let color = color as usize;
self.state.square_piece_values[square][shape][color]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_state() -> ZobristState {
let mut rng = RandomNumberGenerator::default();
ZobristState::new(&mut rng)
}
#[test]
fn hash_empty_board_ai_claude() {
let state = test_state();
let board = Board::empty(None);
let hash = ZobristHash::compute_board_hash(&board, &state);
// Empty board with white to move should only contribute active color
assert_eq!(hash, 0);
}
#[test]
fn hash_board_with_black_to_move_ai_claude() {
let state = test_state();
let mut board = Board::empty(None);
board.set_active_color(Color::Black);
let hash = ZobristHash::compute_board_hash(&board, &state);
assert_eq!(hash, state.black_to_move_value);
}
#[test]
fn hash_different_en_passant_files_ai_claude() {
let mut board1 = Board::empty(None);
let mut board2 = Board::empty(None);
// Different file should produce different hash values, even if ranks
// are the same.
board1.set_en_passant_target(Square::A3);
board2.set_en_passant_target(Square::H3);
let state = test_state();
let hash1 = ZobristHash::compute_board_hash(&board1, &state);
let hash2 = ZobristHash::compute_board_hash(&board2, &state);
assert_ne!(hash1, hash2);
}
#[test]
fn hash_en_passant_same_file_different_rank_ai_claude() {
let mut board1 = Board::empty(None);
let mut board2 = Board::empty(None);
// Same file, different ranks should produce same hash (only file matters)
board1.set_en_passant_target(Square::E3);
board2.set_en_passant_target(Square::E6);
let state = test_state();
let hash1 = ZobristHash::compute_board_hash(&board1, &state);
let hash2 = ZobristHash::compute_board_hash(&board2, &state);
assert_eq!(hash1, hash2);
}
}

View file

@ -1,5 +0,0 @@
[package]
name = "chessfriend"
version = "0.1.0"
edition = "2024"

View file

@ -1,10 +0,0 @@
[package]
name = "chessfriend_core"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.9.1"
thiserror = "2"

View file

@ -1,121 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{Direction, score};
use std::fmt;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum Color {
#[default]
White = 0,
Black = 1,
}
impl Color {
/// Number of colors
pub const NUM: usize = 2;
/// Slice of all possible colors
pub const ALL: [Color; Color::NUM] = [Color::White, Color::Black];
pub fn iter() -> std::slice::Iter<'static, Color> {
Color::ALL.iter()
}
#[must_use]
pub fn into_iter() -> std::array::IntoIter<Color, { Self::NUM }> {
Color::ALL.into_iter()
}
/// The other color
#[must_use]
pub const fn other(&self) -> Color {
match self {
Color::White => Color::Black,
Color::Black => Color::White,
}
}
/// "Forward" direction of pawn pushes for this color.
#[must_use]
pub fn push_direction(&self) -> Direction {
match self {
Color::White => Direction::North,
Color::Black => Direction::South,
}
}
#[must_use]
pub const fn next(&self) -> Color {
Self::ALL[((*self as usize) + 1) % Self::NUM]
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Color::White => "white",
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 {
write!(
f,
"{}",
match self {
Color::White => "white",
Color::Black => "black",
},
)
}
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("no matching color for character '{0}'")]
pub struct ColorFromCharError(char);
impl TryFrom<char> for Color {
type Error = ColorFromCharError;
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'w' | 'W' => Ok(Color::White),
'b' | 'B' => Ok(Color::Black),
_ => Err(ColorFromCharError(value)),
}
}
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("no matching color for string")]
pub struct ColorFromStrError;
impl TryFrom<&str> for Color {
type Error = ColorFromStrError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"w" | "white" => Ok(Color::White),
"b" | "black" => Ok(Color::Black),
_ => Err(ColorFromStrError),
}
}
}
impl std::str::FromStr for Color {
type Err = ColorFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::try_from(s.to_lowercase().as_str())
}
}

View file

@ -1,612 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
mod wings;
pub use wings::Wing;
use crate::Color;
use std::{fmt, str::FromStr};
use thiserror::Error;
macro_rules! try_from_integer {
($type:ident, $int_type:ident) => {
impl TryFrom<$int_type> for $type {
type Error = ();
fn try_from(value: $int_type) -> Result<Self, Self::Error> {
#[allow(clippy::cast_possible_truncation)]
Self::try_from(value as u8)
}
}
};
}
macro_rules! coordinate_enum {
($name: ident, [ $($variant:ident),* ]) => {
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum $name {
$($variant), *
}
impl $name {
pub const NUM: usize = [$(Self::$variant), *].len();
pub const ALL: [Self; Self::NUM] = [$(Self::$variant), *];
}
impl TryFrom<u8> for $name {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
let value_usize = value as usize;
if value_usize < Self::NUM {
Ok($name::ALL[value_usize])
} else {
Err(())
}
}
}
try_from_integer!($name, u16);
try_from_integer!($name, u32);
try_from_integer!($name, u64);
}
}
macro_rules! range_bound_struct {
($vis:vis $type:ident, $repr:ty, $max:expr) => {
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
$vis struct $type($repr);
#[allow(dead_code)]
impl $type {
$vis const NUM: usize = $max;
$vis const FIRST: $type = $type(0);
$vis const LAST: $type = $type($max - 1);
}
impl $type {
#[must_use]
$vis fn new(x: $repr) -> Option<Self> {
if x < $max {
Some(Self(x))
} else {
None
}
}
/// Create a new `Self`
///
/// # Safety
///
/// This function does not perform any bounds checking. It should only be called when
/// the input is already known to be within bounds, i.e. when `x >= Self::FIRST && x < Self::LAST`.
#[must_use]
$vis unsafe fn new_unchecked(x: $repr) -> Self {
debug_assert!((Self::FIRST.0..=Self::LAST.0).contains(&x));
Self(x)
}
#[must_use]
$vis const fn as_index(&self) -> usize {
self.0 as usize
}
$vis fn iter(&self) -> impl Iterator<Item = Self> {
(Self::FIRST.0..=Self::LAST.0).map(Self)
}
}
impl From<$type> for $repr {
fn from(x: $type) -> Self {
x.0
}
}
impl TryFrom<$repr> for $type {
type Error = ();
fn try_from(value: $repr) -> Result<Self, Self::Error> {
Self::new(value).ok_or(())
}
}
}
}
coordinate_enum!(
Direction,
[
North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest
]
);
impl Direction {
#[must_use]
pub fn to_offset(&self) -> i8 {
const OFFSETS: [i8; 8] = [8, 9, 1, -7, -8, -9, -1, 7];
OFFSETS[*self as usize]
}
#[must_use]
pub fn opposite(&self) -> Direction {
const OPPOSITES: [Direction; 8] = [
Direction::South,
Direction::SouthEast,
Direction::East,
Direction::NorthEast,
Direction::North,
Direction::NorthWest,
Direction::West,
Direction::SouthWest,
];
OPPOSITES[*self as usize]
}
}
range_bound_struct!(pub File, u8, 8);
impl File {
pub const A: File = File(0);
pub const B: File = File(1);
pub const C: File = File(2);
pub const D: File = File(3);
pub const E: File = File(4);
pub const F: File = File(5);
pub const G: File = File(6);
pub const H: File = File(7);
pub const ALL: [File; File::NUM] = [
File::A,
File::B,
File::C,
File::D,
File::E,
File::F,
File::G,
File::H,
];
}
range_bound_struct!(pub Rank, u8, 8);
#[allow(dead_code)]
impl Rank {
pub const ONE: Rank = Rank(0);
pub const TWO: Rank = Rank(1);
pub const THREE: Rank = Rank(2);
pub const FOUR: Rank = Rank(3);
pub const FIVE: Rank = Rank(4);
pub const SIX: Rank = Rank(5);
pub const SEVEN: Rank = Rank(6);
pub const EIGHT: Rank = Rank(7);
pub const ALL: [Rank; Self::NUM] = [
Rank::ONE,
Rank::TWO,
Rank::THREE,
Rank::FOUR,
Rank::FIVE,
Rank::SIX,
Rank::SEVEN,
Rank::EIGHT,
];
/// Ranks on which pawns start, by color.
///
/// ```
/// use chessfriend_core::{Color, Rank};
/// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::White as usize], Rank::TWO);
/// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::Black as usize], Rank::SEVEN);
/// ```
pub const PAWN_STARTING_RANKS: [Rank; 2] = [Rank::TWO, Rank::SEVEN];
pub const PAWN_DOUBLE_PUSH_TARGET_RANKS: [Rank; 2] = [Rank::FOUR, Rank::FIVE];
#[must_use]
pub fn is_pawn_starting_rank(&self, color: Color) -> bool {
self == &Self::PAWN_STARTING_RANKS[color as usize]
}
#[must_use]
pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool {
self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize]
}
/// Ranks where promotions happen.
#[must_use]
pub fn is_promotable_rank(&self) -> bool {
matches!(*self, Rank::ONE | Rank::EIGHT)
}
}
#[rustfmt::skip]
coordinate_enum!(Square, [
A1, B1, C1, D1, E1, F1, G1, H1,
A2, B2, C2, D2, E2, F2, G2, H2,
A3, B3, C3, D3, E3, F3, G3, H3,
A4, B4, C4, D4, E4, F4, G4, H4,
A5, B5, C5, D5, E5, F5, G5, H5,
A6, B6, C6, D6, E6, F6, G6, H6,
A7, B7, C7, D7, E7, F7, G7, H7,
A8, B8, C8, D8, E8, F8, G8, H8
]);
/// Generate an enum that maps its values to variants of [Square].
macro_rules! to_square_enum {
($vis:vis $name:ident { $($variant:ident)* }) => {
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
$vis enum $name {
$($variant = Square::$variant as u8,)*
}
impl From<$name> for Square {
fn from(value: $name) -> Self {
unsafe { Square::from_index_unchecked(value as u8) }
}
}
};
}
to_square_enum!(
pub EnPassantTargetSquare {
A3 B3 C3 D3 E3 F3 G3 H3
A6 B6 C6 D6 E6 F6 G6 H6
}
);
impl Square {
/// # Safety
///
/// This function does not do any bounds checking on the input. In debug
/// builds, this function will assert that the argument is in bounds.
#[must_use]
pub unsafe fn from_index_unchecked(x: u8) -> Square {
debug_assert!((x as usize) < Self::NUM);
Self::ALL[x as usize]
}
#[inline]
#[must_use]
pub fn from_file_rank(file: File, rank: Rank) -> Square {
let file_int: u8 = file.into();
let rank_int: u8 = rank.into();
unsafe { Self::from_index_unchecked(rank_int << 3 | file_int) }
}
pub fn from_algebraic_str(s: &str) -> Result<Square, ParseSquareError> {
s.parse()
}
#[must_use]
#[inline]
pub fn file(self) -> File {
unsafe { File::new_unchecked((self as u8) & 0b000_00111) }
}
#[must_use]
#[inline]
pub fn rank(self) -> Rank {
unsafe { Rank::new_unchecked((self as u8) >> 3) }
}
#[must_use]
pub fn file_rank(&self) -> (File, Rank) {
(self.file(), self.rank())
}
#[must_use]
pub fn neighbor(self, direction: Direction) -> Option<Square> {
match direction {
Direction::North => {
if self.rank() == Rank::EIGHT {
return None;
}
}
Direction::NorthEast => {
let (file, rank) = self.file_rank();
if rank == Rank::EIGHT || file == File::H {
return None;
}
}
Direction::East => {
if self.file() == File::H {
return None;
}
}
Direction::SouthEast => {
let (file, rank) = self.file_rank();
if rank == Rank::ONE || file == File::H {
return None;
}
}
Direction::South => {
if self.rank() == Rank::ONE {
return None;
}
}
Direction::SouthWest => {
let (file, rank) = self.file_rank();
if rank == Rank::ONE || file == File::A {
return None;
}
}
Direction::West => {
if self.file() == File::A {
return None;
}
}
Direction::NorthWest => {
let (file, rank) = self.file_rank();
if rank == Rank::EIGHT || file == File::A {
return None;
}
}
}
let index: u8 = self as u8;
let direction = direction.to_offset();
Square::try_from(index.wrapping_add_signed(direction)).ok()
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum ParseSquareError {
#[error("{0}")]
RankError(#[from] ParseRankError),
#[error("{0}")]
FileError(#[from] ParseFileError),
}
impl TryFrom<&str> for Square {
type Error = ParseSquareError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut chars = value.chars();
let file: File = chars
.next()
.and_then(|c| c.try_into().ok())
.ok_or(ParseSquareError::FileError(ParseFileError))?;
let rank: Rank = chars
.next()
.and_then(|c| c.try_into().ok())
.ok_or(ParseSquareError::RankError(ParseRankError))?;
Ok(Square::from_file_rank(file, rank))
}
}
impl FromStr for Square {
type Err = ParseSquareError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
let file: File = chars
.next()
.and_then(|c| c.try_into().ok())
.ok_or(ParseSquareError::FileError(ParseFileError))?;
let rank: Rank = chars
.next()
.and_then(|c| c.try_into().ok())
.ok_or(ParseSquareError::RankError(ParseRankError))?;
Ok(Square::from_file_rank(file, rank))
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[error("invalid rank")]
pub struct ParseRankError;
impl FromStr for Rank {
type Err = ParseRankError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let ch = s
.chars()
.nth(0)
.ok_or(ParseRankError)
.map(|ch| ch.to_ascii_lowercase())?;
let offset = 'a' as usize - (ch as usize);
if offset >= Rank::ALL.len() {
return Err(ParseRankError);
}
Ok(Rank::ALL[offset])
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[error("invalid file")]
pub struct ParseFileError;
impl FromStr for File {
type Err = ParseFileError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let ch = s
.chars()
.nth(0)
.ok_or(ParseFileError)
.map(|ch| ch.to_ascii_lowercase())?;
let offset = '1' as usize - (ch as usize);
if offset >= File::ALL.len() {
return Err(ParseFileError);
}
Ok(File::ALL[offset])
}
}
impl fmt::Display for File {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Into::<char>::into(*self))
}
}
impl fmt::Display for Rank {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Into::<char>::into(*self))
}
}
impl fmt::Display for Square {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.file().fmt(f)?;
self.rank().fmt(f)?;
Ok(())
}
}
impl From<File> for char {
fn from(value: File) -> Self {
let u8value: u8 = value.into();
(u8value + b'a') as char
}
}
impl From<Rank> for char {
fn from(value: Rank) -> Self {
Self::from(value.0 + b'1')
}
}
impl TryFrom<char> for File {
type Error = ();
fn try_from(value: char) -> Result<Self, Self::Error> {
File::try_from(value.to_ascii_lowercase() as u8 - b'a')
}
}
impl TryFrom<char> for Rank {
type Error = ();
fn try_from(value: char) -> Result<Self, Self::Error> {
let result = (value as u8).checked_sub(b'1').ok_or(())?;
Self::try_from(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn direction_offsets() {
assert_eq!(Direction::North.to_offset(), 8);
assert_eq!(Direction::NorthEast.to_offset(), 9);
assert_eq!(Direction::East.to_offset(), 1);
assert_eq!(Direction::SouthEast.to_offset(), -7);
assert_eq!(Direction::South.to_offset(), -8);
assert_eq!(Direction::SouthWest.to_offset(), -9);
assert_eq!(Direction::West.to_offset(), -1);
assert_eq!(Direction::NorthWest.to_offset(), 7);
}
#[test]
fn good_algebraic_input() -> Result<(), ParseSquareError> {
let sq = Square::from_algebraic_str("a4")?;
assert_eq!(sq.file(), File::A);
assert_eq!(sq.rank(), Rank::FOUR);
let sq = Square::from_algebraic_str("B8")?;
assert_eq!(sq.file(), File::B);
assert_eq!(sq.rank(), Rank::EIGHT);
let sq = Square::from_algebraic_str("e4")?;
assert_eq!(sq.file(), File::E);
assert_eq!(sq.rank(), Rank::FOUR);
Ok(())
}
#[test]
fn bad_algebraic_input() {
assert!("a0".parse::<Square>().is_err());
assert!("j3".parse::<Square>().is_err());
assert!("a9".parse::<Square>().is_err());
assert!("b-1".parse::<Square>().is_err());
assert!("a 1".parse::<Square>().is_err());
assert!("".parse::<Square>().is_err());
}
#[test]
fn from_index() -> Result<(), ()> {
let sq = Square::try_from(4u32)?;
assert_eq!(sq.file(), File::E);
assert_eq!(sq.rank(), Rank::ONE);
let sq = Square::try_from(28u32)?;
assert_eq!(sq.file(), File::E);
assert_eq!(sq.rank(), Rank::FOUR);
Ok(())
}
#[test]
fn to_index() {
assert_eq!(Square::A1 as usize, 0);
assert_eq!(Square::H8 as usize, 63);
}
#[test]
fn valid_neighbors() {
let sq = Square::E4;
assert_eq!(sq.neighbor(Direction::North), Some(Square::E5));
assert_eq!(sq.neighbor(Direction::NorthEast), Some(Square::F5));
assert_eq!(sq.neighbor(Direction::East), Some(Square::F4));
assert_eq!(sq.neighbor(Direction::SouthEast), Some(Square::F3));
assert_eq!(sq.neighbor(Direction::South), Some(Square::E3));
assert_eq!(sq.neighbor(Direction::SouthWest), Some(Square::D3));
assert_eq!(sq.neighbor(Direction::West), Some(Square::D4));
assert_eq!(sq.neighbor(Direction::NorthWest), Some(Square::D5));
}
#[test]
fn invalid_neighbors() {
let sq = Square::A1;
assert!(sq.neighbor(Direction::West).is_none());
assert!(sq.neighbor(Direction::SouthWest).is_none());
assert!(sq.neighbor(Direction::South).is_none());
let sq = Square::H1;
assert!(sq.neighbor(Direction::East).is_none());
assert!(sq.neighbor(Direction::SouthEast).is_none());
assert!(sq.neighbor(Direction::South).is_none());
let sq = Square::A8;
assert!(sq.neighbor(Direction::North).is_none());
assert!(sq.neighbor(Direction::NorthWest).is_none());
assert!(sq.neighbor(Direction::West).is_none());
let sq = Square::H8;
assert!(sq.neighbor(Direction::North).is_none());
assert!(sq.neighbor(Direction::NorthEast).is_none());
assert!(sq.neighbor(Direction::East).is_none());
}
#[test]
fn display() {
assert_eq!(format!("{}", Square::C5), "c5");
assert_eq!(format!("{}", Square::A1), "a1");
assert_eq!(format!("{}", Square::H8), "h8");
assert_eq!(format!("{}", Rank::FIVE), "5");
assert_eq!(format!("{}", File::H), "h");
}
#[test]
fn into_char() {
assert_eq!(Into::<char>::into(Rank::ONE), '1');
assert_eq!(Into::<char>::into(File::B), 'b');
}
}

View file

@ -1,22 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Wing {
KingSide = 0,
QueenSide = 1,
}
impl Wing {
pub const NUM: usize = 2;
pub const ALL: [Wing; Self::NUM] = [Self::KingSide, Self::QueenSide];
}
impl std::fmt::Display for Wing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Wing::KingSide => write!(f, "kingside"),
Wing::QueenSide => write!(f, "queenside"),
}
}
}

View file

@ -1,20 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
pub mod colors;
pub mod coordinates;
pub mod pieces;
pub mod random;
pub mod score;
pub mod shapes;
mod macros;
pub use colors::Color;
pub use coordinates::{Direction, File, Rank, Square, Wing};
pub use pieces::Piece;
pub use shapes::{Shape, Slider};
pub mod errors {
pub use crate::coordinates::ParseSquareError;
pub use crate::shapes::ParseShapeError;
}

View file

@ -1,11 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
#[macro_export]
macro_rules! piece {
($color:ident $shape:ident) => {
$crate::Piece::new($crate::Color::$color, $crate::Shape::$shape)
};
($color:ident $shape:ident on $square:ident) => {
$crate::PlacedPiece::new(piece!($color $shape), $crate::Square::$square)
}
}

View file

@ -1,110 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
mod display;
pub use self::display::{PieceDisplay, PieceDisplayStyle};
use crate::{Color, Shape};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Piece {
pub color: Color,
pub shape: Shape,
}
macro_rules! piece_constructor {
($func_name:ident, $type:tt) => {
#[must_use]
pub fn $func_name(color: Color) -> Piece {
Piece {
color,
shape: Shape::$type,
}
}
};
}
macro_rules! is_shape {
($func_name:ident, $shape:ident) => {
#[must_use]
pub fn $func_name(&self) -> bool {
self.shape == Shape::$shape
}
};
}
impl Piece {
#[must_use]
pub fn new(color: Color, shape: Shape) -> Piece {
Piece { color, shape }
}
piece_constructor!(pawn, Pawn);
piece_constructor!(knight, Knight);
piece_constructor!(bishop, Bishop);
piece_constructor!(rook, Rook);
piece_constructor!(queen, Queen);
piece_constructor!(king, King);
is_shape!(is_pawn, Pawn);
is_shape!(is_knight, Knight);
is_shape!(is_bishop, Bishop);
is_shape!(is_rook, Rook);
is_shape!(is_queen, Queen);
is_shape!(is_king, King);
#[must_use]
pub fn to_ascii(self) -> char {
let ch = self.shape.to_ascii();
match self.color {
Color::White => ch,
Color::Black => ch.to_ascii_lowercase(),
}
}
#[must_use]
pub fn to_unicode(self) -> char {
match (self.color, self.shape) {
(Color::Black, Shape::Pawn) => '♟',
(Color::Black, Shape::Knight) => '♞',
(Color::Black, Shape::Bishop) => '♝',
(Color::Black, Shape::Rook) => '♜',
(Color::Black, Shape::Queen) => '♛',
(Color::Black, Shape::King) => '♚',
(Color::White, Shape::Pawn) => '♙',
(Color::White, Shape::Knight) => '♘',
(Color::White, Shape::Bishop) => '♗',
(Color::White, Shape::Rook) => '♖',
(Color::White, Shape::Queen) => '♕',
(Color::White, Shape::King) => '♔',
}
}
}
impl std::fmt::Display for Piece {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
PieceDisplay {
piece: *self,
style: PieceDisplayStyle::default()
}
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_shape() {
assert_eq!("p".parse(), Ok(Shape::Pawn));
}
#[test]
fn shape_into_char() {
assert_eq!(<Shape as Into<char>>::into(Shape::Pawn), 'P');
}
}

View file

@ -1,28 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use super::Piece;
#[derive(Default)]
pub enum PieceDisplayStyle {
#[default]
Unicode,
ASCII,
LongForm,
}
pub struct PieceDisplay {
pub(super) piece: Piece,
pub(super) style: PieceDisplayStyle,
}
impl std::fmt::Display for PieceDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.style {
PieceDisplayStyle::Unicode => write!(f, "{}", self.piece.to_unicode()),
PieceDisplayStyle::ASCII => write!(f, "{}", self.piece.to_ascii()),
PieceDisplayStyle::LongForm => {
write!(f, "{} {}", self.piece.color.name(), self.piece.shape.name())
}
}
}
}

View file

@ -1,44 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use rand::{rngs::StdRng, RngCore, SeedableRng};
pub struct RandomNumberGenerator {
rng: StdRng,
}
impl RandomNumberGenerator {
/// A default value for hashing chess games.
///
/// This value is the SHA-256 hash of a PGN I downloaded from the internet
/// of the six games Kasparov played against Deep Blue in 1996.
#[rustfmt::skip]
const DEFAULT_SEED_VALUE: [u8; 32] = [
0x22, 0x0b, 0xae, 0xd5, 0xc5, 0xb8, 0x20, 0xb1,
0xee, 0x05, 0x4b, 0x72, 0x73, 0x4c, 0x1a, 0xf9,
0xc2, 0xb7, 0x5d, 0x87, 0xc8, 0xa9, 0x44, 0x87,
0x01, 0xda, 0xdb, 0xe0, 0x8e, 0xf8, 0xe8, 0x77,
];
#[must_use]
pub fn new(seed: <StdRng as SeedableRng>::Seed) -> Self {
let rng = StdRng::from_seed(seed);
Self { rng }
}
#[must_use]
pub fn rand(&mut self) -> &mut StdRng {
&mut self.rng
}
pub fn next_u64(&mut self) -> u64 {
self.rng.next_u64()
}
}
impl Default for RandomNumberGenerator {
fn default() -> Self {
let rng = StdRng::from_seed(Self::DEFAULT_SEED_VALUE);
Self { rng }
}
}

View file

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

View file

@ -1,179 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
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,
Knight = 1,
Bishop = 2,
Rook = 3,
Queen = 4,
King = 5,
}
impl Shape {
/// Number of piece shapes
pub const NUM: usize = 6;
/// A slice of all piece shapes
pub const ALL: [Shape; Self::NUM] = [
Shape::Pawn,
Shape::Knight,
Shape::Bishop,
Shape::Rook,
Shape::Queen,
Shape::King,
];
const PROMOTABLE_SHAPES: [Shape; 4] = [Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight];
pub fn iter() -> slice::Iter<'static, Self> {
Shape::ALL.iter()
}
#[must_use]
pub fn into_iter() -> array::IntoIter<Self, { Self::NUM }> {
Shape::ALL.into_iter()
}
/// An iterator over the shapes that a pawn can promote to
pub fn promotable() -> slice::Iter<'static, Shape> {
Self::PROMOTABLE_SHAPES.iter()
}
#[must_use]
pub const fn to_ascii(self) -> char {
match self {
Shape::Pawn => 'P',
Shape::Knight => 'N',
Shape::Bishop => 'B',
Shape::Rook => 'R',
Shape::Queen => 'Q',
Shape::King => 'K',
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Shape::Pawn => "pawn",
Shape::Knight => "knight",
Shape::Bishop => "bishop",
Shape::Rook => "rook",
Shape::Queen => "queen",
Shape::King => "king",
}
}
#[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),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Slider {
Bishop,
Rook,
Queen,
}
impl From<Slider> for Shape {
fn from(value: Slider) -> Self {
match value {
Slider::Bishop => Shape::Bishop,
Slider::Rook => Shape::Rook,
Slider::Queen => Shape::Queen,
}
}
}
impl TryFrom<Shape> for Slider {
type Error = ();
fn try_from(value: Shape) -> Result<Self, Self::Error> {
match value {
Shape::Bishop => Ok(Slider::Bishop),
Shape::Rook => Ok(Slider::Rook),
Shape::Queen => Ok(Slider::Queen),
_ => Err(()),
}
}
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("no matching piece shape for character '{0:?}'")]
pub struct ShapeFromCharError(char);
impl TryFrom<char> for Shape {
type Error = ShapeFromCharError;
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'P' | 'p' => Ok(Shape::Pawn),
'N' | 'n' => Ok(Shape::Knight),
'B' | 'b' => Ok(Shape::Bishop),
'R' | 'r' => Ok(Shape::Rook),
'Q' | 'q' => Ok(Shape::Queen),
'K' | 'k' => Ok(Shape::King),
_ => Err(ShapeFromCharError(value)),
}
}
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("no matching piece shape for string")]
pub struct ParseShapeError;
impl FromStr for Shape {
type Err = ParseShapeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"p" | "pawn" => Ok(Shape::Pawn),
"n" | "knight" => Ok(Shape::Knight),
"b" | "bishop" => Ok(Shape::Bishop),
"r" | "rook" => Ok(Shape::Rook),
"q" | "queen" => Ok(Shape::Queen),
"k" | "king" => Ok(Shape::King),
_ => Err(ParseShapeError),
}
}
}
impl From<&Shape> for char {
fn from(shape: &Shape) -> char {
char::from(*shape)
}
}
impl From<Shape> for char {
fn from(shape: Shape) -> char {
shape.to_ascii()
}
}
impl fmt::Display for Shape {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let self_char: char = self.into();
write!(f, "{self_char}")
}
}

View file

@ -1,47 +0,0 @@
WHITE CHESS KING
Unicode: U+2654, UTF-8: E2 99 94
WHITE CHESS QUEEN
Unicode: U+2655, UTF-8: E2 99 95
WHITE CHESS ROOK
Unicode: U+2656, UTF-8: E2 99 96
WHITE CHESS BISHOP
Unicode: U+2657, UTF-8: E2 99 97
WHITE CHESS KNIGHT
Unicode: U+2658, UTF-8: E2 99 98
WHITE CHESS PAWN
Unicode: U+2659, UTF-8: E2 99 99
BLACK CHESS KING
Unicode: U+265A, UTF-8: E2 99 9A
BLACK CHESS QUEEN
Unicode: U+265B, UTF-8: E2 99 9B
BLACK CHESS ROOK
Unicode: U+265C, UTF-8: E2 99 9C
BLACK CHESS BISHOP
Unicode: U+265D, UTF-8: E2 99 9D
BLACK CHESS KNIGHT
Unicode: U+265E, UTF-8: E2 99 9E
chess pawn
Unicode: U+265F, UTF-8: E2 99 9F

View file

@ -6,12 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.98"
chessfriend_core = { path = "../core" }
chessfriend_board = { path = "../board" }
chessfriend_moves = { path = "../moves" }
chessfriend_position = { path = "../position" }
board = { path = "../board" }
clap = { version = "4.4.12", features = ["derive"] }
rustyline = "13.0.0"
shlex = "1.2.0"
thiserror = "2"

View file

@ -1,31 +1,14 @@
// Eryn Wells <eryn@erynwells.me>
use board::piece::{Color, Piece, Shape};
use board::position::DiagramFormatter;
use board::{Position, Square};
use clap::{Arg, Command};
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
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, value_parser};
use rustyline::{DefaultEditor, error::ReadlineError};
use std::sync::Arc;
use thiserror::Error;
struct CommandResult {
should_continue: bool,
should_print_position: bool,
}
impl Default for CommandResult {
fn default() -> Self {
CommandResult {
should_continue: true,
should_print_position: true,
}
}
}
struct State {
position: Position,
zobrist: Arc<ZobristState>,
#[derive(Eq, PartialEq)]
enum ShouldContinue {
Yes,
No,
}
fn command_line() -> Command {
@ -34,366 +17,73 @@ fn command_line() -> Command {
{all-args}
";
// strip out name/version
const APPLET_TEMPLATE: &str = "\
{about-with-newline}\n\
{usage-heading}\n {usage}\n\
\n\
{all-args}{after-help}\
";
Command::new("explorer")
.multicall(true)
.arg_required_else_help(true)
.subcommand_required(true)
.subcommand_value_name("CMD")
.subcommand_help_heading("COMMANDS")
.subcommand_value_name("APPLET")
.subcommand_help_heading("APPLETS")
.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))
.alias("l")
.about("Load a board position from a FEN string"),
)
.subcommand(
Command::new("make")
.arg(Arg::new("move").required(true))
.alias("m")
.about("Make a move"),
)
.subcommand(Command::new("print").about("Print the board"))
.subcommand(
Command::new("place")
.arg(Arg::new("color").required(true))
.arg(Arg::new("piece").required(true))
.arg(Arg::new("square").required(true))
.alias("p")
.about("Place a piece on the board"),
.arg(Arg::new("square").required(true)),
)
.subcommand(
Command::new("sight")
.arg(Arg::new("square").required(false))
.about("Show sight of a piece on a square"),
)
.subcommand(
Command::new("moves")
.arg(Arg::new("square").required(false))
.about("Show moves of a piece on a square. With no argument, show all moves for the active color."),
)
.subcommand(
Command::new("movement")
.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"))
.subcommand(Command::new("starting").about("Reset to the starting position"))
.subcommand(
Command::new("fen")
.arg(Arg::new("fen").required(true))
.about("Reset to a position described by a FEN string"),
)
.about("Reset the board"),
)
.subcommand(Command::new("print").about("Print the board"))
.subcommand(Command::new("quit").alias("exit").about("Quit the program"))
.subcommand(Command::new("zobrist").about("Print the Zobrist hash of the current board"))
.subcommand(Command::new("quit").about("Quit the program"))
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
enum CommandHandlingError<'a> {
#[error("lexer error")]
LexerError,
#[error("missing {0} argument")]
MissingArgument(&'a str),
#[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> {
let args = shlex::split(line).ok_or(CommandHandlingError::LexerError)?;
let matches = command_line().try_get_matches_from(args)?;
let mut result = CommandResult::default();
fn respond(line: &str, pos: &mut Position) -> Result<ShouldContinue, String> {
let args = shlex::split(line).ok_or("error: Invalid quoting")?;
let matches = command_line()
.try_get_matches_from(args)
.map_err(|e| e.to_string())?;
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)) => {
result.should_continue = false;
result.should_print_position = false;
return Ok(ShouldContinue::No);
}
Some(("fen", _matches)) => {
println!("{}", state.position.to_fen_str()?);
result.should_print_position = false;
Some(("place", _matches)) => {
let piece_arg = _matches.get_one::<String>("piece").unwrap();
let shape = match piece_arg.chars().nth(0) {
Some(c) => Shape::try_from(c).map_err(|_| ()),
None => Err(()),
}
.map_err(|_| "Error: invalid piece specifier")?;
let square_arg = _matches.get_one::<String>("square").unwrap();
let square = Square::from_algebraic_str(square_arg)
.map_err(|_| "Error: invalid square specifier")?;
pos.place_piece(&Piece::new(Color::White, shape), &square)
.map_err(|_| "Error: Unable to place piece")?;
}
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")
.ok_or(CommandHandlingError::MissingArgument("color"))?;
let color = color.parse::<Color>()?;
let shape = matches
.get_one::<String>("piece")
.ok_or(CommandHandlingError::MissingArgument("piece"))?;
let shape = shape.parse::<Shape>()?;
let square = matches
.get_one::<String>("square")
.ok_or(CommandHandlingError::MissingArgument("square"))?;
let square = Square::from_algebraic_str(square)?;
let piece = Piece::new(color, shape);
state
.position
.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 display = state.position.display().highlight(sight);
println!("\n{display}");
result.should_print_position = false;
}
Some(("moves", matches)) => result = do_moves_command(state, matches)?,
Some(("movement", matches)) => result = do_movement_command(state, matches)?,
Some(("reset", matches)) => result = do_reset_command(state, matches)?,
Some(("zobrist", matches)) => result = do_zobrist_command(state, matches),
Some((name, _matches)) => unimplemented!("{name}"),
None => unreachable!("Subcommand required"),
}
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")
.ok_or(CommandHandlingError::MissingArgument("fen"))?;
let mut board = Board::from_fen_str(fen_string.as_str())?;
board.set_zobrist_state(state.zobrist.clone());
state.position = Position::new(board);
Ok(CommandResult {
should_continue: true,
should_print_position: true,
})
}
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,
) -> anyhow::Result<CommandResult> {
match matches.subcommand() {
None | Some(("clear", _)) => state.position = Position::empty(Some(state.zobrist.clone())),
Some(("starting", _)) => state.position = Position::starting(Some(state.zobrist.clone())),
Some(("fen", matches)) => {
let fen = matches
.get_one::<String>("fen")
.ok_or(CommandHandlingError::MissingArgument("fen"))?;
let board = Board::from_fen_str(fen)?;
state.position = Position::new(board);
}
Some((name, _matches)) => unimplemented!("{name}"),
}
Ok(CommandResult::default())
}
fn do_moves_command(
state: &mut State,
matches: &clap::ArgMatches,
) -> anyhow::Result<CommandResult> {
let moves: Vec<GeneratedMove> = if let Some(square) = matches
.get_one::<String>("square")
.and_then(|square| square.parse::<Square>().ok())
{
state
.position
.moves_for_piece(square)
.map(|it| it.filter(|ply| ply.origin() == square))
.map(Iterator::collect)
.ok_or(CommandHandlingError::NoPiece(square))?
} else {
state.position.all_moves(None).collect()
};
let formatted_moves: Vec<String> = moves
.iter()
.map(|ply| {
let origin = ply.origin();
if let Some(piece) = state.position.get_piece(origin) {
format!("{piece}{ply}")
} else {
format!("{ply}??")
}
})
.collect();
if !formatted_moves.is_empty() {
let max_length = formatted_moves
.iter()
.map(|s| s.chars().count())
.max()
.unwrap_or(8)
+ 2;
let columns_count = 80 / max_length;
for row in formatted_moves.chunks(columns_count) {
for ply in row {
print!("{ply:<max_length$}");
}
println!();
}
}
Ok(CommandResult {
should_continue: true,
should_print_position: false,
})
}
fn do_movement_command(
state: &mut State,
matches: &clap::ArgMatches,
) -> anyhow::Result<CommandResult> {
let square = *matches
.get_one::<Square>("square")
.ok_or(CommandHandlingError::MissingArgument("square"))?;
let movement = state.position.movement_piece(square);
let display = state.position.display().highlight(movement);
println!("\n{display}");
Ok(CommandResult {
should_continue: true,
should_print_position: false,
})
}
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}");
} else {
println!("No Zobrist hash available");
}
CommandResult {
should_continue: true,
should_print_position: false,
}
Ok(ShouldContinue::Yes)
}
fn main() -> Result<(), String> {
let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?;
let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {}", err.to_string()))?;
let mut rng = RandomNumberGenerator::default();
let zobrist_state = Arc::new(ZobristState::new(&mut rng));
let starting_position = Position::starting(Some(zobrist_state.clone()));
let mut state = State {
position: starting_position,
zobrist: zobrist_state,
};
let mut should_print_position = true;
let mut pos = Position::empty();
loop {
if should_print_position {
println!("{}", &state.position);
println!("{} to move.", state.position.active_color());
}
let diagram = DiagramFormatter::new(&pos);
println!("{}", diagram);
let readline = editor.readline("\n? ");
let readline = editor.readline("? ");
match readline {
Ok(line) => {
let line = line.trim();
@ -401,14 +91,13 @@ fn main() -> Result<(), String> {
continue;
}
match respond(line, &mut state) {
Ok(result) => {
should_print_position = result.should_print_position;
if !result.should_continue {
match respond(line, &mut pos) {
Ok(should_continue) => {
if should_continue == ShouldContinue::No {
break;
}
}
Err(message) => println!("{message}"),
Err(message) => println!("{}", message),
}
}
Err(ReadlineError::Interrupted) => {
@ -420,7 +109,7 @@ fn main() -> Result<(), String> {
break;
}
Err(err) => {
println!("Error: {err}");
println!("Error: {:?}", err);
break;
}
}

View file

@ -1,7 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use clap::ArgMatches;
pub enum MakeCommandError {}
pub struct MakeCommand {}

View file

@ -1,12 +0,0 @@
[package]
name = "chessfriend_moves"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chessfriend_bitboard = { path = "../bitboard" }
chessfriend_board = { path = "../board" }
chessfriend_core = { path = "../core" }
thiserror = "2"

View file

@ -1,134 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_core::{
errors::{ParseShapeError, ParseSquareError},
Shape, Square,
};
use std::str::FromStr;
use thiserror::Error;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AlgebraicMoveComponents {
/// An empty move
Null,
Regular {
origin: Square,
target: Square,
promotion: Option<Shape>,
},
}
#[derive(Clone, Debug, Error)]
pub enum ParseAlgebraicMoveComponentsError {
#[error("string not long enough to contain valid move")]
InsufficientLength,
#[error("{0}")]
ParseSquareError(#[from] ParseSquareError),
#[error("{0}")]
ParseShapeError(#[from] ParseShapeError),
#[error("{0} is not a promotable shape")]
InvalidPromotionShape(Shape),
}
impl FromStr for AlgebraicMoveComponents {
type Err = ParseAlgebraicMoveComponentsError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s == "0000" {
return Ok(Self::Null);
}
if s.len() < 4 {
return Err(ParseAlgebraicMoveComponentsError::InsufficientLength);
}
let s = s.to_lowercase();
let (origin_string, s) = s.split_at(2);
let s = s.trim_start();
let (target_string, s) = s.split_at(2);
let promotion = if s.len() >= 1 {
let s = s.trim_start();
let (promotion_string, _s) = s.split_at(1);
Some(promotion_string)
} else {
None
};
let origin: Square = origin_string.parse()?;
let target: Square = target_string.parse()?;
let promotion = if let Some(promotion) = promotion {
let promotion: Shape = promotion.parse()?;
if promotion.is_promotable() {
Some(promotion)
} else {
return Err(ParseAlgebraicMoveComponentsError::InvalidPromotionShape(
promotion,
));
}
} else {
None
};
Ok(AlgebraicMoveComponents::Regular {
origin,
target,
promotion,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_null_move() -> Result<(), ParseAlgebraicMoveComponentsError> {
let components: AlgebraicMoveComponents = "0000".parse()?;
assert_eq!(components, AlgebraicMoveComponents::Null);
Ok(())
}
#[test]
fn parse_origin_target_move() -> Result<(), ParseAlgebraicMoveComponentsError> {
let components: AlgebraicMoveComponents = "e2e4".parse()?;
assert_eq!(
components,
AlgebraicMoveComponents::Regular {
origin: Square::E2,
target: Square::E4,
promotion: None
}
);
Ok(())
}
#[test]
fn parse_origin_target_promotion_move() -> Result<(), ParseAlgebraicMoveComponentsError> {
let components: AlgebraicMoveComponents = "h7h8q".parse()?;
assert_eq!(
components,
AlgebraicMoveComponents::Regular {
origin: Square::H7,
target: Square::H8,
promotion: Some(Shape::Queen),
}
);
Ok(())
}
}

View file

@ -1,61 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use chessfriend_core::Shape;
#[repr(u16)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Kind {
Quiet = 0b0000,
DoublePush = 0b0001,
KingSideCastle = 0b0010,
QueenSideCastle = 0b0011,
Capture = 0b0100,
EnPassantCapture = 0b0101,
Promotion = 0b1000,
CapturePromotion = 0b1100,
}
#[repr(u16)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum PromotionShape {
Knight = 0b00,
Bishop = 0b01,
Rook = 0b10,
Queen = 0b11,
}
impl PromotionShape {
pub const NUM: usize = 4;
pub const ALL: [PromotionShape; PromotionShape::NUM] = [
PromotionShape::Knight,
PromotionShape::Bishop,
PromotionShape::Rook,
PromotionShape::Queen,
];
}
impl From<PromotionShape> for Shape {
fn from(value: PromotionShape) -> Self {
match value {
PromotionShape::Knight => Shape::Knight,
PromotionShape::Bishop => Shape::Bishop,
PromotionShape::Rook => Shape::Rook,
PromotionShape::Queen => Shape::Queen,
}
}
}
impl TryFrom<Shape> for PromotionShape {
type Error = ();
fn try_from(value: Shape) -> Result<Self, Self::Error> {
match value {
Shape::Knight => Ok(PromotionShape::Knight),
Shape::Bishop => Ok(PromotionShape::Bishop),
Shape::Rook => Ok(PromotionShape::Rook),
Shape::Queen => Ok(PromotionShape::Queen),
_ => Err(()),
}
}
}

View file

@ -1,61 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
mod all;
mod king;
mod knight;
mod pawn;
mod slider;
pub use all::AllPiecesMoveGenerator;
pub use king::KingMoveGenerator;
pub use knight::KnightMoveGenerator;
pub use pawn::PawnMoveGenerator;
pub use slider::{BishopMoveGenerator, QueenMoveGenerator, RookMoveGenerator};
use crate::Move;
use chessfriend_core::{Shape, Square};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct GeneratedMove {
pub(crate) ply: Move,
}
impl GeneratedMove {
#[must_use]
pub fn origin(&self) -> Square {
self.ply.origin_square()
}
#[must_use]
pub fn target(&self) -> Square {
self.ply.target_square()
}
#[must_use]
pub fn promotion_shape(&self) -> Option<Shape> {
self.ply.promotion_shape()
}
#[must_use]
pub fn ply(&self) -> Move {
self.ply
}
}
impl std::fmt::Display for GeneratedMove {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}]", self.ply)
}
}
impl From<GeneratedMove> for Move {
fn from(value: GeneratedMove) -> Self {
value.ply
}
}
impl From<Move> for GeneratedMove {
fn from(value: Move) -> Self {
GeneratedMove { ply: value }
}
}

View file

@ -1,64 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use super::{
BishopMoveGenerator, GeneratedMove, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator,
QueenMoveGenerator, RookMoveGenerator,
};
use chessfriend_board::Board;
use chessfriend_core::Color;
use std::iter::{Fuse, FusedIterator};
#[must_use]
pub struct AllPiecesMoveGenerator {
pawn: Fuse<PawnMoveGenerator>,
knight: Fuse<KnightMoveGenerator>,
bishop: Fuse<BishopMoveGenerator>,
rook: Fuse<RookMoveGenerator>,
queen: Fuse<QueenMoveGenerator>,
king: Fuse<KingMoveGenerator>,
}
impl AllPiecesMoveGenerator {
pub fn new(board: &Board, color: Option<Color>) -> Self {
Self {
pawn: PawnMoveGenerator::new(board, color).fuse(),
knight: KnightMoveGenerator::new(board, color).fuse(),
bishop: BishopMoveGenerator::new(board, color).fuse(),
rook: RookMoveGenerator::new(board, color).fuse(),
queen: QueenMoveGenerator::new(board, color).fuse(),
king: KingMoveGenerator::new(board, color).fuse(),
}
}
}
impl Iterator for AllPiecesMoveGenerator {
type Item = GeneratedMove;
fn next(&mut self) -> Option<Self::Item> {
macro_rules! return_next_move {
($generator:expr) => {
let next_move = $generator.next();
if next_move.is_some() {
return next_move;
}
};
}
// All of these iterators are fused, meaning they are guaranteed to
// always return None once they've returned None the first time. Fuse<T>
// can optimize this so that the work done in each of the move
// generators' next() methods doesn't have to be repeated every time
// next() is called.
return_next_move!(self.pawn);
return_next_move!(self.knight);
return_next_move!(self.bishop);
return_next_move!(self.rook);
return_next_move!(self.queen);
return_next_move!(self.king);
None
}
}
impl FusedIterator for AllPiecesMoveGenerator {}

View file

@ -1,370 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{GeneratedMove, Move};
use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard};
use chessfriend_board::{Board, CastleParameters};
use chessfriend_core::{Color, Square, Wing};
use std::iter::FusedIterator;
#[must_use]
pub struct KingMoveGenerator {
kings: Vec<KingIterator>,
next_kings_index: usize,
current_king: Option<KingIterator>,
castle_iterator: CastleIterator,
friends: BitBoard,
enemies: BitBoard,
}
impl KingMoveGenerator {
pub fn new(board: &Board, color: Option<Color>) -> Self {
let color = board.unwrap_color(color);
let friends = board.friendly_occupancy(color);
let enemies = board.enemies(color);
let kings: Vec<KingIterator> = board
.kings(color)
.occupied_squares_trailing()
.map(|king| KingIterator {
origin: king,
moves: board
.king_sight(king, Some(color))
.occupied_squares_trailing(),
})
.collect();
Self {
kings,
next_kings_index: 0,
current_king: None,
castle_iterator: CastleIterator::new(board, color),
friends,
enemies,
}
}
}
impl Iterator for KingMoveGenerator {
type Item = GeneratedMove;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.current_king.is_none() {
if self.next_kings_index < self.kings.len() {
self.current_king = Some(self.kings[self.next_kings_index].clone());
self.next_kings_index += 1;
} else {
break;
}
}
if let Some(current_king) = self.current_king.as_mut() {
if let Some(target) = current_king.next() {
let target_bitboard: BitBoard = target.into();
let is_targeting_friendly_piece =
(target_bitboard & self.friends).is_populated();
if is_targeting_friendly_piece {
continue;
}
let is_targeting_enemy_piece = (target_bitboard & self.enemies).is_populated();
if is_targeting_enemy_piece {
return Some(Move::capture(current_king.origin, target).into());
}
return Some(Move::quiet(current_king.origin, target).into());
}
self.current_king = None;
}
}
self.castle_iterator.next()
}
}
impl FusedIterator for KingMoveGenerator {}
#[derive(Clone, Debug)]
struct KingIterator {
origin: Square,
moves: TrailingBitScanner,
}
impl Iterator for KingIterator {
type Item = Square;
fn next(&mut self) -> Option<Self::Item> {
self.moves.next()
}
}
impl FusedIterator for KingIterator {}
#[derive(Clone, Debug)]
struct CastleIterator {
color: Color,
kingside: Option<&'static CastleParameters>,
queenside: Option<&'static CastleParameters>,
}
impl CastleIterator {
fn new(board: &Board, color: Color) -> Self {
let kingside = board.color_can_castle(Wing::KingSide, Some(color)).ok();
let queenside = board.color_can_castle(Wing::QueenSide, Some(color)).ok();
Self {
color,
kingside,
queenside,
}
}
}
impl Iterator for CastleIterator {
type Item = GeneratedMove;
fn next(&mut self) -> Option<Self::Item> {
if let Some(_parameters) = self.kingside.take() {
return Some(Move::castle(self.color, Wing::KingSide).into());
}
if let Some(_parameters) = self.queenside.take() {
return Some(Move::castle(self.color, Wing::QueenSide).into());
}
None
}
}
impl FusedIterator for CastleIterator {}
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_move_list, ply};
use chessfriend_board::test_board;
#[test]
fn white_king_center_square_ai_claude() {
let board = test_board!(White King on E4);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
// All 8 adjacent squares
ply!(E4 - D3), // Southwest
ply!(E4 - D4), // West
ply!(E4 - D5), // Northwest
ply!(E4 - E3), // South
ply!(E4 - E5), // North
ply!(E4 - F3), // Southeast
ply!(E4 - F4), // East
ply!(E4 - F5), // Northeast
]
);
}
#[test]
fn white_king_corner_square_ai_claude() {
let board = test_board!(White King on A1);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
// Only 3 possible moves from corner
ply!(A1 - A2), // North
ply!(A1 - B1), // East
ply!(A1 - B2), // Northeast
]
);
}
#[test]
fn white_king_edge_square_ai_claude() {
let board = test_board!(White King on E1);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
// 5 possible moves from edge
ply!(E1 - D1), // West
ply!(E1 - D2), // Northwest
ply!(E1 - E2), // North
ply!(E1 - F1), // East
ply!(E1 - F2), // Northeast
]
);
}
#[test]
fn white_king_with_captures_ai_claude() {
let board = test_board!(
White King on D4,
Black Pawn on C5, // Can capture
Black Knight on E5, // Can capture
Black Bishop on D3 // Can capture
);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
// Regular moves
ply!(D4 - C3),
ply!(D4 - C4),
ply!(D4 - D5),
ply!(D4 - E3),
ply!(D4 - E4),
// Captures
ply!(D4 x C5), // Capture pawn
ply!(D4 x E5), // Capture knight
ply!(D4 x D3), // Capture bishop
]
);
}
#[test]
fn white_king_blocked_by_friendly_pieces_ai_claude() {
let board = test_board!(
White King on D4,
White Pawn on C4, // Blocks west
White Knight on D5, // Blocks north
White Bishop on E3 // Blocks southeast
);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
ply!(D4 - C3),
ply!(D4 - C5),
ply!(D4 - D3),
ply!(D4 - E4),
ply!(D4 - E5),
// Cannot move to C4, D5, E3 (friendly pieces)
]
);
}
#[test]
fn white_king_castling_kingside_ai_claude() {
let board = test_board!(
White King on E1,
White Rook on H1
// Assuming squares F1, G1 are empty and king/rook haven't moved
);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
// Regular king moves
ply!(E1 - D1),
ply!(E1 - D2),
ply!(E1 - E2),
ply!(E1 - F1),
ply!(E1 - F2),
ply!(White 0-0),
]
);
}
#[test]
fn white_king_castling_queenside_ai_claude() {
let board = test_board!(
White King on E1,
White Rook on A1
// Assuming squares B1, C1, D1 are empty and king/rook haven't moved
);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
// Regular king moves
ply!(E1 - D1),
ply!(E1 - D2),
ply!(E1 - E2),
ply!(E1 - F1),
ply!(E1 - F2),
ply!(White 0-0-0),
]
);
}
#[test]
fn white_king_no_castling_through_check_ai_claude() {
let board = test_board!(
White King on E1,
White Rook on H1,
Black Rook on F8 // Attacks F1, preventing kingside castling
);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
ply!(E1 - D1),
ply!(E1 - D2),
ply!(E1 - E2),
ply!(E1 - F1),
ply!(E1 - F2),
// No castling moves - cannot castle through check
]
);
}
#[test]
fn white_king_castling_blocked_by_pieces_ai_claude() {
let board = test_board!(
White King on E1,
White Rook on H1,
White Knight on G1, // Blocks kingside castling
);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
ply!(E1 - D1),
ply!(E1 - D2),
ply!(E1 - E2),
ply!(E1 - F1),
ply!(E1 - F2),
// No castling - path blocked by knight
]
);
}
#[test]
fn black_king_movement_ai_claude() {
let board = test_board!(Black King on E8);
assert_move_list!(
KingMoveGenerator::new(&board, Some(Color::Black)),
[
ply!(E8 - D7),
ply!(E8 - D8),
ply!(E8 - E7),
ply!(E8 - F7),
ply!(E8 - F8),
]
);
}
#[test]
fn white_king_surrounded_by_enemies_ai_claude() {
let board = test_board!(
White King on E4,
Black Pawn on D3,
Black Pawn on D4,
Black Pawn on D5,
Black Pawn on E3,
Black Pawn on E5,
Black Pawn on F3,
Black Pawn on F4,
Black Pawn on F5
);
assert_move_list!(
KingMoveGenerator::new(&board, None),
[
// Can capture all surrounding enemy pieces
ply!(E4 x D3),
ply!(E4 x D4),
ply!(E4 x D5),
ply!(E4 x E3),
ply!(E4 x E5),
ply!(E4 x F3),
ply!(E4 x F4),
ply!(E4 x F5),
]
);
}
}

View file

@ -1,168 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use super::GeneratedMove;
use crate::Move;
use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard};
use chessfriend_board::Board;
use chessfriend_core::{Color, Square};
use std::iter::FusedIterator;
#[must_use]
pub struct KnightMoveGenerator {
origin_iterator: TrailingBitScanner,
target_iterator: Option<TrailingBitScanner>,
current_origin: Option<Square>,
friends: BitBoard,
enemies: BitBoard,
}
impl KnightMoveGenerator {
pub fn new(board: &Board, color: Option<Color>) -> Self {
let color = board.unwrap_color(color);
let knights = board.knights(color);
let friends = board.friendly_occupancy(color);
let enemies = board.enemies(color);
Self {
origin_iterator: knights.occupied_squares_trailing(),
target_iterator: None,
current_origin: None,
friends,
enemies,
}
}
}
impl Iterator for KnightMoveGenerator {
type Item = GeneratedMove;
fn next(&mut self) -> Option<Self::Item> {
if self.current_origin.is_none() {
self.current_origin = self.origin_iterator.next();
}
let origin = self.current_origin?;
let target_iterator = self.target_iterator.get_or_insert_with(|| {
let knight_moves = BitBoard::knight_moves(origin);
knight_moves.occupied_squares_trailing()
});
if let Some(target) = target_iterator.next() {
let target_bitboard: BitBoard = target.into();
if (target_bitboard & self.friends).is_populated() {
self.next()
} else if (target_bitboard & self.enemies).is_populated() {
let ply = Move::capture(origin, target);
Some(ply.into())
} else {
let ply = Move::quiet(origin, target);
Some(ply.into())
}
} else {
self.current_origin = None;
self.target_iterator = None;
self.next()
}
}
}
impl FusedIterator for KnightMoveGenerator {}
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_move_list, ply};
use chessfriend_board::test_board;
use chessfriend_core::Color;
#[test]
fn white_f5_moves() {
let board = test_board!(White Knight on F5);
assert_move_list!(
KnightMoveGenerator::new(&board, Some(Color::White)),
[
ply!(F5 - G7),
ply!(F5 - H6),
ply!(F5 - H4),
ply!(F5 - G3),
ply!(F5 - E3),
ply!(F5 - D4),
ply!(F5 - D6),
ply!(F5 - E7),
]
);
}
#[test]
fn white_f5_and_c6_moves() {
let board = test_board!(
White Knight on C6,
White Knight on F5,
);
assert_move_list!(
KnightMoveGenerator::new(&board, Some(Color::White)),
[
ply!(C6 - D8),
ply!(C6 - D8),
ply!(C6 - E7),
ply!(C6 - E5),
ply!(C6 - D4),
ply!(C6 - B4),
ply!(C6 - A5),
ply!(C6 - A7),
ply!(C6 - B8),
ply!(F5 - G7),
ply!(F5 - H6),
ply!(F5 - H4),
ply!(F5 - G3),
ply!(F5 - E3),
ply!(F5 - D4),
ply!(F5 - D6),
ply!(F5 - E7),
]
);
}
#[test]
fn white_f5_moves_with_blockers() {
let board = test_board!(
White Knight on F5,
White Queen on G3,
White Bishop on D6,
);
assert_move_list!(
KnightMoveGenerator::new(&board, Some(Color::White)),
[
ply!(F5 - G7),
ply!(F5 - H6),
ply!(F5 - H4),
ply!(F5 - E3),
ply!(F5 - D4),
ply!(F5 - E7),
]
);
}
#[test]
fn white_f5_moves_with_captures() {
let board = test_board!(
White Knight on F5,
White Queen on G3,
White Bishop on D6,
Black Queen on D4,
);
assert_move_list!(
KnightMoveGenerator::new(&board, Some(Color::White)),
[
ply!(F5 - G7),
ply!(F5 - H6),
ply!(F5 - H4),
ply!(F5 - E3),
ply!(F5 - E7),
ply!(F5 x D4),
]
);
}
}

View file

@ -1,539 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
//! Implements a move generator for pawns.
use super::GeneratedMove;
use crate::{Move, PromotionShape};
use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard};
use chessfriend_board::Board;
use chessfriend_core::{Color, Direction, Rank, Square};
use std::{iter::FusedIterator, slice};
pub struct PawnMoveGenerator {
color: Color,
single_pushes: BitBoard,
double_pushes: BitBoard,
left_captures: BitBoard,
right_captures: BitBoard,
en_passant: BitBoard,
target_iterator: TrailingBitScanner,
promotion_iterator: Option<PromotionIterator>,
move_type: MoveType,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MoveType {
SinglePushes,
DoublePushes,
LeftCaptures,
RightCaptures,
}
struct PromotionIterator {
origin: Square,
target: Square,
iterator: slice::Iter<'static, PromotionShape>,
}
impl PawnMoveGenerator {
#[must_use]
pub fn new(board: &Board, color: Option<Color>) -> Self {
let color = board.unwrap_color(color);
let pawns = board.pawns(color);
let occupied = board.occupancy();
let empty = !occupied;
let enemies = board.enemies(color);
// En passant captures present a particular challenge. Include the
// target e.p. square when computing captures (i.e. treat it like an
// enemy piece is on that square) but do not include it when generating
// capture moves. If it is included, a regular capture move will be
// generated where a special e.p. move should be created instead.
//
// So, include it in the enemies set when computing captures, then
// remove it from the left and right captures bitboards before passing
// them into the move generator. Additionally, include the target e.p.
// square in the e.p. bitboard iff the capture bitboards include it.
let en_passant: BitBoard = board.en_passant_target().into();
let (single_pushes, double_pushes) = Self::pushes(pawns, color, empty);
let (left_captures, right_captures) = Self::captures(pawns, color, enemies | en_passant);
Self {
color,
single_pushes,
double_pushes,
left_captures,
right_captures,
en_passant,
target_iterator: single_pushes.occupied_squares_trailing(),
promotion_iterator: None,
move_type: MoveType::SinglePushes,
}
}
fn pushes(pawns: BitBoard, color: Color, empty: BitBoard) -> (BitBoard, BitBoard) {
match color {
Color::White => {
const THIRD_RANK: BitBoard = BitBoard::rank(Rank::THREE);
let single_pushes = pawns.shift_north_one() & empty;
let double_pushes = (single_pushes & THIRD_RANK).shift_north_one() & empty;
(single_pushes, double_pushes)
}
Color::Black => {
const SIXTH_RANK: BitBoard = BitBoard::rank(Rank::SIX);
let single_pushes = pawns.shift_south_one() & empty;
let double_pushes = (single_pushes & SIXTH_RANK).shift_south_one() & empty;
(single_pushes, double_pushes)
}
}
}
fn captures(pawns: BitBoard, color: Color, enemies: BitBoard) -> (BitBoard, BitBoard) {
match color {
Color::White => {
let left_captures = pawns.shift_north_west_one() & enemies;
let right_captures = pawns.shift_north_east_one() & enemies;
(left_captures, right_captures)
}
Color::Black => {
let left_captures = pawns.shift_south_east_one() & enemies;
let right_captures = pawns.shift_south_west_one() & enemies;
(left_captures, right_captures)
}
}
}
fn calculate_origin_square(&self, target: Square) -> Option<Square> {
match self.move_type {
MoveType::SinglePushes => match self.color {
Color::White => target.neighbor(Direction::South),
Color::Black => target.neighbor(Direction::North),
},
MoveType::DoublePushes => match self.color {
Color::White => target
.neighbor(Direction::South)?
.neighbor(Direction::South),
Color::Black => target
.neighbor(Direction::North)?
.neighbor(Direction::North),
},
MoveType::LeftCaptures => match self.color {
Color::White => target.neighbor(Direction::SouthEast),
Color::Black => target.neighbor(Direction::NorthWest),
},
MoveType::RightCaptures => match self.color {
Color::White => target.neighbor(Direction::SouthWest),
Color::Black => target.neighbor(Direction::NorthEast),
},
}
}
fn next_move_type(&mut self) -> Option<MoveType> {
let next_move_type = self.move_type.next();
if let Some(next_move_type) = next_move_type {
let next_bitboard = match next_move_type {
MoveType::SinglePushes => self.single_pushes,
MoveType::DoublePushes => self.double_pushes,
MoveType::LeftCaptures => self.left_captures,
MoveType::RightCaptures => self.right_captures,
};
self.target_iterator = next_bitboard.occupied_squares_trailing();
self.move_type = next_move_type;
}
next_move_type
}
fn next_promotion_move(&mut self) -> Option<GeneratedMove> {
if let Some(promotion_iterator) = self.promotion_iterator.as_mut() {
if let Some(shape) = promotion_iterator.iterator.next() {
let origin = promotion_iterator.origin;
let target = promotion_iterator.target;
return if matches!(
self.move_type,
MoveType::LeftCaptures | MoveType::RightCaptures
) {
Some(Move::capture_promotion(origin, target, *shape).into())
} else {
Some(Move::promotion(origin, target, *shape).into())
};
}
}
None
}
}
impl std::iter::Iterator for PawnMoveGenerator {
type Item = GeneratedMove;
fn next(&mut self) -> Option<Self::Item> {
let next_promotion_move = self.next_promotion_move();
if next_promotion_move.is_some() {
return next_promotion_move;
}
self.promotion_iterator = None;
if let Some(target) = self.target_iterator.next() {
let origin = self
.calculate_origin_square(target)
.expect("bogus origin square");
if target.rank().is_promotable_rank() {
self.promotion_iterator = Some(PromotionIterator {
origin,
target,
iterator: PromotionShape::ALL.iter(),
});
return self.next();
}
match self.move_type {
MoveType::SinglePushes => Some(GeneratedMove {
ply: Move::quiet(origin, target),
}),
MoveType::DoublePushes => Some(GeneratedMove {
ply: Move::double_push(origin, target),
}),
MoveType::LeftCaptures | MoveType::RightCaptures => {
let target_bitboard: BitBoard = target.into();
Some(GeneratedMove {
ply: if (target_bitboard & self.en_passant).is_populated() {
Move::en_passant_capture(origin, target)
} else {
Move::capture(origin, target)
},
})
}
}
} else if self.next_move_type().is_some() {
self.next()
} else {
None
}
}
}
impl FusedIterator for PawnMoveGenerator {}
impl MoveType {
fn next(self) -> Option<Self> {
match self {
MoveType::SinglePushes => Some(MoveType::DoublePushes),
MoveType::DoublePushes => Some(MoveType::LeftCaptures),
MoveType::LeftCaptures => Some(MoveType::RightCaptures),
MoveType::RightCaptures => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move,
};
use chessfriend_board::{fen::FromFenStr, test_board};
use chessfriend_core::{Color, Square};
use std::collections::HashSet;
#[test]
fn black_b7_pushes_and_double_pushes() {
let board = test_board!(Black Pawn on B7);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
assert_eq!(
generated_moves,
[
GeneratedMove {
ply: Move::quiet(Square::B7, Square::B6)
},
GeneratedMove {
ply: Move::double_push(Square::B7, Square::B5)
}
]
.into()
);
}
#[test]
fn black_e2_pushes_and_double_pushes() {
let board = test_board!(White Pawn on E2);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
assert_eq!(
generated_moves,
[
GeneratedMove {
ply: Move::quiet(Square::E2, Square::E3)
},
GeneratedMove {
ply: Move::double_push(Square::E2, Square::E4)
}
]
.into()
);
}
#[test]
fn black_b7_pushes_with_b5_blocker() {
let board = test_board!(Black Pawn on B7, White Bishop on B5);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
assert_eq!(
generated_moves,
[GeneratedMove {
ply: Move::quiet(Square::B7, Square::B6)
}]
.into()
);
}
#[test]
fn black_b7_pushes_with_b6_blocker() {
let board = test_board!(Black Pawn on B7, Black Bishop on B6);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
assert!(generated_moves.is_empty());
}
#[test]
fn white_e2_moves_with_e4_blocker() {
let board = test_board!(White Pawn on E2, White Bishop on E4);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
assert_eq!(
generated_moves,
[GeneratedMove {
ply: Move::quiet(Square::E2, Square::E3)
}]
.into()
);
}
#[test]
fn white_e2_moves_with_e3_blocker() {
let board = test_board!(White Pawn on E2, White Bishop on E3);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
assert!(generated_moves.is_empty());
}
#[test]
fn white_f6_left_captures() {
let white_captures_board = test_board!(White Pawn on F6, Black Queen on E7);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&white_captures_board, Some(Color::White)).collect();
assert_eq!(
generated_moves,
[
GeneratedMove {
ply: Move::capture(Square::F6, Square::E7)
},
GeneratedMove {
ply: Move::quiet(Square::F6, Square::F7)
}
]
.into()
);
}
#[test]
fn black_d5_left_captures() {
let black_captures_board = test_board!(Black Pawn on D5, White Queen on E4);
let generated_moves = PawnMoveGenerator::new(&black_captures_board, Some(Color::Black));
assert_move_list!(generated_moves, [ply!(D5 x E4), ply!(D5 - D4),]);
}
#[test]
fn white_f6_right_captures() {
let white_captures_board = test_board!(White Pawn on F6, Black Queen on G7);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&white_captures_board, Some(Color::White)).collect();
assert_eq!(
generated_moves,
[
GeneratedMove {
ply: Move::capture(Square::F6, Square::G7)
},
GeneratedMove {
ply: Move::quiet(Square::F6, Square::F7)
}
]
.into()
);
}
#[test]
fn black_d5_right_captures() {
let black_captures_board = test_board!(Black Pawn on D5, White Queen on C4);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&black_captures_board, Some(Color::Black)).collect();
assert_eq!(
generated_moves,
[
GeneratedMove {
ply: Move::capture(Square::D5, Square::C4)
},
GeneratedMove {
ply: Move::quiet(Square::D5, Square::D4)
}
]
.into()
);
}
#[test]
fn white_g7_push_promotions() {
let board = test_board!(White Pawn on G7);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
assert_eq!(
generated_moves,
[
Move::promotion(Square::G7, Square::G8, PromotionShape::Knight).into(),
Move::promotion(Square::G7, Square::G8, PromotionShape::Bishop).into(),
Move::promotion(Square::G7, Square::G8, PromotionShape::Rook).into(),
Move::promotion(Square::G7, Square::G8, PromotionShape::Queen).into(),
]
.into()
);
}
#[test]
fn white_d7_push_and_capture_promotions() {
let board = test_board!(
White Pawn on D7,
Black Queen on E8
);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
assert_eq!(
generated_moves,
[
Move::promotion(Square::D7, Square::D8, PromotionShape::Knight).into(),
Move::promotion(Square::D7, Square::D8, PromotionShape::Bishop).into(),
Move::promotion(Square::D7, Square::D8, PromotionShape::Rook).into(),
Move::promotion(Square::D7, Square::D8, PromotionShape::Queen).into(),
Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Knight).into(),
Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Bishop).into(),
Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Rook).into(),
Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Queen).into(),
]
.into()
);
}
#[test]
fn black_a2_push_promotions() {
let board = test_board!(Black Pawn on A2);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
assert_eq!(
generated_moves,
[
Move::promotion(Square::A2, Square::A1, PromotionShape::Knight).into(),
Move::promotion(Square::A2, Square::A1, PromotionShape::Bishop).into(),
Move::promotion(Square::A2, Square::A1, PromotionShape::Rook).into(),
Move::promotion(Square::A2, Square::A1, PromotionShape::Queen).into(),
]
.into()
);
}
#[test]
fn black_h2_push_and_capture_promotions() {
let board = test_board!(
Black Pawn on H2,
White Queen on G1,
);
let generated_moves: HashSet<GeneratedMove> =
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
assert_eq!(
generated_moves,
[
Move::promotion(Square::H2, Square::H1, PromotionShape::Knight).into(),
Move::promotion(Square::H2, Square::H1, PromotionShape::Bishop).into(),
Move::promotion(Square::H2, Square::H1, PromotionShape::Rook).into(),
Move::promotion(Square::H2, Square::H1, PromotionShape::Queen).into(),
Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Knight).into(),
Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Bishop).into(),
Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Rook).into(),
Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Queen).into(),
]
.into()
);
}
#[test]
fn black_e4_captures_d4_en_passant() {
let board = test_board!(Black, [
White Pawn on D4,
Black Pawn on E4
], D3);
let generated_moves = PawnMoveGenerator::new(&board, None);
assert_move_list!(generated_moves, [ply!(E4 - E3), ply!(E4 x D3 e.p.),]);
}
#[test]
fn white_e5_captures_f5_en_passant() {
let board = test_board!(White, [
White Pawn on E5,
Black Pawn on F5
], F6);
let generated_moves = PawnMoveGenerator::new(&board, None);
assert_move_list!(generated_moves, [ply!(E5 - E6), ply!(E5 x F6 e.p.)]);
}
#[test]
fn white_no_en_passant_if_no_pawn() {
let board = test_board!(White, [
White Pawn on A3,
Black Pawn on F5,
], F6);
let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect();
assert_move_list_does_not_contain!(
generated_moves,
[ply!(E5 x F6 e.p.), ply!(G5 x F6 e.p.)]
);
}
#[test]
fn black_no_en_passant_if_no_pawn() {
let board = test_board!(Black, [
White Pawn on A4,
Black Pawn on D4,
], A3);
let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect();
assert_move_list_does_not_contain!(generated_moves, [ply!(B4 x A3 e.p.)]);
}
#[test]
fn black_en_passant_check() {
let board = Board::from_fen_str("8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3").expect("invalid fen");
println!("{}", board.display());
let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect();
assert_move_list_contains!(generated_moves, [ply!(C4 x D3 e.p.)]);
}
}

View file

@ -1,612 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
//! Sliders in chess are the pieces that move (a.k.a. "slide") along straight
//! line paths. Bishops, Rooks, and Queens all do this. All of these pieces
//! function identically, though with different sets of rays emanating outward
//! from their origin squares: rooks along orthogonal lines, bishops along
//! diagonals, and queens along both orthogonal and diagonal lines.
//!
//! This module implements the [`SliderMoveGenerator`] which iterates all the
//! slider moves from a given square. This module also exports
//! [`BishopMoveGenerator`], [`RookMoveGenerator`], and [`QueenMoveGenerator`]
//! that emit moves for their corresponding pieces.
use super::GeneratedMove;
use crate::Move;
use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard};
use chessfriend_board::Board;
use chessfriend_core::{Color, Slider, Square};
use std::iter::FusedIterator;
macro_rules! slider_move_generator {
($vis:vis $name:ident, $slider:ident) => {
#[must_use]
$vis struct $name(SliderMoveGenerator);
impl $name {
pub fn new(board: &Board, color: Option<Color>) -> Self {
Self(SliderMoveGenerator::new(board, Slider::$slider, color))
}
}
impl Iterator for $name {
type Item = $crate::GeneratedMove;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
impl FusedIterator for $name {}
};
}
slider_move_generator!(pub BishopMoveGenerator, Bishop);
slider_move_generator!(pub RookMoveGenerator, Rook);
slider_move_generator!(pub QueenMoveGenerator, Queen);
#[must_use]
struct SliderMoveGenerator {
sliders: Vec<SliderInfo>,
next_sliders_index: usize,
current_slider: Option<SliderInfo>,
enemies: BitBoard,
friends: BitBoard,
}
impl SliderMoveGenerator {
fn new(board: &Board, slider: Slider, color: Option<Color>) -> Self {
let color = board.unwrap_color(color);
let pieces = match slider {
Slider::Bishop => board.bishops(color),
Slider::Rook => board.rooks(color),
Slider::Queen => board.queens(color),
};
let enemies = board.enemies(color);
let friends = board.friendly_occupancy(color);
let sliders = pieces
.occupied_squares_trailing()
.map(|origin| SliderInfo::new(board, origin, slider, color))
.collect();
Self {
sliders,
next_sliders_index: 0,
current_slider: None,
enemies,
friends,
}
}
}
impl Iterator for SliderMoveGenerator {
type Item = GeneratedMove;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.current_slider.is_none() {
if self.next_sliders_index < self.sliders.len() {
self.current_slider = Some(self.sliders[self.next_sliders_index].clone());
self.next_sliders_index += 1;
} else {
return None;
}
}
if let Some(current_slider) = self.current_slider.as_mut() {
if let Some(target) = current_slider.next() {
let target_bitboard: BitBoard = target.into();
let is_targeting_friendly_piece =
(target_bitboard & self.friends).is_populated();
if is_targeting_friendly_piece {
continue;
}
let is_targeting_enemy_piece = (target_bitboard & self.enemies).is_populated();
return Some(if is_targeting_enemy_piece {
Move::capture(current_slider.origin, target).into()
} else {
Move::quiet(current_slider.origin, target).into()
});
}
self.current_slider = None;
}
}
}
}
impl FusedIterator for SliderMoveGenerator {}
#[derive(Clone, Debug)]
struct SliderInfo {
origin: Square,
iterator: TrailingBitScanner,
}
impl SliderInfo {
fn new(board: &Board, origin: Square, slider: Slider, color: Color) -> Self {
let color = Some(color);
let sight = match slider {
Slider::Bishop => board.bishop_sight(origin, color),
Slider::Rook => board.rook_sight(origin, color),
Slider::Queen => board.queen_sight(origin, color),
};
Self {
origin,
iterator: sight.occupied_squares_trailing(),
}
}
}
impl Iterator for SliderInfo {
type Item = Square;
fn next(&mut self) -> Option<Self::Item> {
self.iterator.next()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_move_list, ply};
use chessfriend_board::test_board;
#[test]
fn white_b5_rook() {
let board = test_board!(White Rook on B5);
assert_move_list!(
RookMoveGenerator::new(&board, None),
[
ply!(B5 - A5),
ply!(B5 - C5),
ply!(B5 - D5),
ply!(B5 - E5),
ply!(B5 - F5),
ply!(B5 - G5),
ply!(B5 - H5),
ply!(B5 - B8),
ply!(B5 - B7),
ply!(B5 - B6),
ply!(B5 - B4),
ply!(B5 - B3),
ply!(B5 - B2),
ply!(B5 - B1),
]
);
}
#[test]
fn black_f4_bishop() {
let board = test_board!(White Bishop on F4);
assert_move_list!(
BishopMoveGenerator::new(&board, None),
[
ply!(F4 - B8),
ply!(F4 - C7),
ply!(F4 - D6),
ply!(F4 - E5),
ply!(F4 - H6),
ply!(F4 - G5),
ply!(F4 - C1),
ply!(F4 - D2),
ply!(F4 - E3),
ply!(F4 - H2),
ply!(F4 - G3),
]
);
}
#[test]
fn white_d4_queen_ai_claude() {
let board = test_board!(White Queen on D4);
assert_move_list!(
QueenMoveGenerator::new(&board, None),
[
// Horizontal moves (rook-like)
ply!(D4 - A4),
ply!(D4 - B4),
ply!(D4 - C4),
ply!(D4 - E4),
ply!(D4 - F4),
ply!(D4 - G4),
ply!(D4 - H4),
// Vertical moves (rook-like)
ply!(D4 - D1),
ply!(D4 - D2),
ply!(D4 - D3),
ply!(D4 - D5),
ply!(D4 - D6),
ply!(D4 - D7),
ply!(D4 - D8),
// Diagonal moves (bishop-like)
ply!(D4 - A1),
ply!(D4 - B2),
ply!(D4 - C3),
ply!(D4 - E5),
ply!(D4 - F6),
ply!(D4 - G7),
ply!(D4 - H8),
ply!(D4 - A7),
ply!(D4 - B6),
ply!(D4 - C5),
ply!(D4 - E3),
ply!(D4 - F2),
ply!(D4 - G1),
]
);
}
#[test]
fn white_f3_bishop_with_capture_ai_claude() {
let board = test_board!(
White Bishop on F3,
Black Pawn on C6
);
assert_move_list!(
BishopMoveGenerator::new(&board, None),
[
// Diagonal towards C6 (stops at capture)
ply!(F3 - E4),
ply!(F3 - D5),
ply!(F3 x C6),
// Diagonal towards H1
ply!(F3 - G2),
ply!(F3 - H1),
// Diagonal towards A8
ply!(F3 - G4),
ply!(F3 - H5),
// Diagonal towards E2
ply!(F3 - E2),
ply!(F3 - D1),
]
);
}
#[test]
fn white_e6_rook_with_capture_ai_claude() {
let board = test_board!(
White Rook on E6,
Black Knight on E3
);
assert_move_list!(
RookMoveGenerator::new(&board, None),
[
// Horizontal moves
ply!(E6 - A6),
ply!(E6 - B6),
ply!(E6 - C6),
ply!(E6 - D6),
ply!(E6 - F6),
ply!(E6 - G6),
ply!(E6 - H6),
// Vertical moves up
ply!(E6 - E7),
ply!(E6 - E8),
// Vertical moves down (stops at capture)
ply!(E6 - E5),
ply!(E6 - E4),
ply!(E6 x E3),
]
);
}
#[test]
fn white_c4_queen_with_capture_ai_claude() {
let board = test_board!(
White Queen on C4,
Black Bishop on F7
);
assert_move_list!(
QueenMoveGenerator::new(&board, None),
[
// Horizontal moves
ply!(C4 - A4),
ply!(C4 - B4),
ply!(C4 - D4),
ply!(C4 - E4),
ply!(C4 - F4),
ply!(C4 - G4),
ply!(C4 - H4),
// Vertical moves
ply!(C4 - C1),
ply!(C4 - C2),
ply!(C4 - C3),
ply!(C4 - C5),
ply!(C4 - C6),
ply!(C4 - C7),
ply!(C4 - C8),
// Diagonal moves (A2-G8 diagonal)
ply!(C4 - A2),
ply!(C4 - B3),
ply!(C4 - D5),
ply!(C4 - E6),
ply!(C4 x F7),
// Diagonal moves (F1-A6 diagonal)
ply!(C4 - B5),
ply!(C4 - A6),
ply!(C4 - D3),
ply!(C4 - E2),
ply!(C4 - F1),
]
);
}
#[test]
fn white_rook_blocked_by_friendly_piece_ai_claude() {
let board = test_board!(White Rook on D4, White Pawn on D6);
assert_move_list!(
RookMoveGenerator::new(&board, None),
[
// Horizontal moves (unblocked)
ply!(D4 - A4),
ply!(D4 - B4),
ply!(D4 - C4),
ply!(D4 - E4),
ply!(D4 - F4),
ply!(D4 - G4),
ply!(D4 - H4),
// Vertical moves down (unblocked)
ply!(D4 - D1),
ply!(D4 - D2),
ply!(D4 - D3),
// Vertical moves up (blocked by friendly pawn on D6)
ply!(D4 - D5),
// Cannot move to D6 (occupied by friendly piece)
// Cannot move to D7 or D8 (blocked by friendly piece on D6)
]
);
}
#[test]
fn white_bishop_blocked_by_friendly_pieces_ai_claude() {
let board = test_board!(
White Bishop on E4,
White Knight on C2, // Blocks one diagonal
White Pawn on G6 // Blocks another diagonal
);
assert_move_list!(
BishopMoveGenerator::new(&board, None),
[
// Diagonal towards H1 (unblocked)
ply!(E4 - F3),
ply!(E4 - G2),
ply!(E4 - H1),
// Diagonal towards A8 (blocked by pawn on G6)
ply!(E4 - F5),
// Cannot move to G6 (friendly pawn)
// Cannot move to H7 (blocked by friendly pawn)
// Diagonal towards H7 is blocked at G6
// Diagonal towards A8 (unblocked on the other side)
ply!(E4 - D5),
ply!(E4 - C6),
ply!(E4 - B7),
ply!(E4 - A8),
// Diagonal towards D3 (blocked by knight on C2)
ply!(E4 - D3),
// Cannot move to C2 (friendly knight)
// Cannot move to B1 (blocked by friendly knight)
]
);
}
#[test]
fn white_queen_multiple_friendly_blocks_ai_claude() {
let board = test_board!(
White Queen on D4,
White Pawn on D6, // Blocks vertical up
White Bishop on F4, // Blocks horizontal right
White Knight on F6 // Blocks diagonal
);
assert_move_list!(
QueenMoveGenerator::new(&board, None),
[
// Horizontal moves left (unblocked)
ply!(D4 - A4),
ply!(D4 - B4),
ply!(D4 - C4),
// Horizontal moves right (blocked by bishop on F4)
ply!(D4 - E4),
// Cannot move to F4 (friendly bishop)
// Cannot move to G4, H4 (blocked by friendly bishop)
// Vertical moves down (unblocked)
ply!(D4 - D1),
ply!(D4 - D2),
ply!(D4 - D3),
// Vertical moves up (blocked by pawn on D6)
ply!(D4 - D5),
// Cannot move to D6 (friendly pawn)
// Cannot move to D7, D8 (blocked by friendly pawn)
// Diagonal moves (some blocked, some not)
// Towards A1
ply!(D4 - C3),
ply!(D4 - B2),
ply!(D4 - A1),
// Towards G1
ply!(D4 - E3),
ply!(D4 - F2),
ply!(D4 - G1),
// Towards A7
ply!(D4 - C5),
ply!(D4 - B6),
ply!(D4 - A7),
// Towards G7 (blocked by knight on F6)
ply!(D4 - E5),
// Cannot move to F6 (friendly knight)
// Cannot move to G7, H8 (blocked by friendly knight)
]
);
}
#[test]
fn rook_ray_stops_at_first_piece_ai_claude() {
let board = test_board!(
White Rook on A1,
Black Pawn on A4, // First obstruction
Black Queen on A7 // Should be unreachable
);
assert_move_list!(
RookMoveGenerator::new(&board, None),
[
// Horizontal moves (unblocked)
ply!(A1 - B1),
ply!(A1 - C1),
ply!(A1 - D1),
ply!(A1 - E1),
ply!(A1 - F1),
ply!(A1 - G1),
ply!(A1 - H1),
// Vertical moves up (terminated at A4)
ply!(A1 - A2),
ply!(A1 - A3),
ply!(A1 x A4),
// Cannot reach A5, A6, A7, A8 due to pawn blocking at A4
]
);
}
#[test]
fn bishop_ray_terminated_by_enemy_piece() {
let board = test_board!(
White Bishop on C1,
Black Knight on F4, // Terminates one diagonal
Black Rook on H6, // Should be unreachable behind the knight
);
assert_move_list!(
BishopMoveGenerator::new(&board, None),
[
// Diagonal towards A3
ply!(C1 - B2),
ply!(C1 - A3),
// Diagonal towards H6 (terminated at F4)
ply!(C1 - D2),
ply!(C1 - E3),
ply!(C1 x F4),
// Cannot reach G5, H6 due to knight blocking at F4
]
);
}
#[test]
fn queen_multiple_ray_terminations_ai_claude() {
let board = test_board!(
White Queen on D4,
Black Pawn on D7, // Terminates vertical ray
Black Bishop on A7, // Capturable along diagonal
Black Knight on G4, // Terminates horizontal ray
Black Rook on H4, // Capturable along horizontal ray
White Pawn on F6, // Terminates diagonal ray (friendly)
Black Queen on H8,
);
assert_move_list!(
QueenMoveGenerator::new(&board, None),
[
// Horizontal moves left (unblocked)
ply!(D4 - A4),
ply!(D4 - B4),
ply!(D4 - C4),
// Horizontal moves right (terminated at G4)
ply!(D4 - E4),
ply!(D4 - F4),
ply!(D4 x G4),
// Cannot reach H4 due to knight blocking
// Vertical moves down (unblocked)
ply!(D4 - D1),
ply!(D4 - D2),
ply!(D4 - D3),
// Vertical moves up (terminated at D7)
ply!(D4 - D5),
ply!(D4 - D6),
ply!(D4 x D7),
// Cannot reach D8 due to pawn blocking
// Diagonal moves towards A1
ply!(D4 - C3),
ply!(D4 - B2),
ply!(D4 - A1),
// Diagonal moves towards G1
ply!(D4 - E3),
ply!(D4 - F2),
ply!(D4 - G1),
// Diagonal moves towards A7
ply!(D4 - C5),
ply!(D4 - B6),
ply!(D4 x A7),
// Diagonal moves towards H8 (terminated at F6 by friendly pawn)
ply!(D4 - E5),
// Cannot move to F6 (friendly pawn)
// Cannot reach G7, H8 due to friendly pawn blocking
]
);
}
#[test]
fn rook_chain_of_pieces_ai_claude() {
let board = test_board!(
White Rook on A1,
Black Pawn on A3, // First enemy piece
White Knight on A5, // Friendly piece behind enemy
Black Queen on A7 // Enemy piece behind friendly
);
assert_move_list!(
RookMoveGenerator::new(&board, None),
[
// Horizontal moves (unblocked)
ply!(A1 - B1),
ply!(A1 - C1),
ply!(A1 - D1),
ply!(A1 - E1),
ply!(A1 - F1),
ply!(A1 - G1),
ply!(A1 - H1),
// Vertical moves (ray terminates at first piece)
ply!(A1 - A2),
ply!(A1 x A3),
// Cannot reach A4, A5, A6, A7, A8 - ray terminated at A3
]
);
}
#[test]
fn bishop_ray_termination_all_directions_ai_claude() {
let board = test_board!(
White Bishop on D4,
Black Pawn on B2, // Terminates towards A1
Black Knight on F6, // Terminates towards H8
Black Rook on B6, // Terminates towards A7
Black Queen on F2 // Terminates towards G1
);
assert_move_list!(
BishopMoveGenerator::new(&board, None),
[
// Diagonal towards A1 (terminated at B2)
ply!(D4 - C3),
ply!(D4 x B2), // Capture
// Cannot reach A1
// Diagonal towards H8 (terminated at F6)
ply!(D4 - E5),
ply!(D4 x F6), // Capture
// Cannot reach G7, H8
// Diagonal towards A7 (terminated at B6)
ply!(D4 - C5),
ply!(D4 x B6), // Capture
// Cannot reach A7
// Diagonal towards G1 (terminated at F2)
ply!(D4 - E3),
ply!(D4 x F2),
// Cannot reach G1
]
);
}
}

View file

@ -1,19 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
pub mod algebraic;
pub mod generators;
pub mod testing;
mod defs;
mod macros;
mod make_move;
mod moves;
mod record;
mod unmake_move;
pub use defs::{Kind, PromotionShape};
pub use generators::GeneratedMove;
pub use make_move::{MakeMove, MakeMoveError, MakeMoveResult, ValidateMove};
pub use moves::Move;
pub use record::MoveRecord;
pub use unmake_move::{UnmakeMove, UnmakeMoveError, UnmakeMoveResult};

View file

@ -1,77 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
#[macro_export]
macro_rules! assert_move_list {
($generator:expr, [ $($expected:expr),* $(,)? ]) => {
{
let generated_moves: std::collections::HashSet<$crate::GeneratedMove> = $generator.collect();
let expected_moves: std::collections::HashSet<$crate::GeneratedMove> = [
$($expected.into(),)*
].into();
assert_eq!(
generated_moves,
expected_moves,
"\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}",
generated_moves
.intersection(&expected_moves)
.map(|mv| format!("{}", mv))
.collect::<Vec<String>>(),
generated_moves
.difference(&expected_moves)
.map(|mv| format!("{}", mv))
.collect::<Vec<String>>(),
expected_moves
.difference(&generated_moves)
.map(|mv| format!("{}", mv))
.collect::<Vec<String>>(),
);
}
};
($generator:expr, $expected:expr) => {
{
use std::collections::HashSet;
let generated_moves: HashSet<$crate::GeneratedMove> = $generator.collect();
let expected_moves: HashSet<$crate::GeneratedMove> = $expected.collect();
assert_eq!(
generated_moves,
expected_moves,
"\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}",
generated_moves
.intersection(&expected_moves)
.map(|mv| format!("{}", mv))
.collect::<Vec<String>>(),
generated_moves
.difference(&expected_moves)
.map(|mv| format!("{}", mv))
.collect::<Vec<String>>(),
expected_moves
.difference(&generated_moves)
.map(|mv| format!("{}", mv))
.collect::<Vec<String>>(),
);
}
};
}
#[macro_export]
macro_rules! assert_move_list_contains {
($generated_moves:expr, [ $($expected:expr),* $(,)? ]) => {
$(
assert!($generated_moves.contains(&$expected.into()));
)*
};
}
#[macro_export]
macro_rules! assert_move_list_does_not_contain {
($generated_moves:expr, [ $($expected:expr),* $(,)? ]) => {
{
$(
assert!(!$generated_moves.contains(&$expected.into()));
)*
}
};
}

View file

@ -1,589 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{Move, MoveRecord};
use chessfriend_board::{
Board, BoardProvider, CastleParameters, PlacePieceError, PlacePieceStrategy,
castle::{CastleEvaluationError, CastleRightsOption},
movement::Movement,
};
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
use thiserror::Error;
pub type MakeMoveResult = Result<MoveRecord, MakeMoveError>;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ValidateMove {
#[default]
No,
Yes,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum MakeMoveError {
#[error("no piece on {0}")]
NoPiece(Square),
#[error("{piece} on {square} is not of active color")]
NonActiveColor { piece: Piece, square: Square },
#[error("{0} cannot make move")]
InvalidPiece(Piece),
#[error("cannot capture piece on {0}")]
InvalidCapture(Square),
#[error("cannot capture en passant on {0}")]
InvalidEnPassantCapture(Square),
#[error("no capture square")]
NoCaptureSquare,
#[error("no piece to capture on {0}")]
NoCapturePiece(Square),
#[error("{piece} on {origin} cannot move to {target}")]
NoMove {
piece: Piece,
origin: Square,
target: Square,
},
#[error("{0}")]
PlacePieceError(#[from] PlacePieceError),
#[error("{0}")]
CastleError(#[from] CastleEvaluationError),
#[error("cannot promote on {0}")]
InvalidPromotion(Square),
#[error("move to {0} requires promotion")]
PromotionRequired(Square),
}
pub trait MakeMove {
/// Make a move.
///
/// ## Errors
///
/// Returns one of [`MakeMoveError`] indicating why the move could not be
/// made.
///
fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult;
}
trait MakeMoveInternal {
fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult;
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult;
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult;
fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult;
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult;
fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError>;
fn validate_active_piece(&self, ply: Move) -> Result<Piece, MakeMoveError>;
fn advance_board_state(
&mut self,
ply: &Move,
piece_moved: &Piece,
en_passant_target: Option<Square>,
half_move_clock: HalfMoveClock,
);
}
impl<T: BoardProvider> MakeMove for T {
/// Make a move in the position.
///
/// ## Errors
///
/// If `validate` is [`ValidateMove::Yes`], perform validation of move
/// correctness prior to applying the move. See [`Position::validate_move`].
fn make_move(
&mut self,
ply: Move,
validate: ValidateMove,
) -> Result<MoveRecord, MakeMoveError> {
if ply.is_quiet() {
self.validate_move(ply, validate)?;
return self.make_quiet_move(ply);
}
if ply.is_double_push() {
self.validate_move(ply, validate)?;
return self.make_double_push_move(ply);
}
if ply.is_capture() {
self.validate_move(ply, validate)?;
return self.make_capture_move(ply);
}
if let Some(wing) = ply.castle_wing() {
return self.make_castle_move(ply, wing);
}
if ply.is_promotion() {
self.validate_move(ply, validate)?;
return self.make_promotion_move(ply);
}
unreachable!();
}
}
impl<T: BoardProvider> MakeMoveInternal for T {
fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult {
let board = self.board_mut();
let origin = ply.origin_square();
let piece = board
.get_piece(origin)
.ok_or(MakeMoveError::NoPiece(origin))?;
let target = ply.target_square();
if piece.is_pawn() && target.rank().is_promotable_rank() {
return Err(MakeMoveError::PromotionRequired(target));
}
board
.place_piece(piece, target, PlacePieceStrategy::PreserveExisting)
.map_err(MakeMoveError::PlacePieceError)?;
board.remove_piece(origin);
let record = MoveRecord::new(board, ply, None);
self.advance_board_state(&ply, &piece, None, HalfMoveClock::Advance);
Ok(record)
}
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult {
let board = self.board_mut();
let origin = ply.origin_square();
let piece = board
.remove_piece(origin)
.ok_or(MakeMoveError::NoPiece(origin))?;
let target = ply.target_square();
board
.place_piece(piece, target, PlacePieceStrategy::PreserveExisting)
.map_err(MakeMoveError::PlacePieceError)?;
// Capture move record before setting the en passant square, to ensure
// board state before the change is preserved.
let record = MoveRecord::new(board, ply, None);
let en_passant_target = match target.rank() {
Rank::FOUR => Square::from_file_rank(target.file(), Rank::THREE),
Rank::FIVE => Square::from_file_rank(target.file(), Rank::SIX),
_ => unreachable!(),
};
self.advance_board_state(
&ply,
&piece,
Some(en_passant_target),
HalfMoveClock::Advance,
);
Ok(record)
}
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult {
let origin_square = ply.origin_square();
let target_square = ply.target_square();
let board = self.board_mut();
let piece = board
.get_piece(origin_square)
.ok_or(MakeMoveError::NoPiece(origin_square))?;
if ply.is_en_passant() {
let en_passant_square = board
.en_passant_target()
.ok_or(MakeMoveError::NoCaptureSquare)?;
if target_square != en_passant_square {
return Err(MakeMoveError::InvalidEnPassantCapture(target_square));
}
}
let board = self.board_mut();
let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?;
let captured_piece = board
.remove_piece(capture_square)
.ok_or(MakeMoveError::NoCapturePiece(capture_square))?;
board.remove_piece(origin_square).unwrap();
if let Some(promotion_shape) = ply.promotion_shape() {
let promoted_piece = Piece::new(piece.color, promotion_shape);
board.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?;
} else {
board.place_piece(piece, target_square, PlacePieceStrategy::Replace)?;
}
let record = MoveRecord::new(board, ply, Some(captured_piece));
self.advance_board_state(&ply, &piece, None, HalfMoveClock::Reset);
Ok(record)
}
fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult {
let board = self.board_mut();
board.color_can_castle(wing, None)?;
let active_color = board.active_color();
let parameters = Board::castling_parameters(wing, active_color);
let king = board.remove_piece(parameters.origin.king).unwrap();
board.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?;
let rook = board.remove_piece(parameters.origin.rook).unwrap();
board.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
// Capture move record before revoking castling rights to ensure
// original board state is preserved.
let record = MoveRecord::new(board, ply, None);
board.revoke_castling_rights_active(wing.into());
self.advance_board_state(&ply, &king, None, HalfMoveClock::Advance);
Ok(record)
}
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult {
let board = self.board_mut();
let origin = ply.origin_square();
let piece = board
.get_piece(origin)
.ok_or(MakeMoveError::NoPiece(origin))?;
if !piece.is_pawn() {
return Err(MakeMoveError::InvalidPiece(piece));
}
let target = ply.target_square();
if !target.rank().is_promotable_rank() {
return Err(MakeMoveError::InvalidPromotion(target));
}
if let Some(promotion_shape) = ply.promotion_shape() {
board.remove_piece(origin);
let promoted_piece = Piece::new(piece.color, promotion_shape);
board.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?;
} else {
unreachable!(
"Cannot make a promotion move with a ply that has no promotion shape: {ply:?}",
);
}
let record = MoveRecord::new(board, ply, None);
self.advance_board_state(&ply, &piece, None, HalfMoveClock::Reset);
Ok(record)
}
fn advance_board_state(
&mut self,
ply: &Move,
piece_moved: &Piece,
en_passant_target: Option<Square>,
half_move_clock: HalfMoveClock,
) {
let board = self.board_mut();
board.set_en_passant_target_option(en_passant_target);
match piece_moved.shape {
Shape::Rook => {
let origin = ply.origin_square();
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_rights(None, Wing::KingSide.into());
}
}
let queenside_parameters =
CastleParameters::get(board.active_color(), Wing::QueenSide);
if origin == queenside_parameters.origin.rook {
board.revoke_castling_rights(None, Wing::QueenSide.into());
}
}
Shape::King => {
board.revoke_castling_rights(None, CastleRightsOption::All);
}
_ => {}
}
let previous_active_color = board.active_color();
let active_color = previous_active_color.next();
board.set_active_color(active_color);
match half_move_clock {
HalfMoveClock::Reset => board.half_move_clock = 0,
HalfMoveClock::Advance => board.half_move_clock += 1,
}
if active_color == Color::White {
board.full_move_number += 1;
}
}
fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> {
if validate == ValidateMove::No {
return Ok(());
}
let active_piece = self.validate_active_piece(ply)?;
let board = self.board();
let origin_square = ply.origin_square();
let target_square = ply.target_square();
// Pawns can see squares they can't move to. So, calculating valid
// squares requires a concept that includes Sight, but adds pawn pushes.
// In ChessFriend, that concept is Movement.
let movement = active_piece.movement(origin_square, board);
if !movement.contains(target_square) {
return Err(MakeMoveError::NoMove {
piece: active_piece,
origin: origin_square,
target: target_square,
});
}
// TODO: En Passant capture.
if let Some(capture_square) = ply.capture_square() {
if let Some(captured_piece) = board.get_piece(capture_square) {
if captured_piece.color == active_piece.color {
return Err(MakeMoveError::InvalidCapture(capture_square));
}
} else {
return Err(MakeMoveError::NoPiece(capture_square));
}
}
Ok(())
}
fn validate_active_piece(&self, ply: Move) -> Result<Piece, MakeMoveError> {
let origin_square = ply.origin_square();
let board = self.board();
let active_piece = board
.get_piece(origin_square)
.ok_or(MakeMoveError::NoPiece(origin_square))?;
if active_piece.color != board.active_color() {
return Err(MakeMoveError::NonActiveColor {
piece: active_piece,
square: origin_square,
});
}
Ok(active_piece)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum HalfMoveClock {
Reset,
#[default]
Advance,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Move, PromotionShape};
use chessfriend_board::test_board;
use chessfriend_core::{Color, Square, piece};
type TestResult = Result<(), MakeMoveError>;
#[test]
fn make_quiet_move() -> TestResult {
let mut board = test_board!(White Pawn on C2);
let ply = Move::quiet(Square::C2, Square::C3);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.get_piece(Square::C2), None);
assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn)));
assert_eq!(board.active_color(), Color::Black);
assert_eq!(board.half_move_clock, 1);
board.set_active_color(Color::White);
let ply = Move::quiet(Square::C3, Square::C4);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.get_piece(Square::C3), None);
assert_eq!(board.get_piece(Square::C4), Some(piece!(White Pawn)));
assert_eq!(board.active_color(), Color::Black);
assert_eq!(board.half_move_clock, 2);
Ok(())
}
#[test]
fn make_invalid_quiet_pawn_move() {
let mut board = test_board!(White Pawn on C2);
let ply = Move::quiet(Square::C2, Square::D2);
let result = board.make_move(ply, ValidateMove::Yes);
assert!(result.is_err());
assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::D2), None);
assert_eq!(board.active_color(), Color::White);
assert_eq!(board.half_move_clock, 0);
}
#[test]
fn make_capture_move() -> TestResult {
let mut board = test_board![
White Bishop on C2,
Black Rook on F5,
];
let ply = Move::capture(Square::C2, Square::F5);
let result = board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(result.captured_piece, Some(piece!(Black Rook)));
assert_eq!(board.get_piece(Square::C2), None);
assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop)));
assert_eq!(board.active_color(), Color::Black);
assert_eq!(board.half_move_clock, 0);
Ok(())
}
#[test]
fn make_en_passant_capture_move() -> TestResult {
let mut board = test_board![
Black Pawn on F4,
White Pawn on E2
];
let ply = Move::double_push(Square::E2, Square::E4);
let record = board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(record.en_passant_target, None);
assert_eq!(board.get_piece(Square::E2), None);
assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn)));
assert_eq!(
board.en_passant_target(),
Some(Square::E3),
"en passant square not set"
);
assert_eq!(board.active_color(), Color::Black);
assert_eq!(board.half_move_clock, 1);
let ply = Move::en_passant_capture(Square::F4, Square::E3);
let result = board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(result.captured_piece, Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::F4), None);
assert_eq!(board.get_piece(Square::E3), Some(piece!(Black Pawn)));
assert_eq!(
board.get_piece(Square::E4),
None,
"capture target pawn not removed"
);
Ok(())
}
#[test]
fn make_last_rank_quiet_move_without_promotion() {
let mut board = test_board!(
White Pawn on A7
);
let ply = Move::quiet(Square::A7, Square::A8);
let result = board.make_move(ply, ValidateMove::Yes);
assert!(result.is_err());
assert_eq!(board.active_color(), Color::White);
assert_eq!(board.get_piece(Square::A7), Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::A8), None);
assert_eq!(board.half_move_clock, 0);
}
#[test]
fn make_promotion_move() -> TestResult {
let mut board = test_board![
Black Pawn on E7,
White Pawn on F7,
];
let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.get_piece(Square::F7), None);
assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen)));
assert_eq!(board.active_color(), Color::Black);
assert_eq!(board.half_move_clock, 0);
Ok(())
}
#[test]
fn make_white_kingside_castle() -> TestResult {
let mut board = test_board![
White Rook on H1,
White King on E1,
];
let ply = Move::castle(Color::White, Wing::KingSide);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.active_color(), Color::Black);
assert_eq!(board.get_piece(Square::E1), None);
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));
Ok(())
}
#[test]
fn make_white_queenside_castle() -> TestResult {
let mut board = test_board![
White King on E1,
White Rook on A1,
];
let ply = Move::castle(Color::White, Wing::QueenSide);
board.make_move(ply, ValidateMove::Yes)?;
assert_eq!(board.active_color(), Color::Black);
assert_eq!(board.get_piece(Square::E1), None);
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));
Ok(())
}
}

View file

@ -1,292 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::defs::{Kind, PromotionShape};
use chessfriend_bitboard::BitBoard;
use chessfriend_board::CastleParameters;
use chessfriend_core::{Color, Rank, Shape, Square, Wing};
use std::fmt;
#[macro_export]
macro_rules! ply {
($origin:ident - $target:ident) => {
$crate::Move::quiet(
chessfriend_core::Square::$origin,
chessfriend_core::Square::$target,
)
};
($origin:ident -- $target:ident) => {
$crate::Move::double_push(
chessfriend_core::Square::$origin,
chessfriend_core::Square::$target,
)
};
($origin:ident x $target:ident) => {
$crate::Move::capture(
chessfriend_core::Square::$origin,
chessfriend_core::Square::$target,
)
};
($origin:ident x $target:ident e$(.)?p$(.)?) => {
$crate::Move::en_passant_capture(
chessfriend_core::Square::$origin,
chessfriend_core::Square::$target,
)
};
($origin:ident x $target:ident = $promotion:ident) => {
$crate::Move::capture_promotion(
chessfriend_core::Square::$origin,
chessfriend_core::Square::$target,
$crate::PromotionShape::$promotion,
)
};
($origin:ident - $target:ident = $promotion:ident) => {
$crate::Move::promotion(
chessfriend_core::Square::$origin,
chessfriend_core::Square::$target,
$crate::PromotionShape::$promotion,
)
};
($color:ident 0-0) => {
$crate::Move::castle(
chessfriend_core::Color::$color,
chessfriend_core::Wing::KingSide,
)
};
($color:ident 0-0-0) => {
$crate::Move::castle(
chessfriend_core::Color::$color,
chessfriend_core::Wing::QueenSide,
)
};
}
/// A single player's move. In game theory parlance, this is a "ply."
///
/// ## TODO
///
/// - Rename this class `Ply`.
///
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Move(pub(crate) u16);
fn origin_bits(square: Square) -> u16 {
(square as u16) << 4
}
fn target_bits(square: Square) -> u16 {
(square as u16) << 10
}
impl Move {
#[must_use]
pub const fn null() -> Self {
Move(0)
}
#[must_use]
pub fn quiet(origin: Square, target: Square) -> Self {
Move(origin_bits(origin) | target_bits(target))
}
#[must_use]
pub fn double_push(origin: Square, target: Square) -> Self {
let flag_bits = Kind::DoublePush as u16;
Move(origin_bits(origin) | target_bits(target) | flag_bits)
}
#[must_use]
pub fn capture(origin: Square, target: Square) -> Self {
let flag_bits = Kind::Capture as u16;
Move(origin_bits(origin) | target_bits(target) | flag_bits)
}
#[must_use]
pub fn capture_promotion(origin: Square, target: Square, shape: PromotionShape) -> Self {
let flag_bits = Kind::CapturePromotion as u16;
let shape_bits = shape as u16;
Move(origin_bits(origin) | target_bits(target) | flag_bits | shape_bits)
}
#[must_use]
pub fn en_passant_capture(origin: Square, target: Square) -> Self {
let flag_bits = Kind::EnPassantCapture as u16;
Move(origin_bits(origin) | target_bits(target) | flag_bits)
}
#[must_use]
pub fn promotion(origin: Square, target: Square, shape: PromotionShape) -> Self {
let flag_bits = Kind::Promotion as u16;
let shape_bits = shape as u16;
Move(origin_bits(origin) | target_bits(target) | flag_bits | shape_bits)
}
#[must_use]
pub fn castle(color: Color, wing: Wing) -> Self {
let flag_bits = match wing {
Wing::KingSide => Kind::KingSideCastle,
Wing::QueenSide => Kind::QueenSideCastle,
} as u16;
let parameters = CastleParameters::get(color, wing);
let origin = parameters.origin.king;
let target = parameters.target.king;
Move(origin_bits(origin) | target_bits(target) | flag_bits)
}
}
impl Move {
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn origin_square(&self) -> Square {
((self.0 >> 4) & 0b111_111).try_into().unwrap()
}
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn target_square(&self) -> Square {
(self.0 >> 10).try_into().unwrap()
}
#[must_use]
pub fn capture_square(&self) -> Option<Square> {
if self.is_en_passant() {
let target_square = self.target_square();
return Some(match target_square.rank() {
Rank::THREE => Square::from_file_rank(target_square.file(), Rank::FOUR),
Rank::SIX => Square::from_file_rank(target_square.file(), Rank::FIVE),
_ => unreachable!(),
});
}
if self.is_capture() {
return Some(self.target_square());
}
None
}
#[must_use]
pub fn is_quiet(&self) -> bool {
self.flags() == Kind::Quiet as u16
}
#[must_use]
pub fn is_double_push(&self) -> bool {
self.flags() == Kind::DoublePush as u16
}
#[must_use]
pub fn is_castle(&self) -> bool {
(self.0 & 0b0010) != 0
}
#[must_use]
pub fn castle_wing(&self) -> Option<Wing> {
match self.flags() {
0b0010 => Some(Wing::KingSide),
0b0011 => Some(Wing::QueenSide),
_ => None,
}
}
#[must_use]
pub fn is_capture(&self) -> bool {
(self.0 & Kind::Capture as u16) != 0
}
#[must_use]
pub fn is_en_passant(&self) -> bool {
self.flags() == Kind::EnPassantCapture as u16
}
#[must_use]
pub fn is_promotion(&self) -> bool {
(self.0 & Kind::Promotion as u16) != 0
}
#[must_use]
pub fn promotion_shape(&self) -> Option<Shape> {
if !self.is_promotion() {
return None;
}
Some(match self.special() {
0b00 => Shape::Knight,
0b01 => Shape::Bishop,
0b10 => Shape::Rook,
0b11 => Shape::Queen,
_ => unreachable!(),
})
}
}
impl Move {
#[inline]
fn flags(self) -> u16 {
self.0 & 0b1111
}
#[inline]
fn special(self) -> u16 {
self.0 & 0b11
}
}
impl Move {
pub fn relevant_squares(&self) -> BitBoard {
[
Some(self.origin_square()),
Some(self.target_square()),
self.capture_square(),
]
.into_iter()
.flatten()
.collect()
}
}
impl Move {
fn transfer_char(self) -> char {
if self.is_capture() || self.is_en_passant() {
'x'
} else {
'-'
}
}
}
const KINGSIDE_CASTLE_STR: &str = "0-0";
const QUEENSIDE_CASTLE_STR: &str = "0-0-0";
impl fmt::Display for Move {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(castle) = self.castle_wing() {
return match castle {
Wing::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"),
Wing::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"),
};
}
let origin = self.origin_square();
let target = self.target_square();
let transfer_char = self.transfer_char();
write!(f, "{origin}{transfer_char}{target}")?;
if let Some(promotion) = self.promotion_shape() {
write!(f, "={promotion}")?;
} else if self.is_en_passant() {
write!(f, " e.p.")?;
}
Ok(())
}
}
impl fmt::Debug for Move {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Move")
.field(&format_args!("{:08b}", &self.0))
.finish()
}
}

View file

@ -1,43 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::Move;
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
/// necessary to restore the board position to its state immediately before the
/// move was made.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MoveRecord {
/// The color of the player who made the move
pub color: Color,
/// The move played
pub ply: Move,
/// The en passant square when the move was made
pub en_passant_target: Option<Square>,
/// Castling rights for all players when the move was made
pub castling_rights: CastleRights,
/// The half move clock when the move was made
pub half_move_clock: HalfMoveClock,
/// The piece captured by the move, if any
pub captured_piece: Option<Piece>,
}
impl MoveRecord {
#[must_use]
pub fn new(board: &Board, ply: Move, capture: Option<Piece>) -> Self {
Self {
color: board.active_color(),
ply,
en_passant_target: board.en_passant_target(),
castling_rights: *board.castling_rights(),
half_move_clock: board.half_move_clock,
captured_piece: capture,
}
}
}

View file

@ -1,8 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
pub type TestResult = Result<(), TestError>;
#[derive(Debug, Eq, PartialEq)]
pub enum TestError {
NoLegalMoves,
}

View file

@ -1,486 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::MoveRecord;
use chessfriend_board::{Board, BoardProvider, PlacePieceError, PlacePieceStrategy};
use chessfriend_core::{Piece, Square};
use thiserror::Error;
pub type UnmakeMoveResult = Result<(), UnmakeMoveError>;
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum UnmakeMoveError {
#[error("no move to unmake")]
NoMove,
#[error("no piece on {0}")]
NoPiece(Square),
#[error("no capture square")]
NoCaptureSquare,
#[error("no captured piece to unmake capture move")]
NoCapturedPiece,
#[error("{0}")]
PlacePieceError(#[from] PlacePieceError),
}
pub trait UnmakeMove {
/// Unmake the given move. Unmaking a move that wasn't the most recent one
/// made will likely cause strange results.
///
/// ## To-Do
///
/// Some implementations I've seen in other engines take a move to unmake. I
/// don't understand why they do this because I don't think it makes sense
/// to unmake any move other than the last move made. I need to do some
/// research on this to understand if/when passing a move might be useful or
/// necessary.
///
/// ## Errors
///
/// Returns one of [`UnmakeMoveError`] indicating why the move cannot be
/// unmade.
///
fn unmake_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
}
trait UnmakeMoveInternal {
fn unmake_quiet_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
fn unmake_capture_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
fn unmake_promotion_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
fn unmake_castle_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
}
impl<T: BoardProvider> UnmakeMove for T {
fn unmake_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
let ply = record.ply;
if ply.is_quiet() || ply.is_double_push() {
self.unmake_quiet_move(record)?;
} else if ply.is_capture() || ply.is_en_passant() {
self.unmake_capture_move(record)?;
} else if ply.is_promotion() {
self.unmake_promotion_move(record)?;
} else if ply.is_castle() {
self.unmake_castle_move(record)?;
} else {
unreachable!();
}
let board = self.board_mut();
board.set_active_color(record.color);
board.set_en_passant_target_option(record.en_passant_target);
board.set_castling_rights(record.castling_rights);
board.half_move_clock = record.half_move_clock;
Ok(())
}
}
impl<T: BoardProvider> UnmakeMoveInternal for T {
fn unmake_quiet_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
let board = self.board_mut();
let ply = record.ply;
let target = ply.target_square();
let piece = board
.get_piece(target)
.ok_or(UnmakeMoveError::NoPiece(target))?;
let origin = ply.origin_square();
board.place_piece(piece, origin, PlacePieceStrategy::PreserveExisting)?;
board.remove_piece(target);
Ok(())
}
fn unmake_capture_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
let board = self.board_mut();
let ply = record.ply;
let target = ply.target_square();
let mut piece = board
.get_piece(target)
.ok_or(UnmakeMoveError::NoPiece(target))?;
if ply.is_promotion() {
piece = Piece::pawn(piece.color);
}
let origin = ply.origin_square();
board.place_piece(piece, origin, PlacePieceStrategy::PreserveExisting)?;
let capture_square = ply
.capture_square()
.ok_or(UnmakeMoveError::NoCaptureSquare)?;
let captured_piece = record
.captured_piece
.ok_or(UnmakeMoveError::NoCapturedPiece)?;
board.remove_piece(target);
board.place_piece(
captured_piece,
capture_square,
PlacePieceStrategy::PreserveExisting,
)?;
Ok(())
}
fn unmake_promotion_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
let board = self.board_mut();
let ply = record.ply;
let target = ply.target_square();
let piece = Piece::pawn(
board
.get_piece(target)
.ok_or(UnmakeMoveError::NoPiece(target))?
.color,
);
let origin = ply.origin_square();
board.place_piece(piece, origin, PlacePieceStrategy::PreserveExisting)?;
board.remove_piece(target);
Ok(())
}
fn unmake_castle_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
let ply = record.ply;
let wing = ply.castle_wing().expect("no wing for unmaking castle move");
let color = record.color;
let parameters = Board::castling_parameters(wing, color);
let board = self.board_mut();
let king = board
.get_piece(parameters.target.king)
.ok_or(UnmakeMoveError::NoPiece(parameters.target.king))?;
let rook = board
.get_piece(parameters.target.rook)
.ok_or(UnmakeMoveError::NoPiece(parameters.target.rook))?;
board.place_piece(
king,
parameters.origin.king,
PlacePieceStrategy::PreserveExisting,
)?;
board.place_piece(
rook,
parameters.origin.rook,
PlacePieceStrategy::PreserveExisting,
)?;
board.remove_piece(parameters.target.king);
board.remove_piece(parameters.target.rook);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MakeMove, Move, PromotionShape, ValidateMove};
use chessfriend_board::test_board;
use chessfriend_core::{Color, Square, Wing, piece};
type TestResult = Result<(), Box<dyn std::error::Error>>;
/// Helper function to test make/unmake idempotency
fn test_make_unmake_idempotent(
initial_board: &mut impl BoardProvider,
ply: Move,
) -> TestResult {
// Capture initial state
let initial_state = initial_board.board().clone();
// Make the move
let record = initial_board.make_move(ply, ValidateMove::Yes)?;
// Verify the move changed the board
assert_ne!(
*initial_board.board(),
initial_state,
"Move should change board state"
);
// Unmake the move
initial_board.unmake_move(&record)?;
// Verify we're back to the initial state
assert_eq!(
*initial_board.board(),
initial_state,
"Board should return to initial state after unmake"
);
Ok(())
}
#[test]
fn unmake_quiet_move_ai_claude() -> TestResult {
let mut board = test_board!(White Pawn on C2);
let ply = Move::quiet(Square::C2, Square::C3);
let record = board.make_move(ply, ValidateMove::Yes)?;
// Verify move was made
assert_eq!(board.get_piece(Square::C2), None);
assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn)));
assert_eq!(board.active_color(), Color::Black);
board.unmake_move(&record)?;
// Verify original state restored
assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::C3), None);
assert_eq!(board.active_color(), Color::White);
Ok(())
}
#[test]
fn unmake_double_push_move_ai_claude() -> TestResult {
let mut board = test_board!(White Pawn on E2);
let ply = Move::double_push(Square::E2, Square::E4);
let record = board.make_move(ply, ValidateMove::Yes)?;
// Verify move was made
assert_eq!(board.get_piece(Square::E2), None);
assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn)));
assert_eq!(board.en_passant_target(), Some(Square::E3));
assert_eq!(board.active_color(), Color::Black);
board.unmake_move(&record)?;
// Verify original state restored
assert_eq!(board.get_piece(Square::E2), Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::E4), None);
assert_eq!(board.en_passant_target(), None);
assert_eq!(board.active_color(), Color::White);
Ok(())
}
#[test]
fn unmake_capture_move_ai_claude() -> TestResult {
let mut board = test_board![
White Bishop on C2,
Black Rook on F5,
];
let ply = Move::capture(Square::C2, Square::F5);
let record = board.make_move(ply, ValidateMove::Yes)?;
// Verify move was made
assert_eq!(board.get_piece(Square::C2), None);
assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop)));
assert_eq!(record.captured_piece, Some(piece!(Black Rook)));
assert_eq!(board.active_color(), Color::Black);
board.unmake_move(&record)?;
// Verify original state restored
assert_eq!(board.get_piece(Square::C2), Some(piece!(White Bishop)));
assert_eq!(board.get_piece(Square::F5), Some(piece!(Black Rook)));
assert_eq!(board.active_color(), Color::White);
Ok(())
}
#[test]
fn unmake_en_passant_capture_ai_claude() -> TestResult {
let mut board = test_board![
Black Pawn on F4,
White Pawn on E2
];
// Set up en passant situation
let double_push = Move::double_push(Square::E2, Square::E4);
board.make_move(double_push, ValidateMove::Yes)?;
// Make en passant capture
let en_passant = Move::en_passant_capture(Square::F4, Square::E3);
let record = board.make_move(en_passant, ValidateMove::Yes)?;
// Verify en passant was made
assert_eq!(board.get_piece(Square::F4), None);
assert_eq!(board.get_piece(Square::E3), Some(piece!(Black Pawn)));
assert_eq!(
board.get_piece(Square::E4),
None,
"captured pawn was not removed"
);
assert_eq!(record.captured_piece, Some(piece!(White Pawn)));
board.unmake_move(&record)?;
// Verify state before en passant is restored
assert_eq!(board.get_piece(Square::F4), Some(piece!(Black Pawn)));
assert_eq!(board.get_piece(Square::E3), None);
assert_eq!(
board.get_piece(Square::E4),
Some(piece!(White Pawn)),
"captured pawn was not restored"
);
assert_eq!(board.active_color(), Color::Black);
Ok(())
}
#[test]
fn unmake_promotion_move_ai_claude() -> TestResult {
let mut board = test_board![
White Pawn on F7,
];
let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen);
let record = board.make_move(ply, ValidateMove::Yes)?;
// Verify promotion was made
assert_eq!(board.get_piece(Square::F7), None);
assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen)));
assert_eq!(board.active_color(), Color::Black);
board.unmake_move(&record)?;
// Verify original pawn is restored
assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::F8), None);
assert_eq!(board.active_color(), Color::White);
Ok(())
}
#[test]
fn unmake_capture_promotion_ai_claude() -> TestResult {
let mut board = test_board![
White Pawn on F7,
Black Rook on G8,
];
let ply = Move::capture_promotion(Square::F7, Square::G8, PromotionShape::Queen);
let record = board.make_move(ply, ValidateMove::Yes)?;
// Verify promotion capture was made
assert_eq!(board.get_piece(Square::F7), None);
assert_eq!(board.get_piece(Square::G8), Some(piece!(White Queen)));
assert_eq!(record.captured_piece, Some(piece!(Black Rook)));
board.unmake_move(&record)?;
// Verify original state restored
assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn)));
assert_eq!(board.get_piece(Square::G8), Some(piece!(Black Rook)));
assert_eq!(board.active_color(), Color::White);
Ok(())
}
#[test]
fn unmake_white_kingside_castle_ai_claude() -> TestResult {
let mut board = test_board![
White King on E1,
White Rook on H1,
];
let original_castling_rights = *board.castling_rights();
let ply = Move::castle(Color::White, Wing::KingSide);
let record = board.make_move(ply, ValidateMove::Yes)?;
// Verify castle was made
assert_eq!(board.get_piece(Square::E1), None);
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));
board.unmake_move(&record)?;
// Verify original state restored
assert_eq!(board.get_piece(Square::E1), Some(piece!(White King)));
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.active_color(), Color::White);
Ok(())
}
#[test]
fn unmake_white_queenside_castle_ai_claude() -> TestResult {
let mut board = test_board![
White King on E1,
White Rook on A1,
];
let original_castling_rights = *board.castling_rights();
let ply = Move::castle(Color::White, Wing::QueenSide);
let record = board.make_move(ply, ValidateMove::Yes)?;
// Verify castle was made
assert_eq!(board.get_piece(Square::E1), None);
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));
board.unmake_move(&record)?;
// Verify original state restored
assert_eq!(board.get_piece(Square::E1), Some(piece!(White King)));
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.active_color(), Color::White);
Ok(())
}
#[test]
fn unmake_black_kingside_castle() -> TestResult {
let mut board = test_board!(Black, [
Black King on E8,
Black Rook on H8,
]);
let original_castling_rights = *board.castling_rights();
let ply = Move::castle(Color::Black, Wing::KingSide);
let record = board.make_move(ply, ValidateMove::Yes)?;
// Verify castle was made
assert_eq!(board.get_piece(Square::E8), None);
assert_eq!(board.get_piece(Square::H8), None);
assert_eq!(board.get_piece(Square::G8), Some(piece!(Black King)));
assert_eq!(board.get_piece(Square::F8), Some(piece!(Black Rook)));
board.unmake_move(&record)?;
// Verify original state restored
assert_eq!(board.get_piece(Square::E8), Some(piece!(Black King)));
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.active_color(), Color::Black);
Ok(())
}
}

View file

@ -1,11 +0,0 @@
[package]
name = "perft"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.98"
chessfriend_board = { path = "../board" }
chessfriend_position = { path = "../position" }
clap = { version = "4.4.12", features = ["derive"] }
thiserror = "2"

View file

@ -1,117 +0,0 @@
[
{
"depth":1,
"nodes":8,
"fen":"r6r/1b2k1bq/8/8/7B/8/8/R3K2R b KQ - 3 2"
},
{
"depth":1,
"nodes":8,
"fen":"8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3"
},
{
"depth":1,
"nodes":19,
"fen":"r1bqkbnr/pppppppp/n7/8/8/P7/1PPPPPPP/RNBQKBNR w KQkq - 2 2"
},
{
"depth":1,
"nodes":5,
"fen":"r3k2r/p1pp1pb1/bn2Qnp1/2qPN3/1p2P3/2N5/PPPBBPPP/R3K2R b KQkq - 3 2"
},
{
"depth":1,
"nodes":44,
"fen":"2kr3r/p1ppqpb1/bn2Qnp1/3PN3/1p2P3/2N5/PPPBBPPP/R3K2R b KQ - 3 2"
},
{
"depth":1,
"nodes":39,
"fen":"rnb2k1r/pp1Pbppp/2p5/q7/2B5/8/PPPQNnPP/RNB1K2R w KQ - 3 9"
},
{
"depth":1,
"nodes":9,
"fen":"2r5/3pk3/8/2P5/8/2K5/8/8 w - - 5 4"
},
{
"depth":3,
"nodes":62379,
"fen":"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"
},
{
"depth":3,
"nodes":89890,
"fen":"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"
},
{
"depth":6,
"nodes":1134888,
"fen":"3k4/3p4/8/K1P4r/8/8/8/8 b - - 0 1"
},
{
"depth":6,
"nodes":1015133,
"fen":"8/8/4k3/8/2p5/8/B2P2K1/8 w - - 0 1"
},
{
"depth":6,
"nodes":1440467,
"fen":"8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1"
},
{
"depth":6,
"nodes":661072,
"fen":"5k2/8/8/8/8/8/8/4K2R w K - 0 1"
},
{
"depth":6,
"nodes":803711,
"fen":"3k4/8/8/8/8/8/8/R3K3 w Q - 0 1"
},
{
"depth":4,
"nodes":1274206,
"fen":"r3k2r/1b4bq/8/8/8/8/7B/R3K2R w KQkq - 0 1"
},
{
"depth":4,
"nodes":1720476,
"fen":"r3k2r/8/3Q4/8/8/5q2/8/R3K2R b KQkq - 0 1"
},
{
"depth":6,
"nodes":3821001,
"fen":"2K2r2/4P3/8/8/8/8/8/3k4 w - - 0 1"
},
{
"depth":5,
"nodes":1004658,
"fen":"8/8/1P2K3/8/2n5/1q6/8/5k2 b - - 0 1"
},
{
"depth":6,
"nodes":217342,
"fen":"4k3/1P6/8/8/8/8/K7/8 w - - 0 1"
},
{
"depth":6,
"nodes":92683,
"fen":"8/P1k5/K7/8/8/8/8/8 w - - 0 1"
},
{
"depth":6,
"nodes":2217,
"fen":"K1k5/8/P7/8/8/8/8/8 w - - 0 1"
},
{
"depth":7,
"nodes":567584,
"fen":"8/k1P5/8/1K6/8/8/8/8 w - - 0 1"
},
{
"depth":4,
"nodes":23527,
"fen":"8/8/2k5/5q2/5n2/8/5K2/8 b - - 0 1"
}
]

View file

@ -1,81 +0,0 @@
#!/usr/bin/env python3
# Eryn Wells <eryn@erynwells.me>
'''
New script.
'''
import argparse
import json
import subprocess
def run_perft(fen, depth):
result = subprocess.run(
[
'cargo',
'run',
'--',
'--fen', fen,
str(depth)
],
capture_output=True,
check=True,
text=True
)
nodes_count = 0
for line in result.stdout.splitlines():
if line.startswith('nodes '):
(_, nodes_count) = line.split(' ')
nodes_count = int(nodes_count)
return nodes_count
def parse_args(argv, *a, **kw):
parser = argparse.ArgumentParser(*a, **kw)
parser.add_argument('-c', '--continue', dest='should_continue',
action='store_true')
parser.add_argument('file')
args = parser.parse_args(argv)
return args
def main(argv):
args = parse_args(argv[1:], prog=argv[0])
items = []
with open(args.file, 'r') as f:
items = json.load(f)
should_continue = args.should_continue
for item in items:
fen = item['fen']
depth = item['depth']
expected_nodes_count = int(item['nodes'])
print('---')
print(f'fen={fen}')
print(f'depth={depth}')
print(f'expected-nodes={expected_nodes_count}')
nodes_count = run_perft(fen, depth)
print(f'nodes={nodes_count}')
did_pass = nodes_count == expected_nodes_count
if did_pass:
print('result=PASS')
else:
print('result=FAIL')
if not did_pass and not should_continue:
return -1
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))

View file

@ -1,35 +0,0 @@
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")]
fen: Option<String>,
}
fn main() -> anyhow::Result<()> {
let args = Arguments::parse();
let depth = args.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 counters = position.perft(depth);
println!("\n{counters}");
Ok(())
}

View file

@ -1,13 +0,0 @@
[package]
name = "chessfriend_position"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chessfriend_core = { path = "../core" }
chessfriend_bitboard = { path = "../bitboard" }
chessfriend_board = { path = "../board" }
chessfriend_moves = { path = "../moves" }
thiserror = "2"

View file

@ -1,73 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::sight::SliderRayToSquareExt;
use chessfriend_bitboard::BitBoard;
use chessfriend_core::Shape;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CheckingPieces {
bitboards: [BitBoard; 5],
}
impl CheckingPieces {
pub(crate) fn new(
pawn: BitBoard,
knight: BitBoard,
bishop: BitBoard,
rook: BitBoard,
queen: BitBoard,
) -> CheckingPieces {
CheckingPieces {
bitboards: [pawn, knight, bishop, rook, queen],
}
}
/// The number of checking pieces.
pub fn count(&self) -> u32 {
self.bitboards.iter().map(BitBoard::population_count).sum()
}
/// A BitBoard representing the set of pieces that must be captured to resolve check.
pub fn capture_mask(&self) -> BitBoard {
if self.count() == 0 {
BitBoard::FULL
} else {
self.bitboards
.iter()
.fold(BitBoard::EMPTY, std::ops::BitOr::bitor)
}
}
/// A BitBoard representing the set of squares to which a player can move a piece to block a
/// checking piece.
pub fn push_mask(&self, king: BitBoard) -> BitBoard {
let target = king.first_occupied_square_leading().unwrap();
macro_rules! push_mask_for_shape {
($push_mask:expr, $shape:ident, $king:expr) => {{
let checking_pieces = self.bitboard_for_shape(Shape::$shape);
if !checking_pieces.is_empty() {
if let Some(checking_ray) = checking_pieces
.occupied_squares()
.flat_map(|sq| Shape::$shape.ray_to_square(sq, target).into_iter())
.find(|bb| !(bb & $king).is_empty())
{
$push_mask |= checking_ray & !$king
}
}
}};
}
let mut push_mask = BitBoard::EMPTY;
push_mask_for_shape!(push_mask, Bishop, king);
push_mask_for_shape!(push_mask, Rook, king);
push_mask_for_shape!(push_mask, Queen, king);
push_mask
}
fn bitboard_for_shape(&self, shape: Shape) -> &BitBoard {
&self.bitboards[shape as usize]
}
}

View file

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

View file

@ -1,15 +0,0 @@
// 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 position::Position;
pub mod perft;
#[macro_use]
pub mod testing;

View file

@ -1,36 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
#[macro_export]
macro_rules! position {
[$($color:ident $shape:ident on $square:ident),* $(,)?] => {
$crate::Position::new(chessfriend_board::board!($($color $shape on $square),*))
};
}
#[macro_export]
macro_rules! test_position {
($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => {
{
let board = chessfriend_board::test_board!($to_move, [ $($color $shape on $square),*], $en_passant);
$crate::Position::new(board)
}
};
($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => {
{
let board = chessfriend_board::test_board!($to_move, [ $($color $shape on $square),* ]);
$crate::Position::new(board)
}
};
($($color:ident $shape:ident on $square:ident),* $(,)?) => {
{
let board = chessfriend_board::test_board!($($color $shape on $square),*);
$crate::Position::new(board)
}
};
(empty) => {
Position::new(chessfriend_board::test_board!(empty))
};
(starting) => {
Position::new(chessfriend_board::test_board!(starting))
};
}

View file

@ -1,4 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
mod peterellisjones;
mod single_pieces;

View file

@ -1,32 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
use crate::{assert_move_list, test_position, testing::*};
use chessfriend_core::{piece, Square};
use chessfriend_moves::Builder as MoveBuilder;
use std::collections::HashSet;
#[test]
fn one_king() -> TestResult {
let pos = test_position![
White King on D3,
Black King on H6,
];
let builder = MoveBuilder::push(&piece!(White King on D3));
let expected_moves = HashSet::from_iter([
builder.clone().to(Square::D4).build()?,
builder.clone().to(Square::E4).build()?,
builder.clone().to(Square::E3).build()?,
builder.clone().to(Square::E2).build()?,
builder.clone().to(Square::D2).build()?,
builder.clone().to(Square::C2).build()?,
builder.clone().to(Square::C3).build()?,
builder.clone().to(Square::C4).build()?,
]);
let generated_moves: HashSet<_> = pos.moves().iter().collect();
assert_move_list!(generated_moves, expected_moves, pos);
Ok(())
}

View file

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

@ -1,379 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
mod captures;
use crate::fen::{FromFenStr, FromFenStrError};
use captures::CapturesList;
use chessfriend_bitboard::BitBoard;
use chessfriend_board::{
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,
},
};
use std::{collections::HashSet, fmt, sync::Arc};
#[must_use]
#[derive(Clone, Debug, Default, Eq)]
pub struct Position {
pub(crate) board: Board,
pub(crate) moves: Vec<MoveRecord>,
pub(crate) captures: CapturesList,
/// A set of hashes of board positions seen throughout the move record.
boards_seen: HashSet<u64>,
}
impl Position {
pub fn empty(zobrist: Option<Arc<ZobristState>>) -> Self {
Self::new(Board::empty(zobrist))
}
/// Return a starting position.
pub fn starting(zobrist: Option<Arc<ZobristState>>) -> Self {
Self::new(Board::starting(zobrist))
}
pub fn new(board: Board) -> Self {
Self {
board,
..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 {
/// Place a piece on the board.
///
/// ## Errors
///
/// See [`chessfriend_board::Board::place_piece`].
pub fn place_piece(
&mut self,
piece: Piece,
square: Square,
strategy: PlacePieceStrategy,
) -> Result<Option<Piece>, PlacePieceError> {
self.board.place_piece(piece, square, strategy)
}
#[must_use]
pub fn get_piece(&self, square: Square) -> Option<Piece> {
self.board.get_piece(square)
}
pub fn remove_piece(&mut self, square: Square) -> Option<Piece> {
self.board.remove_piece(square)
}
}
impl Position {
/// Calculate sight of a piece on the provided [`Square`].
pub fn sight_piece(&self, square: Square) -> BitBoard {
self.board.sight_piece(square)
}
/// Calculate movement of a piece on the provided [`Square`].
pub fn movement_piece(&self, square: Square) -> BitBoard {
self.board.movement_piece(square)
}
}
impl Position {
pub fn all_moves(&self, color: Option<Color>) -> AllPiecesMoveGenerator {
AllPiecesMoveGenerator::new(&self.board, color)
}
/// Generate legal moves.
///
/// ## Panics
///
/// If the position failed to make a move generated by the internal move
/// generator, this method will panic.
#[must_use]
pub fn all_legal_moves(
&self,
color: Option<Color>,
) -> Box<dyn Iterator<Item = GeneratedMove> + '_> {
let generator = self.all_moves(color);
let mut test_board = self.board.clone();
Box::new(generator.filter(move |ply| {
let active_color_before_move = test_board.active_color();
let ply: Move = ply.clone().into();
let record = test_board
.make_move(ply, ValidateMove::No)
.unwrap_or_else(|err| {
panic!(
"unable to make generated move [{ply}]: {err}\n\n{}",
test_board.display().highlight(ply.relevant_squares())
);
});
let move_is_legal = !test_board.is_in_check();
test_board.unmake_move(&record).unwrap_or_else(|err| {
panic!(
"unable to unmake generated move [{ply}]: {err}\n\n{}",
test_board.display().highlight(ply.relevant_squares())
);
});
move_is_legal
}))
}
#[must_use]
pub fn moves_for_piece(
&self,
square: Square,
) -> Option<Box<dyn Iterator<Item = GeneratedMove>>> {
self.get_piece(square)
.map(|piece| Self::generator(&self.board, piece))
}
#[must_use]
fn generator(board: &Board, piece: Piece) -> Box<dyn Iterator<Item = GeneratedMove>> {
match piece.shape {
Shape::Pawn => Box::new(PawnMoveGenerator::new(board, Some(piece.color))),
Shape::Knight => Box::new(KnightMoveGenerator::new(board, Some(piece.color))),
Shape::Bishop => Box::new(BishopMoveGenerator::new(board, Some(piece.color))),
Shape::Rook => Box::new(RookMoveGenerator::new(board, Some(piece.color))),
Shape::Queen => Box::new(QueenMoveGenerator::new(board, Some(piece.color))),
Shape::King => Box::new(KingMoveGenerator::new(board, Some(piece.color))),
}
}
}
impl Position {
pub fn sight_active(&self) -> BitBoard {
self.board.sight_active()
}
/// A [`BitBoard`] of all squares the given color can see.
pub fn friendly_sight(&self, color: Color) -> BitBoard {
self.board.friendly_sight(color)
}
/// A [`BitBoard`] of all squares visible by colors that oppose the given color.
pub fn active_color_opposing_sight(&self) -> BitBoard {
self.board.active_color_opposing_sight()
}
}
impl Position {
/// Make a move on the board and record it in the move list. Returns `true`
/// if the board position has been seen before (i.e. it's a repetition).
///
/// ## Errors
///
/// Returns one of [`MakeMoveError`] if the move cannot be made.
///
pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result<bool, MakeMoveError> {
let record = self.board.make_move(ply, validate)?;
if let Some(captured_piece) = record.captured_piece {
self.captures.push(record.color, captured_piece);
}
let has_seen = if let Some(hash) = self.board.zobrist_hash() {
// HashSet::insert() returns true if the value does not exist in the
// set when it's called.
!self.boards_seen.insert(hash)
} else {
false
};
self.moves.push(record.clone());
Ok(has_seen)
}
/// Unmake the last move made on the board and remove its record from the
/// move list.
///
/// ## Errors
///
/// Returns one of [`UnmakeMoveError`] if the move cannot be made.
///
pub fn unmake_last_move(&mut self) -> UnmakeMoveResult {
let last_move_record = self.moves.pop().ok_or(UnmakeMoveError::NoMove)?;
let hash_before_unmake = self.board.zobrist_hash();
let unmake_result = self.board.unmake_move(&last_move_record);
if unmake_result.is_ok() {
if let Some(capture) = last_move_record.captured_piece {
let popped_piece = self.captures.pop(last_move_record.color);
debug_assert_eq!(Some(capture), popped_piece);
}
if let Some(hash_before_unmake) = hash_before_unmake {
self.boards_seen.remove(&hash_before_unmake);
}
} else {
self.moves.push(last_move_record);
}
unmake_result
}
/// Build a move given its origin, target, and possible promotion. Perform
/// some minimal validation. If a move cannot be
#[must_use]
pub fn move_from_algebraic_components(
&self,
components: AlgebraicMoveComponents,
) -> Option<Move> {
match components {
AlgebraicMoveComponents::Null => Some(Move::null()),
AlgebraicMoveComponents::Regular {
origin,
target,
promotion,
} => self.move_from_origin_target(origin, target, promotion),
}
}
fn move_from_origin_target(
&self,
origin: Square,
target: Square,
promotion: Option<Shape>,
) -> Option<Move> {
let piece = self.get_piece(origin)?;
let color = piece.color;
// Pawn and King are the two most interesting shapes here, because of en
// passant, castling and so on. So, let the move generators do their
// thing and find the move that fits the parameters. For the rest of the
// pieces, do something a little more streamlined.
match piece.shape {
Shape::Pawn => PawnMoveGenerator::new(&self.board, None)
.find(|ply| {
ply.origin() == origin
&& ply.target() == target
&& ply.promotion_shape() == promotion
})
.map(std::convert::Into::into),
Shape::King => KingMoveGenerator::new(&self.board, None)
.find(|ply| ply.origin() == origin && ply.target() == target)
.map(std::convert::Into::into),
_ => {
if color != self.board.active_color() {
return None;
}
let target_bitboard: BitBoard = target.into();
if !(self.movement_piece(origin) & target_bitboard).is_populated() {
return None;
}
if self.get_piece(target).is_some() {
return Some(Move::capture(origin, target));
}
Some(Move::quiet(origin, target))
}
}
}
}
impl Position {
#[must_use]
pub fn zobrist_hash(&self) -> Option<u64> {
self.board.zobrist_hash()
}
pub fn set_zobrist_state(&mut self, state: Arc<ZobristState>) {
self.board.set_zobrist_state(state);
}
}
impl Position {
pub fn display(&self) -> DiagramFormatter {
self.board.display()
}
}
impl FromFenStr for Position {
type Error = FromFenStrError;
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
let board = Board::from_fen_str(string)?;
Ok(Position::new(board))
}
}
impl ToFenStr for Position {
type Error = <Board as ToFenStr>::Error;
fn to_fen_str(&self) -> Result<String, Self::Error> {
self.board.to_fen_str()
}
}
impl PartialEq for Position {
fn eq(&self, other: &Self) -> bool {
self.board == other.board
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.board.display())?;
if !self.captures.is_empty() {
write!(f, "\n\n{}", self.captures)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Position, test_position};
use chessfriend_core::piece;
#[test]
fn piece_on_square() {
let pos = test_position![
Black Bishop on F7,
];
let piece = pos.board.get_piece(Square::F7);
assert_eq!(piece, Some(piece!(Black Bishop)));
}
#[test]
fn piece_in_starting_position() {
let pos = test_position!(starting);
assert_eq!(pos.board.get_piece(Square::H1), Some(piece!(White Rook)));
assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook)));
}
}

View file

@ -1,5 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
mod move_builder;
pub use move_builder::{Builder as MoveBuilder, MakeMoveError};

Some files were not shown because too many files have changed in this diff Show more