From 1991d3b52683bf4bbbb87acdb6ae785cfa7a9698 Mon Sep 17 00:00:00 2001 From: Kirill Bobyrev Date: Fri, 21 Jun 2024 18:11:23 -0700 Subject: [PATCH] feat,infra: Clean up the project and continue impl - Add Makefile for OpenBench (#55) - Move all lints from src/lib.rs to Cargo.toml - Continue implementing the engine's UCI handler - Add engine integration test using assert_cmd - Extend the score to support checkmates - Add `pabi bench` command for OpenBench and impl stub - Make minor progress towards Transposition Table impl --- Cargo.lock | 427 ++++++------------ Cargo.toml | 59 +++ Makefile | 20 + benches/cycles.rs | 17 +- benches/time.rs | 26 +- build.rs | 24 +- justfile | 4 +- src/chess/bitboard.rs | 3 +- src/chess/core.rs | 33 +- src/chess/generated.rs | 10 +- src/chess/position.rs | 41 +- src/chess/zobrist.rs | 2 + src/engine/bench.rs | 0 src/engine/mod.rs | 122 ++--- src/engine/openbench.rs | 15 + src/engine/time_manager.rs | 1 + src/engine/uci.rs | 118 +++-- src/evaluation/mod.rs | 66 ++- src/evaluation/pesto.rs | 12 +- src/lib.rs | 42 +- src/main.rs | 6 +- src/search/history.rs | 1 + src/search/minimax.rs | 46 +- src/search/mod.rs | 51 ++- src/search/transposition.rs | 43 +- tests/chess.rs | 95 ++-- {data => tests/data}/README.md | 0 {data => tests/data}/positions.fen | 0 tests/integration.rs | 33 ++ tools/Cargo.toml | 3 - ...{extract_lc0_data.rs => data_extractor.rs} | 24 +- 31 files changed, 731 insertions(+), 613 deletions(-) create mode 100644 Makefile delete mode 100644 src/engine/bench.rs create mode 100644 src/engine/openbench.rs create mode 100644 src/engine/time_manager.rs rename {data => tests/data}/README.md (100%) rename {data => tests/data}/positions.fen (100%) create mode 100644 tests/integration.rs rename tools/src/bin/{extract_lc0_data.rs => data_extractor.rs} (92%) diff --git a/Cargo.lock b/Cargo.lock index 0d1c819d0..db6798bba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,19 +94,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] -name = "autocfg" -version = "1.3.0" +name = "assert_cmd" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] [[package]] -name = "bincode" -version = "1.3.3" +name = "autocfg" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" @@ -120,6 +126,17 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "btoi" version = "0.4.3" @@ -135,12 +152,6 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "cast" version = "0.3.0" @@ -383,15 +394,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] -name = "displaydoc" -version = "0.2.4" +name = "difflib" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" @@ -431,6 +443,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -515,134 +536,14 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -746,12 +647,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "log" version = "0.4.21" @@ -766,13 +661,19 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "num-conv" version = "0.1.0" @@ -815,10 +716,12 @@ version = "2024.6.16" dependencies = [ "anyhow", "arrayvec", + "assert_cmd", "bitflags 2.5.0", "criterion", "iai", "itertools 0.13.0", + "predicates", "pretty_assertions", "rand", "shadow-rs", @@ -830,12 +733,9 @@ name = "pabi-tools" version = "0.1.0" dependencies = [ "anyhow", - "bincode", - "byteorder", "clap", "flate2", "pabi", - "rayon", "tar", ] @@ -891,6 +791,36 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -903,9 +833,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1090,18 +1020,6 @@ dependencies = [ "btoi", ] -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "strsim" version = "0.11.1" @@ -1110,26 +1028,15 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.66" +version = "2.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tar" version = "0.4.41" @@ -1141,6 +1048,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "time" version = "0.3.36" @@ -1174,16 +1087,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -1194,6 +1097,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tz-rs" version = "0.6.14" @@ -1223,12 +1141,27 @@ dependencies = [ "tz-rs", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.4" @@ -1237,27 +1170,15 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -1270,6 +1191,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1441,18 +1371,6 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "xattr" version = "1.3.1" @@ -1469,70 +1387,3 @@ name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" - -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index c7d95ebb9..95eeba590 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,63 @@ include = [ "README.md", ] # Reduce the package size by only including things necessary for building it. +[lints.rust] +# Warn +absolute_paths_not_starting_with_crate = "warn" +let_underscore_drop = "warn" +macro_use_extern_crate = "warn" +missing_docs = "warn" +unused_extern_crates = "warn" +unused_import_braces = "warn" +unused_lifetimes = "warn" +unused_qualifications = "warn" +variant_size_differences = "warn" +# Deny +keyword_idents = "deny" +keyword_idents_2018 = "deny" +keyword_idents_2024 = "deny" +single_use_lifetimes = "deny" +trivial_casts = "deny" +trivial_numeric_casts = "deny" +unreachable_pub = "deny" +unused_results = "deny" + +[lints.clippy] +# Warn +cargo = "warn" +cognitive_complexity = "warn" +complexity = "warn" +correctness = "warn" +dbg_macro = "warn" +nursery = "warn" +pedantic = "warn" +style = "warn" +suspicious = "warn" +decimal_literal_representation = "warn" +# Deny +cargo_common_metadata = "deny" +doc_markdown = "deny" +equatable_if_let = "deny" +float_cmp = "deny" +float_cmp_const = "deny" +get_unwrap = "deny" +implicit_clone = "deny" +imprecise_flops = "deny" +inefficient_to_string = "deny" +linkedlist = "deny" +manual_assert = "deny" +needless_collect = "deny" +perf = "deny" +redundant_clone = "deny" +suboptimal_flops = "deny" +trivial_regex = "deny" +use_self = "deny" + +[lints.rustdoc] +broken_intra_doc_links = "deny" +invalid_rust_codeblocks = "deny" +unescaped_backticks = "deny" + [workspace] members = ["tools"] @@ -37,6 +94,7 @@ shadow-rs = "0.28.0" rand = "0.8.5" [dev-dependencies] +assert_cmd = "2.0.14" criterion = { version = "0.5.1", features = [ "cargo_bench_support", "csv_output", @@ -46,6 +104,7 @@ iai = "0.1.1" pretty_assertions = "1.1.0" # Used for testing and comparing against a reasonable baseline for correctness. shakmaty = "0.27.0" +predicates = "3.1.0" [[bench]] harness = false diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..fa7f60bbf --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Supporting builds through `make` command is a requirement for OpenBench: +# https://github.com/AndyGrant/OpenBench/wiki/Requirements-For-Public-Engines#basic-requirements + +# Variable for the output binary name, defaults to 'pabi' if not provided. +EXE ?= pabi + +ifeq ($(OS),Windows_NT) + EXE_SUFFIX := .exe +else + EXE_SUFFIX := +endif + +# Compile flags for the fastest possible build. +COMPILE_FLAGS := RUSTFLAGS='-C target-feature=+avx2,+fma,+bmi1,+bmi2' + +# Compile the target and add a link to the binary for OpenBench to pick up. +openbench: + $(COMPILE_FLAGS) cargo rustc --profile=fast --bin=pabi -- --emit link=$(EXE)$(EXE_SUFFIX) + +.PHONY: openbench diff --git a/benches/cycles.rs b/benches/cycles.rs index 27a99ab80..ac91a9475 100644 --- a/benches/cycles.rs +++ b/benches/cycles.rs @@ -28,9 +28,9 @@ fn parse_stockfish_book_positions() { // Low depths of known perft results (https://www.chessprogramming.org/Perft_Results). fn perft() { - for (position, depth, nodes) in [ + for (position, depth, nodes) in &[ // Position 1 (starting). - (Position::starting(), 5, 4865609), + (Position::starting(), 5, 4_865_609), // Position 2. ( Position::from_fen( @@ -38,27 +38,27 @@ fn perft() { ) .unwrap(), 4, - 4085603, + 4_085_603, ), // Position 3. ( Position::from_fen("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1").unwrap(), 5, - 674624, + 674_624, ), // Position 4. ( Position::from_fen("r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1") .unwrap(), 4, - 422333, + 422_333, ), // Position 5. ( Position::from_fen("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8") .unwrap(), 4, - 2103487, + 2_103_487, ), // Position 6. ( @@ -67,16 +67,15 @@ fn perft() { ) .unwrap(), 4, - 3894594, + 3_894_594, ), ( Position::from_fen("r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1") .unwrap(), 4, - 422333, + 422_333, ), ] - .iter() { assert_eq!(pabi::chess::position::perft(position, *depth), *nodes); } diff --git a/benches/time.rs b/benches/time.rs index 18d86e61a..da8519b36 100644 --- a/benches/time.rs +++ b/benches/time.rs @@ -11,7 +11,7 @@ fn parse(c: &mut Criterion) { let positions = fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/data/positions.fen")) .unwrap() .lines() - .map(|line| line.to_string()) + .map(ToString::to_string) .collect::>(); c.bench_with_input( BenchmarkId::new( @@ -109,25 +109,23 @@ criterion_group! { // This acts both as performance and correctness test. fn perft_bench(c: &mut Criterion) { let mut group = c.benchmark_group("perft"); - // TODO: Abstract this out and have a single array/dataset of perft positions to - // check. Inlining these is quite unappealing. // TODO: Add Throughput - it should be the number of nodes. - for (position, depth, nodes) in [ + for (position, depth, nodes) in &[ // Position 1. - (Position::starting(), 5, 4865609), - (Position::starting(), 6, 119060324), + (Position::starting(), 5, 4_865_609), + (Position::starting(), 6, 119_060_324), // Position 3. ( Position::from_fen("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1").unwrap(), 6, - 11030083, + 11_030_083, ), // Position 4. ( Position::from_fen("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1") .unwrap(), 6, - 706045033, + 706_045_033, ), // Position 6. ( @@ -136,33 +134,29 @@ fn perft_bench(c: &mut Criterion) { ) .unwrap(), 5, - 164075551, + 164_075_551, ), // Other positions. ( Position::from_fen("r1bqkbnr/pppppppp/2n5/8/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 1 2") .unwrap(), 6, - 336655487, + 336_655_487, ), ( Position::from_fen("rnbqkbnr/pppppppp/8/8/8/N7/PPPPPPPP/R1BQKBNR b KQkq - 1 1") .unwrap(), 6, - 120142144, + 120_142_144, ), ] - .iter() { group.throughput(criterion::Throughput::Elements(*nodes)); group.bench_with_input( BenchmarkId::new( "perft", format!( - "position {}, depth {}, nodes {}", - position.to_string(), - depth, - nodes + "position {position}, depth {depth}, nodes {nodes}" ), ), depth, diff --git a/build.rs b/build.rs index 5f4129803..5de2ad196 100644 --- a/build.rs +++ b/build.rs @@ -31,18 +31,16 @@ fn generate_zobrist_keys() { let mut rng = rand::thread_rng(); let piece_keys: [ZobristKey; NUM_COLORS * NUM_PIECES * NUM_SQUARES] = - std::array::from_fn(|_| rand::Rng::gen(&mut rng)); - generate_file( - &format!("pieces_zobrist_keys"), - &format!("{:?}", piece_keys), - ); + std::array::from_fn(|_| rand::Rng::r#gen(&mut rng)); + generate_file("pieces_zobrist_keys", &format!("{piece_keys:?}")); - let en_passant_keys: [ZobristKey; 8] = std::array::from_fn(|_| rand::Rng::gen(&mut rng)); - generate_file("en_passant_zobrist_keys", &format!("{:?}", en_passant_keys)); + let en_passant_keys: [ZobristKey; 8] = std::array::from_fn(|_| rand::Rng::r#gen(&mut rng)); + generate_file("en_passant_zobrist_keys", &format!("{en_passant_keys:?}")); } // PeSTO tables with modified encoding for easier serialization. -// Piece indices match the order of PieceKind, the planes match the order of Piece. +// Piece indices match the order of PieceKind, the planes match the order of +// Piece. const MIDDLEGAME_VALUE: [i32; 6] = [0, 1025, 477, 365, 337, 82]; const ENDGAME_VALUE: [i32; 6] = [0, 936, 512, 297, 281, 94]; @@ -185,7 +183,7 @@ const ENDGAME_KING_TABLE: [i32; 64] = [ -53, -34, -21, -11, -28, -14, -24, -43 ]; -fn flip(square: usize) -> usize { +const fn flip(square: usize) -> usize { square ^ 56 } @@ -218,20 +216,20 @@ fn generate_pesto_tables() { { populate_piece_values( piece_index, - &middlegame_piece_table, + middlegame_piece_table, &MIDDLEGAME_VALUE, &mut middlegame_table, ); populate_piece_values( piece_index, - &endgame_piece_table, + endgame_piece_table, &ENDGAME_VALUE, &mut endgame_table, ); } - generate_file("pesto_middlegame_table", &format!("{:?}", middlegame_table)); - generate_file("pesto_endgame_table", &format!("{:?}", endgame_table)); + generate_file("pesto_middlegame_table", &format!("{middlegame_table:?}")); + generate_file("pesto_endgame_table", &format!("{endgame_table:?}")); } fn main() -> shadow_rs::SdResult<()> { diff --git a/justfile b/justfile index 9e3f4c8b3..2ead5362e 100644 --- a/justfile +++ b/justfile @@ -25,7 +25,7 @@ fix: cargo +nightly clippy --all-features --fix --allow-staged # Run most tests that are fast and are run by default. -test_basic: +test: {{ compile_flags }} cargo test --profile=fast # Run tests that are slow and are not run by default. @@ -33,7 +33,7 @@ test_slow: {{ compile_flags }} cargo test --profile=fast -- --ignored # Run all tests. -test: test_basic test_slow +test_all: test test_slow bench: {{ compile_flags }} cargo bench --profile=fast diff --git a/src/chess/bitboard.rs b/src/chess/bitboard.rs index 749f6a248..6ffddb574 100644 --- a/src/chess/bitboard.rs +++ b/src/chess/bitboard.rs @@ -428,6 +428,7 @@ impl Pieces { #[cfg(test)] mod tests { + use mem::size_of; use pretty_assertions::assert_eq; use super::*; @@ -435,7 +436,7 @@ mod tests { #[test] fn basics() { - assert_eq!(mem::size_of::(), 8); + assert_eq!(size_of::(), 8); assert_eq!(Bitboard::full().bits, u64::MAX); assert_eq!(Bitboard::empty().bits, u64::MIN); diff --git a/src/chess/core.rs b/src/chess/core.rs index b9e48886b..a70459a0d 100644 --- a/src/chess/core.rs +++ b/src/chess/core.rs @@ -162,6 +162,21 @@ impl Square { Err(_) => None, } } + + fn next(self) -> Option { + if self as u8 == 63 { + None + } else { + Some(unsafe { mem::transmute(self as u8 + 1) }) + } + } + + /// Creates an iterator over all squares, starting from A1 (0) to H8 (63). + #[must_use] pub fn iter() -> SquareIterator { + SquareIterator { + current: Some(Self::A1), + } + } } impl TryFrom for Square { @@ -217,6 +232,22 @@ impl TryFrom<&str> for Square { } } +/// Iterates over squares in the order from A1 to H8, from left to right, from +/// bottom to the top. +pub struct SquareIterator { + current: Option, +} + +impl Iterator for SquareIterator { + type Item = Square; + + fn next(&mut self) -> Option { + let result = self.current; + self.current = self.current.and_then(Square::next); + result + } +} + impl fmt::Display for Square { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", self.file(), self.rank()) @@ -444,7 +475,7 @@ pub struct Piece { } impl Piece { - pub const fn plane(&self) -> usize { + #[must_use] pub const fn plane(&self) -> usize { self.owner as usize * 6 + self.kind as usize } } diff --git a/src/chess/generated.rs b/src/chess/generated.rs index 80d4bafb4..c406382ef 100644 --- a/src/chess/generated.rs +++ b/src/chess/generated.rs @@ -4,13 +4,13 @@ use crate::chess::core::{Piece, Square, BOARD_SIZE}; use crate::chess::zobrist::Key; // All keys required for Zobrist hashing of a chess position. -pub(super) const BLACK_TO_MOVE: Key = 0x9E06BAD39D761293; +pub(super) const BLACK_TO_MOVE: Key = 0x9E06_BAD3_9D76_1293; -pub(super) const WHITE_CAN_CASTLE_SHORT: Key = 0xF05AC573DD61D323; -pub(super) const WHITE_CAN_CASTLE_LONG: Key = 0x41D8B55BA5FEB78B; +pub(super) const WHITE_CAN_CASTLE_SHORT: Key = 0xF05A_C573_DD61_D323; +pub(super) const WHITE_CAN_CASTLE_LONG: Key = 0x41D8_B55B_A5FE_B78B; -pub(super) const BLACK_CAN_CASTLE_SHORT: Key = 0x680988787A43D289; -pub(super) const BLACK_CAN_CASTLE_LONG: Key = 0x2F941F8DFD3E3D1F; +pub(super) const BLACK_CAN_CASTLE_SHORT: Key = 0x6809_8878_7A43_D289; +pub(super) const BLACK_CAN_CASTLE_LONG: Key = 0x2F94_1F8D_FD3E_3D1F; pub(super) const EN_PASSANT_FILES: [Key; 8] = include!(concat!(env!("OUT_DIR"), "/en_passant_zobrist_keys")); diff --git a/src/chess/position.rs b/src/chess/position.rs index b1dfa136b..fa91bd82b 100644 --- a/src/chess/position.rs +++ b/src/chess/position.rs @@ -10,10 +10,21 @@ use std::fmt::{self, Write}; use anyhow::{bail, Context}; +use arrayvec::ArrayVec; +use super::zobrist::RepetitionTable; use crate::chess::bitboard::{Bitboard, Pieces}; use crate::chess::core::{ - CastleRights, File, Move, MoveList, Piece, Player, Promotion, Rank, Square, BOARD_WIDTH, + CastleRights, + File, + Move, + MoveList, + Piece, + Player, + Promotion, + Rank, + Square, + BOARD_WIDTH, }; use crate::chess::{attacks, generated, zobrist}; @@ -104,7 +115,7 @@ impl Position { /// Returns Zobrist hash of the position. // TODO: Compute hash once and incrementally update it in make_move along with // accumulator. - pub fn hash(&self) -> zobrist::Key { + #[must_use] pub fn hash(&self) -> zobrist::Key { self.compute_hash() } @@ -698,7 +709,7 @@ impl fmt::Debug for Position { // bitflags' default fmt::Debug implementation is not very convenient: // dump FEN instead. writeln!(f, "Castling rights: {}", &self.castling)?; - writeln!(f, "FEN: {}", &self.to_string())?; + writeln!(f, "FEN: {}", &self)?; Ok(()) } @@ -1090,6 +1101,30 @@ fn generate_castle_moves( } } +struct PositionHistory { + positions: ArrayVec, + repetitions: RepetitionTable, +} + +impl PositionHistory { + fn new() -> Self { + Self { + positions: ArrayVec::new(), + repetitions: RepetitionTable::new(), + } + } + + fn current_position(&self) -> &Position { + self.positions.last().expect("no positions in history") + } + + fn push(&mut self, position: Position) -> bool { + let hash = position.hash(); + self.positions.push(position); + self.repetitions.record(hash) + } +} + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; diff --git a/src/chess/zobrist.rs b/src/chess/zobrist.rs index 2879f9923..7a0b81bac 100644 --- a/src/chess/zobrist.rs +++ b/src/chess/zobrist.rs @@ -24,6 +24,8 @@ impl RepetitionTable { } /// Returns true if the position has occurred 3 times. + /// + /// In most tournament settings 3-fold repetition counts as a draw. #[must_use] pub(crate) fn record(&mut self, key: Key) -> bool { let count = self.table.entry(key).or_insert(0); diff --git a/src/engine/bench.rs b/src/engine/bench.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 49166be6e..4e6f5b123 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -9,19 +9,22 @@ use core::panic; use std::io::{BufRead, Write}; use std::time::Duration; -use itertools::Itertools; - use crate::chess::core::{Move, Player}; use crate::chess::position::Position; use crate::engine::uci::Command; -use crate::search::go; +use crate::search::{find_best_move, Depth}; +pub mod openbench; +mod time_manager; mod uci; -/// The Engine connects everything together handles commands sent by UCI server, -/// including I/O. +/// The Engine connects everything together and handles commands sent by UCI +/// server. pub struct Engine<'a, R: BufRead, W: Write> { position: Position, + debug: bool, + // TODO: time_manager, + // TODO: transposition_table input: &'a mut R, output: &'a mut W, } @@ -33,18 +36,20 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { pub fn new(input: &'a mut R, output: &'a mut W) -> Self { Self { position: Position::starting(), + debug: false, input, output, } } /// Continuously reads the input stream and executes sent UCI commands until - /// "quit" is sent or it is shut down. + /// "quit" is sent. /// /// The implementation here does not aim to be complete and exhaustive, - /// because the main goal is to make the engine work in relatively - /// simple setups, making it work with all UCI-compatible GUIs and - /// corrupted input is not a priority. + /// because the main goal is to make the engine work in relatively simple + /// setups, making it work with all UCI-compatible GUIs and corrupted input + /// is not a priority. For supported commands and their options see + /// [`Command`]. /// /// NOTE: The assumption is that the UCI input stream is **correct**. It is /// tournament manager's responsibility to send uncorrupted input and make @@ -65,14 +70,14 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { }, } match Command::parse(&line) { - Command::Uci => self.handle_uci()?, - Command::Debug { on } => todo!(), - Command::IsReady => self.handle_isready()?, + Command::Uci => self.handshake()?, + Command::Debug { on } => self.debug = on, + Command::IsReady => self.sync()?, Command::SetOption { option, value } => todo!(), - Command::SetPosition { fen, moves } => todo!(), - Command::NewGame => todo!(), + Command::SetPosition { fen, moves } => self.set_position(fen, moves)?, + Command::NewGame => self.new_game()?, Command::Go { - depth, + max_depth, wtime, btime, winc, @@ -81,14 +86,17 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { mate, movetime, infinite, - } => todo!(), - Command::Stop => self.handle_stop()?, + } => self.go( + max_depth, wtime, btime, winc, binc, nodes, mate, movetime, infinite, + )?, + Command::Stop => self.stop_search()?, Command::Quit => { - self.handle_stop()?; + self.stop_search()?; break; }, + Command::State => todo!(), Command::Unknown(command) => { - writeln!(self.output, "info string Unsupported command: {command}")? + writeln!(self.output, "info string Unsupported command: {command}")?; }, } } @@ -96,7 +104,7 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { } /// Responds to the `uci` handshake command by identifying the engine. - fn handle_uci(&mut self) -> anyhow::Result<()> { + fn handshake(&mut self) -> anyhow::Result<()> { writeln!( self.output, "id name {} {}", @@ -109,7 +117,7 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { } /// Syncs with the UCI server by responding with `readyok`. - fn handle_isready(&mut self) -> anyhow::Result<()> { + fn sync(&mut self) -> anyhow::Result<()> { writeln!(self.output, "readyok")?; Ok(()) } @@ -125,58 +133,56 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { Ok(()) } - fn handle_ucinewgame(&mut self) -> anyhow::Result<()> { - // TODO: Implement this method - reset search state. + fn new_game(&mut self) -> anyhow::Result<()> { + // TODO: Reset search state. + // TODO: Clear transposition table. + // TODO: Reset time manager. Ok(()) } /// Changes the position of the board to the one specified in the command. - fn handle_position( - &mut self, - stream: &mut std::slice::Iter<&str>, - tokens: &[&str], - ) -> anyhow::Result<()> { - match stream.next() { - Some(&"startpos") => self.position = Position::starting(), - Some(&"fen") => { - const FEN_SIZE: usize = 6; - const COMMAND_START_SIZE: usize = 2; - if tokens.len() < COMMAND_START_SIZE + FEN_SIZE { - writeln!( - self.output, - "info string FEN consists of 6 pieces, got {}", - tokens.len() - 2 - )?; - } - self.position = Position::from_fen(&stream.take(FEN_SIZE).join(" "))?; - }, - _ => writeln!( - self.output, - "info string Expected `position [fen | startpos] - moves ... `, got: {:?}", - tokens.join(" ") - )?, - } - if stream.next() == Some(&"moves") { - for next_move in stream { - match Move::from_uci(next_move) { - Ok(next_move) => self.position.make_move(&next_move), - Err(e) => writeln!(self.output, "info string Unexpected UCI move: {e}")?, - } + fn set_position(&mut self, fen: Option, moves: Vec) -> anyhow::Result<()> { + match fen { + Some(fen) => self.position = Position::from_fen(&fen)?, + None => self.position = Position::starting(), + }; + for next_move in moves { + match Move::from_uci(&next_move) { + Ok(next_move) => self.position.make_move(&next_move), + Err(_) => unreachable!(), } } Ok(()) } - // TODO: Handle: wtime btime winc binc - fn handle_go(&mut self, command: Command::Go) -> anyhow::Result<()> { + fn go( + &mut self, + max_depth: Option, + wtime: Option, + btime: Option, + winc: Option, + binc: Option, + nodes: Option, + mate: Option, + movetime: Option, + infinite: bool, + ) -> anyhow::Result<()> { + if mate.is_some() { + todo!() + } + let (time, increment) = match self.position.us() { + Player::White => (wtime, winc), + Player::Black => (btime, binc), + }; + let next_move = find_best_move(&self.position, max_depth, time, self.output); + writeln!(self.output, "bestmove {next_move}")?; Ok(()) } /// Stops the search immediately. /// /// NOTE: This is a no-op for now. - fn handle_stop(&mut self) -> anyhow::Result<()> { + fn stop_search(&mut self) -> anyhow::Result<()> { // TODO: Implement this method. Ok(()) } diff --git a/src/engine/openbench.rs b/src/engine/openbench.rs new file mode 100644 index 000000000..7f4c95ec4 --- /dev/null +++ b/src/engine/openbench.rs @@ -0,0 +1,15 @@ +//! Implementing [`bench`] command is a [requirement for OpenBench], which is an +//! incredibly important tool for measuring the performance and strenght of the +//! engine. +//! +//! [requirement for OpenBench]: https://github.com/AndyGrant/OpenBench/wiki/Requirements-For-Public-Engines#basic-requirements + +/// Runs search on a small set of positions to provide an estimate of engine's +/// performance. +/// +/// NOTE: This function **has to run less than 60 seconds**. +/// +/// See for more details. +pub fn bench(out: &mut dyn std::io::Write) { + todo!() +} diff --git a/src/engine/time_manager.rs b/src/engine/time_manager.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/engine/time_manager.rs @@ -0,0 +1 @@ + diff --git a/src/engine/uci.rs b/src/engine/uci.rs index e2eda19ac..e20bd6f83 100644 --- a/src/engine/uci.rs +++ b/src/engine/uci.rs @@ -1,3 +1,7 @@ +use std::time::Duration; + +use crate::search::Depth; + #[derive(Debug, PartialEq)] pub(super) enum Command { Uci, @@ -15,18 +19,23 @@ pub(super) enum Command { }, NewGame, Go { - depth: Option, - wtime: Option, - btime: Option, - winc: Option, - binc: Option, - nodes: Option, - mate: Option, - movetime: Option, + max_depth: Option, + wtime: Option, + btime: Option, + winc: Option, + binc: Option, + nodes: Option, + mate: Option, + movetime: Option, infinite: bool, }, Stop, Quit, + /// This is an extension to the UCI protocol useful for debugging. The + /// response will contain the static evaluation of the current position and + /// the engine internal state (current settings, search options, + /// transposition table information and so on). + State, Unknown(String), } @@ -44,7 +53,7 @@ pub(super) enum OptionValue { } fn parse_go(parts: &[&str]) -> Command { - let mut depth = None; + let mut max_depth = None; let mut wtime = None; let mut btime = None; let mut winc = None; @@ -58,14 +67,24 @@ fn parse_go(parts: &[&str]) -> Command { while i < parts.len() { match parts[i] { - "depth" if i + 1 < parts.len() => depth = parts[i + 1].parse().ok(), - "wtime" if i + 1 < parts.len() => wtime = parts[i + 1].parse().ok(), - "btime" if i + 1 < parts.len() => btime = parts[i + 1].parse().ok(), - "winc" if i + 1 < parts.len() => winc = parts[i + 1].parse().ok(), - "binc" if i + 1 < parts.len() => binc = parts[i + 1].parse().ok(), + "depth" if i + 1 < parts.len() => max_depth = parts[i + 1].parse().ok(), + "wtime" if i + 1 < parts.len() => { + wtime = parts[i + 1].parse().map(Duration::from_micros).ok(); + }, + "btime" if i + 1 < parts.len() => { + btime = parts[i + 1].parse().map(Duration::from_micros).ok(); + }, + "winc" if i + 1 < parts.len() => { + winc = parts[i + 1].parse().map(Duration::from_micros).ok(); + }, + "binc" if i + 1 < parts.len() => { + binc = parts[i + 1].parse().map(Duration::from_micros).ok(); + }, "nodes" if i + 1 < parts.len() => nodes = parts[i + 1].parse().ok(), "mate" if i + 1 < parts.len() => mate = parts[i + 1].parse().ok(), - "movetime" if i + 1 < parts.len() => movetime = parts[i + 1].parse().ok(), + "movetime" if i + 1 < parts.len() => { + movetime = parts[i + 1].parse().map(Duration::from_micros).ok(); + }, "infinite" => infinite = true, _ => {}, } @@ -77,7 +96,7 @@ fn parse_go(parts: &[&str]) -> Command { } Command::Go { - depth, + max_depth, wtime, btime, winc, @@ -132,7 +151,7 @@ fn parse_setposition(parts: &[&str]) -> Command { let moves = if let Some(moves_index) = moves_index { parts[moves_index + 1..] .iter() - .map(|s| s.to_string()) + .map(|s| (*s).to_string()) .collect() } else { vec![] @@ -145,22 +164,23 @@ impl Command { let parts: Vec<&str> = input.split_whitespace().collect(); if parts.is_empty() { - return Command::Unknown(input.to_string()); + return Self::Unknown(input.to_string()); } match parts[0] { - "uci" => Command::Uci, - "debug" if parts.len() > 1 => Command::Debug { + "uci" => Self::Uci, + "debug" if parts.len() > 1 => Self::Debug { on: parts[1] == "on", }, - "isready" => Command::IsReady, + "isready" => Self::IsReady, "setoption" => parse_setoption(&parts), "position" => parse_setposition(&parts), - "ucinewgame" => Command::NewGame, + "ucinewgame" => Self::NewGame, "go" => parse_go(&parts), - "stop" => Command::Stop, - "quit" => Command::Quit, - _ => Command::Unknown(input.to_string()), + "stop" => Self::Stop, + "quit" => Self::Quit, + "state" => Self::State, + _ => Self::Unknown(input.to_string()), } } } @@ -239,23 +259,23 @@ mod tests { #[test] fn parse_go() { - assert_eq!(Command::parse("go depth 20 wtime 300000 btime 300000 winc 10000 binc 10000 nodes 500000 mate 10 movetime 5000 infinite"), + assert_eq!(Command::parse("go depth 20 wtime 300000 btime 300000 winc 10000 binc 10000 nodes 500000 movetime 5000"), Command::Go { - depth: Some(20), - wtime: Some(300000), - btime: Some(300000), - winc: Some(10000), - binc: Some(10000), - nodes: Some(500000), - mate: Some(10), - movetime: Some(5000), - infinite: true, + max_depth: Some(20), + wtime: Some(Duration::from_micros(300_000)), + btime: Some(Duration::from_micros(300_000)), + winc: Some(Duration::from_micros(10000)), + binc: Some(Duration::from_micros(10000)), + nodes: Some(500_000), + mate: None, + movetime: Some(Duration::from_micros(5000)), + infinite: false, }); assert_eq!( Command::parse("go depth 10"), Command::Go { - depth: Some(10), + max_depth: Some(10), wtime: None, btime: None, winc: None, @@ -270,8 +290,8 @@ mod tests { assert_eq!( Command::parse("go wtime 1000"), Command::Go { - depth: None, - wtime: Some(1000), + max_depth: None, + wtime: Some(Duration::from_micros(1000)), btime: None, winc: None, binc: None, @@ -285,7 +305,7 @@ mod tests { assert_eq!( Command::parse("go infinite"), Command::Go { - depth: None, + max_depth: None, wtime: None, btime: None, winc: None, @@ -296,6 +316,21 @@ mod tests { infinite: true, } ); + + assert_eq!( + Command::parse("go mate 42"), + Command::Go { + max_depth: None, + wtime: None, + btime: None, + winc: None, + binc: None, + nodes: None, + mate: Some(42), + movetime: None, + infinite: false, + } + ); } #[test] @@ -308,6 +343,11 @@ mod tests { assert_eq!(Command::parse("quit"), Command::Quit); } + #[test] + fn parse_state() { + assert_eq!(Command::parse("state"), Command::State); + } + #[test] fn unknown() { assert_eq!( diff --git a/src/evaluation/mod.rs b/src/evaluation/mod.rs index c32ef919f..de4d95bce 100644 --- a/src/evaluation/mod.rs +++ b/src/evaluation/mod.rs @@ -13,18 +13,50 @@ pub(crate) mod features; pub(crate) mod pesto; /// A thin wrapper around i32 for ergonomics and type safety. -// TODO: Support "Mate in X" by using the unoccupied range of i32. +// TODO: Use i16 once the evaluation is NN-based. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct Score { - /// Evaluation relative value in centipawn (100 CP = 1 "pawn") units. - value: i32, -} +pub struct Score(i32); impl Score { - /// Corresponds to checkmating the opponent. - pub(crate) const MAX: Self = Self { value: 32_000 }; - /// Corresponds to being checkmated by opponent. - pub(crate) const MIN: Self = Self { value: -32_000 }; + pub(crate) const DRAW: Self = Self(0); + pub(crate) const INFINITY: Self = Self(2_000_000_000); + /// `[-INFINITY, -INFINITY + MATE_RANGE)` and `(INFINITY - MATE_RANGE, + /// INFINITY]` are reserved for mate scores. + /// `[-INFINITY + MATE_RANGE, INFINITY - MATE_RANGE]` if for centipawn + /// evaluations. + const MATE_RANGE: i32 = 1000; + + /// Creates a new score in centipawn units. Centipawn units do not mean in + /// terms of NNUE evaluation, but it is convenient for GUIs and UCI + /// purposes, as well as human intepretation. + /// + /// The value must be in the range `[-INFINITY + MATE_RANGE, INFINITY - + /// MATE_RANGE]`. + #[must_use] pub fn cp(value: i32) -> Self { + assert!(value.abs() < Self::INFINITY.0 - Self::MATE_RANGE); + Self(value) + } + + /// Creates a new score representing player's victory in `moves` *full* + /// moves. + #[must_use] pub fn mate(moves: u8) -> Self { + Self(Self::INFINITY.0 - i32::from(moves)) + } + + /// Returns the number of moves until mate. + /// + /// # Panics + /// + /// Panics if the score is not a mate score. + #[must_use] pub fn mate_in(&self) -> u8 { + assert!(self.is_mate()); + todo!() + } + + /// Returns `true` if the score represents a mate, not centipawn evaluation. + #[must_use] pub fn is_mate(&self) -> bool { + self.0.abs() >= Self::INFINITY.0 - Self::MATE_RANGE + } } impl Neg for Score { @@ -32,21 +64,17 @@ impl Neg for Score { /// Mirrors evaluation to other player's perspective. fn neg(self) -> Self::Output { - Self { - value: self.value.neg(), - } - } -} - -impl From for Score { - fn from(value: i32) -> Self { - Self { value } + Self(-self.0) } } impl Display for Score { /// Formats the score as centipawn units for UCI interface. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "cp {}", self.value) + if self.is_mate() { + write!(f, "mate {}", self.mate_in()) + } else { + write!(f, "cp {}", self.0) + } } } diff --git a/src/evaluation/pesto.rs b/src/evaluation/pesto.rs index 6ff935117..f235419d2 100644 --- a/src/evaluation/pesto.rs +++ b/src/evaluation/pesto.rs @@ -27,8 +27,7 @@ pub(crate) fn evaluate(position: &Position) -> Score { let mut game_phase = 0; - for square in 0..64u8 { - let square = Square::try_from(square).expect("valid square"); + for square in Square::iter() { if let Some(piece) = position.at(square) { if piece.owner == Player::White { middlegame_white += MIDDLEGAME_VALUES[piece.plane()][square as usize]; @@ -53,14 +52,13 @@ pub(crate) fn evaluate(position: &Position) -> Score { let middlegame_phase = std::cmp::min(game_phase, 24); let endgame_phase = 24 - middlegame_phase; - Score::from((middlegame_score * middlegame_phase + endgame_score * endgame_phase) / 24) + Score::cp((middlegame_score * middlegame_phase + endgame_score * endgame_phase) / 24) } #[cfg(test)] mod tests { - use crate::chess::core::{Piece, PieceKind, Player, Square}; - use super::*; + use crate::chess::core::{Piece, PieceKind, Player, Square}; // Check that the tables are correctly built and loaded. #[test] @@ -98,7 +96,7 @@ mod tests { #[test] fn starting_position() { - assert_eq!(evaluate(&Position::starting()), Score::from(0)); + assert_eq!(evaluate(&Position::starting()), Score::cp(0)); } #[test] @@ -112,6 +110,6 @@ mod tests { &Position::from_fen("rnbq1bnr/pp4pp/4kp2/2pp4/8/N7/PPPPPP1P/R1BQ1K1R w - - 4 11") .expect("valid position") ) - ) + ); } } diff --git a/src/lib.rs b/src/lib.rs index 9f88e91c1..f2d260553 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,51 +2,13 @@ //! //! [README]: https://github.com/kirillbobyrev/pabi/blob/main/README.md -// TODO: Gradually move most of warnings to deny. -#![warn( - missing_docs, - variant_size_differences, - absolute_paths_not_starting_with_crate, - macro_use_extern_crate, - unused_extern_crates, - unused_import_braces, - unused_lifetimes, - unused_qualifications, - let_underscore_drop -)] -#![warn( - clippy::correctness, - clippy::suspicious, - clippy::style, - clippy::complexity, - clippy::pedantic, - clippy::nursery, - clippy::cargo -)] -#![deny( - // rustc - unreachable_pub, - keyword_idents, - keyword_idents_2018, - keyword_idents_2024, - unused_results, - trivial_casts, - trivial_numeric_casts, - single_use_lifetimes, - // clippy - clippy::perf, - // rustdoc - rustdoc::broken_intra_doc_links, - rustdoc::invalid_rust_codeblocks, - rustdoc::unescaped_backticks -)] - // TODO: Re-export types for convenience. pub mod chess; +pub mod engine; pub mod evaluation; pub mod search; -mod engine; +pub use engine::openbench::bench; pub use engine::Engine; use shadow_rs::shadow; diff --git a/src/main.rs b/src/main.rs index 3d4644953..9ddceb972 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,8 @@ use std::env; fn main() -> anyhow::Result<()> { let args: Vec = env::args().collect(); - if args.len() > 1 && args[1] == "bench" { - todo!() + if args.len() == 2 && args[1] == "bench" { + pabi::engine::openbench::bench(&mut std::io::stdout().lock()); } pabi::print_engine_info(); @@ -12,6 +12,6 @@ fn main() -> anyhow::Result<()> { let mut input = std::io::stdin().lock(); let mut output = std::io::stdout().lock(); - let mut engine = pabi::Engine::new(&mut input, &mut output); + let mut engine = pabi::engine::Engine::new(&mut input, &mut output); engine.uci_loop() } diff --git a/src/search/history.rs b/src/search/history.rs index e69de29bb..8b1378917 100644 --- a/src/search/history.rs +++ b/src/search/history.rs @@ -0,0 +1 @@ + diff --git a/src/search/minimax.rs b/src/search/minimax.rs index 25c691562..eefd652a6 100644 --- a/src/search/minimax.rs +++ b/src/search/minimax.rs @@ -19,21 +19,22 @@ pub(super) fn negamax(context: &mut Context, depth: u8, alpha: Score, beta: Scor if position.is_checkmate() { // The player to move is in checkmate. - return Score::MIN; + // TODO: Mate in. + return -Score::INFINITY; } // TODO: is_draw: stalemate + 50 move rule + 3 repetitions. if position.is_stalemate() { // TODO: Maybe handle stalemate differently since it's a "precise" // evaluation. - return Score::from(0); + return Score::DRAW; } if depth == 0 { return evaluate(position); } - let mut best_eval = Score::MIN; + let mut best_eval = -Score::INFINITY; let mut alpha = alpha; // TODO: Do not copy here, figure out how to beat the borrow checker. @@ -72,7 +73,7 @@ mod tests { fn zero_depth() { let mut state = Context::new(&Position::starting()); assert_eq!( - negamax(&mut state, 0, Score::MIN, Score::MAX), + negamax(&mut state, 0, -Score::INFINITY, Score::INFINITY), evaluate(&Position::starting()) ); } @@ -80,27 +81,26 @@ mod tests { #[test] fn starting_position() { let mut state = Context::new(&Position::starting()); - assert!(negamax(&mut state, 1, Score::MIN, Score::MAX) >= Score::from(0)); + assert!(negamax(&mut state, 1, -Score::INFINITY, Score::INFINITY) >= Score::cp(0)); } - /* - #[test] - fn symmetric_evaluation() { - let original_position = - Position::from_fen("rnbq1bnr/pp4pp/4kp2/2pp4/8/N7/PPPPPP1P/R1BQ1K1R b - - 4 11") - .expect("valid position"); - let mut state = Context::new(&original_position); - let original_evaluation = negamax(&mut state, 1, Score::MIN, Score::MAX); - - let symmetric_position = - Position::from_fen("rnbq1bnr/pp4pp/4kp2/2pp4/8/N7/PPPPPP1P/R1BQ1K1R w - - 4 11") - .expect("valid position"); - let mut state = Context::new(&symmetric_position); - let symmetric_evaluation = negamax(&mut state, 1, Score::MIN, Score::MAX); - - assert_eq!(original_evaluation, -symmetric_evaluation); - } - */ + // #[test] + // fn symmetric_evaluation() { + // let original_position = + // Position::from_fen("rnbq1bnr/pp4pp/4kp2/2pp4/8/N7/PPPPPP1P/R1BQ1K1R b - - + // 4 11") .expect("valid position"); + // let mut state = Context::new(&original_position); + // let original_evaluation = negamax(&mut state, 1, Score::MIN, Score::MAX); + // + // let symmetric_position = + // Position::from_fen("rnbq1bnr/pp4pp/4kp2/2pp4/8/N7/PPPPPP1P/R1BQ1K1R w - - + // 4 11") .expect("valid position"); + // let mut state = Context::new(&symmetric_position); + // let symmetric_evaluation = negamax(&mut state, 1, Score::MIN, + // Score::MAX); + // + // assert_eq!(original_evaluation, -symmetric_evaluation); + // } // #[test] // fn find_mate_losing_position() { diff --git a/src/search/mod.rs b/src/search/mod.rs index 6ffec7c90..fc43ded36 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -15,10 +15,12 @@ use crate::chess::core::Move; use crate::chess::position::Position; use crate::evaluation::Score; +mod history; pub(crate) mod minimax; mod transposition; -type Depth = u8; +/// Search depth in plies. +pub type Depth = u8; /// The search depth does not grow fast and an upper limit is set for improving /// performance. @@ -44,28 +46,38 @@ impl Context { } } -pub(crate) struct Settings { - depth: Option, - time: Option, +pub(crate) struct Limiter { + pub(crate) timer: Instant, + pub(crate) depth: Option, + pub(crate) time: Option, } /// Adding reserve time to ensure that the engine does not exceed the time /// limit. -// TODO: Tweak this. -const RESERVE: Duration = Duration::from_millis(500); +// TODO: Tweak/tune this. +const RESERVE: Duration = Duration::from_millis(100); /// Runs the search algorithm to find the best move under given time /// constraints. -pub fn go(position: &Position, limit: Duration, output: &mut impl Write) -> Move { - let timer = Instant::now(); +pub(crate) fn find_best_move( + position: &Position, + max_depth: Option, + time: Option, + output: &mut impl Write, +) -> Move { + let limiter = Limiter { + timer: Instant::now(), + depth: max_depth, + time, + }; let mut context = Context::new(position); let mut best_move = None; - const MAX_DEPTH: Depth = 8; + let max_depth = limiter.depth.unwrap_or(Depth::MAX); - for depth in 1..MAX_DEPTH { + for depth in 1..max_depth { let (next_move, score) = find_best_move_and_score(position, depth, &mut context); best_move = Some(next_move); @@ -76,12 +88,14 @@ pub fn go(position: &Position, limit: Duration, output: &mut impl Write) -> Move score, &best_move.unwrap().to_string(), context.num_nodes, - timer.elapsed().as_millis(), + limiter.timer.elapsed().as_millis(), ) .unwrap(); - if timer.elapsed() + RESERVE >= limit { - break; + if let Some(time_limit) = limiter.time { + if limiter.timer.elapsed() + RESERVE >= time_limit { + break; + } } } @@ -89,9 +103,10 @@ pub fn go(position: &Position, limit: Duration, output: &mut impl Write) -> Move output, "info nodes {} nps {}", context.num_nodes, - (context.num_nodes as f64 / timer.elapsed().as_secs_f64()) as u64, + (context.num_nodes as f64 / limiter.timer.elapsed().as_secs_f64()) as u64, ) .unwrap(); + best_move.unwrap() } @@ -100,13 +115,15 @@ fn find_best_move_and_score( depth: Depth, context: &mut Context, ) -> (Move, Score) { + assert!(depth > 0); + context.num_nodes += 1; let mut best_move = None; - let mut best_score = Score::MIN; + let mut best_score = -Score::INFINITY; - let alpha = Score::MIN; - let beta = Score::MAX; + let alpha = -Score::INFINITY; + let beta = Score::INFINITY; for next_move in position.generate_moves() { let mut next_position = position.clone(); diff --git a/src/search/transposition.rs b/src/search/transposition.rs index af8e5f9ae..110ba483d 100644 --- a/src/search/transposition.rs +++ b/src/search/transposition.rs @@ -1,22 +1,43 @@ //! Implements Zobrist hashing and [Transposition Table] functionality. //! -//! [Transposition Table](https://www.chessprogramming.org/Transposition_Table +//! [Transposition Table]: https://www.chessprogramming.org/Transposition_Table + +use std::collections::HashMap; use crate::chess::zobrist::Key; +use crate::evaluation::Score; + +pub(super) struct Entry { + pub(super) depth: u8, + pub(super) score: Score, + pub(super) best_move: Option, + pub(super) bound: Bound, + pub(super) flags: u8, +} -pub(super) struct Entry {} +pub(super) enum Bound { + Exact, + Lower, + Upper, +} -// TODO: Migrate to RawTable instead for better performance? -pub(super) struct TranspositionTable {} +pub(super) struct TranspositionTable { + // TODO: Migrate to RawTable instead for better performance? + table: HashMap, + size: usize, +} impl TranspositionTable { #[must_use] - pub(super) fn new() -> Self { - todo!() + pub(super) fn new(size: usize) -> Self { + Self { + table: HashMap::with_capacity(size), + size, + } } pub(super) fn clear(&mut self) { - todo!() + self.table.clear(); } #[must_use] @@ -28,3 +49,11 @@ impl TranspositionTable { todo!() } } + +#[cfg(test)] +mod tests { + #[test] + fn clear() { + todo!() + } +} diff --git a/tests/chess.rs b/tests/chess.rs index 150c89085..2f708684b 100644 --- a/tests/chess.rs +++ b/tests/chess.rs @@ -47,67 +47,67 @@ fn basic_positions() { #[test] #[should_panic(expected = "expected 1 white king, got 0")] fn no_white_king() { - Position::try_from("3k4/8/8/8/8/8/8/8 w - - 0 1").unwrap(); + let _ = setup("3k4/8/8/8/8/8/8/8 w - - 0 1"); } #[test] #[should_panic(expected = "expected 1 black king, got 0")] fn no_black_king() { - Position::try_from("8/8/8/8/8/8/8/3K4 w - - 0 1").unwrap(); + let _ = setup("8/8/8/8/8/8/8/3K4 w - - 0 1"); } #[test] #[should_panic(expected = "expected 1 white king, got 3")] fn too_many_kings() { - Position::try_from("1kkk4/8/8/8/8/8/8/1KKK4 w - - 0 1").unwrap(); + let _ = setup("1kkk4/8/8/8/8/8/8/1KKK4 w - - 0 1"); } #[test] #[should_panic(expected = "expected <= 8 white pawns, got 9")] fn too_many_white_pawns() { - Position::try_from("rnbqkbnr/pppppppp/8/8/8/P7/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap(); + let _ = setup("rnbqkbnr/pppppppp/8/8/8/P7/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); } #[test] #[should_panic(expected = "expected <= 8 black pawns, got 9")] fn too_many_black_pawns() { - Position::try_from("rnbqkbnr/pppppppp/p7/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1").unwrap(); + let _ = setup("rnbqkbnr/pppppppp/p7/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); } #[test] #[should_panic(expected = "pawns can not be placed on backranks")] fn pawns_on_backranks() { - Position::try_from("3kr3/8/8/8/8/5Q2/8/1KP5 w - - 0 1").unwrap(); + let _ = setup("3kr3/8/8/8/8/5Q2/8/1KP5 w - - 0 1"); } #[test] #[should_panic(expected = "expected en passant square to be on rank 6, got 3")] fn wrong_en_passant_player() { - Position::try_from("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e3 0 1").unwrap(); + let _ = setup("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e3 0 1"); } #[test] #[should_panic(expected = "expected en passant square to be on rank 3, got 4")] fn wrong_en_passant_rank() { - Position::try_from("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq e4 0 1").unwrap(); + let _ = setup("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq e4 0 1"); } #[test] #[should_panic(expected = "en passant square is not beyond pushed pawn")] fn en_passant_not_beyond_pawn() { - Position::try_from("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq d3 0 1").unwrap(); + let _ = setup("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq d3 0 1"); } #[test] #[should_panic(expected = "more than 1 check after double pawn push is impossible")] fn en_passant_double_check() { - Position::try_from("r2qkbnr/ppp3Np/8/4Q3/4P3/8/PP4PP/RNB1KB1R b KQkq e3 0 1").unwrap(); + let _ = setup("r2qkbnr/ppp3Np/8/4Q3/4P3/8/PP4PP/RNB1KB1R b KQkq e3 0 1"); } #[test] #[should_panic(expected = "expected <= 2 checks, got 3")] fn triple_check() { - Position::try_from("2r3r1/P3k3/prp5/1B5p/5P2/2Q1n2p/PP4KP/3R4 w - - 0 34").unwrap(); + let _ = setup("2r3r1/P3k3/prp5/1B5p/5P2/2Q1n2p/PP4KP/3R4 w - - 0 34"); } #[test] @@ -116,14 +116,13 @@ fn triple_check() { original pawn square or the pushed pawn itself" )] fn check_with_unrelated_en_passant() { - Position::try_from("rnbqk1nr/bb3p1p/1q2r3/2pPp3/3P4/7P/1PP1NpPP/R1BQKBNR w KQkq c6 0 1") - .unwrap(); + let _ = setup("rnbqk1nr/bb3p1p/1q2r3/2pPp3/3P4/7P/1PP1NpPP/R1BQKBNR w KQkq c6 0 1"); } #[test] #[should_panic(expected = "doubly pushed pawn can not be the only blocker on a diagonal")] fn double_push_blocks_existing_check() { - Position::try_from("q6k/8/8/3pP3/8/8/8/7K w - d6 0 1").unwrap(); + let _ = Position::try_from("q6k/8/8/3pP3/8/8/8/7K w - d6 0 1"); } #[test] @@ -171,15 +170,16 @@ fn no_crash() { .is_err()); } -// This test is very expensive in the Debug setting (could take 200+ seconds): -// disable it by default. +// This test is very expensive in the Debug setting (could take 200+ seconds). #[ignore] #[test] fn arbitrary_positions() { - for serialized_position in - fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/data/positions.fen")) - .unwrap() - .lines() + for serialized_position in fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/positions.fen" + )) + .unwrap() + .lines() { let position = Position::try_from(serialized_position).unwrap(); assert_eq!(position.to_string(), sanitize_fen(serialized_position)); @@ -517,15 +517,16 @@ fn make_moves() { ); } -// This test is very expensive in the Debug setting (could take 200+ seconds): -// disable it by default. +// This test is very expensive in the Debug setting (could take 200+ seconds). #[ignore] #[test] fn random_positions() { - for serialized_position in - fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/data/positions.fen")) - .unwrap() - .lines() + for serialized_position in fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/positions.fen" + )) + .unwrap() + .lines() { let position = Position::from_fen(serialized_position).unwrap(); let shakmaty_setup: shakmaty::fen::Fen = serialized_position.parse().unwrap(); @@ -536,7 +537,7 @@ fn random_positions() { assert_eq!( moves .iter() - .map(|m| m.to_string()) + .map(std::string::ToString::to_string) .sorted() .collect::>(), shakmaty::Position::legal_moves(&shakmaty_position) @@ -605,9 +606,9 @@ fn perft_starting_position() { fn perft_expensive_starting() { // Position 1. let position = Position::starting(); - assert_eq!(perft(&position, 4), 197281); - assert_eq!(perft(&position, 5), 4865609); - assert_eq!(perft(&position, 6), 119060324); + assert_eq!(perft(&position, 4), 197_281); + assert_eq!(perft(&position, 5), 4_865_609); + assert_eq!(perft(&position, 6), 119_060_324); } // Positions from https://www.chessprogramming.org/Perft_Results @@ -626,8 +627,8 @@ fn perft_kiwipete() { fn perft_kiwipete_expensive() { // Position 2. let position = setup("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"); - assert_eq!(perft(&position, 4), 4085603); - assert_eq!(perft(&position, 5), 193690690); + assert_eq!(perft(&position, 4), 4_085_603); + assert_eq!(perft(&position, 5), 193_690_690); } // Position 3. @@ -643,8 +644,8 @@ fn perft_endgame() { #[ignore] fn perft_endgame_expensive() { let position = setup("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"); - assert_eq!(perft(&position, 4), 4085603); - assert_eq!(perft(&position, 5), 193690690); + assert_eq!(perft(&position, 4), 4_085_603); + assert_eq!(perft(&position, 5), 193_690_690); } // Position 4. @@ -660,8 +661,8 @@ fn perft_complex() { #[ignore] fn perft_complex_expensive() { let position = setup("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1"); - assert_eq!(perft(&position, 4), 422333); - assert_eq!(perft(&position, 5), 15833292); + assert_eq!(perft(&position, 4), 422_333); + assert_eq!(perft(&position, 5), 15_833_292); } // Position 5. @@ -677,8 +678,8 @@ fn perft_fifth() { #[ignore] fn perft_fifth_expensive() { let position = setup("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"); - assert_eq!(perft(&position, 4), 2103487); - assert_eq!(perft(&position, 5), 89941194); + assert_eq!(perft(&position, 4), 2_103_487); + assert_eq!(perft(&position, 5), 89_941_194); } // Position 6. @@ -696,8 +697,8 @@ fn perft_sixth() { fn perft_sixth_expensive() { let position = setup("r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"); - assert_eq!(perft(&position, 4), 3894594); - assert_eq!(perft(&position, 5), 164075551); + assert_eq!(perft(&position, 4), 3_894_594); + assert_eq!(perft(&position, 5), 164_075_551); } // Other positions. @@ -709,7 +710,7 @@ fn perft_complex_middlegame() { assert_eq!(perft(&position, 1), 46); assert_eq!(perft(&position, 2), 1149); assert_eq!(perft(&position, 3), 51032); - assert_eq!(perft(&position, 4), 1352097); + assert_eq!(perft(&position, 4), 1_352_097); } #[test] @@ -720,7 +721,7 @@ fn perft_endgame_promotions() { assert_eq!(perft(&position, 3), 949); assert_eq!(perft(&position, 4), 4848); assert_eq!(perft(&position, 5), 67834); - assert_eq!(perft(&position, 6), 390018); + assert_eq!(perft(&position, 6), 390_018); } #[test] @@ -741,7 +742,7 @@ fn perft_queen_endgame() { assert_eq!(perft(&position, 2), 97); assert_eq!(perft(&position, 3), 2422); assert_eq!(perft(&position, 4), 11436); - assert_eq!(perft(&position, 5), 291937); + assert_eq!(perft(&position, 5), 291_937); } #[test] @@ -751,7 +752,7 @@ fn perft_tactical_opening() { assert_eq!(perft(&position, 1), 29); assert_eq!(perft(&position, 2), 605); assert_eq!(perft(&position, 3), 18210); - assert_eq!(perft(&position, 4), 413607); + assert_eq!(perft(&position, 4), 413_607); } #[test] @@ -762,7 +763,7 @@ fn perft_advanced_pawn_race() { assert_eq!(perft(&position, 3), 461); assert_eq!(perft(&position, 4), 5919); assert_eq!(perft(&position, 5), 38616); - assert_eq!(perft(&position, 6), 565553); + assert_eq!(perft(&position, 6), 565_553); } #[test] @@ -773,7 +774,7 @@ fn perft_queen_vs_pawns() { assert_eq!(perft(&position, 3), 461); assert_eq!(perft(&position, 4), 5919); assert_eq!(perft(&position, 5), 38616); - assert_eq!(perft(&position, 6), 565553); + assert_eq!(perft(&position, 6), 565_553); } #[test] @@ -786,7 +787,7 @@ fn perft_promotion_options() { #[test] fn perft_cpw_challenge() { let position = setup("rnb1kbnr/pp1pp1pp/1qp2p2/8/Q1P5/N7/PP1PPPPP/1RB1KBNR b Kkq - 2 4"); - assert_eq!(perft(&position, 7), 14794751816); + assert_eq!(perft(&position, 7), 14_794_751_816); } #[test] diff --git a/data/README.md b/tests/data/README.md similarity index 100% rename from data/README.md rename to tests/data/README.md diff --git a/data/positions.fen b/tests/data/positions.fen similarity index 100% rename from data/positions.fen rename to tests/data/positions.fen diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 000000000..85fe16455 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,33 @@ +use assert_cmd::Command; +use predicates::boolean::PredicateBooleanExt; +use predicates::str::{contains, is_match}; + +const BINARY_NAME: &str = "pabi"; + +#[test] +fn uci_setup() { + let mut cmd = Command::cargo_bin(BINARY_NAME).expect("Binary should be built"); + + drop( + cmd.write_stdin("uci\n") // Write the uci command to stdin + .assert() + .success() + .stdout( + contains("id name") + .and(contains("id author")) + .and(contains("uciok")), + ), + ); +} + +#[test] +fn openbench_output() { + let mut cmd = Command::cargo_bin(BINARY_NAME).expect("Binary should be built"); + let _ = cmd.arg("bench"); + + drop( + cmd.assert() + .stdout(is_match(r"^\d+ nodes \d+ nps$").unwrap()) + .success(), + ); +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index d93bc8139..e87ca36dd 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -5,10 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.86" -bincode = "1.3.3" -byteorder = "1.5.0" clap = { version = "4.5.6", features = ["derive"] } flate2 = "1.0.30" pabi = { path = ".." } -rayon = "1.10" tar = "0.4.41" diff --git a/tools/src/bin/extract_lc0_data.rs b/tools/src/bin/data_extractor.rs similarity index 92% rename from tools/src/bin/extract_lc0_data.rs rename to tools/src/bin/data_extractor.rs index d61ff4686..44117933d 100644 --- a/tools/src/bin/extract_lc0_data.rs +++ b/tools/src/bin/data_extractor.rs @@ -25,18 +25,18 @@ struct Args { /// Maximum number of samples to extract. #[arg(long)] limit: Option, - /// Only positions with |eval| <= q_threshold will be kept. Practically, + /// Only positions with |eval| <= value_threshold will be kept. Practically, /// distinguishing between very high evals shouldn't be very important, - /// because if an engine reaches that position, it is likely to be - /// winning/losing anyway. + /// because if an engine reaches that eval, it is likely to keep the + /// advantage and convert to victory or find a checkmate. /// - /// Q-value to CP conversion formula: + /// Value to CP conversion formula: /// - /// cp = 660.6 * q / (1 - 0.9751875 * q^10) + /// cp = 660.6 * v / (1 - 0.9751875 * v^10) /// - /// q = 0.9 corresponds to cp = 900 + /// v = 0.9 corresponds to cp = 900 #[arg(long, default_value_t = 0.9)] - q_threshold: f32, + value_threshold: f32, /// Remove positions with less than min_pieces pieces on the board. This is /// useful, because most tournaments allow using 6 man tablebases. #[arg(long, default_value_t = 6)] @@ -162,12 +162,12 @@ fn extract_planes(sample: &V6TrainingData) -> Vec { ] } -fn keep_sample(sample: &V6TrainingData, q_threshold: f32, filter_captures: bool) -> bool { +fn keep_sample(sample: &V6TrainingData, value_threshold: f32, filter_captures: bool) -> bool { assert!(sample.version == 6 && sample.input_format == 1); if sample.invariance_info & (1 << 6) != 0 { return false; } - if sample.best_q.abs() > q_threshold { + if sample.best_q.abs() > value_threshold { return false; } @@ -238,14 +238,14 @@ fn serialize_sample(sample: &V6TrainingData, out: &mut BufWriter) - fn process_archive( archive: impl BufRead, output: &mut BufWriter, - q_threshold: f32, + value_threshold: f32, filter_captures: bool, ) -> io::Result { let mut num_samples = 0; for sample in extract_training_samples(archive)? .into_iter() - .filter(|sample| keep_sample(sample, q_threshold, filter_captures)) + .filter(|sample| keep_sample(sample, value_threshold, filter_captures)) { serialize_sample(&sample, output)?; num_samples += 1 @@ -285,7 +285,7 @@ fn main() -> anyhow::Result<()> { let total_samples = process_archive( io::BufReader::new(archive), &mut io::BufWriter::new(out_file), - args.q_threshold, + args.value_threshold, args.filter_captures, )?; println!("Extracted {:} samples", total_samples);