Compare commits
1 commit
main
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d8653894a |
106 changed files with 3423 additions and 11325 deletions
617
Cargo.lock
generated
617
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
16
Cargo.toml
16
Cargo.toml
|
|
@ -1,16 +0,0 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"bitboard",
|
||||
"board",
|
||||
"chessfriend",
|
||||
"core",
|
||||
"explorer",
|
||||
"moves",
|
||||
"perft",
|
||||
"position",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[profile.release-debug]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
4
Makefile
4
Makefile
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
|
||||
all:
|
||||
cargo build
|
||||
113
README.md
113
README.md
|
|
@ -1,113 +0,0 @@
|
|||
ChessFriend
|
||||
===========
|
||||
|
||||
A chess engine written in Rust.
|
||||
|
||||
The project is divided into crates for major components of the engine. These
|
||||
crates are collected in a Cargo workspace. All crates have the `chessfriend_`
|
||||
naming prefix. The directory structure omits this prefix, and I also frequently
|
||||
do when referring to them.
|
||||
|
||||
|
||||
|
||||
## Engine Crates
|
||||
|
||||
The engine is divided into several crates, each providing vital types and
|
||||
functionality.
|
||||
|
||||
|
||||
|
||||
### `core`
|
||||
|
||||
A collection of types for representing core concepts in a chess game and the
|
||||
engine. Types like `Color` (player or piece color), `Shape` (the shape of a
|
||||
piece: knight, etc), `Piece` (a piece of a particular color and shape), and
|
||||
`Score` (for scoring a board position) live here.
|
||||
|
||||
|
||||
|
||||
### `bitboard`
|
||||
|
||||
Implements an efficient BitBoard type. Bitboards use a 64-bit bit field to mark a
|
||||
square on a board as occupied or free.
|
||||
|
||||
|
||||
|
||||
### `board`
|
||||
|
||||
Implements a `Board` type that represents a moment-in-time board position. FEN
|
||||
parsing and production lives here.
|
||||
|
||||
|
||||
|
||||
### `moves`
|
||||
|
||||
The `Move` type lives here, along with routines for encoding moves in efficient
|
||||
forms, parsing moves from algebraic notation, and recording moves in a game
|
||||
context. Additionally, the move generators for each shape of piece are here.
|
||||
|
||||
|
||||
|
||||
### `position`
|
||||
|
||||
Exports the `Position` type, representing a board position within the context of
|
||||
a game. As such, it also provides a move list, and methods to make and unmake
|
||||
moves.
|
||||
|
||||
|
||||
|
||||
## Support Crates
|
||||
|
||||
These crates are for debugging and testing.
|
||||
|
||||
|
||||
|
||||
### `explorer`
|
||||
|
||||
This crate implements a small command-line application for "exploring" board
|
||||
positions. I meant for this program to be a debugging utility so that I could
|
||||
examine bitboards and other board structures live. It has grown over time to
|
||||
also support more aspects of interacting with the engine. So you can also use it
|
||||
to play a game!
|
||||
|
||||
|
||||
|
||||
### `perft`
|
||||
|
||||
A small Perft utility that executes perft to a given depth from some starting
|
||||
position.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Build the engine in the usual Rusty way.
|
||||
|
||||
```sh
|
||||
$ cargo build
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
Test in the usual Rusty way.
|
||||
|
||||
```sh
|
||||
$ cargo test
|
||||
```
|
||||
|
||||
The engine has a fairly comprehensive unit test suite, as well as a decent pile
|
||||
of integration tests.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Authors
|
||||
|
||||
This engine is built entirely by me, Eryn Wells.
|
||||
|
|
@ -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"
|
||||
|
|
@ -1,596 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::bit_scanner::{LeadingBitScanner, TrailingBitScanner};
|
||||
use crate::direction::IterationDirection;
|
||||
use crate::library;
|
||||
use chessfriend_core::{Color, Direction, File, Rank, Square};
|
||||
use forward_ref::{forward_ref_binop, forward_ref_op_assign, forward_ref_unop};
|
||||
use std::fmt;
|
||||
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not};
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
const SQUARES_NUM: u8 = Square::NUM as u8;
|
||||
|
||||
/// A bitfield representation of a chess board that uses the bits of a 64-bit
|
||||
/// unsigned integer to represent whether a square on the board is occupied.
|
||||
/// Squares are laid out as follows, starting at the bottom left, going row-wise,
|
||||
/// and ending at the top right corner:
|
||||
///
|
||||
/// ```text
|
||||
/// +-------------------------+
|
||||
/// 8 | 56 57 58 59 60 61 62 63 |
|
||||
/// 7 | 48 49 50 51 52 53 54 55 |
|
||||
/// 6 | 40 41 42 43 44 45 46 47 |
|
||||
/// 5 | 32 33 34 35 36 37 38 39 |
|
||||
/// 4 | 24 25 26 27 28 29 30 31 |
|
||||
/// 3 | 16 17 18 19 20 21 22 23 |
|
||||
/// 2 | 8 9 10 11 12 13 14 15 |
|
||||
/// 1 | 0 1 2 3 4 5 6 7 |
|
||||
/// +-------------------------+
|
||||
/// A B C D E F G H
|
||||
/// ```
|
||||
///
|
||||
#[must_use]
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub struct BitBoard(pub(crate) u64);
|
||||
|
||||
macro_rules! moves_getter {
|
||||
($getter_name:ident) => {
|
||||
pub fn $getter_name(sq: Square) -> BitBoard {
|
||||
library::library().$getter_name(sq)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl BitBoard {
|
||||
pub const EMPTY: BitBoard = BitBoard(u64::MIN);
|
||||
pub const FULL: BitBoard = BitBoard(u64::MAX);
|
||||
|
||||
pub const fn new(bits: u64) -> BitBoard {
|
||||
BitBoard(bits)
|
||||
}
|
||||
|
||||
pub const fn rank(rank: Rank) -> BitBoard {
|
||||
library::RANKS[rank.as_index()]
|
||||
}
|
||||
|
||||
pub const fn file(file: File) -> BitBoard {
|
||||
library::FILES[file.as_index()]
|
||||
}
|
||||
|
||||
pub fn ray(sq: Square, dir: Direction) -> BitBoard {
|
||||
library::library().ray(sq, dir)
|
||||
}
|
||||
|
||||
pub fn pawn_attacks(sq: Square, color: Color) -> BitBoard {
|
||||
library::library().pawn_attacks(sq, color)
|
||||
}
|
||||
|
||||
pub fn pawn_pushes(sq: Square, color: Color) -> BitBoard {
|
||||
library::library().pawn_pushes(sq, color)
|
||||
}
|
||||
|
||||
moves_getter!(knight_moves);
|
||||
moves_getter!(bishop_moves);
|
||||
moves_getter!(rook_moves);
|
||||
moves_getter!(queen_moves);
|
||||
moves_getter!(king_moves);
|
||||
|
||||
pub const fn kingside(color: Color) -> &'static BitBoard {
|
||||
&library::KINGSIDES[color as usize]
|
||||
}
|
||||
|
||||
pub const fn queenside(color: Color) -> &'static BitBoard {
|
||||
&library::QUEENSIDES[color as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl BitBoard {
|
||||
/// Converts this [`BitBoard`] to an unsigned 64-bit integer.
|
||||
#[must_use]
|
||||
pub const fn as_bits(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns `true` if this [`BitBoard`] has no bits set. This is the opposite
|
||||
/// of [`BitBoard::is_populated`].
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_bitboard::BitBoard;
|
||||
/// assert!(BitBoard::EMPTY.is_empty());
|
||||
/// assert!(!BitBoard::full().is_empty());
|
||||
/// assert!(!BitBoard::new(0b1000).is_empty());
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`BitBoard`] has at least one bit set. This is the
|
||||
/// opposite of [`BitBoard::is_empty`].
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_bitboard::BitBoard;
|
||||
/// assert!(!BitBoard::EMPTY.is_populated());
|
||||
/// assert!(BitBoard::full().is_populated());
|
||||
/// assert!(BitBoard::new(0b1).is_populated());
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn is_populated(&self) -> bool {
|
||||
self.0 != 0
|
||||
}
|
||||
|
||||
/// Returns `true` if this [`BitBoard`] has the bit corresponding to `square` set.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_bitboard::BitBoard;
|
||||
/// use chessfriend_core::Square;
|
||||
///
|
||||
/// let square = Square::E4;
|
||||
/// let mut bitboard = BitBoard::new(0b1001100);
|
||||
///
|
||||
/// assert!(bitboard.contains(Square::C1));
|
||||
/// assert!(!bitboard.contains(Square::B1));
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn contains(self, square: Square) -> bool {
|
||||
let square_bitboard: BitBoard = square.into();
|
||||
!(self & square_bitboard).is_empty()
|
||||
}
|
||||
|
||||
/// Counts the number of set squares (1 bits) in this [`BitBoard`].
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_bitboard::BitBoard;
|
||||
/// assert_eq!(BitBoard::EMPTY.population_count(), 0);
|
||||
/// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6);
|
||||
/// assert_eq!(BitBoard::FULL.population_count(), 64);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn population_count(&self) -> u32 {
|
||||
self.0.count_ones()
|
||||
}
|
||||
|
||||
/// Set a square in this [`BitBoard`] by toggling the corresponding bit to 1.
|
||||
/// This always succeeds, even if the bit was already set.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_bitboard::BitBoard;
|
||||
/// use chessfriend_core::Square;
|
||||
///
|
||||
/// let mut bitboard = BitBoard::new(0b1001100);
|
||||
/// bitboard.set(Square::E4);
|
||||
/// assert!(bitboard.contains(Square::E4));
|
||||
/// ```
|
||||
pub fn set(&mut self, square: Square) {
|
||||
let square_bitboard: BitBoard = square.into();
|
||||
self.0 |= square_bitboard.0;
|
||||
}
|
||||
|
||||
/// Clear a square (set it to 0) in this [`BitBoard`]. This always succeeds
|
||||
/// even if the bit is not set.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_bitboard::BitBoard;
|
||||
/// use chessfriend_core::Square;
|
||||
///
|
||||
/// let mut bitboard = BitBoard::new(0b1001100);
|
||||
/// bitboard.clear(Square::C1);
|
||||
/// assert!(!bitboard.contains(Square::C1));
|
||||
/// ```
|
||||
pub fn clear(&mut self, square: Square) {
|
||||
let square_bitboard: BitBoard = square.into();
|
||||
self.0 &= !square_bitboard.0;
|
||||
}
|
||||
|
||||
/// Returns `true` if this [`BitBoard`] represents a single square.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_bitboard::BitBoard;
|
||||
/// assert!(!BitBoard::EMPTY.is_single_square(), "Empty bitboards represent no squares");
|
||||
/// assert!(!BitBoard::FULL.is_single_square(), "Full bitboards represent all the squares");
|
||||
/// assert!(!BitBoard::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares");
|
||||
/// assert!(BitBoard::new(0b10000000000000).is_single_square());
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn is_single_square(&self) -> bool {
|
||||
self.0.is_power_of_two()
|
||||
}
|
||||
|
||||
/// Return an Iterator over the occupied squares.
|
||||
#[must_use]
|
||||
pub fn occupied_squares(
|
||||
&self,
|
||||
direction: &IterationDirection,
|
||||
) -> Box<dyn Iterator<Item = Square>> {
|
||||
match direction {
|
||||
IterationDirection::Leading => Box::new(self.occupied_squares_leading()),
|
||||
IterationDirection::Trailing => Box::new(self.occupied_squares_trailing()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate through the occupied squares in a direction specified by a
|
||||
/// compass direction. This method is mose useful for bitboards of slider
|
||||
/// rays so that iteration proceeds in order along the ray's direction.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_bitboard::BitBoard;
|
||||
/// use chessfriend_core::{Direction, Square};
|
||||
///
|
||||
/// let ray = BitBoard::ray(Square::E4, Direction::North);
|
||||
/// assert_eq!(
|
||||
/// ray.occupied_squares_direction(Direction::North).collect::<Vec<Square>>(),
|
||||
/// vec![Square::E5, Square::E6, Square::E7, Square::E8]
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
#[must_use]
|
||||
pub fn occupied_squares_direction(
|
||||
&self,
|
||||
direction: Direction,
|
||||
) -> Box<dyn Iterator<Item = Square>> {
|
||||
match direction {
|
||||
Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => {
|
||||
Box::new(self.occupied_squares_trailing())
|
||||
}
|
||||
Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => {
|
||||
Box::new(self.occupied_squares_leading())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn occupied_squares_leading(&self) -> LeadingBitScanner {
|
||||
LeadingBitScanner::new(self.0)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn occupied_squares_trailing(&self) -> TrailingBitScanner {
|
||||
TrailingBitScanner::new(self.0)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn first_occupied_square_direction(&self, direction: Direction) -> Option<Square> {
|
||||
match direction {
|
||||
Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => {
|
||||
self.first_occupied_square_trailing()
|
||||
}
|
||||
Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => {
|
||||
self.first_occupied_square_leading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the first occupied square in the given direction.
|
||||
///
|
||||
/// ## To-Do
|
||||
///
|
||||
/// - Take `direction` by value instead of reference
|
||||
///
|
||||
#[must_use]
|
||||
pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option<Square> {
|
||||
match direction {
|
||||
IterationDirection::Leading => self.first_occupied_square_leading(),
|
||||
IterationDirection::Trailing => self.first_occupied_square_trailing(),
|
||||
}
|
||||
}
|
||||
|
||||
/// If the board is not empty, returns the first occupied square on the
|
||||
/// board, starting at the leading (most-significant) end of the board. If
|
||||
/// the board is empty, returns `None`.
|
||||
#[must_use]
|
||||
pub fn first_occupied_square_leading(self) -> Option<Square> {
|
||||
let leading_zeros = self.leading_zeros();
|
||||
if leading_zeros < SQUARES_NUM {
|
||||
unsafe {
|
||||
Some(Square::from_index_unchecked(
|
||||
SQUARES_NUM - leading_zeros - 1,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// If the board is not empty, returns the first occupied square on the
|
||||
/// board, starting at the trailing (least-significant) end of the board.
|
||||
/// If the board is empty, returns `None`.
|
||||
#[must_use]
|
||||
pub fn first_occupied_square_trailing(self) -> Option<Square> {
|
||||
let trailing_zeros = self.trailing_zeros();
|
||||
|
||||
if trailing_zeros < SQUARES_NUM {
|
||||
unsafe { Some(Square::from_index_unchecked(trailing_zeros)) }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BitBoard {
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn leading_zeros(self) -> u8 {
|
||||
self.0.leading_zeros() as u8
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn trailing_zeros(self) -> u8 {
|
||||
self.0.trailing_zeros() as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BitBoard {
|
||||
fn default() -> Self {
|
||||
BitBoard::EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BitBoard> for u64 {
|
||||
fn from(value: BitBoard) -> Self {
|
||||
value.as_bits()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for BitBoard {
|
||||
fn from(value: File) -> Self {
|
||||
library::FILES[value.as_index()]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Square>> for BitBoard {
|
||||
fn from(value: Option<Square>) -> Self {
|
||||
value.map_or(BitBoard::EMPTY, Into::<BitBoard>::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rank> for BitBoard {
|
||||
fn from(value: Rank) -> Self {
|
||||
library::FILES[value.as_index()]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Square> for BitBoard {
|
||||
fn from(value: Square) -> Self {
|
||||
BitBoard(1u64 << value as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Square> for BitBoard {
|
||||
fn from_iter<T: IntoIterator<Item = Square>>(iter: T) -> Self {
|
||||
iter.into_iter().fold(BitBoard::EMPTY, |mut acc, sq| {
|
||||
acc.set(sq);
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TryFromBitBoardError {
|
||||
NotSingleSquare,
|
||||
}
|
||||
|
||||
impl TryFrom<BitBoard> for Square {
|
||||
type Error = TryFromBitBoardError;
|
||||
|
||||
fn try_from(value: BitBoard) -> Result<Self, Self::Error> {
|
||||
if !value.is_single_square() {
|
||||
return Err(TryFromBitBoardError::NotSingleSquare);
|
||||
}
|
||||
|
||||
unsafe { Ok(Square::from_index_unchecked(value.trailing_zeros())) }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Binary for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Delegate to u64's implementation of Binary.
|
||||
fmt::Binary::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::LowerHex for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Delegate to u64's implementation of LowerHex.
|
||||
fmt::LowerHex::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::UpperHex for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Delegate to u64's implementation of UpperHex.
|
||||
fmt::UpperHex::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let binary_ranks = format!("{:064b}", self.0)
|
||||
.chars()
|
||||
.rev()
|
||||
.map(String::from)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let mut ranks_written = 0;
|
||||
for rank in binary_ranks.chunks(8).rev() {
|
||||
write!(f, "{}", rank.join(" "))?;
|
||||
|
||||
ranks_written += 1;
|
||||
if ranks_written < 8 {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(f, "BitBoard({:064b})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! infix_op {
|
||||
($trait_type:ident, $func_name:ident, $left_type:ty, $right_type:ty) => {
|
||||
impl std::ops::$trait_type<$right_type> for $left_type {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn $func_name(self, rhs: $right_type) -> Self::Output {
|
||||
BitBoard(std::ops::$trait_type::$func_name(self.0, rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
forward_ref_binop!(impl $trait_type, $func_name for $left_type, $right_type);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assign_op {
|
||||
($trait_type:ident, $func_name:ident, $type:ty) => {
|
||||
impl $trait_type for $type {
|
||||
#[inline]
|
||||
fn $func_name(&mut self, rhs: $type) {
|
||||
$trait_type::$func_name(&mut self.0, rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
forward_ref_op_assign!(impl $trait_type, $func_name for $type, $type);
|
||||
};
|
||||
}
|
||||
|
||||
infix_op!(BitAnd, bitand, BitBoard, BitBoard);
|
||||
infix_op!(BitOr, bitor, BitBoard, BitBoard);
|
||||
infix_op!(BitXor, bitxor, BitBoard, BitBoard);
|
||||
|
||||
assign_op!(BitAndAssign, bitand_assign, BitBoard);
|
||||
assign_op!(BitOrAssign, bitor_assign, BitBoard);
|
||||
assign_op!(BitXorAssign, bitxor_assign, BitBoard);
|
||||
|
||||
impl Not for BitBoard {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn not(self) -> Self::Output {
|
||||
BitBoard(!self.0)
|
||||
}
|
||||
}
|
||||
|
||||
forward_ref_unop!(impl Not, not for BitBoard);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bitboard;
|
||||
use chessfriend_core::Square;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn display_and_debug() {
|
||||
let bb = BitBoard::file(File::A)
|
||||
| BitBoard::file(File::D)
|
||||
| BitBoard::rank(Rank::FIVE)
|
||||
| BitBoard::rank(Rank::EIGHT);
|
||||
println!("{}", &bb);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::unreadable_literal)]
|
||||
fn rank() {
|
||||
assert_eq!(BitBoard::rank(Rank::ONE).0, 0xFF, "Rank 1");
|
||||
assert_eq!(BitBoard::rank(Rank::TWO).0, 0xFF00, "Rank 2");
|
||||
assert_eq!(BitBoard::rank(Rank::THREE).0, 0xFF0000, "Rank 3");
|
||||
assert_eq!(BitBoard::rank(Rank::FOUR).0, 0xFF000000, "Rank 4");
|
||||
assert_eq!(BitBoard::rank(Rank::FIVE).0, 0xFF00000000, "Rank 5");
|
||||
assert_eq!(BitBoard::rank(Rank::SIX).0, 0xFF0000000000, "Rank 6");
|
||||
assert_eq!(BitBoard::rank(Rank::SEVEN).0, 0xFF000000000000, "Rank 7");
|
||||
assert_eq!(BitBoard::rank(Rank::EIGHT).0, 0xFF00000000000000, "Rank 8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_rank_occupancy() {
|
||||
#[allow(clippy::unreadable_literal)]
|
||||
let bb = BitBoard(0b01010100);
|
||||
|
||||
let expected_squares = [Square::G1, Square::E1, Square::C1];
|
||||
bb.occupied_squares(&IterationDirection::Leading)
|
||||
.zip(expected_squares)
|
||||
.for_each(|(a, b)| assert_eq!(a, b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occupancy_spot_check() {
|
||||
#[allow(clippy::unreadable_literal)]
|
||||
let bb =
|
||||
BitBoard(0b10000000_00000000_00100000_00000100_00000000_00000000_00010000_00001000);
|
||||
|
||||
let expected_squares = [Square::H8, Square::F6, Square::C5, Square::E2, Square::D1];
|
||||
|
||||
bb.occupied_squares(&IterationDirection::Leading)
|
||||
.zip(expected_squares)
|
||||
.for_each(|(a, b)| assert_eq!(a, b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xor() {
|
||||
let a = bitboard![C5 G7];
|
||||
let b = bitboard![B5 G7 H3];
|
||||
|
||||
assert_eq!(a ^ b, bitboard![B5 C5 H3]);
|
||||
assert_eq!(a ^ BitBoard::EMPTY, a);
|
||||
assert_eq!(BitBoard::EMPTY ^ BitBoard::EMPTY, BitBoard::EMPTY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitand_assign() {
|
||||
let mut a = bitboard![C5 G7];
|
||||
let b = bitboard![B5 G7 H3];
|
||||
|
||||
a &= b;
|
||||
|
||||
assert_eq!(a, bitboard![G7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitor_assign() {
|
||||
let mut a = bitboard![C5 G7];
|
||||
let b = bitboard![B5 G7 H3];
|
||||
|
||||
a |= b;
|
||||
|
||||
assert_eq!(a, bitboard![B5 C5 G7 H3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_square() {
|
||||
assert_eq!(BitBoard::from(Square::A1), BitBoard(0b1));
|
||||
assert_eq!(BitBoard::from(Square::H8), BitBoard(1 << 63));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_occupied_squares() {
|
||||
let bb = bitboard![A8 E1];
|
||||
assert_eq!(bb.first_occupied_square_leading(), Some(Square::A8));
|
||||
assert_eq!(bb.first_occupied_square_trailing(), Some(Square::E1));
|
||||
|
||||
let bb = bitboard![D6 E7 F8];
|
||||
assert_eq!(bb.first_occupied_square_trailing(), Some(Square::D6));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#[derive(Default)]
|
||||
pub enum IterationDirection {
|
||||
#[default]
|
||||
Leading,
|
||||
Trailing,
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
pub mod bit_scanner;
|
||||
|
||||
mod bitboard;
|
||||
mod direction;
|
||||
mod library;
|
||||
mod shifts;
|
||||
|
||||
pub use bitboard::BitBoard;
|
||||
pub use direction::IterationDirection;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bitboard {
|
||||
($($sq:ident)* $(,)?) => {
|
||||
{
|
||||
let mut bitboard = $crate::BitBoard::EMPTY;
|
||||
$(bitboard.set(chessfriend_core::Square::$sq);)*
|
||||
bitboard
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
//! # The Bitboard Library
|
||||
//!
|
||||
//! This module implements a collection of commonly used bitboards that can be
|
||||
//! looked up efficiently.
|
||||
//!
|
||||
//! The `library()` method returns a static instance of a `Library`, which
|
||||
//! provides getters for all available bitboards.
|
||||
|
||||
use crate::BitBoard;
|
||||
use chessfriend_core::{Color, Direction, File, Rank, Square};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[allow(clippy::identity_op)]
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub(crate) const RANKS: [BitBoard; Rank::NUM] = [
|
||||
BitBoard(0xFF << (0 * 8)),
|
||||
BitBoard(0xFF << (1 * 8)),
|
||||
BitBoard(0xFF << (2 * 8)),
|
||||
BitBoard(0xFF << (3 * 8)),
|
||||
BitBoard(0xFF << (4 * 8)),
|
||||
BitBoard(0xFF << (5 * 8)),
|
||||
BitBoard(0xFF << (6 * 8)),
|
||||
BitBoard(0xFF << (7 * 8)),
|
||||
];
|
||||
|
||||
#[allow(clippy::identity_op)]
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub(crate) const FILES: [BitBoard; File::NUM] = [
|
||||
BitBoard(0x0101_0101_0101_0101 << 0),
|
||||
BitBoard(0x0101_0101_0101_0101 << 1),
|
||||
BitBoard(0x0101_0101_0101_0101 << 2),
|
||||
BitBoard(0x0101_0101_0101_0101 << 3),
|
||||
BitBoard(0x0101_0101_0101_0101 << 4),
|
||||
BitBoard(0x0101_0101_0101_0101 << 5),
|
||||
BitBoard(0x0101_0101_0101_0101 << 6),
|
||||
BitBoard(0x0101_0101_0101_0101 << 7),
|
||||
];
|
||||
|
||||
/// Bitboards representing the kingside of the board, per color.
|
||||
pub(crate) const KINGSIDES: [BitBoard; Color::NUM] = [
|
||||
BitBoard(0xF0F0_F0F0_F0F0_F0F0),
|
||||
BitBoard(0x0F0F_0F0F_0F0F_0F0F),
|
||||
];
|
||||
|
||||
/// Bitboards representing the queenside of the board, per color.
|
||||
pub(crate) const QUEENSIDES: [BitBoard; Color::NUM] = [
|
||||
BitBoard(0x0F0F_0F0F_0F0F_0F0F),
|
||||
BitBoard(0xF0F0_F0F0_F0F0_F0F0),
|
||||
];
|
||||
|
||||
/// A bitboard representing the light squares.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const LIGHT_SQUARES: BitBoard =
|
||||
BitBoard(0x5555 | 0x5555 << 16 | 0x5555 << 32 | 0x5555 << 48);
|
||||
|
||||
/// A bitboad representing the dark squares
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const DARK_SQUARES: BitBoard = BitBoard(!LIGHT_SQUARES.0);
|
||||
|
||||
pub(super) fn library() -> &'static MoveLibrary {
|
||||
static MOVE_LIBRARY: MoveLibraryWrapper = MoveLibraryWrapper::new();
|
||||
MOVE_LIBRARY.library()
|
||||
}
|
||||
|
||||
macro_rules! library_getter {
|
||||
($name:ident) => {
|
||||
pub(super) const fn $name(&self, sq: Square) -> BitBoard {
|
||||
self.$name[sq as usize]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct MoveLibraryWrapper {
|
||||
library: OnceLock<MoveLibrary>,
|
||||
}
|
||||
|
||||
impl MoveLibraryWrapper {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
library: OnceLock::<MoveLibrary>::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn library(&self) -> &MoveLibrary {
|
||||
self.library.get_or_init(|| {
|
||||
let mut library = MoveLibrary::new();
|
||||
library.init();
|
||||
library
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct MoveLibrary {
|
||||
// Rays
|
||||
rays: [[BitBoard; Direction::NUM]; Square::NUM],
|
||||
|
||||
// Piecewise move tables
|
||||
pawn_attacks: [[BitBoard; Square::NUM]; Color::NUM],
|
||||
pawn_pushes: [[BitBoard; Square::NUM]; Color::NUM],
|
||||
knight_moves: [BitBoard; Square::NUM],
|
||||
bishop_moves: [BitBoard; Square::NUM],
|
||||
rook_moves: [BitBoard; Square::NUM],
|
||||
queen_moves: [BitBoard; Square::NUM],
|
||||
king_moves: [BitBoard; Square::NUM],
|
||||
}
|
||||
|
||||
impl MoveLibrary {
|
||||
const fn new() -> MoveLibrary {
|
||||
MoveLibrary {
|
||||
rays: [[BitBoard::EMPTY; Direction::NUM]; Square::NUM],
|
||||
pawn_attacks: [[BitBoard::EMPTY; Square::NUM]; Color::NUM],
|
||||
pawn_pushes: [[BitBoard::EMPTY; Square::NUM]; Color::NUM],
|
||||
knight_moves: [BitBoard::EMPTY; Square::NUM],
|
||||
bishop_moves: [BitBoard::EMPTY; Square::NUM],
|
||||
rook_moves: [BitBoard::EMPTY; Square::NUM],
|
||||
queen_moves: [BitBoard::EMPTY; Square::NUM],
|
||||
king_moves: [BitBoard::EMPTY; Square::NUM],
|
||||
}
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
for sq in Square::ALL {
|
||||
self.init_pawn_moves(sq);
|
||||
self.init_orthogonal_rays(sq);
|
||||
self.init_diagonal_rays(sq);
|
||||
self.init_knight_moves(sq as usize);
|
||||
self.init_bishop_moves(sq);
|
||||
self.init_rook_moves(sq);
|
||||
self.init_queen_moves(sq);
|
||||
self.init_king_moves(sq as usize);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_orthogonal_rays(&mut self, sq: Square) {
|
||||
let sq_bb: BitBoard = sq.into();
|
||||
let rays = &mut self.rays[sq as usize];
|
||||
rays[Direction::North as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_north_one);
|
||||
rays[Direction::South as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_south_one);
|
||||
rays[Direction::East as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_east_one);
|
||||
rays[Direction::West as usize] = Self::_generate_ray(sq_bb, BitBoard::shift_west_one);
|
||||
}
|
||||
|
||||
fn init_diagonal_rays(&mut self, sq: Square) {
|
||||
let sq_bb: BitBoard = sq.into();
|
||||
let rays = &mut self.rays[sq as usize];
|
||||
rays[Direction::NorthEast as usize] =
|
||||
Self::_generate_ray(sq_bb, BitBoard::shift_north_east_one);
|
||||
rays[Direction::NorthWest as usize] =
|
||||
Self::_generate_ray(sq_bb, BitBoard::shift_north_west_one);
|
||||
rays[Direction::SouthWest as usize] =
|
||||
Self::_generate_ray(sq_bb, BitBoard::shift_south_west_one);
|
||||
rays[Direction::SouthEast as usize] =
|
||||
Self::_generate_ray(sq_bb, BitBoard::shift_south_east_one);
|
||||
}
|
||||
|
||||
fn init_king_moves(&mut self, idx: usize) {
|
||||
let king = BitBoard::new(1 << idx);
|
||||
let mut attacks = king.shift_east_one() | king.shift_west_one();
|
||||
|
||||
let king = king | attacks;
|
||||
attacks |= king.shift_north_one() | king.shift_south_one();
|
||||
|
||||
self.king_moves[idx] = attacks;
|
||||
}
|
||||
|
||||
/// Calculate bitboards representing knight moves from each square on the
|
||||
/// board. The algorithm is described on the [Chess Programming Wiki][cpw].
|
||||
///
|
||||
/// [cpw]: https://www.chessprogramming.org/Knight_Pattern
|
||||
fn init_knight_moves(&mut self, idx: usize) {
|
||||
let knight = BitBoard::new(1 << idx);
|
||||
|
||||
let east = knight.shift_east_one();
|
||||
let west = knight.shift_west_one();
|
||||
|
||||
let mut attacks = (east | west).shift_north(2);
|
||||
attacks |= (east | west).shift_south(2);
|
||||
|
||||
let east = east.shift_east_one();
|
||||
let west = west.shift_west_one();
|
||||
|
||||
attacks |= (east | west).shift_north_one();
|
||||
attacks |= (east | west).shift_south_one();
|
||||
|
||||
self.knight_moves[idx] = attacks;
|
||||
}
|
||||
|
||||
fn init_bishop_moves(&mut self, sq: Square) {
|
||||
let rays = self.rays[sq as usize];
|
||||
self.bishop_moves[sq as usize] = rays[Direction::NorthWest as usize]
|
||||
| rays[Direction::NorthEast as usize]
|
||||
| rays[Direction::SouthEast as usize]
|
||||
| rays[Direction::SouthWest as usize];
|
||||
}
|
||||
|
||||
fn init_rook_moves(&mut self, sq: Square) {
|
||||
let rays = self.rays[sq as usize];
|
||||
self.rook_moves[sq as usize] = rays[Direction::North as usize]
|
||||
| rays[Direction::East as usize]
|
||||
| rays[Direction::South as usize]
|
||||
| rays[Direction::West as usize];
|
||||
}
|
||||
|
||||
fn init_queen_moves(&mut self, sq: Square) {
|
||||
let rook_moves = self.rook_moves[sq as usize];
|
||||
let bishop_moves = self.bishop_moves[sq as usize];
|
||||
self.queen_moves[sq as usize] = rook_moves | bishop_moves;
|
||||
}
|
||||
|
||||
fn init_pawn_moves(&mut self, sq: Square) {
|
||||
let bitboard: BitBoard = sq.into();
|
||||
|
||||
self.pawn_attacks[Color::White as usize][sq as usize] =
|
||||
bitboard.shift_north_west_one() | bitboard.shift_north_east_one();
|
||||
self.pawn_attacks[Color::Black as usize][sq as usize] =
|
||||
bitboard.shift_south_west_one() | bitboard.shift_south_east_one();
|
||||
|
||||
self.pawn_pushes[Color::White as usize][sq as usize] = {
|
||||
let mut push = bitboard.shift_north_one();
|
||||
if !(bitboard & RANKS[1]).is_empty() {
|
||||
push |= push.shift_north_one();
|
||||
}
|
||||
|
||||
push
|
||||
};
|
||||
|
||||
self.pawn_pushes[Color::Black as usize][sq as usize] = {
|
||||
let mut push = bitboard.shift_south_one();
|
||||
if !(bitboard & RANKS[6]).is_empty() {
|
||||
push |= push.shift_south_one();
|
||||
}
|
||||
|
||||
push
|
||||
};
|
||||
}
|
||||
|
||||
fn _generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard {
|
||||
let mut ray = BitBoard::EMPTY;
|
||||
|
||||
let mut iter = shift(&sq);
|
||||
while !iter.is_empty() {
|
||||
ray |= iter;
|
||||
iter = shift(&iter);
|
||||
}
|
||||
|
||||
ray
|
||||
}
|
||||
|
||||
pub(super) const fn ray(&self, sq: Square, dir: Direction) -> BitBoard {
|
||||
self.rays[sq as usize][dir as usize]
|
||||
}
|
||||
|
||||
pub(super) const fn pawn_pushes(&self, sq: Square, color: Color) -> BitBoard {
|
||||
self.pawn_pushes[color as usize][sq as usize]
|
||||
}
|
||||
|
||||
pub(super) const fn pawn_attacks(&self, sq: Square, color: Color) -> BitBoard {
|
||||
self.pawn_attacks[color as usize][sq as usize]
|
||||
}
|
||||
|
||||
library_getter!(knight_moves);
|
||||
library_getter!(bishop_moves);
|
||||
library_getter!(rook_moves);
|
||||
library_getter!(queen_moves);
|
||||
library_getter!(king_moves);
|
||||
}
|
||||
0
position/Cargo.lock → board/Cargo.lock
generated
0
position/Cargo.lock → board/Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use chessfriend_core::Square;
|
||||
|
||||
macro_rules! bit_scanner {
|
||||
($name:ident) => {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct $name {
|
||||
pub(crate) struct $name {
|
||||
bits: u64,
|
||||
shift: usize,
|
||||
}
|
||||
|
|
@ -21,16 +18,8 @@ macro_rules! bit_scanner {
|
|||
bit_scanner!(LeadingBitScanner);
|
||||
bit_scanner!(TrailingBitScanner);
|
||||
|
||||
fn index_to_square(index: usize) -> Square {
|
||||
debug_assert!(index < Square::NUM);
|
||||
unsafe {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
Square::from_index_unchecked(index as u8)
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for LeadingBitScanner {
|
||||
type Item = Square;
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let u64bits = u64::BITS as usize;
|
||||
|
|
@ -51,12 +40,12 @@ impl Iterator for LeadingBitScanner {
|
|||
// Shift 1 additional place to account for the 1 that `leading_zeros` found.
|
||||
self.shift += leading_zeros + 1;
|
||||
|
||||
Some(index_to_square(position))
|
||||
Some(position)
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for TrailingBitScanner {
|
||||
type Item = Square;
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let u64bits = u64::BITS as usize;
|
||||
|
|
@ -77,7 +66,7 @@ impl Iterator for TrailingBitScanner {
|
|||
// Shift 1 additional place to account for the 1 that `leading_zeros` found.
|
||||
self.shift += trailing_zeros + 1;
|
||||
|
||||
Some(index_to_square(position))
|
||||
Some(position)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,17 +83,17 @@ mod tests {
|
|||
#[test]
|
||||
fn leading_one() {
|
||||
let mut scanner = LeadingBitScanner::new(1);
|
||||
assert_eq!(scanner.next(), Some(Square::A1));
|
||||
assert_eq!(scanner.next(), Some(0));
|
||||
assert_eq!(scanner.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_complex() {
|
||||
let mut scanner = LeadingBitScanner::new(0b_1100_0101);
|
||||
assert_eq!(scanner.next(), Some(Square::H1));
|
||||
assert_eq!(scanner.next(), Some(Square::G1));
|
||||
assert_eq!(scanner.next(), Some(Square::C1));
|
||||
assert_eq!(scanner.next(), Some(Square::A1));
|
||||
let mut scanner = LeadingBitScanner::new(0b11000101);
|
||||
assert_eq!(scanner.next(), Some(7));
|
||||
assert_eq!(scanner.next(), Some(6));
|
||||
assert_eq!(scanner.next(), Some(2));
|
||||
assert_eq!(scanner.next(), Some(0));
|
||||
assert_eq!(scanner.next(), None);
|
||||
}
|
||||
|
||||
|
|
@ -117,17 +106,17 @@ mod tests {
|
|||
#[test]
|
||||
fn trailing_one() {
|
||||
let mut scanner = TrailingBitScanner::new(1);
|
||||
assert_eq!(scanner.next(), Some(Square::A1));
|
||||
assert_eq!(scanner.next(), Some(0));
|
||||
assert_eq!(scanner.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_complex() {
|
||||
let mut scanner = TrailingBitScanner::new(0b_1100_0101);
|
||||
assert_eq!(scanner.next(), Some(Square::A1));
|
||||
assert_eq!(scanner.next(), Some(Square::C1));
|
||||
assert_eq!(scanner.next(), Some(Square::G1));
|
||||
assert_eq!(scanner.next(), Some(Square::H1));
|
||||
let mut scanner = TrailingBitScanner::new(0b11000101);
|
||||
assert_eq!(scanner.next(), Some(0));
|
||||
assert_eq!(scanner.next(), Some(2));
|
||||
assert_eq!(scanner.next(), Some(6));
|
||||
assert_eq!(scanner.next(), Some(7));
|
||||
assert_eq!(scanner.next(), None);
|
||||
}
|
||||
}
|
||||
262
board/src/bitboard/bitboard.rs
Normal file
262
board/src/bitboard/bitboard.rs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::library::{library, FILES, RANKS};
|
||||
use super::LeadingBitScanner;
|
||||
use crate::{square::Direction, Square};
|
||||
use std::fmt;
|
||||
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not};
|
||||
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub(crate) struct BitBoard(pub(super) u64);
|
||||
|
||||
macro_rules! moves_getter {
|
||||
($getter_name:ident) => {
|
||||
pub fn $getter_name(sq: Square) -> BitBoard {
|
||||
library().$getter_name(sq)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl BitBoard {
|
||||
pub const fn empty() -> BitBoard {
|
||||
BitBoard(0)
|
||||
}
|
||||
|
||||
pub fn new(bits: u64) -> BitBoard {
|
||||
BitBoard(bits)
|
||||
}
|
||||
|
||||
pub fn rank(rank: usize) -> BitBoard {
|
||||
assert!(rank < 8);
|
||||
RANKS[rank]
|
||||
}
|
||||
|
||||
pub fn file(file: usize) -> BitBoard {
|
||||
assert!(file < 8);
|
||||
FILES[file]
|
||||
}
|
||||
|
||||
pub fn ray(sq: Square, dir: Direction) -> BitBoard {
|
||||
library().ray(sq, dir)
|
||||
}
|
||||
|
||||
moves_getter!(knight_moves);
|
||||
moves_getter!(bishop_moves);
|
||||
moves_getter!(rook_moves);
|
||||
moves_getter!(queen_moves);
|
||||
moves_getter!(king_moves);
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
|
||||
pub fn has_piece_at(self, sq: Square) -> bool {
|
||||
!(self & sq.into()).is_empty()
|
||||
}
|
||||
|
||||
pub fn place_piece_at(&mut self, sq: Square) {
|
||||
let sq_bb: BitBoard = sq.into();
|
||||
*self |= sq_bb
|
||||
}
|
||||
|
||||
fn remove_piece_at(&mut self, sq: Square) {
|
||||
let sq_bb: BitBoard = sq.into();
|
||||
*self &= !sq_bb
|
||||
}
|
||||
}
|
||||
|
||||
impl BitBoard {
|
||||
/// Return an Iterator over the occupied squares, starting from the leading
|
||||
/// (most-significant bit) end of the field.
|
||||
pub(crate) fn occupied_squares(&self) -> impl Iterator<Item = Square> {
|
||||
LeadingBitScanner::new(self.0).map(Square::from_index)
|
||||
}
|
||||
|
||||
/// Return an Iterator over the occupied squares, starting from the trailing
|
||||
/// (least-significant bit) end of the field.
|
||||
pub(crate) fn occupied_squares_trailing(&self) -> impl Iterator<Item = Square> {
|
||||
LeadingBitScanner::new(self.0).map(Square::from_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Square> for BitBoard {
|
||||
fn from(value: Square) -> Self {
|
||||
BitBoard(1 << value as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Binary for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Delegate to u64's implementation of Binary.
|
||||
fmt::Binary::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::LowerHex for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Delegate to u64's implementation of LowerHex.
|
||||
fmt::LowerHex::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::UpperHex for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Delegate to u64's implementation of UpperHex.
|
||||
fmt::UpperHex::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let binary_ranks = format!("{:064b}", self.0)
|
||||
.chars()
|
||||
.rev()
|
||||
.map(|c| String::from(c))
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let mut ranks_written = 0;
|
||||
for rank in binary_ranks.chunks(8).rev() {
|
||||
let joined_rank = rank.join(" ");
|
||||
write!(f, "{}", joined_rank)?;
|
||||
|
||||
ranks_written += 1;
|
||||
if ranks_written < 8 {
|
||||
write!(f, "\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for BitBoard {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
f.debug_tuple("BitBoard")
|
||||
.field(&format_args!("{:064b}", self.0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! infix_op {
|
||||
($trait_type:ident, $func_name:ident, $left_type:ty, $right_type:ty) => {
|
||||
impl $trait_type<$right_type> for $left_type {
|
||||
type Output = BitBoard;
|
||||
|
||||
#[inline]
|
||||
fn $func_name(self, rhs: $right_type) -> Self::Output {
|
||||
BitBoard($trait_type::$func_name(self.0, rhs.0))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assign_op {
|
||||
($trait_type:ident, $func_name:ident, $left_type:ty) => {
|
||||
impl $trait_type for $left_type {
|
||||
#[inline]
|
||||
fn $func_name(&mut self, rhs: Self) {
|
||||
$trait_type::$func_name(&mut self.0, rhs.0)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
infix_op!(BitAnd, bitand, BitBoard, BitBoard);
|
||||
|
||||
assign_op!(BitAndAssign, bitand_assign, BitBoard);
|
||||
assign_op!(BitOrAssign, bitor_assign, BitBoard);
|
||||
|
||||
infix_op!(BitOr, bitor, BitBoard, BitBoard);
|
||||
|
||||
impl Not for BitBoard {
|
||||
type Output = BitBoard;
|
||||
|
||||
#[inline]
|
||||
fn not(self) -> Self::Output {
|
||||
BitBoard(!self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Not for &BitBoard {
|
||||
type Output = BitBoard;
|
||||
|
||||
#[inline]
|
||||
fn not(self) -> Self::Output {
|
||||
BitBoard(!self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Square;
|
||||
|
||||
#[test]
|
||||
fn display_and_debug() {
|
||||
let bb = BitBoard::file(0) | BitBoard::file(3) | BitBoard::rank(7) | BitBoard::rank(4);
|
||||
println!("{}", &bb);
|
||||
println!("{:?}", &bb);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rank() {
|
||||
assert_eq!(BitBoard::rank(0).0, 0xFF, "Rank 1");
|
||||
assert_eq!(BitBoard::rank(1).0, 0xFF00, "Rank 2");
|
||||
assert_eq!(BitBoard::rank(2).0, 0xFF0000, "Rank 3");
|
||||
assert_eq!(BitBoard::rank(3).0, 0xFF000000, "Rank 4");
|
||||
assert_eq!(BitBoard::rank(4).0, 0xFF00000000, "Rank 5");
|
||||
assert_eq!(BitBoard::rank(5).0, 0xFF0000000000, "Rank 6");
|
||||
assert_eq!(BitBoard::rank(6).0, 0xFF000000000000, "Rank 7");
|
||||
assert_eq!(BitBoard::rank(7).0, 0xFF00000000000000, "Rank 8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_empty() {
|
||||
assert!(BitBoard(0).is_empty());
|
||||
assert!(!BitBoard(0xFF).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_piece_at() {
|
||||
let bb = BitBoard(0b1001100);
|
||||
assert!(bb.has_piece_at(Square::C1));
|
||||
assert!(!bb.has_piece_at(Square::B1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_piece_at() {
|
||||
let sq = Square::E4;
|
||||
let mut bb = BitBoard(0b1001100);
|
||||
bb.place_piece_at(sq);
|
||||
assert!(bb.has_piece_at(sq));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_piece_at() {
|
||||
let sq = Square::A3;
|
||||
let mut bb = BitBoard(0b1001100);
|
||||
bb.remove_piece_at(sq);
|
||||
assert!(!bb.has_piece_at(sq));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_rank_occupancy() {
|
||||
let bb = BitBoard(0b01010100);
|
||||
let expected_squares = [Square::G1, Square::E1, Square::C1];
|
||||
for (a, b) in bb.occupied_squares().zip(expected_squares.iter().cloned()) {
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occupancy_spot_check() {
|
||||
let bb =
|
||||
BitBoard(0b10000000_00000000_00100000_00000100_00000000_00000000_00010000_00001000);
|
||||
|
||||
let expected_squares = [Square::H8, Square::F6, Square::C5, Square::E2, Square::D1];
|
||||
|
||||
for (a, b) in bb.occupied_squares().zip(expected_squares.iter().cloned()) {
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
189
board/src/bitboard/library.rs
Normal file
189
board/src/bitboard/library.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::BitBoard;
|
||||
use crate::{square::Direction, Square};
|
||||
use std::sync::Once;
|
||||
|
||||
pub(super) const RANKS: [BitBoard; 8] = [
|
||||
BitBoard(0xFF << 0 * 8),
|
||||
BitBoard(0xFF << 1 * 8),
|
||||
BitBoard(0xFF << 2 * 8),
|
||||
BitBoard(0xFF << 3 * 8),
|
||||
BitBoard(0xFF << 4 * 8),
|
||||
BitBoard(0xFF << 5 * 8),
|
||||
BitBoard(0xFF << 6 * 8),
|
||||
BitBoard(0xFF << 7 * 8),
|
||||
];
|
||||
|
||||
pub(super) const FILES: [BitBoard; 8] = [
|
||||
BitBoard(0x0101010101010101 << 0),
|
||||
BitBoard(0x0101010101010101 << 1),
|
||||
BitBoard(0x0101010101010101 << 2),
|
||||
BitBoard(0x0101010101010101 << 3),
|
||||
BitBoard(0x0101010101010101 << 4),
|
||||
BitBoard(0x0101010101010101 << 5),
|
||||
BitBoard(0x0101010101010101 << 6),
|
||||
BitBoard(0x0101010101010101 << 7),
|
||||
];
|
||||
|
||||
pub(super) const LIGHT_SQUARES: BitBoard =
|
||||
BitBoard(0x5555 | 0x5555 << 16 | 0x5555 << 32 | 0x5555 << 48);
|
||||
pub(super) const DARK_SQUARES: BitBoard = BitBoard(!LIGHT_SQUARES.0);
|
||||
|
||||
pub(super) fn library() -> &'static MoveLibrary {
|
||||
static MOVE_LIBRARY_INIT: Once = Once::new();
|
||||
static mut MOVE_LIBRARY: MoveLibrary = MoveLibrary::new();
|
||||
|
||||
unsafe {
|
||||
MOVE_LIBRARY_INIT.call_once(|| {
|
||||
MOVE_LIBRARY.init();
|
||||
});
|
||||
|
||||
&MOVE_LIBRARY
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! library_getter {
|
||||
($name:ident) => {
|
||||
pub(super) fn $name(&self, sq: Square) -> BitBoard {
|
||||
self.$name[sq as usize]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct MoveLibrary {
|
||||
// Rays
|
||||
rays: [[BitBoard; 8]; Square::NUM],
|
||||
|
||||
// Piecewise move tables
|
||||
knight_moves: [BitBoard; 64],
|
||||
bishop_moves: [BitBoard; 64],
|
||||
rook_moves: [BitBoard; 64],
|
||||
queen_moves: [BitBoard; 64],
|
||||
king_moves: [BitBoard; 64],
|
||||
}
|
||||
|
||||
impl MoveLibrary {
|
||||
const fn new() -> MoveLibrary {
|
||||
MoveLibrary {
|
||||
rays: [[BitBoard::empty(); 8]; Square::NUM],
|
||||
knight_moves: [BitBoard::empty(); 64],
|
||||
bishop_moves: [BitBoard::empty(); 64],
|
||||
rook_moves: [BitBoard::empty(); 64],
|
||||
queen_moves: [BitBoard::empty(); 64],
|
||||
king_moves: [BitBoard::empty(); 64],
|
||||
}
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
for sq in Square::ALL {
|
||||
self.init_orthogonal_rays(sq);
|
||||
self.init_diagonal_rays(sq);
|
||||
self.init_knight_moves(sq as usize);
|
||||
self.init_bishop_moves(sq);
|
||||
self.init_rook_moves(sq);
|
||||
self.init_queen_moves(sq);
|
||||
self.init_king_moves(sq as usize);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_orthogonal_rays(&mut self, sq: Square) {
|
||||
let sq_bb: BitBoard = sq.into();
|
||||
let rays = &mut self.rays[sq as usize];
|
||||
rays[Direction::North as usize] = Self::generate_ray(sq_bb, BitBoard::shift_north_one);
|
||||
rays[Direction::South as usize] = Self::generate_ray(sq_bb, BitBoard::shift_south_one);
|
||||
rays[Direction::East as usize] = Self::generate_ray(sq_bb, BitBoard::shift_east_one);
|
||||
rays[Direction::West as usize] = Self::generate_ray(sq_bb, BitBoard::shift_west_one);
|
||||
}
|
||||
|
||||
fn init_diagonal_rays(&mut self, sq: Square) {
|
||||
let sq_bb: BitBoard = sq.into();
|
||||
let rays = &mut self.rays[sq as usize];
|
||||
rays[Direction::NorthEast as usize] =
|
||||
Self::generate_ray(sq_bb, BitBoard::shift_north_east_one);
|
||||
rays[Direction::NorthWest as usize] =
|
||||
Self::generate_ray(sq_bb, BitBoard::shift_north_west_one);
|
||||
rays[Direction::SouthWest as usize] =
|
||||
Self::generate_ray(sq_bb, BitBoard::shift_south_west_one);
|
||||
rays[Direction::SouthEast as usize] =
|
||||
Self::generate_ray(sq_bb, BitBoard::shift_south_east_one);
|
||||
}
|
||||
|
||||
fn init_king_moves(&mut self, idx: usize) {
|
||||
let king = BitBoard::new(1 << idx);
|
||||
let mut attacks = king.shift_east_one() | king.shift_west_one();
|
||||
|
||||
let king = king | attacks;
|
||||
attacks |= king.shift_north_one() | king.shift_south_one();
|
||||
|
||||
self.king_moves[idx] = attacks;
|
||||
}
|
||||
|
||||
/// Calculate bitboards representing knight moves from each square on the
|
||||
/// board. The algorithm is described on the [Chess Programming Wiki][cpw].
|
||||
///
|
||||
/// [cpw]: https://www.chessprogramming.org/Knight_Pattern
|
||||
fn init_knight_moves(&mut self, idx: usize) {
|
||||
let knight = BitBoard::new(1 << idx);
|
||||
|
||||
let east = knight.shift_east_one();
|
||||
let west = knight.shift_west_one();
|
||||
|
||||
let mut attacks = (east | west).shift_north(2);
|
||||
attacks |= (east | west).shift_south(2);
|
||||
|
||||
let east = east.shift_east_one();
|
||||
let west = west.shift_west_one();
|
||||
|
||||
attacks |= (east | west).shift_north_one();
|
||||
attacks |= (east | west).shift_south_one();
|
||||
|
||||
self.knight_moves[idx] = attacks;
|
||||
}
|
||||
|
||||
fn init_bishop_moves(&mut self, sq: Square) {
|
||||
let rays = self.rays[sq as usize];
|
||||
self.bishop_moves[sq as usize] = rays[Direction::NorthWest as usize]
|
||||
| rays[Direction::NorthEast as usize]
|
||||
| rays[Direction::SouthEast as usize]
|
||||
| rays[Direction::SouthWest as usize];
|
||||
}
|
||||
|
||||
fn init_rook_moves(&mut self, sq: Square) {
|
||||
let rays = self.rays[sq as usize];
|
||||
self.rook_moves[sq as usize] = rays[Direction::North as usize]
|
||||
| rays[Direction::East as usize]
|
||||
| rays[Direction::South as usize]
|
||||
| rays[Direction::West as usize];
|
||||
}
|
||||
|
||||
fn init_queen_moves(&mut self, sq: Square) {
|
||||
let rook_moves = self.rook_moves[sq as usize];
|
||||
let bishop_moves = self.bishop_moves[sq as usize];
|
||||
self.queen_moves[sq as usize] = rook_moves | bishop_moves;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn generate_ray(sq: BitBoard, shift: fn(BitBoard) -> BitBoard) -> BitBoard {
|
||||
let mut ray = BitBoard::empty();
|
||||
|
||||
let mut iter = shift(sq);
|
||||
while !iter.is_empty() {
|
||||
ray |= iter;
|
||||
iter = shift(iter);
|
||||
}
|
||||
|
||||
ray
|
||||
}
|
||||
|
||||
pub(super) fn ray(&self, sq: Square, dir: Direction) -> BitBoard {
|
||||
self.rays[sq as usize][dir as usize]
|
||||
}
|
||||
|
||||
library_getter!(knight_moves);
|
||||
library_getter!(bishop_moves);
|
||||
library_getter!(rook_moves);
|
||||
library_getter!(queen_moves);
|
||||
library_getter!(king_moves);
|
||||
}
|
||||
7
board/src/bitboard/mod.rs
Normal file
7
board/src/bitboard/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
mod bit_scanner;
|
||||
mod bitboard;
|
||||
mod library;
|
||||
mod shifts;
|
||||
|
||||
pub(crate) use self::bit_scanner::{LeadingBitScanner, TrailingBitScanner};
|
||||
pub(crate) use self::bitboard::BitBoard;
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,392 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{
|
||||
CastleRights, PieceSet,
|
||||
display::DiagramFormatter,
|
||||
piece_sets::{Counter, PlacePieceError, PlacePieceStrategy},
|
||||
zobrist::{ZobristHash, ZobristState},
|
||||
};
|
||||
use chessfriend_bitboard::BitBoard;
|
||||
use chessfriend_core::{Color, Piece, Shape, Square};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type HalfMoveClock = u32;
|
||||
pub type FullMoveClock = u32;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Board {
|
||||
active_color: Color,
|
||||
pieces: PieceSet,
|
||||
castling_rights: CastleRights,
|
||||
en_passant_target: Option<Square>,
|
||||
pub half_move_clock: HalfMoveClock,
|
||||
pub full_move_number: FullMoveClock,
|
||||
zobrist_hash: Option<ZobristHash>,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
/// An empty board
|
||||
#[must_use]
|
||||
pub fn empty(zobrist: Option<Arc<ZobristState>>) -> Self {
|
||||
let mut board = Self {
|
||||
zobrist_hash: zobrist.map(ZobristHash::new),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
board.recompute_zobrist_hash();
|
||||
|
||||
board
|
||||
}
|
||||
|
||||
/// The starting position
|
||||
#[must_use]
|
||||
pub fn starting(zobrist: Option<Arc<ZobristState>>) -> Self {
|
||||
const BLACK_PIECES: [BitBoard; Shape::NUM] = [
|
||||
BitBoard::new(0b0000_0000_1111_1111 << 48),
|
||||
BitBoard::new(0b0100_0010_0000_0000 << 48),
|
||||
BitBoard::new(0b0010_0100_0000_0000 << 48),
|
||||
BitBoard::new(0b1000_0001_0000_0000 << 48),
|
||||
BitBoard::new(0b0000_1000_0000_0000 << 48),
|
||||
BitBoard::new(0b0001_0000_0000_0000 << 48),
|
||||
];
|
||||
|
||||
const WHITE_PIECES: [BitBoard; Shape::NUM] = [
|
||||
BitBoard::new(0b1111_1111_0000_0000),
|
||||
BitBoard::new(0b0000_0000_0100_0010),
|
||||
BitBoard::new(0b0000_0000_0010_0100),
|
||||
BitBoard::new(0b0000_0000_1000_0001),
|
||||
BitBoard::new(0b0000_0000_0000_1000),
|
||||
BitBoard::new(0b0000_0000_0001_0000),
|
||||
];
|
||||
|
||||
let mut board = Self {
|
||||
pieces: PieceSet::new([WHITE_PIECES, BLACK_PIECES]),
|
||||
zobrist_hash: zobrist.map(ZobristHash::new),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
board.recompute_zobrist_hash();
|
||||
|
||||
board
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
#[must_use]
|
||||
pub fn active_color(&self) -> Color {
|
||||
self.active_color
|
||||
}
|
||||
|
||||
pub fn set_active_color(&mut self, color: Color) {
|
||||
if color == self.active_color {
|
||||
return;
|
||||
}
|
||||
|
||||
self.active_color = color;
|
||||
|
||||
if let Some(zobrist) = self.zobrist_hash.as_mut() {
|
||||
zobrist.update_setting_active_color(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
#[must_use]
|
||||
pub fn castling_rights(&self) -> &CastleRights {
|
||||
&self.castling_rights
|
||||
}
|
||||
|
||||
pub(crate) fn castling_rights_mut(&mut self) -> &mut CastleRights {
|
||||
&mut self.castling_rights
|
||||
}
|
||||
|
||||
/// Replace castling rights with new rights wholesale.
|
||||
pub fn set_castling_rights(&mut self, rights: CastleRights) {
|
||||
if rights == self.castling_rights {
|
||||
return;
|
||||
}
|
||||
|
||||
let old_rights = self.castling_rights;
|
||||
self.castling_rights = rights;
|
||||
|
||||
self.update_zobrist_hash_castling_rights(old_rights);
|
||||
}
|
||||
|
||||
pub(crate) fn update_zobrist_hash_castling_rights(&mut self, old_rights: CastleRights) {
|
||||
let new_rights = self.castling_rights;
|
||||
if old_rights == new_rights {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(zobrist) = self.zobrist_hash.as_mut() {
|
||||
zobrist.update_modifying_castling_rights(new_rights, old_rights);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn castling_king(&self, square: Square) -> Option<Piece> {
|
||||
let active_color = self.active_color();
|
||||
self.get_piece(square)
|
||||
.filter(|piece| piece.color == active_color && piece.is_king())
|
||||
}
|
||||
|
||||
pub(crate) fn castling_rook(&self, square: Square) -> Option<Piece> {
|
||||
let active_color = self.active_color();
|
||||
self.get_piece(square)
|
||||
.filter(|piece| piece.color == active_color && piece.is_rook())
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
/// Returns a copy of the current en passant square, if one exists.
|
||||
#[must_use]
|
||||
pub fn en_passant_target(&self) -> Option<Square> {
|
||||
self.en_passant_target
|
||||
}
|
||||
|
||||
pub fn set_en_passant_target(&mut self, square: Square) {
|
||||
self.set_en_passant_target_option(Some(square));
|
||||
}
|
||||
|
||||
pub fn set_en_passant_target_option(&mut self, square: Option<Square>) {
|
||||
let old_target = self.en_passant_target;
|
||||
self.en_passant_target = square;
|
||||
self.update_zobrist_hash_en_passant_target(old_target);
|
||||
}
|
||||
|
||||
pub fn clear_en_passant_target(&mut self) {
|
||||
let old_target = self.en_passant_target;
|
||||
self.en_passant_target = None;
|
||||
self.update_zobrist_hash_en_passant_target(old_target);
|
||||
}
|
||||
|
||||
fn update_zobrist_hash_en_passant_target(&mut self, old_target: Option<Square>) {
|
||||
let new_target = self.en_passant_target;
|
||||
|
||||
if old_target == new_target {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(zobrist) = self.zobrist_hash.as_mut() {
|
||||
zobrist.update_setting_en_passant_target(old_target, new_target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
#[must_use]
|
||||
pub fn get_piece(&self, square: Square) -> Option<Piece> {
|
||||
self.pieces.get(square)
|
||||
}
|
||||
|
||||
pub fn find_pieces(&self, piece: Piece) -> BitBoard {
|
||||
self.pieces.find_pieces(piece)
|
||||
}
|
||||
|
||||
/// Place a piece on the board.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// When is called with [`PlacePieceStrategy::PreserveExisting`], and a
|
||||
/// piece already exists on `square`, this method returns a
|
||||
/// [`PlacePieceError::ExistingPiece`] error.
|
||||
///
|
||||
pub fn place_piece(
|
||||
&mut self,
|
||||
piece: Piece,
|
||||
square: Square,
|
||||
strategy: PlacePieceStrategy,
|
||||
) -> Result<Option<Piece>, PlacePieceError> {
|
||||
let place_result = self.pieces.place(piece, square, strategy);
|
||||
|
||||
if let Ok(Some(existing_piece)) = place_result.as_ref() {
|
||||
if let Some(zobrist) = self.zobrist_hash.as_mut() {
|
||||
zobrist.update_removing_piece(square, *existing_piece);
|
||||
zobrist.update_adding_piece(square, piece);
|
||||
}
|
||||
}
|
||||
|
||||
place_result
|
||||
}
|
||||
|
||||
pub fn remove_piece(&mut self, square: Square) -> Option<Piece> {
|
||||
let removed_piece = self.pieces.remove(square);
|
||||
|
||||
if let Some(piece) = removed_piece {
|
||||
if let Some(zobrist) = self.zobrist_hash.as_mut() {
|
||||
zobrist.update_removing_piece(square, piece);
|
||||
}
|
||||
}
|
||||
|
||||
removed_piece
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn count_piece(&self, piece: &Piece) -> Counter {
|
||||
self.pieces.count(piece)
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Square, Piece)> {
|
||||
self.pieces.iter()
|
||||
}
|
||||
|
||||
/// A [`BitBoard`] of squares occupied by pieces of all colors.
|
||||
pub fn occupancy(&self) -> BitBoard {
|
||||
self.pieces.occpuancy()
|
||||
}
|
||||
|
||||
/// A [`BitBoard`] of squares that are vacant.
|
||||
pub fn vacancy(&self) -> BitBoard {
|
||||
!self.occupancy()
|
||||
}
|
||||
|
||||
pub fn friendly_occupancy(&self, color: Color) -> BitBoard {
|
||||
self.pieces.friendly_occupancy(color)
|
||||
}
|
||||
|
||||
pub fn opposing_occupancy(&self, color: Color) -> BitBoard {
|
||||
self.pieces.opposing_occupancy(color)
|
||||
}
|
||||
|
||||
pub fn enemies(&self, color: Color) -> BitBoard {
|
||||
self.pieces.opposing_occupancy(color)
|
||||
}
|
||||
|
||||
/// Return a [`BitBoard`] of all pawns of a given color.
|
||||
pub fn pawns(&self, color: Color) -> BitBoard {
|
||||
self.pieces.find_pieces(Piece::pawn(color))
|
||||
}
|
||||
|
||||
pub fn knights(&self, color: Color) -> BitBoard {
|
||||
self.find_pieces(Piece::knight(color))
|
||||
}
|
||||
|
||||
pub fn bishops(&self, color: Color) -> BitBoard {
|
||||
self.find_pieces(Piece::bishop(color))
|
||||
}
|
||||
|
||||
pub fn rooks(&self, color: Color) -> BitBoard {
|
||||
self.find_pieces(Piece::rook(color))
|
||||
}
|
||||
|
||||
pub fn queens(&self, color: Color) -> BitBoard {
|
||||
self.find_pieces(Piece::queen(color))
|
||||
}
|
||||
|
||||
pub fn kings(&self, color: Color) -> BitBoard {
|
||||
self.find_pieces(Piece::king(color))
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn zobrist_hash(&self) -> Option<u64> {
|
||||
self.zobrist_hash.as_ref().map(ZobristHash::hash_value)
|
||||
}
|
||||
|
||||
pub fn recompute_zobrist_hash(&mut self) {
|
||||
// Avoid overlapping borrows when borrowing zobrist_hash.as_mut() and
|
||||
// then also borrowing self to update the board hash by computing the
|
||||
// hash with the static function first, and then setting the hash value
|
||||
// on the zobrist instance. Unfortuantely this requires unwrapping
|
||||
// self.zobrist_hash twice. C'est la vie.
|
||||
|
||||
let new_hash = self.zobrist_hash.as_ref().map(|zobrist| {
|
||||
let state = zobrist.state();
|
||||
ZobristHash::compute_board_hash(self, state.as_ref())
|
||||
});
|
||||
|
||||
if let (Some(new_hash), Some(zobrist)) = (new_hash, self.zobrist_hash.as_mut()) {
|
||||
zobrist.set_hash_value(new_hash);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zobrist_state(&self) -> Option<Arc<ZobristState>> {
|
||||
self.zobrist_hash.as_ref().map(ZobristHash::state)
|
||||
}
|
||||
|
||||
pub fn set_zobrist_state(&mut self, state: Arc<ZobristState>) {
|
||||
self.zobrist_hash = Some(ZobristHash::new(state));
|
||||
self.recompute_zobrist_hash();
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn display(&self) -> DiagramFormatter<'_> {
|
||||
DiagramFormatter::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
#[must_use]
|
||||
pub fn unwrap_color(&self, color: Option<Color>) -> Color {
|
||||
color.unwrap_or(self.active_color)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_board;
|
||||
use chessfriend_core::{piece, random::RandomNumberGenerator};
|
||||
|
||||
#[test]
|
||||
fn get_piece_on_square() {
|
||||
let board = test_board![
|
||||
Black Bishop on F7,
|
||||
];
|
||||
|
||||
assert_eq!(board.get_piece(Square::F7), Some(piece!(Black Bishop)));
|
||||
}
|
||||
|
||||
// MARK: - Zobrist Hashing
|
||||
|
||||
fn test_state() -> ZobristState {
|
||||
let mut rng = RandomNumberGenerator::default();
|
||||
ZobristState::new(&mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zobrist_hash_set_for_empty_board() {
|
||||
let state = Arc::new(test_state());
|
||||
let board = Board::empty(Some(state.clone()));
|
||||
let hash = board.zobrist_hash();
|
||||
assert_eq!(hash, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zobrist_hash_set_for_starting_position_board() {
|
||||
let state = Arc::new(test_state());
|
||||
let board = Board::starting(Some(state.clone()));
|
||||
let hash = board.zobrist_hash();
|
||||
assert!(hash.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zobrist_hash_updated_when_changing_active_color() {
|
||||
let state = Arc::new(test_state());
|
||||
|
||||
let mut board = Board::empty(Some(state.clone()));
|
||||
board.set_active_color(Color::Black);
|
||||
|
||||
// Just verify that the value is real and has changed. The actual value
|
||||
// computation is covered by the tests in zobrist.rs.
|
||||
let hash = board.zobrist_hash();
|
||||
assert!(hash.is_some());
|
||||
assert_ne!(hash, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zobrist_hash_updated_when_changing_en_passant_target() {
|
||||
let state = Arc::new(test_state());
|
||||
|
||||
let mut board = Board::empty(Some(state.clone()));
|
||||
board.set_en_passant_target(Square::C3);
|
||||
|
||||
// Just verify that the value is real and has changed. The actual value
|
||||
// computation is covered by the tests in zobrist.rs.
|
||||
let hash = board.zobrist_hash();
|
||||
assert!(hash.is_some());
|
||||
assert_ne!(hash, Some(0));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::Board;
|
||||
|
||||
pub trait BoardProvider {
|
||||
fn board(&self) -> &Board;
|
||||
fn board_mut(&mut self) -> &mut Board;
|
||||
}
|
||||
|
||||
impl BoardProvider for Board {
|
||||
fn board(&self) -> &Board {
|
||||
self
|
||||
}
|
||||
|
||||
fn board_mut(&mut self) -> &mut Board {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod parameters;
|
||||
mod rights;
|
||||
|
||||
pub use parameters::Parameters;
|
||||
pub use rights::{CastleRightsOption, Rights};
|
||||
|
||||
use crate::{Board, CastleParameters};
|
||||
use chessfriend_core::{Color, Wing};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
|
||||
pub enum CastleEvaluationError {
|
||||
#[error("{color} does not have the right to castle {wing}")]
|
||||
NoRights { color: Color, wing: Wing },
|
||||
#[error("no king")]
|
||||
NoKing,
|
||||
#[error("no rook")]
|
||||
NoRook,
|
||||
#[error("castling path is not clear")]
|
||||
ObstructingPieces,
|
||||
#[error("opposing pieces check castling path")]
|
||||
CheckingPieces,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
#[must_use]
|
||||
pub fn castling_parameters(wing: Wing, color: Color) -> &'static CastleParameters {
|
||||
&CastleParameters::BY_COLOR[color as usize][wing as usize]
|
||||
}
|
||||
|
||||
/// Evaluates whether the active color can castle toward the given wing of the board in the
|
||||
/// current position.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns an error indicating why the active color cannot castle.
|
||||
pub fn color_can_castle(
|
||||
&self,
|
||||
wing: Wing,
|
||||
color: Option<Color>,
|
||||
) -> Result<&'static CastleParameters, CastleEvaluationError> {
|
||||
// TODO: Cache this result. It's expensive!
|
||||
// TODO: Does this actually need to rely on internal state, i.e. active_color?
|
||||
|
||||
let color = self.unwrap_color(color);
|
||||
|
||||
if !self.has_castling_right_unwrapped(color, wing) {
|
||||
return Err(CastleEvaluationError::NoRights { color, wing });
|
||||
}
|
||||
|
||||
let parameters = Self::castling_parameters(wing, color);
|
||||
|
||||
if self.castling_king(parameters.origin.king).is_none() {
|
||||
return Err(CastleEvaluationError::NoKing);
|
||||
}
|
||||
|
||||
if self.castling_rook(parameters.origin.rook).is_none() {
|
||||
return Err(CastleEvaluationError::NoRook);
|
||||
}
|
||||
|
||||
// All squares must be clear.
|
||||
let has_obstructing_pieces = (self.occupancy() & parameters.clear).is_populated();
|
||||
if has_obstructing_pieces {
|
||||
return Err(CastleEvaluationError::ObstructingPieces);
|
||||
}
|
||||
|
||||
// King cannot pass through check.
|
||||
let opposing_sight = self.opposing_sight(color);
|
||||
let opposing_pieces_can_see_castling_path =
|
||||
(parameters.check & opposing_sight).is_populated();
|
||||
if opposing_pieces_can_see_castling_path {
|
||||
return Err(CastleEvaluationError::CheckingPieces);
|
||||
}
|
||||
|
||||
Ok(parameters)
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
#[must_use]
|
||||
pub fn has_castling_right(&self, color: Option<Color>, wing: Wing) -> bool {
|
||||
self.has_castling_right_unwrapped(self.unwrap_color(color), wing)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn has_castling_right_active(&self, wing: Wing) -> bool {
|
||||
self.has_castling_right_unwrapped(self.active_color(), wing)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn has_castling_right_unwrapped(&self, color: Color, wing: Wing) -> bool {
|
||||
self.castling_rights().get(color, wing.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn grant_castling_rights(&mut self, color: Option<Color>, rights: CastleRightsOption) {
|
||||
let color = self.unwrap_color(color);
|
||||
self.grant_castling_rights_unwrapped(color, rights);
|
||||
}
|
||||
|
||||
pub fn grant_castling_rights_active(&mut self, rights: CastleRightsOption) {
|
||||
self.grant_castling_rights_unwrapped(self.active_color(), rights);
|
||||
}
|
||||
|
||||
pub fn grant_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) {
|
||||
let old_rights = *self.castling_rights();
|
||||
self.castling_rights_mut().grant(color, rights);
|
||||
self.update_zobrist_hash_castling_rights(old_rights);
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn revoke_all_castling_rights(&mut self) {
|
||||
self.castling_rights_mut().revoke_all();
|
||||
}
|
||||
|
||||
pub fn revoke_castling_rights(&mut self, color: Option<Color>, rights: CastleRightsOption) {
|
||||
let color = self.unwrap_color(color);
|
||||
self.revoke_castling_rights_unwrapped(color, rights);
|
||||
}
|
||||
|
||||
pub fn revoke_castling_rights_active(&mut self, rights: CastleRightsOption) {
|
||||
self.revoke_castling_rights_unwrapped(self.active_color(), rights);
|
||||
}
|
||||
|
||||
pub fn revoke_castling_rights_unwrapped(&mut self, color: Color, rights: CastleRightsOption) {
|
||||
let old_rights = *self.castling_rights();
|
||||
self.castling_rights_mut().revoke(color, rights);
|
||||
self.update_zobrist_hash_castling_rights(old_rights);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_board;
|
||||
use chessfriend_core::{Color, Wing, piece};
|
||||
|
||||
#[test]
|
||||
fn king_on_starting_square_can_castle() {
|
||||
let board = test_board!(
|
||||
White King on E1,
|
||||
White Rook on A1,
|
||||
White Rook on H1
|
||||
);
|
||||
|
||||
assert!(board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
|
||||
assert!(board.has_castling_right_unwrapped(Color::White, Wing::QueenSide));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn king_for_castle() {
|
||||
let pos = test_board![
|
||||
White King on E1,
|
||||
White Rook on H1,
|
||||
White Rook on A1,
|
||||
];
|
||||
|
||||
let kingside_parameters = Board::castling_parameters(Wing::KingSide, Color::White);
|
||||
assert_eq!(
|
||||
pos.castling_king(kingside_parameters.origin.king),
|
||||
Some(piece!(White King))
|
||||
);
|
||||
|
||||
let queenside_parameters = Board::castling_parameters(Wing::QueenSide, Color::White);
|
||||
assert_eq!(
|
||||
pos.castling_king(queenside_parameters.origin.king),
|
||||
Some(piece!(White King))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rook_for_castle() {
|
||||
let pos = test_board![
|
||||
White King on E1,
|
||||
White Rook on H1,
|
||||
];
|
||||
|
||||
let kingside_parameters = Board::castling_parameters(Wing::KingSide, Color::White);
|
||||
assert_eq!(
|
||||
pos.castling_rook(kingside_parameters.origin.rook),
|
||||
Some(piece!(White Rook))
|
||||
);
|
||||
|
||||
let pos = test_board![
|
||||
White King on E1,
|
||||
White Rook on A1,
|
||||
];
|
||||
|
||||
let queenside_parameters = Board::castling_parameters(Wing::QueenSide, Color::White);
|
||||
assert_eq!(
|
||||
pos.castling_rook(queenside_parameters.origin.rook),
|
||||
Some(piece!(White Rook))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_can_castle() -> Result<(), CastleEvaluationError> {
|
||||
let pos = test_board![
|
||||
White King on E1,
|
||||
White Rook on H1,
|
||||
White Rook on A1,
|
||||
];
|
||||
|
||||
pos.color_can_castle(Wing::KingSide, None)?;
|
||||
pos.color_can_castle(Wing::QueenSide, None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_cannot_castle_missing_king() {
|
||||
let pos = test_board![
|
||||
White King on E2,
|
||||
White Rook on H1,
|
||||
White Rook on A1,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
pos.color_can_castle(Wing::KingSide, None),
|
||||
Err(CastleEvaluationError::NoKing)
|
||||
);
|
||||
assert_eq!(
|
||||
pos.color_can_castle(Wing::QueenSide, None),
|
||||
Err(CastleEvaluationError::NoKing)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_cannot_castle_missing_rook() {
|
||||
let pos = test_board![
|
||||
White King on E1,
|
||||
White Rook on A1,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
pos.color_can_castle(Wing::KingSide, None),
|
||||
Err(CastleEvaluationError::NoRook)
|
||||
);
|
||||
|
||||
let pos = test_board![
|
||||
White King on E1,
|
||||
White Rook on H1,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
pos.color_can_castle(Wing::QueenSide, None),
|
||||
Err(CastleEvaluationError::NoRook)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_cannot_castle_obstructing_piece() {
|
||||
let pos = test_board![
|
||||
White King on E1,
|
||||
White Bishop on F1,
|
||||
White Rook on H1,
|
||||
White Rook on A1,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
pos.color_can_castle(Wing::KingSide, None),
|
||||
Err(CastleEvaluationError::ObstructingPieces)
|
||||
);
|
||||
assert!(pos.color_can_castle(Wing::QueenSide, None).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_cannot_castle_checking_pieces() {
|
||||
let pos = test_board![
|
||||
White King on E1,
|
||||
White Rook on H1,
|
||||
White Rook on A1,
|
||||
Black Queen on C6,
|
||||
];
|
||||
|
||||
assert!(pos.color_can_castle(Wing::KingSide, None).is_ok());
|
||||
assert_eq!(
|
||||
pos.color_can_castle(Wing::QueenSide, None),
|
||||
Err(CastleEvaluationError::CheckingPieces)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use chessfriend_core::{Color, Wing};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum CastleRightsOption {
|
||||
Wing(Wing),
|
||||
All,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub struct Rights(u8);
|
||||
|
||||
impl Rights {
|
||||
/// Returns `true` if the player has the right to castle on the given side
|
||||
/// of the board.
|
||||
///
|
||||
/// A player retains the right to castle on a particular side of the board
|
||||
/// as long as they have not moved their king, or the rook on that side of
|
||||
/// the board.
|
||||
#[must_use]
|
||||
pub fn get(self, color: Color, option: CastleRightsOption) -> bool {
|
||||
(self.0 & Self::flags(color, option)) != 0
|
||||
}
|
||||
|
||||
pub fn grant(&mut self, color: Color, option: CastleRightsOption) {
|
||||
self.0 |= Self::flags(color, option);
|
||||
}
|
||||
|
||||
pub fn revoke(&mut self, color: Color, option: CastleRightsOption) {
|
||||
self.0 &= !Self::flags(color, option);
|
||||
}
|
||||
|
||||
/// Revoke castling rights for all colors and all sides of the board.
|
||||
pub fn revoke_all(&mut self) {
|
||||
self.0 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Rights {
|
||||
pub(crate) fn as_index(self) -> usize {
|
||||
self.0 as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl Rights {
|
||||
const fn flags(color: Color, option: CastleRightsOption) -> u8 {
|
||||
option.as_flags() << (color as u8 * 2)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Rights {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Flags({:08b})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Rights {
|
||||
fn default() -> Self {
|
||||
Self(0b0000_1111)
|
||||
}
|
||||
}
|
||||
|
||||
impl CastleRightsOption {
|
||||
#[must_use]
|
||||
pub const fn as_flags(self) -> u8 {
|
||||
match self {
|
||||
Self::Wing(wing) => 1 << (wing as u8),
|
||||
Self::All => (1 << Wing::KingSide as u8) | (1 << Wing::QueenSide as u8),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Wing> for CastleRightsOption {
|
||||
fn from(value: Wing) -> Self {
|
||||
Self::Wing(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bitfield_offsets() {
|
||||
assert_eq!(Rights::flags(Color::White, Wing::KingSide.into()), 1);
|
||||
assert_eq!(Rights::flags(Color::White, Wing::QueenSide.into()), 1 << 1);
|
||||
assert_eq!(Rights::flags(Color::Black, Wing::KingSide.into()), 1 << 2);
|
||||
assert_eq!(Rights::flags(Color::Black, Wing::QueenSide.into()), 1 << 3);
|
||||
|
||||
assert_eq!(Rights::flags(Color::White, CastleRightsOption::All), 0b11);
|
||||
assert_eq!(Rights::flags(Color::Black, CastleRightsOption::All), 0b1100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_rights() {
|
||||
let mut rights = Rights::default();
|
||||
assert!(rights.get(Color::White, Wing::KingSide.into()));
|
||||
assert!(rights.get(Color::White, Wing::QueenSide.into()));
|
||||
assert!(rights.get(Color::Black, Wing::KingSide.into()));
|
||||
assert!(rights.get(Color::Black, Wing::QueenSide.into()));
|
||||
|
||||
rights.revoke(Color::White, Wing::QueenSide.into());
|
||||
assert!(rights.get(Color::White, Wing::KingSide.into()));
|
||||
assert!(!rights.get(Color::White, Wing::QueenSide.into()));
|
||||
assert!(rights.get(Color::Black, Wing::KingSide.into()));
|
||||
assert!(rights.get(Color::Black, Wing::QueenSide.into()));
|
||||
|
||||
rights.grant(Color::White, Wing::QueenSide.into());
|
||||
assert!(rights.get(Color::White, Wing::KingSide.into()));
|
||||
assert!(rights.get(Color::White, Wing::QueenSide.into()));
|
||||
assert!(rights.get(Color::Black, Wing::KingSide.into()));
|
||||
assert!(rights.get(Color::Black, Wing::QueenSide.into()));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::Board;
|
||||
use chessfriend_bitboard::BitBoard;
|
||||
use chessfriend_core::{Color, Piece};
|
||||
|
||||
impl Board {
|
||||
/// Return whether the active color is in check.
|
||||
#[must_use]
|
||||
pub fn is_in_check(&self) -> bool {
|
||||
let color = self.active_color();
|
||||
let king = self.king_bitboard(color);
|
||||
let opposing_sight = self.opposing_sight(color);
|
||||
(king & opposing_sight).is_populated()
|
||||
}
|
||||
|
||||
fn king_bitboard(&self, color: Color) -> BitBoard {
|
||||
self.find_pieces(Piece::king(color))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_board;
|
||||
use chessfriend_core::Color;
|
||||
|
||||
#[test]
|
||||
fn active_color_is_in_check() {
|
||||
let board = test_board!(
|
||||
White King on A3,
|
||||
Black Rook on F3,
|
||||
);
|
||||
|
||||
assert!(board.is_in_check());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_color_is_not_in_check() {
|
||||
let board = test_board!(
|
||||
White King on A3,
|
||||
Black Rook on B4,
|
||||
);
|
||||
|
||||
assert!(!board.is_in_check());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +1,9 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::Board;
|
||||
use chessfriend_bitboard::BitBoard;
|
||||
use chessfriend_core::{File, Rank, Square};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum PieceDisplayStyle {
|
||||
#[default]
|
||||
Unicode,
|
||||
ASCII,
|
||||
pub trait ASCIIDisplay {
|
||||
fn fmt(&self, &mut f: fmt::Formatter) -> fmt::Result;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub struct DiagramFormatter<'a> {
|
||||
board: &'a Board,
|
||||
piece_style: PieceDisplayStyle,
|
||||
marked_squares: BitBoard,
|
||||
highlighted_squares: BitBoard,
|
||||
}
|
||||
|
||||
impl<'a> DiagramFormatter<'a> {
|
||||
pub fn new(board: &'a Board) -> Self {
|
||||
Self {
|
||||
board,
|
||||
piece_style: PieceDisplayStyle::default(),
|
||||
marked_squares: BitBoard::default(),
|
||||
highlighted_squares: BitBoard::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark(mut self, bitboard: BitBoard) -> Self {
|
||||
self.marked_squares = bitboard;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight(mut self, bitboard: BitBoard) -> Self {
|
||||
self.highlighted_squares = bitboard;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DiagramFormatter<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, " ╔═════════════════╗")?;
|
||||
|
||||
for rank in Rank::ALL.into_iter().rev() {
|
||||
write!(f, "{rank} ║ ")?;
|
||||
|
||||
for file in File::ALL {
|
||||
let square = Square::from_file_rank(file, rank);
|
||||
|
||||
let should_highlight = self.highlighted_squares.contains(square);
|
||||
if should_highlight {
|
||||
write!(f, "\x1b[7m")?;
|
||||
}
|
||||
|
||||
if let Some(piece) = self.board.get_piece(square) {
|
||||
let ch = match self.piece_style {
|
||||
PieceDisplayStyle::Unicode => piece.to_unicode(),
|
||||
PieceDisplayStyle::ASCII => piece.to_ascii(),
|
||||
};
|
||||
write!(f, "{ch}")?;
|
||||
} else {
|
||||
let ch = if self.marked_squares.contains(square) {
|
||||
'*'
|
||||
} else {
|
||||
'·'
|
||||
};
|
||||
write!(f, "{ch}")?;
|
||||
}
|
||||
|
||||
if should_highlight {
|
||||
write!(f, "\x1b[0m")?;
|
||||
}
|
||||
|
||||
write!(f, " ")?;
|
||||
}
|
||||
|
||||
writeln!(f, "║")?;
|
||||
}
|
||||
|
||||
writeln!(f, " ╚═════════════════╝")?;
|
||||
write!(f, " a b c d e f g h")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{test_board, Board};
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn empty() {
|
||||
let board = test_board!(empty);
|
||||
let diagram = DiagramFormatter::new(&board);
|
||||
println!("{diagram}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn one_king() {
|
||||
let board = test_board![Black King on H3];
|
||||
let diagram = DiagramFormatter::new(&board);
|
||||
println!("{diagram}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn starting() {
|
||||
let board = test_board!(starting);
|
||||
let diagram = DiagramFormatter::new(&board);
|
||||
println!("{diagram}");
|
||||
}
|
||||
pub trait UnicodeDisplay {
|
||||
fn fmt(&self, &mut f: fmt::Formatter) -> fmt::Result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use chessfriend_core::{Rank, Square};
|
||||
|
||||
/// En passant information.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct EnPassant {
|
||||
target: Square,
|
||||
capture: Square,
|
||||
}
|
||||
|
||||
impl EnPassant {
|
||||
fn _capture_square(target: Square) -> Option<Square> {
|
||||
let (file, rank) = target.file_rank();
|
||||
match rank {
|
||||
Rank::THREE => Some(Square::from_file_rank(file, Rank::FOUR)),
|
||||
Rank::SIX => Some(Square::from_file_rank(file, Rank::FIVE)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return en passant information for a particular target square. The target
|
||||
/// square is the square a pawn capturing en passant will move to.
|
||||
///
|
||||
/// Return `None` if the square is not eligible for en passant.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_board::en_passant::EnPassant;
|
||||
/// use chessfriend_core::Square;
|
||||
/// assert!(EnPassant::from_target_square(Square::E3).is_some());
|
||||
/// assert!(EnPassant::from_target_square(Square::B4).is_none());
|
||||
/// ```
|
||||
pub fn from_target_square(target: Square) -> Option<Self> {
|
||||
Self::_capture_square(target).map(|capture| Self { target, capture })
|
||||
}
|
||||
|
||||
/// The square the capturing piece will move to.
|
||||
pub fn target_square(self) -> Square {
|
||||
self.target
|
||||
}
|
||||
|
||||
/// The square on which the captured pawn sits.
|
||||
pub fn capture_square(self) -> Square {
|
||||
self.capture
|
||||
}
|
||||
}
|
||||
348
board/src/fen.rs
348
board/src/fen.rs
|
|
@ -1,348 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{Board, piece_sets::PlacePieceStrategy};
|
||||
use chessfriend_core::{
|
||||
Color, File, Piece, Rank, Square, Wing, coordinates::ParseSquareError, piece,
|
||||
};
|
||||
use std::fmt::Write;
|
||||
use thiserror::Error;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fen {
|
||||
($fen_string:literal) => {{
|
||||
use $crate::fen::FromFenStr;
|
||||
$crate::Board::from_fen_str($fen_string)
|
||||
}};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
pub enum ToFenStrError {
|
||||
#[error("{0}")]
|
||||
FmtError(#[from] std::fmt::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
pub enum FromFenStrError {
|
||||
#[error("missing {0} field")]
|
||||
MissingField(Field),
|
||||
|
||||
#[error("missing piece placement")]
|
||||
MissingPlacement,
|
||||
|
||||
#[error("invalid value")]
|
||||
InvalidValue,
|
||||
|
||||
#[error("{0}")]
|
||||
ParseIntError(#[from] std::num::ParseIntError),
|
||||
|
||||
#[error("{0}")]
|
||||
ParseSquareError(#[from] ParseSquareError),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Field {
|
||||
Placements,
|
||||
PlayerToMove,
|
||||
CastlingRights,
|
||||
EnPassantSquare,
|
||||
HalfMoveClock,
|
||||
FullMoveCounter,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Field {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Field::Placements => write!(f, "Placements"),
|
||||
Field::PlayerToMove => write!(f, "Player To Move"),
|
||||
Field::CastlingRights => write!(f, "Castling Rights"),
|
||||
Field::EnPassantSquare => write!(f, "En Passant Square"),
|
||||
Field::HalfMoveClock => write!(f, "Half Move Clock"),
|
||||
Field::FullMoveCounter => write!(f, "Full move Counter"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToFenStr {
|
||||
type Error;
|
||||
|
||||
/// Create a FEN string from `Self`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
///
|
||||
fn to_fen_str(&self) -> Result<String, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait FromFenStr: Sized {
|
||||
type Error;
|
||||
|
||||
/// Create a `Self` from a FEN string.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
///
|
||||
fn from_fen_str(string: &str) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
impl ToFenStr for Board {
|
||||
type Error = ToFenStrError;
|
||||
|
||||
fn to_fen_str(&self) -> Result<String, Self::Error> {
|
||||
let mut fen_string = String::new();
|
||||
|
||||
let mut empty_squares: u8 = 0;
|
||||
for rank in Rank::ALL.into_iter().rev() {
|
||||
for file in File::ALL {
|
||||
let square = Square::from_file_rank(file, rank);
|
||||
match self.get_piece(square) {
|
||||
Some(piece) => {
|
||||
if empty_squares > 0 {
|
||||
write!(fen_string, "{empty_squares}")
|
||||
.map_err(ToFenStrError::FmtError)?;
|
||||
empty_squares = 0;
|
||||
}
|
||||
write!(fen_string, "{}", piece.to_fen_str()?)
|
||||
.map_err(ToFenStrError::FmtError)?;
|
||||
}
|
||||
None => empty_squares += 1,
|
||||
}
|
||||
}
|
||||
|
||||
if empty_squares > 0 {
|
||||
write!(fen_string, "{empty_squares}").map_err(ToFenStrError::FmtError)?;
|
||||
empty_squares = 0;
|
||||
}
|
||||
|
||||
if rank != Rank::ONE {
|
||||
write!(fen_string, "/").map_err(ToFenStrError::FmtError)?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(fen_string, " {}", self.active_color().to_fen_str()?)
|
||||
.map_err(ToFenStrError::FmtError)?;
|
||||
|
||||
let castling = [
|
||||
(Color::White, Wing::KingSide),
|
||||
(Color::White, Wing::QueenSide),
|
||||
(Color::Black, Wing::KingSide),
|
||||
(Color::Black, Wing::QueenSide),
|
||||
]
|
||||
.map(|(color, wing)| {
|
||||
if !self.has_castling_right_unwrapped(color, wing) {
|
||||
return "";
|
||||
}
|
||||
|
||||
match (color, wing) {
|
||||
(Color::White, Wing::KingSide) => "K",
|
||||
(Color::White, Wing::QueenSide) => "Q",
|
||||
(Color::Black, Wing::KingSide) => "k",
|
||||
(Color::Black, Wing::QueenSide) => "q",
|
||||
}
|
||||
})
|
||||
.concat();
|
||||
|
||||
write!(
|
||||
fen_string,
|
||||
" {}",
|
||||
if castling.is_empty() { "-" } else { &castling }
|
||||
)
|
||||
.map_err(ToFenStrError::FmtError)?;
|
||||
|
||||
write!(
|
||||
fen_string,
|
||||
" {}",
|
||||
self.en_passant_target()
|
||||
.map_or("-".to_string(), |square| square.to_string())
|
||||
)
|
||||
.map_err(ToFenStrError::FmtError)?;
|
||||
|
||||
write!(fen_string, " {}", self.half_move_clock).map_err(ToFenStrError::FmtError)?;
|
||||
write!(fen_string, " {}", self.full_move_number).map_err(ToFenStrError::FmtError)?;
|
||||
|
||||
Ok(fen_string)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFenStr for Color {
|
||||
type Error = ToFenStrError;
|
||||
|
||||
fn to_fen_str(&self) -> Result<String, Self::Error> {
|
||||
match self {
|
||||
Color::White => Ok("w".to_string()),
|
||||
Color::Black => Ok("b".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFenStr for Piece {
|
||||
type Error = ToFenStrError;
|
||||
|
||||
fn to_fen_str(&self) -> Result<String, Self::Error> {
|
||||
let ascii: char = self.to_ascii();
|
||||
Ok(String::from(match self.color {
|
||||
Color::White => ascii.to_ascii_uppercase(),
|
||||
Color::Black => ascii.to_ascii_lowercase(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromFenStr for Board {
|
||||
type Error = FromFenStrError;
|
||||
|
||||
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
|
||||
let mut board = Board::empty(None);
|
||||
|
||||
let mut fields = string.split(' ');
|
||||
|
||||
let placements = fields
|
||||
.next()
|
||||
.ok_or(FromFenStrError::MissingField(Field::Placements))?;
|
||||
let ranks = placements.split('/');
|
||||
|
||||
for (rank, pieces) in Rank::ALL.iter().rev().zip(ranks) {
|
||||
let mut files = File::ALL.iter();
|
||||
for ch in pieces.chars() {
|
||||
if let Some(skip) = ch.to_digit(10) {
|
||||
// TODO: Use advance_by() when it's available.
|
||||
for _ in 0..skip {
|
||||
files.next();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = files.next().ok_or(FromFenStrError::MissingPlacement)?;
|
||||
let piece = Piece::from_fen_str(&ch.to_string())?;
|
||||
|
||||
let _ = board.place_piece(
|
||||
piece,
|
||||
Square::from_file_rank(*file, *rank),
|
||||
PlacePieceStrategy::default(),
|
||||
);
|
||||
}
|
||||
|
||||
debug_assert_eq!(files.next(), None);
|
||||
}
|
||||
|
||||
let active_color = Color::from_fen_str(
|
||||
fields
|
||||
.next()
|
||||
.ok_or(FromFenStrError::MissingField(Field::PlayerToMove))?,
|
||||
)?;
|
||||
board.set_active_color(active_color);
|
||||
|
||||
let color_wing_from_char = |ch| match ch {
|
||||
'K' => Some((Color::White, Wing::KingSide)),
|
||||
'Q' => Some((Color::White, Wing::QueenSide)),
|
||||
'k' => Some((Color::Black, Wing::KingSide)),
|
||||
'q' => Some((Color::Black, Wing::QueenSide)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let castling_rights = fields
|
||||
.next()
|
||||
.ok_or(FromFenStrError::MissingField(Field::CastlingRights))?;
|
||||
board.revoke_all_castling_rights();
|
||||
if castling_rights != "-" {
|
||||
for ch in castling_rights.chars() {
|
||||
let (color, wing) =
|
||||
color_wing_from_char(ch).ok_or(FromFenStrError::InvalidValue)?;
|
||||
board.grant_castling_rights_unwrapped(color, wing.into());
|
||||
}
|
||||
}
|
||||
|
||||
let en_passant_square = fields
|
||||
.next()
|
||||
.ok_or(FromFenStrError::MissingField(Field::EnPassantSquare))?;
|
||||
if en_passant_square != "-" {
|
||||
let square = Square::from_algebraic_str(en_passant_square)
|
||||
.map_err(FromFenStrError::ParseSquareError)?;
|
||||
board.set_en_passant_target(square);
|
||||
}
|
||||
|
||||
let half_move_clock = fields
|
||||
.next()
|
||||
.ok_or(FromFenStrError::MissingField(Field::HalfMoveClock))?;
|
||||
let half_move_clock: u32 = half_move_clock
|
||||
.parse()
|
||||
.map_err(FromFenStrError::ParseIntError)?;
|
||||
board.half_move_clock = half_move_clock;
|
||||
|
||||
let full_move_counter = fields
|
||||
.next()
|
||||
.ok_or(FromFenStrError::MissingField(Field::FullMoveCounter))?;
|
||||
let full_move_counter: u32 = full_move_counter
|
||||
.parse()
|
||||
.map_err(FromFenStrError::ParseIntError)?;
|
||||
board.full_move_number = full_move_counter;
|
||||
|
||||
debug_assert_eq!(fields.next(), None);
|
||||
|
||||
Ok(board)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromFenStr for Color {
|
||||
type Error = FromFenStrError;
|
||||
|
||||
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
|
||||
if string.len() != 1 {
|
||||
return Err(FromFenStrError::InvalidValue);
|
||||
}
|
||||
|
||||
match string.chars().take(1).next().unwrap() {
|
||||
'w' => Ok(Color::White),
|
||||
'b' => Ok(Color::Black),
|
||||
_ => Err(FromFenStrError::InvalidValue),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromFenStr for Piece {
|
||||
type Error = FromFenStrError;
|
||||
|
||||
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
|
||||
if string.len() != 1 {
|
||||
return Err(FromFenStrError::InvalidValue);
|
||||
}
|
||||
|
||||
match string.chars().take(1).next().unwrap() {
|
||||
'P' => Ok(piece!(White Pawn)),
|
||||
'N' => Ok(piece!(White Knight)),
|
||||
'B' => Ok(piece!(White Bishop)),
|
||||
'R' => Ok(piece!(White Rook)),
|
||||
'Q' => Ok(piece!(White Queen)),
|
||||
'K' => Ok(piece!(White King)),
|
||||
'p' => Ok(piece!(Black Pawn)),
|
||||
'n' => Ok(piece!(Black Knight)),
|
||||
'b' => Ok(piece!(Black Bishop)),
|
||||
'r' => Ok(piece!(Black Rook)),
|
||||
'q' => Ok(piece!(Black Queen)),
|
||||
'k' => Ok(piece!(Black King)),
|
||||
_ => Err(FromFenStrError::InvalidValue),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_board;
|
||||
|
||||
#[test]
|
||||
fn starting_position() {
|
||||
let pos = test_board!(starting);
|
||||
|
||||
assert_eq!(
|
||||
pos.to_fen_str().unwrap(),
|
||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_starting_fen() {
|
||||
let board = fen!("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 0").unwrap();
|
||||
let expected = Board::starting(None);
|
||||
assert_eq!(board, expected, "{board:#?}\n{expected:#?}");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,15 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
pub mod board;
|
||||
pub mod castle;
|
||||
pub mod display;
|
||||
pub mod en_passant;
|
||||
pub mod fen;
|
||||
pub mod macros;
|
||||
pub mod movement;
|
||||
pub mod sight;
|
||||
pub mod zobrist;
|
||||
mod bitboard;
|
||||
//mod moves;
|
||||
#[macro_use]
|
||||
pub mod piece;
|
||||
#[macro_use]
|
||||
pub mod position;
|
||||
mod square;
|
||||
|
||||
mod board_provider;
|
||||
mod check;
|
||||
mod piece_sets;
|
||||
|
||||
pub use board::Board;
|
||||
pub use board_provider::BoardProvider;
|
||||
pub use castle::Parameters as CastleParameters;
|
||||
pub use castle::Rights as CastleRights;
|
||||
pub use piece_sets::{PlacePieceError, PlacePieceStrategy};
|
||||
pub use zobrist::ZobristState;
|
||||
|
||||
use piece_sets::PieceSet;
|
||||
pub(crate) use bitboard::BitBoard;
|
||||
//pub use moves::Move;
|
||||
pub use piece::Color;
|
||||
pub use position::Position;
|
||||
pub use square::{File, Rank, Square};
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! test_board {
|
||||
($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => {
|
||||
{
|
||||
let mut board = $crate::Board::empty(Some($crate::test_zobrist!()));
|
||||
$(let _ = board.place_piece(
|
||||
chessfriend_core::Piece::new(
|
||||
chessfriend_core::Color::$color,
|
||||
chessfriend_core::Shape::$shape
|
||||
),
|
||||
chessfriend_core::Square::$square,
|
||||
$crate::PlacePieceStrategy::default());
|
||||
)*
|
||||
board.set_active_color(chessfriend_core::Color::$to_move);
|
||||
board.set_en_passant_target(chessfriend_core::Square::$en_passant);
|
||||
|
||||
println!("{}", board.display());
|
||||
|
||||
board
|
||||
}
|
||||
};
|
||||
($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => {
|
||||
{
|
||||
let mut board = $crate::Board::empty(Some($crate::test_zobrist!()));
|
||||
$(let _ = board.place_piece(
|
||||
chessfriend_core::Piece::new(
|
||||
chessfriend_core::Color::$color,
|
||||
chessfriend_core::Shape::$shape
|
||||
),
|
||||
chessfriend_core::Square::$square,
|
||||
$crate::PlacePieceStrategy::default());
|
||||
)*
|
||||
board.set_active_color(chessfriend_core::Color::$to_move);
|
||||
|
||||
println!("{}", board.display());
|
||||
|
||||
board
|
||||
}
|
||||
};
|
||||
($($color:ident $shape:ident on $square:ident),* $(,)?) => {
|
||||
{
|
||||
let mut board = $crate::Board::empty(Some($crate::test_zobrist!()));
|
||||
$(let _ = board.place_piece(
|
||||
chessfriend_core::Piece::new(
|
||||
chessfriend_core::Color::$color,
|
||||
chessfriend_core::Shape::$shape
|
||||
),
|
||||
chessfriend_core::Square::$square,
|
||||
$crate::PlacePieceStrategy::default());
|
||||
)*
|
||||
|
||||
println!("{}", board.display());
|
||||
|
||||
board
|
||||
}
|
||||
};
|
||||
(empty) => {
|
||||
{
|
||||
let board = Board::empty(Some($crate::test_zobrist!()));
|
||||
println!("{}", board.display());
|
||||
board
|
||||
}
|
||||
};
|
||||
(starting) => {
|
||||
{
|
||||
let board = Board::starting(Some($crate::test_zobrist!()));
|
||||
println!("{}", board.display());
|
||||
board
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! test_zobrist {
|
||||
() => {{
|
||||
let mut rng = chessfriend_core::random::RandomNumberGenerator::default();
|
||||
let state = $crate::zobrist::ZobristState::new(&mut rng);
|
||||
std::sync::Arc::new(state)
|
||||
}};
|
||||
}
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
//! Defines routines for computing the movement of a piece. Movement is the set
|
||||
//! of squares a piece can move to. For all pieces except pawns, the Movement
|
||||
//! set is equal to the Sight set.
|
||||
|
||||
use crate::{Board, sight::Sight};
|
||||
use chessfriend_bitboard::BitBoard;
|
||||
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
|
||||
|
||||
impl Board {
|
||||
pub fn movement_piece(&self, square: Square) -> BitBoard {
|
||||
if let Some(piece) = self.get_piece(square) {
|
||||
piece.movement(square, self)
|
||||
} else {
|
||||
BitBoard::EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Movement {
|
||||
fn movement(&self, square: Square, board: &Board) -> BitBoard;
|
||||
}
|
||||
|
||||
impl Movement for Piece {
|
||||
fn movement(&self, square: Square, board: &Board) -> BitBoard {
|
||||
let color = self.color;
|
||||
let opposing_occupancy = board.opposing_occupancy(color);
|
||||
|
||||
match self.shape {
|
||||
Shape::Pawn => {
|
||||
let en_passant_square: BitBoard = board.en_passant_target().into();
|
||||
// Pawns can only move to squares they can see to capture.
|
||||
let sight = self.sight(square, board) & (opposing_occupancy | en_passant_square);
|
||||
let pushes = pawn_pushes(square.into(), self.color, board.occupancy());
|
||||
sight | pushes
|
||||
}
|
||||
Shape::King => {
|
||||
let kingside_target_square =
|
||||
if board.color_can_castle(Wing::KingSide, Some(color)).is_ok() {
|
||||
let parameters = Board::castling_parameters(Wing::KingSide, color);
|
||||
parameters.target.king.into()
|
||||
} else {
|
||||
BitBoard::EMPTY
|
||||
};
|
||||
|
||||
let queenside_target_square = if board
|
||||
.color_can_castle(Wing::QueenSide, Some(self.color))
|
||||
.is_ok()
|
||||
{
|
||||
let parameters = Board::castling_parameters(Wing::QueenSide, color);
|
||||
parameters.target.king.into()
|
||||
} else {
|
||||
BitBoard::EMPTY
|
||||
};
|
||||
|
||||
self.sight(square, board) | kingside_target_square | queenside_target_square
|
||||
}
|
||||
_ => self.sight(square, board),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pawn_pushes(pawn: BitBoard, color: Color, occupancy: BitBoard) -> BitBoard {
|
||||
let vacancy = !occupancy;
|
||||
|
||||
match color {
|
||||
Color::White => {
|
||||
const SECOND_RANK: BitBoard = BitBoard::rank(Rank::TWO);
|
||||
|
||||
let mut pushes = pawn.shift_north_one() & vacancy;
|
||||
if !(pawn & SECOND_RANK).is_empty() {
|
||||
// Double push
|
||||
pushes = pushes | (pushes.shift_north_one() & vacancy);
|
||||
}
|
||||
|
||||
pushes
|
||||
}
|
||||
Color::Black => {
|
||||
const SEVENTH_RANK: BitBoard = BitBoard::rank(Rank::SEVEN);
|
||||
|
||||
let mut pushes = pawn.shift_south_one() & vacancy;
|
||||
if !(pawn & SEVENTH_RANK).is_empty() {
|
||||
// Double push
|
||||
pushes = pushes | (pushes.shift_south_one() & vacancy);
|
||||
}
|
||||
|
||||
pushes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::pawn_pushes;
|
||||
use chessfriend_bitboard::{BitBoard, bitboard};
|
||||
use chessfriend_core::{Color, Square};
|
||||
|
||||
#[test]
|
||||
fn white_pushes_empty_board() {
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::E4.into(), Color::White, BitBoard::EMPTY),
|
||||
bitboard![E5]
|
||||
);
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::E2.into(), Color::White, BitBoard::EMPTY),
|
||||
bitboard![E3 E4]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_pawn_empty_board() {
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::A4.into(), Color::Black, BitBoard::EMPTY),
|
||||
bitboard![A3]
|
||||
);
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::B7.into(), Color::Black, BitBoard::EMPTY),
|
||||
bitboard![B6 B5]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_pushes_blocker() {
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::C5.into(), Color::White, bitboard![C6]),
|
||||
BitBoard::EMPTY
|
||||
);
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::D2.into(), Color::White, bitboard![D4]),
|
||||
bitboard![D3]
|
||||
);
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::D2.into(), Color::White, bitboard![D3]),
|
||||
BitBoard::EMPTY
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_pushes_blocker() {
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::C5.into(), Color::Black, bitboard![C4]),
|
||||
BitBoard::EMPTY
|
||||
);
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::D7.into(), Color::Black, bitboard![D5]),
|
||||
bitboard![D6]
|
||||
);
|
||||
assert_eq!(
|
||||
pawn_pushes(Square::D7.into(), Color::Black, bitboard![D6]),
|
||||
BitBoard::EMPTY
|
||||
);
|
||||
}
|
||||
}
|
||||
144
board/src/moves/bishop.rs
Normal file
144
board/src/moves/bishop.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight};
|
||||
use crate::{
|
||||
piece::{Color, Piece, PlacedPiece},
|
||||
Move, Position,
|
||||
};
|
||||
|
||||
move_generator_declaration!(ClassicalMoveGenerator);
|
||||
|
||||
impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> {
|
||||
fn piece(color: Color) -> Piece {
|
||||
Piece::bishop(color)
|
||||
}
|
||||
|
||||
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
|
||||
let piece = placed_piece.piece();
|
||||
let color = piece.color();
|
||||
let square = placed_piece.square();
|
||||
|
||||
let empty_squares = position.empty_squares();
|
||||
let friendly_pieces = position.bitboard_for_color(color);
|
||||
let opposing_pieces = position.bitboard_for_color(color.other());
|
||||
|
||||
let sight = classical::BishopSight.sight(square, position);
|
||||
|
||||
let quiet_moves_bb = sight & (empty_squares | !friendly_pieces);
|
||||
let capture_moves_bb = sight & opposing_pieces;
|
||||
|
||||
let map_to_move = |sq| Move::new(piece, square, sq);
|
||||
let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move);
|
||||
let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move);
|
||||
|
||||
MoveSet::new(placed_piece)
|
||||
.quiet_moves(quiet_moves_bb, quiet_moves)
|
||||
.capture_moves(capture_moves_bb, capture_moves)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
bitboard::BitBoard,
|
||||
piece::{Color, Piece},
|
||||
position::DiagramFormatter,
|
||||
Position, Square,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn classical_single_bishop_bitboard() {
|
||||
let mut pos = Position::empty();
|
||||
[(Piece::bishop(Color::White), Square::A1)]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {:?} on {}", &p, &sq))
|
||||
});
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
assert_eq!(
|
||||
generator.bitboard(),
|
||||
BitBoard::new(
|
||||
0b10000000_01000000_00100000_00010000_00001000_00000100_00000010_00000000
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that a bishop can move up to, but not onto, a friendly piece.
|
||||
#[test]
|
||||
fn classical_single_bishop_with_same_color_blocker_bitboard() {
|
||||
let mut pos = Position::empty();
|
||||
[
|
||||
(Piece::bishop(Color::White), Square::A1),
|
||||
(Piece::knight(Color::White), Square::E5),
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {} on {}", p, sq))
|
||||
});
|
||||
|
||||
println!("{}", DiagramFormatter::new(&pos));
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
assert_eq!(
|
||||
generator.bitboard(),
|
||||
BitBoard::new(
|
||||
0b00000000_00000000_00000000_00000000_00001000_00000100_00000010_00000000
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that a rook can move up to, and then capture, an enemy piece.
|
||||
#[test]
|
||||
fn classical_single_bishop_with_opposing_color_blocker_bitboard() {
|
||||
let mut pos = Position::empty();
|
||||
[
|
||||
(Piece::bishop(Color::White), Square::A1),
|
||||
(Piece::knight(Color::Black), Square::C3),
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {} on {}", p, sq))
|
||||
});
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
assert_eq!(
|
||||
generator.bitboard(),
|
||||
BitBoard::new(
|
||||
0b00000000_00000000_00000000_00000000_00000000_00000100_00000010_00000000
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classical_single_bishop_in_center() {
|
||||
let mut pos = Position::empty();
|
||||
[(Piece::bishop(Color::White), Square::E4)]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {} on {}", p, sq))
|
||||
});
|
||||
|
||||
println!("{}", DiagramFormatter::new(&pos));
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
let bitboard = generator.bitboard();
|
||||
let expected = BitBoard::new(
|
||||
0b00000001_10000010_01000100_00101000_00000000_00101000_01000100_10000010,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bitboard, expected,
|
||||
"actual:\n{}\nexpected:\n{}",
|
||||
bitboard, expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
165
board/src/moves/king.rs
Normal file
165
board/src/moves/king.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
//! Declares the KingMoveGenerator type. This struct is responsible for
|
||||
//! generating the possible moves for the king in the given position.
|
||||
|
||||
use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight};
|
||||
use crate::{
|
||||
bitboard::BitBoard,
|
||||
piece::{Color, Piece, PlacedPiece},
|
||||
position::BoardSide,
|
||||
Move, Position,
|
||||
};
|
||||
|
||||
move_generator_declaration!(KingMoveGenerator, struct);
|
||||
move_generator_declaration!(KingMoveGenerator, new);
|
||||
move_generator_declaration!(KingMoveGenerator, getters);
|
||||
|
||||
impl<'a> KingMoveGenerator<'a> {
|
||||
#[allow(unused_variables)]
|
||||
fn king_side_castle(position: &Position, color: Color) -> Option<Move> {
|
||||
if !position.player_has_right_to_castle(color, BoardSide::King) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// TODO: Implement king side castle.
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn queen_side_castle(position: &Position, color: Color) -> Option<Move> {
|
||||
if !position.player_has_right_to_castle(color, BoardSide::Queen) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// TODO: Implement queen side castle.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'pos> MoveGeneratorInternal for KingMoveGenerator<'pos> {
|
||||
fn piece(color: Color) -> Piece {
|
||||
Piece::king(color)
|
||||
}
|
||||
|
||||
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
|
||||
let piece = placed_piece.piece();
|
||||
let color = piece.color();
|
||||
let square = placed_piece.square();
|
||||
|
||||
let empty_squares = position.empty_squares();
|
||||
let opposing_pieces = position.bitboard_for_color(color.other());
|
||||
|
||||
let all_moves = classical::KingSight.sight(square, position);
|
||||
let quiet_moves_bb = all_moves & empty_squares;
|
||||
let capture_moves_bb = all_moves & opposing_pieces;
|
||||
|
||||
// TODO: Handle checks. Prevent moving a king to a square attacked by a
|
||||
// piece of the opposite color.
|
||||
|
||||
let map_to_move = |sq| Move::new(piece, square, sq);
|
||||
|
||||
let king_side_castle = Self::king_side_castle(position, color);
|
||||
let queen_side_castle = Self::queen_side_castle(position, color);
|
||||
let quiet_moves = quiet_moves_bb
|
||||
.occupied_squares()
|
||||
.map(map_to_move)
|
||||
.chain(king_side_castle.iter().cloned())
|
||||
.chain(queen_side_castle.iter().cloned());
|
||||
let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move);
|
||||
|
||||
MoveSet::new(placed_piece)
|
||||
.quiet_moves(quiet_moves_bb, quiet_moves)
|
||||
.capture_moves(capture_moves_bb, capture_moves)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{piece::Piece, Position, Square};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn one_king() {
|
||||
let mut pos = Position::empty();
|
||||
pos.place_piece(Piece::king(Color::White), Square::E4)
|
||||
.expect("Failed to place king on e4");
|
||||
|
||||
let generator = KingMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
let bb = generator.bitboard();
|
||||
assert_eq!(
|
||||
bb,
|
||||
BitBoard::new(
|
||||
0b00000000_00000000_00000000_00111000_00101000_00111000_00000000_00000000
|
||||
)
|
||||
);
|
||||
|
||||
let expected_moves = [
|
||||
Move::new(Piece::king(Color::White), Square::E4, Square::D5),
|
||||
Move::new(Piece::king(Color::White), Square::E4, Square::E5),
|
||||
Move::new(Piece::king(Color::White), Square::E4, Square::F5),
|
||||
Move::new(Piece::king(Color::White), Square::E4, Square::F4),
|
||||
Move::new(Piece::king(Color::White), Square::E4, Square::F3),
|
||||
Move::new(Piece::king(Color::White), Square::E4, Square::E3),
|
||||
Move::new(Piece::king(Color::White), Square::E4, Square::D3),
|
||||
Move::new(Piece::king(Color::White), Square::E4, Square::D4),
|
||||
];
|
||||
|
||||
let mut generated_moves: HashSet<Move> = generator.iter().cloned().collect();
|
||||
|
||||
for ex_move in expected_moves {
|
||||
assert!(
|
||||
generated_moves.remove(&ex_move),
|
||||
"{:#?} was not generated",
|
||||
&ex_move
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
generated_moves.is_empty(),
|
||||
"Moves unexpectedly present: {:#?}",
|
||||
generated_moves
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_king_corner() {
|
||||
let mut pos = Position::empty();
|
||||
pos.place_piece(Piece::king(Color::White), Square::A1)
|
||||
.expect("Failed to place king on a1");
|
||||
|
||||
let generator = KingMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
let bb = generator.bitboard();
|
||||
assert_eq!(
|
||||
bb,
|
||||
BitBoard::new(
|
||||
0b00000000_00000000_00000000_00000000_00000000_00000000_00000011_00000010
|
||||
)
|
||||
);
|
||||
|
||||
let expected_moves = [
|
||||
Move::new(Piece::king(Color::White), Square::A1, Square::A2),
|
||||
Move::new(Piece::king(Color::White), Square::A1, Square::B1),
|
||||
Move::new(Piece::king(Color::White), Square::A1, Square::B2),
|
||||
];
|
||||
|
||||
let mut generated_moves: HashSet<Move> = generator.iter().cloned().collect();
|
||||
|
||||
for ex_move in expected_moves {
|
||||
assert!(
|
||||
generated_moves.remove(&ex_move),
|
||||
"{:#?} was not generated",
|
||||
&ex_move
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
generated_moves.is_empty(),
|
||||
"Moves unexpectedly present: {:#?}",
|
||||
generated_moves
|
||||
);
|
||||
}
|
||||
}
|
||||
90
board/src/moves/knight.rs
Normal file
90
board/src/moves/knight.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet};
|
||||
use crate::{
|
||||
bitboard::BitBoard,
|
||||
piece::{Color, Piece, PlacedPiece},
|
||||
Move, Position,
|
||||
};
|
||||
|
||||
move_generator_declaration!(KnightMoveGenerator);
|
||||
|
||||
impl<'pos> MoveGeneratorInternal for KnightMoveGenerator<'pos> {
|
||||
fn piece(color: Color) -> Piece {
|
||||
Piece::knight(color)
|
||||
}
|
||||
|
||||
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
|
||||
let opposing_pieces = position.bitboard_for_color(placed_piece.piece().color().other());
|
||||
let empty_squares = position.empty_squares();
|
||||
let knight_moves = BitBoard::knight_moves(placed_piece.square());
|
||||
|
||||
let quiet_moves_bb = knight_moves & empty_squares;
|
||||
let capture_moves_bb = knight_moves & opposing_pieces;
|
||||
|
||||
let quiet_moves = quiet_moves_bb
|
||||
.occupied_squares()
|
||||
.map(|to_sq| Move::new(placed_piece.piece(), placed_piece.square(), to_sq));
|
||||
let capture_moves = capture_moves_bb.occupied_squares().map(|to_sq| {
|
||||
let captured_piece = position.piece_on_square(to_sq).unwrap();
|
||||
Move::new(placed_piece.piece(), placed_piece.square(), to_sq).capturing(captured_piece)
|
||||
});
|
||||
|
||||
MoveSet::new(placed_piece)
|
||||
.quiet_moves(quiet_moves_bb, quiet_moves)
|
||||
.capture_moves(capture_moves_bb, capture_moves)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Square;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn one_knight() {
|
||||
let mut pos = Position::empty();
|
||||
pos.place_piece(Piece::knight(Color::White), Square::E4)
|
||||
.expect("Failed to place knight on e4");
|
||||
|
||||
let generator = KnightMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
/*
|
||||
let bb = generator.bitboard();
|
||||
assert_eq!(
|
||||
bb,
|
||||
BitBoard::new(
|
||||
0b00000000_00000000_00000000_00111000_00101000_00111000_00000000_00000000
|
||||
)
|
||||
);
|
||||
*/
|
||||
|
||||
let expected_moves = [
|
||||
Move::new(Piece::knight(Color::White), Square::E4, Square::C3),
|
||||
Move::new(Piece::knight(Color::White), Square::E4, Square::D2),
|
||||
Move::new(Piece::knight(Color::White), Square::E4, Square::F2),
|
||||
Move::new(Piece::knight(Color::White), Square::E4, Square::G3),
|
||||
Move::new(Piece::knight(Color::White), Square::E4, Square::C5),
|
||||
Move::new(Piece::knight(Color::White), Square::E4, Square::D6),
|
||||
Move::new(Piece::knight(Color::White), Square::E4, Square::G5),
|
||||
Move::new(Piece::knight(Color::White), Square::E4, Square::F6),
|
||||
];
|
||||
|
||||
let mut generated_moves: HashSet<Move> = generator.iter().cloned().collect();
|
||||
|
||||
for ex_move in expected_moves {
|
||||
assert!(
|
||||
generated_moves.remove(&ex_move),
|
||||
"{:#?} was not generated",
|
||||
&ex_move
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
generated_moves.is_empty(),
|
||||
"Moves unexpectedly present: {:#?}",
|
||||
generated_moves
|
||||
);
|
||||
}
|
||||
}
|
||||
94
board/src/moves/mod.rs
Normal file
94
board/src/moves/mod.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod bishop;
|
||||
mod classical;
|
||||
mod king;
|
||||
mod knight;
|
||||
mod r#move;
|
||||
mod move_generator;
|
||||
mod move_set;
|
||||
mod pawn;
|
||||
mod queen;
|
||||
mod rook;
|
||||
pub(crate) mod sight;
|
||||
|
||||
pub use move_generator::Moves;
|
||||
pub use r#move::Move;
|
||||
|
||||
pub(self) use move_set::MoveSet;
|
||||
|
||||
use crate::{
|
||||
piece::{Color, Piece, PlacedPiece},
|
||||
Position, Square,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
trait MoveGenerator {
|
||||
fn iter(&self) -> dyn Iterator<Item = Move>;
|
||||
fn moves(&self, color: Color) -> dyn Iterator<Item = Move>;
|
||||
fn attacks(&self, color: Color) -> dyn Iterator<Item = Move>;
|
||||
}
|
||||
|
||||
macro_rules! move_generator_declaration {
|
||||
($name:ident) => {
|
||||
move_generator_declaration!($name, struct);
|
||||
move_generator_declaration!($name, new);
|
||||
move_generator_declaration!($name, getters);
|
||||
};
|
||||
($name:ident, struct) => {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(super) struct $name<'pos> {
|
||||
position: &'pos crate::Position,
|
||||
color: crate::piece::Color,
|
||||
move_sets: std::collections::BTreeMap<crate::Square, crate::moves::MoveSet>,
|
||||
}
|
||||
};
|
||||
($name:ident, new) => {
|
||||
impl<'pos> $name<'pos> {
|
||||
pub(super) fn new(position: &Position, color: crate::piece::Color) -> $name {
|
||||
$name {
|
||||
position,
|
||||
color,
|
||||
move_sets: Self::move_sets(position, color),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
($name:ident, getters) => {
|
||||
impl<'pos> $name<'pos> {
|
||||
pub(super) fn iter(&self) -> impl Iterator<Item = &crate::Move> + '_ {
|
||||
self.move_sets.values().map(|set| set.moves()).flatten()
|
||||
}
|
||||
|
||||
fn bitboard(&self) -> crate::bitboard::BitBoard {
|
||||
self.move_sets
|
||||
.values()
|
||||
.fold(crate::bitboard::BitBoard::empty(), |partial, mv_set| {
|
||||
partial | mv_set.bitboard()
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(self) use move_generator_declaration;
|
||||
|
||||
trait MoveGeneratorInternal {
|
||||
fn piece(color: Color) -> Piece;
|
||||
|
||||
fn move_sets(position: &Position, color: Color) -> BTreeMap<Square, MoveSet> {
|
||||
let piece = Self::piece(color);
|
||||
BTreeMap::from_iter(
|
||||
position
|
||||
.bitboard_for_piece(piece)
|
||||
.occupied_squares()
|
||||
.map(|sq| {
|
||||
let placed_piece = PlacedPiece::new(piece, sq);
|
||||
let move_set = Self::move_set_for_piece(position, placed_piece);
|
||||
(sq, move_set)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet;
|
||||
}
|
||||
255
board/src/moves/move.rs
Normal file
255
board/src/moves/move.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{piece::*, position::BoardSide, Square};
|
||||
|
||||
/// A move that transfers a piece from one square to another.
|
||||
trait Move<C: Color, S: Shape>: Sized {
|
||||
fn piece(&self) -> Piece<C, S>;
|
||||
fn from_square(&self) -> Square;
|
||||
fn to_square(&self) -> Square;
|
||||
}
|
||||
|
||||
/// A move that captures an opposing piece.
|
||||
trait Capturing<C: Color, S: Shape, CapturedS: Shape>: Move<C, S> {
|
||||
type CaptureMove;
|
||||
|
||||
fn capturing(self, capturing: CapturedS) -> Self::CaptureMove;
|
||||
fn captured_piece(&self) -> Piece<C::Other, CapturedS>;
|
||||
}
|
||||
|
||||
/// A move that promotes a pawn to another piece.
|
||||
trait Promoting<C: Color, S: Shape, PromotingS: Shape>: Move<C, Pawn> {
|
||||
type PromotionMove;
|
||||
|
||||
fn promoting_to(self, shape: PromotingS) -> Self::PromotionMove;
|
||||
fn promoting_piece(&self) -> Piece<C, S>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
pub struct SimpleMove<C, S> {
|
||||
piece: Piece<C, S>,
|
||||
from_square: Square,
|
||||
to_square: Square,
|
||||
}
|
||||
|
||||
impl<C: Color, S: Shape> SimpleMove<C, S> {
|
||||
fn new(piece: Piece<C, S>, from_square: Square, to_square: Square) -> Self {
|
||||
SimpleMove {
|
||||
piece,
|
||||
from_square,
|
||||
to_square,
|
||||
}
|
||||
}
|
||||
|
||||
fn capturing<CapturedS: Shape>(
|
||||
self,
|
||||
captured_piece: Piece<C::Other, CapturedS>,
|
||||
) -> Capture<C, S, CapturedS> {
|
||||
Capture {
|
||||
r#move: self,
|
||||
captured_piece,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, S> Move<C, S> for Piece<C, S> {
|
||||
fn piece(&self) -> Piece<C, S> {
|
||||
self.piece
|
||||
}
|
||||
|
||||
fn from_square(&self) -> Square {
|
||||
self.from_square
|
||||
}
|
||||
|
||||
fn to_square(&self) -> Square {
|
||||
self.to_square
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Capture<C: Color, S: Shape, CapturedS: Shape> {
|
||||
r#move: dyn Move<C, S>,
|
||||
captured_piece: Piece<C::Other, CapturedS>,
|
||||
}
|
||||
|
||||
pub struct Promotion<C: Color, PromS: Shape> {
|
||||
r#move: dyn Move<C, Pawn>,
|
||||
promoting_to_shape: PromS,
|
||||
}
|
||||
|
||||
pub struct CapturingMove<C: Color, S: Shape, CapturingShape: Shape> {
|
||||
capturing: PlacedPiece<C::Other, CapturingShape>,
|
||||
}
|
||||
|
||||
impl<C: Color, S: Shape> Move<C, S> {
|
||||
pub fn is_castle(&self) -> bool {
|
||||
let color = self.piece.color();
|
||||
self.piece.shape() == Shape::King
|
||||
&& self.from == Square::KING_STARTING_SQUARES[color as usize]
|
||||
&& Square::KING_CASTLE_TARGET_SQUARES[color as usize].contains(&self.to)
|
||||
}
|
||||
|
||||
pub fn is_kingside_castle(&self) -> bool {
|
||||
let color = self.piece.color();
|
||||
self.piece.shape() == Shape::King
|
||||
&& self.from == Square::KING_STARTING_SQUARES[color as usize]
|
||||
&& self.to
|
||||
== Square::KING_CASTLE_TARGET_SQUARES[color as usize][BoardSide::King as usize]
|
||||
}
|
||||
|
||||
pub fn is_queenside_castle(&self) -> bool {
|
||||
let color = self.piece.color();
|
||||
self.piece.shape() == Shape::King
|
||||
&& self.from == Square::KING_STARTING_SQUARES[color as usize]
|
||||
&& self.to
|
||||
== Square::KING_CASTLE_TARGET_SQUARES[color as usize][BoardSide::Queen as usize]
|
||||
}
|
||||
|
||||
pub fn is_capture(&self) -> bool {
|
||||
self.capturing.is_some()
|
||||
}
|
||||
|
||||
pub fn is_promotion(&self) -> bool {
|
||||
self.promoting_to.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
mod move_formatter {
|
||||
use super::Move;
|
||||
use crate::{piece::Shape, Position};
|
||||
use std::fmt;
|
||||
|
||||
enum Style {
|
||||
Short,
|
||||
Long,
|
||||
}
|
||||
|
||||
pub(crate) struct AlgebraicMoveFormatter<'m> {
|
||||
r#move: &'m Move,
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'pos, 'm> AlgebraicMoveFormatter<'m> {
|
||||
pub(crate) fn new(mv: &'m Move) -> AlgebraicMoveFormatter<'m> {
|
||||
AlgebraicMoveFormatter {
|
||||
r#move: mv,
|
||||
style: Style::Short,
|
||||
}
|
||||
}
|
||||
|
||||
fn style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
fn fmt_kingside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "0-0")
|
||||
}
|
||||
|
||||
fn fmt_queenside_castle(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "0-0-0")
|
||||
}
|
||||
|
||||
fn fmt_short(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn fmt_long(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// TODO: Figure out how to write the short algebraic form, where a
|
||||
// disambiguating coordiate is specified when two of the same piece
|
||||
// cam move to the same square.
|
||||
|
||||
// TODO: Write better pawn moves.
|
||||
|
||||
let mv = self.r#move;
|
||||
|
||||
let shape = mv.piece.shape();
|
||||
if shape != Shape::Pawn {
|
||||
write!(f, "{}", shape)?;
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{}{}{}",
|
||||
mv.from,
|
||||
if mv.is_capture() { 'x' } else { '-' },
|
||||
mv.to,
|
||||
)?;
|
||||
|
||||
if let Some(promoting_to) = mv.promoting_to {
|
||||
write!(f, "={}", promoting_to)?;
|
||||
}
|
||||
|
||||
// TODO: Write check (+) and checkmate (#) symbols
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'pos, 'mv> fmt::Display for AlgebraicMoveFormatter<'mv> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.r#move.is_kingside_castle() {
|
||||
return self.fmt_kingside_castle(f);
|
||||
} else if self.r#move.is_queenside_castle() {
|
||||
return self.fmt_queenside_castle(f);
|
||||
}
|
||||
|
||||
match self.style {
|
||||
Style::Short => self.fmt_short(f),
|
||||
Style::Long => self.fmt_long(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AlgebraicMoveFormatter, Style};
|
||||
|
||||
macro_rules! chess_move {
|
||||
($color:ident $shape:ident $from_square:ident - $to_square:ident) => {
|
||||
crate::Move::new(
|
||||
crate::piece::Piece::new(crate::piece::Color::$color, crate::piece::Shape::$shape),
|
||||
crate::Square::$from_square,
|
||||
crate::Square::$to_square,
|
||||
)
|
||||
};
|
||||
($color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident) => {
|
||||
chess_move!($color $shape $from_square - $to_square)
|
||||
.capturing(crate::piece::PlacedPiece::new(
|
||||
crate::piece::Piece::new(
|
||||
crate::piece::Color::$captured_color,
|
||||
crate::piece::Shape::$captured_shape,
|
||||
),
|
||||
crate::Square::$to_square,
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! test_algebraic_formatter {
|
||||
($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident x $to_square:ident, $captured_color:ident $captured_shape:ident, $output:expr) => {
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
let mv = chess_move!(
|
||||
$color $shape $from_square x $to_square,
|
||||
$captured_color $captured_shape
|
||||
);
|
||||
|
||||
let formatter = AlgebraicMoveFormatter::new(&mv).style(Style::$style);
|
||||
assert_eq!(format!("{}", formatter), $output);
|
||||
}
|
||||
};
|
||||
($test_name:ident, $style:ident, $color:ident $shape:ident $from_square:ident - $to_square:ident, $output:expr) => {
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
let mv = chess_move!($color $shape $from_square-$to_square);
|
||||
|
||||
let formatter = AlgebraicMoveFormatter::new(&mv).style(Style::$style);
|
||||
assert_eq!(format!("{}", formatter), $output);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_algebraic_formatter!(long_pawn_move, Long, White Pawn E4 - E5, "e4-e5");
|
||||
test_algebraic_formatter!(long_bishop_move, Long, White Bishop A4 - D7, "Ba4-d7");
|
||||
test_algebraic_formatter!(long_bishop_capture, Long, White Bishop A2 x E6, Black Knight, "Ba2xe6");
|
||||
}
|
||||
}
|
||||
44
board/src/moves/move_generator.rs
Normal file
44
board/src/moves/move_generator.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::{
|
||||
bishop::ClassicalMoveGenerator as BishopMoveGenerator, king::KingMoveGenerator,
|
||||
knight::KnightMoveGenerator, pawn::PawnMoveGenerator,
|
||||
queen::ClassicalMoveGenerator as QueenMoveGenerator,
|
||||
rook::ClassicalMoveGenerator as RookMoveGenerator, Move,
|
||||
};
|
||||
use crate::piece::Color;
|
||||
use crate::Position;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct Moves<'pos> {
|
||||
pawn_moves: PawnMoveGenerator<'pos>,
|
||||
knight_moves: KnightMoveGenerator<'pos>,
|
||||
bishop_moves: BishopMoveGenerator<'pos>,
|
||||
rook_moves: RookMoveGenerator<'pos>,
|
||||
queen_moves: QueenMoveGenerator<'pos>,
|
||||
king_moves: KingMoveGenerator<'pos>,
|
||||
}
|
||||
|
||||
impl<'a> Moves<'a> {
|
||||
pub fn new(position: &Position, color: Color) -> Moves {
|
||||
Moves {
|
||||
pawn_moves: PawnMoveGenerator::new(position, color),
|
||||
knight_moves: KnightMoveGenerator::new(position, color),
|
||||
bishop_moves: BishopMoveGenerator::new(position, color),
|
||||
rook_moves: RookMoveGenerator::new(position, color),
|
||||
queen_moves: QueenMoveGenerator::new(position, color),
|
||||
king_moves: KingMoveGenerator::new(position, color),
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = Move> + '_ {
|
||||
self.pawn_moves
|
||||
.iter()
|
||||
.chain(self.knight_moves.iter())
|
||||
.chain(self.bishop_moves.iter())
|
||||
.chain(self.rook_moves.iter())
|
||||
.chain(self.queen_moves.iter())
|
||||
.chain(self.king_moves.iter())
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
71
board/src/moves/move_set.rs
Normal file
71
board/src/moves/move_set.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
use crate::{bitboard::BitBoard, piece::PlacedPiece, Move};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct BitBoardSet {
|
||||
quiet: BitBoard,
|
||||
captures: BitBoard,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct MoveListSet {
|
||||
quiet: Vec<Move>,
|
||||
captures: Vec<Move>,
|
||||
}
|
||||
|
||||
/// A set of moves for a piece on the board.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(super) struct MoveSet {
|
||||
piece: PlacedPiece,
|
||||
bitboards: BitBoardSet,
|
||||
move_lists: MoveListSet,
|
||||
}
|
||||
|
||||
impl MoveSet {
|
||||
pub(super) fn new(piece: PlacedPiece) -> MoveSet {
|
||||
MoveSet {
|
||||
piece,
|
||||
bitboards: BitBoardSet {
|
||||
quiet: BitBoard::empty(),
|
||||
captures: BitBoard::empty(),
|
||||
},
|
||||
move_lists: MoveListSet {
|
||||
quiet: Vec::new(),
|
||||
captures: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn quiet_moves(
|
||||
mut self,
|
||||
bitboard: BitBoard,
|
||||
move_list: impl Iterator<Item = Move>,
|
||||
) -> MoveSet {
|
||||
self.bitboards.quiet = bitboard;
|
||||
self.move_lists.quiet = move_list.collect();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn capture_moves(
|
||||
mut self,
|
||||
bitboard: BitBoard,
|
||||
move_list: impl Iterator<Item = Move>,
|
||||
) -> MoveSet {
|
||||
self.bitboards.captures = bitboard;
|
||||
self.move_lists.captures = move_list.collect();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a BitBoard representing all possible moves.
|
||||
pub(super) fn bitboard(&self) -> BitBoard {
|
||||
self.bitboards.captures | self.bitboards.quiet
|
||||
}
|
||||
|
||||
pub(super) fn moves(&self) -> impl Iterator<Item = &Move> {
|
||||
self.move_lists
|
||||
.captures
|
||||
.iter()
|
||||
.chain(self.move_lists.quiet.iter())
|
||||
}
|
||||
}
|
||||
394
board/src/moves/pawn.rs
Normal file
394
board/src/moves/pawn.rs
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{
|
||||
bitboard::BitBoard,
|
||||
piece::{Color, Piece, Shape},
|
||||
Move, Position,
|
||||
};
|
||||
|
||||
enum MoveList {
|
||||
Quiet = 0,
|
||||
Promotions = 1,
|
||||
Captures = 2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct MoveIterator(usize, usize);
|
||||
|
||||
struct MoveGenerationParameters {
|
||||
starting_rank: BitBoard,
|
||||
promotion_rank: BitBoard,
|
||||
push_shift: fn(BitBoard) -> BitBoard,
|
||||
left_capture_shift: fn(BitBoard) -> BitBoard,
|
||||
right_capture_shift: fn(BitBoard) -> BitBoard,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub(super) struct PawnMoveGenerator<'pos> {
|
||||
color: Color,
|
||||
position: &'pos Position,
|
||||
|
||||
did_populate_move_lists: bool,
|
||||
|
||||
pushes: BitBoard,
|
||||
attacks: BitBoard,
|
||||
|
||||
move_lists: [Vec<Move>; 3],
|
||||
move_iterator: MoveIterator,
|
||||
}
|
||||
|
||||
impl<'pos> PawnMoveGenerator<'pos> {
|
||||
pub(super) fn new(position: &Position, color: Color) -> PawnMoveGenerator {
|
||||
PawnMoveGenerator {
|
||||
position,
|
||||
color,
|
||||
did_populate_move_lists: false,
|
||||
pushes: BitBoard::empty(),
|
||||
attacks: BitBoard::empty(),
|
||||
move_lists: [Vec::new(), Vec::new(), Vec::new()],
|
||||
move_iterator: MoveIterator(0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn iter(&self) -> impl Iterator<Item = &Move> + '_ {
|
||||
self.move_lists.iter().flatten()
|
||||
}
|
||||
|
||||
fn generate_moves(&mut self) {
|
||||
self.generate_move_bitboards();
|
||||
self.populate_move_lists();
|
||||
}
|
||||
|
||||
fn move_generation_parameters(&self) -> MoveGenerationParameters {
|
||||
match self.color {
|
||||
Color::White => MoveGenerationParameters {
|
||||
starting_rank: BitBoard::rank(1),
|
||||
promotion_rank: BitBoard::rank(7),
|
||||
push_shift: BitBoard::shift_north_one,
|
||||
left_capture_shift: BitBoard::shift_north_west_one,
|
||||
right_capture_shift: BitBoard::shift_north_east_one,
|
||||
},
|
||||
Color::Black => MoveGenerationParameters {
|
||||
starting_rank: BitBoard::rank(6),
|
||||
promotion_rank: BitBoard::rank(0),
|
||||
push_shift: BitBoard::shift_south_one,
|
||||
left_capture_shift: BitBoard::shift_south_east_one,
|
||||
right_capture_shift: BitBoard::shift_south_west_one,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn quiet_move_list(&mut self) -> &mut Vec<Move> {
|
||||
&mut self.move_lists[MoveList::Quiet as usize]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn promotion_move_list(&mut self) -> &mut Vec<Move> {
|
||||
&mut self.move_lists[MoveList::Promotions as usize]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn capture_move_list(&mut self) -> &mut Vec<Move> {
|
||||
&mut self.move_lists[MoveList::Captures as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'pos> PawnMoveGenerator<'pos> {
|
||||
fn generate_move_bitboards(&mut self) {
|
||||
let parameters = self.move_generation_parameters();
|
||||
self.generate_pushes_bitboard(¶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<Move> {
|
||||
// TODO: En passant. I think the way to do this is to have the position mark
|
||||
// an en passant square when the conditions are correct, i.e. when the
|
||||
// opposing player has pushed a pawn two squares off the initial rank, and
|
||||
// then check in these routines if a pawn of this color attacks that square.
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'pos> Iterator for PawnMoveGenerator<'pos> {
|
||||
type Item = Move;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.did_populate_move_lists {
|
||||
self.generate_moves();
|
||||
}
|
||||
|
||||
let iter = &mut self.move_iterator;
|
||||
|
||||
// Find the next non-empty list.
|
||||
loop {
|
||||
if iter.0 >= self.move_lists.len() || !self.move_lists[iter.0].is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
iter.0 += 1;
|
||||
}
|
||||
|
||||
if let Some(move_list) = self.move_lists.get(iter.0) {
|
||||
let next_move = move_list[iter.1].clone();
|
||||
|
||||
iter.1 += 1;
|
||||
if iter.1 >= move_list.len() {
|
||||
// Increment the list index here. On the next iteration, find the next non-empty list.
|
||||
iter.0 += 1;
|
||||
iter.1 = 0;
|
||||
}
|
||||
|
||||
Some(next_move)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::piece::PlacedPiece;
|
||||
use crate::position::DiagramFormatter;
|
||||
use crate::{Position, Square};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn one_2square_push() {
|
||||
let mut pos = Position::empty();
|
||||
|
||||
pos.place_piece(Piece::pawn(Color::White), Square::E2)
|
||||
.expect("Failed to place pawn on e2");
|
||||
|
||||
let generator = PawnMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
let expected_moves = HashSet::from_iter(
|
||||
[
|
||||
Move::new(Piece::pawn(Color::White), Square::E2, Square::E3),
|
||||
Move::new(Piece::pawn(Color::White), Square::E2, Square::E4),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
let generated_moves: HashSet<Move> = generator.collect();
|
||||
|
||||
assert_eq!(generated_moves, expected_moves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_1square_push() {
|
||||
let mut pos = Position::empty();
|
||||
|
||||
pos.place_piece(Piece::pawn(Color::White), Square::E3)
|
||||
.expect("Failed to place pawn on e3");
|
||||
|
||||
let generator = PawnMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
let expected_moves = HashSet::from_iter(
|
||||
[Move::new(Piece::pawn(Color::White), Square::E3, Square::E4)].into_iter(),
|
||||
);
|
||||
|
||||
let generated_moves: HashSet<Move> = generator.collect();
|
||||
|
||||
assert_eq!(generated_moves, expected_moves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_obstructed_2square_push() {
|
||||
let mut pos = Position::empty();
|
||||
|
||||
pos.place_piece(Piece::pawn(Color::White), Square::E2)
|
||||
.expect("Failed to place pawn on e2");
|
||||
pos.place_piece(Piece::knight(Color::White), Square::E4)
|
||||
.expect("Failed to place knight on e4");
|
||||
|
||||
println!("{}", DiagramFormatter::new(&pos));
|
||||
|
||||
let generator = PawnMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
let expected_moves = HashSet::from_iter(
|
||||
[Move::new(Piece::pawn(Color::White), Square::E2, Square::E3)].into_iter(),
|
||||
);
|
||||
|
||||
let generated_moves: HashSet<Move> = generator.collect();
|
||||
|
||||
assert_eq!(generated_moves, expected_moves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_obstructed_1square_push() {
|
||||
let mut pos = Position::empty();
|
||||
|
||||
pos.place_piece(Piece::pawn(Color::White), Square::E2)
|
||||
.expect("Failed to place pawn on e2");
|
||||
pos.place_piece(Piece::knight(Color::White), Square::E3)
|
||||
.expect("Failed to place knight on e4");
|
||||
|
||||
println!("{}", DiagramFormatter::new(&pos));
|
||||
|
||||
let generator = PawnMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
let generated_moves: HashSet<Move> = generator.collect();
|
||||
|
||||
assert_eq!(generated_moves, HashSet::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_attack() {
|
||||
let mut pos = Position::empty();
|
||||
pos.place_piece(Piece::pawn(Color::White), Square::E4)
|
||||
.expect("Failed to place pawn on e4");
|
||||
pos.place_piece(Piece::bishop(Color::White), Square::E5)
|
||||
.expect("Failed to place pawn on e4");
|
||||
pos.place_piece(Piece::knight(Color::Black), Square::D5)
|
||||
.expect("Failed to place knight on d5");
|
||||
|
||||
println!("{}", DiagramFormatter::new(&pos));
|
||||
|
||||
let generator = PawnMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
let expected_moves = HashSet::from_iter(
|
||||
[Move::new(Piece::pawn(Color::White), Square::E4, Square::D5)
|
||||
.capturing(PlacedPiece::new(Piece::knight(Color::Black), Square::D5))]
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
let generated_moves: HashSet<Move> = generator.collect();
|
||||
|
||||
assert_eq!(generated_moves, expected_moves);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_double_attack() {
|
||||
let mut pos = Position::empty();
|
||||
pos.place_piece(Piece::pawn(Color::White), Square::E4)
|
||||
.expect("Failed to place pawn on e4");
|
||||
pos.place_piece(Piece::bishop(Color::White), Square::E5)
|
||||
.expect("Failed to place pawn on e4");
|
||||
pos.place_piece(Piece::knight(Color::Black), Square::D5)
|
||||
.expect("Failed to place knight on d5");
|
||||
pos.place_piece(Piece::queen(Color::Black), Square::F5)
|
||||
.expect("Failed to place knight on f5");
|
||||
|
||||
println!("{}", DiagramFormatter::new(&pos));
|
||||
|
||||
let generator = PawnMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
let expected_moves = HashSet::from_iter(
|
||||
[
|
||||
Move::new(Piece::pawn(Color::White), Square::E4, Square::D5)
|
||||
.capturing(PlacedPiece::new(Piece::knight(Color::Black), Square::D5)),
|
||||
Move::new(Piece::pawn(Color::White), Square::E4, Square::F5)
|
||||
.capturing(PlacedPiece::new(Piece::queen(Color::Black), Square::F5)),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
let generated_moves: HashSet<Move> = generator.collect();
|
||||
|
||||
assert_eq!(
|
||||
generated_moves, expected_moves,
|
||||
"generated: {:#?}\nexpected: {:#?}",
|
||||
generated_moves, expected_moves
|
||||
);
|
||||
}
|
||||
}
|
||||
146
board/src/moves/queen.rs
Normal file
146
board/src/moves/queen.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight};
|
||||
use crate::{
|
||||
piece::{Color, Piece, PlacedPiece},
|
||||
Move, Position,
|
||||
};
|
||||
|
||||
move_generator_declaration!(ClassicalMoveGenerator);
|
||||
|
||||
impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> {
|
||||
fn piece(color: Color) -> Piece {
|
||||
Piece::queen(color)
|
||||
}
|
||||
|
||||
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
|
||||
let piece = placed_piece.piece();
|
||||
let color = piece.color();
|
||||
let square = placed_piece.square();
|
||||
|
||||
let blockers = position.occupied_squares();
|
||||
let empty_squares = !blockers;
|
||||
let friendly_pieces = position.bitboard_for_color(color);
|
||||
let opposing_pieces = position.bitboard_for_color(color.other());
|
||||
|
||||
let mut all_moves = classical::QueenSight.sight(square, position);
|
||||
|
||||
let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces);
|
||||
let capture_moves_bb = all_moves & opposing_pieces;
|
||||
|
||||
let map_to_move = |sq| Move::new(piece, square, sq);
|
||||
let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move);
|
||||
let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move);
|
||||
|
||||
MoveSet::new(placed_piece)
|
||||
.quiet_moves(quiet_moves_bb, quiet_moves)
|
||||
.capture_moves(capture_moves_bb, capture_moves)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
bitboard::BitBoard,
|
||||
piece::{Color, Piece},
|
||||
position::DiagramFormatter,
|
||||
Position, Square,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn classical_single_queen_bitboard() {
|
||||
let mut pos = Position::empty();
|
||||
[(Piece::queen(Color::White), Square::A1)]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {:?} on {}", &p, &sq))
|
||||
});
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
let bitboard = generator.bitboard();
|
||||
let expected = BitBoard::new(
|
||||
0b10000001_01000001_00100001_00010001_00001001_00000101_00000011_11111110,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bitboard, expected,
|
||||
"actual:\n{}\nexpected:\n{}",
|
||||
bitboard, expected
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that a rook can move up to, but not onto, a friendly piece.
|
||||
#[test]
|
||||
fn classical_single_queen_with_same_color_blocker_bitboard() {
|
||||
let mut pos = Position::empty();
|
||||
[
|
||||
(Piece::queen(Color::White), Square::A1),
|
||||
(Piece::knight(Color::White), Square::E1),
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {} on {}", p, sq))
|
||||
});
|
||||
|
||||
println!("{}", DiagramFormatter::new(&pos));
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
let bitboard = generator.bitboard();
|
||||
let expected = BitBoard::new(
|
||||
0b10000001_01000001_00100001_00010001_00001001_00000101_00000011_00001110,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bitboard, expected,
|
||||
"actual:\n{}\nexpected:\n{}",
|
||||
bitboard, expected
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that a rook can move up to, and then capture, an enemy piece.
|
||||
#[test]
|
||||
fn classical_single_queen_with_opposing_color_blocker_bitboard() {
|
||||
let mut pos = Position::empty();
|
||||
[
|
||||
(Piece::queen(Color::White), Square::A1),
|
||||
(Piece::knight(Color::Black), Square::E5),
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {} on {}", p, sq))
|
||||
});
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
assert_eq!(
|
||||
generator.bitboard(),
|
||||
BitBoard::new(
|
||||
0b00000001_00000001_00000001_00010001_00001001_00000101_00000011_11111110,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classical_single_queen_in_center() {
|
||||
let mut pos = Position::empty();
|
||||
[(Piece::queen(Color::White), Square::E4)]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {} on {}", p, sq))
|
||||
});
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
assert_eq!(
|
||||
generator.bitboard(),
|
||||
BitBoard::new(
|
||||
0b00010001_10010010_01010100_00111000_11101111_00111000_01010100_10010010
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
140
board/src/moves/rook.rs
Normal file
140
board/src/moves/rook.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::{classical, move_generator_declaration, MoveGeneratorInternal, MoveSet, PieceSight};
|
||||
use crate::{
|
||||
piece::{Color, Piece, PlacedPiece},
|
||||
Move, Position,
|
||||
};
|
||||
|
||||
move_generator_declaration!(ClassicalMoveGenerator);
|
||||
|
||||
impl<'pos> MoveGeneratorInternal for ClassicalMoveGenerator<'pos> {
|
||||
fn piece(color: Color) -> Piece {
|
||||
Piece::rook(color)
|
||||
}
|
||||
|
||||
fn move_set_for_piece(position: &Position, placed_piece: PlacedPiece) -> MoveSet {
|
||||
let piece = placed_piece.piece();
|
||||
let color = piece.color();
|
||||
let square = placed_piece.square();
|
||||
|
||||
let blockers = position.occupied_squares();
|
||||
let empty_squares = !blockers;
|
||||
let friendly_pieces = position.bitboard_for_color(color);
|
||||
let opposing_pieces = position.bitboard_for_color(color.other());
|
||||
|
||||
let all_moves = classical::RookSight.sight(square, position);
|
||||
|
||||
let quiet_moves_bb = all_moves & (empty_squares | !friendly_pieces);
|
||||
let capture_moves_bb = all_moves & opposing_pieces;
|
||||
|
||||
let map_to_move = |sq| Move::new(piece, square, sq);
|
||||
let quiet_moves = quiet_moves_bb.occupied_squares().map(map_to_move);
|
||||
let capture_moves = capture_moves_bb.occupied_squares().map(map_to_move);
|
||||
|
||||
MoveSet::new(placed_piece)
|
||||
.quiet_moves(quiet_moves_bb, quiet_moves)
|
||||
.capture_moves(capture_moves_bb, capture_moves)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
bitboard::BitBoard,
|
||||
piece::{Color, Piece},
|
||||
position::DiagramFormatter,
|
||||
Position, Square,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn classical_single_rook_bitboard() {
|
||||
let mut pos = Position::empty();
|
||||
[(Piece::rook(Color::White), Square::A1)]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {:?} on {}", &p, &sq))
|
||||
});
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
assert_eq!(
|
||||
generator.bitboard(),
|
||||
BitBoard::new(
|
||||
0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_11111110
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that a rook can move up to, but not onto, a friendly piece.
|
||||
#[test]
|
||||
fn classical_single_rook_with_same_color_blocker_bitboard() {
|
||||
let mut pos = Position::empty();
|
||||
[
|
||||
(Piece::rook(Color::White), Square::A1),
|
||||
(Piece::knight(Color::White), Square::E1),
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {} on {}", p, sq))
|
||||
});
|
||||
|
||||
println!("{}", DiagramFormatter::new(&pos));
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
assert_eq!(
|
||||
generator.bitboard(),
|
||||
BitBoard::new(
|
||||
0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00001110
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that a rook can move up to, and then capture, an enemy piece.
|
||||
#[test]
|
||||
fn classical_single_rook_with_opposing_color_blocker_bitboard() {
|
||||
let mut pos = Position::empty();
|
||||
[
|
||||
(Piece::rook(Color::White), Square::A1),
|
||||
(Piece::knight(Color::Black), Square::E1),
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {} on {}", p, sq))
|
||||
});
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
assert_eq!(
|
||||
generator.bitboard(),
|
||||
BitBoard::new(
|
||||
0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00011110
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classical_single_rook_in_center() {
|
||||
let mut pos = Position::empty();
|
||||
[(Piece::rook(Color::White), Square::E4)]
|
||||
.into_iter()
|
||||
.for_each(|(p, sq)| {
|
||||
pos.place_piece(p, sq)
|
||||
.expect(&format!("Unable to place {} on {}", p, sq))
|
||||
});
|
||||
|
||||
let generator = ClassicalMoveGenerator::new(&pos, Color::White);
|
||||
|
||||
assert_eq!(
|
||||
generator.bitboard(),
|
||||
BitBoard::new(
|
||||
0b00010000_00010000_00010000_00010000_11101111_00010000_00010000_00010000
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
165
board/src/moves/sight.rs
Normal file
165
board/src/moves/sight.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{piece::*, square::Direction, BitBoard, Position, Square};
|
||||
|
||||
pub(crate) trait Sight {
|
||||
fn sight_on_empty_board(self, square: Square) -> BitBoard;
|
||||
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard;
|
||||
}
|
||||
|
||||
impl Sight for Piece<White, Pawn> {
|
||||
fn sight_on_empty_board(self, square: Square) -> BitBoard {
|
||||
let pawn: BitBoard = square.into();
|
||||
pawn.shift_north_west_one() | pawn.shift_north_east_one()
|
||||
}
|
||||
|
||||
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
|
||||
let mut possible_squares = position.empty_squares() | position.opposing_pieces();
|
||||
if let Some(en_passant) = position.en_passant_square() {
|
||||
possible_squares |= en_passant.into();
|
||||
}
|
||||
|
||||
self.sight_on_empty_board(square) & possible_squares
|
||||
}
|
||||
}
|
||||
|
||||
impl Sight for Piece<Black, Pawn> {
|
||||
fn sight_on_empty_board(self, square: Square) -> BitBoard {
|
||||
let pawn: BitBoard = square.into();
|
||||
pawn.shift_south_west_one() | pawn.shift_south_east_one()
|
||||
}
|
||||
|
||||
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
|
||||
let mut possible_squares = position.empty_squares() | position.opposing_pieces();
|
||||
if let Some(en_passant) = position.en_passant_square() {
|
||||
possible_squares |= en_passant.into();
|
||||
}
|
||||
|
||||
self.sight_on_empty_board(square) & possible_squares
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Sight for Piece<C, Knight> {
|
||||
fn sight_on_empty_board(self, square: Square) -> BitBoard {
|
||||
BitBoard::knight_moves(square)
|
||||
}
|
||||
|
||||
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
|
||||
self.sight_on_empty_board(square) & !position.friendly_pieces()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Sight for Piece<C, Bishop> {
|
||||
fn sight_on_empty_board(self, square: Square) -> BitBoard {
|
||||
BitBoard::bishop_moves(square)
|
||||
}
|
||||
|
||||
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
|
||||
let mut sight = BitBoard::empty();
|
||||
|
||||
let blockers = position.occupied_squares();
|
||||
macro_rules! update_moves_with_ray {
|
||||
($direction:ident, $occupied_squares:tt) => {
|
||||
let ray = BitBoard::ray(square, Direction::$direction);
|
||||
if let Some(first_occupied_square) =
|
||||
BitBoard::$occupied_squares(&(ray & blockers)).next()
|
||||
{
|
||||
let remainder = BitBoard::ray(first_occupied_square, Direction::$direction);
|
||||
let attack_ray = ray & !remainder;
|
||||
sight |= attack_ray;
|
||||
} else {
|
||||
sight |= ray;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
update_moves_with_ray!(NorthEast, occupied_squares_trailing);
|
||||
update_moves_with_ray!(NorthWest, occupied_squares_trailing);
|
||||
update_moves_with_ray!(SouthEast, occupied_squares);
|
||||
update_moves_with_ray!(SouthWest, occupied_squares);
|
||||
|
||||
sight
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Sight for Piece<C, Rook> {
|
||||
fn sight_on_empty_board(self, square: Square) -> BitBoard {
|
||||
BitBoard::rook_moves(square)
|
||||
}
|
||||
|
||||
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
|
||||
let mut sight = BitBoard::empty();
|
||||
|
||||
let blockers = position.occupied_squares();
|
||||
|
||||
macro_rules! update_moves_with_ray {
|
||||
($direction:ident, $occupied_squares:tt) => {
|
||||
let ray = BitBoard::ray(square, Direction::$direction);
|
||||
if let Some(first_occupied_square) =
|
||||
BitBoard::$occupied_squares(&(ray & blockers)).next()
|
||||
{
|
||||
let remainder = BitBoard::ray(first_occupied_square, Direction::$direction);
|
||||
let attack_ray = ray & !remainder;
|
||||
sight |= attack_ray;
|
||||
} else {
|
||||
sight |= ray;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
update_moves_with_ray!(North, occupied_squares_trailing);
|
||||
update_moves_with_ray!(East, occupied_squares_trailing);
|
||||
update_moves_with_ray!(South, occupied_squares);
|
||||
update_moves_with_ray!(West, occupied_squares);
|
||||
|
||||
sight
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Sight for Piece<C, Queen> {
|
||||
fn sight_on_empty_board(self, square: Square) -> BitBoard {
|
||||
BitBoard::queen_moves(square)
|
||||
}
|
||||
|
||||
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
|
||||
let mut sight = BitBoard::empty();
|
||||
|
||||
let blockers = position.occupied_squares();
|
||||
|
||||
macro_rules! update_moves_with_ray {
|
||||
($direction:ident, $occupied_squares:tt) => {
|
||||
let ray = BitBoard::ray(square, Direction::$direction);
|
||||
if let Some(first_occupied_square) =
|
||||
BitBoard::$occupied_squares(&(ray & blockers)).next()
|
||||
{
|
||||
let remainder = BitBoard::ray(first_occupied_square, Direction::$direction);
|
||||
let attack_ray = ray & !remainder;
|
||||
sight |= attack_ray;
|
||||
} else {
|
||||
sight |= ray;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
update_moves_with_ray!(NorthWest, occupied_squares_trailing);
|
||||
update_moves_with_ray!(North, occupied_squares_trailing);
|
||||
update_moves_with_ray!(NorthEast, occupied_squares_trailing);
|
||||
update_moves_with_ray!(East, occupied_squares_trailing);
|
||||
update_moves_with_ray!(SouthEast, occupied_squares);
|
||||
update_moves_with_ray!(South, occupied_squares);
|
||||
update_moves_with_ray!(SouthWest, occupied_squares);
|
||||
update_moves_with_ray!(West, occupied_squares);
|
||||
|
||||
sight
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Sight for Piece<C, King> {
|
||||
fn sight_on_empty_board(self, square: Square) -> BitBoard {
|
||||
BitBoard::king_moves(square)
|
||||
}
|
||||
|
||||
fn sight<PosC>(self, square: Square, position: &Position<PosC>) -> BitBoard {
|
||||
self.sight_on_empty_board(square) & !position.friendly_pieces()
|
||||
}
|
||||
}
|
||||
177
board/src/piece.rs
Normal file
177
board/src/piece.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{bitboard::BitBoard, Square};
|
||||
use std::fmt;
|
||||
|
||||
pub trait Color: Sized {
|
||||
type Other: Color;
|
||||
}
|
||||
|
||||
macro_rules! into_int_type {
|
||||
($name:ident, $int_type:ident, $value:expr) => {
|
||||
impl Into<$int_type> for $name {
|
||||
fn into(self) -> $int_type {
|
||||
$value
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! color_struct {
|
||||
($name:ident, $index:expr, $other_color:ident) => {
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct $name;
|
||||
|
||||
impl Color for $name {
|
||||
type Other = $other_color;
|
||||
}
|
||||
|
||||
into_int_type!($name, u8, $index);
|
||||
into_int_type!($name, usize, $index);
|
||||
};
|
||||
}
|
||||
|
||||
color_struct!(White, 0, Black);
|
||||
color_struct!(Black, 1, White);
|
||||
|
||||
macro_rules! shape_struct {
|
||||
($name:ident, $index:expr, $char:expr, $white_char:expr, $black_char:expr) => {
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct $name;
|
||||
|
||||
impl Shape for $name {}
|
||||
|
||||
into_int_type!($name, u8, $index);
|
||||
into_int_type!($name, usize, $index);
|
||||
|
||||
impl Into<char> for $name {
|
||||
fn into(self) -> char {
|
||||
$char
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Piece<$name, White> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", $white_char)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Piece<$name, Black> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", $black_char)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub trait Shape: Sized {}
|
||||
|
||||
shape_struct!(Pawn, 0, 'P', '♙', '♟');
|
||||
shape_struct!(Knight, 1, 'N', '♘', '♞');
|
||||
shape_struct!(Bishop, 2, 'B', '♗', '♝');
|
||||
shape_struct!(Rook, 3, 'R', '♖', '♜');
|
||||
shape_struct!(Queen, 4, 'Q', '♕', '♛');
|
||||
shape_struct!(King, 5, 'K', '♔', '♚');
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct TryFromError;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum PiecePlacementError {
|
||||
ExistsOnSquare,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! piece {
|
||||
($color:ident $shape:ident) => {
|
||||
Piece::new(Color::$color, Shape::$shape)
|
||||
};
|
||||
($color:ident $shape:ident on $square:ident) => {
|
||||
PlacedPiece::new(piece!($color $shape), Square::$square)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Piece<C: Color, S: Shape> {
|
||||
color: C,
|
||||
shape: S,
|
||||
}
|
||||
|
||||
macro_rules! piece_constructor {
|
||||
($func_name:ident, $shape:tt) => {
|
||||
pub fn $func_name(color: C) -> Piece<C, S> {
|
||||
Piece {
|
||||
color,
|
||||
shape: $shape,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<C, S> Piece<C, S>
|
||||
where
|
||||
C: Color,
|
||||
S: Shape,
|
||||
{
|
||||
pub fn new(color: C, shape: S) -> Piece<C, S> {
|
||||
Piece { color, shape }
|
||||
}
|
||||
|
||||
piece_constructor!(pawn, Pawn);
|
||||
piece_constructor!(knight, Knight);
|
||||
piece_constructor!(bishop, Bishop);
|
||||
piece_constructor!(rook, Rook);
|
||||
piece_constructor!(queen, Queen);
|
||||
piece_constructor!(king, King);
|
||||
|
||||
pub fn color(&self) -> C {
|
||||
self.color
|
||||
}
|
||||
|
||||
pub fn shape(&self) -> S {
|
||||
self.shape
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct PlacedPiece<C, S> {
|
||||
piece: Piece<C, S>,
|
||||
square: Square,
|
||||
}
|
||||
|
||||
impl<C, S> PlacedPiece<C, S> {
|
||||
pub const fn new(piece: Piece<C, S>, square: Square) -> PlacedPiece<C, S> {
|
||||
PlacedPiece { piece, square }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn piece(&self) -> Piece<C, S> {
|
||||
self.piece
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn square(&self) -> Square {
|
||||
self.square
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn bitboard(&self) -> BitBoard {
|
||||
self.square.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn shape_try_from() {
|
||||
assert_eq!(Shape::try_from('p'), Ok(Shape::Pawn));
|
||||
assert_eq!(Shape::try_from("p"), Ok(Shape::Pawn));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_into_char() {
|
||||
assert_eq!(<Shape as Into<char>>::into(Shape::Pawn) as char, 'p');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod counts;
|
||||
mod mailbox;
|
||||
|
||||
use self::{counts::Counts, mailbox::Mailbox};
|
||||
use chessfriend_bitboard::{BitBoard, IterationDirection};
|
||||
use chessfriend_core::{Color, Piece, Shape, Square};
|
||||
use std::{
|
||||
hash::{Hash, Hasher},
|
||||
ops::BitOr,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) use counts::Counter;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum PlacePieceStrategy {
|
||||
#[default]
|
||||
Replace,
|
||||
PreserveExisting,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
pub enum PlacePieceError {
|
||||
#[error("cannot place piece on {square} with existing {piece}")]
|
||||
ExisitingPiece { piece: Piece, square: Square },
|
||||
}
|
||||
|
||||
/// The internal data structure of a [`Board`] that efficiently manages the
|
||||
/// placement of pieces on the board.
|
||||
#[derive(Clone, Debug, Default, Eq)]
|
||||
pub struct PieceSet {
|
||||
mailbox: Mailbox,
|
||||
counts: Counts,
|
||||
color_occupancy: [BitBoard; Color::NUM],
|
||||
shape_occupancy: [BitBoard; Shape::NUM],
|
||||
}
|
||||
|
||||
impl PieceSet {
|
||||
pub(crate) fn new(pieces: [[BitBoard; Shape::NUM]; Color::NUM]) -> Self {
|
||||
let mut mailbox = Mailbox::default();
|
||||
let mut counts = Counts::default();
|
||||
let mut color_occupancy: [BitBoard; Color::NUM] = Default::default();
|
||||
let mut shape_occupancy: [BitBoard; Shape::NUM] = Default::default();
|
||||
|
||||
for (color_index, color) in Color::into_iter().enumerate() {
|
||||
for (shape_index, shape) in Shape::into_iter().enumerate() {
|
||||
let bitboard = pieces[color_index][shape_index];
|
||||
|
||||
color_occupancy[color_index] |= bitboard;
|
||||
shape_occupancy[shape_index] |= bitboard;
|
||||
|
||||
for square in bitboard.occupied_squares(&IterationDirection::default()) {
|
||||
let piece = Piece::new(color, shape);
|
||||
mailbox.set(piece, square);
|
||||
counts.increment(color, shape);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
mailbox,
|
||||
counts,
|
||||
color_occupancy,
|
||||
shape_occupancy,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Square, Piece)> {
|
||||
self.mailbox.iter()
|
||||
}
|
||||
|
||||
/// A [`BitBoard`] representing all the pieces on the board.
|
||||
pub fn occpuancy(&self) -> BitBoard {
|
||||
self.color_occupancy
|
||||
.iter()
|
||||
.fold(BitBoard::default(), BitOr::bitor)
|
||||
}
|
||||
|
||||
pub fn friendly_occupancy(&self, color: Color) -> BitBoard {
|
||||
self.color_occupancy[color as usize]
|
||||
}
|
||||
|
||||
pub fn opposing_occupancy(&self, color: Color) -> BitBoard {
|
||||
let color_index = color as usize;
|
||||
|
||||
self.color_occupancy.iter().enumerate().fold(
|
||||
BitBoard::default(),
|
||||
|acc, (index, occupancy)| {
|
||||
if index == color_index {
|
||||
acc
|
||||
} else {
|
||||
acc | occupancy
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, square: Square) -> Option<Piece> {
|
||||
self.mailbox.get(square)
|
||||
}
|
||||
|
||||
pub(crate) fn count(&self, piece: &Piece) -> Counter {
|
||||
self.counts.get(piece.color, piece.shape)
|
||||
}
|
||||
|
||||
// TODO: Rename this. Maybe get_all() is better?
|
||||
pub(crate) fn find_pieces(&self, piece: Piece) -> BitBoard {
|
||||
let color_occupancy = self.color_occupancy[piece.color as usize];
|
||||
let shape_occupancy = self.shape_occupancy[piece.shape as usize];
|
||||
color_occupancy & shape_occupancy
|
||||
}
|
||||
|
||||
pub(crate) fn place(
|
||||
&mut self,
|
||||
piece: Piece,
|
||||
square: Square,
|
||||
strategy: PlacePieceStrategy,
|
||||
) -> Result<Option<Piece>, PlacePieceError> {
|
||||
let existing_piece = self.mailbox.get(square);
|
||||
|
||||
if strategy == PlacePieceStrategy::PreserveExisting {
|
||||
if let Some(piece) = existing_piece {
|
||||
return Err(PlacePieceError::ExisitingPiece { piece, square });
|
||||
}
|
||||
}
|
||||
|
||||
let color = piece.color;
|
||||
let shape = piece.shape;
|
||||
|
||||
self.color_occupancy[color as usize].set(square);
|
||||
self.shape_occupancy[shape as usize].set(square);
|
||||
self.counts.increment(color, shape);
|
||||
self.mailbox.set(piece, square);
|
||||
|
||||
Ok(existing_piece)
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, square: Square) -> Option<Piece> {
|
||||
if let Some(piece) = self.mailbox.get(square) {
|
||||
let color_index = piece.color as usize;
|
||||
let shape_index = piece.shape as usize;
|
||||
|
||||
self.color_occupancy[color_index].clear(square);
|
||||
self.shape_occupancy[shape_index].clear(square);
|
||||
self.counts.decrement(piece.color, piece.shape);
|
||||
self.mailbox.remove(square);
|
||||
|
||||
Some(piece)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for PieceSet {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.color_occupancy.hash(state);
|
||||
self.shape_occupancy.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PieceSet {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.color_occupancy == other.color_occupancy
|
||||
&& self.shape_occupancy == other.shape_occupancy
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use chessfriend_core::{Color, Shape, Square};
|
||||
|
||||
pub(crate) type Counter = u8;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub(super) struct Counts([[Counter; Shape::NUM]; Color::NUM]);
|
||||
|
||||
impl Counts {
|
||||
pub fn get(&self, color: Color, shape: Shape) -> Counter {
|
||||
self.0[color as usize][shape as usize]
|
||||
}
|
||||
|
||||
pub fn increment(&mut self, color: Color, shape: Shape) {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
const SQUARE_NUM: u8 = Square::NUM as u8;
|
||||
|
||||
let updated_value = self.0[color as usize][shape as usize] + 1;
|
||||
if updated_value > SQUARE_NUM {
|
||||
let shape_name = shape.name();
|
||||
panic!("piece count for {color} {shape_name} overflowed");
|
||||
}
|
||||
|
||||
self.0[color as usize][shape as usize] = updated_value;
|
||||
}
|
||||
|
||||
pub fn decrement(&mut self, color: Color, shape: Shape) {
|
||||
let count = self.0[color as usize][shape as usize];
|
||||
let updated_count = count.checked_sub(1).unwrap_or_else(|| {
|
||||
let shape_name = shape.name();
|
||||
panic!("piece count for {color} {shape_name} should not underflow");
|
||||
});
|
||||
self.0[color as usize][shape as usize] = updated_count;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn set(&mut self, color: Color, shape: Shape, value: u8) {
|
||||
self.0[color as usize][shape as usize] = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "underflowed")]
|
||||
fn underflow() {
|
||||
let mut counts = Counts::default();
|
||||
counts.decrement(Color::White, Shape::Queen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "overflowed")]
|
||||
fn overflow() {
|
||||
let mut counts = Counts::default();
|
||||
counts.set(Color::White, Shape::Queen, 64);
|
||||
counts.increment(Color::White, Shape::Queen);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use chessfriend_core::{Piece, Square};
|
||||
use std::iter::FromIterator;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub(crate) struct Mailbox([Option<Piece>; Square::NUM]);
|
||||
|
||||
impl Mailbox {
|
||||
pub fn get(&self, square: Square) -> Option<Piece> {
|
||||
self.0[square as usize]
|
||||
}
|
||||
|
||||
pub fn set(&mut self, piece: Piece, square: Square) {
|
||||
self.0[square as usize] = Some(piece);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, square: Square) {
|
||||
self.0[square as usize] = None;
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Square, Piece)> {
|
||||
Square::ALL
|
||||
.into_iter()
|
||||
.zip(self.0)
|
||||
.filter_map(|(square, piece)| piece.map(|piece| (square, piece)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mailbox {
|
||||
fn default() -> Self {
|
||||
Self([None; Square::NUM])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[Option<Piece>; Square::NUM]> for Mailbox {
|
||||
fn from(value: [Option<Piece>; Square::NUM]) -> Self {
|
||||
Mailbox(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(Square, Piece)> for Mailbox {
|
||||
fn from_iter<T: IntoIterator<Item = (Square, Piece)>>(iter: T) -> Self {
|
||||
iter.into_iter()
|
||||
.fold(Self::default(), |mut mailbox, (square, piece)| {
|
||||
mailbox.set(piece, square);
|
||||
mailbox
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chessfriend_core::piece;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn iter_returns_all_pieces() {
|
||||
let mut mailbox = Mailbox::default();
|
||||
mailbox.set(piece!(White Queen), Square::C3);
|
||||
mailbox.set(piece!(White Rook), Square::H8);
|
||||
mailbox.set(piece!(Black Bishop), Square::E4);
|
||||
mailbox.set(piece!(Black Pawn), Square::F6);
|
||||
|
||||
let pieces = mailbox.iter().collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
pieces,
|
||||
HashSet::from([
|
||||
(Square::C3, piece!(White Queen)),
|
||||
(Square::H8, piece!(White Rook)),
|
||||
(Square::E4, piece!(Black Bishop)),
|
||||
(Square::F6, piece!(Black Pawn)),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
73
board/src/position/diagram_formatter.rs
Normal file
73
board/src/position/diagram_formatter.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{File, Position, Rank, Square};
|
||||
use std::{fmt, fmt::Write};
|
||||
|
||||
pub struct DiagramFormatter<'a, PosC>(&'a Position<PosC>);
|
||||
|
||||
impl<'a, PosC> DiagramFormatter<'a, PosC> {
|
||||
pub fn new(position: &'a Position<PosC>) -> DiagramFormatter<PosC> {
|
||||
DiagramFormatter(position)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, PosC> fmt::Display for DiagramFormatter<'a, PosC> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut output = String::new();
|
||||
|
||||
output.push_str(" +-----------------+\n");
|
||||
|
||||
for rank in Rank::ALL.iter().rev() {
|
||||
write!(output, "{} | ", rank)?;
|
||||
|
||||
for file in File::ALL.iter() {
|
||||
let square = Square::from_file_rank(*file, *rank);
|
||||
match self.0.piece_on_square(square) {
|
||||
Some(placed_piece) => write!(output, "{} ", placed_piece.piece())?,
|
||||
None => output.push_str(". "),
|
||||
}
|
||||
}
|
||||
|
||||
output.push_str("|\n");
|
||||
}
|
||||
|
||||
output.push_str(" +-----------------+\n");
|
||||
output.push_str(" a b c d e f g h\n");
|
||||
|
||||
write!(f, "{}", output)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::piece::{Color, Piece};
|
||||
use crate::Position;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let pos = Position::empty();
|
||||
let diagram = DiagramFormatter(&pos);
|
||||
println!("{}", diagram);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_king() {
|
||||
let mut pos = Position::empty();
|
||||
pos.place_piece(
|
||||
Piece::king(Color::Black),
|
||||
Square::from_algebraic_str("h3").expect("h3"),
|
||||
)
|
||||
.expect("Unable to place piece");
|
||||
|
||||
let diagram = DiagramFormatter(&pos);
|
||||
println!("{}", diagram);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starting() {
|
||||
let pos = Position::starting();
|
||||
let diagram = DiagramFormatter(&pos);
|
||||
println!("{}", diagram);
|
||||
}
|
||||
}
|
||||
86
board/src/position/flags.rs
Normal file
86
board/src/position/flags.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::position::BoardSide;
|
||||
use crate::piece::Color;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub(super) struct Flags(u8);
|
||||
|
||||
impl Flags {
|
||||
#[inline]
|
||||
pub(super) fn player_has_right_to_castle_flag_offset<C: Color>(
|
||||
color: C,
|
||||
side: BoardSide,
|
||||
) -> u8 {
|
||||
((color as u8) << 1) + side as u8
|
||||
}
|
||||
|
||||
pub(super) fn player_has_right_to_castle<C: Color>(&self, color: C, side: BoardSide) -> bool {
|
||||
(self.0 & (1 << Self::player_has_right_to_castle_flag_offset(color, side))) != 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn set_player_has_right_to_castle_flag<C: Color>(
|
||||
&mut self,
|
||||
color: C,
|
||||
side: BoardSide,
|
||||
) {
|
||||
self.0 |= 1 << Self::player_has_right_to_castle_flag_offset(color, side);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn clear_player_has_right_to_castle_flag<C: Color>(
|
||||
&mut self,
|
||||
color: C,
|
||||
side: BoardSide,
|
||||
) {
|
||||
self.0 &= !(1 << Self::player_has_right_to_castle_flag_offset(color, side));
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Flags {
|
||||
fn default() -> Self {
|
||||
Flags(0b00001111)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Color;
|
||||
|
||||
#[test]
|
||||
fn castle_flags() {
|
||||
assert_eq!(
|
||||
Flags::player_has_right_to_castle_flag_offset(Color::White, BoardSide::King),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
Flags::player_has_right_to_castle_flag_offset(Color::White, BoardSide::Queen),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
Flags::player_has_right_to_castle_flag_offset(Color::Black, BoardSide::King),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
Flags::player_has_right_to_castle_flag_offset(Color::Black, BoardSide::Queen),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults() {
|
||||
let mut flags: Flags = Default::default();
|
||||
assert!(flags.player_has_right_to_castle(Color::White, BoardSide::King));
|
||||
assert!(flags.player_has_right_to_castle(Color::White, BoardSide::Queen));
|
||||
assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::King));
|
||||
assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::Queen));
|
||||
|
||||
flags.clear_player_has_right_to_castle_flag(Color::White, BoardSide::Queen);
|
||||
assert!(flags.player_has_right_to_castle(Color::White, BoardSide::King));
|
||||
assert!(!flags.player_has_right_to_castle(Color::White, BoardSide::Queen));
|
||||
assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::King));
|
||||
assert!(flags.player_has_right_to_castle(Color::Black, BoardSide::Queen));
|
||||
}
|
||||
}
|
||||
12
board/src/position/mod.rs
Normal file
12
board/src/position/mod.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod diagram_formatter;
|
||||
mod flags;
|
||||
mod pieces;
|
||||
mod position;
|
||||
|
||||
pub use diagram_formatter::DiagramFormatter;
|
||||
pub use pieces::Pieces;
|
||||
pub use position::Position;
|
||||
|
||||
pub(crate) use position::BoardSide;
|
||||
141
board/src/position/pieces.rs
Normal file
141
board/src/position/pieces.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::Position;
|
||||
use crate::bitboard::BitBoard;
|
||||
use crate::piece::*;
|
||||
use crate::Square;
|
||||
|
||||
pub struct Pieces<'a, PosC, C: Color> {
|
||||
color: C,
|
||||
position: &'a Position<PosC>,
|
||||
|
||||
current_shape: Option<dyn Shape>,
|
||||
|
||||
shape_iterator: std::array::IntoIter<dyn Shape, 6>,
|
||||
square_iterator: Option<Box<dyn Iterator<Item = Square>>>,
|
||||
}
|
||||
|
||||
impl<'a, PosC, C> Pieces<'a, PosC, C> {
|
||||
pub(crate) fn new(position: &Position<PosC>, color: C) -> Pieces<PosC, C> {
|
||||
Pieces {
|
||||
color,
|
||||
position,
|
||||
current_shape: None,
|
||||
shape_iterator: [Pawn, Knight, Bishop, Rook, Queen, King].into_iter(),
|
||||
square_iterator: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, PosC, C, S> Iterator for Pieces<'a, PosC, C>
|
||||
where
|
||||
C: Color,
|
||||
S: Shape,
|
||||
{
|
||||
type Item = PlacedPiece<C, S>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(square_iterator) = &mut self.square_iterator {
|
||||
if let (Some(square), Some(shape)) = (square_iterator.next(), self.current_shape) {
|
||||
return Some(PlacedPiece::new(Piece::new(self.color, shape), square));
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_shape: Option<Shape> = None;
|
||||
let mut next_nonempty_bitboard: Option<BitBoard> = None;
|
||||
|
||||
while let Some(shape) = self.shape_iterator.next() {
|
||||
let piece = Piece::new(self.color, *shape);
|
||||
|
||||
let bitboard = self.position.bitboard_for_piece(piece);
|
||||
if bitboard.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
next_nonempty_bitboard = Some(bitboard);
|
||||
current_shape = Some(*shape);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if let (Some(bitboard), Some(shape)) = (next_nonempty_bitboard, current_shape) {
|
||||
let mut square_iterator = bitboard.occupied_squares();
|
||||
|
||||
let mut next_placed_piece: Option<PlacedPiece> = None;
|
||||
if let Some(square) = square_iterator.next() {
|
||||
next_placed_piece = Some(PlacedPiece::new(Piece::new(self.color, shape), square));
|
||||
}
|
||||
|
||||
self.square_iterator = Some(Box::new(square_iterator));
|
||||
self.current_shape = Some(shape);
|
||||
|
||||
return next_placed_piece;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::piece::{Color, Piece, Shape};
|
||||
use crate::Position;
|
||||
use crate::Square;
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn place_piece_in_position<PosC, C, S>(
|
||||
pos: &mut Position<PosC>,
|
||||
piece: Piece<C, S>,
|
||||
sq: Square,
|
||||
) {
|
||||
pos.place_piece(piece, sq)
|
||||
.expect("Unable to place {piece:?} queen on {sq}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let pos = Position::empty();
|
||||
let mut pieces = pos.pieces(Color::White);
|
||||
assert_eq!(pieces.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one() {
|
||||
let sq = Square::E4;
|
||||
|
||||
let mut pos = Position::empty();
|
||||
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Queen), Square::E4);
|
||||
println!("{:?}", &pos);
|
||||
|
||||
let mut pieces = pos.pieces(Color::White);
|
||||
assert_eq!(
|
||||
pieces.next(),
|
||||
Some(PlacedPiece::new(Piece::new(Color::White, Shape::Queen), sq))
|
||||
);
|
||||
assert_eq!(pieces.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_pieces() {
|
||||
let mut pos = Position::empty();
|
||||
|
||||
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Queen), Square::E4);
|
||||
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::King), Square::E1);
|
||||
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Pawn), Square::B2);
|
||||
place_piece_in_position(&mut pos, Piece::new(Color::White, Shape::Pawn), Square::C2);
|
||||
|
||||
println!("{:?}", &pos);
|
||||
|
||||
let expected_placed_pieces = HashSet::from([
|
||||
PlacedPiece::new(Piece::new(Color::White, Shape::Queen), Square::E4),
|
||||
PlacedPiece::new(Piece::new(Color::White, Shape::King), Square::E1),
|
||||
PlacedPiece::new(Piece::new(Color::White, Shape::Pawn), Square::B2),
|
||||
PlacedPiece::new(Piece::new(Color::White, Shape::Pawn), Square::C2),
|
||||
]);
|
||||
|
||||
let placed_pieces = HashSet::from_iter(pos.pieces(Color::White));
|
||||
|
||||
assert_eq!(placed_pieces, expected_placed_pieces);
|
||||
}
|
||||
}
|
||||
332
board/src/position/position.rs
Normal file
332
board/src/position/position.rs
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::{flags::Flags, Pieces};
|
||||
use crate::{
|
||||
//moves::Moves,
|
||||
piece::{Color, Piece, PiecePlacementError, PlacedPiece, Shape},
|
||||
BitBoard,
|
||||
Square,
|
||||
};
|
||||
use std::fmt;
|
||||
use std::fmt::Write;
|
||||
|
||||
/// A lateral side of the board relative to the player. Kingside is the side the
|
||||
/// player's king starts on. Queenside is the side of the board the player's
|
||||
/// queen starts on.
|
||||
pub(crate) enum BoardSide {
|
||||
King,
|
||||
Queen,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! position {
|
||||
[$($color:ident $shape:ident on $square:ident,)*] => {
|
||||
Position::from_iter([
|
||||
$(piece!($color $shape on $square)),*
|
||||
].into_iter())
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, Hash, PartialEq)]
|
||||
pub struct Position<ColorToMove> {
|
||||
color_to_move: ColorToMove,
|
||||
|
||||
flags: Flags,
|
||||
|
||||
/// Composite bitboards for all the pieces of a particular color.
|
||||
pieces_per_color: [BitBoard; 2],
|
||||
|
||||
/// Bitboards representing positions of particular piece types per color.
|
||||
pieces_per_type: [[BitBoard; 6]; 2],
|
||||
|
||||
en_passant_square: Option<Square>,
|
||||
}
|
||||
|
||||
impl<ColorToMove> Position<ColorToMove> {
|
||||
pub fn empty() -> Position<ColorToMove> {
|
||||
Position {
|
||||
color_to_move: crate::piece::White,
|
||||
flags: Default::default(),
|
||||
pieces_per_color: [BitBoard::empty(); 2],
|
||||
pieces_per_type: [
|
||||
[
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
],
|
||||
[
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
BitBoard::empty(),
|
||||
],
|
||||
],
|
||||
en_passant_square: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a starting position.
|
||||
pub fn starting() -> Position<ColorToMove> {
|
||||
let white_pieces = [
|
||||
BitBoard::new(0x00FF000000000000),
|
||||
BitBoard::new(0x4200000000000000),
|
||||
BitBoard::new(0x2400000000000000),
|
||||
BitBoard::new(0x8100000000000000),
|
||||
BitBoard::new(0x1000000000000000),
|
||||
BitBoard::new(0x0800000000000000),
|
||||
];
|
||||
|
||||
let black_pieces = [
|
||||
BitBoard::new(0xFF00),
|
||||
BitBoard::new(0x0042),
|
||||
BitBoard::new(0x0024),
|
||||
BitBoard::new(0x0081),
|
||||
BitBoard::new(0x0010),
|
||||
BitBoard::new(0x0008),
|
||||
];
|
||||
|
||||
Position {
|
||||
color_to_move: crate::piece::White,
|
||||
flags: Default::default(),
|
||||
pieces_per_color: [
|
||||
white_pieces.iter().fold(BitBoard::empty(), |a, b| a | *b),
|
||||
black_pieces.iter().fold(BitBoard::empty(), |a, b| a | *b),
|
||||
],
|
||||
pieces_per_type: [white_pieces, black_pieces],
|
||||
en_passant_square: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the player has the right to castle on the given side of
|
||||
/// the board.
|
||||
///
|
||||
/// The right to castle on a particular side of the board is retained as
|
||||
/// long as the player has not moved their king, or the rook on that side of
|
||||
/// the board.
|
||||
///
|
||||
/// The following requirements must also be met:
|
||||
///
|
||||
/// 1. The spaces between the king and rook must be clear
|
||||
/// 2. The king must not be in check
|
||||
/// 3. In the course of castling on that side, the king must not pass
|
||||
/// through a square that an enemy piece can see
|
||||
pub(crate) fn player_has_right_to_castle<C>(&self, color: C, side: BoardSide) -> bool {
|
||||
self.flags.player_has_right_to_castle(color, side)
|
||||
}
|
||||
|
||||
pub fn place_piece<C, S>(
|
||||
&mut self,
|
||||
piece: Piece<C, S>,
|
||||
square: Square,
|
||||
) -> Result<(), PiecePlacementError> {
|
||||
let type_bb = self.bitboard_for_piece_mut(piece);
|
||||
|
||||
if type_bb.has_piece_at(square) {
|
||||
return Err(PiecePlacementError::ExistsOnSquare);
|
||||
}
|
||||
|
||||
type_bb.place_piece_at(square);
|
||||
|
||||
let color_bb = &mut self.bitboard_for_color_mut(piece.color());
|
||||
color_bb.place_piece_at(square);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub fn move_generator<C>(&self, color: C) -> Moves {
|
||||
// Moves::new(self, color)
|
||||
// }
|
||||
|
||||
pub(crate) fn bitboard_for_piece<C, S>(&self, piece: Piece<C, S>) -> BitBoard {
|
||||
self.pieces_per_type[piece.color() as usize][piece.shape() as usize]
|
||||
}
|
||||
|
||||
fn bitboard_for_piece_mut<C, S>(&mut self, piece: Piece<C, S>) -> &mut BitBoard {
|
||||
&mut self.pieces_per_type[piece.color() as usize][piece.shape() as usize]
|
||||
}
|
||||
|
||||
pub(crate) fn bitboard_for_color<C>(&self, color: C) -> BitBoard {
|
||||
self.pieces_per_color[color as usize]
|
||||
}
|
||||
|
||||
fn bitboard_for_color_mut<C>(&mut self, color: C) -> &mut BitBoard {
|
||||
&mut self.pieces_per_color[color as usize]
|
||||
}
|
||||
|
||||
pub fn piece_on_square<C, S>(&self, sq: Square) -> Option<PlacedPiece<C, S>> {
|
||||
for color in Color::iter() {
|
||||
for shape in Shape::iter() {
|
||||
let piece = Piece::new(color, *shape);
|
||||
let bb = self.bitboard_for_piece(piece);
|
||||
if bb.has_piece_at(sq) {
|
||||
return Some(PlacedPiece::new(piece, sq));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn pieces<C>(&self, color: C) -> Pieces<ColorToMove, C> {
|
||||
Pieces::new(&self, color)
|
||||
}
|
||||
|
||||
pub fn king<C>(&self, color: C) -> PlacedPiece<C, crate::piece::King> {
|
||||
let king = Piece::king(color);
|
||||
let bitboard = self.bitboard_for_piece(king);
|
||||
let square = bitboard.occupied_squares().next().unwrap();
|
||||
PlacedPiece::new(king, square)
|
||||
}
|
||||
|
||||
pub fn is_king_in_check(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColorToMove> Position<ColorToMove> {
|
||||
/// Return a BitBoard representing the set of squares containing a piece.
|
||||
#[inline]
|
||||
pub(crate) fn occupied_squares(&self) -> BitBoard {
|
||||
self.pieces_per_color[Color::White as usize] | self.pieces_per_color[Color::Black as usize]
|
||||
}
|
||||
|
||||
/// A BitBoard representing the set of empty squares. This set is the
|
||||
/// inverse of `occupied_squares`.
|
||||
#[inline]
|
||||
pub(crate) fn empty_squares(&self) -> BitBoard {
|
||||
!self.occupied_squares()
|
||||
}
|
||||
|
||||
/// A BitBoard representing squares occupied by the current player's pieces.
|
||||
pub(crate) fn friendly_pieces(&self) -> BitBoard {
|
||||
self.bitboard_for_color(self.color_to_move)
|
||||
}
|
||||
|
||||
/// A BitBoard representing squares occupied by the opposing player's pieces.
|
||||
pub(crate) fn opposing_pieces(&self) -> BitBoard {
|
||||
self.bitboard_for_color(self.color_to_move.other())
|
||||
}
|
||||
|
||||
/// If the previous move create an en passant elibile square, return `Some`.
|
||||
/// Otherwise, return `None`.
|
||||
pub(crate) fn en_passant_square(&self) -> Option<Square> {
|
||||
self.en_passant_square
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColorToMove> Position<ColorToMove> {
|
||||
fn sight_of_pieces_of_color<C>(&self, color: C) -> BitBoard {
|
||||
self.pieces(color)
|
||||
.map(|placed_piece| {
|
||||
self.sight_of_piece(placed_piece)
|
||||
.sight(placed_piece.square(), self)
|
||||
})
|
||||
.fold(BitBoard::empty(), std::ops::BitOr::bitor)
|
||||
}
|
||||
|
||||
fn sight_of_piece<C, S>(&self, placed_piece: PlacedPiece<C, S>) -> BitBoard {
|
||||
placed_piece.piece().sight()
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColorToMove> Default for Position<ColorToMove> {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColorToMove, C, S> FromIterator<PlacedPiece<C, S>> for Position<ColorToMove> {
|
||||
fn from_iter<T: IntoIterator<Item = PlacedPiece<C, S>>>(iter: T) -> Self {
|
||||
let mut position = Position::empty();
|
||||
for placed_piece in iter {
|
||||
_ = position.place_piece(placed_piece.piece(), placed_piece.square());
|
||||
}
|
||||
|
||||
position
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColorToMove> fmt::Debug for Position<ColorToMove> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut output = String::new();
|
||||
|
||||
output.push_str("Position {\n");
|
||||
write!(output, " color_to_move: {:?},\n", self.color_to_move)?;
|
||||
|
||||
output.push_str(" pieces_per_color: [\n");
|
||||
for bb in self.pieces_per_color {
|
||||
write!(output, " {bb:?},\n")?;
|
||||
}
|
||||
output.push_str(" ],\n");
|
||||
|
||||
output.push_str(" pieces_per_type: [\n");
|
||||
for color_bbs in self.pieces_per_type {
|
||||
output.push_str(" [\n");
|
||||
for bb in color_bbs {
|
||||
write!(output, " {bb:?},\n")?;
|
||||
}
|
||||
output.push_str(" ],\n");
|
||||
}
|
||||
output.push_str(" ],\n}");
|
||||
|
||||
write!(f, "{}", output)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{piece::Shape, position::DiagramFormatter};
|
||||
|
||||
#[test]
|
||||
fn place_piece() {
|
||||
let mut position = Position::empty();
|
||||
|
||||
let piece = Piece::new(Color::White, Shape::Queen);
|
||||
let square = Square::E4;
|
||||
|
||||
position
|
||||
.place_piece(piece, square)
|
||||
.expect("Unable to place white queen on e4");
|
||||
|
||||
assert_eq!(
|
||||
position.bitboard_for_color(piece.color()).clone(),
|
||||
BitBoard::new(1 << 28)
|
||||
);
|
||||
assert_eq!(
|
||||
position.bitboard_for_piece(piece).clone(),
|
||||
BitBoard::new(1 << 28)
|
||||
);
|
||||
|
||||
position
|
||||
.place_piece(piece, square)
|
||||
.expect_err("Placed white queen on e4 a second time?!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn king_is_not_in_check() {
|
||||
let position = position![
|
||||
White King on E4,
|
||||
Black Rook on D8,
|
||||
];
|
||||
println!("{}", DiagramFormatter::new(&position));
|
||||
|
||||
assert!(!position.is_king_in_check());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn king_is_in_check() {
|
||||
let position = position![
|
||||
White King on E4,
|
||||
Black Rook on E8,
|
||||
];
|
||||
println!("{}", DiagramFormatter::new(&position));
|
||||
|
||||
assert!(position.is_king_in_check());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,423 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
//! Defines routines for computing sight of a piece. Sight is the set of squares
|
||||
//! that a piece can see. In other words, it's the set of squares attacked or
|
||||
//! controled by a piece.
|
||||
//!
|
||||
//! These functions use some common terms to describe arguments.
|
||||
//!
|
||||
//! occupancy
|
||||
//! : The set of occupied squares on the board.
|
||||
//!
|
||||
//! vacancy
|
||||
//! : The set of empty squares on the board, `!occpuancy`.
|
||||
//!
|
||||
//! blockers
|
||||
//! : The set of squares occupied by friendly pieces that block moves to that
|
||||
//! square and beyond.
|
||||
|
||||
use crate::Board;
|
||||
use chessfriend_bitboard::{BitBoard, IterationDirection};
|
||||
use chessfriend_core::{Color, Direction, Piece, Shape, Square};
|
||||
use std::ops::BitOr;
|
||||
|
||||
impl Board {
|
||||
/// Compute sight of the piece on the given square.
|
||||
pub fn sight_piece(&self, square: Square) -> BitBoard {
|
||||
if let Some(piece) = self.get_piece(square) {
|
||||
piece.sight(square, self)
|
||||
} else {
|
||||
BitBoard::EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate sight of all pieces of the given [`Color`]. If `color` is
|
||||
/// `None`, calculate sight of the active color.
|
||||
pub fn sight(&self, color: Option<Color>) -> BitBoard {
|
||||
self.sight_unwrapped(self.unwrap_color(color))
|
||||
}
|
||||
|
||||
/// Calculate sight of all pieces of the active color.
|
||||
pub fn sight_active(&self) -> BitBoard {
|
||||
self.sight_unwrapped(self.active_color())
|
||||
}
|
||||
|
||||
/// Calculate sight of all pieces of the given [`Color`].
|
||||
pub fn sight_unwrapped(&self, color: Color) -> BitBoard {
|
||||
self.friendly_occupancy(color)
|
||||
.occupied_squares_leading()
|
||||
.map(|square| self.sight_piece(square))
|
||||
.fold(BitBoard::EMPTY, BitOr::bitor)
|
||||
}
|
||||
|
||||
/// A [`BitBoard`] of all squares the given color can see.
|
||||
pub fn friendly_sight(&self, color: Color) -> BitBoard {
|
||||
// TODO: Probably want to implement a caching layer here.
|
||||
self.friendly_occupancy(color)
|
||||
.occupied_squares(&IterationDirection::default())
|
||||
.map(|square| self.sight_piece(square))
|
||||
.fold(BitBoard::EMPTY, BitOr::bitor)
|
||||
}
|
||||
|
||||
pub fn active_color_opposing_sight(&self) -> BitBoard {
|
||||
self.opposing_sight(self.active_color())
|
||||
}
|
||||
|
||||
/// A [`BitBoard`] of all squares visible by colors that oppose the given color.
|
||||
pub fn opposing_sight(&self, color: Color) -> BitBoard {
|
||||
// TODO: Probably want to implement a caching layer here.
|
||||
Color::ALL
|
||||
.into_iter()
|
||||
.filter_map(|c| {
|
||||
if c == color {
|
||||
None
|
||||
} else {
|
||||
Some(self.friendly_sight(c))
|
||||
}
|
||||
})
|
||||
.fold(BitBoard::EMPTY, BitOr::bitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Sight {
|
||||
fn sight(&self, square: Square, board: &Board) -> BitBoard;
|
||||
}
|
||||
|
||||
impl Sight for Piece {
|
||||
fn sight(&self, square: Square, board: &Board) -> BitBoard {
|
||||
let occupancy = board.occupancy();
|
||||
let info = SightInfo {
|
||||
square,
|
||||
occupancy,
|
||||
friendly_occupancy: board.friendly_occupancy(self.color),
|
||||
};
|
||||
|
||||
match self.shape {
|
||||
Shape::Pawn => {
|
||||
let en_passant_square: BitBoard = board.en_passant_target().into();
|
||||
match self.color {
|
||||
Color::White => white_pawn_sight(&info, en_passant_square),
|
||||
Color::Black => black_pawn_sight(&info, en_passant_square),
|
||||
}
|
||||
}
|
||||
Shape::Knight => knight_sight(&info),
|
||||
Shape::Bishop => bishop_sight(&info),
|
||||
Shape::Rook => rook_sight(&info),
|
||||
Shape::Queen => queen_sight(&info),
|
||||
Shape::King => king_sight(&info),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! sight_method {
|
||||
($name:ident) => {
|
||||
pub fn $name(&self, square: Square, color: Option<Color>) -> BitBoard {
|
||||
let color = self.unwrap_color(color);
|
||||
|
||||
let info = SightInfo {
|
||||
square,
|
||||
occupancy: self.occupancy(),
|
||||
friendly_occupancy: self.friendly_occupancy(color),
|
||||
};
|
||||
|
||||
$name(&info)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Board {
|
||||
sight_method!(bishop_sight);
|
||||
sight_method!(rook_sight);
|
||||
sight_method!(queen_sight);
|
||||
sight_method!(king_sight);
|
||||
}
|
||||
|
||||
struct SightInfo {
|
||||
square: Square,
|
||||
occupancy: BitBoard,
|
||||
friendly_occupancy: BitBoard,
|
||||
}
|
||||
|
||||
/// Compute sight of a white pawn.
|
||||
fn white_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard {
|
||||
let possible_squares = !info.friendly_occupancy | en_passant_square;
|
||||
|
||||
let pawn: BitBoard = info.square.into();
|
||||
let pawn = pawn.shift_north_west_one() | pawn.shift_north_east_one();
|
||||
|
||||
pawn & possible_squares
|
||||
}
|
||||
|
||||
fn black_pawn_sight(info: &SightInfo, en_passant_square: BitBoard) -> BitBoard {
|
||||
let possible_squares = !info.friendly_occupancy | en_passant_square;
|
||||
|
||||
let pawn: BitBoard = info.square.into();
|
||||
let pawn = pawn.shift_south_west_one() | pawn.shift_south_east_one();
|
||||
|
||||
pawn & possible_squares
|
||||
}
|
||||
|
||||
fn knight_sight(info: &SightInfo) -> BitBoard {
|
||||
BitBoard::knight_moves(info.square)
|
||||
}
|
||||
|
||||
fn ray_in_direction(square: Square, blockers: BitBoard, direction: Direction) -> BitBoard {
|
||||
let ray = BitBoard::ray(square, direction);
|
||||
|
||||
let ray_blockers = ray & blockers;
|
||||
if let Some(first_occupied_square) = ray_blockers.first_occupied_square_direction(direction) {
|
||||
let remainder = BitBoard::ray(first_occupied_square, direction);
|
||||
let attack_ray = ray & !remainder;
|
||||
attack_ray
|
||||
} else {
|
||||
ray
|
||||
}
|
||||
}
|
||||
|
||||
fn bishop_sight(info: &SightInfo) -> BitBoard {
|
||||
let bishop = info.square;
|
||||
let occupancy = info.occupancy;
|
||||
|
||||
let sight = ray_in_direction(bishop, occupancy, Direction::NorthEast)
|
||||
| ray_in_direction(bishop, occupancy, Direction::SouthEast)
|
||||
| ray_in_direction(bishop, occupancy, Direction::SouthWest)
|
||||
| ray_in_direction(bishop, occupancy, Direction::NorthWest);
|
||||
|
||||
sight
|
||||
}
|
||||
|
||||
fn rook_sight(info: &SightInfo) -> BitBoard {
|
||||
let rook = info.square;
|
||||
let occupancy = info.occupancy;
|
||||
|
||||
let sight = ray_in_direction(rook, occupancy, Direction::North)
|
||||
| ray_in_direction(rook, occupancy, Direction::East)
|
||||
| ray_in_direction(rook, occupancy, Direction::South)
|
||||
| ray_in_direction(rook, occupancy, Direction::West);
|
||||
|
||||
sight
|
||||
}
|
||||
|
||||
fn queen_sight(info: &SightInfo) -> BitBoard {
|
||||
let queen = info.square;
|
||||
let occupancy = info.occupancy;
|
||||
|
||||
let sight = ray_in_direction(queen, occupancy, Direction::NorthWest)
|
||||
| ray_in_direction(queen, occupancy, Direction::North)
|
||||
| ray_in_direction(queen, occupancy, Direction::NorthEast)
|
||||
| ray_in_direction(queen, occupancy, Direction::East)
|
||||
| ray_in_direction(queen, occupancy, Direction::SouthEast)
|
||||
| ray_in_direction(queen, occupancy, Direction::South)
|
||||
| ray_in_direction(queen, occupancy, Direction::SouthWest)
|
||||
| ray_in_direction(queen, occupancy, Direction::West);
|
||||
|
||||
sight
|
||||
}
|
||||
|
||||
fn king_sight(info: &SightInfo) -> BitBoard {
|
||||
BitBoard::king_moves(info.square)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_board;
|
||||
use chessfriend_bitboard::bitboard;
|
||||
use chessfriend_core::piece;
|
||||
|
||||
macro_rules! sight_test {
|
||||
($test_name:ident, $position:expr, $piece:expr, $square:expr, $bitboard:expr) => {
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
use chessfriend_core::Square;
|
||||
use $crate::sight::Sight;
|
||||
|
||||
let pos = $position;
|
||||
let piece = $piece;
|
||||
let sight = piece.sight($square, &pos);
|
||||
|
||||
assert_eq!(sight, $bitboard);
|
||||
}
|
||||
};
|
||||
($test_name:ident, $piece:expr, $square:expr, $bitboard:expr) => {
|
||||
sight_test! {
|
||||
$test_name,
|
||||
$crate::Board::empty(Some($crate::test_zobrist!())),
|
||||
$piece,
|
||||
$square,
|
||||
$bitboard
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn friendly_sight() {
|
||||
let pos = test_board!(
|
||||
White King on E4,
|
||||
);
|
||||
|
||||
let sight = pos.sight_active();
|
||||
assert_eq!(sight, bitboard![E5 F5 F4 F3 E3 D3 D4 D5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opposing_sight() {
|
||||
let pos = test_board!(
|
||||
White King on E4,
|
||||
Black Rook on E7,
|
||||
);
|
||||
|
||||
let sight = pos.active_color_opposing_sight();
|
||||
assert_eq!(sight, bitboard![A7 B7 C7 D7 F7 G7 H7 E8 E6 E5 E4]);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn pawns_and_knights_cannot_make_rays() {
|
||||
// assert_eq!(Shape::Pawn.ray_to_square(Square::F7, Square::E8), None);
|
||||
// assert_eq!(Shape::Knight.ray_to_square(Square::F6, Square::E8), None);
|
||||
// }
|
||||
|
||||
mod pawn {
|
||||
use crate::{sight::Sight, test_board};
|
||||
use chessfriend_bitboard::{BitBoard, bitboard};
|
||||
use chessfriend_core::{Square, piece};
|
||||
|
||||
sight_test!(e4_pawn, piece!(White Pawn), Square::E4, bitboard![D5 F5]);
|
||||
|
||||
sight_test!(
|
||||
e4_pawn_one_blocker,
|
||||
test_board![
|
||||
White Bishop on D5,
|
||||
White Pawn on E4,
|
||||
],
|
||||
piece!(White Pawn),
|
||||
Square::E4,
|
||||
bitboard!(F5)
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn e4_pawn_two_blocker() {
|
||||
let pos = test_board!(
|
||||
White Bishop on D5,
|
||||
White Queen on F5,
|
||||
White Pawn on E4,
|
||||
);
|
||||
|
||||
let piece = piece!(White Pawn);
|
||||
let sight = piece.sight(Square::E4, &pos);
|
||||
|
||||
assert_eq!(sight, BitBoard::EMPTY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e4_pawn_capturable() {
|
||||
let pos = test_board!(
|
||||
Black Bishop on D5,
|
||||
White Queen on F5,
|
||||
White Pawn on E4,
|
||||
);
|
||||
|
||||
let piece = piece!(White Pawn);
|
||||
let sight = piece.sight(Square::E4, &pos);
|
||||
|
||||
assert_eq!(sight, bitboard![D5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn e5_en_passant() {
|
||||
let pos = test_board!(White, [
|
||||
White Pawn on E5,
|
||||
Black Pawn on D5,
|
||||
], D6);
|
||||
let piece = piece!(White Pawn);
|
||||
let sight = piece.sight(Square::E5, &pos);
|
||||
|
||||
assert_eq!(sight, bitboard!(D6 F6));
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_use]
|
||||
mod knight {
|
||||
use chessfriend_bitboard::bitboard;
|
||||
use chessfriend_core::piece;
|
||||
|
||||
sight_test!(
|
||||
f6_knight,
|
||||
piece!(Black Knight),
|
||||
Square::F6,
|
||||
bitboard![H7 G8 E8 D7 D5 E4 G4 H5]
|
||||
);
|
||||
}
|
||||
|
||||
mod bishop {
|
||||
use super::*;
|
||||
|
||||
sight_test!(
|
||||
c2_bishop,
|
||||
piece!(Black Bishop),
|
||||
Square::C2,
|
||||
bitboard![D1 B3 A4 B1 D3 E4 F5 G6 H7]
|
||||
);
|
||||
|
||||
// #[test]
|
||||
// fn ray_to_square() {
|
||||
// let generated_ray = Shape::Bishop.ray_to_square(Square::C5, Square::E7);
|
||||
// let expected_ray = bitboard![D6 E7];
|
||||
// assert_eq!(generated_ray, Some(expected_ray));
|
||||
// }
|
||||
}
|
||||
|
||||
mod rook {
|
||||
use super::*;
|
||||
use crate::test_board;
|
||||
|
||||
sight_test!(
|
||||
g3_rook,
|
||||
piece!(White Rook),
|
||||
Square::G3,
|
||||
bitboard![G1 G2 G4 G5 G6 G7 G8 A3 B3 C3 D3 E3 F3 H3]
|
||||
);
|
||||
|
||||
sight_test!(
|
||||
e4_rook_with_e1_white_king_e7_black_king,
|
||||
test_board![
|
||||
White Rook on E4,
|
||||
White King on E2,
|
||||
Black King on E7,
|
||||
],
|
||||
piece!(White Rook),
|
||||
Square::E4,
|
||||
bitboard![A4 B4 C4 D4 F4 G4 H4 E2 E3 E5 E6 E7]
|
||||
);
|
||||
|
||||
// #[test]
|
||||
// fn ray_to_square() {
|
||||
// let generated_ray = Shape::Rook.ray_to_square(Square::C2, Square::C6);
|
||||
// let expected_ray = bitboard![C3 C4 C5 C6];
|
||||
// assert_eq!(generated_ray, Some(expected_ray));
|
||||
|
||||
// let generated_ray = Shape::Rook.ray_to_square(Square::D2, Square::H2);
|
||||
// let expected_ray = bitboard![E2 F2 G2 H2];
|
||||
// assert_eq!(generated_ray, Some(expected_ray));
|
||||
|
||||
// let generated_ray = Shape::Rook.ray_to_square(Square::G6, Square::B6);
|
||||
// let expected_ray = bitboard![B6 C6 D6 E6 F6];
|
||||
// assert_eq!(generated_ray, Some(expected_ray));
|
||||
|
||||
// let generated_ray = Shape::Rook.ray_to_square(Square::A6, Square::A3);
|
||||
// let expected_ray = bitboard![A5 A4 A3];
|
||||
// assert_eq!(generated_ray, Some(expected_ray));
|
||||
// }
|
||||
}
|
||||
|
||||
mod king {
|
||||
use chessfriend_bitboard::bitboard;
|
||||
use chessfriend_core::piece;
|
||||
|
||||
sight_test!(
|
||||
e1_king,
|
||||
piece!(White King),
|
||||
Square::E1,
|
||||
bitboard![D1 D2 E2 F2 F1]
|
||||
);
|
||||
}
|
||||
}
|
||||
334
board/src/square.rs
Normal file
334
board/src/square.rs
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
pub enum Direction {
|
||||
North,
|
||||
NorthWest,
|
||||
West,
|
||||
SouthWest,
|
||||
South,
|
||||
SouthEast,
|
||||
East,
|
||||
NorthEast,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseFileError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseSquareError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SquareOutOfBoundsError;
|
||||
|
||||
macro_rules! coordinate_enum {
|
||||
($name: ident, $($variant:ident),*) => {
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum $name {
|
||||
$($variant), *
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub const NUM: usize = [$(Self::$variant), *].len();
|
||||
pub const ALL: [Self; Self::NUM] = [$(Self::$variant), *];
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn from_index(index: usize) -> Self {
|
||||
assert!(
|
||||
index < Self::NUM,
|
||||
"Index {} out of bounds for {}.",
|
||||
index,
|
||||
stringify!($name)
|
||||
);
|
||||
Self::try_index(index).unwrap()
|
||||
}
|
||||
|
||||
pub fn try_index(index: usize) -> Option<Self> {
|
||||
$(
|
||||
#[allow(non_upper_case_globals)]
|
||||
const $variant: usize = $name::$variant as usize;
|
||||
)*
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
match index {
|
||||
$($variant => Some($name::$variant),)*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
coordinate_enum!(Rank,
|
||||
One, Two, Three, Four, Five, Six, Seven, Eight
|
||||
);
|
||||
|
||||
#[rustfmt::skip]
|
||||
coordinate_enum!(File,
|
||||
A, B, C, D, E, F, G, H
|
||||
);
|
||||
|
||||
#[rustfmt::skip]
|
||||
coordinate_enum!(Square,
|
||||
A1, B1, C1, D1, E1, F1, G1, H1,
|
||||
A2, B2, C2, D2, E2, F2, G2, H2,
|
||||
A3, B3, C3, D3, E3, F3, G3, H3,
|
||||
A4, B4, C4, D4, E4, F4, G4, H4,
|
||||
A5, B5, C5, D5, E5, F5, G5, H5,
|
||||
A6, B6, C6, D6, E6, F6, G6, H6,
|
||||
A7, B7, C7, D7, E7, F7, G7, H7,
|
||||
A8, B8, C8, D8, E8, F8, G8, H8
|
||||
);
|
||||
|
||||
impl Into<char> for File {
|
||||
fn into(self) -> char {
|
||||
('a' as u8 + self as u8) as char
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<char> for File {
|
||||
type Error = ParseFileError;
|
||||
|
||||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||
let lowercase_value = value.to_ascii_lowercase();
|
||||
for file in File::ALL.iter() {
|
||||
if lowercase_value == (*file).into() {
|
||||
return Ok(*file);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ParseFileError)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for File {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Into::<char>::into(*self).to_uppercase())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<char> for Rank {
|
||||
fn into(self) -> char {
|
||||
('1' as u8 + self as u8) as char
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<char> for Rank {
|
||||
type Error = ParseFileError;
|
||||
|
||||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||
let lowercase_value = value.to_ascii_lowercase();
|
||||
for rank in Self::ALL.iter().cloned() {
|
||||
if lowercase_value == rank.into() {
|
||||
return Ok(rank);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ParseFileError)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Rank {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Into::<char>::into(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Square {
|
||||
#[inline]
|
||||
pub fn from_file_rank(file: File, rank: Rank) -> Square {
|
||||
Self::from_index((rank as usize) << 3 | file as usize)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn file(self) -> File {
|
||||
File::from_index(self as usize & 0b000111)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rank(self) -> Rank {
|
||||
Rank::from_index(self as usize >> 3)
|
||||
}
|
||||
}
|
||||
|
||||
impl Square {
|
||||
pub(crate) const KING_STARTING_SQUARES: [Square; 2] = [Square::E1, Square::E8];
|
||||
pub(crate) const KING_CASTLE_TARGET_SQUARES: [[Square; 2]; 2] =
|
||||
[[Square::C1, Square::G1], [Square::C8, Square::G8]];
|
||||
|
||||
pub fn from_algebraic_str(s: &str) -> Result<Square, ParseSquareError> {
|
||||
s.parse()
|
||||
}
|
||||
|
||||
pub fn neighbor(self, direction: Direction) -> Option<Square> {
|
||||
match direction {
|
||||
Direction::North => Square::try_index(self as usize + 8),
|
||||
Direction::NorthWest => {
|
||||
if self.rank() != Rank::Eight {
|
||||
Square::try_index(self as usize + 7)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Direction::West => {
|
||||
if self.file() != File::A {
|
||||
Square::try_index(self as usize - 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Direction::SouthWest => {
|
||||
if self.rank() != Rank::One {
|
||||
Square::try_index(self as usize - 9)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Direction::South => {
|
||||
if self.rank() != Rank::One {
|
||||
Square::try_index(self as usize - 8)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Direction::SouthEast => {
|
||||
if self.rank() != Rank::One {
|
||||
Square::try_index(self as usize - 7)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Direction::East => {
|
||||
if self.file() != File::H {
|
||||
Square::try_index(self as usize + 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Direction::NorthEast => Square::try_index(self as usize + 9),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Square {
|
||||
type Err = ParseSquareError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut chars = s.chars();
|
||||
|
||||
let file: File = chars
|
||||
.next()
|
||||
.and_then(|c| c.try_into().ok())
|
||||
.ok_or(ParseSquareError)?;
|
||||
|
||||
let rank: Rank = chars
|
||||
.next()
|
||||
.and_then(|c| c.try_into().ok())
|
||||
.ok_or(ParseSquareError)?;
|
||||
|
||||
if !chars.next().is_none() {
|
||||
return Err(ParseSquareError);
|
||||
}
|
||||
|
||||
Ok(Square::from_file_rank(file, rank))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Square {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}{}",
|
||||
('a' as u8 + self.file() as u8) as char,
|
||||
self.rank() as usize + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn good_algebraic_input() {
|
||||
let sq = Square::from_algebraic_str("a4").expect("Failed to parse 'a4' square");
|
||||
assert_eq!(sq.file(), File::A);
|
||||
assert_eq!(sq.rank(), Rank::Four);
|
||||
|
||||
let sq = Square::from_algebraic_str("B8").expect("Failed to parse 'B8' square");
|
||||
assert_eq!(sq.file(), File::B);
|
||||
assert_eq!(sq.rank(), Rank::Eight);
|
||||
|
||||
let sq = Square::from_algebraic_str("e4").expect("Failed to parse 'B8' square");
|
||||
assert_eq!(sq.file(), File::E);
|
||||
assert_eq!(sq.rank(), Rank::Four);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_algebraic_input() {
|
||||
Square::from_algebraic_str("a0").expect_err("Got valid Square for 'a0'");
|
||||
Square::from_algebraic_str("j3").expect_err("Got valid Square for 'j3'");
|
||||
Square::from_algebraic_str("a11").expect_err("Got valid Square for 'a11'");
|
||||
Square::from_algebraic_str("b-1").expect_err("Got valid Square for 'b-1'");
|
||||
Square::from_algebraic_str("a 1").expect_err("Got valid Square for 'a 1'");
|
||||
Square::from_algebraic_str("").expect_err("Got valid Square for ''");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_index() {
|
||||
let sq = Square::try_index(4).expect("Unable to get Square from index");
|
||||
assert_eq!(sq.file(), File::E);
|
||||
assert_eq!(sq.rank(), Rank::One);
|
||||
|
||||
let sq = Square::try_index(28).expect("Unable to get Square from index");
|
||||
assert_eq!(sq.file(), File::E);
|
||||
assert_eq!(sq.rank(), Rank::Four);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_neighbors() {
|
||||
let sq = Square::E4;
|
||||
|
||||
assert_eq!(sq.neighbor(Direction::North), Some(Square::E5));
|
||||
assert_eq!(sq.neighbor(Direction::NorthEast), Some(Square::F5));
|
||||
assert_eq!(sq.neighbor(Direction::East), Some(Square::F4));
|
||||
assert_eq!(sq.neighbor(Direction::SouthEast), Some(Square::F3));
|
||||
assert_eq!(sq.neighbor(Direction::South), Some(Square::E3));
|
||||
assert_eq!(sq.neighbor(Direction::SouthWest), Some(Square::D3));
|
||||
assert_eq!(sq.neighbor(Direction::West), Some(Square::D4));
|
||||
assert_eq!(sq.neighbor(Direction::NorthWest), Some(Square::D5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_neighbors() {
|
||||
let sq = Square::A1;
|
||||
assert!(sq.neighbor(Direction::West).is_none());
|
||||
assert!(sq.neighbor(Direction::SouthWest).is_none());
|
||||
assert!(sq.neighbor(Direction::South).is_none());
|
||||
|
||||
let sq = Square::H1;
|
||||
assert!(sq.neighbor(Direction::East).is_none());
|
||||
assert!(sq.neighbor(Direction::SouthEast).is_none());
|
||||
assert!(sq.neighbor(Direction::South).is_none());
|
||||
|
||||
let sq = Square::A8;
|
||||
assert!(sq.neighbor(Direction::North).is_none());
|
||||
assert!(sq.neighbor(Direction::NorthWest).is_none());
|
||||
assert!(sq.neighbor(Direction::West).is_none());
|
||||
|
||||
let sq = Square::H8;
|
||||
assert!(sq.neighbor(Direction::North).is_none());
|
||||
assert!(sq.neighbor(Direction::NorthEast).is_none());
|
||||
assert!(sq.neighbor(Direction::East).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!(format!("{}", Square::C5), "c5");
|
||||
assert_eq!(format!("{}", Square::A1), "a1");
|
||||
assert_eq!(format!("{}", Square::H8), "h8");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
//! This module implements facilities for computing hash values of board
|
||||
//! positions via the Zobrist hashing algorithm.
|
||||
//!
|
||||
//! ## See Also
|
||||
//!
|
||||
//! * The Chess Programming Wiki page on [Zobrist Hashing][1]
|
||||
//!
|
||||
//! [1]: https://www.chessprogramming.org/Zobrist_Hashing
|
||||
|
||||
use crate::{castle, Board};
|
||||
use chessfriend_core::{random::RandomNumberGenerator, Color, File, Piece, Shape, Square};
|
||||
use rand::Fill;
|
||||
use std::sync::Arc;
|
||||
|
||||
const NUM_CASTLING_RIGHTS_VALUES: usize = 16;
|
||||
|
||||
type HashValue = u64;
|
||||
|
||||
type SquarePieceValues = [[[HashValue; Color::NUM]; Shape::NUM]; Square::NUM];
|
||||
type CastlingRightsValues = [HashValue; NUM_CASTLING_RIGHTS_VALUES];
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ZobristHash {
|
||||
// TODO: Keep this field in mind if ChessFriend grows threads. It may also
|
||||
// need a Mutex<>.
|
||||
state: Arc<ZobristState>,
|
||||
|
||||
/// The current hash value.
|
||||
hash_value: HashValue,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ZobristState {
|
||||
square_piece_values: SquarePieceValues,
|
||||
black_to_move_value: HashValue,
|
||||
castling_rights_values: CastlingRightsValues,
|
||||
en_passant_file_values: [HashValue; File::NUM],
|
||||
}
|
||||
|
||||
impl ZobristState {
|
||||
#[must_use]
|
||||
pub fn new(rng: &mut RandomNumberGenerator) -> Self {
|
||||
let square_piece_values = {
|
||||
let mut values = [[[0; Color::NUM]; Shape::NUM]; Square::NUM];
|
||||
for square in Square::ALL {
|
||||
for shape in Shape::ALL {
|
||||
for color in Color::ALL {
|
||||
values[square as usize][shape as usize][color as usize] = rng.next_u64();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
values
|
||||
};
|
||||
|
||||
let mut castling_rights_values: CastlingRightsValues = [0; NUM_CASTLING_RIGHTS_VALUES];
|
||||
castling_rights_values.fill(rng.rand());
|
||||
|
||||
let mut en_passant_file_values = [0; File::NUM];
|
||||
en_passant_file_values.fill(rng.rand());
|
||||
|
||||
Self {
|
||||
square_piece_values,
|
||||
black_to_move_value: rng.next_u64(),
|
||||
castling_rights_values,
|
||||
en_passant_file_values,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ZobristHash {
|
||||
#[must_use]
|
||||
pub fn new(state: Arc<ZobristState>) -> Self {
|
||||
Self {
|
||||
state,
|
||||
hash_value: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn hash_value(&self) -> HashValue {
|
||||
self.hash_value
|
||||
}
|
||||
|
||||
pub(crate) fn set_hash_value(&mut self, value: HashValue) {
|
||||
self.hash_value = value;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn state(&self) -> Arc<ZobristState> {
|
||||
self.state.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn compute_board_hash(board: &Board, state: &ZobristState) -> HashValue {
|
||||
let mut hash_value: HashValue = 0;
|
||||
|
||||
for (square, piece) in board.iter() {
|
||||
hash_value ^= state.square_piece_values[square as usize][piece.shape as usize]
|
||||
[piece.color as usize];
|
||||
}
|
||||
|
||||
if board.active_color() == Color::Black {
|
||||
hash_value ^= state.black_to_move_value;
|
||||
}
|
||||
|
||||
if let Some(square) = board.en_passant_target() {
|
||||
hash_value ^= state.en_passant_file_values[square.file().as_index()];
|
||||
}
|
||||
|
||||
hash_value
|
||||
}
|
||||
|
||||
pub fn recompute_hash(&mut self, board: &Board) -> HashValue {
|
||||
self.hash_value = Self::compute_board_hash(board, self.state.as_ref());
|
||||
self.hash_value
|
||||
}
|
||||
|
||||
pub fn update_adding_piece(&mut self, square: Square, piece: Piece) -> HashValue {
|
||||
self.hash_value ^= self.xor_piece_square_operand(square, piece.shape, piece.color);
|
||||
self.hash_value
|
||||
}
|
||||
|
||||
pub fn update_removing_piece(&mut self, square: Square, piece: Piece) -> HashValue {
|
||||
self.hash_value ^= self.xor_piece_square_operand(square, piece.shape, piece.color);
|
||||
self.hash_value
|
||||
}
|
||||
|
||||
pub fn update_setting_active_color(&mut self, _color: Color) -> HashValue {
|
||||
self.hash_value ^= self.state.black_to_move_value;
|
||||
self.hash_value
|
||||
}
|
||||
|
||||
pub fn update_modifying_castling_rights(
|
||||
&mut self,
|
||||
new_rights: castle::Rights,
|
||||
old_rights: castle::Rights,
|
||||
) -> HashValue {
|
||||
let state = self.state.as_ref();
|
||||
self.hash_value ^= state.castling_rights_values[new_rights.as_index()];
|
||||
self.hash_value ^= state.castling_rights_values[old_rights.as_index()];
|
||||
self.hash_value
|
||||
}
|
||||
|
||||
pub fn update_setting_en_passant_target(
|
||||
&mut self,
|
||||
old_target: Option<Square>,
|
||||
new_target: Option<Square>,
|
||||
) -> HashValue {
|
||||
let state = self.state.as_ref();
|
||||
|
||||
if let Some(old_target) = old_target {
|
||||
self.hash_value ^= state.en_passant_file_values[old_target.file().as_index()];
|
||||
}
|
||||
if let Some(new_target) = new_target {
|
||||
self.hash_value ^= state.en_passant_file_values[new_target.file().as_index()];
|
||||
}
|
||||
|
||||
self.hash_value
|
||||
}
|
||||
|
||||
fn xor_piece_square_operand(&self, square: Square, shape: Shape, color: Color) -> HashValue {
|
||||
let square = square as usize;
|
||||
let shape = shape as usize;
|
||||
let color = color as usize;
|
||||
self.state.square_piece_values[square][shape][color]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_state() -> ZobristState {
|
||||
let mut rng = RandomNumberGenerator::default();
|
||||
ZobristState::new(&mut rng)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_empty_board_ai_claude() {
|
||||
let state = test_state();
|
||||
let board = Board::empty(None);
|
||||
let hash = ZobristHash::compute_board_hash(&board, &state);
|
||||
|
||||
// Empty board with white to move should only contribute active color
|
||||
assert_eq!(hash, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_board_with_black_to_move_ai_claude() {
|
||||
let state = test_state();
|
||||
|
||||
let mut board = Board::empty(None);
|
||||
board.set_active_color(Color::Black);
|
||||
|
||||
let hash = ZobristHash::compute_board_hash(&board, &state);
|
||||
assert_eq!(hash, state.black_to_move_value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_different_en_passant_files_ai_claude() {
|
||||
let mut board1 = Board::empty(None);
|
||||
let mut board2 = Board::empty(None);
|
||||
|
||||
// Different file should produce different hash values, even if ranks
|
||||
// are the same.
|
||||
board1.set_en_passant_target(Square::A3);
|
||||
board2.set_en_passant_target(Square::H3);
|
||||
|
||||
let state = test_state();
|
||||
let hash1 = ZobristHash::compute_board_hash(&board1, &state);
|
||||
let hash2 = ZobristHash::compute_board_hash(&board2, &state);
|
||||
|
||||
assert_ne!(hash1, hash2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_en_passant_same_file_different_rank_ai_claude() {
|
||||
let mut board1 = Board::empty(None);
|
||||
let mut board2 = Board::empty(None);
|
||||
|
||||
// Same file, different ranks should produce same hash (only file matters)
|
||||
board1.set_en_passant_target(Square::E3);
|
||||
board2.set_en_passant_target(Square::E6);
|
||||
|
||||
let state = test_state();
|
||||
let hash1 = ZobristHash::compute_board_hash(&board1, &state);
|
||||
let hash2 = ZobristHash::compute_board_hash(&board2, &state);
|
||||
|
||||
assert_eq!(hash1, hash2);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
[package]
|
||||
name = "chessfriend"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{Direction, score};
|
||||
use std::fmt;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub enum Color {
|
||||
#[default]
|
||||
White = 0,
|
||||
Black = 1,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// Number of colors
|
||||
pub const NUM: usize = 2;
|
||||
|
||||
/// Slice of all possible colors
|
||||
pub const ALL: [Color; Color::NUM] = [Color::White, Color::Black];
|
||||
|
||||
pub fn iter() -> std::slice::Iter<'static, Color> {
|
||||
Color::ALL.iter()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_iter() -> std::array::IntoIter<Color, { Self::NUM }> {
|
||||
Color::ALL.into_iter()
|
||||
}
|
||||
|
||||
/// The other color
|
||||
#[must_use]
|
||||
pub const fn other(&self) -> Color {
|
||||
match self {
|
||||
Color::White => Color::Black,
|
||||
Color::Black => Color::White,
|
||||
}
|
||||
}
|
||||
|
||||
/// "Forward" direction of pawn pushes for this color.
|
||||
#[must_use]
|
||||
pub fn push_direction(&self) -> Direction {
|
||||
match self {
|
||||
Color::White => Direction::North,
|
||||
Color::Black => Direction::South,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn next(&self) -> Color {
|
||||
Self::ALL[((*self as usize) + 1) % Self::NUM]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Color::White => "white",
|
||||
Color::Black => "black",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn score_factor(self) -> score::Value {
|
||||
match self {
|
||||
Color::White => 1,
|
||||
Color::Black => -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Color {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Color::White => "white",
|
||||
Color::Black => "black",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
|
||||
#[error("no matching color for character '{0}'")]
|
||||
pub struct ColorFromCharError(char);
|
||||
|
||||
impl TryFrom<char> for Color {
|
||||
type Error = ColorFromCharError;
|
||||
|
||||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
'w' | 'W' => Ok(Color::White),
|
||||
'b' | 'B' => Ok(Color::Black),
|
||||
_ => Err(ColorFromCharError(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
|
||||
#[error("no matching color for string")]
|
||||
pub struct ColorFromStrError;
|
||||
|
||||
impl TryFrom<&str> for Color {
|
||||
type Error = ColorFromStrError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
"w" | "white" => Ok(Color::White),
|
||||
"b" | "black" => Ok(Color::Black),
|
||||
_ => Err(ColorFromStrError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Color {
|
||||
type Err = ColorFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s.to_lowercase().as_str())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,612 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod wings;
|
||||
|
||||
pub use wings::Wing;
|
||||
|
||||
use crate::Color;
|
||||
use std::{fmt, str::FromStr};
|
||||
use thiserror::Error;
|
||||
|
||||
macro_rules! try_from_integer {
|
||||
($type:ident, $int_type:ident) => {
|
||||
impl TryFrom<$int_type> for $type {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: $int_type) -> Result<Self, Self::Error> {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
Self::try_from(value as u8)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! coordinate_enum {
|
||||
($name: ident, [ $($variant:ident),* ]) => {
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum $name {
|
||||
$($variant), *
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub const NUM: usize = [$(Self::$variant), *].len();
|
||||
pub const ALL: [Self; Self::NUM] = [$(Self::$variant), *];
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for $name {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
let value_usize = value as usize;
|
||||
if value_usize < Self::NUM {
|
||||
Ok($name::ALL[value_usize])
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try_from_integer!($name, u16);
|
||||
try_from_integer!($name, u32);
|
||||
try_from_integer!($name, u64);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! range_bound_struct {
|
||||
($vis:vis $type:ident, $repr:ty, $max:expr) => {
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
$vis struct $type($repr);
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl $type {
|
||||
$vis const NUM: usize = $max;
|
||||
$vis const FIRST: $type = $type(0);
|
||||
$vis const LAST: $type = $type($max - 1);
|
||||
}
|
||||
|
||||
impl $type {
|
||||
#[must_use]
|
||||
$vis fn new(x: $repr) -> Option<Self> {
|
||||
if x < $max {
|
||||
Some(Self(x))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `Self`
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function does not perform any bounds checking. It should only be called when
|
||||
/// the input is already known to be within bounds, i.e. when `x >= Self::FIRST && x < Self::LAST`.
|
||||
#[must_use]
|
||||
$vis unsafe fn new_unchecked(x: $repr) -> Self {
|
||||
debug_assert!((Self::FIRST.0..=Self::LAST.0).contains(&x));
|
||||
Self(x)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
$vis const fn as_index(&self) -> usize {
|
||||
self.0 as usize
|
||||
}
|
||||
|
||||
$vis fn iter(&self) -> impl Iterator<Item = Self> {
|
||||
(Self::FIRST.0..=Self::LAST.0).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$type> for $repr {
|
||||
fn from(x: $type) -> Self {
|
||||
x.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<$repr> for $type {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: $repr) -> Result<Self, Self::Error> {
|
||||
Self::new(value).ok_or(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coordinate_enum!(
|
||||
Direction,
|
||||
[
|
||||
North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest
|
||||
]
|
||||
);
|
||||
|
||||
impl Direction {
|
||||
#[must_use]
|
||||
pub fn to_offset(&self) -> i8 {
|
||||
const OFFSETS: [i8; 8] = [8, 9, 1, -7, -8, -9, -1, 7];
|
||||
OFFSETS[*self as usize]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn opposite(&self) -> Direction {
|
||||
const OPPOSITES: [Direction; 8] = [
|
||||
Direction::South,
|
||||
Direction::SouthEast,
|
||||
Direction::East,
|
||||
Direction::NorthEast,
|
||||
Direction::North,
|
||||
Direction::NorthWest,
|
||||
Direction::West,
|
||||
Direction::SouthWest,
|
||||
];
|
||||
|
||||
OPPOSITES[*self as usize]
|
||||
}
|
||||
}
|
||||
|
||||
range_bound_struct!(pub File, u8, 8);
|
||||
|
||||
impl File {
|
||||
pub const A: File = File(0);
|
||||
pub const B: File = File(1);
|
||||
pub const C: File = File(2);
|
||||
pub const D: File = File(3);
|
||||
pub const E: File = File(4);
|
||||
pub const F: File = File(5);
|
||||
pub const G: File = File(6);
|
||||
pub const H: File = File(7);
|
||||
|
||||
pub const ALL: [File; File::NUM] = [
|
||||
File::A,
|
||||
File::B,
|
||||
File::C,
|
||||
File::D,
|
||||
File::E,
|
||||
File::F,
|
||||
File::G,
|
||||
File::H,
|
||||
];
|
||||
}
|
||||
|
||||
range_bound_struct!(pub Rank, u8, 8);
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Rank {
|
||||
pub const ONE: Rank = Rank(0);
|
||||
pub const TWO: Rank = Rank(1);
|
||||
pub const THREE: Rank = Rank(2);
|
||||
pub const FOUR: Rank = Rank(3);
|
||||
pub const FIVE: Rank = Rank(4);
|
||||
pub const SIX: Rank = Rank(5);
|
||||
pub const SEVEN: Rank = Rank(6);
|
||||
pub const EIGHT: Rank = Rank(7);
|
||||
|
||||
pub const ALL: [Rank; Self::NUM] = [
|
||||
Rank::ONE,
|
||||
Rank::TWO,
|
||||
Rank::THREE,
|
||||
Rank::FOUR,
|
||||
Rank::FIVE,
|
||||
Rank::SIX,
|
||||
Rank::SEVEN,
|
||||
Rank::EIGHT,
|
||||
];
|
||||
|
||||
/// Ranks on which pawns start, by color.
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_core::{Color, Rank};
|
||||
/// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::White as usize], Rank::TWO);
|
||||
/// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::Black as usize], Rank::SEVEN);
|
||||
/// ```
|
||||
pub const PAWN_STARTING_RANKS: [Rank; 2] = [Rank::TWO, Rank::SEVEN];
|
||||
|
||||
pub const PAWN_DOUBLE_PUSH_TARGET_RANKS: [Rank; 2] = [Rank::FOUR, Rank::FIVE];
|
||||
|
||||
#[must_use]
|
||||
pub fn is_pawn_starting_rank(&self, color: Color) -> bool {
|
||||
self == &Self::PAWN_STARTING_RANKS[color as usize]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool {
|
||||
self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize]
|
||||
}
|
||||
|
||||
/// Ranks where promotions happen.
|
||||
#[must_use]
|
||||
pub fn is_promotable_rank(&self) -> bool {
|
||||
matches!(*self, Rank::ONE | Rank::EIGHT)
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
coordinate_enum!(Square, [
|
||||
A1, B1, C1, D1, E1, F1, G1, H1,
|
||||
A2, B2, C2, D2, E2, F2, G2, H2,
|
||||
A3, B3, C3, D3, E3, F3, G3, H3,
|
||||
A4, B4, C4, D4, E4, F4, G4, H4,
|
||||
A5, B5, C5, D5, E5, F5, G5, H5,
|
||||
A6, B6, C6, D6, E6, F6, G6, H6,
|
||||
A7, B7, C7, D7, E7, F7, G7, H7,
|
||||
A8, B8, C8, D8, E8, F8, G8, H8
|
||||
]);
|
||||
|
||||
/// Generate an enum that maps its values to variants of [Square].
|
||||
macro_rules! to_square_enum {
|
||||
($vis:vis $name:ident { $($variant:ident)* }) => {
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
$vis enum $name {
|
||||
$($variant = Square::$variant as u8,)*
|
||||
}
|
||||
|
||||
impl From<$name> for Square {
|
||||
fn from(value: $name) -> Self {
|
||||
unsafe { Square::from_index_unchecked(value as u8) }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
to_square_enum!(
|
||||
pub EnPassantTargetSquare {
|
||||
A3 B3 C3 D3 E3 F3 G3 H3
|
||||
A6 B6 C6 D6 E6 F6 G6 H6
|
||||
}
|
||||
);
|
||||
|
||||
impl Square {
|
||||
/// # Safety
|
||||
///
|
||||
/// This function does not do any bounds checking on the input. In debug
|
||||
/// builds, this function will assert that the argument is in bounds.
|
||||
#[must_use]
|
||||
pub unsafe fn from_index_unchecked(x: u8) -> Square {
|
||||
debug_assert!((x as usize) < Self::NUM);
|
||||
Self::ALL[x as usize]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn from_file_rank(file: File, rank: Rank) -> Square {
|
||||
let file_int: u8 = file.into();
|
||||
let rank_int: u8 = rank.into();
|
||||
unsafe { Self::from_index_unchecked(rank_int << 3 | file_int) }
|
||||
}
|
||||
|
||||
pub fn from_algebraic_str(s: &str) -> Result<Square, ParseSquareError> {
|
||||
s.parse()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn file(self) -> File {
|
||||
unsafe { File::new_unchecked((self as u8) & 0b000_00111) }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn rank(self) -> Rank {
|
||||
unsafe { Rank::new_unchecked((self as u8) >> 3) }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn file_rank(&self) -> (File, Rank) {
|
||||
(self.file(), self.rank())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn neighbor(self, direction: Direction) -> Option<Square> {
|
||||
match direction {
|
||||
Direction::North => {
|
||||
if self.rank() == Rank::EIGHT {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Direction::NorthEast => {
|
||||
let (file, rank) = self.file_rank();
|
||||
if rank == Rank::EIGHT || file == File::H {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Direction::East => {
|
||||
if self.file() == File::H {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Direction::SouthEast => {
|
||||
let (file, rank) = self.file_rank();
|
||||
if rank == Rank::ONE || file == File::H {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Direction::South => {
|
||||
if self.rank() == Rank::ONE {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Direction::SouthWest => {
|
||||
let (file, rank) = self.file_rank();
|
||||
if rank == Rank::ONE || file == File::A {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Direction::West => {
|
||||
if self.file() == File::A {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Direction::NorthWest => {
|
||||
let (file, rank) = self.file_rank();
|
||||
if rank == Rank::EIGHT || file == File::A {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let index: u8 = self as u8;
|
||||
let direction = direction.to_offset();
|
||||
Square::try_from(index.wrapping_add_signed(direction)).ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
pub enum ParseSquareError {
|
||||
#[error("{0}")]
|
||||
RankError(#[from] ParseRankError),
|
||||
|
||||
#[error("{0}")]
|
||||
FileError(#[from] ParseFileError),
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Square {
|
||||
type Error = ParseSquareError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let mut chars = value.chars();
|
||||
|
||||
let file: File = chars
|
||||
.next()
|
||||
.and_then(|c| c.try_into().ok())
|
||||
.ok_or(ParseSquareError::FileError(ParseFileError))?;
|
||||
|
||||
let rank: Rank = chars
|
||||
.next()
|
||||
.and_then(|c| c.try_into().ok())
|
||||
.ok_or(ParseSquareError::RankError(ParseRankError))?;
|
||||
|
||||
Ok(Square::from_file_rank(file, rank))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Square {
|
||||
type Err = ParseSquareError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut chars = s.chars();
|
||||
|
||||
let file: File = chars
|
||||
.next()
|
||||
.and_then(|c| c.try_into().ok())
|
||||
.ok_or(ParseSquareError::FileError(ParseFileError))?;
|
||||
|
||||
let rank: Rank = chars
|
||||
.next()
|
||||
.and_then(|c| c.try_into().ok())
|
||||
.ok_or(ParseSquareError::RankError(ParseRankError))?;
|
||||
|
||||
Ok(Square::from_file_rank(file, rank))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
#[error("invalid rank")]
|
||||
pub struct ParseRankError;
|
||||
|
||||
impl FromStr for Rank {
|
||||
type Err = ParseRankError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let ch = s
|
||||
.chars()
|
||||
.nth(0)
|
||||
.ok_or(ParseRankError)
|
||||
.map(|ch| ch.to_ascii_lowercase())?;
|
||||
|
||||
let offset = 'a' as usize - (ch as usize);
|
||||
if offset >= Rank::ALL.len() {
|
||||
return Err(ParseRankError);
|
||||
}
|
||||
|
||||
Ok(Rank::ALL[offset])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
#[error("invalid file")]
|
||||
pub struct ParseFileError;
|
||||
|
||||
impl FromStr for File {
|
||||
type Err = ParseFileError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let ch = s
|
||||
.chars()
|
||||
.nth(0)
|
||||
.ok_or(ParseFileError)
|
||||
.map(|ch| ch.to_ascii_lowercase())?;
|
||||
|
||||
let offset = '1' as usize - (ch as usize);
|
||||
if offset >= File::ALL.len() {
|
||||
return Err(ParseFileError);
|
||||
}
|
||||
|
||||
Ok(File::ALL[offset])
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for File {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Into::<char>::into(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Rank {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Into::<char>::into(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Square {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.file().fmt(f)?;
|
||||
self.rank().fmt(f)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for char {
|
||||
fn from(value: File) -> Self {
|
||||
let u8value: u8 = value.into();
|
||||
(u8value + b'a') as char
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rank> for char {
|
||||
fn from(value: Rank) -> Self {
|
||||
Self::from(value.0 + b'1')
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<char> for File {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||
File::try_from(value.to_ascii_lowercase() as u8 - b'a')
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<char> for Rank {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||
let result = (value as u8).checked_sub(b'1').ok_or(())?;
|
||||
Self::try_from(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn direction_offsets() {
|
||||
assert_eq!(Direction::North.to_offset(), 8);
|
||||
assert_eq!(Direction::NorthEast.to_offset(), 9);
|
||||
assert_eq!(Direction::East.to_offset(), 1);
|
||||
assert_eq!(Direction::SouthEast.to_offset(), -7);
|
||||
assert_eq!(Direction::South.to_offset(), -8);
|
||||
assert_eq!(Direction::SouthWest.to_offset(), -9);
|
||||
assert_eq!(Direction::West.to_offset(), -1);
|
||||
assert_eq!(Direction::NorthWest.to_offset(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn good_algebraic_input() -> Result<(), ParseSquareError> {
|
||||
let sq = Square::from_algebraic_str("a4")?;
|
||||
assert_eq!(sq.file(), File::A);
|
||||
assert_eq!(sq.rank(), Rank::FOUR);
|
||||
|
||||
let sq = Square::from_algebraic_str("B8")?;
|
||||
assert_eq!(sq.file(), File::B);
|
||||
assert_eq!(sq.rank(), Rank::EIGHT);
|
||||
|
||||
let sq = Square::from_algebraic_str("e4")?;
|
||||
assert_eq!(sq.file(), File::E);
|
||||
assert_eq!(sq.rank(), Rank::FOUR);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_algebraic_input() {
|
||||
assert!("a0".parse::<Square>().is_err());
|
||||
assert!("j3".parse::<Square>().is_err());
|
||||
assert!("a9".parse::<Square>().is_err());
|
||||
assert!("b-1".parse::<Square>().is_err());
|
||||
assert!("a 1".parse::<Square>().is_err());
|
||||
assert!("".parse::<Square>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_index() -> Result<(), ()> {
|
||||
let sq = Square::try_from(4u32)?;
|
||||
assert_eq!(sq.file(), File::E);
|
||||
assert_eq!(sq.rank(), Rank::ONE);
|
||||
|
||||
let sq = Square::try_from(28u32)?;
|
||||
assert_eq!(sq.file(), File::E);
|
||||
assert_eq!(sq.rank(), Rank::FOUR);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_index() {
|
||||
assert_eq!(Square::A1 as usize, 0);
|
||||
assert_eq!(Square::H8 as usize, 63);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_neighbors() {
|
||||
let sq = Square::E4;
|
||||
|
||||
assert_eq!(sq.neighbor(Direction::North), Some(Square::E5));
|
||||
assert_eq!(sq.neighbor(Direction::NorthEast), Some(Square::F5));
|
||||
assert_eq!(sq.neighbor(Direction::East), Some(Square::F4));
|
||||
assert_eq!(sq.neighbor(Direction::SouthEast), Some(Square::F3));
|
||||
assert_eq!(sq.neighbor(Direction::South), Some(Square::E3));
|
||||
assert_eq!(sq.neighbor(Direction::SouthWest), Some(Square::D3));
|
||||
assert_eq!(sq.neighbor(Direction::West), Some(Square::D4));
|
||||
assert_eq!(sq.neighbor(Direction::NorthWest), Some(Square::D5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_neighbors() {
|
||||
let sq = Square::A1;
|
||||
assert!(sq.neighbor(Direction::West).is_none());
|
||||
assert!(sq.neighbor(Direction::SouthWest).is_none());
|
||||
assert!(sq.neighbor(Direction::South).is_none());
|
||||
|
||||
let sq = Square::H1;
|
||||
assert!(sq.neighbor(Direction::East).is_none());
|
||||
assert!(sq.neighbor(Direction::SouthEast).is_none());
|
||||
assert!(sq.neighbor(Direction::South).is_none());
|
||||
|
||||
let sq = Square::A8;
|
||||
assert!(sq.neighbor(Direction::North).is_none());
|
||||
assert!(sq.neighbor(Direction::NorthWest).is_none());
|
||||
assert!(sq.neighbor(Direction::West).is_none());
|
||||
|
||||
let sq = Square::H8;
|
||||
assert!(sq.neighbor(Direction::North).is_none());
|
||||
assert!(sq.neighbor(Direction::NorthEast).is_none());
|
||||
assert!(sq.neighbor(Direction::East).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!(format!("{}", Square::C5), "c5");
|
||||
assert_eq!(format!("{}", Square::A1), "a1");
|
||||
assert_eq!(format!("{}", Square::H8), "h8");
|
||||
assert_eq!(format!("{}", Rank::FIVE), "5");
|
||||
assert_eq!(format!("{}", File::H), "h");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_char() {
|
||||
assert_eq!(Into::<char>::into(Rank::ONE), '1');
|
||||
assert_eq!(Into::<char>::into(File::B), 'b');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Wing {
|
||||
KingSide = 0,
|
||||
QueenSide = 1,
|
||||
}
|
||||
|
||||
impl Wing {
|
||||
pub const NUM: usize = 2;
|
||||
pub const ALL: [Wing; Self::NUM] = [Self::KingSide, Self::QueenSide];
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Wing {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Wing::KingSide => write!(f, "kingside"),
|
||||
Wing::QueenSide => write!(f, "queenside"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
pub mod colors;
|
||||
pub mod coordinates;
|
||||
pub mod pieces;
|
||||
pub mod random;
|
||||
pub mod score;
|
||||
pub mod shapes;
|
||||
|
||||
mod macros;
|
||||
|
||||
pub use colors::Color;
|
||||
pub use coordinates::{Direction, File, Rank, Square, Wing};
|
||||
pub use pieces::Piece;
|
||||
pub use shapes::{Shape, Slider};
|
||||
|
||||
pub mod errors {
|
||||
pub use crate::coordinates::ParseSquareError;
|
||||
pub use crate::shapes::ParseShapeError;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! piece {
|
||||
($color:ident $shape:ident) => {
|
||||
$crate::Piece::new($crate::Color::$color, $crate::Shape::$shape)
|
||||
};
|
||||
($color:ident $shape:ident on $square:ident) => {
|
||||
$crate::PlacedPiece::new(piece!($color $shape), $crate::Square::$square)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod display;
|
||||
|
||||
pub use self::display::{PieceDisplay, PieceDisplayStyle};
|
||||
|
||||
use crate::{Color, Shape};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Piece {
|
||||
pub color: Color,
|
||||
pub shape: Shape,
|
||||
}
|
||||
|
||||
macro_rules! piece_constructor {
|
||||
($func_name:ident, $type:tt) => {
|
||||
#[must_use]
|
||||
pub fn $func_name(color: Color) -> Piece {
|
||||
Piece {
|
||||
color,
|
||||
shape: Shape::$type,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! is_shape {
|
||||
($func_name:ident, $shape:ident) => {
|
||||
#[must_use]
|
||||
pub fn $func_name(&self) -> bool {
|
||||
self.shape == Shape::$shape
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Piece {
|
||||
#[must_use]
|
||||
pub fn new(color: Color, shape: Shape) -> Piece {
|
||||
Piece { color, shape }
|
||||
}
|
||||
|
||||
piece_constructor!(pawn, Pawn);
|
||||
piece_constructor!(knight, Knight);
|
||||
piece_constructor!(bishop, Bishop);
|
||||
piece_constructor!(rook, Rook);
|
||||
piece_constructor!(queen, Queen);
|
||||
piece_constructor!(king, King);
|
||||
|
||||
is_shape!(is_pawn, Pawn);
|
||||
is_shape!(is_knight, Knight);
|
||||
is_shape!(is_bishop, Bishop);
|
||||
is_shape!(is_rook, Rook);
|
||||
is_shape!(is_queen, Queen);
|
||||
is_shape!(is_king, King);
|
||||
|
||||
#[must_use]
|
||||
pub fn to_ascii(self) -> char {
|
||||
let ch = self.shape.to_ascii();
|
||||
match self.color {
|
||||
Color::White => ch,
|
||||
Color::Black => ch.to_ascii_lowercase(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_unicode(self) -> char {
|
||||
match (self.color, self.shape) {
|
||||
(Color::Black, Shape::Pawn) => '♟',
|
||||
(Color::Black, Shape::Knight) => '♞',
|
||||
(Color::Black, Shape::Bishop) => '♝',
|
||||
(Color::Black, Shape::Rook) => '♜',
|
||||
(Color::Black, Shape::Queen) => '♛',
|
||||
(Color::Black, Shape::King) => '♚',
|
||||
(Color::White, Shape::Pawn) => '♙',
|
||||
(Color::White, Shape::Knight) => '♘',
|
||||
(Color::White, Shape::Bishop) => '♗',
|
||||
(Color::White, Shape::Rook) => '♖',
|
||||
(Color::White, Shape::Queen) => '♕',
|
||||
(Color::White, Shape::King) => '♔',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Piece {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
PieceDisplay {
|
||||
piece: *self,
|
||||
style: PieceDisplayStyle::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_shape() {
|
||||
assert_eq!("p".parse(), Ok(Shape::Pawn));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_into_char() {
|
||||
assert_eq!(<Shape as Into<char>>::into(Shape::Pawn), 'P');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::Piece;
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum PieceDisplayStyle {
|
||||
#[default]
|
||||
Unicode,
|
||||
ASCII,
|
||||
LongForm,
|
||||
}
|
||||
|
||||
pub struct PieceDisplay {
|
||||
pub(super) piece: Piece,
|
||||
pub(super) style: PieceDisplayStyle,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PieceDisplay {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.style {
|
||||
PieceDisplayStyle::Unicode => write!(f, "{}", self.piece.to_unicode()),
|
||||
PieceDisplayStyle::ASCII => write!(f, "{}", self.piece.to_ascii()),
|
||||
PieceDisplayStyle::LongForm => {
|
||||
write!(f, "{} {}", self.piece.color.name(), self.piece.shape.name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use rand::{rngs::StdRng, RngCore, SeedableRng};
|
||||
|
||||
pub struct RandomNumberGenerator {
|
||||
rng: StdRng,
|
||||
}
|
||||
|
||||
impl RandomNumberGenerator {
|
||||
/// A default value for hashing chess games.
|
||||
///
|
||||
/// This value is the SHA-256 hash of a PGN I downloaded from the internet
|
||||
/// of the six games Kasparov played against Deep Blue in 1996.
|
||||
#[rustfmt::skip]
|
||||
const DEFAULT_SEED_VALUE: [u8; 32] = [
|
||||
0x22, 0x0b, 0xae, 0xd5, 0xc5, 0xb8, 0x20, 0xb1,
|
||||
0xee, 0x05, 0x4b, 0x72, 0x73, 0x4c, 0x1a, 0xf9,
|
||||
0xc2, 0xb7, 0x5d, 0x87, 0xc8, 0xa9, 0x44, 0x87,
|
||||
0x01, 0xda, 0xdb, 0xe0, 0x8e, 0xf8, 0xe8, 0x77,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub fn new(seed: <StdRng as SeedableRng>::Seed) -> Self {
|
||||
let rng = StdRng::from_seed(seed);
|
||||
|
||||
Self { rng }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn rand(&mut self) -> &mut StdRng {
|
||||
&mut self.rng
|
||||
}
|
||||
|
||||
pub fn next_u64(&mut self) -> u64 {
|
||||
self.rng.next_u64()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RandomNumberGenerator {
|
||||
fn default() -> Self {
|
||||
let rng = StdRng::from_seed(Self::DEFAULT_SEED_VALUE);
|
||||
Self { rng }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign},
|
||||
};
|
||||
|
||||
pub(crate) type Value = i32;
|
||||
|
||||
/// A score for a position in centipawns.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Score(Value);
|
||||
|
||||
impl Score {
|
||||
pub const ZERO: Score = Score(0);
|
||||
|
||||
/// The minimum possible value of a score. Notably, this is *not* the
|
||||
/// minimum value for the inner integer value so negation works correctly.
|
||||
/// This property is important during search, which relies on being able to
|
||||
/// negate "infinity".
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_core::score::Score;
|
||||
/// assert_eq!(-Score::MIN, Score::MAX);
|
||||
/// ```
|
||||
///
|
||||
pub const MIN: Score = Score(Value::MIN + 1);
|
||||
|
||||
/// The maximum possible value of a score.
|
||||
pub const MAX: Score = Score(Value::MAX);
|
||||
|
||||
pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0;
|
||||
|
||||
#[must_use]
|
||||
pub const fn new(value: Value) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Returns `true` if this [`Score`] is zero.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use chessfriend_core::score::Score;
|
||||
/// assert!(Score::ZERO.is_zero());
|
||||
/// assert!(Score::new(0).is_zero());
|
||||
/// ```
|
||||
///
|
||||
#[must_use]
|
||||
pub const fn is_zero(&self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Score {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Score(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for Score {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.0 += rhs.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Score {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Score(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign for Score {
|
||||
fn sub_assign(&mut self, rhs: Self) {
|
||||
self.0 -= rhs.0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Value> for Score {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, rhs: Value) -> Self::Output {
|
||||
Score(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Score> for Value {
|
||||
type Output = Score;
|
||||
|
||||
fn mul(self, rhs: Score) -> Self::Output {
|
||||
Score(self * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Score {
|
||||
type Output = Self;
|
||||
|
||||
fn neg(self) -> Self::Output {
|
||||
Score(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value> for Score {
|
||||
fn from(value: Value) -> Self {
|
||||
Score(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Score {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let value = self.0;
|
||||
if *self == Self::MAX {
|
||||
write!(f, "INF")
|
||||
} else if *self == Self::MIN {
|
||||
write!(f, "-INF")
|
||||
} else {
|
||||
write!(f, "{value}cp")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use std::{array, fmt, slice, str::FromStr};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::score::Score;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Shape {
|
||||
Pawn = 0,
|
||||
Knight = 1,
|
||||
Bishop = 2,
|
||||
Rook = 3,
|
||||
Queen = 4,
|
||||
King = 5,
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
/// Number of piece shapes
|
||||
pub const NUM: usize = 6;
|
||||
|
||||
/// A slice of all piece shapes
|
||||
pub const ALL: [Shape; Self::NUM] = [
|
||||
Shape::Pawn,
|
||||
Shape::Knight,
|
||||
Shape::Bishop,
|
||||
Shape::Rook,
|
||||
Shape::Queen,
|
||||
Shape::King,
|
||||
];
|
||||
|
||||
const PROMOTABLE_SHAPES: [Shape; 4] = [Shape::Queen, Shape::Rook, Shape::Bishop, Shape::Knight];
|
||||
|
||||
pub fn iter() -> slice::Iter<'static, Self> {
|
||||
Shape::ALL.iter()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_iter() -> array::IntoIter<Self, { Self::NUM }> {
|
||||
Shape::ALL.into_iter()
|
||||
}
|
||||
|
||||
/// An iterator over the shapes that a pawn can promote to
|
||||
pub fn promotable() -> slice::Iter<'static, Shape> {
|
||||
Self::PROMOTABLE_SHAPES.iter()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn to_ascii(self) -> char {
|
||||
match self {
|
||||
Shape::Pawn => 'P',
|
||||
Shape::Knight => 'N',
|
||||
Shape::Bishop => 'B',
|
||||
Shape::Rook => 'R',
|
||||
Shape::Queen => 'Q',
|
||||
Shape::King => 'K',
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Shape::Pawn => "pawn",
|
||||
Shape::Knight => "knight",
|
||||
Shape::Bishop => "bishop",
|
||||
Shape::Rook => "rook",
|
||||
Shape::Queen => "queen",
|
||||
Shape::King => "king",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn is_promotable(&self) -> bool {
|
||||
matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn score(self) -> Score {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
const CP_PER_PT: i32 = Score::CENTIPAWNS_PER_POINT as i32;
|
||||
|
||||
match self {
|
||||
Shape::Pawn => Score::new(CP_PER_PT),
|
||||
Shape::Knight | Shape::Bishop => Score::new(3 * CP_PER_PT),
|
||||
Shape::Rook => Score::new(5 * CP_PER_PT),
|
||||
Shape::Queen => Score::new(9 * CP_PER_PT),
|
||||
Shape::King => Score::new(200 * CP_PER_PT),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Slider {
|
||||
Bishop,
|
||||
Rook,
|
||||
Queen,
|
||||
}
|
||||
|
||||
impl From<Slider> for Shape {
|
||||
fn from(value: Slider) -> Self {
|
||||
match value {
|
||||
Slider::Bishop => Shape::Bishop,
|
||||
Slider::Rook => Shape::Rook,
|
||||
Slider::Queen => Shape::Queen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Shape> for Slider {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Shape) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Shape::Bishop => Ok(Slider::Bishop),
|
||||
Shape::Rook => Ok(Slider::Rook),
|
||||
Shape::Queen => Ok(Slider::Queen),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
|
||||
#[error("no matching piece shape for character '{0:?}'")]
|
||||
pub struct ShapeFromCharError(char);
|
||||
|
||||
impl TryFrom<char> for Shape {
|
||||
type Error = ShapeFromCharError;
|
||||
|
||||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
'P' | 'p' => Ok(Shape::Pawn),
|
||||
'N' | 'n' => Ok(Shape::Knight),
|
||||
'B' | 'b' => Ok(Shape::Bishop),
|
||||
'R' | 'r' => Ok(Shape::Rook),
|
||||
'Q' | 'q' => Ok(Shape::Queen),
|
||||
'K' | 'k' => Ok(Shape::King),
|
||||
_ => Err(ShapeFromCharError(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
|
||||
#[error("no matching piece shape for string")]
|
||||
pub struct ParseShapeError;
|
||||
|
||||
impl FromStr for Shape {
|
||||
type Err = ParseShapeError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"p" | "pawn" => Ok(Shape::Pawn),
|
||||
"n" | "knight" => Ok(Shape::Knight),
|
||||
"b" | "bishop" => Ok(Shape::Bishop),
|
||||
"r" | "rook" => Ok(Shape::Rook),
|
||||
"q" | "queen" => Ok(Shape::Queen),
|
||||
"k" | "king" => Ok(Shape::King),
|
||||
_ => Err(ParseShapeError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Shape> for char {
|
||||
fn from(shape: &Shape) -> char {
|
||||
char::from(*shape)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Shape> for char {
|
||||
fn from(shape: Shape) -> char {
|
||||
shape.to_ascii()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Shape {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let self_char: char = self.into();
|
||||
write!(f, "{self_char}")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,31 +1,14 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
use board::piece::{Color, Piece, Shape};
|
||||
use board::position::DiagramFormatter;
|
||||
use board::{Position, Square};
|
||||
use clap::{Arg, Command};
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::DefaultEditor;
|
||||
|
||||
use chessfriend_board::{Board, ZobristState, fen::FromFenStr};
|
||||
use chessfriend_core::{Color, Piece, Shape, Square, Wing, random::RandomNumberGenerator};
|
||||
use chessfriend_moves::{GeneratedMove, ValidateMove, algebraic::AlgebraicMoveComponents};
|
||||
use chessfriend_position::{PlacePieceStrategy, Position, fen::ToFenStr};
|
||||
use clap::{Arg, Command, value_parser};
|
||||
use rustyline::{DefaultEditor, error::ReadlineError};
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
struct CommandResult {
|
||||
should_continue: bool,
|
||||
should_print_position: bool,
|
||||
}
|
||||
|
||||
impl Default for CommandResult {
|
||||
fn default() -> Self {
|
||||
CommandResult {
|
||||
should_continue: true,
|
||||
should_print_position: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
position: Position,
|
||||
zobrist: Arc<ZobristState>,
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum ShouldContinue {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
fn command_line() -> Command {
|
||||
|
|
@ -34,366 +17,73 @@ fn command_line() -> Command {
|
|||
{all-args}
|
||||
";
|
||||
|
||||
// strip out name/version
|
||||
const APPLET_TEMPLATE: &str = "\
|
||||
{about-with-newline}\n\
|
||||
{usage-heading}\n {usage}\n\
|
||||
\n\
|
||||
{all-args}{after-help}\
|
||||
";
|
||||
|
||||
Command::new("explorer")
|
||||
.multicall(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand_required(true)
|
||||
.subcommand_value_name("CMD")
|
||||
.subcommand_help_heading("COMMANDS")
|
||||
.subcommand_value_name("APPLET")
|
||||
.subcommand_help_heading("APPLETS")
|
||||
.help_template(PARSER_TEMPLATE)
|
||||
.subcommand(Command::new("fen").about("Print the current position as a FEN string"))
|
||||
.subcommand(Command::new("flags").about("Print flags for the current position"))
|
||||
.subcommand(
|
||||
Command::new("load")
|
||||
.arg(Arg::new("fen").required(true))
|
||||
.alias("l")
|
||||
.about("Load a board position from a FEN string"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("make")
|
||||
.arg(Arg::new("move").required(true))
|
||||
.alias("m")
|
||||
.about("Make a move"),
|
||||
)
|
||||
.subcommand(Command::new("print").about("Print the board"))
|
||||
.subcommand(
|
||||
Command::new("place")
|
||||
.arg(Arg::new("color").required(true))
|
||||
.arg(Arg::new("piece").required(true))
|
||||
.arg(Arg::new("square").required(true))
|
||||
.alias("p")
|
||||
.about("Place a piece on the board"),
|
||||
.arg(Arg::new("square").required(true)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("sight")
|
||||
.arg(Arg::new("square").required(false))
|
||||
.about("Show sight of a piece on a square"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("moves")
|
||||
.arg(Arg::new("square").required(false))
|
||||
.about("Show moves of a piece on a square. With no argument, show all moves for the active color."),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("movement")
|
||||
.arg(Arg::new("square").required(true))
|
||||
.about("Show moves of a piece on a square"),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("perft")
|
||||
.arg(Arg::new("depth")
|
||||
.required(true)
|
||||
.value_parser(value_parser!(usize))
|
||||
)
|
||||
.about("Run Perft on the current position to the given depth")
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("reset")
|
||||
.subcommand(Command::new("clear").about("Reset to a cleared board"))
|
||||
.subcommand(Command::new("starting").about("Reset to the starting position"))
|
||||
.subcommand(
|
||||
Command::new("fen")
|
||||
.arg(Arg::new("fen").required(true))
|
||||
.about("Reset to a position described by a FEN string"),
|
||||
)
|
||||
.about("Reset the board"),
|
||||
)
|
||||
.subcommand(Command::new("print").about("Print the board"))
|
||||
.subcommand(Command::new("quit").alias("exit").about("Quit the program"))
|
||||
.subcommand(Command::new("zobrist").about("Print the Zobrist hash of the current board"))
|
||||
.subcommand(Command::new("quit").about("Quit the program"))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
enum CommandHandlingError<'a> {
|
||||
#[error("lexer error")]
|
||||
LexerError,
|
||||
|
||||
#[error("missing {0} argument")]
|
||||
MissingArgument(&'a str),
|
||||
|
||||
#[error("no piece on {0}")]
|
||||
NoPiece(Square),
|
||||
|
||||
#[error("{value:?} is not a valid value for {argument_name:?}")]
|
||||
ValueError {
|
||||
argument_name: &'static str,
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn respond(line: &str, state: &mut State) -> anyhow::Result<CommandResult> {
|
||||
let args = shlex::split(line).ok_or(CommandHandlingError::LexerError)?;
|
||||
let matches = command_line().try_get_matches_from(args)?;
|
||||
|
||||
let mut result = CommandResult::default();
|
||||
fn respond(line: &str, pos: &mut Position) -> Result<ShouldContinue, String> {
|
||||
let args = shlex::split(line).ok_or("error: Invalid quoting")?;
|
||||
let matches = command_line()
|
||||
.try_get_matches_from(args)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match matches.subcommand() {
|
||||
Some(("flags", matches)) => result = do_flags_command(state, matches),
|
||||
Some(("load", matches)) => result = do_load_command(state, matches)?,
|
||||
Some(("print", _matches)) => {}
|
||||
Some(("quit", _matches)) => {
|
||||
result.should_continue = false;
|
||||
result.should_print_position = false;
|
||||
return Ok(ShouldContinue::No);
|
||||
}
|
||||
Some(("fen", _matches)) => {
|
||||
println!("{}", state.position.to_fen_str()?);
|
||||
result.should_print_position = false;
|
||||
Some(("place", _matches)) => {
|
||||
let piece_arg = _matches.get_one::<String>("piece").unwrap();
|
||||
let shape = match piece_arg.chars().nth(0) {
|
||||
Some(c) => Shape::try_from(c).map_err(|_| ()),
|
||||
None => Err(()),
|
||||
}
|
||||
.map_err(|_| "Error: invalid piece specifier")?;
|
||||
|
||||
let square_arg = _matches.get_one::<String>("square").unwrap();
|
||||
let square = Square::from_algebraic_str(square_arg)
|
||||
.map_err(|_| "Error: invalid square specifier")?;
|
||||
|
||||
pos.place_piece(&Piece::new(Color::White, shape), &square)
|
||||
.map_err(|_| "Error: Unable to place piece")?;
|
||||
}
|
||||
Some(("make", matches)) => result = do_make_command(state, matches)?,
|
||||
Some(("perft", matches)) => result = do_perft_command(state, matches)?,
|
||||
Some(("place", matches)) => {
|
||||
let color = matches
|
||||
.get_one::<String>("color")
|
||||
.ok_or(CommandHandlingError::MissingArgument("color"))?;
|
||||
let color = color.parse::<Color>()?;
|
||||
|
||||
let shape = matches
|
||||
.get_one::<String>("piece")
|
||||
.ok_or(CommandHandlingError::MissingArgument("piece"))?;
|
||||
let shape = shape.parse::<Shape>()?;
|
||||
|
||||
let square = matches
|
||||
.get_one::<String>("square")
|
||||
.ok_or(CommandHandlingError::MissingArgument("square"))?;
|
||||
let square = Square::from_algebraic_str(square)?;
|
||||
|
||||
let piece = Piece::new(color, shape);
|
||||
|
||||
state
|
||||
.position
|
||||
.place_piece(piece, square, PlacePieceStrategy::default())?;
|
||||
}
|
||||
Some(("sight", matches)) => {
|
||||
let sight = if let Some(square) = matches.get_one::<String>("square") {
|
||||
let square: Square = square.parse()?;
|
||||
state.position.sight_piece(square)
|
||||
} else {
|
||||
state.position.sight_active()
|
||||
};
|
||||
|
||||
let display = state.position.display().highlight(sight);
|
||||
println!("\n{display}");
|
||||
|
||||
result.should_print_position = false;
|
||||
}
|
||||
Some(("moves", matches)) => result = do_moves_command(state, matches)?,
|
||||
Some(("movement", matches)) => result = do_movement_command(state, matches)?,
|
||||
Some(("reset", matches)) => result = do_reset_command(state, matches)?,
|
||||
Some(("zobrist", matches)) => result = do_zobrist_command(state, matches),
|
||||
Some((name, _matches)) => unimplemented!("{name}"),
|
||||
None => unreachable!("Subcommand required"),
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn do_flags_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult {
|
||||
let board = state.position.board();
|
||||
|
||||
println!("Castling:");
|
||||
|
||||
for (color, wing) in [
|
||||
(Color::White, Wing::KingSide),
|
||||
(Color::White, Wing::QueenSide),
|
||||
(Color::Black, Wing::KingSide),
|
||||
(Color::Black, Wing::QueenSide),
|
||||
] {
|
||||
let has_right = board.has_castling_right_unwrapped(color, wing.into());
|
||||
let can_castle = board.color_can_castle(wing, Some(color));
|
||||
|
||||
let can_castle_message = match can_castle {
|
||||
Ok(_) => "ok".to_string(),
|
||||
Err(error) => format!("{error}"),
|
||||
};
|
||||
|
||||
println!(" {color} {wing}: {has_right}, {can_castle_message}");
|
||||
}
|
||||
|
||||
CommandResult {
|
||||
should_continue: true,
|
||||
should_print_position: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn do_load_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result<CommandResult> {
|
||||
let fen_string = matches
|
||||
.get_one::<String>("fen")
|
||||
.ok_or(CommandHandlingError::MissingArgument("fen"))?;
|
||||
|
||||
let mut board = Board::from_fen_str(fen_string.as_str())?;
|
||||
board.set_zobrist_state(state.zobrist.clone());
|
||||
|
||||
state.position = Position::new(board);
|
||||
|
||||
Ok(CommandResult {
|
||||
should_continue: true,
|
||||
should_print_position: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn do_make_command(state: &mut State, matches: &clap::ArgMatches) -> anyhow::Result<CommandResult> {
|
||||
let move_string = matches
|
||||
.get_one::<String>("move")
|
||||
.ok_or(CommandHandlingError::MissingArgument("move"))?;
|
||||
|
||||
let algebraic_move: AlgebraicMoveComponents = move_string.parse()?;
|
||||
|
||||
let encoded_move = state
|
||||
.position
|
||||
.move_from_algebraic_components(algebraic_move)
|
||||
.ok_or(CommandHandlingError::ValueError {
|
||||
argument_name: "move",
|
||||
value: move_string.to_string(),
|
||||
})?;
|
||||
|
||||
state.position.make_move(encoded_move, ValidateMove::Yes)?;
|
||||
|
||||
Ok(CommandResult::default())
|
||||
}
|
||||
|
||||
fn do_reset_command(
|
||||
state: &mut State,
|
||||
matches: &clap::ArgMatches,
|
||||
) -> anyhow::Result<CommandResult> {
|
||||
match matches.subcommand() {
|
||||
None | Some(("clear", _)) => state.position = Position::empty(Some(state.zobrist.clone())),
|
||||
Some(("starting", _)) => state.position = Position::starting(Some(state.zobrist.clone())),
|
||||
Some(("fen", matches)) => {
|
||||
let fen = matches
|
||||
.get_one::<String>("fen")
|
||||
.ok_or(CommandHandlingError::MissingArgument("fen"))?;
|
||||
let board = Board::from_fen_str(fen)?;
|
||||
state.position = Position::new(board);
|
||||
}
|
||||
Some((name, _matches)) => unimplemented!("{name}"),
|
||||
}
|
||||
|
||||
Ok(CommandResult::default())
|
||||
}
|
||||
|
||||
fn do_moves_command(
|
||||
state: &mut State,
|
||||
matches: &clap::ArgMatches,
|
||||
) -> anyhow::Result<CommandResult> {
|
||||
let moves: Vec<GeneratedMove> = if let Some(square) = matches
|
||||
.get_one::<String>("square")
|
||||
.and_then(|square| square.parse::<Square>().ok())
|
||||
{
|
||||
state
|
||||
.position
|
||||
.moves_for_piece(square)
|
||||
.map(|it| it.filter(|ply| ply.origin() == square))
|
||||
.map(Iterator::collect)
|
||||
.ok_or(CommandHandlingError::NoPiece(square))?
|
||||
} else {
|
||||
state.position.all_moves(None).collect()
|
||||
};
|
||||
|
||||
let formatted_moves: Vec<String> = moves
|
||||
.iter()
|
||||
.map(|ply| {
|
||||
let origin = ply.origin();
|
||||
if let Some(piece) = state.position.get_piece(origin) {
|
||||
format!("{piece}{ply}")
|
||||
} else {
|
||||
format!("{ply}??")
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !formatted_moves.is_empty() {
|
||||
let max_length = formatted_moves
|
||||
.iter()
|
||||
.map(|s| s.chars().count())
|
||||
.max()
|
||||
.unwrap_or(8)
|
||||
+ 2;
|
||||
|
||||
let columns_count = 80 / max_length;
|
||||
for row in formatted_moves.chunks(columns_count) {
|
||||
for ply in row {
|
||||
print!("{ply:<max_length$}");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CommandResult {
|
||||
should_continue: true,
|
||||
should_print_position: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn do_movement_command(
|
||||
state: &mut State,
|
||||
matches: &clap::ArgMatches,
|
||||
) -> anyhow::Result<CommandResult> {
|
||||
let square = *matches
|
||||
.get_one::<Square>("square")
|
||||
.ok_or(CommandHandlingError::MissingArgument("square"))?;
|
||||
|
||||
let movement = state.position.movement_piece(square);
|
||||
let display = state.position.display().highlight(movement);
|
||||
println!("\n{display}");
|
||||
|
||||
Ok(CommandResult {
|
||||
should_continue: true,
|
||||
should_print_position: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn do_perft_command(
|
||||
state: &mut State,
|
||||
matches: &clap::ArgMatches,
|
||||
) -> anyhow::Result<CommandResult> {
|
||||
let depth = *matches
|
||||
.get_one::<usize>("depth")
|
||||
.ok_or(CommandHandlingError::MissingArgument("depth"))?;
|
||||
|
||||
let mut position = state.position.clone();
|
||||
let counters = position.perft(depth);
|
||||
|
||||
println!("{counters}");
|
||||
|
||||
Ok(CommandResult {
|
||||
should_continue: true,
|
||||
should_print_position: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn do_zobrist_command(state: &mut State, _matches: &clap::ArgMatches) -> CommandResult {
|
||||
if let Some(hash) = state.position.zobrist_hash() {
|
||||
println!("hash:{hash}");
|
||||
} else {
|
||||
println!("No Zobrist hash available");
|
||||
}
|
||||
|
||||
CommandResult {
|
||||
should_continue: true,
|
||||
should_print_position: false,
|
||||
}
|
||||
Ok(ShouldContinue::Yes)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {err}"))?;
|
||||
let mut editor = DefaultEditor::new().map_err(|err| format!("Error: {}", err.to_string()))?;
|
||||
|
||||
let mut rng = RandomNumberGenerator::default();
|
||||
let zobrist_state = Arc::new(ZobristState::new(&mut rng));
|
||||
|
||||
let starting_position = Position::starting(Some(zobrist_state.clone()));
|
||||
let mut state = State {
|
||||
position: starting_position,
|
||||
zobrist: zobrist_state,
|
||||
};
|
||||
|
||||
let mut should_print_position = true;
|
||||
let mut pos = Position::empty();
|
||||
|
||||
loop {
|
||||
if should_print_position {
|
||||
println!("{}", &state.position);
|
||||
println!("{} to move.", state.position.active_color());
|
||||
}
|
||||
let diagram = DiagramFormatter::new(&pos);
|
||||
println!("{}", diagram);
|
||||
|
||||
let readline = editor.readline("\n? ");
|
||||
let readline = editor.readline("? ");
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
let line = line.trim();
|
||||
|
|
@ -401,14 +91,13 @@ fn main() -> Result<(), String> {
|
|||
continue;
|
||||
}
|
||||
|
||||
match respond(line, &mut state) {
|
||||
Ok(result) => {
|
||||
should_print_position = result.should_print_position;
|
||||
if !result.should_continue {
|
||||
match respond(line, &mut pos) {
|
||||
Ok(should_continue) => {
|
||||
if should_continue == ShouldContinue::No {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(message) => println!("{message}"),
|
||||
Err(message) => println!("{}", message),
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
|
|
@ -420,7 +109,7 @@ fn main() -> Result<(), String> {
|
|||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {err}");
|
||||
println!("Error: {:?}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use clap::ArgMatches;
|
||||
|
||||
pub enum MakeCommandError {}
|
||||
|
||||
pub struct MakeCommand {}
|
||||
|
|
@ -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"
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use chessfriend_core::{
|
||||
errors::{ParseShapeError, ParseSquareError},
|
||||
Shape, Square,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum AlgebraicMoveComponents {
|
||||
/// An empty move
|
||||
Null,
|
||||
|
||||
Regular {
|
||||
origin: Square,
|
||||
target: Square,
|
||||
promotion: Option<Shape>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum ParseAlgebraicMoveComponentsError {
|
||||
#[error("string not long enough to contain valid move")]
|
||||
InsufficientLength,
|
||||
|
||||
#[error("{0}")]
|
||||
ParseSquareError(#[from] ParseSquareError),
|
||||
|
||||
#[error("{0}")]
|
||||
ParseShapeError(#[from] ParseShapeError),
|
||||
|
||||
#[error("{0} is not a promotable shape")]
|
||||
InvalidPromotionShape(Shape),
|
||||
}
|
||||
|
||||
impl FromStr for AlgebraicMoveComponents {
|
||||
type Err = ParseAlgebraicMoveComponentsError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.trim();
|
||||
|
||||
if s == "0000" {
|
||||
return Ok(Self::Null);
|
||||
}
|
||||
|
||||
if s.len() < 4 {
|
||||
return Err(ParseAlgebraicMoveComponentsError::InsufficientLength);
|
||||
}
|
||||
|
||||
let s = s.to_lowercase();
|
||||
let (origin_string, s) = s.split_at(2);
|
||||
|
||||
let s = s.trim_start();
|
||||
let (target_string, s) = s.split_at(2);
|
||||
|
||||
let promotion = if s.len() >= 1 {
|
||||
let s = s.trim_start();
|
||||
let (promotion_string, _s) = s.split_at(1);
|
||||
|
||||
Some(promotion_string)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let origin: Square = origin_string.parse()?;
|
||||
let target: Square = target_string.parse()?;
|
||||
|
||||
let promotion = if let Some(promotion) = promotion {
|
||||
let promotion: Shape = promotion.parse()?;
|
||||
if promotion.is_promotable() {
|
||||
Some(promotion)
|
||||
} else {
|
||||
return Err(ParseAlgebraicMoveComponentsError::InvalidPromotionShape(
|
||||
promotion,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(AlgebraicMoveComponents::Regular {
|
||||
origin,
|
||||
target,
|
||||
promotion,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_null_move() -> Result<(), ParseAlgebraicMoveComponentsError> {
|
||||
let components: AlgebraicMoveComponents = "0000".parse()?;
|
||||
|
||||
assert_eq!(components, AlgebraicMoveComponents::Null);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_origin_target_move() -> Result<(), ParseAlgebraicMoveComponentsError> {
|
||||
let components: AlgebraicMoveComponents = "e2e4".parse()?;
|
||||
|
||||
assert_eq!(
|
||||
components,
|
||||
AlgebraicMoveComponents::Regular {
|
||||
origin: Square::E2,
|
||||
target: Square::E4,
|
||||
promotion: None
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_origin_target_promotion_move() -> Result<(), ParseAlgebraicMoveComponentsError> {
|
||||
let components: AlgebraicMoveComponents = "h7h8q".parse()?;
|
||||
|
||||
assert_eq!(
|
||||
components,
|
||||
AlgebraicMoveComponents::Regular {
|
||||
origin: Square::H7,
|
||||
target: Square::H8,
|
||||
promotion: Some(Shape::Queen),
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use chessfriend_core::Shape;
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Kind {
|
||||
Quiet = 0b0000,
|
||||
DoublePush = 0b0001,
|
||||
KingSideCastle = 0b0010,
|
||||
QueenSideCastle = 0b0011,
|
||||
Capture = 0b0100,
|
||||
EnPassantCapture = 0b0101,
|
||||
Promotion = 0b1000,
|
||||
CapturePromotion = 0b1100,
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PromotionShape {
|
||||
Knight = 0b00,
|
||||
Bishop = 0b01,
|
||||
Rook = 0b10,
|
||||
Queen = 0b11,
|
||||
}
|
||||
|
||||
impl PromotionShape {
|
||||
pub const NUM: usize = 4;
|
||||
|
||||
pub const ALL: [PromotionShape; PromotionShape::NUM] = [
|
||||
PromotionShape::Knight,
|
||||
PromotionShape::Bishop,
|
||||
PromotionShape::Rook,
|
||||
PromotionShape::Queen,
|
||||
];
|
||||
}
|
||||
|
||||
impl From<PromotionShape> for Shape {
|
||||
fn from(value: PromotionShape) -> Self {
|
||||
match value {
|
||||
PromotionShape::Knight => Shape::Knight,
|
||||
PromotionShape::Bishop => Shape::Bishop,
|
||||
PromotionShape::Rook => Shape::Rook,
|
||||
PromotionShape::Queen => Shape::Queen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Shape> for PromotionShape {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Shape) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Shape::Knight => Ok(PromotionShape::Knight),
|
||||
Shape::Bishop => Ok(PromotionShape::Bishop),
|
||||
Shape::Rook => Ok(PromotionShape::Rook),
|
||||
Shape::Queen => Ok(PromotionShape::Queen),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod all;
|
||||
mod king;
|
||||
mod knight;
|
||||
mod pawn;
|
||||
mod slider;
|
||||
|
||||
pub use all::AllPiecesMoveGenerator;
|
||||
pub use king::KingMoveGenerator;
|
||||
pub use knight::KnightMoveGenerator;
|
||||
pub use pawn::PawnMoveGenerator;
|
||||
pub use slider::{BishopMoveGenerator, QueenMoveGenerator, RookMoveGenerator};
|
||||
|
||||
use crate::Move;
|
||||
use chessfriend_core::{Shape, Square};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct GeneratedMove {
|
||||
pub(crate) ply: Move,
|
||||
}
|
||||
|
||||
impl GeneratedMove {
|
||||
#[must_use]
|
||||
pub fn origin(&self) -> Square {
|
||||
self.ply.origin_square()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn target(&self) -> Square {
|
||||
self.ply.target_square()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn promotion_shape(&self) -> Option<Shape> {
|
||||
self.ply.promotion_shape()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn ply(&self) -> Move {
|
||||
self.ply
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GeneratedMove {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[{}]", self.ply)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GeneratedMove> for Move {
|
||||
fn from(value: GeneratedMove) -> Self {
|
||||
value.ply
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Move> for GeneratedMove {
|
||||
fn from(value: Move) -> Self {
|
||||
GeneratedMove { ply: value }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::{
|
||||
BishopMoveGenerator, GeneratedMove, KingMoveGenerator, KnightMoveGenerator, PawnMoveGenerator,
|
||||
QueenMoveGenerator, RookMoveGenerator,
|
||||
};
|
||||
use chessfriend_board::Board;
|
||||
use chessfriend_core::Color;
|
||||
use std::iter::{Fuse, FusedIterator};
|
||||
|
||||
#[must_use]
|
||||
pub struct AllPiecesMoveGenerator {
|
||||
pawn: Fuse<PawnMoveGenerator>,
|
||||
knight: Fuse<KnightMoveGenerator>,
|
||||
bishop: Fuse<BishopMoveGenerator>,
|
||||
rook: Fuse<RookMoveGenerator>,
|
||||
queen: Fuse<QueenMoveGenerator>,
|
||||
king: Fuse<KingMoveGenerator>,
|
||||
}
|
||||
|
||||
impl AllPiecesMoveGenerator {
|
||||
pub fn new(board: &Board, color: Option<Color>) -> Self {
|
||||
Self {
|
||||
pawn: PawnMoveGenerator::new(board, color).fuse(),
|
||||
knight: KnightMoveGenerator::new(board, color).fuse(),
|
||||
bishop: BishopMoveGenerator::new(board, color).fuse(),
|
||||
rook: RookMoveGenerator::new(board, color).fuse(),
|
||||
queen: QueenMoveGenerator::new(board, color).fuse(),
|
||||
king: KingMoveGenerator::new(board, color).fuse(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for AllPiecesMoveGenerator {
|
||||
type Item = GeneratedMove;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
macro_rules! return_next_move {
|
||||
($generator:expr) => {
|
||||
let next_move = $generator.next();
|
||||
if next_move.is_some() {
|
||||
return next_move;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// All of these iterators are fused, meaning they are guaranteed to
|
||||
// always return None once they've returned None the first time. Fuse<T>
|
||||
// can optimize this so that the work done in each of the move
|
||||
// generators' next() methods doesn't have to be repeated every time
|
||||
// next() is called.
|
||||
|
||||
return_next_move!(self.pawn);
|
||||
return_next_move!(self.knight);
|
||||
return_next_move!(self.bishop);
|
||||
return_next_move!(self.rook);
|
||||
return_next_move!(self.queen);
|
||||
return_next_move!(self.king);
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for AllPiecesMoveGenerator {}
|
||||
|
|
@ -1,370 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{GeneratedMove, Move};
|
||||
use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard};
|
||||
use chessfriend_board::{Board, CastleParameters};
|
||||
use chessfriend_core::{Color, Square, Wing};
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
#[must_use]
|
||||
pub struct KingMoveGenerator {
|
||||
kings: Vec<KingIterator>,
|
||||
next_kings_index: usize,
|
||||
current_king: Option<KingIterator>,
|
||||
castle_iterator: CastleIterator,
|
||||
friends: BitBoard,
|
||||
enemies: BitBoard,
|
||||
}
|
||||
|
||||
impl KingMoveGenerator {
|
||||
pub fn new(board: &Board, color: Option<Color>) -> Self {
|
||||
let color = board.unwrap_color(color);
|
||||
|
||||
let friends = board.friendly_occupancy(color);
|
||||
let enemies = board.enemies(color);
|
||||
|
||||
let kings: Vec<KingIterator> = board
|
||||
.kings(color)
|
||||
.occupied_squares_trailing()
|
||||
.map(|king| KingIterator {
|
||||
origin: king,
|
||||
moves: board
|
||||
.king_sight(king, Some(color))
|
||||
.occupied_squares_trailing(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
kings,
|
||||
next_kings_index: 0,
|
||||
current_king: None,
|
||||
castle_iterator: CastleIterator::new(board, color),
|
||||
friends,
|
||||
enemies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for KingMoveGenerator {
|
||||
type Item = GeneratedMove;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.current_king.is_none() {
|
||||
if self.next_kings_index < self.kings.len() {
|
||||
self.current_king = Some(self.kings[self.next_kings_index].clone());
|
||||
self.next_kings_index += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(current_king) = self.current_king.as_mut() {
|
||||
if let Some(target) = current_king.next() {
|
||||
let target_bitboard: BitBoard = target.into();
|
||||
|
||||
let is_targeting_friendly_piece =
|
||||
(target_bitboard & self.friends).is_populated();
|
||||
if is_targeting_friendly_piece {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_targeting_enemy_piece = (target_bitboard & self.enemies).is_populated();
|
||||
if is_targeting_enemy_piece {
|
||||
return Some(Move::capture(current_king.origin, target).into());
|
||||
}
|
||||
|
||||
return Some(Move::quiet(current_king.origin, target).into());
|
||||
}
|
||||
|
||||
self.current_king = None;
|
||||
}
|
||||
}
|
||||
|
||||
self.castle_iterator.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for KingMoveGenerator {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct KingIterator {
|
||||
origin: Square,
|
||||
moves: TrailingBitScanner,
|
||||
}
|
||||
|
||||
impl Iterator for KingIterator {
|
||||
type Item = Square;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.moves.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for KingIterator {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CastleIterator {
|
||||
color: Color,
|
||||
kingside: Option<&'static CastleParameters>,
|
||||
queenside: Option<&'static CastleParameters>,
|
||||
}
|
||||
|
||||
impl CastleIterator {
|
||||
fn new(board: &Board, color: Color) -> Self {
|
||||
let kingside = board.color_can_castle(Wing::KingSide, Some(color)).ok();
|
||||
let queenside = board.color_can_castle(Wing::QueenSide, Some(color)).ok();
|
||||
|
||||
Self {
|
||||
color,
|
||||
kingside,
|
||||
queenside,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for CastleIterator {
|
||||
type Item = GeneratedMove;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(_parameters) = self.kingside.take() {
|
||||
return Some(Move::castle(self.color, Wing::KingSide).into());
|
||||
}
|
||||
|
||||
if let Some(_parameters) = self.queenside.take() {
|
||||
return Some(Move::castle(self.color, Wing::QueenSide).into());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for CastleIterator {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{assert_move_list, ply};
|
||||
use chessfriend_board::test_board;
|
||||
|
||||
#[test]
|
||||
fn white_king_center_square_ai_claude() {
|
||||
let board = test_board!(White King on E4);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
// All 8 adjacent squares
|
||||
ply!(E4 - D3), // Southwest
|
||||
ply!(E4 - D4), // West
|
||||
ply!(E4 - D5), // Northwest
|
||||
ply!(E4 - E3), // South
|
||||
ply!(E4 - E5), // North
|
||||
ply!(E4 - F3), // Southeast
|
||||
ply!(E4 - F4), // East
|
||||
ply!(E4 - F5), // Northeast
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_king_corner_square_ai_claude() {
|
||||
let board = test_board!(White King on A1);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Only 3 possible moves from corner
|
||||
ply!(A1 - A2), // North
|
||||
ply!(A1 - B1), // East
|
||||
ply!(A1 - B2), // Northeast
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_king_edge_square_ai_claude() {
|
||||
let board = test_board!(White King on E1);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
// 5 possible moves from edge
|
||||
ply!(E1 - D1), // West
|
||||
ply!(E1 - D2), // Northwest
|
||||
ply!(E1 - E2), // North
|
||||
ply!(E1 - F1), // East
|
||||
ply!(E1 - F2), // Northeast
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_king_with_captures_ai_claude() {
|
||||
let board = test_board!(
|
||||
White King on D4,
|
||||
Black Pawn on C5, // Can capture
|
||||
Black Knight on E5, // Can capture
|
||||
Black Bishop on D3 // Can capture
|
||||
);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Regular moves
|
||||
ply!(D4 - C3),
|
||||
ply!(D4 - C4),
|
||||
ply!(D4 - D5),
|
||||
ply!(D4 - E3),
|
||||
ply!(D4 - E4),
|
||||
// Captures
|
||||
ply!(D4 x C5), // Capture pawn
|
||||
ply!(D4 x E5), // Capture knight
|
||||
ply!(D4 x D3), // Capture bishop
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_king_blocked_by_friendly_pieces_ai_claude() {
|
||||
let board = test_board!(
|
||||
White King on D4,
|
||||
White Pawn on C4, // Blocks west
|
||||
White Knight on D5, // Blocks north
|
||||
White Bishop on E3 // Blocks southeast
|
||||
);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
ply!(D4 - C3),
|
||||
ply!(D4 - C5),
|
||||
ply!(D4 - D3),
|
||||
ply!(D4 - E4),
|
||||
ply!(D4 - E5),
|
||||
// Cannot move to C4, D5, E3 (friendly pieces)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_king_castling_kingside_ai_claude() {
|
||||
let board = test_board!(
|
||||
White King on E1,
|
||||
White Rook on H1
|
||||
// Assuming squares F1, G1 are empty and king/rook haven't moved
|
||||
);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Regular king moves
|
||||
ply!(E1 - D1),
|
||||
ply!(E1 - D2),
|
||||
ply!(E1 - E2),
|
||||
ply!(E1 - F1),
|
||||
ply!(E1 - F2),
|
||||
ply!(White 0-0),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_king_castling_queenside_ai_claude() {
|
||||
let board = test_board!(
|
||||
White King on E1,
|
||||
White Rook on A1
|
||||
// Assuming squares B1, C1, D1 are empty and king/rook haven't moved
|
||||
);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Regular king moves
|
||||
ply!(E1 - D1),
|
||||
ply!(E1 - D2),
|
||||
ply!(E1 - E2),
|
||||
ply!(E1 - F1),
|
||||
ply!(E1 - F2),
|
||||
ply!(White 0-0-0),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_king_no_castling_through_check_ai_claude() {
|
||||
let board = test_board!(
|
||||
White King on E1,
|
||||
White Rook on H1,
|
||||
Black Rook on F8 // Attacks F1, preventing kingside castling
|
||||
);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
ply!(E1 - D1),
|
||||
ply!(E1 - D2),
|
||||
ply!(E1 - E2),
|
||||
ply!(E1 - F1),
|
||||
ply!(E1 - F2),
|
||||
// No castling moves - cannot castle through check
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_king_castling_blocked_by_pieces_ai_claude() {
|
||||
let board = test_board!(
|
||||
White King on E1,
|
||||
White Rook on H1,
|
||||
White Knight on G1, // Blocks kingside castling
|
||||
);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
ply!(E1 - D1),
|
||||
ply!(E1 - D2),
|
||||
ply!(E1 - E2),
|
||||
ply!(E1 - F1),
|
||||
ply!(E1 - F2),
|
||||
// No castling - path blocked by knight
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_king_movement_ai_claude() {
|
||||
let board = test_board!(Black King on E8);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, Some(Color::Black)),
|
||||
[
|
||||
ply!(E8 - D7),
|
||||
ply!(E8 - D8),
|
||||
ply!(E8 - E7),
|
||||
ply!(E8 - F7),
|
||||
ply!(E8 - F8),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_king_surrounded_by_enemies_ai_claude() {
|
||||
let board = test_board!(
|
||||
White King on E4,
|
||||
Black Pawn on D3,
|
||||
Black Pawn on D4,
|
||||
Black Pawn on D5,
|
||||
Black Pawn on E3,
|
||||
Black Pawn on E5,
|
||||
Black Pawn on F3,
|
||||
Black Pawn on F4,
|
||||
Black Pawn on F5
|
||||
);
|
||||
assert_move_list!(
|
||||
KingMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Can capture all surrounding enemy pieces
|
||||
ply!(E4 x D3),
|
||||
ply!(E4 x D4),
|
||||
ply!(E4 x D5),
|
||||
ply!(E4 x E3),
|
||||
ply!(E4 x E5),
|
||||
ply!(E4 x F3),
|
||||
ply!(E4 x F4),
|
||||
ply!(E4 x F5),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use super::GeneratedMove;
|
||||
use crate::Move;
|
||||
use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard};
|
||||
use chessfriend_board::Board;
|
||||
use chessfriend_core::{Color, Square};
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
#[must_use]
|
||||
pub struct KnightMoveGenerator {
|
||||
origin_iterator: TrailingBitScanner,
|
||||
target_iterator: Option<TrailingBitScanner>,
|
||||
current_origin: Option<Square>,
|
||||
friends: BitBoard,
|
||||
enemies: BitBoard,
|
||||
}
|
||||
|
||||
impl KnightMoveGenerator {
|
||||
pub fn new(board: &Board, color: Option<Color>) -> Self {
|
||||
let color = board.unwrap_color(color);
|
||||
|
||||
let knights = board.knights(color);
|
||||
let friends = board.friendly_occupancy(color);
|
||||
let enemies = board.enemies(color);
|
||||
|
||||
Self {
|
||||
origin_iterator: knights.occupied_squares_trailing(),
|
||||
target_iterator: None,
|
||||
current_origin: None,
|
||||
friends,
|
||||
enemies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for KnightMoveGenerator {
|
||||
type Item = GeneratedMove;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_origin.is_none() {
|
||||
self.current_origin = self.origin_iterator.next();
|
||||
}
|
||||
|
||||
let origin = self.current_origin?;
|
||||
|
||||
let target_iterator = self.target_iterator.get_or_insert_with(|| {
|
||||
let knight_moves = BitBoard::knight_moves(origin);
|
||||
knight_moves.occupied_squares_trailing()
|
||||
});
|
||||
|
||||
if let Some(target) = target_iterator.next() {
|
||||
let target_bitboard: BitBoard = target.into();
|
||||
if (target_bitboard & self.friends).is_populated() {
|
||||
self.next()
|
||||
} else if (target_bitboard & self.enemies).is_populated() {
|
||||
let ply = Move::capture(origin, target);
|
||||
Some(ply.into())
|
||||
} else {
|
||||
let ply = Move::quiet(origin, target);
|
||||
Some(ply.into())
|
||||
}
|
||||
} else {
|
||||
self.current_origin = None;
|
||||
self.target_iterator = None;
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for KnightMoveGenerator {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{assert_move_list, ply};
|
||||
use chessfriend_board::test_board;
|
||||
use chessfriend_core::Color;
|
||||
|
||||
#[test]
|
||||
fn white_f5_moves() {
|
||||
let board = test_board!(White Knight on F5);
|
||||
assert_move_list!(
|
||||
KnightMoveGenerator::new(&board, Some(Color::White)),
|
||||
[
|
||||
ply!(F5 - G7),
|
||||
ply!(F5 - H6),
|
||||
ply!(F5 - H4),
|
||||
ply!(F5 - G3),
|
||||
ply!(F5 - E3),
|
||||
ply!(F5 - D4),
|
||||
ply!(F5 - D6),
|
||||
ply!(F5 - E7),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_f5_and_c6_moves() {
|
||||
let board = test_board!(
|
||||
White Knight on C6,
|
||||
White Knight on F5,
|
||||
);
|
||||
assert_move_list!(
|
||||
KnightMoveGenerator::new(&board, Some(Color::White)),
|
||||
[
|
||||
ply!(C6 - D8),
|
||||
ply!(C6 - D8),
|
||||
ply!(C6 - E7),
|
||||
ply!(C6 - E5),
|
||||
ply!(C6 - D4),
|
||||
ply!(C6 - B4),
|
||||
ply!(C6 - A5),
|
||||
ply!(C6 - A7),
|
||||
ply!(C6 - B8),
|
||||
ply!(F5 - G7),
|
||||
ply!(F5 - H6),
|
||||
ply!(F5 - H4),
|
||||
ply!(F5 - G3),
|
||||
ply!(F5 - E3),
|
||||
ply!(F5 - D4),
|
||||
ply!(F5 - D6),
|
||||
ply!(F5 - E7),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_f5_moves_with_blockers() {
|
||||
let board = test_board!(
|
||||
White Knight on F5,
|
||||
White Queen on G3,
|
||||
White Bishop on D6,
|
||||
);
|
||||
assert_move_list!(
|
||||
KnightMoveGenerator::new(&board, Some(Color::White)),
|
||||
[
|
||||
ply!(F5 - G7),
|
||||
ply!(F5 - H6),
|
||||
ply!(F5 - H4),
|
||||
ply!(F5 - E3),
|
||||
ply!(F5 - D4),
|
||||
ply!(F5 - E7),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_f5_moves_with_captures() {
|
||||
let board = test_board!(
|
||||
White Knight on F5,
|
||||
White Queen on G3,
|
||||
White Bishop on D6,
|
||||
Black Queen on D4,
|
||||
);
|
||||
assert_move_list!(
|
||||
KnightMoveGenerator::new(&board, Some(Color::White)),
|
||||
[
|
||||
ply!(F5 - G7),
|
||||
ply!(F5 - H6),
|
||||
ply!(F5 - H4),
|
||||
ply!(F5 - E3),
|
||||
ply!(F5 - E7),
|
||||
ply!(F5 x D4),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,539 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
//! Implements a move generator for pawns.
|
||||
|
||||
use super::GeneratedMove;
|
||||
use crate::{Move, PromotionShape};
|
||||
use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard};
|
||||
use chessfriend_board::Board;
|
||||
use chessfriend_core::{Color, Direction, Rank, Square};
|
||||
use std::{iter::FusedIterator, slice};
|
||||
|
||||
pub struct PawnMoveGenerator {
|
||||
color: Color,
|
||||
single_pushes: BitBoard,
|
||||
double_pushes: BitBoard,
|
||||
left_captures: BitBoard,
|
||||
right_captures: BitBoard,
|
||||
en_passant: BitBoard,
|
||||
target_iterator: TrailingBitScanner,
|
||||
promotion_iterator: Option<PromotionIterator>,
|
||||
move_type: MoveType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum MoveType {
|
||||
SinglePushes,
|
||||
DoublePushes,
|
||||
LeftCaptures,
|
||||
RightCaptures,
|
||||
}
|
||||
|
||||
struct PromotionIterator {
|
||||
origin: Square,
|
||||
target: Square,
|
||||
iterator: slice::Iter<'static, PromotionShape>,
|
||||
}
|
||||
|
||||
impl PawnMoveGenerator {
|
||||
#[must_use]
|
||||
pub fn new(board: &Board, color: Option<Color>) -> Self {
|
||||
let color = board.unwrap_color(color);
|
||||
|
||||
let pawns = board.pawns(color);
|
||||
let occupied = board.occupancy();
|
||||
let empty = !occupied;
|
||||
let enemies = board.enemies(color);
|
||||
|
||||
// En passant captures present a particular challenge. Include the
|
||||
// target e.p. square when computing captures (i.e. treat it like an
|
||||
// enemy piece is on that square) but do not include it when generating
|
||||
// capture moves. If it is included, a regular capture move will be
|
||||
// generated where a special e.p. move should be created instead.
|
||||
//
|
||||
// So, include it in the enemies set when computing captures, then
|
||||
// remove it from the left and right captures bitboards before passing
|
||||
// them into the move generator. Additionally, include the target e.p.
|
||||
// square in the e.p. bitboard iff the capture bitboards include it.
|
||||
|
||||
let en_passant: BitBoard = board.en_passant_target().into();
|
||||
|
||||
let (single_pushes, double_pushes) = Self::pushes(pawns, color, empty);
|
||||
let (left_captures, right_captures) = Self::captures(pawns, color, enemies | en_passant);
|
||||
|
||||
Self {
|
||||
color,
|
||||
single_pushes,
|
||||
double_pushes,
|
||||
left_captures,
|
||||
right_captures,
|
||||
en_passant,
|
||||
target_iterator: single_pushes.occupied_squares_trailing(),
|
||||
promotion_iterator: None,
|
||||
move_type: MoveType::SinglePushes,
|
||||
}
|
||||
}
|
||||
|
||||
fn pushes(pawns: BitBoard, color: Color, empty: BitBoard) -> (BitBoard, BitBoard) {
|
||||
match color {
|
||||
Color::White => {
|
||||
const THIRD_RANK: BitBoard = BitBoard::rank(Rank::THREE);
|
||||
|
||||
let single_pushes = pawns.shift_north_one() & empty;
|
||||
let double_pushes = (single_pushes & THIRD_RANK).shift_north_one() & empty;
|
||||
|
||||
(single_pushes, double_pushes)
|
||||
}
|
||||
Color::Black => {
|
||||
const SIXTH_RANK: BitBoard = BitBoard::rank(Rank::SIX);
|
||||
|
||||
let single_pushes = pawns.shift_south_one() & empty;
|
||||
let double_pushes = (single_pushes & SIXTH_RANK).shift_south_one() & empty;
|
||||
|
||||
(single_pushes, double_pushes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn captures(pawns: BitBoard, color: Color, enemies: BitBoard) -> (BitBoard, BitBoard) {
|
||||
match color {
|
||||
Color::White => {
|
||||
let left_captures = pawns.shift_north_west_one() & enemies;
|
||||
let right_captures = pawns.shift_north_east_one() & enemies;
|
||||
(left_captures, right_captures)
|
||||
}
|
||||
Color::Black => {
|
||||
let left_captures = pawns.shift_south_east_one() & enemies;
|
||||
let right_captures = pawns.shift_south_west_one() & enemies;
|
||||
(left_captures, right_captures)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_origin_square(&self, target: Square) -> Option<Square> {
|
||||
match self.move_type {
|
||||
MoveType::SinglePushes => match self.color {
|
||||
Color::White => target.neighbor(Direction::South),
|
||||
Color::Black => target.neighbor(Direction::North),
|
||||
},
|
||||
MoveType::DoublePushes => match self.color {
|
||||
Color::White => target
|
||||
.neighbor(Direction::South)?
|
||||
.neighbor(Direction::South),
|
||||
Color::Black => target
|
||||
.neighbor(Direction::North)?
|
||||
.neighbor(Direction::North),
|
||||
},
|
||||
MoveType::LeftCaptures => match self.color {
|
||||
Color::White => target.neighbor(Direction::SouthEast),
|
||||
Color::Black => target.neighbor(Direction::NorthWest),
|
||||
},
|
||||
MoveType::RightCaptures => match self.color {
|
||||
Color::White => target.neighbor(Direction::SouthWest),
|
||||
Color::Black => target.neighbor(Direction::NorthEast),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn next_move_type(&mut self) -> Option<MoveType> {
|
||||
let next_move_type = self.move_type.next();
|
||||
|
||||
if let Some(next_move_type) = next_move_type {
|
||||
let next_bitboard = match next_move_type {
|
||||
MoveType::SinglePushes => self.single_pushes,
|
||||
MoveType::DoublePushes => self.double_pushes,
|
||||
MoveType::LeftCaptures => self.left_captures,
|
||||
MoveType::RightCaptures => self.right_captures,
|
||||
};
|
||||
|
||||
self.target_iterator = next_bitboard.occupied_squares_trailing();
|
||||
self.move_type = next_move_type;
|
||||
}
|
||||
|
||||
next_move_type
|
||||
}
|
||||
|
||||
fn next_promotion_move(&mut self) -> Option<GeneratedMove> {
|
||||
if let Some(promotion_iterator) = self.promotion_iterator.as_mut() {
|
||||
if let Some(shape) = promotion_iterator.iterator.next() {
|
||||
let origin = promotion_iterator.origin;
|
||||
let target = promotion_iterator.target;
|
||||
|
||||
return if matches!(
|
||||
self.move_type,
|
||||
MoveType::LeftCaptures | MoveType::RightCaptures
|
||||
) {
|
||||
Some(Move::capture_promotion(origin, target, *shape).into())
|
||||
} else {
|
||||
Some(Move::promotion(origin, target, *shape).into())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Iterator for PawnMoveGenerator {
|
||||
type Item = GeneratedMove;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let next_promotion_move = self.next_promotion_move();
|
||||
if next_promotion_move.is_some() {
|
||||
return next_promotion_move;
|
||||
}
|
||||
|
||||
self.promotion_iterator = None;
|
||||
|
||||
if let Some(target) = self.target_iterator.next() {
|
||||
let origin = self
|
||||
.calculate_origin_square(target)
|
||||
.expect("bogus origin square");
|
||||
|
||||
if target.rank().is_promotable_rank() {
|
||||
self.promotion_iterator = Some(PromotionIterator {
|
||||
origin,
|
||||
target,
|
||||
iterator: PromotionShape::ALL.iter(),
|
||||
});
|
||||
|
||||
return self.next();
|
||||
}
|
||||
|
||||
match self.move_type {
|
||||
MoveType::SinglePushes => Some(GeneratedMove {
|
||||
ply: Move::quiet(origin, target),
|
||||
}),
|
||||
MoveType::DoublePushes => Some(GeneratedMove {
|
||||
ply: Move::double_push(origin, target),
|
||||
}),
|
||||
MoveType::LeftCaptures | MoveType::RightCaptures => {
|
||||
let target_bitboard: BitBoard = target.into();
|
||||
Some(GeneratedMove {
|
||||
ply: if (target_bitboard & self.en_passant).is_populated() {
|
||||
Move::en_passant_capture(origin, target)
|
||||
} else {
|
||||
Move::capture(origin, target)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if self.next_move_type().is_some() {
|
||||
self.next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for PawnMoveGenerator {}
|
||||
|
||||
impl MoveType {
|
||||
fn next(self) -> Option<Self> {
|
||||
match self {
|
||||
MoveType::SinglePushes => Some(MoveType::DoublePushes),
|
||||
MoveType::DoublePushes => Some(MoveType::LeftCaptures),
|
||||
MoveType::LeftCaptures => Some(MoveType::RightCaptures),
|
||||
MoveType::RightCaptures => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
assert_move_list, assert_move_list_contains, assert_move_list_does_not_contain, ply, Move,
|
||||
};
|
||||
use chessfriend_board::{fen::FromFenStr, test_board};
|
||||
use chessfriend_core::{Color, Square};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn black_b7_pushes_and_double_pushes() {
|
||||
let board = test_board!(Black Pawn on B7);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[
|
||||
GeneratedMove {
|
||||
ply: Move::quiet(Square::B7, Square::B6)
|
||||
},
|
||||
GeneratedMove {
|
||||
ply: Move::double_push(Square::B7, Square::B5)
|
||||
}
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_e2_pushes_and_double_pushes() {
|
||||
let board = test_board!(White Pawn on E2);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[
|
||||
GeneratedMove {
|
||||
ply: Move::quiet(Square::E2, Square::E3)
|
||||
},
|
||||
GeneratedMove {
|
||||
ply: Move::double_push(Square::E2, Square::E4)
|
||||
}
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_b7_pushes_with_b5_blocker() {
|
||||
let board = test_board!(Black Pawn on B7, White Bishop on B5);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[GeneratedMove {
|
||||
ply: Move::quiet(Square::B7, Square::B6)
|
||||
}]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_b7_pushes_with_b6_blocker() {
|
||||
let board = test_board!(Black Pawn on B7, Black Bishop on B6);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
|
||||
assert!(generated_moves.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_e2_moves_with_e4_blocker() {
|
||||
let board = test_board!(White Pawn on E2, White Bishop on E4);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[GeneratedMove {
|
||||
ply: Move::quiet(Square::E2, Square::E3)
|
||||
}]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_e2_moves_with_e3_blocker() {
|
||||
let board = test_board!(White Pawn on E2, White Bishop on E3);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
|
||||
assert!(generated_moves.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_f6_left_captures() {
|
||||
let white_captures_board = test_board!(White Pawn on F6, Black Queen on E7);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&white_captures_board, Some(Color::White)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[
|
||||
GeneratedMove {
|
||||
ply: Move::capture(Square::F6, Square::E7)
|
||||
},
|
||||
GeneratedMove {
|
||||
ply: Move::quiet(Square::F6, Square::F7)
|
||||
}
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_d5_left_captures() {
|
||||
let black_captures_board = test_board!(Black Pawn on D5, White Queen on E4);
|
||||
let generated_moves = PawnMoveGenerator::new(&black_captures_board, Some(Color::Black));
|
||||
assert_move_list!(generated_moves, [ply!(D5 x E4), ply!(D5 - D4),]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_f6_right_captures() {
|
||||
let white_captures_board = test_board!(White Pawn on F6, Black Queen on G7);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&white_captures_board, Some(Color::White)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[
|
||||
GeneratedMove {
|
||||
ply: Move::capture(Square::F6, Square::G7)
|
||||
},
|
||||
GeneratedMove {
|
||||
ply: Move::quiet(Square::F6, Square::F7)
|
||||
}
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_d5_right_captures() {
|
||||
let black_captures_board = test_board!(Black Pawn on D5, White Queen on C4);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&black_captures_board, Some(Color::Black)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[
|
||||
GeneratedMove {
|
||||
ply: Move::capture(Square::D5, Square::C4)
|
||||
},
|
||||
GeneratedMove {
|
||||
ply: Move::quiet(Square::D5, Square::D4)
|
||||
}
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_g7_push_promotions() {
|
||||
let board = test_board!(White Pawn on G7);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[
|
||||
Move::promotion(Square::G7, Square::G8, PromotionShape::Knight).into(),
|
||||
Move::promotion(Square::G7, Square::G8, PromotionShape::Bishop).into(),
|
||||
Move::promotion(Square::G7, Square::G8, PromotionShape::Rook).into(),
|
||||
Move::promotion(Square::G7, Square::G8, PromotionShape::Queen).into(),
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_d7_push_and_capture_promotions() {
|
||||
let board = test_board!(
|
||||
White Pawn on D7,
|
||||
Black Queen on E8
|
||||
);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::White)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[
|
||||
Move::promotion(Square::D7, Square::D8, PromotionShape::Knight).into(),
|
||||
Move::promotion(Square::D7, Square::D8, PromotionShape::Bishop).into(),
|
||||
Move::promotion(Square::D7, Square::D8, PromotionShape::Rook).into(),
|
||||
Move::promotion(Square::D7, Square::D8, PromotionShape::Queen).into(),
|
||||
Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Knight).into(),
|
||||
Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Bishop).into(),
|
||||
Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Rook).into(),
|
||||
Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Queen).into(),
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_a2_push_promotions() {
|
||||
let board = test_board!(Black Pawn on A2);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[
|
||||
Move::promotion(Square::A2, Square::A1, PromotionShape::Knight).into(),
|
||||
Move::promotion(Square::A2, Square::A1, PromotionShape::Bishop).into(),
|
||||
Move::promotion(Square::A2, Square::A1, PromotionShape::Rook).into(),
|
||||
Move::promotion(Square::A2, Square::A1, PromotionShape::Queen).into(),
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_h2_push_and_capture_promotions() {
|
||||
let board = test_board!(
|
||||
Black Pawn on H2,
|
||||
White Queen on G1,
|
||||
);
|
||||
let generated_moves: HashSet<GeneratedMove> =
|
||||
PawnMoveGenerator::new(&board, Some(Color::Black)).collect();
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
[
|
||||
Move::promotion(Square::H2, Square::H1, PromotionShape::Knight).into(),
|
||||
Move::promotion(Square::H2, Square::H1, PromotionShape::Bishop).into(),
|
||||
Move::promotion(Square::H2, Square::H1, PromotionShape::Rook).into(),
|
||||
Move::promotion(Square::H2, Square::H1, PromotionShape::Queen).into(),
|
||||
Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Knight).into(),
|
||||
Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Bishop).into(),
|
||||
Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Rook).into(),
|
||||
Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Queen).into(),
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_e4_captures_d4_en_passant() {
|
||||
let board = test_board!(Black, [
|
||||
White Pawn on D4,
|
||||
Black Pawn on E4
|
||||
], D3);
|
||||
|
||||
let generated_moves = PawnMoveGenerator::new(&board, None);
|
||||
|
||||
assert_move_list!(generated_moves, [ply!(E4 - E3), ply!(E4 x D3 e.p.),]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_e5_captures_f5_en_passant() {
|
||||
let board = test_board!(White, [
|
||||
White Pawn on E5,
|
||||
Black Pawn on F5
|
||||
], F6);
|
||||
|
||||
let generated_moves = PawnMoveGenerator::new(&board, None);
|
||||
|
||||
assert_move_list!(generated_moves, [ply!(E5 - E6), ply!(E5 x F6 e.p.)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_no_en_passant_if_no_pawn() {
|
||||
let board = test_board!(White, [
|
||||
White Pawn on A3,
|
||||
Black Pawn on F5,
|
||||
], F6);
|
||||
|
||||
let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect();
|
||||
|
||||
assert_move_list_does_not_contain!(
|
||||
generated_moves,
|
||||
[ply!(E5 x F6 e.p.), ply!(G5 x F6 e.p.)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_no_en_passant_if_no_pawn() {
|
||||
let board = test_board!(Black, [
|
||||
White Pawn on A4,
|
||||
Black Pawn on D4,
|
||||
], A3);
|
||||
|
||||
let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect();
|
||||
|
||||
assert_move_list_does_not_contain!(generated_moves, [ply!(B4 x A3 e.p.)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_en_passant_check() {
|
||||
let board = Board::from_fen_str("8/8/8/2k5/2pP4/8/B7/4K3 b - d3 0 3").expect("invalid fen");
|
||||
println!("{}", board.display());
|
||||
|
||||
let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect();
|
||||
assert_move_list_contains!(generated_moves, [ply!(C4 x D3 e.p.)]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,612 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
//! Sliders in chess are the pieces that move (a.k.a. "slide") along straight
|
||||
//! line paths. Bishops, Rooks, and Queens all do this. All of these pieces
|
||||
//! function identically, though with different sets of rays emanating outward
|
||||
//! from their origin squares: rooks along orthogonal lines, bishops along
|
||||
//! diagonals, and queens along both orthogonal and diagonal lines.
|
||||
//!
|
||||
//! This module implements the [`SliderMoveGenerator`] which iterates all the
|
||||
//! slider moves from a given square. This module also exports
|
||||
//! [`BishopMoveGenerator`], [`RookMoveGenerator`], and [`QueenMoveGenerator`]
|
||||
//! that emit moves for their corresponding pieces.
|
||||
|
||||
use super::GeneratedMove;
|
||||
use crate::Move;
|
||||
use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard};
|
||||
use chessfriend_board::Board;
|
||||
use chessfriend_core::{Color, Slider, Square};
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
macro_rules! slider_move_generator {
|
||||
($vis:vis $name:ident, $slider:ident) => {
|
||||
#[must_use]
|
||||
$vis struct $name(SliderMoveGenerator);
|
||||
|
||||
impl $name {
|
||||
pub fn new(board: &Board, color: Option<Color>) -> Self {
|
||||
Self(SliderMoveGenerator::new(board, Slider::$slider, color))
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for $name {
|
||||
type Item = $crate::GeneratedMove;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for $name {}
|
||||
};
|
||||
}
|
||||
|
||||
slider_move_generator!(pub BishopMoveGenerator, Bishop);
|
||||
slider_move_generator!(pub RookMoveGenerator, Rook);
|
||||
slider_move_generator!(pub QueenMoveGenerator, Queen);
|
||||
|
||||
#[must_use]
|
||||
struct SliderMoveGenerator {
|
||||
sliders: Vec<SliderInfo>,
|
||||
next_sliders_index: usize,
|
||||
current_slider: Option<SliderInfo>,
|
||||
enemies: BitBoard,
|
||||
friends: BitBoard,
|
||||
}
|
||||
|
||||
impl SliderMoveGenerator {
|
||||
fn new(board: &Board, slider: Slider, color: Option<Color>) -> Self {
|
||||
let color = board.unwrap_color(color);
|
||||
|
||||
let pieces = match slider {
|
||||
Slider::Bishop => board.bishops(color),
|
||||
Slider::Rook => board.rooks(color),
|
||||
Slider::Queen => board.queens(color),
|
||||
};
|
||||
|
||||
let enemies = board.enemies(color);
|
||||
let friends = board.friendly_occupancy(color);
|
||||
|
||||
let sliders = pieces
|
||||
.occupied_squares_trailing()
|
||||
.map(|origin| SliderInfo::new(board, origin, slider, color))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
sliders,
|
||||
next_sliders_index: 0,
|
||||
current_slider: None,
|
||||
enemies,
|
||||
friends,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SliderMoveGenerator {
|
||||
type Item = GeneratedMove;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.current_slider.is_none() {
|
||||
if self.next_sliders_index < self.sliders.len() {
|
||||
self.current_slider = Some(self.sliders[self.next_sliders_index].clone());
|
||||
self.next_sliders_index += 1;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(current_slider) = self.current_slider.as_mut() {
|
||||
if let Some(target) = current_slider.next() {
|
||||
let target_bitboard: BitBoard = target.into();
|
||||
|
||||
let is_targeting_friendly_piece =
|
||||
(target_bitboard & self.friends).is_populated();
|
||||
if is_targeting_friendly_piece {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_targeting_enemy_piece = (target_bitboard & self.enemies).is_populated();
|
||||
return Some(if is_targeting_enemy_piece {
|
||||
Move::capture(current_slider.origin, target).into()
|
||||
} else {
|
||||
Move::quiet(current_slider.origin, target).into()
|
||||
});
|
||||
}
|
||||
|
||||
self.current_slider = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for SliderMoveGenerator {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SliderInfo {
|
||||
origin: Square,
|
||||
iterator: TrailingBitScanner,
|
||||
}
|
||||
|
||||
impl SliderInfo {
|
||||
fn new(board: &Board, origin: Square, slider: Slider, color: Color) -> Self {
|
||||
let color = Some(color);
|
||||
|
||||
let sight = match slider {
|
||||
Slider::Bishop => board.bishop_sight(origin, color),
|
||||
Slider::Rook => board.rook_sight(origin, color),
|
||||
Slider::Queen => board.queen_sight(origin, color),
|
||||
};
|
||||
|
||||
Self {
|
||||
origin,
|
||||
iterator: sight.occupied_squares_trailing(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SliderInfo {
|
||||
type Item = Square;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iterator.next()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{assert_move_list, ply};
|
||||
use chessfriend_board::test_board;
|
||||
|
||||
#[test]
|
||||
fn white_b5_rook() {
|
||||
let board = test_board!(White Rook on B5);
|
||||
assert_move_list!(
|
||||
RookMoveGenerator::new(&board, None),
|
||||
[
|
||||
ply!(B5 - A5),
|
||||
ply!(B5 - C5),
|
||||
ply!(B5 - D5),
|
||||
ply!(B5 - E5),
|
||||
ply!(B5 - F5),
|
||||
ply!(B5 - G5),
|
||||
ply!(B5 - H5),
|
||||
ply!(B5 - B8),
|
||||
ply!(B5 - B7),
|
||||
ply!(B5 - B6),
|
||||
ply!(B5 - B4),
|
||||
ply!(B5 - B3),
|
||||
ply!(B5 - B2),
|
||||
ply!(B5 - B1),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_f4_bishop() {
|
||||
let board = test_board!(White Bishop on F4);
|
||||
assert_move_list!(
|
||||
BishopMoveGenerator::new(&board, None),
|
||||
[
|
||||
ply!(F4 - B8),
|
||||
ply!(F4 - C7),
|
||||
ply!(F4 - D6),
|
||||
ply!(F4 - E5),
|
||||
ply!(F4 - H6),
|
||||
ply!(F4 - G5),
|
||||
ply!(F4 - C1),
|
||||
ply!(F4 - D2),
|
||||
ply!(F4 - E3),
|
||||
ply!(F4 - H2),
|
||||
ply!(F4 - G3),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_d4_queen_ai_claude() {
|
||||
let board = test_board!(White Queen on D4);
|
||||
assert_move_list!(
|
||||
QueenMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Horizontal moves (rook-like)
|
||||
ply!(D4 - A4),
|
||||
ply!(D4 - B4),
|
||||
ply!(D4 - C4),
|
||||
ply!(D4 - E4),
|
||||
ply!(D4 - F4),
|
||||
ply!(D4 - G4),
|
||||
ply!(D4 - H4),
|
||||
// Vertical moves (rook-like)
|
||||
ply!(D4 - D1),
|
||||
ply!(D4 - D2),
|
||||
ply!(D4 - D3),
|
||||
ply!(D4 - D5),
|
||||
ply!(D4 - D6),
|
||||
ply!(D4 - D7),
|
||||
ply!(D4 - D8),
|
||||
// Diagonal moves (bishop-like)
|
||||
ply!(D4 - A1),
|
||||
ply!(D4 - B2),
|
||||
ply!(D4 - C3),
|
||||
ply!(D4 - E5),
|
||||
ply!(D4 - F6),
|
||||
ply!(D4 - G7),
|
||||
ply!(D4 - H8),
|
||||
ply!(D4 - A7),
|
||||
ply!(D4 - B6),
|
||||
ply!(D4 - C5),
|
||||
ply!(D4 - E3),
|
||||
ply!(D4 - F2),
|
||||
ply!(D4 - G1),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_f3_bishop_with_capture_ai_claude() {
|
||||
let board = test_board!(
|
||||
White Bishop on F3,
|
||||
Black Pawn on C6
|
||||
);
|
||||
assert_move_list!(
|
||||
BishopMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Diagonal towards C6 (stops at capture)
|
||||
ply!(F3 - E4),
|
||||
ply!(F3 - D5),
|
||||
ply!(F3 x C6),
|
||||
// Diagonal towards H1
|
||||
ply!(F3 - G2),
|
||||
ply!(F3 - H1),
|
||||
// Diagonal towards A8
|
||||
ply!(F3 - G4),
|
||||
ply!(F3 - H5),
|
||||
// Diagonal towards E2
|
||||
ply!(F3 - E2),
|
||||
ply!(F3 - D1),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_e6_rook_with_capture_ai_claude() {
|
||||
let board = test_board!(
|
||||
White Rook on E6,
|
||||
Black Knight on E3
|
||||
);
|
||||
assert_move_list!(
|
||||
RookMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Horizontal moves
|
||||
ply!(E6 - A6),
|
||||
ply!(E6 - B6),
|
||||
ply!(E6 - C6),
|
||||
ply!(E6 - D6),
|
||||
ply!(E6 - F6),
|
||||
ply!(E6 - G6),
|
||||
ply!(E6 - H6),
|
||||
// Vertical moves up
|
||||
ply!(E6 - E7),
|
||||
ply!(E6 - E8),
|
||||
// Vertical moves down (stops at capture)
|
||||
ply!(E6 - E5),
|
||||
ply!(E6 - E4),
|
||||
ply!(E6 x E3),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_c4_queen_with_capture_ai_claude() {
|
||||
let board = test_board!(
|
||||
White Queen on C4,
|
||||
Black Bishop on F7
|
||||
);
|
||||
assert_move_list!(
|
||||
QueenMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Horizontal moves
|
||||
ply!(C4 - A4),
|
||||
ply!(C4 - B4),
|
||||
ply!(C4 - D4),
|
||||
ply!(C4 - E4),
|
||||
ply!(C4 - F4),
|
||||
ply!(C4 - G4),
|
||||
ply!(C4 - H4),
|
||||
// Vertical moves
|
||||
ply!(C4 - C1),
|
||||
ply!(C4 - C2),
|
||||
ply!(C4 - C3),
|
||||
ply!(C4 - C5),
|
||||
ply!(C4 - C6),
|
||||
ply!(C4 - C7),
|
||||
ply!(C4 - C8),
|
||||
// Diagonal moves (A2-G8 diagonal)
|
||||
ply!(C4 - A2),
|
||||
ply!(C4 - B3),
|
||||
ply!(C4 - D5),
|
||||
ply!(C4 - E6),
|
||||
ply!(C4 x F7),
|
||||
// Diagonal moves (F1-A6 diagonal)
|
||||
ply!(C4 - B5),
|
||||
ply!(C4 - A6),
|
||||
ply!(C4 - D3),
|
||||
ply!(C4 - E2),
|
||||
ply!(C4 - F1),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_rook_blocked_by_friendly_piece_ai_claude() {
|
||||
let board = test_board!(White Rook on D4, White Pawn on D6);
|
||||
assert_move_list!(
|
||||
RookMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Horizontal moves (unblocked)
|
||||
ply!(D4 - A4),
|
||||
ply!(D4 - B4),
|
||||
ply!(D4 - C4),
|
||||
ply!(D4 - E4),
|
||||
ply!(D4 - F4),
|
||||
ply!(D4 - G4),
|
||||
ply!(D4 - H4),
|
||||
// Vertical moves down (unblocked)
|
||||
ply!(D4 - D1),
|
||||
ply!(D4 - D2),
|
||||
ply!(D4 - D3),
|
||||
// Vertical moves up (blocked by friendly pawn on D6)
|
||||
ply!(D4 - D5),
|
||||
// Cannot move to D6 (occupied by friendly piece)
|
||||
// Cannot move to D7 or D8 (blocked by friendly piece on D6)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_bishop_blocked_by_friendly_pieces_ai_claude() {
|
||||
let board = test_board!(
|
||||
White Bishop on E4,
|
||||
White Knight on C2, // Blocks one diagonal
|
||||
White Pawn on G6 // Blocks another diagonal
|
||||
);
|
||||
assert_move_list!(
|
||||
BishopMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Diagonal towards H1 (unblocked)
|
||||
ply!(E4 - F3),
|
||||
ply!(E4 - G2),
|
||||
ply!(E4 - H1),
|
||||
// Diagonal towards A8 (blocked by pawn on G6)
|
||||
ply!(E4 - F5),
|
||||
// Cannot move to G6 (friendly pawn)
|
||||
// Cannot move to H7 (blocked by friendly pawn)
|
||||
// Diagonal towards H7 is blocked at G6
|
||||
// Diagonal towards A8 (unblocked on the other side)
|
||||
ply!(E4 - D5),
|
||||
ply!(E4 - C6),
|
||||
ply!(E4 - B7),
|
||||
ply!(E4 - A8),
|
||||
// Diagonal towards D3 (blocked by knight on C2)
|
||||
ply!(E4 - D3),
|
||||
// Cannot move to C2 (friendly knight)
|
||||
// Cannot move to B1 (blocked by friendly knight)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn white_queen_multiple_friendly_blocks_ai_claude() {
|
||||
let board = test_board!(
|
||||
White Queen on D4,
|
||||
White Pawn on D6, // Blocks vertical up
|
||||
White Bishop on F4, // Blocks horizontal right
|
||||
White Knight on F6 // Blocks diagonal
|
||||
);
|
||||
assert_move_list!(
|
||||
QueenMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Horizontal moves left (unblocked)
|
||||
ply!(D4 - A4),
|
||||
ply!(D4 - B4),
|
||||
ply!(D4 - C4),
|
||||
// Horizontal moves right (blocked by bishop on F4)
|
||||
ply!(D4 - E4),
|
||||
// Cannot move to F4 (friendly bishop)
|
||||
// Cannot move to G4, H4 (blocked by friendly bishop)
|
||||
|
||||
// Vertical moves down (unblocked)
|
||||
ply!(D4 - D1),
|
||||
ply!(D4 - D2),
|
||||
ply!(D4 - D3),
|
||||
// Vertical moves up (blocked by pawn on D6)
|
||||
ply!(D4 - D5),
|
||||
// Cannot move to D6 (friendly pawn)
|
||||
// Cannot move to D7, D8 (blocked by friendly pawn)
|
||||
|
||||
// Diagonal moves (some blocked, some not)
|
||||
// Towards A1
|
||||
ply!(D4 - C3),
|
||||
ply!(D4 - B2),
|
||||
ply!(D4 - A1),
|
||||
// Towards G1
|
||||
ply!(D4 - E3),
|
||||
ply!(D4 - F2),
|
||||
ply!(D4 - G1),
|
||||
// Towards A7
|
||||
ply!(D4 - C5),
|
||||
ply!(D4 - B6),
|
||||
ply!(D4 - A7),
|
||||
// Towards G7 (blocked by knight on F6)
|
||||
ply!(D4 - E5),
|
||||
// Cannot move to F6 (friendly knight)
|
||||
// Cannot move to G7, H8 (blocked by friendly knight)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rook_ray_stops_at_first_piece_ai_claude() {
|
||||
let board = test_board!(
|
||||
White Rook on A1,
|
||||
Black Pawn on A4, // First obstruction
|
||||
Black Queen on A7 // Should be unreachable
|
||||
);
|
||||
assert_move_list!(
|
||||
RookMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Horizontal moves (unblocked)
|
||||
ply!(A1 - B1),
|
||||
ply!(A1 - C1),
|
||||
ply!(A1 - D1),
|
||||
ply!(A1 - E1),
|
||||
ply!(A1 - F1),
|
||||
ply!(A1 - G1),
|
||||
ply!(A1 - H1),
|
||||
// Vertical moves up (terminated at A4)
|
||||
ply!(A1 - A2),
|
||||
ply!(A1 - A3),
|
||||
ply!(A1 x A4),
|
||||
// Cannot reach A5, A6, A7, A8 due to pawn blocking at A4
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bishop_ray_terminated_by_enemy_piece() {
|
||||
let board = test_board!(
|
||||
White Bishop on C1,
|
||||
Black Knight on F4, // Terminates one diagonal
|
||||
Black Rook on H6, // Should be unreachable behind the knight
|
||||
);
|
||||
assert_move_list!(
|
||||
BishopMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Diagonal towards A3
|
||||
ply!(C1 - B2),
|
||||
ply!(C1 - A3),
|
||||
// Diagonal towards H6 (terminated at F4)
|
||||
ply!(C1 - D2),
|
||||
ply!(C1 - E3),
|
||||
ply!(C1 x F4),
|
||||
// Cannot reach G5, H6 due to knight blocking at F4
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queen_multiple_ray_terminations_ai_claude() {
|
||||
let board = test_board!(
|
||||
White Queen on D4,
|
||||
Black Pawn on D7, // Terminates vertical ray
|
||||
Black Bishop on A7, // Capturable along diagonal
|
||||
Black Knight on G4, // Terminates horizontal ray
|
||||
Black Rook on H4, // Capturable along horizontal ray
|
||||
White Pawn on F6, // Terminates diagonal ray (friendly)
|
||||
Black Queen on H8,
|
||||
);
|
||||
assert_move_list!(
|
||||
QueenMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Horizontal moves left (unblocked)
|
||||
ply!(D4 - A4),
|
||||
ply!(D4 - B4),
|
||||
ply!(D4 - C4),
|
||||
// Horizontal moves right (terminated at G4)
|
||||
ply!(D4 - E4),
|
||||
ply!(D4 - F4),
|
||||
ply!(D4 x G4),
|
||||
// Cannot reach H4 due to knight blocking
|
||||
// Vertical moves down (unblocked)
|
||||
ply!(D4 - D1),
|
||||
ply!(D4 - D2),
|
||||
ply!(D4 - D3),
|
||||
// Vertical moves up (terminated at D7)
|
||||
ply!(D4 - D5),
|
||||
ply!(D4 - D6),
|
||||
ply!(D4 x D7),
|
||||
// Cannot reach D8 due to pawn blocking
|
||||
// Diagonal moves towards A1
|
||||
ply!(D4 - C3),
|
||||
ply!(D4 - B2),
|
||||
ply!(D4 - A1),
|
||||
// Diagonal moves towards G1
|
||||
ply!(D4 - E3),
|
||||
ply!(D4 - F2),
|
||||
ply!(D4 - G1),
|
||||
// Diagonal moves towards A7
|
||||
ply!(D4 - C5),
|
||||
ply!(D4 - B6),
|
||||
ply!(D4 x A7),
|
||||
// Diagonal moves towards H8 (terminated at F6 by friendly pawn)
|
||||
ply!(D4 - E5),
|
||||
// Cannot move to F6 (friendly pawn)
|
||||
// Cannot reach G7, H8 due to friendly pawn blocking
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rook_chain_of_pieces_ai_claude() {
|
||||
let board = test_board!(
|
||||
White Rook on A1,
|
||||
Black Pawn on A3, // First enemy piece
|
||||
White Knight on A5, // Friendly piece behind enemy
|
||||
Black Queen on A7 // Enemy piece behind friendly
|
||||
);
|
||||
assert_move_list!(
|
||||
RookMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Horizontal moves (unblocked)
|
||||
ply!(A1 - B1),
|
||||
ply!(A1 - C1),
|
||||
ply!(A1 - D1),
|
||||
ply!(A1 - E1),
|
||||
ply!(A1 - F1),
|
||||
ply!(A1 - G1),
|
||||
ply!(A1 - H1),
|
||||
// Vertical moves (ray terminates at first piece)
|
||||
ply!(A1 - A2),
|
||||
ply!(A1 x A3),
|
||||
// Cannot reach A4, A5, A6, A7, A8 - ray terminated at A3
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bishop_ray_termination_all_directions_ai_claude() {
|
||||
let board = test_board!(
|
||||
White Bishop on D4,
|
||||
Black Pawn on B2, // Terminates towards A1
|
||||
Black Knight on F6, // Terminates towards H8
|
||||
Black Rook on B6, // Terminates towards A7
|
||||
Black Queen on F2 // Terminates towards G1
|
||||
);
|
||||
assert_move_list!(
|
||||
BishopMoveGenerator::new(&board, None),
|
||||
[
|
||||
// Diagonal towards A1 (terminated at B2)
|
||||
ply!(D4 - C3),
|
||||
ply!(D4 x B2), // Capture
|
||||
// Cannot reach A1
|
||||
|
||||
// Diagonal towards H8 (terminated at F6)
|
||||
ply!(D4 - E5),
|
||||
ply!(D4 x F6), // Capture
|
||||
// Cannot reach G7, H8
|
||||
|
||||
// Diagonal towards A7 (terminated at B6)
|
||||
ply!(D4 - C5),
|
||||
ply!(D4 x B6), // Capture
|
||||
// Cannot reach A7
|
||||
|
||||
// Diagonal towards G1 (terminated at F2)
|
||||
ply!(D4 - E3),
|
||||
ply!(D4 x F2),
|
||||
// Cannot reach G1
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
pub mod algebraic;
|
||||
pub mod generators;
|
||||
pub mod testing;
|
||||
|
||||
mod defs;
|
||||
mod macros;
|
||||
mod make_move;
|
||||
mod moves;
|
||||
mod record;
|
||||
mod unmake_move;
|
||||
|
||||
pub use defs::{Kind, PromotionShape};
|
||||
pub use generators::GeneratedMove;
|
||||
pub use make_move::{MakeMove, MakeMoveError, MakeMoveResult, ValidateMove};
|
||||
pub use moves::Move;
|
||||
pub use record::MoveRecord;
|
||||
pub use unmake_move::{UnmakeMove, UnmakeMoveError, UnmakeMoveResult};
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_move_list {
|
||||
($generator:expr, [ $($expected:expr),* $(,)? ]) => {
|
||||
{
|
||||
let generated_moves: std::collections::HashSet<$crate::GeneratedMove> = $generator.collect();
|
||||
let expected_moves: std::collections::HashSet<$crate::GeneratedMove> = [
|
||||
$($expected.into(),)*
|
||||
].into();
|
||||
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
expected_moves,
|
||||
"\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}",
|
||||
generated_moves
|
||||
.intersection(&expected_moves)
|
||||
.map(|mv| format!("{}", mv))
|
||||
.collect::<Vec<String>>(),
|
||||
generated_moves
|
||||
.difference(&expected_moves)
|
||||
.map(|mv| format!("{}", mv))
|
||||
.collect::<Vec<String>>(),
|
||||
expected_moves
|
||||
.difference(&generated_moves)
|
||||
.map(|mv| format!("{}", mv))
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
}
|
||||
};
|
||||
($generator:expr, $expected:expr) => {
|
||||
{
|
||||
use std::collections::HashSet;
|
||||
|
||||
let generated_moves: HashSet<$crate::GeneratedMove> = $generator.collect();
|
||||
let expected_moves: HashSet<$crate::GeneratedMove> = $expected.collect();
|
||||
|
||||
assert_eq!(
|
||||
generated_moves,
|
||||
expected_moves,
|
||||
"\n\tMatching: {:?}\n\tGenerated, not expected: {:?}\n\tExpected, not generated: {:?}",
|
||||
generated_moves
|
||||
.intersection(&expected_moves)
|
||||
.map(|mv| format!("{}", mv))
|
||||
.collect::<Vec<String>>(),
|
||||
generated_moves
|
||||
.difference(&expected_moves)
|
||||
.map(|mv| format!("{}", mv))
|
||||
.collect::<Vec<String>>(),
|
||||
expected_moves
|
||||
.difference(&generated_moves)
|
||||
.map(|mv| format!("{}", mv))
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_move_list_contains {
|
||||
($generated_moves:expr, [ $($expected:expr),* $(,)? ]) => {
|
||||
$(
|
||||
assert!($generated_moves.contains(&$expected.into()));
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_move_list_does_not_contain {
|
||||
($generated_moves:expr, [ $($expected:expr),* $(,)? ]) => {
|
||||
{
|
||||
$(
|
||||
assert!(!$generated_moves.contains(&$expected.into()));
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,589 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{Move, MoveRecord};
|
||||
use chessfriend_board::{
|
||||
Board, BoardProvider, CastleParameters, PlacePieceError, PlacePieceStrategy,
|
||||
castle::{CastleEvaluationError, CastleRightsOption},
|
||||
movement::Movement,
|
||||
};
|
||||
use chessfriend_core::{Color, Piece, Rank, Shape, Square, Wing};
|
||||
use thiserror::Error;
|
||||
|
||||
pub type MakeMoveResult = Result<MoveRecord, MakeMoveError>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum ValidateMove {
|
||||
#[default]
|
||||
No,
|
||||
Yes,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
pub enum MakeMoveError {
|
||||
#[error("no piece on {0}")]
|
||||
NoPiece(Square),
|
||||
|
||||
#[error("{piece} on {square} is not of active color")]
|
||||
NonActiveColor { piece: Piece, square: Square },
|
||||
|
||||
#[error("{0} cannot make move")]
|
||||
InvalidPiece(Piece),
|
||||
|
||||
#[error("cannot capture piece on {0}")]
|
||||
InvalidCapture(Square),
|
||||
|
||||
#[error("cannot capture en passant on {0}")]
|
||||
InvalidEnPassantCapture(Square),
|
||||
|
||||
#[error("no capture square")]
|
||||
NoCaptureSquare,
|
||||
|
||||
#[error("no piece to capture on {0}")]
|
||||
NoCapturePiece(Square),
|
||||
|
||||
#[error("{piece} on {origin} cannot move to {target}")]
|
||||
NoMove {
|
||||
piece: Piece,
|
||||
origin: Square,
|
||||
target: Square,
|
||||
},
|
||||
|
||||
#[error("{0}")]
|
||||
PlacePieceError(#[from] PlacePieceError),
|
||||
|
||||
#[error("{0}")]
|
||||
CastleError(#[from] CastleEvaluationError),
|
||||
|
||||
#[error("cannot promote on {0}")]
|
||||
InvalidPromotion(Square),
|
||||
|
||||
#[error("move to {0} requires promotion")]
|
||||
PromotionRequired(Square),
|
||||
}
|
||||
|
||||
pub trait MakeMove {
|
||||
/// Make a move.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns one of [`MakeMoveError`] indicating why the move could not be
|
||||
/// made.
|
||||
///
|
||||
fn make_move(&mut self, ply: Move, validate: ValidateMove) -> MakeMoveResult;
|
||||
}
|
||||
|
||||
trait MakeMoveInternal {
|
||||
fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult;
|
||||
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult;
|
||||
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult;
|
||||
fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult;
|
||||
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult;
|
||||
|
||||
fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError>;
|
||||
fn validate_active_piece(&self, ply: Move) -> Result<Piece, MakeMoveError>;
|
||||
|
||||
fn advance_board_state(
|
||||
&mut self,
|
||||
ply: &Move,
|
||||
piece_moved: &Piece,
|
||||
en_passant_target: Option<Square>,
|
||||
half_move_clock: HalfMoveClock,
|
||||
);
|
||||
}
|
||||
|
||||
impl<T: BoardProvider> MakeMove for T {
|
||||
/// Make a move in the position.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// If `validate` is [`ValidateMove::Yes`], perform validation of move
|
||||
/// correctness prior to applying the move. See [`Position::validate_move`].
|
||||
fn make_move(
|
||||
&mut self,
|
||||
ply: Move,
|
||||
validate: ValidateMove,
|
||||
) -> Result<MoveRecord, MakeMoveError> {
|
||||
if ply.is_quiet() {
|
||||
self.validate_move(ply, validate)?;
|
||||
return self.make_quiet_move(ply);
|
||||
}
|
||||
|
||||
if ply.is_double_push() {
|
||||
self.validate_move(ply, validate)?;
|
||||
return self.make_double_push_move(ply);
|
||||
}
|
||||
|
||||
if ply.is_capture() {
|
||||
self.validate_move(ply, validate)?;
|
||||
return self.make_capture_move(ply);
|
||||
}
|
||||
|
||||
if let Some(wing) = ply.castle_wing() {
|
||||
return self.make_castle_move(ply, wing);
|
||||
}
|
||||
|
||||
if ply.is_promotion() {
|
||||
self.validate_move(ply, validate)?;
|
||||
return self.make_promotion_move(ply);
|
||||
}
|
||||
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BoardProvider> MakeMoveInternal for T {
|
||||
fn make_quiet_move(&mut self, ply: Move) -> MakeMoveResult {
|
||||
let board = self.board_mut();
|
||||
|
||||
let origin = ply.origin_square();
|
||||
|
||||
let piece = board
|
||||
.get_piece(origin)
|
||||
.ok_or(MakeMoveError::NoPiece(origin))?;
|
||||
|
||||
let target = ply.target_square();
|
||||
if piece.is_pawn() && target.rank().is_promotable_rank() {
|
||||
return Err(MakeMoveError::PromotionRequired(target));
|
||||
}
|
||||
|
||||
board
|
||||
.place_piece(piece, target, PlacePieceStrategy::PreserveExisting)
|
||||
.map_err(MakeMoveError::PlacePieceError)?;
|
||||
|
||||
board.remove_piece(origin);
|
||||
|
||||
let record = MoveRecord::new(board, ply, None);
|
||||
|
||||
self.advance_board_state(&ply, &piece, None, HalfMoveClock::Advance);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn make_double_push_move(&mut self, ply: Move) -> MakeMoveResult {
|
||||
let board = self.board_mut();
|
||||
|
||||
let origin = ply.origin_square();
|
||||
let piece = board
|
||||
.remove_piece(origin)
|
||||
.ok_or(MakeMoveError::NoPiece(origin))?;
|
||||
|
||||
let target = ply.target_square();
|
||||
board
|
||||
.place_piece(piece, target, PlacePieceStrategy::PreserveExisting)
|
||||
.map_err(MakeMoveError::PlacePieceError)?;
|
||||
|
||||
// Capture move record before setting the en passant square, to ensure
|
||||
// board state before the change is preserved.
|
||||
let record = MoveRecord::new(board, ply, None);
|
||||
|
||||
let en_passant_target = match target.rank() {
|
||||
Rank::FOUR => Square::from_file_rank(target.file(), Rank::THREE),
|
||||
Rank::FIVE => Square::from_file_rank(target.file(), Rank::SIX),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.advance_board_state(
|
||||
&ply,
|
||||
&piece,
|
||||
Some(en_passant_target),
|
||||
HalfMoveClock::Advance,
|
||||
);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn make_capture_move(&mut self, ply: Move) -> MakeMoveResult {
|
||||
let origin_square = ply.origin_square();
|
||||
let target_square = ply.target_square();
|
||||
|
||||
let board = self.board_mut();
|
||||
|
||||
let piece = board
|
||||
.get_piece(origin_square)
|
||||
.ok_or(MakeMoveError::NoPiece(origin_square))?;
|
||||
|
||||
if ply.is_en_passant() {
|
||||
let en_passant_square = board
|
||||
.en_passant_target()
|
||||
.ok_or(MakeMoveError::NoCaptureSquare)?;
|
||||
if target_square != en_passant_square {
|
||||
return Err(MakeMoveError::InvalidEnPassantCapture(target_square));
|
||||
}
|
||||
}
|
||||
|
||||
let board = self.board_mut();
|
||||
|
||||
let capture_square = ply.capture_square().ok_or(MakeMoveError::NoCaptureSquare)?;
|
||||
let captured_piece = board
|
||||
.remove_piece(capture_square)
|
||||
.ok_or(MakeMoveError::NoCapturePiece(capture_square))?;
|
||||
|
||||
board.remove_piece(origin_square).unwrap();
|
||||
|
||||
if let Some(promotion_shape) = ply.promotion_shape() {
|
||||
let promoted_piece = Piece::new(piece.color, promotion_shape);
|
||||
board.place_piece(promoted_piece, target_square, PlacePieceStrategy::Replace)?;
|
||||
} else {
|
||||
board.place_piece(piece, target_square, PlacePieceStrategy::Replace)?;
|
||||
}
|
||||
|
||||
let record = MoveRecord::new(board, ply, Some(captured_piece));
|
||||
|
||||
self.advance_board_state(&ply, &piece, None, HalfMoveClock::Reset);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn make_castle_move(&mut self, ply: Move, wing: Wing) -> MakeMoveResult {
|
||||
let board = self.board_mut();
|
||||
|
||||
board.color_can_castle(wing, None)?;
|
||||
|
||||
let active_color = board.active_color();
|
||||
let parameters = Board::castling_parameters(wing, active_color);
|
||||
|
||||
let king = board.remove_piece(parameters.origin.king).unwrap();
|
||||
board.place_piece(king, parameters.target.king, PlacePieceStrategy::default())?;
|
||||
|
||||
let rook = board.remove_piece(parameters.origin.rook).unwrap();
|
||||
board.place_piece(rook, parameters.target.rook, PlacePieceStrategy::default())?;
|
||||
|
||||
// Capture move record before revoking castling rights to ensure
|
||||
// original board state is preserved.
|
||||
let record = MoveRecord::new(board, ply, None);
|
||||
|
||||
board.revoke_castling_rights_active(wing.into());
|
||||
|
||||
self.advance_board_state(&ply, &king, None, HalfMoveClock::Advance);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn make_promotion_move(&mut self, ply: Move) -> MakeMoveResult {
|
||||
let board = self.board_mut();
|
||||
|
||||
let origin = ply.origin_square();
|
||||
|
||||
let piece = board
|
||||
.get_piece(origin)
|
||||
.ok_or(MakeMoveError::NoPiece(origin))?;
|
||||
if !piece.is_pawn() {
|
||||
return Err(MakeMoveError::InvalidPiece(piece));
|
||||
}
|
||||
|
||||
let target = ply.target_square();
|
||||
if !target.rank().is_promotable_rank() {
|
||||
return Err(MakeMoveError::InvalidPromotion(target));
|
||||
}
|
||||
|
||||
if let Some(promotion_shape) = ply.promotion_shape() {
|
||||
board.remove_piece(origin);
|
||||
let promoted_piece = Piece::new(piece.color, promotion_shape);
|
||||
board.place_piece(promoted_piece, target, PlacePieceStrategy::PreserveExisting)?;
|
||||
} else {
|
||||
unreachable!(
|
||||
"Cannot make a promotion move with a ply that has no promotion shape: {ply:?}",
|
||||
);
|
||||
}
|
||||
|
||||
let record = MoveRecord::new(board, ply, None);
|
||||
|
||||
self.advance_board_state(&ply, &piece, None, HalfMoveClock::Reset);
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn advance_board_state(
|
||||
&mut self,
|
||||
ply: &Move,
|
||||
piece_moved: &Piece,
|
||||
en_passant_target: Option<Square>,
|
||||
half_move_clock: HalfMoveClock,
|
||||
) {
|
||||
let board = self.board_mut();
|
||||
|
||||
board.set_en_passant_target_option(en_passant_target);
|
||||
|
||||
match piece_moved.shape {
|
||||
Shape::Rook => {
|
||||
let origin = ply.origin_square();
|
||||
|
||||
if board.has_castling_right(None, Wing::KingSide) {
|
||||
let kingside_parameters =
|
||||
CastleParameters::get(board.active_color(), Wing::KingSide);
|
||||
if origin == kingside_parameters.origin.rook {
|
||||
board.revoke_castling_rights(None, Wing::KingSide.into());
|
||||
}
|
||||
}
|
||||
|
||||
let queenside_parameters =
|
||||
CastleParameters::get(board.active_color(), Wing::QueenSide);
|
||||
if origin == queenside_parameters.origin.rook {
|
||||
board.revoke_castling_rights(None, Wing::QueenSide.into());
|
||||
}
|
||||
}
|
||||
Shape::King => {
|
||||
board.revoke_castling_rights(None, CastleRightsOption::All);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let previous_active_color = board.active_color();
|
||||
let active_color = previous_active_color.next();
|
||||
board.set_active_color(active_color);
|
||||
|
||||
match half_move_clock {
|
||||
HalfMoveClock::Reset => board.half_move_clock = 0,
|
||||
HalfMoveClock::Advance => board.half_move_clock += 1,
|
||||
}
|
||||
|
||||
if active_color == Color::White {
|
||||
board.full_move_number += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_move(&self, ply: Move, validate: ValidateMove) -> Result<(), MakeMoveError> {
|
||||
if validate == ValidateMove::No {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let active_piece = self.validate_active_piece(ply)?;
|
||||
|
||||
let board = self.board();
|
||||
let origin_square = ply.origin_square();
|
||||
let target_square = ply.target_square();
|
||||
|
||||
// Pawns can see squares they can't move to. So, calculating valid
|
||||
// squares requires a concept that includes Sight, but adds pawn pushes.
|
||||
// In ChessFriend, that concept is Movement.
|
||||
let movement = active_piece.movement(origin_square, board);
|
||||
if !movement.contains(target_square) {
|
||||
return Err(MakeMoveError::NoMove {
|
||||
piece: active_piece,
|
||||
origin: origin_square,
|
||||
target: target_square,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: En Passant capture.
|
||||
|
||||
if let Some(capture_square) = ply.capture_square() {
|
||||
if let Some(captured_piece) = board.get_piece(capture_square) {
|
||||
if captured_piece.color == active_piece.color {
|
||||
return Err(MakeMoveError::InvalidCapture(capture_square));
|
||||
}
|
||||
} else {
|
||||
return Err(MakeMoveError::NoPiece(capture_square));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_active_piece(&self, ply: Move) -> Result<Piece, MakeMoveError> {
|
||||
let origin_square = ply.origin_square();
|
||||
|
||||
let board = self.board();
|
||||
|
||||
let active_piece = board
|
||||
.get_piece(origin_square)
|
||||
.ok_or(MakeMoveError::NoPiece(origin_square))?;
|
||||
|
||||
if active_piece.color != board.active_color() {
|
||||
return Err(MakeMoveError::NonActiveColor {
|
||||
piece: active_piece,
|
||||
square: origin_square,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(active_piece)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
enum HalfMoveClock {
|
||||
Reset,
|
||||
#[default]
|
||||
Advance,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Move, PromotionShape};
|
||||
use chessfriend_board::test_board;
|
||||
use chessfriend_core::{Color, Square, piece};
|
||||
|
||||
type TestResult = Result<(), MakeMoveError>;
|
||||
|
||||
#[test]
|
||||
fn make_quiet_move() -> TestResult {
|
||||
let mut board = test_board!(White Pawn on C2);
|
||||
|
||||
let ply = Move::quiet(Square::C2, Square::C3);
|
||||
board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(board.get_piece(Square::C2), None);
|
||||
assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
assert_eq!(board.half_move_clock, 1);
|
||||
|
||||
board.set_active_color(Color::White);
|
||||
|
||||
let ply = Move::quiet(Square::C3, Square::C4);
|
||||
board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(board.get_piece(Square::C3), None);
|
||||
assert_eq!(board.get_piece(Square::C4), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
assert_eq!(board.half_move_clock, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_invalid_quiet_pawn_move() {
|
||||
let mut board = test_board!(White Pawn on C2);
|
||||
|
||||
let ply = Move::quiet(Square::C2, Square::D2);
|
||||
let result = board.make_move(ply, ValidateMove::Yes);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.get_piece(Square::D2), None);
|
||||
assert_eq!(board.active_color(), Color::White);
|
||||
assert_eq!(board.half_move_clock, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_capture_move() -> TestResult {
|
||||
let mut board = test_board![
|
||||
White Bishop on C2,
|
||||
Black Rook on F5,
|
||||
];
|
||||
|
||||
let ply = Move::capture(Square::C2, Square::F5);
|
||||
let result = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(result.captured_piece, Some(piece!(Black Rook)));
|
||||
|
||||
assert_eq!(board.get_piece(Square::C2), None);
|
||||
assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop)));
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
assert_eq!(board.half_move_clock, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_en_passant_capture_move() -> TestResult {
|
||||
let mut board = test_board![
|
||||
Black Pawn on F4,
|
||||
White Pawn on E2
|
||||
];
|
||||
|
||||
let ply = Move::double_push(Square::E2, Square::E4);
|
||||
let record = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(record.en_passant_target, None);
|
||||
|
||||
assert_eq!(board.get_piece(Square::E2), None);
|
||||
assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn)));
|
||||
assert_eq!(
|
||||
board.en_passant_target(),
|
||||
Some(Square::E3),
|
||||
"en passant square not set"
|
||||
);
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
assert_eq!(board.half_move_clock, 1);
|
||||
|
||||
let ply = Move::en_passant_capture(Square::F4, Square::E3);
|
||||
let result = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(result.captured_piece, Some(piece!(White Pawn)));
|
||||
|
||||
assert_eq!(board.get_piece(Square::F4), None);
|
||||
assert_eq!(board.get_piece(Square::E3), Some(piece!(Black Pawn)));
|
||||
assert_eq!(
|
||||
board.get_piece(Square::E4),
|
||||
None,
|
||||
"capture target pawn not removed"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_last_rank_quiet_move_without_promotion() {
|
||||
let mut board = test_board!(
|
||||
White Pawn on A7
|
||||
);
|
||||
|
||||
let ply = Move::quiet(Square::A7, Square::A8);
|
||||
let result = board.make_move(ply, ValidateMove::Yes);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(board.active_color(), Color::White);
|
||||
assert_eq!(board.get_piece(Square::A7), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.get_piece(Square::A8), None);
|
||||
assert_eq!(board.half_move_clock, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_promotion_move() -> TestResult {
|
||||
let mut board = test_board![
|
||||
Black Pawn on E7,
|
||||
White Pawn on F7,
|
||||
];
|
||||
|
||||
let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen);
|
||||
board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(board.get_piece(Square::F7), None);
|
||||
assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen)));
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
assert_eq!(board.half_move_clock, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_white_kingside_castle() -> TestResult {
|
||||
let mut board = test_board![
|
||||
White Rook on H1,
|
||||
White King on E1,
|
||||
];
|
||||
|
||||
let ply = Move::castle(Color::White, Wing::KingSide);
|
||||
board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
assert_eq!(board.get_piece(Square::E1), None);
|
||||
assert_eq!(board.get_piece(Square::H1), None);
|
||||
assert_eq!(board.get_piece(Square::G1), Some(piece!(White King)));
|
||||
assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook)));
|
||||
assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_white_queenside_castle() -> TestResult {
|
||||
let mut board = test_board![
|
||||
White King on E1,
|
||||
White Rook on A1,
|
||||
];
|
||||
|
||||
let ply = Move::castle(Color::White, Wing::QueenSide);
|
||||
board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
assert_eq!(board.get_piece(Square::E1), None);
|
||||
assert_eq!(board.get_piece(Square::A1), None);
|
||||
assert_eq!(board.get_piece(Square::C1), Some(piece!(White King)));
|
||||
assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook)));
|
||||
assert!(!board.has_castling_right_unwrapped(Color::White, Wing::QueenSide));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,292 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::defs::{Kind, PromotionShape};
|
||||
use chessfriend_bitboard::BitBoard;
|
||||
use chessfriend_board::CastleParameters;
|
||||
use chessfriend_core::{Color, Rank, Shape, Square, Wing};
|
||||
use std::fmt;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ply {
|
||||
($origin:ident - $target:ident) => {
|
||||
$crate::Move::quiet(
|
||||
chessfriend_core::Square::$origin,
|
||||
chessfriend_core::Square::$target,
|
||||
)
|
||||
};
|
||||
($origin:ident -- $target:ident) => {
|
||||
$crate::Move::double_push(
|
||||
chessfriend_core::Square::$origin,
|
||||
chessfriend_core::Square::$target,
|
||||
)
|
||||
};
|
||||
($origin:ident x $target:ident) => {
|
||||
$crate::Move::capture(
|
||||
chessfriend_core::Square::$origin,
|
||||
chessfriend_core::Square::$target,
|
||||
)
|
||||
};
|
||||
($origin:ident x $target:ident e$(.)?p$(.)?) => {
|
||||
$crate::Move::en_passant_capture(
|
||||
chessfriend_core::Square::$origin,
|
||||
chessfriend_core::Square::$target,
|
||||
)
|
||||
};
|
||||
($origin:ident x $target:ident = $promotion:ident) => {
|
||||
$crate::Move::capture_promotion(
|
||||
chessfriend_core::Square::$origin,
|
||||
chessfriend_core::Square::$target,
|
||||
$crate::PromotionShape::$promotion,
|
||||
)
|
||||
};
|
||||
($origin:ident - $target:ident = $promotion:ident) => {
|
||||
$crate::Move::promotion(
|
||||
chessfriend_core::Square::$origin,
|
||||
chessfriend_core::Square::$target,
|
||||
$crate::PromotionShape::$promotion,
|
||||
)
|
||||
};
|
||||
($color:ident 0-0) => {
|
||||
$crate::Move::castle(
|
||||
chessfriend_core::Color::$color,
|
||||
chessfriend_core::Wing::KingSide,
|
||||
)
|
||||
};
|
||||
($color:ident 0-0-0) => {
|
||||
$crate::Move::castle(
|
||||
chessfriend_core::Color::$color,
|
||||
chessfriend_core::Wing::QueenSide,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// A single player's move. In game theory parlance, this is a "ply."
|
||||
///
|
||||
/// ## TODO
|
||||
///
|
||||
/// - Rename this class `Ply`.
|
||||
///
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub struct Move(pub(crate) u16);
|
||||
|
||||
fn origin_bits(square: Square) -> u16 {
|
||||
(square as u16) << 4
|
||||
}
|
||||
|
||||
fn target_bits(square: Square) -> u16 {
|
||||
(square as u16) << 10
|
||||
}
|
||||
|
||||
impl Move {
|
||||
#[must_use]
|
||||
pub const fn null() -> Self {
|
||||
Move(0)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn quiet(origin: Square, target: Square) -> Self {
|
||||
Move(origin_bits(origin) | target_bits(target))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn double_push(origin: Square, target: Square) -> Self {
|
||||
let flag_bits = Kind::DoublePush as u16;
|
||||
Move(origin_bits(origin) | target_bits(target) | flag_bits)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn capture(origin: Square, target: Square) -> Self {
|
||||
let flag_bits = Kind::Capture as u16;
|
||||
Move(origin_bits(origin) | target_bits(target) | flag_bits)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn capture_promotion(origin: Square, target: Square, shape: PromotionShape) -> Self {
|
||||
let flag_bits = Kind::CapturePromotion as u16;
|
||||
let shape_bits = shape as u16;
|
||||
Move(origin_bits(origin) | target_bits(target) | flag_bits | shape_bits)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn en_passant_capture(origin: Square, target: Square) -> Self {
|
||||
let flag_bits = Kind::EnPassantCapture as u16;
|
||||
Move(origin_bits(origin) | target_bits(target) | flag_bits)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn promotion(origin: Square, target: Square, shape: PromotionShape) -> Self {
|
||||
let flag_bits = Kind::Promotion as u16;
|
||||
let shape_bits = shape as u16;
|
||||
Move(origin_bits(origin) | target_bits(target) | flag_bits | shape_bits)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn castle(color: Color, wing: Wing) -> Self {
|
||||
let flag_bits = match wing {
|
||||
Wing::KingSide => Kind::KingSideCastle,
|
||||
Wing::QueenSide => Kind::QueenSideCastle,
|
||||
} as u16;
|
||||
|
||||
let parameters = CastleParameters::get(color, wing);
|
||||
let origin = parameters.origin.king;
|
||||
let target = parameters.target.king;
|
||||
|
||||
Move(origin_bits(origin) | target_bits(target) | flag_bits)
|
||||
}
|
||||
}
|
||||
|
||||
impl Move {
|
||||
#[must_use]
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn origin_square(&self) -> Square {
|
||||
((self.0 >> 4) & 0b111_111).try_into().unwrap()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn target_square(&self) -> Square {
|
||||
(self.0 >> 10).try_into().unwrap()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn capture_square(&self) -> Option<Square> {
|
||||
if self.is_en_passant() {
|
||||
let target_square = self.target_square();
|
||||
return Some(match target_square.rank() {
|
||||
Rank::THREE => Square::from_file_rank(target_square.file(), Rank::FOUR),
|
||||
Rank::SIX => Square::from_file_rank(target_square.file(), Rank::FIVE),
|
||||
_ => unreachable!(),
|
||||
});
|
||||
}
|
||||
|
||||
if self.is_capture() {
|
||||
return Some(self.target_square());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_quiet(&self) -> bool {
|
||||
self.flags() == Kind::Quiet as u16
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_double_push(&self) -> bool {
|
||||
self.flags() == Kind::DoublePush as u16
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_castle(&self) -> bool {
|
||||
(self.0 & 0b0010) != 0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn castle_wing(&self) -> Option<Wing> {
|
||||
match self.flags() {
|
||||
0b0010 => Some(Wing::KingSide),
|
||||
0b0011 => Some(Wing::QueenSide),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_capture(&self) -> bool {
|
||||
(self.0 & Kind::Capture as u16) != 0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_en_passant(&self) -> bool {
|
||||
self.flags() == Kind::EnPassantCapture as u16
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_promotion(&self) -> bool {
|
||||
(self.0 & Kind::Promotion as u16) != 0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn promotion_shape(&self) -> Option<Shape> {
|
||||
if !self.is_promotion() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(match self.special() {
|
||||
0b00 => Shape::Knight,
|
||||
0b01 => Shape::Bishop,
|
||||
0b10 => Shape::Rook,
|
||||
0b11 => Shape::Queen,
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Move {
|
||||
#[inline]
|
||||
fn flags(self) -> u16 {
|
||||
self.0 & 0b1111
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn special(self) -> u16 {
|
||||
self.0 & 0b11
|
||||
}
|
||||
}
|
||||
|
||||
impl Move {
|
||||
pub fn relevant_squares(&self) -> BitBoard {
|
||||
[
|
||||
Some(self.origin_square()),
|
||||
Some(self.target_square()),
|
||||
self.capture_square(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Move {
|
||||
fn transfer_char(self) -> char {
|
||||
if self.is_capture() || self.is_en_passant() {
|
||||
'x'
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const KINGSIDE_CASTLE_STR: &str = "0-0";
|
||||
const QUEENSIDE_CASTLE_STR: &str = "0-0-0";
|
||||
|
||||
impl fmt::Display for Move {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(castle) = self.castle_wing() {
|
||||
return match castle {
|
||||
Wing::KingSide => write!(f, "{KINGSIDE_CASTLE_STR}"),
|
||||
Wing::QueenSide => write!(f, "{QUEENSIDE_CASTLE_STR}"),
|
||||
};
|
||||
}
|
||||
|
||||
let origin = self.origin_square();
|
||||
let target = self.target_square();
|
||||
let transfer_char = self.transfer_char();
|
||||
write!(f, "{origin}{transfer_char}{target}")?;
|
||||
|
||||
if let Some(promotion) = self.promotion_shape() {
|
||||
write!(f, "={promotion}")?;
|
||||
} else if self.is_en_passant() {
|
||||
write!(f, " e.p.")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Move {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Move")
|
||||
.field(&format_args!("{:08b}", &self.0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::Move;
|
||||
use chessfriend_board::{Board, CastleRights, board::HalfMoveClock};
|
||||
use chessfriend_core::{Color, Piece, Square};
|
||||
|
||||
/// A record of a move made on a board. This struct contains all the information
|
||||
/// necessary to restore the board position to its state immediately before the
|
||||
/// move was made.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MoveRecord {
|
||||
/// The color of the player who made the move
|
||||
pub color: Color,
|
||||
|
||||
/// The move played
|
||||
pub ply: Move,
|
||||
|
||||
/// The en passant square when the move was made
|
||||
pub en_passant_target: Option<Square>,
|
||||
|
||||
/// Castling rights for all players when the move was made
|
||||
pub castling_rights: CastleRights,
|
||||
|
||||
/// The half move clock when the move was made
|
||||
pub half_move_clock: HalfMoveClock,
|
||||
|
||||
/// The piece captured by the move, if any
|
||||
pub captured_piece: Option<Piece>,
|
||||
}
|
||||
|
||||
impl MoveRecord {
|
||||
#[must_use]
|
||||
pub fn new(board: &Board, ply: Move, capture: Option<Piece>) -> Self {
|
||||
Self {
|
||||
color: board.active_color(),
|
||||
ply,
|
||||
en_passant_target: board.en_passant_target(),
|
||||
castling_rights: *board.castling_rights(),
|
||||
half_move_clock: board.half_move_clock,
|
||||
captured_piece: capture,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
pub type TestResult = Result<(), TestError>;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum TestError {
|
||||
NoLegalMoves,
|
||||
}
|
||||
|
|
@ -1,486 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::MoveRecord;
|
||||
use chessfriend_board::{Board, BoardProvider, PlacePieceError, PlacePieceStrategy};
|
||||
use chessfriend_core::{Piece, Square};
|
||||
use thiserror::Error;
|
||||
|
||||
pub type UnmakeMoveResult = Result<(), UnmakeMoveError>;
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
pub enum UnmakeMoveError {
|
||||
#[error("no move to unmake")]
|
||||
NoMove,
|
||||
|
||||
#[error("no piece on {0}")]
|
||||
NoPiece(Square),
|
||||
|
||||
#[error("no capture square")]
|
||||
NoCaptureSquare,
|
||||
|
||||
#[error("no captured piece to unmake capture move")]
|
||||
NoCapturedPiece,
|
||||
|
||||
#[error("{0}")]
|
||||
PlacePieceError(#[from] PlacePieceError),
|
||||
}
|
||||
|
||||
pub trait UnmakeMove {
|
||||
/// Unmake the given move. Unmaking a move that wasn't the most recent one
|
||||
/// made will likely cause strange results.
|
||||
///
|
||||
/// ## To-Do
|
||||
///
|
||||
/// Some implementations I've seen in other engines take a move to unmake. I
|
||||
/// don't understand why they do this because I don't think it makes sense
|
||||
/// to unmake any move other than the last move made. I need to do some
|
||||
/// research on this to understand if/when passing a move might be useful or
|
||||
/// necessary.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns one of [`UnmakeMoveError`] indicating why the move cannot be
|
||||
/// unmade.
|
||||
///
|
||||
fn unmake_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
|
||||
}
|
||||
|
||||
trait UnmakeMoveInternal {
|
||||
fn unmake_quiet_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
|
||||
fn unmake_capture_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
|
||||
fn unmake_promotion_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
|
||||
fn unmake_castle_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult;
|
||||
}
|
||||
|
||||
impl<T: BoardProvider> UnmakeMove for T {
|
||||
fn unmake_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
|
||||
let ply = record.ply;
|
||||
|
||||
if ply.is_quiet() || ply.is_double_push() {
|
||||
self.unmake_quiet_move(record)?;
|
||||
} else if ply.is_capture() || ply.is_en_passant() {
|
||||
self.unmake_capture_move(record)?;
|
||||
} else if ply.is_promotion() {
|
||||
self.unmake_promotion_move(record)?;
|
||||
} else if ply.is_castle() {
|
||||
self.unmake_castle_move(record)?;
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
let board = self.board_mut();
|
||||
board.set_active_color(record.color);
|
||||
board.set_en_passant_target_option(record.en_passant_target);
|
||||
board.set_castling_rights(record.castling_rights);
|
||||
board.half_move_clock = record.half_move_clock;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BoardProvider> UnmakeMoveInternal for T {
|
||||
fn unmake_quiet_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
|
||||
let board = self.board_mut();
|
||||
|
||||
let ply = record.ply;
|
||||
|
||||
let target = ply.target_square();
|
||||
let piece = board
|
||||
.get_piece(target)
|
||||
.ok_or(UnmakeMoveError::NoPiece(target))?;
|
||||
|
||||
let origin = ply.origin_square();
|
||||
board.place_piece(piece, origin, PlacePieceStrategy::PreserveExisting)?;
|
||||
|
||||
board.remove_piece(target);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unmake_capture_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
|
||||
let board = self.board_mut();
|
||||
|
||||
let ply = record.ply;
|
||||
|
||||
let target = ply.target_square();
|
||||
|
||||
let mut piece = board
|
||||
.get_piece(target)
|
||||
.ok_or(UnmakeMoveError::NoPiece(target))?;
|
||||
if ply.is_promotion() {
|
||||
piece = Piece::pawn(piece.color);
|
||||
}
|
||||
|
||||
let origin = ply.origin_square();
|
||||
board.place_piece(piece, origin, PlacePieceStrategy::PreserveExisting)?;
|
||||
|
||||
let capture_square = ply
|
||||
.capture_square()
|
||||
.ok_or(UnmakeMoveError::NoCaptureSquare)?;
|
||||
let captured_piece = record
|
||||
.captured_piece
|
||||
.ok_or(UnmakeMoveError::NoCapturedPiece)?;
|
||||
|
||||
board.remove_piece(target);
|
||||
|
||||
board.place_piece(
|
||||
captured_piece,
|
||||
capture_square,
|
||||
PlacePieceStrategy::PreserveExisting,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unmake_promotion_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
|
||||
let board = self.board_mut();
|
||||
|
||||
let ply = record.ply;
|
||||
|
||||
let target = ply.target_square();
|
||||
|
||||
let piece = Piece::pawn(
|
||||
board
|
||||
.get_piece(target)
|
||||
.ok_or(UnmakeMoveError::NoPiece(target))?
|
||||
.color,
|
||||
);
|
||||
|
||||
let origin = ply.origin_square();
|
||||
board.place_piece(piece, origin, PlacePieceStrategy::PreserveExisting)?;
|
||||
|
||||
board.remove_piece(target);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unmake_castle_move(&mut self, record: &MoveRecord) -> UnmakeMoveResult {
|
||||
let ply = record.ply;
|
||||
|
||||
let wing = ply.castle_wing().expect("no wing for unmaking castle move");
|
||||
let color = record.color;
|
||||
let parameters = Board::castling_parameters(wing, color);
|
||||
|
||||
let board = self.board_mut();
|
||||
|
||||
let king = board
|
||||
.get_piece(parameters.target.king)
|
||||
.ok_or(UnmakeMoveError::NoPiece(parameters.target.king))?;
|
||||
let rook = board
|
||||
.get_piece(parameters.target.rook)
|
||||
.ok_or(UnmakeMoveError::NoPiece(parameters.target.rook))?;
|
||||
|
||||
board.place_piece(
|
||||
king,
|
||||
parameters.origin.king,
|
||||
PlacePieceStrategy::PreserveExisting,
|
||||
)?;
|
||||
board.place_piece(
|
||||
rook,
|
||||
parameters.origin.rook,
|
||||
PlacePieceStrategy::PreserveExisting,
|
||||
)?;
|
||||
|
||||
board.remove_piece(parameters.target.king);
|
||||
board.remove_piece(parameters.target.rook);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{MakeMove, Move, PromotionShape, ValidateMove};
|
||||
use chessfriend_board::test_board;
|
||||
use chessfriend_core::{Color, Square, Wing, piece};
|
||||
|
||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
/// Helper function to test make/unmake idempotency
|
||||
fn test_make_unmake_idempotent(
|
||||
initial_board: &mut impl BoardProvider,
|
||||
ply: Move,
|
||||
) -> TestResult {
|
||||
// Capture initial state
|
||||
let initial_state = initial_board.board().clone();
|
||||
|
||||
// Make the move
|
||||
let record = initial_board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
// Verify the move changed the board
|
||||
assert_ne!(
|
||||
*initial_board.board(),
|
||||
initial_state,
|
||||
"Move should change board state"
|
||||
);
|
||||
|
||||
// Unmake the move
|
||||
initial_board.unmake_move(&record)?;
|
||||
|
||||
// Verify we're back to the initial state
|
||||
assert_eq!(
|
||||
*initial_board.board(),
|
||||
initial_state,
|
||||
"Board should return to initial state after unmake"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmake_quiet_move_ai_claude() -> TestResult {
|
||||
let mut board = test_board!(White Pawn on C2);
|
||||
|
||||
let ply = Move::quiet(Square::C2, Square::C3);
|
||||
let record = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
// Verify move was made
|
||||
assert_eq!(board.get_piece(Square::C2), None);
|
||||
assert_eq!(board.get_piece(Square::C3), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
|
||||
board.unmake_move(&record)?;
|
||||
|
||||
// Verify original state restored
|
||||
assert_eq!(board.get_piece(Square::C2), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.get_piece(Square::C3), None);
|
||||
assert_eq!(board.active_color(), Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmake_double_push_move_ai_claude() -> TestResult {
|
||||
let mut board = test_board!(White Pawn on E2);
|
||||
|
||||
let ply = Move::double_push(Square::E2, Square::E4);
|
||||
let record = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
// Verify move was made
|
||||
assert_eq!(board.get_piece(Square::E2), None);
|
||||
assert_eq!(board.get_piece(Square::E4), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.en_passant_target(), Some(Square::E3));
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
|
||||
board.unmake_move(&record)?;
|
||||
|
||||
// Verify original state restored
|
||||
assert_eq!(board.get_piece(Square::E2), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.get_piece(Square::E4), None);
|
||||
assert_eq!(board.en_passant_target(), None);
|
||||
assert_eq!(board.active_color(), Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmake_capture_move_ai_claude() -> TestResult {
|
||||
let mut board = test_board![
|
||||
White Bishop on C2,
|
||||
Black Rook on F5,
|
||||
];
|
||||
|
||||
let ply = Move::capture(Square::C2, Square::F5);
|
||||
let record = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
// Verify move was made
|
||||
assert_eq!(board.get_piece(Square::C2), None);
|
||||
assert_eq!(board.get_piece(Square::F5), Some(piece!(White Bishop)));
|
||||
assert_eq!(record.captured_piece, Some(piece!(Black Rook)));
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
|
||||
board.unmake_move(&record)?;
|
||||
|
||||
// Verify original state restored
|
||||
assert_eq!(board.get_piece(Square::C2), Some(piece!(White Bishop)));
|
||||
assert_eq!(board.get_piece(Square::F5), Some(piece!(Black Rook)));
|
||||
assert_eq!(board.active_color(), Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmake_en_passant_capture_ai_claude() -> TestResult {
|
||||
let mut board = test_board![
|
||||
Black Pawn on F4,
|
||||
White Pawn on E2
|
||||
];
|
||||
|
||||
// Set up en passant situation
|
||||
let double_push = Move::double_push(Square::E2, Square::E4);
|
||||
board.make_move(double_push, ValidateMove::Yes)?;
|
||||
|
||||
// Make en passant capture
|
||||
let en_passant = Move::en_passant_capture(Square::F4, Square::E3);
|
||||
let record = board.make_move(en_passant, ValidateMove::Yes)?;
|
||||
|
||||
// Verify en passant was made
|
||||
assert_eq!(board.get_piece(Square::F4), None);
|
||||
assert_eq!(board.get_piece(Square::E3), Some(piece!(Black Pawn)));
|
||||
assert_eq!(
|
||||
board.get_piece(Square::E4),
|
||||
None,
|
||||
"captured pawn was not removed"
|
||||
);
|
||||
assert_eq!(record.captured_piece, Some(piece!(White Pawn)));
|
||||
|
||||
board.unmake_move(&record)?;
|
||||
|
||||
// Verify state before en passant is restored
|
||||
assert_eq!(board.get_piece(Square::F4), Some(piece!(Black Pawn)));
|
||||
assert_eq!(board.get_piece(Square::E3), None);
|
||||
assert_eq!(
|
||||
board.get_piece(Square::E4),
|
||||
Some(piece!(White Pawn)),
|
||||
"captured pawn was not restored"
|
||||
);
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmake_promotion_move_ai_claude() -> TestResult {
|
||||
let mut board = test_board![
|
||||
White Pawn on F7,
|
||||
];
|
||||
|
||||
let ply = Move::promotion(Square::F7, Square::F8, PromotionShape::Queen);
|
||||
let record = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
// Verify promotion was made
|
||||
assert_eq!(board.get_piece(Square::F7), None);
|
||||
assert_eq!(board.get_piece(Square::F8), Some(piece!(White Queen)));
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
|
||||
board.unmake_move(&record)?;
|
||||
|
||||
// Verify original pawn is restored
|
||||
assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.get_piece(Square::F8), None);
|
||||
assert_eq!(board.active_color(), Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmake_capture_promotion_ai_claude() -> TestResult {
|
||||
let mut board = test_board![
|
||||
White Pawn on F7,
|
||||
Black Rook on G8,
|
||||
];
|
||||
|
||||
let ply = Move::capture_promotion(Square::F7, Square::G8, PromotionShape::Queen);
|
||||
let record = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
// Verify promotion capture was made
|
||||
assert_eq!(board.get_piece(Square::F7), None);
|
||||
assert_eq!(board.get_piece(Square::G8), Some(piece!(White Queen)));
|
||||
assert_eq!(record.captured_piece, Some(piece!(Black Rook)));
|
||||
|
||||
board.unmake_move(&record)?;
|
||||
|
||||
// Verify original state restored
|
||||
assert_eq!(board.get_piece(Square::F7), Some(piece!(White Pawn)));
|
||||
assert_eq!(board.get_piece(Square::G8), Some(piece!(Black Rook)));
|
||||
assert_eq!(board.active_color(), Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmake_white_kingside_castle_ai_claude() -> TestResult {
|
||||
let mut board = test_board![
|
||||
White King on E1,
|
||||
White Rook on H1,
|
||||
];
|
||||
|
||||
let original_castling_rights = *board.castling_rights();
|
||||
|
||||
let ply = Move::castle(Color::White, Wing::KingSide);
|
||||
let record = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
// Verify castle was made
|
||||
assert_eq!(board.get_piece(Square::E1), None);
|
||||
assert_eq!(board.get_piece(Square::H1), None);
|
||||
assert_eq!(board.get_piece(Square::G1), Some(piece!(White King)));
|
||||
assert_eq!(board.get_piece(Square::F1), Some(piece!(White Rook)));
|
||||
assert!(!board.has_castling_right_unwrapped(Color::White, Wing::KingSide));
|
||||
|
||||
board.unmake_move(&record)?;
|
||||
|
||||
// Verify original state restored
|
||||
assert_eq!(board.get_piece(Square::E1), Some(piece!(White King)));
|
||||
assert_eq!(board.get_piece(Square::H1), Some(piece!(White Rook)));
|
||||
assert_eq!(board.get_piece(Square::G1), None);
|
||||
assert_eq!(board.get_piece(Square::F1), None);
|
||||
assert_eq!(*board.castling_rights(), original_castling_rights);
|
||||
assert_eq!(board.active_color(), Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmake_white_queenside_castle_ai_claude() -> TestResult {
|
||||
let mut board = test_board![
|
||||
White King on E1,
|
||||
White Rook on A1,
|
||||
];
|
||||
|
||||
let original_castling_rights = *board.castling_rights();
|
||||
|
||||
let ply = Move::castle(Color::White, Wing::QueenSide);
|
||||
let record = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
// Verify castle was made
|
||||
assert_eq!(board.get_piece(Square::E1), None);
|
||||
assert_eq!(board.get_piece(Square::A1), None);
|
||||
assert_eq!(board.get_piece(Square::C1), Some(piece!(White King)));
|
||||
assert_eq!(board.get_piece(Square::D1), Some(piece!(White Rook)));
|
||||
assert!(!board.has_castling_right_unwrapped(Color::White, Wing::QueenSide));
|
||||
|
||||
board.unmake_move(&record)?;
|
||||
|
||||
// Verify original state restored
|
||||
assert_eq!(board.get_piece(Square::E1), Some(piece!(White King)));
|
||||
assert_eq!(board.get_piece(Square::A1), Some(piece!(White Rook)));
|
||||
assert_eq!(board.get_piece(Square::C1), None);
|
||||
assert_eq!(board.get_piece(Square::D1), None);
|
||||
assert_eq!(*board.castling_rights(), original_castling_rights);
|
||||
assert_eq!(board.active_color(), Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmake_black_kingside_castle() -> TestResult {
|
||||
let mut board = test_board!(Black, [
|
||||
Black King on E8,
|
||||
Black Rook on H8,
|
||||
]);
|
||||
|
||||
let original_castling_rights = *board.castling_rights();
|
||||
|
||||
let ply = Move::castle(Color::Black, Wing::KingSide);
|
||||
let record = board.make_move(ply, ValidateMove::Yes)?;
|
||||
|
||||
// Verify castle was made
|
||||
assert_eq!(board.get_piece(Square::E8), None);
|
||||
assert_eq!(board.get_piece(Square::H8), None);
|
||||
assert_eq!(board.get_piece(Square::G8), Some(piece!(Black King)));
|
||||
assert_eq!(board.get_piece(Square::F8), Some(piece!(Black Rook)));
|
||||
|
||||
board.unmake_move(&record)?;
|
||||
|
||||
// Verify original state restored
|
||||
assert_eq!(board.get_piece(Square::E8), Some(piece!(Black King)));
|
||||
assert_eq!(board.get_piece(Square::H8), Some(piece!(Black Rook)));
|
||||
assert_eq!(board.get_piece(Square::G8), None);
|
||||
assert_eq!(board.get_piece(Square::F8), None);
|
||||
assert_eq!(*board.castling_rights(), original_castling_rights);
|
||||
assert_eq!(board.active_color(), Color::Black);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
'''
|
||||
New script.
|
||||
'''
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
def run_perft(fen, depth):
|
||||
result = subprocess.run(
|
||||
[
|
||||
'cargo',
|
||||
'run',
|
||||
'--',
|
||||
'--fen', fen,
|
||||
str(depth)
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
nodes_count = 0
|
||||
for line in result.stdout.splitlines():
|
||||
if line.startswith('nodes '):
|
||||
(_, nodes_count) = line.split(' ')
|
||||
nodes_count = int(nodes_count)
|
||||
|
||||
return nodes_count
|
||||
|
||||
|
||||
def parse_args(argv, *a, **kw):
|
||||
parser = argparse.ArgumentParser(*a, **kw)
|
||||
parser.add_argument('-c', '--continue', dest='should_continue',
|
||||
action='store_true')
|
||||
parser.add_argument('file')
|
||||
args = parser.parse_args(argv)
|
||||
return args
|
||||
|
||||
|
||||
def main(argv):
|
||||
args = parse_args(argv[1:], prog=argv[0])
|
||||
|
||||
items = []
|
||||
with open(args.file, 'r') as f:
|
||||
items = json.load(f)
|
||||
|
||||
should_continue = args.should_continue
|
||||
|
||||
for item in items:
|
||||
fen = item['fen']
|
||||
depth = item['depth']
|
||||
expected_nodes_count = int(item['nodes'])
|
||||
|
||||
print('---')
|
||||
print(f'fen={fen}')
|
||||
print(f'depth={depth}')
|
||||
print(f'expected-nodes={expected_nodes_count}')
|
||||
|
||||
nodes_count = run_perft(fen, depth)
|
||||
print(f'nodes={nodes_count}')
|
||||
|
||||
did_pass = nodes_count == expected_nodes_count
|
||||
if did_pass:
|
||||
print('result=PASS')
|
||||
else:
|
||||
print('result=FAIL')
|
||||
|
||||
if not did_pass and not should_continue:
|
||||
return -1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
use chessfriend_position::{
|
||||
Position,
|
||||
fen::{FromFenStr, ToFenStr},
|
||||
};
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "Perft")]
|
||||
struct Arguments {
|
||||
#[arg(long, short, value_name = "INT")]
|
||||
depth: usize,
|
||||
|
||||
#[arg(long, short, value_name = "FEN")]
|
||||
fen: Option<String>,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = Arguments::parse();
|
||||
let depth = args.depth;
|
||||
|
||||
let mut position = if let Some(fen) = args.fen {
|
||||
Position::from_fen_str(&fen)?
|
||||
} else {
|
||||
Position::starting(None)
|
||||
};
|
||||
|
||||
println!("fen \"{}\"", position.to_fen_str().unwrap());
|
||||
println!("depth {depth}");
|
||||
|
||||
let counters = position.perft(depth);
|
||||
|
||||
println!("\n{counters}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::sight::SliderRayToSquareExt;
|
||||
use chessfriend_bitboard::BitBoard;
|
||||
use chessfriend_core::Shape;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct CheckingPieces {
|
||||
bitboards: [BitBoard; 5],
|
||||
}
|
||||
|
||||
impl CheckingPieces {
|
||||
pub(crate) fn new(
|
||||
pawn: BitBoard,
|
||||
knight: BitBoard,
|
||||
bishop: BitBoard,
|
||||
rook: BitBoard,
|
||||
queen: BitBoard,
|
||||
) -> CheckingPieces {
|
||||
CheckingPieces {
|
||||
bitboards: [pawn, knight, bishop, rook, queen],
|
||||
}
|
||||
}
|
||||
|
||||
/// The number of checking pieces.
|
||||
pub fn count(&self) -> u32 {
|
||||
self.bitboards.iter().map(BitBoard::population_count).sum()
|
||||
}
|
||||
|
||||
/// A BitBoard representing the set of pieces that must be captured to resolve check.
|
||||
pub fn capture_mask(&self) -> BitBoard {
|
||||
if self.count() == 0 {
|
||||
BitBoard::FULL
|
||||
} else {
|
||||
self.bitboards
|
||||
.iter()
|
||||
.fold(BitBoard::EMPTY, std::ops::BitOr::bitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// A BitBoard representing the set of squares to which a player can move a piece to block a
|
||||
/// checking piece.
|
||||
pub fn push_mask(&self, king: BitBoard) -> BitBoard {
|
||||
let target = king.first_occupied_square_leading().unwrap();
|
||||
|
||||
macro_rules! push_mask_for_shape {
|
||||
($push_mask:expr, $shape:ident, $king:expr) => {{
|
||||
let checking_pieces = self.bitboard_for_shape(Shape::$shape);
|
||||
if !checking_pieces.is_empty() {
|
||||
if let Some(checking_ray) = checking_pieces
|
||||
.occupied_squares()
|
||||
.flat_map(|sq| Shape::$shape.ray_to_square(sq, target).into_iter())
|
||||
.find(|bb| !(bb & $king).is_empty())
|
||||
{
|
||||
$push_mask |= checking_ray & !$king
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
let mut push_mask = BitBoard::EMPTY;
|
||||
|
||||
push_mask_for_shape!(push_mask, Bishop, king);
|
||||
push_mask_for_shape!(push_mask, Rook, king);
|
||||
push_mask_for_shape!(push_mask, Queen, king);
|
||||
|
||||
push_mask
|
||||
}
|
||||
|
||||
fn bitboard_for_shape(&self, shape: Shape) -> &BitBoard {
|
||||
&self.bitboards[shape as usize]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::Position;
|
||||
use chessfriend_board::Board;
|
||||
use chessfriend_core::{Color, Piece, Shape, score::Score};
|
||||
|
||||
struct Evaluator;
|
||||
|
||||
impl Evaluator {
|
||||
pub fn evaluate_symmetric_unwrapped(position: &Position, color: Color) -> Score {
|
||||
let board = &position.board;
|
||||
|
||||
let material_balance = Self::material_balance(board, color);
|
||||
|
||||
let score = material_balance;
|
||||
|
||||
score
|
||||
}
|
||||
|
||||
/// Evaluate a board using the symmetric evaluation algorithm defined by
|
||||
/// Claude Shannon.
|
||||
fn material_balance(board: &Board, color: Color) -> Score {
|
||||
let other_color = color.other();
|
||||
|
||||
Shape::into_iter().fold(Score::ZERO, |acc, shape| {
|
||||
let (active_pieces, other_pieces) = (
|
||||
i32::from(board.count_piece(&Piece::new(color, shape))),
|
||||
i32::from(board.count_piece(&Piece::new(other_color, shape))),
|
||||
);
|
||||
|
||||
let factor = shape.score() * (active_pieces - other_pieces);
|
||||
|
||||
acc + factor
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chessfriend_board::fen;
|
||||
|
||||
#[test]
|
||||
fn pawn_material_balance() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let board = fen!("8/8/8/8/8/3P4/8/8 w - - 0 1")?;
|
||||
assert_eq!(
|
||||
Evaluator::material_balance(&board, Color::White),
|
||||
100i32.into()
|
||||
);
|
||||
|
||||
let board = fen!("8/8/3p4/8/8/3P4/8/8 w - - 0 1")?;
|
||||
assert_eq!(Evaluator::material_balance(&board, Color::White), 0.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starting_position_is_even() {
|
||||
let position = Position::new(Board::starting(None));
|
||||
assert_eq!(
|
||||
Evaluator::evaluate_symmetric_unwrapped(&position, Color::White),
|
||||
Evaluator::evaluate_symmetric_unwrapped(&position, Color::Black)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod evaluation;
|
||||
mod position;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
pub use chessfriend_board::{PlacePieceError, PlacePieceStrategy, fen};
|
||||
pub use chessfriend_moves::{GeneratedMove, ValidateMove};
|
||||
pub use position::Position;
|
||||
|
||||
pub mod perft;
|
||||
#[macro_use]
|
||||
pub mod testing;
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! position {
|
||||
[$($color:ident $shape:ident on $square:ident),* $(,)?] => {
|
||||
$crate::Position::new(chessfriend_board::board!($($color $shape on $square),*))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! test_position {
|
||||
($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ], $en_passant:ident) => {
|
||||
{
|
||||
let board = chessfriend_board::test_board!($to_move, [ $($color $shape on $square),*], $en_passant);
|
||||
$crate::Position::new(board)
|
||||
}
|
||||
};
|
||||
($to_move:ident, [ $($color:ident $shape:ident on $square:ident),* $(,)? ]) => {
|
||||
{
|
||||
let board = chessfriend_board::test_board!($to_move, [ $($color $shape on $square),* ]);
|
||||
$crate::Position::new(board)
|
||||
}
|
||||
};
|
||||
($($color:ident $shape:ident on $square:ident),* $(,)?) => {
|
||||
{
|
||||
let board = chessfriend_board::test_board!($($color $shape on $square),*);
|
||||
$crate::Position::new(board)
|
||||
}
|
||||
};
|
||||
(empty) => {
|
||||
Position::new(chessfriend_board::test_board!(empty))
|
||||
};
|
||||
(starting) => {
|
||||
Position::new(chessfriend_board::test_board!(starting))
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod peterellisjones;
|
||||
mod single_pieces;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{assert_move_list, test_position, testing::*};
|
||||
use chessfriend_core::{piece, Square};
|
||||
use chessfriend_moves::Builder as MoveBuilder;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn one_king() -> TestResult {
|
||||
let pos = test_position![
|
||||
White King on D3,
|
||||
Black King on H6,
|
||||
];
|
||||
|
||||
let builder = MoveBuilder::push(&piece!(White King on D3));
|
||||
let expected_moves = HashSet::from_iter([
|
||||
builder.clone().to(Square::D4).build()?,
|
||||
builder.clone().to(Square::E4).build()?,
|
||||
builder.clone().to(Square::E3).build()?,
|
||||
builder.clone().to(Square::E2).build()?,
|
||||
builder.clone().to(Square::D2).build()?,
|
||||
builder.clone().to(Square::C2).build()?,
|
||||
builder.clone().to(Square::C3).build()?,
|
||||
builder.clone().to(Square::C4).build()?,
|
||||
]);
|
||||
|
||||
let generated_moves: HashSet<_> = pos.moves().iter().collect();
|
||||
|
||||
assert_move_list!(generated_moves, expected_moves, pos);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
use crate::{GeneratedMove, Position, ValidateMove};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct PerftCounters {
|
||||
nodes: u64,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn perft(&mut self, depth: usize) -> PerftCounters {
|
||||
self.perft_recursive(0, depth)
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
fn perft_recursive(&mut self, depth: usize, max_depth: usize) -> PerftCounters {
|
||||
let mut counters = PerftCounters::default();
|
||||
|
||||
if depth == max_depth {
|
||||
counters.count_node();
|
||||
return counters;
|
||||
}
|
||||
|
||||
let legal_moves: Vec<GeneratedMove> = self.all_legal_moves(None).collect();
|
||||
|
||||
for generated_ply in legal_moves {
|
||||
let ply = generated_ply.ply();
|
||||
|
||||
let has_seen_position = self
|
||||
.make_move(ply, ValidateMove::No)
|
||||
.expect("unable to make generated move");
|
||||
|
||||
let recursive_counters = if has_seen_position {
|
||||
let mut counters = PerftCounters::default();
|
||||
counters.count_node();
|
||||
counters
|
||||
} else {
|
||||
self.perft_recursive(depth + 1, max_depth)
|
||||
};
|
||||
|
||||
self.unmake_last_move().expect("unable to unmake last move");
|
||||
|
||||
counters.fold(&recursive_counters);
|
||||
|
||||
if depth == 0 {
|
||||
println!(" {ply}: {}", recursive_counters.nodes);
|
||||
}
|
||||
}
|
||||
|
||||
counters
|
||||
}
|
||||
}
|
||||
|
||||
impl PerftCounters {
|
||||
fn count_node(&mut self) {
|
||||
self.nodes += 1;
|
||||
}
|
||||
fn fold(&mut self, results: &Self) {
|
||||
self.nodes += results.nodes;
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PerftCounters {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "Perft Results")?;
|
||||
write!(f, " Nodes: {}", self.nodes)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,379 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod captures;
|
||||
|
||||
use crate::fen::{FromFenStr, FromFenStrError};
|
||||
use captures::CapturesList;
|
||||
use chessfriend_bitboard::BitBoard;
|
||||
use chessfriend_board::{
|
||||
Board, PlacePieceError, PlacePieceStrategy, ZobristState, display::DiagramFormatter,
|
||||
fen::ToFenStr,
|
||||
};
|
||||
use chessfriend_core::{Color, Piece, Shape, Square};
|
||||
use chessfriend_moves::{
|
||||
GeneratedMove, MakeMove, MakeMoveError, Move, MoveRecord, UnmakeMove, UnmakeMoveError,
|
||||
UnmakeMoveResult, ValidateMove,
|
||||
algebraic::AlgebraicMoveComponents,
|
||||
generators::{
|
||||
AllPiecesMoveGenerator, BishopMoveGenerator, KingMoveGenerator, KnightMoveGenerator,
|
||||
PawnMoveGenerator, QueenMoveGenerator, RookMoveGenerator,
|
||||
},
|
||||
};
|
||||
use std::{collections::HashSet, fmt, sync::Arc};
|
||||
|
||||
#[must_use]
|
||||
#[derive(Clone, Debug, Default, Eq)]
|
||||
pub struct Position {
|
||||
pub(crate) board: Board,
|
||||
pub(crate) moves: Vec<MoveRecord>,
|
||||
pub(crate) captures: CapturesList,
|
||||
|
||||
/// A set of hashes of board positions seen throughout the move record.
|
||||
boards_seen: HashSet<u64>,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn empty(zobrist: Option<Arc<ZobristState>>) -> Self {
|
||||
Self::new(Board::empty(zobrist))
|
||||
}
|
||||
|
||||
/// Return a starting position.
|
||||
pub fn starting(zobrist: Option<Arc<ZobristState>>) -> Self {
|
||||
Self::new(Board::starting(zobrist))
|
||||
}
|
||||
|
||||
pub fn new(board: Board) -> Self {
|
||||
Self {
|
||||
board,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn board(&self) -> &Board {
|
||||
&self.board
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn active_color(&self) -> Color {
|
||||
self.board.active_color()
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// Place a piece on the board.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// See [`chessfriend_board::Board::place_piece`].
|
||||
pub fn place_piece(
|
||||
&mut self,
|
||||
piece: Piece,
|
||||
square: Square,
|
||||
strategy: PlacePieceStrategy,
|
||||
) -> Result<Option<Piece>, PlacePieceError> {
|
||||
self.board.place_piece(piece, square, strategy)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_piece(&self, square: Square) -> Option<Piece> {
|
||||
self.board.get_piece(square)
|
||||
}
|
||||
|
||||
pub fn remove_piece(&mut self, square: Square) -> Option<Piece> {
|
||||
self.board.remove_piece(square)
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// Calculate sight of a piece on the provided [`Square`].
|
||||
pub fn sight_piece(&self, square: Square) -> BitBoard {
|
||||
self.board.sight_piece(square)
|
||||
}
|
||||
|
||||
/// Calculate movement of a piece on the provided [`Square`].
|
||||
pub fn movement_piece(&self, square: Square) -> BitBoard {
|
||||
self.board.movement_piece(square)
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn all_moves(&self, color: Option<Color>) -> AllPiecesMoveGenerator {
|
||||
AllPiecesMoveGenerator::new(&self.board, color)
|
||||
}
|
||||
|
||||
/// Generate legal moves.
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// If the position failed to make a move generated by the internal move
|
||||
/// generator, this method will panic.
|
||||
#[must_use]
|
||||
pub fn all_legal_moves(
|
||||
&self,
|
||||
color: Option<Color>,
|
||||
) -> Box<dyn Iterator<Item = GeneratedMove> + '_> {
|
||||
let generator = self.all_moves(color);
|
||||
|
||||
let mut test_board = self.board.clone();
|
||||
Box::new(generator.filter(move |ply| {
|
||||
let active_color_before_move = test_board.active_color();
|
||||
|
||||
let ply: Move = ply.clone().into();
|
||||
let record = test_board
|
||||
.make_move(ply, ValidateMove::No)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"unable to make generated move [{ply}]: {err}\n\n{}",
|
||||
test_board.display().highlight(ply.relevant_squares())
|
||||
);
|
||||
});
|
||||
|
||||
let move_is_legal = !test_board.is_in_check();
|
||||
|
||||
test_board.unmake_move(&record).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"unable to unmake generated move [{ply}]: {err}\n\n{}",
|
||||
test_board.display().highlight(ply.relevant_squares())
|
||||
);
|
||||
});
|
||||
|
||||
move_is_legal
|
||||
}))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn moves_for_piece(
|
||||
&self,
|
||||
square: Square,
|
||||
) -> Option<Box<dyn Iterator<Item = GeneratedMove>>> {
|
||||
self.get_piece(square)
|
||||
.map(|piece| Self::generator(&self.board, piece))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn generator(board: &Board, piece: Piece) -> Box<dyn Iterator<Item = GeneratedMove>> {
|
||||
match piece.shape {
|
||||
Shape::Pawn => Box::new(PawnMoveGenerator::new(board, Some(piece.color))),
|
||||
Shape::Knight => Box::new(KnightMoveGenerator::new(board, Some(piece.color))),
|
||||
Shape::Bishop => Box::new(BishopMoveGenerator::new(board, Some(piece.color))),
|
||||
Shape::Rook => Box::new(RookMoveGenerator::new(board, Some(piece.color))),
|
||||
Shape::Queen => Box::new(QueenMoveGenerator::new(board, Some(piece.color))),
|
||||
Shape::King => Box::new(KingMoveGenerator::new(board, Some(piece.color))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn sight_active(&self) -> BitBoard {
|
||||
self.board.sight_active()
|
||||
}
|
||||
|
||||
/// A [`BitBoard`] of all squares the given color can see.
|
||||
pub fn friendly_sight(&self, color: Color) -> BitBoard {
|
||||
self.board.friendly_sight(color)
|
||||
}
|
||||
|
||||
/// A [`BitBoard`] of all squares visible by colors that oppose the given color.
|
||||
pub fn active_color_opposing_sight(&self) -> BitBoard {
|
||||
self.board.active_color_opposing_sight()
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// Make a move on the board and record it in the move list. Returns `true`
|
||||
/// if the board position has been seen before (i.e. it's a repetition).
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns one of [`MakeMoveError`] if the move cannot be made.
|
||||
///
|
||||
pub fn make_move(&mut self, ply: Move, validate: ValidateMove) -> Result<bool, MakeMoveError> {
|
||||
let record = self.board.make_move(ply, validate)?;
|
||||
|
||||
if let Some(captured_piece) = record.captured_piece {
|
||||
self.captures.push(record.color, captured_piece);
|
||||
}
|
||||
|
||||
let has_seen = if let Some(hash) = self.board.zobrist_hash() {
|
||||
// HashSet::insert() returns true if the value does not exist in the
|
||||
// set when it's called.
|
||||
!self.boards_seen.insert(hash)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
self.moves.push(record.clone());
|
||||
|
||||
Ok(has_seen)
|
||||
}
|
||||
|
||||
/// Unmake the last move made on the board and remove its record from the
|
||||
/// move list.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns one of [`UnmakeMoveError`] if the move cannot be made.
|
||||
///
|
||||
pub fn unmake_last_move(&mut self) -> UnmakeMoveResult {
|
||||
let last_move_record = self.moves.pop().ok_or(UnmakeMoveError::NoMove)?;
|
||||
|
||||
let hash_before_unmake = self.board.zobrist_hash();
|
||||
|
||||
let unmake_result = self.board.unmake_move(&last_move_record);
|
||||
|
||||
if unmake_result.is_ok() {
|
||||
if let Some(capture) = last_move_record.captured_piece {
|
||||
let popped_piece = self.captures.pop(last_move_record.color);
|
||||
debug_assert_eq!(Some(capture), popped_piece);
|
||||
}
|
||||
|
||||
if let Some(hash_before_unmake) = hash_before_unmake {
|
||||
self.boards_seen.remove(&hash_before_unmake);
|
||||
}
|
||||
} else {
|
||||
self.moves.push(last_move_record);
|
||||
}
|
||||
|
||||
unmake_result
|
||||
}
|
||||
|
||||
/// Build a move given its origin, target, and possible promotion. Perform
|
||||
/// some minimal validation. If a move cannot be
|
||||
#[must_use]
|
||||
pub fn move_from_algebraic_components(
|
||||
&self,
|
||||
components: AlgebraicMoveComponents,
|
||||
) -> Option<Move> {
|
||||
match components {
|
||||
AlgebraicMoveComponents::Null => Some(Move::null()),
|
||||
AlgebraicMoveComponents::Regular {
|
||||
origin,
|
||||
target,
|
||||
promotion,
|
||||
} => self.move_from_origin_target(origin, target, promotion),
|
||||
}
|
||||
}
|
||||
|
||||
fn move_from_origin_target(
|
||||
&self,
|
||||
origin: Square,
|
||||
target: Square,
|
||||
promotion: Option<Shape>,
|
||||
) -> Option<Move> {
|
||||
let piece = self.get_piece(origin)?;
|
||||
|
||||
let color = piece.color;
|
||||
|
||||
// Pawn and King are the two most interesting shapes here, because of en
|
||||
// passant, castling and so on. So, let the move generators do their
|
||||
// thing and find the move that fits the parameters. For the rest of the
|
||||
// pieces, do something a little more streamlined.
|
||||
|
||||
match piece.shape {
|
||||
Shape::Pawn => PawnMoveGenerator::new(&self.board, None)
|
||||
.find(|ply| {
|
||||
ply.origin() == origin
|
||||
&& ply.target() == target
|
||||
&& ply.promotion_shape() == promotion
|
||||
})
|
||||
.map(std::convert::Into::into),
|
||||
Shape::King => KingMoveGenerator::new(&self.board, None)
|
||||
.find(|ply| ply.origin() == origin && ply.target() == target)
|
||||
.map(std::convert::Into::into),
|
||||
_ => {
|
||||
if color != self.board.active_color() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let target_bitboard: BitBoard = target.into();
|
||||
if !(self.movement_piece(origin) & target_bitboard).is_populated() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.get_piece(target).is_some() {
|
||||
return Some(Move::capture(origin, target));
|
||||
}
|
||||
|
||||
Some(Move::quiet(origin, target))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
#[must_use]
|
||||
pub fn zobrist_hash(&self) -> Option<u64> {
|
||||
self.board.zobrist_hash()
|
||||
}
|
||||
|
||||
pub fn set_zobrist_state(&mut self, state: Arc<ZobristState>) {
|
||||
self.board.set_zobrist_state(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn display(&self) -> DiagramFormatter {
|
||||
self.board.display()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromFenStr for Position {
|
||||
type Error = FromFenStrError;
|
||||
|
||||
fn from_fen_str(string: &str) -> Result<Self, Self::Error> {
|
||||
let board = Board::from_fen_str(string)?;
|
||||
Ok(Position::new(board))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFenStr for Position {
|
||||
type Error = <Board as ToFenStr>::Error;
|
||||
|
||||
fn to_fen_str(&self) -> Result<String, Self::Error> {
|
||||
self.board.to_fen_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Position {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.board == other.board
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Position {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.board.display())?;
|
||||
|
||||
if !self.captures.is_empty() {
|
||||
write!(f, "\n\n{}", self.captures)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Position, test_position};
|
||||
use chessfriend_core::piece;
|
||||
|
||||
#[test]
|
||||
fn piece_on_square() {
|
||||
let pos = test_position![
|
||||
Black Bishop on F7,
|
||||
];
|
||||
|
||||
let piece = pos.board.get_piece(Square::F7);
|
||||
assert_eq!(piece, Some(piece!(Black Bishop)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn piece_in_starting_position() {
|
||||
let pos = test_position!(starting);
|
||||
|
||||
assert_eq!(pos.board.get_piece(Square::H1), Some(piece!(White Rook)));
|
||||
assert_eq!(pos.board.get_piece(Square::A8), Some(piece!(Black Rook)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
mod move_builder;
|
||||
|
||||
pub use move_builder::{Builder as MoveBuilder, MakeMoveError};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue