diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 337e104..0000000 --- a/Cargo.lock +++ /dev/null @@ -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", -] diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 37c14db..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[workspace] -members = [ - "bitboard", - "board", - "chessfriend", - "core", - "explorer", - "moves", - "perft", - "position", -] -resolver = "3" - -[profile.release-debug] -inherits = "release" -debug = true diff --git a/ChessFriend.code-workspace b/ChessFriend.code-workspace deleted file mode 100644 index b51044b..0000000 --- a/ChessFriend.code-workspace +++ /dev/null @@ -1,7 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ] -} diff --git a/Makefile b/Makefile deleted file mode 100644 index 500830f..0000000 --- a/Makefile +++ /dev/null @@ -1,4 +0,0 @@ - - -all: - cargo build diff --git a/doc/Notes.md b/Notes.md similarity index 100% rename from doc/Notes.md rename to Notes.md diff --git a/README.md b/README.md deleted file mode 100644 index 5ca6cb2..0000000 --- a/README.md +++ /dev/null @@ -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. diff --git a/bitboard/Cargo.toml b/bitboard/Cargo.toml deleted file mode 100644 index 26df504..0000000 --- a/bitboard/Cargo.toml +++ /dev/null @@ -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" diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs deleted file mode 100644 index 35ce927..0000000 --- a/bitboard/src/bitboard.rs +++ /dev/null @@ -1,596 +0,0 @@ -// Eryn Wells - -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> { - 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::E5, Square::E6, Square::E7, Square::E8] - /// ); - /// ``` - /// - #[must_use] - pub fn occupied_squares_direction( - &self, - direction: Direction, - ) -> Box> { - 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 { - 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 { - 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 { - 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 { - 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 for u64 { - fn from(value: BitBoard) -> Self { - value.as_bits() - } -} - -impl From for BitBoard { - fn from(value: File) -> Self { - library::FILES[value.as_index()] - } -} - -impl From> for BitBoard { - fn from(value: Option) -> Self { - value.map_or(BitBoard::EMPTY, Into::::into) - } -} - -impl From for BitBoard { - fn from(value: Rank) -> Self { - library::FILES[value.as_index()] - } -} - -impl From for BitBoard { - fn from(value: Square) -> Self { - BitBoard(1u64 << value as u32) - } -} - -impl FromIterator for BitBoard { - fn from_iter>(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 for Square { - type Error = TryFromBitBoardError; - - fn try_from(value: BitBoard) -> Result { - 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::>(); - - 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)); - } -} diff --git a/bitboard/src/direction.rs b/bitboard/src/direction.rs deleted file mode 100644 index b1298fc..0000000 --- a/bitboard/src/direction.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[derive(Default)] -pub enum IterationDirection { - #[default] - Leading, - Trailing, -} diff --git a/bitboard/src/lib.rs b/bitboard/src/lib.rs deleted file mode 100644 index 12a25ed..0000000 --- a/bitboard/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Eryn Wells - -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 - } - }; -} diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs deleted file mode 100644 index 3ea670c..0000000 --- a/bitboard/src/library.rs +++ /dev/null @@ -1,269 +0,0 @@ -// Eryn Wells - -//! # 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, -} - -impl MoveLibraryWrapper { - const fn new() -> Self { - Self { - library: OnceLock::::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); -} diff --git a/position/Cargo.lock b/board/Cargo.lock similarity index 100% rename from position/Cargo.lock rename to board/Cargo.lock diff --git a/board/Cargo.toml b/board/Cargo.toml index 90de6eb..6b600a9 100644 --- a/board/Cargo.toml +++ b/board/Cargo.toml @@ -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" diff --git a/bitboard/src/bit_scanner.rs b/board/src/bitboard/bit_scanner.rs similarity index 68% rename from bitboard/src/bit_scanner.rs rename to board/src/bitboard/bit_scanner.rs index c4b0be2..bf90394 100644 --- a/bitboard/src/bit_scanner.rs +++ b/board/src/bitboard/bit_scanner.rs @@ -1,11 +1,8 @@ // Eryn Wells -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 { 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 { 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); } } diff --git a/board/src/bitboard/bitboard.rs b/board/src/bitboard/bitboard.rs new file mode 100644 index 0000000..23c2126 --- /dev/null +++ b/board/src/bitboard/bitboard.rs @@ -0,0 +1,262 @@ +// Eryn Wells + +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 { + 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 { + LeadingBitScanner::new(self.0).map(Square::from_index) + } +} + +impl From 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::>(); + + 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); + } + } +} diff --git a/board/src/bitboard/library.rs b/board/src/bitboard/library.rs new file mode 100644 index 0000000..697d12a --- /dev/null +++ b/board/src/bitboard/library.rs @@ -0,0 +1,189 @@ +// Eryn Wells + +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); +} diff --git a/board/src/bitboard/mod.rs b/board/src/bitboard/mod.rs new file mode 100644 index 0000000..f462e3b --- /dev/null +++ b/board/src/bitboard/mod.rs @@ -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; diff --git a/bitboard/src/shifts.rs b/board/src/bitboard/shifts.rs similarity index 78% rename from bitboard/src/shifts.rs rename to board/src/bitboard/shifts.rs index 98b5adf..8866d4c 100644 --- a/bitboard/src/shifts.rs +++ b/board/src/bitboard/shifts.rs @@ -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) } } diff --git a/board/src/board.rs b/board/src/board.rs deleted file mode 100644 index 338d4aa..0000000 --- a/board/src/board.rs +++ /dev/null @@ -1,392 +0,0 @@ -// Eryn Wells - -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, - pub half_move_clock: HalfMoveClock, - pub full_move_number: FullMoveClock, - zobrist_hash: Option, -} - -impl Board { - /// An empty board - #[must_use] - pub fn empty(zobrist: Option>) -> 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>) -> 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 { - 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 { - 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 { - 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) { - 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) { - 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 { - 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, 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 { - 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 { - 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 { - 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> { - self.zobrist_hash.as_ref().map(ZobristHash::state) - } - - pub fn set_zobrist_state(&mut self, state: Arc) { - 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.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)); - } -} diff --git a/board/src/board_provider.rs b/board/src/board_provider.rs deleted file mode 100644 index a653bfc..0000000 --- a/board/src/board_provider.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Eryn Wells - -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 - } -} diff --git a/board/src/castle.rs b/board/src/castle.rs deleted file mode 100644 index 4ba9a4b..0000000 --- a/board/src/castle.rs +++ /dev/null @@ -1,286 +0,0 @@ -// Eryn Wells - -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, - ) -> 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, 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, 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, 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) - ); - } -} diff --git a/board/src/castle/parameters.rs b/board/src/castle/parameters.rs deleted file mode 100644 index 1052ab6..0000000 --- a/board/src/castle/parameters.rs +++ /dev/null @@ -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] - } -} diff --git a/board/src/castle/rights.rs b/board/src/castle/rights.rs deleted file mode 100644 index 2c0a961..0000000 --- a/board/src/castle/rights.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Eryn Wells - -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 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())); - } -} diff --git a/board/src/check.rs b/board/src/check.rs deleted file mode 100644 index 6d8ceba..0000000 --- a/board/src/check.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Eryn Wells - -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()); - } -} diff --git a/board/src/display.rs b/board/src/display.rs index b7eb86f..31757c6 100644 --- a/board/src/display.rs +++ b/board/src/display.rs @@ -1,119 +1,9 @@ // Eryn Wells -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; } diff --git a/board/src/en_passant.rs b/board/src/en_passant.rs deleted file mode 100644 index 2da5abc..0000000 --- a/board/src/en_passant.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Eryn Wells - -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 { - 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::_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 - } -} diff --git a/board/src/fen.rs b/board/src/fen.rs deleted file mode 100644 index fe418a5..0000000 --- a/board/src/fen.rs +++ /dev/null @@ -1,348 +0,0 @@ -// Eryn Wells - -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; -} - -pub trait FromFenStr: Sized { - type Error; - - /// Create a `Self` from a FEN string. - /// - /// # Errors - /// - /// - fn from_fen_str(string: &str) -> Result; -} - -impl ToFenStr for Board { - type Error = ToFenStrError; - - fn to_fen_str(&self) -> Result { - 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 { - 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 { - 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 { - 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 { - 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 { - 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:#?}"); - } -} diff --git a/board/src/lib.rs b/board/src/lib.rs index ae80da8..bcc682a 100644 --- a/board/src/lib.rs +++ b/board/src/lib.rs @@ -1,24 +1,15 @@ // Eryn Wells -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}; diff --git a/board/src/macros.rs b/board/src/macros.rs deleted file mode 100644 index 29df6fc..0000000 --- a/board/src/macros.rs +++ /dev/null @@ -1,82 +0,0 @@ -// Eryn Wells - -#[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) - }}; -} diff --git a/board/src/movement.rs b/board/src/movement.rs deleted file mode 100644 index 3ebf44c..0000000 --- a/board/src/movement.rs +++ /dev/null @@ -1,154 +0,0 @@ -// Eryn Wells - -//! 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 - ); - } -} diff --git a/board/src/moves/bishop.rs b/board/src/moves/bishop.rs new file mode 100644 index 0000000..6aec9e4 --- /dev/null +++ b/board/src/moves/bishop.rs @@ -0,0 +1,144 @@ +// Eryn Wells + +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 + ); + } +} diff --git a/chessfriend/src/lib.rs b/board/src/moves/classical.rs similarity index 97% rename from chessfriend/src/lib.rs rename to board/src/moves/classical.rs index 4e01ba5..8aa971d 100644 --- a/chessfriend/src/lib.rs +++ b/board/src/moves/classical.rs @@ -1,2 +1 @@ // Eryn Wells - diff --git a/board/src/moves/king.rs b/board/src/moves/king.rs new file mode 100644 index 0000000..6cf0b77 --- /dev/null +++ b/board/src/moves/king.rs @@ -0,0 +1,165 @@ +// Eryn Wells + +//! 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 { + 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 { + 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 = 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 = 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 + ); + } +} diff --git a/board/src/moves/knight.rs b/board/src/moves/knight.rs new file mode 100644 index 0000000..11f6134 --- /dev/null +++ b/board/src/moves/knight.rs @@ -0,0 +1,90 @@ +// Eryn Wells + +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 = 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 + ); + } +} diff --git a/board/src/moves/mod.rs b/board/src/moves/mod.rs new file mode 100644 index 0000000..0d30d3f --- /dev/null +++ b/board/src/moves/mod.rs @@ -0,0 +1,94 @@ +// Eryn Wells + +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; + fn moves(&self, color: Color) -> dyn Iterator; + fn attacks(&self, color: Color) -> dyn Iterator; +} + +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, + } + }; + ($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 + '_ { + 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 { + 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; +} diff --git a/board/src/moves/move.rs b/board/src/moves/move.rs new file mode 100644 index 0000000..0f88aaf --- /dev/null +++ b/board/src/moves/move.rs @@ -0,0 +1,255 @@ +// Eryn Wells + +use crate::{piece::*, position::BoardSide, Square}; + +/// A move that transfers a piece from one square to another. +trait Move: Sized { + fn piece(&self) -> Piece; + fn from_square(&self) -> Square; + fn to_square(&self) -> Square; +} + +/// A move that captures an opposing piece. +trait Capturing: Move { + type CaptureMove; + + fn capturing(self, capturing: CapturedS) -> Self::CaptureMove; + fn captured_piece(&self) -> Piece; +} + +/// A move that promotes a pawn to another piece. +trait Promoting: Move { + type PromotionMove; + + fn promoting_to(self, shape: PromotingS) -> Self::PromotionMove; + fn promoting_piece(&self) -> Piece; +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct SimpleMove { + piece: Piece, + from_square: Square, + to_square: Square, +} + +impl SimpleMove { + fn new(piece: Piece, from_square: Square, to_square: Square) -> Self { + SimpleMove { + piece, + from_square, + to_square, + } + } + + fn capturing( + self, + captured_piece: Piece, + ) -> Capture { + Capture { + r#move: self, + captured_piece, + } + } +} + +impl Move for Piece { + fn piece(&self) -> Piece { + self.piece + } + + fn from_square(&self) -> Square { + self.from_square + } + + fn to_square(&self) -> Square { + self.to_square + } +} + +pub struct Capture { + r#move: dyn Move, + captured_piece: Piece, +} + +pub struct Promotion { + r#move: dyn Move, + promoting_to_shape: PromS, +} + +pub struct CapturingMove { + capturing: PlacedPiece, +} + +impl Move { + 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"); + } +} diff --git a/board/src/moves/move_generator.rs b/board/src/moves/move_generator.rs new file mode 100644 index 0000000..de61d3d --- /dev/null +++ b/board/src/moves/move_generator.rs @@ -0,0 +1,44 @@ +// Eryn Wells + +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 + '_ { + 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() + } +} diff --git a/board/src/moves/move_set.rs b/board/src/moves/move_set.rs new file mode 100644 index 0000000..683907c --- /dev/null +++ b/board/src/moves/move_set.rs @@ -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, + captures: Vec, +} + +/// 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, + ) -> 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, + ) -> 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 { + self.move_lists + .captures + .iter() + .chain(self.move_lists.quiet.iter()) + } +} diff --git a/board/src/moves/pawn.rs b/board/src/moves/pawn.rs new file mode 100644 index 0000000..a8f3313 --- /dev/null +++ b/board/src/moves/pawn.rs @@ -0,0 +1,394 @@ +// Eryn Wells + +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; 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 + '_ { + 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 { + &mut self.move_lists[MoveList::Quiet as usize] + } + + #[inline] + fn promotion_move_list(&mut self) -> &mut Vec { + &mut self.move_lists[MoveList::Promotions as usize] + } + + #[inline] + fn capture_move_list(&mut self) -> &mut Vec { + &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(¶meters); + self.generate_attacks_bitboard(¶meters); + } + + 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(¶meters); + 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 { + // 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 { + 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 = 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 = 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 = 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 = 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 = 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 = generator.collect(); + + assert_eq!( + generated_moves, expected_moves, + "generated: {:#?}\nexpected: {:#?}", + generated_moves, expected_moves + ); + } +} diff --git a/board/src/moves/queen.rs b/board/src/moves/queen.rs new file mode 100644 index 0000000..cdefb69 --- /dev/null +++ b/board/src/moves/queen.rs @@ -0,0 +1,146 @@ +// Eryn Wells + +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 + ) + ); + } +} diff --git a/board/src/moves/rook.rs b/board/src/moves/rook.rs new file mode 100644 index 0000000..783ed47 --- /dev/null +++ b/board/src/moves/rook.rs @@ -0,0 +1,140 @@ +// Eryn Wells + +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 + ) + ); + } +} diff --git a/board/src/moves/sight.rs b/board/src/moves/sight.rs new file mode 100644 index 0000000..771a524 --- /dev/null +++ b/board/src/moves/sight.rs @@ -0,0 +1,165 @@ +// Eryn Wells + +use crate::{piece::*, square::Direction, BitBoard, Position, Square}; + +pub(crate) trait Sight { + fn sight_on_empty_board(self, square: Square) -> BitBoard; + fn sight(self, square: Square, position: &Position) -> BitBoard; +} + +impl Sight for Piece { + 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(self, square: Square, position: &Position) -> 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 { + 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(self, square: Square, position: &Position) -> 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 { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::knight_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> BitBoard { + self.sight_on_empty_board(square) & !position.friendly_pieces() + } +} + +impl Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::bishop_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> 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 Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::rook_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> 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 Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::queen_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> 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 Sight for Piece { + fn sight_on_empty_board(self, square: Square) -> BitBoard { + BitBoard::king_moves(square) + } + + fn sight(self, square: Square, position: &Position) -> BitBoard { + self.sight_on_empty_board(square) & !position.friendly_pieces() + } +} diff --git a/board/src/piece.rs b/board/src/piece.rs new file mode 100644 index 0000000..0646f65 --- /dev/null +++ b/board/src/piece.rs @@ -0,0 +1,177 @@ +// Eryn Wells + +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 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 { + color: C, + shape: S, +} + +macro_rules! piece_constructor { + ($func_name:ident, $shape:tt) => { + pub fn $func_name(color: C) -> Piece { + Piece { + color, + shape: $shape, + } + } + }; +} + +impl Piece +where + C: Color, + S: Shape, +{ + pub fn new(color: C, shape: S) -> 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); + + pub fn color(&self) -> C { + self.color + } + + pub fn shape(&self) -> S { + self.shape + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct PlacedPiece { + piece: Piece, + square: Square, +} + +impl PlacedPiece { + pub const fn new(piece: Piece, square: Square) -> PlacedPiece { + PlacedPiece { piece, square } + } + + #[inline] + pub fn piece(&self) -> Piece { + 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!(>::into(Shape::Pawn) as char, 'p'); + } +} diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs deleted file mode 100644 index 52af054..0000000 --- a/board/src/piece_sets.rs +++ /dev/null @@ -1,169 +0,0 @@ -// Eryn Wells - -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 { - 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 { - 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, 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 { - 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(&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 - } -} diff --git a/board/src/piece_sets/counts.rs b/board/src/piece_sets/counts.rs deleted file mode 100644 index 7d3cade..0000000 --- a/board/src/piece_sets/counts.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Eryn Wells - -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); - } -} diff --git a/board/src/piece_sets/mailbox.rs b/board/src/piece_sets/mailbox.rs deleted file mode 100644 index 74b32bc..0000000 --- a/board/src/piece_sets/mailbox.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Eryn Wells - -use chessfriend_core::{Piece, Square}; -use std::iter::FromIterator; - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub(crate) struct Mailbox([Option; Square::NUM]); - -impl Mailbox { - pub fn get(&self, square: Square) -> Option { - 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 { - 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; Square::NUM]> for Mailbox { - fn from(value: [Option; Square::NUM]) -> Self { - Mailbox(value) - } -} - -impl FromIterator<(Square, Piece)> for Mailbox { - fn from_iter>(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::>(); - 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)), - ]) - ); - } -} diff --git a/board/src/position/diagram_formatter.rs b/board/src/position/diagram_formatter.rs new file mode 100644 index 0000000..731830a --- /dev/null +++ b/board/src/position/diagram_formatter.rs @@ -0,0 +1,73 @@ +// Eryn Wells + +use crate::{File, Position, Rank, Square}; +use std::{fmt, fmt::Write}; + +pub struct DiagramFormatter<'a, PosC>(&'a Position); + +impl<'a, PosC> DiagramFormatter<'a, PosC> { + pub fn new(position: &'a Position) -> DiagramFormatter { + 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); + } +} diff --git a/board/src/position/flags.rs b/board/src/position/flags.rs new file mode 100644 index 0000000..f8b05ec --- /dev/null +++ b/board/src/position/flags.rs @@ -0,0 +1,86 @@ +// Eryn Wells + +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( + color: C, + side: BoardSide, + ) -> u8 { + ((color as u8) << 1) + side as u8 + } + + pub(super) fn player_has_right_to_castle(&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( + &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( + &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)); + } +} diff --git a/board/src/position/mod.rs b/board/src/position/mod.rs new file mode 100644 index 0000000..650959d --- /dev/null +++ b/board/src/position/mod.rs @@ -0,0 +1,12 @@ +// Eryn Wells + +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; diff --git a/board/src/position/pieces.rs b/board/src/position/pieces.rs new file mode 100644 index 0000000..15f6695 --- /dev/null +++ b/board/src/position/pieces.rs @@ -0,0 +1,141 @@ +// Eryn Wells + +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, + + current_shape: Option, + + shape_iterator: std::array::IntoIter, + square_iterator: Option>>, +} + +impl<'a, PosC, C> Pieces<'a, PosC, C> { + pub(crate) fn new(position: &Position, color: C) -> Pieces { + 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; + + fn next(&mut self) -> Option { + 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 = None; + let mut next_nonempty_bitboard: Option = 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 = 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( + pos: &mut Position, + piece: Piece, + 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); + } +} diff --git a/board/src/position/position.rs b/board/src/position/position.rs new file mode 100644 index 0000000..73de794 --- /dev/null +++ b/board/src/position/position.rs @@ -0,0 +1,332 @@ +// Eryn Wells + +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 { + 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, +} + +impl Position { + pub fn empty() -> Position { + 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 { + 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(&self, color: C, side: BoardSide) -> bool { + self.flags.player_has_right_to_castle(color, side) + } + + pub fn place_piece( + &mut self, + piece: Piece, + 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(&self, color: C) -> Moves { + // Moves::new(self, color) + // } + + pub(crate) fn bitboard_for_piece(&self, piece: Piece) -> BitBoard { + self.pieces_per_type[piece.color() as usize][piece.shape() as usize] + } + + fn bitboard_for_piece_mut(&mut self, piece: Piece) -> &mut BitBoard { + &mut self.pieces_per_type[piece.color() as usize][piece.shape() as usize] + } + + pub(crate) fn bitboard_for_color(&self, color: C) -> BitBoard { + self.pieces_per_color[color as usize] + } + + fn bitboard_for_color_mut(&mut self, color: C) -> &mut BitBoard { + &mut self.pieces_per_color[color as usize] + } + + pub fn piece_on_square(&self, sq: Square) -> Option> { + 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(&self, color: C) -> Pieces { + Pieces::new(&self, color) + } + + pub fn king(&self, color: C) -> PlacedPiece { + 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 Position { + /// 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 { + self.en_passant_square + } +} + +impl Position { + fn sight_of_pieces_of_color(&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(&self, placed_piece: PlacedPiece) -> BitBoard { + placed_piece.piece().sight() + } +} + +impl Default for Position { + fn default() -> Self { + Self::empty() + } +} + +impl FromIterator> for Position { + fn from_iter>>(iter: T) -> Self { + let mut position = Position::empty(); + for placed_piece in iter { + _ = position.place_piece(placed_piece.piece(), placed_piece.square()); + } + + position + } +} + +impl fmt::Debug for Position { + 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()); + } +} diff --git a/board/src/sight.rs b/board/src/sight.rs deleted file mode 100644 index e682cb0..0000000 --- a/board/src/sight.rs +++ /dev/null @@ -1,423 +0,0 @@ -// Eryn Wells - -//! 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) -> 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) -> 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] - ); - } -} diff --git a/board/src/square.rs b/board/src/square.rs new file mode 100644 index 0000000..d380b29 --- /dev/null +++ b/board/src/square.rs @@ -0,0 +1,334 @@ +// Eryn Wells + +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 { + $( + #[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 for File { + fn into(self) -> char { + ('a' as u8 + self as u8) as char + } +} + +impl TryFrom for File { + type Error = ParseFileError; + + fn try_from(value: char) -> Result { + 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::::into(*self).to_uppercase()) + } +} + +impl Into for Rank { + fn into(self) -> char { + ('1' as u8 + self as u8) as char + } +} + +impl TryFrom for Rank { + type Error = ParseFileError; + + fn try_from(value: char) -> Result { + 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::::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 { + s.parse() + } + + pub fn neighbor(self, direction: Direction) -> Option { + 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 { + 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"); + } +} diff --git a/board/src/zobrist.rs b/board/src/zobrist.rs deleted file mode 100644 index b5a5d43..0000000 --- a/board/src/zobrist.rs +++ /dev/null @@ -1,234 +0,0 @@ -// Eryn Wells - -//! 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, - - /// 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) -> 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 { - 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, - new_target: Option, - ) -> 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); - } -} diff --git a/chessfriend/Cargo.toml b/chessfriend/Cargo.toml deleted file mode 100644 index 25bc79e..0000000 --- a/chessfriend/Cargo.toml +++ /dev/null @@ -1,5 +0,0 @@ -[package] -name = "chessfriend" -version = "0.1.0" -edition = "2024" - diff --git a/core/Cargo.toml b/core/Cargo.toml deleted file mode 100644 index 26d0835..0000000 --- a/core/Cargo.toml +++ /dev/null @@ -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" diff --git a/core/src/colors.rs b/core/src/colors.rs deleted file mode 100644 index e1255b7..0000000 --- a/core/src/colors.rs +++ /dev/null @@ -1,121 +0,0 @@ -// Eryn Wells - -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::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 for Color { - type Error = ColorFromCharError; - - fn try_from(value: char) -> Result { - 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 { - 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::try_from(s.to_lowercase().as_str()) - } -} diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs deleted file mode 100644 index 308fc3e..0000000 --- a/core/src/coordinates.rs +++ /dev/null @@ -1,612 +0,0 @@ -// Eryn Wells - -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 { - #[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 for $name { - type Error = (); - - fn try_from(value: u8) -> Result { - 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 { - 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 { - (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::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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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::::into(*self)) - } -} - -impl fmt::Display for Rank { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", Into::::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 for char { - fn from(value: File) -> Self { - let u8value: u8 = value.into(); - (u8value + b'a') as char - } -} - -impl From for char { - fn from(value: Rank) -> Self { - Self::from(value.0 + b'1') - } -} - -impl TryFrom for File { - type Error = (); - - fn try_from(value: char) -> Result { - File::try_from(value.to_ascii_lowercase() as u8 - b'a') - } -} - -impl TryFrom for Rank { - type Error = (); - - fn try_from(value: char) -> Result { - 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::().is_err()); - assert!("j3".parse::().is_err()); - assert!("a9".parse::().is_err()); - assert!("b-1".parse::().is_err()); - assert!("a 1".parse::().is_err()); - assert!("".parse::().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::::into(Rank::ONE), '1'); - assert_eq!(Into::::into(File::B), 'b'); - } -} diff --git a/core/src/coordinates/wings.rs b/core/src/coordinates/wings.rs deleted file mode 100644 index ed0d0b8..0000000 --- a/core/src/coordinates/wings.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Eryn Wells - -#[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"), - } - } -} diff --git a/core/src/lib.rs b/core/src/lib.rs deleted file mode 100644 index f298a81..0000000 --- a/core/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Eryn Wells - -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; -} diff --git a/core/src/macros.rs b/core/src/macros.rs deleted file mode 100644 index dc99d9a..0000000 --- a/core/src/macros.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Eryn Wells - -#[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) - } -} diff --git a/core/src/pieces.rs b/core/src/pieces.rs deleted file mode 100644 index 5291a4d..0000000 --- a/core/src/pieces.rs +++ /dev/null @@ -1,110 +0,0 @@ -// Eryn Wells - -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!(>::into(Shape::Pawn), 'P'); - } -} diff --git a/core/src/pieces/display.rs b/core/src/pieces/display.rs deleted file mode 100644 index 77fadb6..0000000 --- a/core/src/pieces/display.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Eryn Wells - -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()) - } - } - } -} diff --git a/core/src/random.rs b/core/src/random.rs deleted file mode 100644 index 29b357c..0000000 --- a/core/src/random.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Eryn Wells - -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: ::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 } - } -} diff --git a/core/src/score.rs b/core/src/score.rs deleted file mode 100644 index 3528861..0000000 --- a/core/src/score.rs +++ /dev/null @@ -1,126 +0,0 @@ -// Eryn Wells - -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 for Score { - type Output = Self; - - fn mul(self, rhs: Value) -> Self::Output { - Score(self.0 * rhs) - } -} - -impl Mul 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 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") - } - } -} diff --git a/core/src/shapes.rs b/core/src/shapes.rs deleted file mode 100644 index 77126ba..0000000 --- a/core/src/shapes.rs +++ /dev/null @@ -1,179 +0,0 @@ -// Eryn Wells - -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 { - 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 for Shape { - fn from(value: Slider) -> Self { - match value { - Slider::Bishop => Shape::Bishop, - Slider::Rook => Shape::Rook, - Slider::Queen => Shape::Queen, - } - } -} - -impl TryFrom for Slider { - type Error = (); - - fn try_from(value: Shape) -> Result { - 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 for Shape { - type Error = ShapeFromCharError; - - fn try_from(value: char) -> Result { - 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 { - 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 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}") - } -} diff --git a/doc/ChessPieces.txt b/doc/ChessPieces.txt deleted file mode 100644 index 0c641ce..0000000 --- a/doc/ChessPieces.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/explorer/Cargo.toml b/explorer/Cargo.toml index 00fa045..5d49281 100644 --- a/explorer/Cargo.toml +++ b/explorer/Cargo.toml @@ -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" diff --git a/explorer/src/main.rs b/explorer/src/main.rs index cfaf102..7eebb2b 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,31 +1,14 @@ -// Eryn Wells +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, +#[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 { - 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 { + 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::("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::("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::("color") - .ok_or(CommandHandlingError::MissingArgument("color"))?; - let color = color.parse::()?; - - let shape = matches - .get_one::("piece") - .ok_or(CommandHandlingError::MissingArgument("piece"))?; - let shape = shape.parse::()?; - - let square = matches - .get_one::("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::("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 { - let fen_string = matches - .get_one::("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 { - let move_string = matches - .get_one::("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 { - 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::("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 { - let moves: Vec = if let Some(square) = matches - .get_one::("square") - .and_then(|square| square.parse::().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 = 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: anyhow::Result { - let square = *matches - .get_one::("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 { - let depth = *matches - .get_one::("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; } } diff --git a/explorer/src/make_command.rs b/explorer/src/make_command.rs deleted file mode 100644 index ef3d809..0000000 --- a/explorer/src/make_command.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Eryn Wells - -use clap::ArgMatches; - -pub enum MakeCommandError {} - -pub struct MakeCommand {} diff --git a/moves/Cargo.toml b/moves/Cargo.toml deleted file mode 100644 index 797f7c8..0000000 --- a/moves/Cargo.toml +++ /dev/null @@ -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" diff --git a/moves/src/algebraic.rs b/moves/src/algebraic.rs deleted file mode 100644 index f1c5936..0000000 --- a/moves/src/algebraic.rs +++ /dev/null @@ -1,134 +0,0 @@ -// Eryn Wells - -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, - }, -} - -#[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 { - 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(()) - } -} diff --git a/moves/src/defs.rs b/moves/src/defs.rs deleted file mode 100644 index 03f2a19..0000000 --- a/moves/src/defs.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Eryn Wells - -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 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 for PromotionShape { - type Error = (); - - fn try_from(value: Shape) -> Result { - match value { - Shape::Knight => Ok(PromotionShape::Knight), - Shape::Bishop => Ok(PromotionShape::Bishop), - Shape::Rook => Ok(PromotionShape::Rook), - Shape::Queen => Ok(PromotionShape::Queen), - _ => Err(()), - } - } -} diff --git a/moves/src/generators.rs b/moves/src/generators.rs deleted file mode 100644 index 04e6a56..0000000 --- a/moves/src/generators.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Eryn Wells - -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 { - 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 for Move { - fn from(value: GeneratedMove) -> Self { - value.ply - } -} - -impl From for GeneratedMove { - fn from(value: Move) -> Self { - GeneratedMove { ply: value } - } -} diff --git a/moves/src/generators/all.rs b/moves/src/generators/all.rs deleted file mode 100644 index 581930f..0000000 --- a/moves/src/generators/all.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Eryn Wells - -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, - knight: Fuse, - bishop: Fuse, - rook: Fuse, - queen: Fuse, - king: Fuse, -} - -impl AllPiecesMoveGenerator { - pub fn new(board: &Board, color: Option) -> 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 { - 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 - // 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 {} diff --git a/moves/src/generators/king.rs b/moves/src/generators/king.rs deleted file mode 100644 index ed6458b..0000000 --- a/moves/src/generators/king.rs +++ /dev/null @@ -1,370 +0,0 @@ -// Eryn Wells - -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, - next_kings_index: usize, - current_king: Option, - castle_iterator: CastleIterator, - friends: BitBoard, - enemies: BitBoard, -} - -impl KingMoveGenerator { - pub fn new(board: &Board, color: Option) -> Self { - let color = board.unwrap_color(color); - - let friends = board.friendly_occupancy(color); - let enemies = board.enemies(color); - - let kings: Vec = 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 { - 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.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 { - 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), - ] - ); - } -} diff --git a/moves/src/generators/knight.rs b/moves/src/generators/knight.rs deleted file mode 100644 index 96bd3bd..0000000 --- a/moves/src/generators/knight.rs +++ /dev/null @@ -1,168 +0,0 @@ -// Eryn Wells - -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, - current_origin: Option, - friends: BitBoard, - enemies: BitBoard, -} - -impl KnightMoveGenerator { - pub fn new(board: &Board, color: Option) -> 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 { - 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), - ] - ); - } -} diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs deleted file mode 100644 index 6d07541..0000000 --- a/moves/src/generators/pawn.rs +++ /dev/null @@ -1,539 +0,0 @@ -// Eryn Wells - -//! 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, - 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) -> 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 { - 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 { - 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 { - 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 { - 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 { - 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 = - 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 = - 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 = - 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 = - 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 = - 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 = - 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 = - 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 = - 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 = - 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 = - 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 = - 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 = - 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 = - 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.)]); - } -} diff --git a/moves/src/generators/slider.rs b/moves/src/generators/slider.rs deleted file mode 100644 index cd704d7..0000000 --- a/moves/src/generators/slider.rs +++ /dev/null @@ -1,612 +0,0 @@ -// Eryn Wells - -//! 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) -> Self { - Self(SliderMoveGenerator::new(board, Slider::$slider, color)) - } - } - - impl Iterator for $name { - type Item = $crate::GeneratedMove; - - fn next(&mut self) -> Option { - 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, - next_sliders_index: usize, - current_slider: Option, - enemies: BitBoard, - friends: BitBoard, -} - -impl SliderMoveGenerator { - fn new(board: &Board, slider: Slider, color: Option) -> 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 { - 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.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 - ] - ); - } -} diff --git a/moves/src/lib.rs b/moves/src/lib.rs deleted file mode 100644 index 69d1d54..0000000 --- a/moves/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -// Eryn Wells - -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}; diff --git a/moves/src/macros.rs b/moves/src/macros.rs deleted file mode 100644 index 4c9c66b..0000000 --- a/moves/src/macros.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Eryn Wells - -#[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::>(), - generated_moves - .difference(&expected_moves) - .map(|mv| format!("{}", mv)) - .collect::>(), - expected_moves - .difference(&generated_moves) - .map(|mv| format!("{}", mv)) - .collect::>(), - ); - } - }; - ($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::>(), - generated_moves - .difference(&expected_moves) - .map(|mv| format!("{}", mv)) - .collect::>(), - expected_moves - .difference(&generated_moves) - .map(|mv| format!("{}", mv)) - .collect::>(), - ); - } - }; -} - -#[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())); - )* - } - }; -} diff --git a/moves/src/make_move.rs b/moves/src/make_move.rs deleted file mode 100644 index bcbca0a..0000000 --- a/moves/src/make_move.rs +++ /dev/null @@ -1,589 +0,0 @@ -// Eryn Wells - -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; - -#[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; - - fn advance_board_state( - &mut self, - ply: &Move, - piece_moved: &Piece, - en_passant_target: Option, - half_move_clock: HalfMoveClock, - ); -} - -impl 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 { - 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 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, - 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 { - 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(()) - } -} diff --git a/moves/src/moves.rs b/moves/src/moves.rs deleted file mode 100644 index db5636b..0000000 --- a/moves/src/moves.rs +++ /dev/null @@ -1,292 +0,0 @@ -// Eryn Wells - -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 { - 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 { - 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 { - 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() - } -} diff --git a/moves/src/record.rs b/moves/src/record.rs deleted file mode 100644 index 24472b8..0000000 --- a/moves/src/record.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Eryn Wells - -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, - - /// 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, -} - -impl MoveRecord { - #[must_use] - pub fn new(board: &Board, ply: Move, capture: Option) -> 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, - } - } -} diff --git a/moves/src/testing.rs b/moves/src/testing.rs deleted file mode 100644 index c4ef5a5..0000000 --- a/moves/src/testing.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Eryn Wells - -pub type TestResult = Result<(), TestError>; - -#[derive(Debug, Eq, PartialEq)] -pub enum TestError { - NoLegalMoves, -} diff --git a/moves/src/unmake_move.rs b/moves/src/unmake_move.rs deleted file mode 100644 index 6833608..0000000 --- a/moves/src/unmake_move.rs +++ /dev/null @@ -1,486 +0,0 @@ -// Eryn Wells - -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 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 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>; - - /// 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(()) - } -} diff --git a/perft/Cargo.toml b/perft/Cargo.toml deleted file mode 100644 index 193bb43..0000000 --- a/perft/Cargo.toml +++ /dev/null @@ -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" diff --git a/perft/data/peterellisjones-perft-positions.json b/perft/data/peterellisjones-perft-positions.json deleted file mode 100644 index eb48f34..0000000 --- a/perft/data/peterellisjones-perft-positions.json +++ /dev/null @@ -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" - } -] \ No newline at end of file diff --git a/perft/scripts/check-positions b/perft/scripts/check-positions deleted file mode 100755 index a7b24ec..0000000 --- a/perft/scripts/check-positions +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# Eryn Wells - -''' -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)) diff --git a/perft/src/main.rs b/perft/src/main.rs deleted file mode 100644 index d1e7f77..0000000 --- a/perft/src/main.rs +++ /dev/null @@ -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, -} - -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(()) -} diff --git a/position/Cargo.toml b/position/Cargo.toml deleted file mode 100644 index c0c5225..0000000 --- a/position/Cargo.toml +++ /dev/null @@ -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" diff --git a/position/src/check.rs b/position/src/check.rs deleted file mode 100644 index 8619e29..0000000 --- a/position/src/check.rs +++ /dev/null @@ -1,73 +0,0 @@ -// Eryn Wells - -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] - } -} diff --git a/position/src/evaluation.rs b/position/src/evaluation.rs deleted file mode 100644 index 8496760..0000000 --- a/position/src/evaluation.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Eryn Wells - -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> { - 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) - ); - } -} diff --git a/position/src/lib.rs b/position/src/lib.rs deleted file mode 100644 index 7ccee47..0000000 --- a/position/src/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Eryn Wells - -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; diff --git a/position/src/macros.rs b/position/src/macros.rs deleted file mode 100644 index 1d66d1b..0000000 --- a/position/src/macros.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Eryn Wells - -#[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)) - }; -} diff --git a/position/src/move_generator/tests.rs b/position/src/move_generator/tests.rs deleted file mode 100644 index e6674d3..0000000 --- a/position/src/move_generator/tests.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Eryn Wells - -mod peterellisjones; -mod single_pieces; diff --git a/position/src/move_generator/tests/single_pieces.rs b/position/src/move_generator/tests/single_pieces.rs deleted file mode 100644 index f8d06fb..0000000 --- a/position/src/move_generator/tests/single_pieces.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Eryn Wells - -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(()) -} diff --git a/position/src/perft.rs b/position/src/perft.rs deleted file mode 100644 index 683cb67..0000000 --- a/position/src/perft.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Eryn Wells - -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 = 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) - } -} diff --git a/position/src/position.rs b/position/src/position.rs deleted file mode 100644 index 1763875..0000000 --- a/position/src/position.rs +++ /dev/null @@ -1,379 +0,0 @@ -// Eryn Wells - -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, - pub(crate) captures: CapturesList, - - /// A set of hashes of board positions seen throughout the move record. - boards_seen: HashSet, -} - -impl Position { - pub fn empty(zobrist: Option>) -> Self { - Self::new(Board::empty(zobrist)) - } - - /// Return a starting position. - pub fn starting(zobrist: Option>) -> 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, PlacePieceError> { - self.board.place_piece(piece, square, strategy) - } - - #[must_use] - pub fn get_piece(&self, square: Square) -> Option { - self.board.get_piece(square) - } - - pub fn remove_piece(&mut self, square: Square) -> Option { - 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) -> 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, - ) -> Box + '_> { - 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>> { - self.get_piece(square) - .map(|piece| Self::generator(&self.board, piece)) - } - - #[must_use] - fn generator(board: &Board, piece: Piece) -> Box> { - 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 { - 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 { - 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, - ) -> Option { - 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 { - self.board.zobrist_hash() - } - - pub fn set_zobrist_state(&mut self, state: Arc) { - 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 { - let board = Board::from_fen_str(string)?; - Ok(Position::new(board)) - } -} - -impl ToFenStr for Position { - type Error = ::Error; - - fn to_fen_str(&self) -> Result { - 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))); - } -} diff --git a/position/src/position/builders/mod.rs b/position/src/position/builders/mod.rs deleted file mode 100644 index 67ee23e..0000000 --- a/position/src/position/builders/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Eryn Wells - -mod move_builder; - -pub use move_builder::{Builder as MoveBuilder, MakeMoveError}; diff --git a/position/src/position/builders/move_builder.rs b/position/src/position/builders/move_builder.rs deleted file mode 100644 index 9cf0806..0000000 --- a/position/src/position/builders/move_builder.rs +++ /dev/null @@ -1,383 +0,0 @@ -// Eryn Wells - -use crate::Position; -use chessfriend_bitboard::BitBoard; -use chessfriend_board::{castle, castle::Castle, en_passant::EnPassant}; -use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; -use chessfriend_moves::Move; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum MakeMoveError { - PlayerOutOfTurn, - NoPiece, - NoCapturedPiece, - NoLegalMoves, - IllegalCastle, - IllegalSquare(Square), -} - -/// A position builder that builds a new position by making a move. -#[derive(Clone)] -pub struct Builder<'p, M: MoveToMake> { - position: &'p Position, - move_to_make: M, -} - -pub trait MoveToMake {} - -pub struct NoMove; - -pub enum ValidatedMove { - RegularMove { - from_square: Square, - to_square: Square, - moving_piece: PlacedPiece, - captured_piece: Option, - promotion: Option, - castling_rights: castle::Rights, - en_passant: Option, - should_increment_ply: bool, - }, - Castle { - castle: Castle, - king: PlacedPiece, - rook: PlacedPiece, - castling_rights: castle::Rights, - }, -} - -impl MoveToMake for NoMove {} -impl MoveToMake for ValidatedMove {} - -impl<'p> Builder<'p, NoMove> { - pub fn new(position: &'p Position) -> Self { - Builder { - position, - move_to_make: NoMove, - } - } -} - -impl<'p, M> Builder<'p, M> -where - M: MoveToMake, -{ - pub fn make(self, mv: &Move) -> Result, MakeMoveError> { - let origin_square = mv.origin_square(); - - let piece = self - .position - .board - .piece_on_square(origin_square) - .ok_or(MakeMoveError::NoPiece)?; - - let target_square = mv.target_square(); - - let moves = self - .position - .moves_for_piece(&piece) - .ok_or(MakeMoveError::NoLegalMoves)?; - - match mv.castle() { - Some(castle) => { - if !moves.can_castle(castle) { - return Err(MakeMoveError::IllegalCastle); - } - } - None => { - if !moves.can_move_to_square(target_square) { - return Err(MakeMoveError::IllegalSquare(target_square)); - } - } - } - - let player = self.position.player_to_move(); - - let captured_piece = if mv.is_en_passant() { - // En passant captures the pawn directly ahead (in the player's direction) of the en passant square. - let capture_square = match player { - Color::White => target_square.neighbor(Direction::South), - Color::Black => target_square.neighbor(Direction::North), - } - .ok_or(MakeMoveError::NoCapturedPiece)?; - - Some( - self.position - .board - .piece_on_square(capture_square) - .ok_or(MakeMoveError::NoCapturedPiece)?, - ) - } else if mv.is_capture() { - Some( - self.position - .board - .piece_on_square(target_square) - .ok_or(MakeMoveError::NoCapturedPiece)?, - ) - } else { - None - }; - - // TODO: Check whether the move is legal. - - let piece_is_king = piece.is_king(); - let mut castling_rights = self.position.board.castling_rights; - - if piece_is_king { - castling_rights.clear_player_has_right_to_castle_flag(player, Castle::KingSide); - castling_rights.clear_player_has_right_to_castle_flag(player, Castle::QueenSide); - } else if piece.is_kingside_rook() { - castling_rights.clear_player_has_right_to_castle_flag(player, Castle::KingSide); - } else if piece.is_queenside_rook() { - castling_rights.clear_player_has_right_to_castle_flag(player, Castle::QueenSide); - } - - if let Some(castle) = mv.castle() { - println!("piece is king: {}", piece_is_king); - if !piece_is_king || !self.position.player_can_castle(player, castle) { - return Err(MakeMoveError::IllegalCastle); - } - - let rook = self - .position - .rook_for_castle(player, castle) - .ok_or(MakeMoveError::NoPiece)?; - - Ok(Builder { - position: self.position, - move_to_make: ValidatedMove::Castle { - castle, - king: piece, - rook, - castling_rights, - }, - }) - } else { - let en_passant = if mv.is_double_push() { - match piece.color() { - Color::White => target_square.neighbor(Direction::South), - Color::Black => target_square.neighbor(Direction::North), - } - .and_then(EnPassant::from_target_square) - } else { - None - }; - - Ok(Builder { - position: self.position, - move_to_make: ValidatedMove::RegularMove { - from_square: origin_square, - to_square: target_square, - moving_piece: piece, - captured_piece, - promotion: mv.promotion(), - castling_rights, - en_passant, - should_increment_ply: !(mv.is_capture() || piece.is_pawn()), - }, - }) - } - } -} - -impl<'p> Builder<'p, ValidatedMove> { - pub fn build(&self) -> Position { - let player = self.position.player_to_move(); - - let updated_move_number = self.position.board.move_counter.fullmove_number - + if player == Color::Black { 1 } else { 0 }; - - match self.move_to_make { - ValidatedMove::RegularMove { - from_square, - to_square, - moving_piece, - captured_piece, - promotion, - castling_rights, - en_passant, - should_increment_ply: increment_ply, - } => { - let mut board = self.position.board.clone(); - - board.castling_rights = castling_rights; - board.en_passant = en_passant; - - if let Some(captured_piece) = captured_piece { - board.remove_piece_from_square(captured_piece.square()); - } - - if let Some(promotion) = promotion { - board.remove_piece_from_square(moving_piece.square()); - board.place_piece_on_square(Piece::new(player, promotion), to_square); - } else { - board.remove_piece_from_square(from_square); - board.place_piece_on_square(moving_piece.piece(), to_square); - } - - let ply = if increment_ply { - self.position.board.move_counter.halfmove_number + 1 - } else { - 0 - }; - - Position::new(board) - } - ValidatedMove::Castle { - castle, - king, - rook, - castling_rights, - } => { - let mut board = self.position.board.clone(); - - board.castling_rights = castling_rights; - - let next_active_color = board.move_counter.active_color.next(); - board.move_counter.active_color = next_active_color; - - let parameters = castle.parameters(player); - - board.remove_piece_from_square(king.square()); - board.place_piece_on_square(king.piece(), parameters.king_target_square()); - - board.remove_piece_from_square(rook.square()); - board.place_piece_on_square(rook.piece(), parameters.rook_target_square()); - - Position::new(board) - } - } - } -} - -impl<'p> From<&'p Position> for Builder<'p, NoMove> { - fn from(position: &'p Position) -> Self { - Self { - position, - move_to_make: NoMove, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::testing::*; - use crate::{position, test_position}; - use chessfriend_core::{piece, File}; - use chessfriend_moves::Builder as MoveBuilder; - - #[test] - fn move_white_pawn_one_square() -> TestResult { - let pos = position![White Pawn on E2]; - let mv = MoveBuilder::new() - .from(Square::E2) - .to(Square::E3) - .build() - .map_err(TestError::BuildMove)?; - - let new_position = Builder::::new(&pos) - .make(&mv) - .map_err(|err| TestError::MakeMove(err))? - .build(); - println!("{}", &new_position); - - assert_eq!( - new_position.board.piece_on_square(Square::E3), - Some(piece!(White Pawn on E3)) - ); - - Ok(()) - } - - #[test] - fn move_white_pawn_two_squares() -> TestResult { - let pos = test_position![White Pawn on E2]; - - let mv = MoveBuilder::double_push(File::E, Color::White).build()?; - - let new_position = Builder::new(&pos).make(&mv)?.build(); - println!("{}", &new_position); - - assert_eq!( - new_position.board.piece_on_square(Square::E4), - Some(piece!(White Pawn on E4)) - ); - - let en_passant = new_position.en_passant(); - assert!(en_passant.is_some()); - assert_eq!(en_passant.map(EnPassant::target_square), Some(Square::E3)); - - Ok(()) - } - - #[test] - fn white_kingside_castle() -> TestResult { - let pos = test_position![ - White King on E1, - White Rook on H1, - White Pawn on E2, - White Pawn on F2, - White Pawn on G2, - White Pawn on H2 - ]; - - let mv = MoveBuilder::castling(Color::White, Castle::KingSide).build()?; - - let new_position = Builder::new(&pos).make(&mv)?.build(); - println!("{}", &new_position); - - assert_eq!( - new_position.board.piece_on_square(Square::G1), - Some(piece!(White King on G1)) - ); - assert_eq!( - new_position.board.piece_on_square(Square::F1), - Some(piece!(White Rook on F1)) - ); - - Ok(()) - } - - #[test] - fn en_passant_capture() -> TestResult { - let pos = test_position!(Black, [ - White Pawn on B5, - Black Pawn on A7, - ]); - - let black_pawn_move = MoveBuilder::double_push(File::A, Color::Black).build()?; - - assert!(black_pawn_move.is_double_push()); - assert!(!black_pawn_move.is_en_passant()); - - let en_passant_position = Builder::new(&pos).make(&black_pawn_move)?.build(); - println!("{en_passant_position}"); - - assert_eq!( - en_passant_position.board.piece_on_square(Square::A5), - Some(piece!(Black Pawn on A5)) - ); - assert_eq!( - en_passant_position.board.piece_on_square(Square::B5), - Some(piece!(White Pawn on B5)) - ); - - let white_pawn_capture = MoveBuilder::push(&piece!(White Pawn on B5)) - .capturing_en_passant_on(Square::A6) - .build()?; - let en_passant_capture = Builder::new(&en_passant_position) - .make(&white_pawn_capture)? - .build(); - println!("{en_passant_capture}"); - - assert_eq!(en_passant_capture.board.piece_on_square(Square::A5), None); - assert_eq!(en_passant_capture.board.piece_on_square(Square::B5), None); - assert_eq!( - en_passant_capture.board.piece_on_square(Square::A6), - Some(piece!(White Pawn on A6)) - ); - - Ok(()) - } -} diff --git a/position/src/position/captures.rs b/position/src/position/captures.rs deleted file mode 100644 index 4c7f4bb..0000000 --- a/position/src/position/captures.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Eryn Wells - -use std::fmt::Display; - -use chessfriend_core::{Color, Piece}; - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub(crate) struct CapturesList([Vec; Color::NUM]); - -impl CapturesList { - pub fn push(&mut self, color: Color, piece: Piece) { - self.0[color as usize].push(piece); - } - - pub fn pop(&mut self, color: Color) -> Option { - self.0[color as usize].pop() - } - - pub fn is_empty(&self) -> bool { - self.0.iter().all(Vec::is_empty) - } -} - -impl Display for CapturesList { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut is_first = true; - for color in Color::into_iter() { - if is_first { - is_first = false; - } else { - writeln!(f)?; - } - - write!(f, "{color}: ")?; - - let captures = &self.0[color as usize]; - for piece in captures { - write!(f, "{piece}")?; - } - - write!(f, "")?; - } - - Ok(()) - } -} diff --git a/position/src/position/position.rs b/position/src/position/position.rs deleted file mode 100644 index 80e9f65..0000000 --- a/position/src/position/position.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Eryn Wells - -/*** - * Keeping this code around for a little while because it might still come in - * handy, but it should be considered dead. - ***/ - -/* -impl Position { - pub fn moves(&self) -> &Moves { - self.moves.get_or_init(|| { - let player_to_move = self.player_to_move(); - let checking_pieces = self.checking_pieces(); - match checking_pieces.count() { - // Normal, unrestricted move generation - 0 => Moves::new( - &self.board, - player_to_move, - BitBoard::full(), - BitBoard::full(), - ), - 1 => { - // Calculate push and capture masks for checking piece. Moves are restricted to - // those that intersect those masks. - let capture_mask = checking_pieces.capture_mask(); - let push_mask = checking_pieces.push_mask(self.king_bitboard(player_to_move)); - Moves::new(&self.board, player_to_move, capture_mask, push_mask) - } - // With more than one checking piece, the only legal moves are king moves. - _ => Moves::new( - &self.board, - player_to_move, - BitBoard::empty(), - BitBoard::empty(), - ), - } - }) - } - - pub(crate) fn moves_for_piece(&self, piece: &PlacedPiece) -> Option<&MoveSet> { - self.moves().moves_for_piece(piece) - } - - pub(crate) fn checking_pieces(&self) -> CheckingPieces { - let opponent = self.player_to_move().other(); - let king_square = self.king_square(self.player_to_move()); - - let checking_pawns = { - // The current player's pawn attack moves *from* this square are the - // same as the pawn moves for the opposing player attacking this square. - let pawn_moves_to_king_square = - BitBoard::pawn_attacks(king_square, self.player_to_move()); - let opposing_pawn = Piece::pawn(opponent); - let opposing_pawns = self.board.bitboard_for_piece(opposing_pawn); - - pawn_moves_to_king_square & opposing_pawns - }; - - macro_rules! checking_piece { - ($moves_bb_fn:path, $piece_fn:ident) => {{ - let moves_from_opposing_square = $moves_bb_fn(king_square); - let piece = Piece::$piece_fn(opponent); - let opposing_pieces = self.board.bitboard_for_piece(piece); - - moves_from_opposing_square & opposing_pieces - }}; - } - - let checking_knights = checking_piece!(BitBoard::knight_moves, knight); - let checking_bishops = checking_piece!(BitBoard::bishop_moves, bishop); - let checking_rooks = checking_piece!(BitBoard::rook_moves, rook); - let checking_queens = checking_piece!(BitBoard::queen_moves, queen); - - CheckingPieces::new( - checking_pawns, - checking_knights, - checking_bishops, - checking_rooks, - checking_queens, - ) - } -} -*/ - -#[cfg(test)] -mod tests { - // #[test] - // fn king_is_in_check() { - // let pos = position![ - // White King on E1, - // Black Rook on E8, - // ]; - // assert!(pos.is_king_in_check()); - // } - - // #[test] - // fn king_is_not_in_check() { - // let pos = position![ - // White King on F1, - // Black Rook on E8, - // ]; - // assert!(!pos.is_king_in_check()); - // } - - // #[test] - // fn king_not_on_starting_square_cannot_castle() { - // let pos = test_position!(White King on E4); - // let rights = pos.board.castling_rights; - // assert!(!rights.color_has_right(Color::White, Castle::KingSide)); - // assert!(!rights.color_has_right(Color::White, Castle::QueenSide)); - // } - - // #[test] - // fn danger_squares() { - // let pos = test_position!(Black, [ - // White King on E1, - // Black King on E7, - // White Rook on E4, - // ]); - - // let danger_squares = pos.king_danger(Color::Black); - // let expected = bitboard![D1 F1 D2 E2 F2 E3 A4 B4 C4 D4 F4 G4 H4 E5 E6 E7 E8]; - // assert_eq_bitboards!(danger_squares, expected); - // } -} diff --git a/position/src/testing.rs b/position/src/testing.rs deleted file mode 100644 index c5f4491..0000000 --- a/position/src/testing.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Eryn Wells - -use chessfriend_moves::MakeMoveError; - -#[macro_export] -macro_rules! assert_eq_bitboards { - ($result:expr, $expected:expr) => {{ - let result = $result; - let expected = $expected; - assert_eq!( - result, expected, - "Result:\n{}\nExpected:\n{}", - result, expected - ); - }}; -} - -pub type TestResult = Result<(), TestError>; - -#[derive(Debug, Eq, PartialEq)] -pub enum TestError { - MakeMove(MakeMoveError), - NoLegalMoves, -} - -impl From for TestError { - fn from(value: MakeMoveError) -> Self { - TestError::MakeMove(value) - } -} diff --git a/position/tests/peterellisjones.rs b/position/tests/peterellisjones.rs deleted file mode 100644 index 06ccc45..0000000 --- a/position/tests/peterellisjones.rs +++ /dev/null @@ -1,166 +0,0 @@ -// Eryn Wells - -//! Move generator tests based on board positions described in [Peter Ellis -//! Jones][1]' excellent [blog post][2] on generated legal chess moves. -//! -//! [1]: https://peterellisjones.com -//! [2]: https://peterellisjones.com/posts/generating-legal-chess-moves-efficiently/ - -use chessfriend_core::Color; -use chessfriend_moves::{ - Move, assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, -}; -use chessfriend_position::test_position; -use std::collections::HashSet; - -#[test] -fn pseudo_legal_move_generation() { - let pos = test_position!(Black, [ - Black King on E8, - White King on E1, - White Rook on F5, - ]); - - let king_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); - - assert_move_list_does_not_contain!(king_moves, [ply!(E8 - F8), ply!(E8 - F7)]); -} - -#[test] -fn gotcha_king_moves_away_from_checking_slider() { - let position = test_position!(Black, [ - Black King on E7, - White King on E1, - White Rook on E4, - ]); - - let king_moves: HashSet = position.all_legal_moves(None).map(Move::from).collect(); - - assert_move_list_does_not_contain!(king_moves, [ply!(E7 - E8)]); -} - -#[test] -fn check_evasions_1() { - let pos = test_position!(Black, [ - Black King on E8, - White King on E1, - White Knight on F6, - ]); - - let generated_moves = pos.all_legal_moves(Some(Color::Black)); - - assert_move_list!( - generated_moves, - [ply!(E8 - D8), ply!(E8 - E7), ply!(E8 - F7), ply!(E8 - F8),] - ); -} - -#[test] -fn check_evasions_double_check() { - let pos = test_position!(Black, [ - Black King on E8, - Black Bishop on F6, - White King on E1, - White Knight on G7, - White Rook on E5, - ]); - - let generated_moves = pos.all_legal_moves(Some(Color::Black)); - - assert_move_list!( - generated_moves, - [ply!(E8 - D8), ply!(E8 - D7), ply!(E8 - F7), ply!(E8 - F8),] - ); -} - -#[test] -fn single_check_with_blocker() { - let pos = test_position!(Black, [ - Black King on E8, - Black Knight on G6, - White King on E1, - White Rook on E5, - ]); - - let generated_moves = pos.all_legal_moves(Some(Color::Black)); - - assert_move_list!( - generated_moves, - [ - // King moves - ply!(E8 - D8), - ply!(E8 - D7), - ply!(E8 - F7), - ply!(E8 - F8), - // Knight moves - ply!(G6 - E7), - ply!(G6 x E5), - ] - ); -} - -#[test] -fn en_passant_check_capture() { - let pos = test_position!(Black, [ - Black King on C5, - Black Pawn on E4, - White Pawn on D4, - ], D3); - - assert!(pos.board().is_in_check()); - - let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); - - assert_move_list_contains!(generated_moves, [ply!(E4 x D3 e.p.)]); -} - -#[test] -fn en_passant_check_block() { - let pos = test_position!(Black, [ - Black King on B5, - Black Pawn on E4, - White Pawn on D4, - White Queen on F1, - ], D3); - - assert!(pos.board().is_in_check()); - - let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); - - assert_move_list_contains!(generated_moves, [ply!(E4 x D3 e.p.)]); -} - -#[test] -fn pinned_pieces_rook_cannot_move_out_of_pin() { - let pos = test_position!(Black, [ - Black King on E8, - Black Rook on E6, - White Queen on E3, - White King on C1, - ]); - - assert!(!pos.board().is_in_check()); - - let rook_moves: HashSet<_> = pos.all_legal_moves(None).collect(); - - assert_move_list_does_not_contain!(rook_moves, [ply!(E6 - D6), ply!(E6 - F6)]); - - assert_move_list_contains!( - rook_moves, - [ply!(E6 x E3), ply!(E6 - E4), ply!(E6 - E5), ply!(E6 - E7)] - ); -} - -#[test] -fn en_passant_discovered_check() { - let pos = test_position!(Black, [ - Black King on A4, - Black Pawn on E4, - White Pawn on D4, - White Queen on H4, - ], D3); - - let generated_moves: HashSet<_> = pos.all_legal_moves(Some(Color::Black)).collect(); - - assert_move_list_does_not_contain!(generated_moves, [ply!(E4 x D3 e.p.)]); -} diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index b377055..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,7 +0,0 @@ -style_edition = "2024" - -imports_layout = "HorizontalVertical" -group_imports = "StdExternalCrate" - -wrap_comments = true -