diff --git a/.travis.yml b/.travis.yml
index 092c7f53..9dad6b1d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,5 @@
language: cpp
-dist: bionic
+dist: focal
matrix:
include:
@@ -7,33 +7,33 @@ matrix:
compiler: gcc
addons:
apt:
- packages: ['g++-8', 'g++-8-multilib', 'g++-multilib', 'valgrind', 'expect', 'curl']
- env:
- - COMPILER=g++-8
- - COMP=gcc
-
- - os: linux
- compiler: clang
- addons:
- apt:
- packages: ['clang-10', 'llvm-10-dev', 'g++-multilib', 'valgrind', 'expect', 'curl']
- env:
- - COMPILER=clang++-10
- - COMP=clang
-
- - os: osx
- osx_image: xcode12
- compiler: gcc
+ packages: ['g++-multilib', 'valgrind', 'expect', 'curl', 'libopenblas-dev']
env:
- COMPILER=g++
- COMP=gcc
- - os: osx
- osx_image: xcode12
- compiler: clang
- env:
- - COMPILER=clang++
- - COMP=clang
+# - os: linux
+# compiler: clang
+# addons:
+# apt:
+# packages: ['clang-10', 'llvm-10-dev', 'g++-multilib', 'valgrind', 'expect', 'curl', 'openblas']
+# env:
+# - COMPILER=clang++-10
+# - COMP=clang
+#
+# - os: osx
+# osx_image: xcode12
+# compiler: gcc
+# env:
+# - COMPILER=g++
+# - COMP=gcc
+#
+# - os: osx
+# osx_image: xcode12
+# compiler: clang
+# env:
+# - COMPILER=clang++
+# - COMP=clang
branches:
only:
@@ -65,16 +65,13 @@ script:
- make clean && make -j2 ARCH=x86-64-ssse3 build && ../tests/signature.sh $benchref
- make clean && make -j2 ARCH=x86-64-sse3-popcnt build && ../tests/signature.sh $benchref
- make clean && make -j2 ARCH=x86-64 build && ../tests/signature.sh $benchref
- - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then make clean && make -j2 ARCH=general-64 build && ../tests/signature.sh $benchref; fi
- - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then make clean && make -j2 ARCH=x86-32 optimize=no debug=yes build && ../tests/signature.sh $benchref; fi
- - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then make clean && make -j2 ARCH=x86-32-sse41-popcnt build && ../tests/signature.sh $benchref; fi
- - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then make clean && make -j2 ARCH=x86-32-sse2 build && ../tests/signature.sh $benchref; fi
- - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then make clean && make -j2 ARCH=x86-32 build && ../tests/signature.sh $benchref; fi
- - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then make clean && make -j2 ARCH=general-32 build && ../tests/signature.sh $benchref; fi
- # workaround: exclude a custom version of llvm+clang, which doesn't find llvm-profdata on ubuntu
- - if [[ "$TRAVIS_OS_NAME" != "linux" || "$COMP" == "gcc" ]]; then make clean && make -j2 ARCH=x86-64-modern profile-build && ../tests/signature.sh $benchref; fi
+ # TODO avoid _mm_malloc
+ # - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then make clean && make -j2 ARCH=general-64 build && ../tests/signature.sh $benchref; fi
+ - make clean && make -j2 ARCH=x86-64-modern profile-build && ../tests/signature.sh $benchref
# compile only for some more advanced architectures (might not run in travis)
+ - make clean && make -j2 ARCH=x86-64-avx2 blas=yes build
+
- make clean && make -j2 ARCH=x86-64-avx2 build
- make clean && make -j2 ARCH=x86-64-bmi2 build
- make clean && make -j2 ARCH=x86-64-avx512 build
@@ -91,11 +88,16 @@ script:
# Valgrind
#
- export CXXFLAGS="-O1 -fno-inline"
- - if [ -x "$(command -v valgrind )" ]; then make clean && make -j2 ARCH=x86-64-modern debug=yes optimize=no build > /dev/null && ../tests/instrumented.sh --valgrind; fi
- - if [ -x "$(command -v valgrind )" ]; then ../tests/instrumented.sh --valgrind-thread; fi
+ - make clean && make -j2 ARCH=x86-64-modern debug=yes optimize=no build > /dev/null && ../tests/instrumented.sh --valgrind
+ - ../tests/instrumented.sh --valgrind-thread
#
# Sanitizer
#
- - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then make clean && make -j2 ARCH=x86-64-modern sanitize=undefined optimize=no debug=yes build > /dev/null && ../tests/instrumented.sh --sanitizer-undefined; fi
- - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then make clean && make -j2 ARCH=x86-64-modern sanitize=thread optimize=no debug=yes build > /dev/null && ../tests/instrumented.sh --sanitizer-thread; fi
+ - make clean && make -j2 ARCH=x86-64-modern sanitize=undefined optimize=no debug=yes build > /dev/null && ../tests/instrumented.sh --sanitizer-undefined
+ - make clean && make -j2 ARCH=x86-64-modern sanitize=thread optimize=no debug=yes build > /dev/null && ../tests/instrumented.sh --sanitizer-thread
+
+ # NNUE testing
+ - export CXXFLAGS="-O1 -fno-inline"
+ - make clean && make -j2 ARCH=x86-64-modern debug=no optimize=no build > /dev/null && ../tests/instrumented_learn.sh --valgrind
+ - make clean && make -j2 ARCH=x86-64-modern sanitize=undefined optimize=no debug=yes build > /dev/null && ../tests/instrumented_learn.sh --sanitizer-undefined
diff --git a/AUTHORS b/AUTHORS
index c96f870a..b31a36e9 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -19,6 +19,7 @@ Alain Savard (Rocky640)
Alayan Feh (Alayan-stk-2)
Alexander Kure
Alexander Pagel (Lolligerhans)
+Alfredo Menezes (lonfom169)
Ali AlZhrani (Cooffe)
Andrew Grant (AndyGrant)
Andrey Neporada (nepal)
@@ -36,12 +37,14 @@ Bryan Cross (crossbr)
candirufish
Chess13234
Chris Cain (ceebo)
+Dale Weiler (graphitemaster)
Dan Schmidt (dfannius)
Daniel Axtens (daxtens)
Daniel Dugovic (ddugovic)
-Dariusz Orzechowski
+Dariusz Orzechowski (dorzechowski)
David Zar
Daylen Yang (daylen)
+Deshawn Mohan-Smith (GoldenRare)
DiscanX
Dominik Schlösser (domschl)
double-beep
@@ -83,7 +86,7 @@ Jekaa
Jerry Donald Watson (jerrydonaldwatson)
jjoshua2
Jonathan Calovski (Mysseno)
-Jonathan Dumale (SFisGOD)
+Jonathan Buladas Dumale (SFisGOD)
Joost VandeVondele (vondele)
Jörg Oster (joergoster)
Joseph Ellis (jhellis3)
@@ -109,6 +112,7 @@ Mark Tenzer (31m059)
marotear
Matthew Lai (matthewlai)
Matthew Sullivan (Matt14916)
+Maxim Molchanov (Maxim)
Michael An (man)
Michael Byrne (MichaelB7)
Michael Chaly (Vizvezdenec)
diff --git a/README.md b/README.md
index 56ce7d3e..99168e3f 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,37 @@
Stockfish NNUE
## Overview
+
Stockfish NNUE is a port of a shogi neural network named NNUE (efficiently updateable neural network backwards) to Stockfish 11. To learn more about the Stockfish chess engine, look [here](stockfish.md) for an overview and [here](https://github.com/official-stockfish/Stockfish) for the official repository.
-## Compilation Instructions for Mac
+=======
+## Building
+
+To compile:
+```
+make -jN ARCH=... build
+```
+
+To compile with Profile Guided Optimizations. Requires that the computer that is used for compilation supports the selected `ARCH`.
+```
+make -jN ARCH=... profile-build
+```
+
+`N` is the number of threads to use for compilation.
+
+`ARCH` is one of:
+`x86-64-vnni512`, `x86-64-vnni256`, `x86-64-avx512`, `x86-64-bmi2`, `x86-64-avx2`,
+`x86-64-sse41-popcnt`, `x86-64-modern`, `x86-64-ssse3`, `x86-64-sse3-popcnt`,
+`x86-64`, `x86-32-sse41-popcnt`, `x86-32-sse2`, `x86-32`, `ppc-64`, `ppc-32,
+armv7`, `armv7-neon`, `armv8`, `apple-silicon`, `general-64`, `general-32`.
+
+`ARCH` needs to be chosen based based on the instruction set of the CPU that will run stockfish. `x86-64-modern` will produce a binary that works on most common processors, but other options may increase performance for specific hardware.
+
+Additional options:
+
+- `blas=[yes/no]` - whether to use an external BLAS library. Default is `no`. Using an external BLAS library may have a significantly improve learning performance and by default expects openBLAS to be installed.
+
+### Building Instructions for Mac
1. Ensure that you have OpenBlas Installed
```
@@ -24,62 +52,91 @@ cd src
make profile-learn ARCH=x86-64 COMP=gcc
```
-
## Training Guide
+
### Generating Training Data
-To generate training data from the classic eval, use the gensfen command with the setting "Use NNUE" set to "false". The given example is generation in its simplest form. There are more commands.
+
+To generate training data from the classic eval, use the gensfen command with the setting "Use NNUE" set to "false". The given example is generation in its simplest form. There are more commands.
+
```
uci
+setoption name PruneAtShallowDepth value false
setoption name Use NNUE value false
setoption name Threads value x
setoption name Hash value y
setoption name SyzygyPath value path
isready
-gensfen depth a loop b use_draw_in_training_data_generation 1 eval_limit 32000 use_raw_nnue_eval 0
+gensfen depth a loop b use_draw_in_training_data_generation 1 eval_limit 32000
```
-Specify how many threads and how much memory you would like to use with the x and y values. The option SyzygyPath is not necessary, but if you would like to use it, you must first have Syzygy endgame tablebases on your computer, which you can find [here](http://oics.olympuschess.com/tracker/index.php). You will need to have a torrent client to download these tablebases, as that is probably the fastest way to obtain them. The path is the path to the folder containing those tablebases. It does not have to be surrounded in quotes.
-use_raw_nnue_eval controls if the training data generator or trainer uses raw NNUE eval values. Don't forget to set use_raw_nnue_eval 0 when initial training data are generated. Otherwise, the gensfen command will crash.
+- `depth` is the searched depth per move, or how far the engine looks forward. This value is an integer.
+- `loop` is the amount of positions generated. This value is also an integer.
+
+Specify how many threads and how much memory you would like to use with the `x` and `y` values. The option SyzygyPath is not necessary, but if you would like to use it, you must first have Syzygy endgame tablebases on your computer, which you can find [here](http://oics.olympuschess.com/tracker/index.php). You will need to have a torrent client to download these tablebases, as that is probably the fastest way to obtain them. The `path` is the path to the folder containing those tablebases. It does not have to be surrounded in quotes.
+
+This will create a file named "generated_kifu.binpack" in the same folder as the binary containing the generated training data. Once generation is done, you can rename the file to something like "1billiondepth12.binpack" to remember the depth and quantity of the positions and move it to a folder named "trainingdata" in the same directory as the binaries.
+
+You will also need validation data that is used for loss calculation and accuracy computation. Validation data is generated in the same way as training data, but generally at most 1 million positions should be used as there's no need for more and it would just slow the learning process down. It may also be better to slightly increase the depth for validation data. After generation you can rename the validation data file to "val.binpack" and drop it in a folder named "validationdata" in the same directory to make it easier.
+
+More information about gensfen and available options can be found in the [docs](docs/gensfen.md)
+
+### Training a network
+
+#### Training a Completely New Network
+
+Whether a new network is created or not is controlled by the UCI option `SkipLoadingEval`. If set to true then a new network will be created, which allows learning from scratch. If left at its default (false) then a network will be loaded and trained further. The second scenario is described in the reinforcement learning paragraph.
+
+A simple command chain to start with training could look like this:
-This will save a file named "generated_kifu.bin" in the same folder as the binary. Once generation is done, rename the file to something like "1billiondepth12.bin" to remember the depth and quantity of the positions and move it to a folder named "trainingdata" in the same directory as the binaries.
-#### Generation Parameters
-- Depth is the searched depth per move, or how far the engine looks forward. This value is an integer.
-- Loop is the amount of positions generated. This value is also an integer
-### Generating Validation Data
-The process is the same as the generation of training data, except for the fact that you need to set loop to 1 million, because you don't need a lot of validation data. The depth should be the same as before or slightly higher than the depth of the training data. After generation rename the validation data file to val.bin and drop it in a folder named "validationdata" in the same directory to make it easier.
-### Training a Completely New Network
-Use the "learn" binary. Create an empty folder named "evalsave" in the same directory as the binaries.
```
uci
+setoption name EnableTranspositionTable value false
+setoption name PruneAtShallowDepth value false
setoption name SkipLoadingEval value true
-setoption name Use NNUE value true
+setoption name Use NNUE value pure
setoption name Threads value x
isready
-learn targetdir trainingdata loop 100 batchsize 1000000 use_draw_in_training 1 use_draw_in_validation 1 eta 1 lambda 1 eval_limit 32000 nn_batch_size 1000 newbob_decay 0.5 eval_save_interval 250000000 loss_output_interval 1000000 mirror_percentage 50 validation_set_file_name validationdata\val.bin
+learn targetdir trainingdata epochs 10000 batchsize 1000000 use_draw_in_training 1 use_draw_in_validation 1 lr 1 lambda 1 eval_limit 32000 nn_batch_size 1000 newbob_decay 0.5 eval_save_interval 250000000 loss_output_interval 1000000 validation_set_file_name validationdata\val.binpack
```
-Nets get saved in the "evalsave" folder.
-#### Training Parameters
-- eta is the learning rate
-- lambda is the amount of weight it puts to eval of learning data vs win/draw/loss results. 1 puts all weight on eval, lambda 0 puts all weight on WDL results.
+This will utilize training data files in the "trainingdata" directory and validation data from file "validationdata\val.bin". Produced nets are saved in the "evalsave" folder.
-### Reinforcement Learning
-If you would like to do some reinforcement learning on your original network, you must first generate training data using the learn binaries with the setting `Use NNUE` set to true. Make sure that your previously trained network is in the eval folder. Use the commands specified above. Make sure `SkipLoadingEval` is set to false so that the data generated is using the neural net's eval by typing the command `setoption name SkipLoadingEval value false` before typing the `isready` command. You should aim to generate less positions than the first run, around 1/10 of the number of positions generated in the first run. The depth should be higher as well. You should also do the same for validation data, with the depth being higher than the last run.
+More information about learn and available parameters can be found in the [docs](docs/learn.md)
-After you have generated the training data, you must move it into your training data folder and delete the older data so that the binary does not accidentally train on the same data again. Do the same for the validation data and name it to val-1.bin to make it less confusing. Make sure the evalsave folder is empty. Then, using the same binary, type in the training commands shown above. Do __NOT__ set `SkipLoadingEval` to true, it must be false or you will get a completely new network, instead of a network trained with reinforcement learning. You should also set eval_save_interval to a number that is lower than the amount of positions in your training data, perhaps also 1/10 of the original value. The validation file should be set to the new validation data, not the old data.
+#### Reinforcement Learning
-After training is finished, your new net should be located in the "final" folder under the "evalsave" directory. You should test this new network against the older network to see if there are any improvements.
+If you would like to do some reinforcement learning on your original network, you must first generate training data with the setting `Use NNUE` set to `pure` and using the previous network (either name it "nn.bin" and put into alongside the binary or provide the `EvalFile` UCI option). Use the commands specified above. You should aim to generate less positions than the first run, around 1/10 of the number of positions generated in the first run. The depth should be higher as well. You should also do the same for validation data, with the depth being higher than the last run.
+
+After you have generated the training data, you must move it into your training data folder and move the older data so that the binary does not train on the same data again. Do the same for the validation data. Make sure the "evalsave" folder is empty. Then, using the same binary, type in the training commands shown above. Do __NOT__ set `SkipLoadingEval` to true, it must be false or you will get a completely new network, instead of a network trained with reinforcement learning. You should also set `eval_save_interval` to a number that is lower than the amount of positions in your training data, perhaps also 1/10 of the original value.
+
+After training is finished, your new net should be located in the "final" folder under the "evalsave" directory. You should test this new network against the older network to see if there are any improvements. Don't rely on the automatic rejection for network quality, sometimes even rejected nets can be better than the previous ones.
## Using Your Trained Net
+
If you want to use your generated net, copy the net located in the "final" folder under the "evalsave" directory and move it into a new folder named "eval" under the directory with the binaries. You can then use the halfkp_256x2 binaries pertaining to your CPU with a standard chess GUI, such as Cutechess. Refer to the [releases page](https://abrok.eu/stockfish) to find out which binary is best for your CPU.
-If the engine does not load any net file, or shows "Error! *** not found or wrong format", please try to specify the net with the full file path with the "EvalFile" option by typing the command `setoption name EvalFile value path` where path is the full file path. The "Use NNUE" option must be set to true with the command `setoption name Use NNUE value true`.
+If the engine does not load any net file, or shows "Error! *** not found or wrong format", please try to specify the net with the full file path with the `EvalFile` UCI option by typing the command `setoption name EvalFile value path` where path is the full file path. The `Use NNUE` UCI option must be set either to `true` or `pure` with the command `setoption name Use NNUE value true/pure`.
+
+## Training data formats.
+
+Currently there are 3 training data formats. Two of them are supported directly.
+
+- `.bin` - the original training data format. Uses 40 bytes per entry. Is supported directly by the `gensfen` and `learn` commands.
+- `.plain` - a human readable training data format. This one is not supported directly by the `gensfen` and `learn` commands. It should not be used for data exchange because it's less compact than other formats. It is mostly useful for inspection of the data.
+- `.binpack` - a compact binary training data format that exploits positions chains to further reduce size. It uses on average between 2 to 3 bytes per entry when generating data with `gensfen`. It is supported directly by `gensfen` and `learn` commands. It is currently the default for the `gensfen` command. A more in depth description can be found [here](docs/binpack.md)
+
+### Conversion between formats.
+
+There is a builting converted that support all 3 formats described above. Any of them can be converted to any other. For more information and usage guide see [here](docs/convert.md).
## Resources
+
+- [Training NNUE for SF](https://docs.google.com/document/d/1os5GH8GGJbV0nKAfXD-qySBclFzKKtXKHbAnA-un8tA/edit) google document with important information and coding priorities
+- [Gensfen data (vondele)](https://drive.google.com/drive/folders/1mftuzYdl9o6tBaceR3d_VBQIrgKJsFpl) over 2b fens available
- [Stockfish NNUE Wiki](https://www.qhapaq.org/shogi/shogiwiki/stockfish-nnue/)
- [Training instructions](https://twitter.com/mktakizawa/status/1273042640280252416) from the creator of the Elmo shogi engine
- [Original Talkchess thread](http://talkchess.com/forum3/viewtopic.php?t=74059) discussing Stockfish NNUE
-- [Guide to Stockfish NNUE](http://yaneuraou.yaneu.com/2020/06/19/stockfish-nnue-the-complete-guide/)
+- [Guide to Stockfish NNUE](http://yaneuraou.yaneu.com/2020/06/19/stockfish-nnue-the-complete-guide/)
- [Unofficial Stockfish Discord](https://discord.gg/nv8gDtt)
A more updated list can be found in the #sf-nnue-resources channel in the Discord.
diff --git a/Top CPU Contributors.txt b/Top CPU Contributors.txt
index 0ea5ac72..482e9000 100644
--- a/Top CPU Contributors.txt
+++ b/Top CPU Contributors.txt
@@ -1,154 +1,173 @@
-Contributors with >10,000 CPU hours as of January 7, 2020
+Contributors with >10,000 CPU hours as of Sept 2, 2020
Thank you!
Username CPU Hours Games played
--------------------------------------------------
-noobpwnftw 9305707 695548021
-mlang 780050 61648867
-dew 621626 43921547
-mibere 524702 42238645
-crunchy 354587 27344275
-cw 354495 27274181
-fastgm 332801 22804359
-JojoM 295750 20437451
-CSU_Dynasty 262015 21828122
-Fisherman 232181 18939229
-ctoks 218866 17622052
-glinscott 201989 13780820
-tvijlbrief 201204 15337115
-velislav 188630 14348485
-gvreuls 187164 15149976
-bking_US 180289 11876016
-nordlandia 172076 13467830
-leszek 157152 11443978
-Thanar 148021 12365359
-spams 141975 10319326
-drabel 138073 11121749
-vdv 137850 9394330
-mgrabiak 133578 10454324
-TueRens 132485 10878471
-bcross 129683 11557084
-marrco 126078 9356740
-sqrt2 125830 9724586
-robal 122873 9593418
-vdbergh 120766 8926915
-malala 115926 8002293
-CoffeeOne 114241 5004100
-dsmith 113189 7570238
-BrunoBanani 104644 7436849
-Data 92328 8220352
-mhoram 89333 6695109
-davar 87924 7009424
-xoto 81094 6869316
-ElbertoOne 80899 7023771
-grandphish2 78067 6160199
-brabos 77212 6186135
-psk 75733 5984901
-BRAVONE 73875 5054681
-sunu 70771 5597972
-sterni1971 70605 5590573
-MaZePallas 66886 5188978
-Vizvezdenec 63708 4967313
-nssy 63462 5259388
-jromang 61634 4940891
-teddybaer 61231 5407666
-Pking_cda 60099 5293873
-solarlight 57469 5028306
-dv8silencer 56913 3883992
-tinker 54936 4086118
-renouve 49732 3501516
-Freja 49543 3733019
-robnjr 46972 4053117
-rap 46563 3219146
-Bobo1239 46036 3817196
-ttruscott 45304 3649765
-racerschmacer 44881 3975413
-finfish 44764 3370515
-eva42 41783 3599691
-biffhero 40263 3111352
-bigpen0r 39817 3291647
-mhunt 38871 2691355
-ronaldjerum 38820 3240695
-Antihistamine 38785 2761312
-pb00067 38038 3086320
-speedycpu 37591 3003273
-rkl 37207 3289580
-VoyagerOne 37050 3441673
-jbwiebe 35320 2805433
-cuistot 34191 2146279
-homyur 33927 2850481
-manap 32873 2327384
-gri 32538 2515779
-oryx 31267 2899051
-EthanOConnor 30959 2090311
-SC 30832 2730764
-csnodgrass 29505 2688994
-jmdana 29458 2205261
-strelock 28219 2067805
-jkiiski 27832 1904470
-Pyafue 27533 1902349
-Garf 27515 2747562
-eastorwest 27421 2317535
-slakovv 26903 2021889
-Prcuvu 24835 2170122
-anst 24714 2190091
-hyperbolic.tom 24319 2017394
-Patrick_G 23687 1801617
-Sharaf_DG 22896 1786697
-nabildanial 22195 1519409
-chriswk 21931 1868317
-achambord 21665 1767323
-Zirie 20887 1472937
-team-oh 20217 1636708
-Isidor 20096 1680691
-ncfish1 19931 1520927
-nesoneg 19875 1463031
-Spprtr 19853 1548165
-JanErik 19849 1703875
-agg177 19478 1395014
-SFTUser 19231 1567999
-xor12 19017 1680165
-sg4032 18431 1641865
-rstoesser 18118 1293588
-MazeOfGalious 17917 1629593
-j3corre 17743 941444
-cisco2015 17725 1690126
-ianh2105 17706 1632562
-dex 17678 1467203
-jundery 17194 1115855
-iisiraider 17019 1101015
-horst.prack 17012 1465656
-Adrian.Schmidt123 16563 1281436
-purplefishies 16342 1092533
-wei 16274 1745989
-ville 16144 1384026
-eudhan 15712 1283717
-OuaisBla 15581 972000
-DragonLord 15559 1162790
-dju 14716 875569
-chris 14479 1487385
-0xB00B1ES 14079 1001120
-OssumOpossum 13776 1007129
-enedene 13460 905279
-bpfliegel 13346 884523
-Ente 13198 1156722
-IgorLeMasson 13087 1147232
-jpulman 13000 870599
-ako027ako 12775 1173203
-Nikolay.IT 12352 1068349
-Andrew Grant 12327 895539
-joster 12008 950160
-AdrianSA 11996 804972
-Nesa92 11455 1111993
-fatmurphy 11345 853210
-Dark_wizzie 11108 1007152
-modolief 10869 896470
-mschmidt 10757 803401
-infinity 10594 727027
-mabichito 10524 749391
-Thomas A. Anderson 10474 732094
-thijsk 10431 719357
-Flopzee 10339 894821
-crocogoat 10104 1013854
-SapphireBrand 10104 969604
-stocky 10017 699440
+noobpwnftw 19352969 1231459677
+mlang 957168 61657446
+dew 949885 56893432
+mibere 703817 46865007
+crunchy 427035 27344275
+cw 416006 27521077
+JojoM 415904 24479564
+fastgm 404873 23953472
+CSU_Dynasty 335774 22850550
+tvijlbrief 335199 21871270
+Fisherman 325053 21786603
+gvreuls 311480 20751516
+ctoks 275877 18710423
+velislav 241267 15596372
+glinscott 217799 13780820
+nordlandia 211692 13484886
+bcross 206213 14934233
+bking_US 198894 11876016
+leszek 189170 11446821
+mgrabiak 183896 11778092
+drabel 181408 12489478
+TueRens 181349 12192000
+Thanar 179852 12365359
+vdv 175171 9881246
+robal 166948 10702862
+spams 157128 10319326
+marrco 149947 9376421
+sqrt2 147963 9724586
+vdbergh 137041 8926915
+CoffeeOne 136294 5004100
+malala 136182 8002293
+mhoram 128934 8177193
+davar 122092 7960001
+dsmith 122059 7570238
+xoto 119696 8222144
+grandphish2 116481 7582197
+Data 113305 8220352
+BrunoBanani 112960 7436849
+ElbertoOne 99028 7023771
+MaZePallas 98571 6362619
+brabos 92118 6186135
+psk 89957 5984901
+sunu 88463 6007033
+sterni1971 86948 5613788
+Vizvezdenec 83752 5343724
+BRAVONE 81239 5054681
+nssy 76497 5259388
+teddybaer 75125 5407666
+Pking_cda 73776 5293873
+jromang 70695 4940891
+solarlight 70517 5028306
+dv8silencer 70287 3883992
+Bobo1239 68515 4652287
+racerschmacer 67468 4935996
+manap 66273 4121774
+tinker 63458 4213726
+linrock 59082 4516053
+robnjr 57262 4053117
+Freja 56938 3733019
+ttruscott 56005 3679485
+renouve 53811 3501516
+cuistot 52532 3014920
+finfish 51360 3370515
+eva42 51272 3599691
+rkl 50759 3840947
+rap 49985 3219146
+pb00067 49727 3298270
+ronaldjerum 47654 3240695
+bigpen0r 47278 3291647
+biffhero 46564 3111352
+VoyagerOne 45386 3445881
+speedycpu 43842 3003273
+jbwiebe 43305 2805433
+Antihistamine 41788 2761312
+mhunt 41735 2691355
+eastorwest 40387 2812173
+homyur 39893 2850481
+gri 39871 2515779
+oryx 38228 2941656
+0x3C33 37773 2529097
+SC 37290 2731014
+csnodgrass 36207 2688994
+jmdana 36108 2205261
+strelock 34716 2074055
+Garf 33800 2747562
+EthanOConnor 33370 2090311
+slakovv 32915 2021889
+Spprtr 32591 2139601
+Prcuvu 30377 2170122
+anst 30301 2190091
+jkiiski 30136 1904470
+hyperbolic.tom 29840 2017394
+Pyafue 29650 1902349
+OuaisBla 27629 1578000
+chriswk 26902 1868317
+achambord 26582 1767323
+Patrick_G 26276 1801617
+yorkman 26193 1992080
+SFTUser 25182 1675689
+nabildanial 24942 1519409
+Sharaf_DG 24765 1786697
+ncfish1 24411 1520927
+agg177 23890 1395014
+JanErik 23408 1703875
+Isidor 23388 1680691
+Norabor 22976 1587862
+cisco2015 22880 1759669
+Zirie 22542 1472937
+team-oh 22272 1636708
+MazeOfGalious 21978 1629593
+sg4032 21945 1643065
+ianh2105 21725 1632562
+xor12 21628 1680365
+dex 21612 1467203
+nesoneg 21494 1463031
+horst.prack 20878 1465656
+0xB00B1ES 20590 1208666
+j3corre 20405 941444
+Adrian.Schmidt123 20316 1281436
+wei 19973 1745989
+rstoesser 19569 1293588
+eudhan 19274 1283717
+Ente 19070 1373058
+jundery 18445 1115855
+iisiraider 18247 1101015
+ville 17883 1384026
+chris 17698 1487385
+purplefishies 17595 1092533
+DragonLord 17014 1162790
+dju 16515 929427
+IgorLeMasson 16064 1147232
+ako027ako 15671 1173203
+Nikolay.IT 15154 1068349
+Andrew Grant 15114 895539
+yurikvelo 15027 1165616
+OssumOpossum 14857 1007129
+enedene 14476 905279
+bpfliegel 14298 884523
+jpulman 13982 870599
+joster 13794 950160
+Nesa92 13786 1114691
+Dark_wizzie 13422 1007152
+Hjax 13350 900887
+Fifis 13313 965473
+mabichito 12903 749391
+thijsk 12886 722107
+crocogoat 12876 1048802
+AdrianSA 12860 804972
+Flopzee 12698 894821
+fatmurphy 12547 853210
+SapphireBrand 12416 969604
+modolief 12386 896470
+scuzzi 12362 833465
+pgontarz 12151 848794
+stocky 11954 699440
+mschmidt 11941 803401
+infinity 11470 727027
+torbjo 11387 728873
+Thomas A. Anderson 11372 732094
+snicolet 11106 869170
+amicic 10779 733593
+rpngn 10712 688203
+d64 10680 771144
+basepi 10637 744851
+jjoshua2 10559 670905
+dzjp 10343 732529
+ols 10259 570669
+lbraesch 10252 647825
diff --git a/appveyor.yml b/appveyor.yml
index a3732a23..ab608409 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -63,7 +63,7 @@ build_script:
- cmake --build . --config %CONFIGURATION% -- /verbosity:minimal
- ps: |
# Download default NNUE net from fishtest
- $nnuenet = Get-Content -Path src\ucioption.cpp | Select-String -CaseSensitive -Pattern "Option" | Select-String -CaseSensitive -Pattern "nn-[a-z0-9]{12}.nnue"
+ $nnuenet = Get-Content -Path src\evaluate.h | Select-String -CaseSensitive -Pattern "EvalFileDefaultName" | Select-String -CaseSensitive -Pattern "nn-[a-z0-9]{12}.nnue"
$dummy = $nnuenet -match "(?nn-[a-z0-9]{12}.nnue)"
$nnuenet = $Matches.nnuenet
Write-Host "Default net:" $nnuenet
diff --git a/docs/binpack.md b/docs/binpack.md
new file mode 100644
index 00000000..1940a5dc
--- /dev/null
+++ b/docs/binpack.md
@@ -0,0 +1,42 @@
+# Binpack
+
+Binpack is a binary training data storage format designed to take advantage of position chains differing by a single move. Therefore it is very good at compactly storing data generated from real games (as opposed to random positions for example sourced from an opening book).
+
+It is currently implemented through a single header library in `extra/nnue_data_binpack_format.h`.
+
+Below follows a rough description of the format in a BNF-like notation.
+
+```
+[[nodiscard]] std::uint16_t signedToUnsigned(std::int16_t a) {
+ std::uint16_t r;
+ std::memcpy(&r, &a, sizeof(std::uint16_t));
+ if (r & 0x8000) r ^= 0x7FFF; // flip value bits if negative
+ r = (r << 1) | (r >> 15); // store sign bit at bit 0
+ return r;
+}
+
+file := *
+block := BINP*
+chain :=
+stem := (32 bytes)
+pos := https://github.com/Sopel97/nnue_data_compress/blob/master/src/chess/Position.h#L1166 (24 bytes)
+move := https://github.com/Sopel97/nnue_data_compress/blob/master/src/chess/Chess.h#L1044 (2 bytes)
+score := signedToUnsigned(score) (2 bytes, big endian)
+ply_and_result := ply bitwise_or (signedToUnsigned(result) << 14) (2 bytes, big endian)
+rule50 := rule_50_counter (2 bytes, big endian)
+ // this is a small defect from old version,
+ I didn't want to break backwards compatibility. Effectively means that there's
+ one byte left for something else in the future because rule50 always fits in one byte.
+
+movetext := *
+count := number of plies in the movetext (2 bytes, big endian). Can be 0.
+move_and_score := (~2 bytes)
+encoded_move := oof this one is complicated to explain.
+ https://github.com/Sopel97/nnue_data_compress/blob/master/src/compress_file.cpp#L827.
+ https://github.com/Sopel97/chess_pos_db/blob/master/docs/bcgn/variable_length.md
+
+encoded_score := https://en.wikipedia.org/wiki/Variable-width_encoding
+ with block size of 4 bits + 1 bit for extension bit.
+ Encoded value is signedToUnsigned(-prev_score - current_score)
+ (scores are always seen from the perspective of side to move in , that's why the '-' before prev_score)
+```
\ No newline at end of file
diff --git a/docs/convert.md b/docs/convert.md
new file mode 100644
index 00000000..132f66e0
--- /dev/null
+++ b/docs/convert.md
@@ -0,0 +1,18 @@
+# Convert
+
+`convert` allows conversion of training data between any of `.plain`, `.bin`, and `.binpack`.
+
+As all commands in stockfish `convert` can be invoked either from command line (as `stockfish.exe convert ...`) or in the interactive prompt.
+
+The syntax of this command is as follows:
+```
+convert from_path to_path [append] [validate]
+```
+
+`from_path` is the path to the file to convert from. The type of the data is deduced based on its extension (one of `.plain`, `.bin`, `.binpack`).
+`to_path` is the path to an output file. The type of the data is deduced from its extension. If the file does not exist it is created.
+
+`append` and `validate` can come in any order and are optional.
+If `append` not specified then the output file will be truncated prior to any writes. If `append` is specified then the converted training data will be appended to the end of the output file.
+
+If `validate` is specified then the conversion will stop on the first illegal move found and a diagnostic will be shown.
\ No newline at end of file
diff --git a/docs/gensfen.md b/docs/gensfen.md
new file mode 100644
index 00000000..48f7f5e7
--- /dev/null
+++ b/docs/gensfen.md
@@ -0,0 +1,67 @@
+# Gensfen
+
+`gensfen` command allows generation of training data from self-play in a manner that suits training better than traditional games. It introduces random moves to diversify openings, and fixed depth evaluation.
+
+As all commands in stockfish `gensfen` can be invoked either from command line (as `stockfish.exe gensfen ...`, but this is not recommended because it's not possible to specify UCI options before `gensfen` executes) or in the interactive prompt.
+
+It is recommended to set the `PruneAtShallowDepth` UCI option to `false` as it will increase the quality of fixed depth searches.
+
+It is recommended to keep the `EnableTranspositionTable` UCI option at the default `true` value as it will make the generation process faster without noticably harming the uniformity of the data.
+
+`gensfen` takes named parameters in the form of `gensfen param_1_name param_1_value param_2_name param_2_value ...`.
+
+Currently the following options are available:
+
+`set_recommended_uci_options` - this is a modifier not a parameter, no value follows it. If specified then some UCI options are set to recommended values.
+
+`depth` - minimum depth of evaluation of each position. Default: 3.
+
+`depth2` - maximum depth of evaluation of each position. If not specified then the same as `depth`.
+
+`nodes` - the number of nodes to use for evaluation of each position. This number is multiplied by the number of PVs of the current search. This does NOT override the `depth` and `depth2` options. If specified then whichever of depth or nodes limit is reached first applies.
+
+`loop` - the number of training data entries to generate. 1 entry == 1 position. Default: 8000000000 (8B).
+
+`output_file_name` - the name of the file to output to. If the extension is not present or doesn't match the selected training data format the right extension will be appened. Default: generated_kifu
+
+`eval_limit` - evaluations with higher absolute value than this will not be written and will terminate a self-play game. Should not exceed 10000 which is VALUE_KNOWN_WIN, but is only hardcapped at mate in 2 (\~30000). Default: 3000
+
+`random_move_minply` - the minimal ply at which a random move may be executed instead of a move chosen by search. Default: 1.
+
+`random_move_maxply` - the maximal ply at which a random move may be executed instead of a move chosen by search. Default: 24.
+
+`random_move_count` - maximum number of random moves in a single self-play game. Default: 5.
+
+`random_move_like_apery` - either 0 or 1. If 1 then random king moves will be followed by a random king move from the opponent whenever possible with 50% probability. Default: 0.
+
+`random_multi_pv` - the number of PVs used for determining the random move. If not specified then a truly random move will be chosen. If specified then a multiPV search will be performed the random move will be one of the moves chosen by the search.
+
+`random_multi_pv_diff` - Makes the multiPV random move selection consider only moves that are at most `random_multi_pv_diff` worse than the next best move. Default: 30000 (all multiPV moves).
+
+`random_multi_pv_depth` - the depth to use for multiPV search for random move. Default: `depth2`.
+
+`write_minply` - minimum ply for which the training data entry will be emitted. Default: 16.
+
+`write_maxply` - maximum ply for which the training data entry will be emitted. Default: 400.
+
+`book` - a path to an opening book to use for the starting positions. Currently only .epd format is supported. If not specified then the starting position is always the standard chess starting position.
+
+`save_every` - the number of training data entries per file. If not specified then there will be always one file. If specified there may be more than one file generated (each having at most `save_every` training data entries) and each file will have a unique number attached.
+
+`random_file_name` - if specified then the output filename will be chosen randomly. Overrides `output_file_name`.
+
+`write_out_draw_game_in_training_data_generation` - either 0 or 1. If 1 then training data from drawn games will be emitted too. Default: 1.
+
+`use_draw_in_training_data_generation` - deprecated, alias for `write_out_draw_game_in_training_data_generation`
+
+`detect_draw_by_consecutive_low_score` - either 0 or 1. If 1 then drawn games will be adjudicated when the score remains 0 for at least 8 plies after ply 80. Default: 1.
+
+`use_game_draw_adjudication` - deprecated, alias for `detect_draw_by_consecutive_low_score`
+
+`detect_draw_by_insufficient_mating_material` - either 0 or 1. If 1 then position with insufficient material will be adjudicated as draws. Default: 1.
+
+`sfen_format` - format of the training data to use. Either `bin` or `binpack`. Default: `binpack`.
+
+`ensure_quiet` - this is a flag option. When specified the positions will be from the qsearch leaf.
+
+`seed` - seed for the PRNG. Can be either a number or a string. If it's a string then its hash will be used. If not specified then the current time will be used.
diff --git a/docs/learn.md b/docs/learn.md
new file mode 100644
index 00000000..30a7c951
--- /dev/null
+++ b/docs/learn.md
@@ -0,0 +1,114 @@
+# Learn
+
+`learn` command allows training a network from training data.
+
+As all commands in stockfish `learn` can be invoked either from command line (as `stockfish.exe learn ...`, but this is not recommended because it's not possible to specify UCI options before `learn` executes) or in the interactive prompt.
+
+`learn` takes named parameters in the form of `learn param_1_name param_1_value param_2_name param_2_value ...`. Unrecognized parameters form a list of paths to training data files.
+
+It is recommended to set the `EnableTranspositionTable` UCI option to `false` to reduce the interference between qsearches which are used to provide shallow evaluation. Using TT may cause the shallow evaluation to diverge from the real evaluation of the net, hiding imperfections.
+
+It is recommended to set the `PruneAtShallowDepth` UCI option to `false` as it will provide more accurate shallow evaluation.
+
+It is **required** to set the `Use NNUE` UCI option to `pure` as otherwise the function being optimized will not always match the function being probed, in which case not much can be learned.
+
+Currently the following options are available:
+
+`set_recommended_uci_options` - this is a modifier not a parameter, no value follows it. If specified then some UCI options are set to recommended values.
+
+`bat` - the size of a batch in multiples of 10000. This determines how many entries are read and shuffled at once during training. Default: 100 (meaning batch size of 1000000).
+
+`targetdir` - path to the direction from which training data will be read. All files in this directory are read sequentially. If not specified then only the list of files from positional arguments will be used. If specified then files from the given directory will be used after the explicitly specified files.
+
+`epochs` - the number of weight update cycles (epochs) to train the network for. One such cycle is `batchsize` positions. If not specified then the training will loop forever.
+
+`basedir` - the base directory for the paths. Default: "" (current directory)
+
+`batchsize` - same as `bat` but doesn't scale by 10000. Default: 1000000
+
+`lr` - initial learning rate. Default: 1.
+
+`use_draw_games_in_training` - either 0 or 1. If 1 then draws will be used in training too. Default: 1.
+
+`use_draw_in_training` - deprecated, alias for `use_draw_games_in_training`
+
+`use_draw_games_in_validation` - either 0 or 1. If 1 then draws will be used in validation too. Default: 1.
+
+`use_draw_in_validation` - deprecated, alias for `use_draw_games_in_validation`
+
+`skip_duplicated_positions_in_training` - either 0 or 1. If 1 then a small hashtable will be used to try to eliminate duplicated position from training. Default: 0.
+
+`use_hash_in_training` - deprecated, alias for `skip_duplicated_positions_in_training`
+
+`winning_probability_coefficient` - some magic value for winning probability. If you need to read this then don't touch it. Default: 1.0 / PawnValueEg / 4.0 * std::log(10.0)
+
+`use_wdl` - either 0 or 1. If 1 then the evaluations will be converted to win/draw/loss percentages prior to learning on them. (Slightly changes the gradient because eval has a different derivative than wdl). Default: 0.
+
+`lambda` - value in range [0..1]. 1 means that only evaluation is used for learning, 0 means that only game result is used. Values inbetween result in interpolation between the two contributions. See `lambda_limit` for when this is applied. Default: 1.0.
+
+`lambda2` - value in range [0..1]. 1 means that only evaluation is used for learning, 0 means that only game result is used. Values inbetween result in interpolation between the two contributions. See `lambda_limit` for when this is applied. Default: 1.0.
+
+`lambda_limit` - the maximum absolute score value for which `lambda` is used as opposed to `lambda2`. For positions with absolute evaluation higher than `lambda_limit` `lambda2` will be used. Default: 32000 (so always `lambda`).
+
+`max_grad` - the maximum allowed loss gradient for backpropagation. Effectively a form of gradient clipping. Useful for the first iterations with a randomly generated net as with higher lr backpropagation often overshoots and kills the net. The default value is fairly conservative, values as low as 0.25 could be used with lr of 1.0 without problems. Default: 1.0.
+
+`reduction_gameply` - the minimum ply after which positions won't be skipped. Positions at plies below this value are skipped with a probability that lessens linearly with the ply (reaching 0 at `reduction_gameply`). Default: 1.
+
+`eval_limit` - positions with absolute evaluation higher than this will be skipped. Default: 32000 (nothing is skipped).
+
+`save_only_once` - this is a modifier not a parameter, no value follows it. If specified then there will be only one network file generated.
+
+`no_shuffle` - this is a modifier not a parameter, no value follows it. If specified then data within a batch won't be shuffled.
+
+`nn_batch_size` - minibatch size used for learning. Should be smaller than batch size. Default: 1000.
+
+`newbob_decay` - learning rate will be multiplied by this factor every time a net is rejected (so in other words it controls LR drops). Default: 0.5 (no LR drops)
+
+`assume_quiet` - this is a flag option. When specified learn will not perform qsearch to reach a quiet position.
+
+`smart_fen_skipping` - this is a flag option. When specified some position that are not good candidates for teaching are skipped. This includes positions where the best move is a capture or promotion, and position where a king is in check.
+
+`newbob_num_trials` - determines after how many subsequent rejected nets the training process will be terminated. Default: 4.
+
+`auto_lr_drop` - every time this many positions are processed the learning rate is multiplied by `newbob_decay`. In other words this value specifies for how many positions a single learning rate stage lasts. If 0 then doesn't have any effect. Default: 0.
+
+`nn_options` - if you're reading this you don't use it. It passes messages directly to the network evaluation. I don't know what it can do either.
+
+`eval_save_interval` - every `eval_save_interval` positions the network will be saved and either accepted or rejected (in which case an LR drop follows). Default: 100000000 (100M). (generally people use values in 10M-100M range)
+
+`loss_output_interval` - every `loss_output_interval` fitness statistics are displayed. Default: 1000000 (1M)
+
+`validation_set_file_name` - path to the file with training data to be used for validation (loss computation and move accuracy)
+
+`sfen_read_size` - the number of sfens to always keep in the buffer. Default: 10000000 (10M)
+
+`thread_buffer_size` - the number of sfens to copy at once to each thread requesting more sfens for learning. Default: 10000
+
+`seed` - seed for the PRNG. Can be either a number or a string. If it's a string then its hash will be used. If not specified then the current time will be used.
+
+`verbose` - this is a modifier, not a parameter. When used there will be more detailed output during training.
+
+## Legacy subcommands and parameters
+
+### Convert
+
+`convert_plain`
+`convert_bin`
+`interpolate_eval`
+`check_invalid_fen`
+`check_illegal_move`
+`convert_bin_from_pgn-extract`
+`pgn_eval_side_to_move`
+`convert_no_eval_fens_as_score_zero`
+`src_score_min_value`
+`src_score_max_value`
+`dest_score_min_value`
+`dest_score_max_value`
+
+### Shuffle
+
+`shuffle`
+`buffer_size`
+`shuffleq`
+`shufflem`
+`output_file_name`
diff --git a/docs/transform.md b/docs/transform.md
new file mode 100644
index 00000000..82e963fe
--- /dev/null
+++ b/docs/transform.md
@@ -0,0 +1,21 @@
+# Transform
+
+`transform` command exposes subcommands that perform some specific transformation over data. The call syntax is `transform `. Currently implemented subcommands are listed and described below.
+
+## `nudged_static`
+
+`transform nudged_static` takes named parameters in the form of `gensfen param_1_name param_1_value param_2_name param_2_value ...` and flag parameters which don't require values.
+
+This command goes through positions in the input files and replaces the scores with new ones - generated from static eval - but slightly adjusted based on the scores in the original input file.
+
+Currently the following options are available:
+
+`input_file` - path to the input file. Supports bin and binpack formats. Default: in.binpack.
+
+`output_file` - path to the output file. Supports bin and binpack formats. Default: out.binpack.
+
+`absolute` - states that the adjustment should be bounded by an absolute value. After this token follows the maximum absolute adjustment. Values are always adjusted towards scores in the input file. This is the default mode. Default maximum adjustement: 5.
+
+`relative` - states that the adjustment should be bounded by a value relative in magnitude to the static eval value. After this token follows the maximum relative change - a floating point value greater than 0. For example a value of 0.1 only allows changing the static eval by at most 10% towards the score from the input file.
+
+`interpolate` states that the output score should be a value interpolated between static eval and the score from the input file. After this token follows the interpolation constant `t`. `t` of 0 means that only static eval is used. `t` of 1 means that only score from the input file is used. `t` of 0.5 means that the static eval and input score are averaged. It accepts values outside of range `<0, 1>`, but the usefulness is questionable.
diff --git a/script/extract_bin.py b/script/extract_bin.py
new file mode 100644
index 00000000..9574aa17
--- /dev/null
+++ b/script/extract_bin.py
@@ -0,0 +1,42 @@
+import sys
+
+ENTRY_SIZE = 40
+NUM_ENTRIES_IN_CHUNK = 1024*1024
+
+def copy(infile, outfile, count, times):
+ if times > 1:
+ outfile.write(infile.read(count*ENTRY_SIZE)*times)
+ else:
+ offset = 0
+ while offset < count:
+ to_read = NUM_ENTRIES_IN_CHUNK if offset + NUM_ENTRIES_IN_CHUNK <= count else count - offset
+
+ outfile.write(infile.read(to_read*ENTRY_SIZE))
+
+ offset += NUM_ENTRIES_IN_CHUNK
+
+def work():
+ filename = sys.argv[1]
+ offset = int(sys.argv[2])
+ count = int(sys.argv[3])
+ times = int(sys.argv[4]) if len(sys.argv) >= 5 else 1
+
+ with open(filename, 'rb') as infile:
+ infile.seek(offset * ENTRY_SIZE)
+ filename_parts = filename.split('.')
+ out_path = '.'.join(filename_parts[:-1]) + '_' + str(offset) + '_' + str(count) + '_' + str(times) + '.' + filename_parts[-1]
+ with open(out_path, 'wb') as outfile:
+ copy(infile, outfile, count, times)
+
+def show_help():
+ print('Usage: python extract_bin.py filename offset count [times]')
+ print('filename - the path to the .bin file to process')
+ print('offset - the number of sfens to skip')
+ print('count - the number of sfens to extract')
+ print('times - the number of times to repeat the extracted sfens. Default = 1')
+ print('The result is saved in a new file named `filename.stem`_`offset`_`count`_`times`.bin')
+
+if len(sys.argv) < 4:
+ show_help()
+else:
+ work()
diff --git a/script/shuffle_binpack.py b/script/shuffle_binpack.py
new file mode 100644
index 00000000..409d4907
--- /dev/null
+++ b/script/shuffle_binpack.py
@@ -0,0 +1,69 @@
+import struct
+import sys
+import os
+import random
+from pathlib import Path
+
+def index_binpack(file):
+ print('Indexing...')
+ index = []
+ offset = 0
+ report_every = 100
+ prev_mib = -report_every
+ while file.peek():
+ chunk_header = file.read(8)
+ assert chunk_header[0:4] == b'BINP'
+ size = struct.unpack(' /dev/null
+ $(PGOGENSFEN) > /dev/null
@echo ""
@echo "Step 3/4. Building optimized executable ..."
$(MAKE) ARCH=$(ARCH) COMP=$(COMP) objclean
@@ -745,12 +778,13 @@ install:
-cp $(EXE) $(BINDIR)
-strip $(BINDIR)/$(EXE)
-#clean all
+# clean all
clean: objclean profileclean
@rm -f .depend *~ core
+# evaluation network (nnue)
net:
- $(eval nnuenet := $(shell grep EvalFile ucioption.cpp | grep Option | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/'))
+ $(eval nnuenet := $(shell grep EvalFileDefaultName evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/'))
@echo "Default net: $(nnuenet)"
$(eval nnuedownloadurl := https://tests.stockfishchess.org/api/nn/$(nnuenet))
$(eval curl_or_wget := $(shell if hash curl 2>/dev/null; then echo "curl -skL"; elif hash wget 2>/dev/null; then echo "wget -qO-"; fi))
@@ -772,7 +806,6 @@ net:
echo "shasum / sha256sum not found, skipping net validation"; \
fi
-
# clean binaries and objects
objclean:
@rm -f $(EXE) *.o ./syzygy/*.o ./nnue/*.o ./nnue/features/*.o ./learn/*.o ./extra/*.o ./eval/*.o
@@ -782,6 +815,7 @@ profileclean:
@rm -rf profdir
@rm -f bench.txt *.gcda *.gcno ./syzygy/*.gcda ./nnue/*.gcda ./nnue/features/*.gcda *.s ./learn/*.gcda ./extra/*.gcda ./eval/*.gcda
@rm -f stockfish.profdata *.profraw
+ @rm -f $(PGO_TRAINING_DATA_FILE)
default:
help
@@ -792,7 +826,7 @@ default:
all: $(EXE) .depend
-config-sanity:
+config-sanity: net
@echo ""
@echo "Config:"
@echo "debug: '$(debug)'"
@@ -913,6 +947,6 @@ profile-learn: config-sanity objclean profileclean
rm generated_kifu.bin
.depend:
- -@$(CXX) $(DEPENDFLAGS) -MM $(SRCS) > $@ 2> /dev/null
+ -@$(CXX) $(DEPENDFLAGS) -MM $(SRCS) > $@
-include .depend
diff --git a/src/benchmark.cpp b/src/benchmark.cpp
index 806e9840..ffb631a2 100644
--- a/src/benchmark.cpp
+++ b/src/benchmark.cpp
@@ -164,5 +164,7 @@ vector setup_bench(const Position& current, istream& is) {
++posCounter;
}
+ list.emplace_back("setoption name Use NNUE value true");
+
return list;
}
diff --git a/src/eval/evaluate_common.h b/src/eval/evaluate_common.h
deleted file mode 100644
index b043f2e1..00000000
--- a/src/eval/evaluate_common.h
+++ /dev/null
@@ -1,82 +0,0 @@
-#ifndef _EVALUATE_COMMON_H_
-#define _EVALUATE_COMMON_H_
-
-// A common header-like function for modern evaluation functions (EVAL_KPPT and EVAL_KPP_KKPT).
-
-#if defined(EVAL_NNUE) || defined(EVAL_LEARN)
-#include
-
-// KK file name
-#define KK_BIN "KK_synthesized.bin"
-
-// KKP file name
-#define KKP_BIN "KKP_synthesized.bin"
-
-// KPP file name
-#define KPP_BIN "KPP_synthesized.bin"
-
-namespace Eval
-{
-
-#if defined(USE_EVAL_HASH)
- // prefetch function
- void prefetch_evalhash(const Key key);
-#endif
-
- // An operator that applies the function f to each parameter of the evaluation function.
- // Used for parameter analysis etc.
- // type indicates the survey target.
- // type = -1 :KK,KKP,KPP all
- // type = 0: KK only
- // type = 1: KKP only
- // type = 2: KPP only
- void foreach_eval_param(std::functionf, int type = -1);
-
- // --------------------------
- // for learning
- // --------------------------
-
-#if defined(EVAL_LEARN)
- // Initialize the gradient array during learning
- // Pass the learning rate as an argument. If 0.0, the default value is used.
- // The epoch of update_weights() gradually changes from eta to eta2 until eta_epoch.
- // After eta2_epoch, gradually change from eta2 to eta3.
- void init_grad(double eta1, uint64_t eta_epoch, double eta2, uint64_t eta2_epoch, double eta3);
-
- // Add the gradient difference value to the gradient array for all features that appear in the current phase.
- // freeze[0]: Flag that kk does not learn
- // freeze[1]: Flag that kkp does not learn
- // freeze[2]: Flag that kpp does not learn
- // freeze[3]: Flag that kppp does not learn
- void add_grad(Position& pos, Color rootColor, double delt_grad, const std::array& freeze);
-
- // Do SGD or AdaGrad or something based on the current gradient.
- // epoch: Generation counter (starting from 0)
- // freeze[0]: Flag that kk does not learn
- // freeze[1]: Flag that kkp does not learn
- // freeze[2]: Flag that kpp does not learn
- // freeze[3]: Flag that kppp does not learn
- void update_weights(uint64_t epoch, const std::array& freeze);
-
- // Save the evaluation function parameters to a file.
- // You can specify the extension added to the end of the file.
- void save_eval(std::string suffix);
-
- // Get the current eta.
- double get_eta();
-
- // --learning related commands
-
- // A function that normalizes KK. Note that it is not completely equivalent to the original evaluation function.
- // By making the values of kkp and kpp as close to zero as possible, the value of the feature factor (which is zero) that did not appear during learning
- // The idea of ensuring it is valid.
- void regularize_kk();
-
-#endif
-
-
-}
-
-#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN)
-
-#endif // _EVALUATE_KPPT_COMMON_H_
diff --git a/src/evaluate.cpp b/src/evaluate.cpp
index 8edc9bb8..dd204a52 100644
--- a/src/evaluate.cpp
+++ b/src/evaluate.cpp
@@ -20,61 +20,25 @@
#include
#include
#include // For std::memset
+#include
#include
#include
#include
-#include
+#include
+#include
+
+#include "nnue/evaluate_nnue.h"
#include "bitboard.h"
#include "evaluate.h"
#include "material.h"
+#include "misc.h"
#include "pawns.h"
#include "thread.h"
#include "uci.h"
+#include "incbin/incbin.h"
-#ifdef EVAL_LEARN
-namespace Learner
-{
- extern bool use_raw_nnue_eval;
-}
-#endif
-
-namespace Eval {
-
- bool useNNUE;
- std::string eval_file_loaded="None";
-
- void init_NNUE() {
-
- useNNUE = Options["Use NNUE"];
- std::string eval_file = std::string(Options["EvalFile"]);
- if (useNNUE && eval_file_loaded != eval_file)
- if (Eval::NNUE::load_eval_file(eval_file))
- eval_file_loaded = eval_file;
- }
-
- void verify_NNUE() {
-
- std::string eval_file = std::string(Options["EvalFile"]);
- if (useNNUE && eval_file_loaded != eval_file)
- {
- UCI::OptionsMap defaults;
- UCI::init(defaults);
-
- sync_cout << "info string ERROR: NNUE evaluation used, but the network file " << eval_file << " was not loaded successfully." << sync_endl;
- sync_cout << "info string ERROR: The UCI option EvalFile might need to specify the full path, including the directory/folder name, to the file." << sync_endl;
- sync_cout << "info string ERROR: The default net can be downloaded from: https://tests.stockfishchess.org/api/nn/"+std::string(defaults["EvalFile"]) << sync_endl;
- sync_cout << "info string ERROR: If the UCI option Use NNUE is set to true, network evaluation parameters compatible with the program must be available." << sync_endl;
- sync_cout << "info string ERROR: The engine will be terminated now." << sync_endl;
- std::exit(EXIT_FAILURE);
- }
-
- if (useNNUE)
- sync_cout << "info string NNUE evaluation using " << eval_file << " enabled." << sync_endl;
- else
- sync_cout << "info string classical evaluation enabled." << sync_endl;
- }
-}
+using namespace std;
namespace Trace {
@@ -120,11 +84,11 @@ using namespace Trace;
namespace {
// Threshold for lazy and space evaluation
- constexpr Value LazyThreshold1 = Value(1400);
- constexpr Value LazyThreshold2 = Value(1300);
- constexpr Value SpaceThreshold = Value(12222);
- constexpr Value NNUEThreshold1 = Value(550);
- constexpr Value NNUEThreshold2 = Value(150);
+ constexpr Value LazyThreshold1 = Value(1565);
+ constexpr Value LazyThreshold2 = Value(1102);
+ constexpr Value SpaceThreshold = Value(11551);
+ constexpr Value NNUEThreshold1 = Value(682);
+ constexpr Value NNUEThreshold2 = Value(176);
// KingAttackWeights[PieceType] contains king attack weights by piece type
constexpr int KingAttackWeights[PIECE_TYPE_NB] = { 0, 0, 81, 52, 44, 10 };
@@ -132,7 +96,7 @@ namespace {
// SafeCheck[PieceType][single/multiple] contains safe check bonus by piece type,
// higher if multiple safe checks are possible for that piece type.
constexpr int SafeCheck[][2] = {
- {}, {}, {792, 1283}, {645, 967}, {1084, 1897}, {772, 1119}
+ {}, {}, {803, 1292}, {639, 974}, {1087, 1878}, {759, 1132}
};
#define S(mg, eg) make_score(mg, eg)
@@ -140,19 +104,25 @@ namespace {
// MobilityBonus[PieceType-2][attacked] contains bonuses for middle and end game,
// indexed by piece type and number of attacked squares in the mobility area.
constexpr Score MobilityBonus[][32] = {
- { S(-62,-81), S(-53,-56), S(-12,-31), S( -4,-16), S( 3, 5), S( 13, 11), // Knight
- S( 22, 17), S( 28, 20), S( 33, 25) },
- { S(-48,-59), S(-20,-23), S( 16, -3), S( 26, 13), S( 38, 24), S( 51, 42), // Bishop
- S( 55, 54), S( 63, 57), S( 63, 65), S( 68, 73), S( 81, 78), S( 81, 86),
- S( 91, 88), S( 98, 97) },
- { S(-60,-78), S(-20,-17), S( 2, 23), S( 3, 39), S( 3, 70), S( 11, 99), // Rook
- S( 22,103), S( 31,121), S( 40,134), S( 40,139), S( 41,158), S( 48,164),
- S( 57,168), S( 57,169), S( 62,172) },
- { S(-30,-48), S(-12,-30), S( -8, -7), S( -9, 19), S( 20, 40), S( 23, 55), // Queen
- S( 23, 59), S( 35, 75), S( 38, 78), S( 53, 96), S( 64, 96), S( 65,100),
- S( 65,121), S( 66,127), S( 67,131), S( 67,133), S( 72,136), S( 72,141),
- S( 77,147), S( 79,150), S( 93,151), S(108,168), S(108,168), S(108,171),
- S(110,182), S(114,182), S(114,192), S(116,219) }
+ { S(-62,-79), S(-53,-57), S(-12,-31), S( -3,-17), S( 3, 7), S( 12, 13), // Knight
+ S( 21, 16), S( 28, 21), S( 37, 26) },
+ { S(-47,-59), S(-20,-25), S( 14, -8), S( 29, 12), S( 39, 21), S( 53, 40), // Bishop
+ S( 53, 56), S( 60, 58), S( 62, 65), S( 69, 72), S( 78, 78), S( 83, 87),
+ S( 91, 88), S( 96, 98) },
+ { S(-60,-82), S(-24,-15), S( 0, 17) ,S( 3, 43), S( 4, 72), S( 14,100), // Rook
+ S( 20,102), S( 30,122), S( 41,133), S(41 ,139), S( 41,153), S( 45,160),
+ S( 57,165), S( 58,170), S( 67,175) },
+ { S(-29,-49), S(-16,-29), S( -8, -8), S( -8, 17), S( 18, 39), S( 25, 54), // Queen
+ S( 23, 59), S( 37, 73), S( 41, 76), S( 54, 95), S( 65, 95) ,S( 68,101),
+ S( 69,124), S( 70,128), S( 70,132), S( 70,133) ,S( 71,136), S( 72,140),
+ S( 74,147), S( 76,149), S( 90,153), S(104,169), S(105,171), S(106,171),
+ S(112,178), S(114,185), S(114,187), S(119,221) }
+ };
+
+ // BishopPawns[distance from edge] contains a file-dependent penalty for pawns on
+ // squares of the same color as our bishop.
+ constexpr Score BishopPawns[int(FILE_NB) / 2] = {
+ S(3, 8), S(3, 9), S(1, 8), S(3, 7)
};
// KingProtector[knight/bishop] contains penalty for each distance unit to own king
@@ -160,32 +130,31 @@ namespace {
// Outpost[knight/bishop] contains bonuses for each knight or bishop occupying a
// pawn protected square on rank 4 to 6 which is also safe from a pawn attack.
- constexpr Score Outpost[] = { S(56, 36), S(30, 23) };
+ constexpr Score Outpost[] = { S(56, 34), S(31, 23) };
// PassedRank[Rank] contains a bonus according to the rank of a passed pawn
constexpr Score PassedRank[RANK_NB] = {
- S(0, 0), S(10, 28), S(17, 33), S(15, 41), S(62, 72), S(168, 177), S(276, 260)
+ S(0, 0), S(9, 28), S(15, 31), S(17, 39), S(64, 70), S(171, 177), S(277, 260)
};
// RookOnFile[semiopen/open] contains bonuses for each rook when there is
// no (friendly) pawn on the rook file.
- constexpr Score RookOnFile[] = { S(19, 7), S(48, 29) };
+ constexpr Score RookOnFile[] = { S(19, 7), S(48, 27) };
// ThreatByMinor/ByRook[attacked PieceType] contains bonuses according to
// which piece type attacks which one. Attacks on lesser pieces which are
// pawn-defended are not considered.
constexpr Score ThreatByMinor[PIECE_TYPE_NB] = {
- S(0, 0), S(5, 32), S(57, 41), S(77, 56), S(88, 119), S(79, 161)
+ S(0, 0), S(5, 32), S(55, 41), S(77, 56), S(89, 119), S(79, 162)
};
constexpr Score ThreatByRook[PIECE_TYPE_NB] = {
- S(0, 0), S(3, 46), S(37, 68), S(42, 60), S(0, 38), S(58, 41)
+ S(0, 0), S(3, 44), S(37, 68), S(42, 60), S(0, 39), S(58, 43)
};
// Assorted bonuses and penalties
constexpr Score BadOutpost = S( -7, 36);
constexpr Score BishopOnKingRing = S( 24, 0);
- constexpr Score BishopPawns = S( 3, 7);
constexpr Score BishopXRayPawns = S( 4, 5);
constexpr Score CorneredBishop = S( 50, 50);
constexpr Score FlankAttacks = S( 8, 0);
@@ -198,7 +167,6 @@ namespace {
constexpr Score ReachableOutpost = S( 31, 22);
constexpr Score RestrictedPiece = S( 7, 7);
constexpr Score RookOnKingRing = S( 16, 0);
- constexpr Score RookOnQueenFile = S( 6, 11);
constexpr Score SliderOnQueen = S( 60, 18);
constexpr Score ThreatByKing = S( 24, 89);
constexpr Score ThreatByPawnPush = S( 48, 39);
@@ -387,7 +355,7 @@ namespace {
// when the bishop is outside the pawn chain.
Bitboard blocked = pos.pieces(Us, PAWN) & shift(pos.pieces());
- score -= BishopPawns * pos.pawns_on_same_color_squares(Us, s)
+ score -= BishopPawns[edge_distance(file_of(s))] * pos.pawns_on_same_color_squares(Us, s)
* (!(attackedBy[Us][PAWN] & s) + popcount(blocked & CenterFiles));
// Penalty for all enemy pawns x-rayed
@@ -414,10 +382,6 @@ namespace {
if (Pt == ROOK)
{
- // Bonus for rook on the same file as a queen
- if (file_bb(s) & pos.pieces(QUEEN))
- score += RookOnQueenFile;
-
// Bonus for rook on an open or semi-open file
if (pos.is_on_semiopen_file(Us, s))
score += RookOnFile[pos.is_on_semiopen_file(Them, s)];
@@ -515,18 +479,18 @@ namespace {
int kingFlankAttack = popcount(b1) + popcount(b2);
int kingFlankDefense = popcount(b3);
- kingDanger += kingAttackersCount[Them] * kingAttackersWeight[Them]
- + 185 * popcount(kingRing[Us] & weak)
- + 148 * popcount(unsafeChecks)
- + 98 * popcount(pos.blockers_for_king(Us))
- + 69 * kingAttacksCount[Them]
- + 3 * kingFlankAttack * kingFlankAttack / 8
- + mg_value(mobility[Them] - mobility[Us])
- - 873 * !pos.count(Them)
- - 100 * bool(attackedBy[Us][KNIGHT] & attackedBy[Us][KING])
- - 6 * mg_value(score) / 8
- - 4 * kingFlankDefense
- + 37;
+ kingDanger += kingAttackersCount[Them] * kingAttackersWeight[Them] // (~10 Elo)
+ + 185 * popcount(kingRing[Us] & weak) // (~15 Elo)
+ + 148 * popcount(unsafeChecks) // (~4 Elo)
+ + 98 * popcount(pos.blockers_for_king(Us)) // (~2 Elo)
+ + 69 * kingAttacksCount[Them] // (~0.5 Elo)
+ + 3 * kingFlankAttack * kingFlankAttack / 8 // (~0.5 Elo)
+ + mg_value(mobility[Them] - mobility[Us]) // (~0.5 Elo)
+ - 873 * !pos.count(Them) // (~24 Elo)
+ - 100 * bool(attackedBy[Us][KNIGHT] & attackedBy[Us][KING]) // (~5 Elo)
+ - 6 * mg_value(score) / 8 // (~8 Elo)
+ - 4 * kingFlankDefense // (~5 Elo)
+ + 37; // (~0.5 Elo)
// Transform the kingDanger units into a Score, and subtract it from the evaluation
if (kingDanger > 100)
@@ -843,7 +807,9 @@ namespace {
sf = 37 + 3 * (pos.count(WHITE) == 1 ? pos.count(BLACK) + pos.count(BLACK)
: pos.count(WHITE) + pos.count(WHITE));
else
- sf = std::min(sf, 36 + 7 * pos.count(strongSide));
+ sf = std::min(sf, 36 + 7 * pos.count(strongSide)) - 4 * !pawnsOnBothFlanks;
+
+ sf -= 4 * !pawnsOnBothFlanks;
}
// Interpolate between the middlegame and (scaled by 'sf') endgame score
@@ -947,19 +913,47 @@ make_v:
/// evaluation of the position from the point of view of the side to move.
Value Eval::evaluate(const Position& pos) {
-#ifdef EVAL_LEARN
- if (Learner::use_raw_nnue_eval) {
- return NNUE::evaluate(pos);
+
+ Value v;
+
+ if (NNUE::useNNUE == NNUE::UseNNUEMode::Pure) {
+ v = NNUE::evaluate(pos);
+
+ // Guarantee evaluation does not hit the tablebase range
+ v = std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1);
+
+ return v;
}
-#endif
+ else if (NNUE::useNNUE == NNUE::UseNNUEMode::False)
+ v = Evaluation(pos).value();
+ else
+ {
+ // Scale and shift NNUE for compatibility with search and classical evaluation
+ auto adjusted_NNUE = [&](){
+ int mat = pos.non_pawn_material() + PawnValueMg * pos.count();
+ return NNUE::evaluate(pos) * (720 + mat / 32) / 1024 + Tempo;
+ };
- bool classical = !Eval::useNNUE
- || abs(eg_value(pos.psq_score())) * 16 > NNUEThreshold1 * (16 + pos.rule50_count());
- Value v = classical ? Evaluation(pos).value()
- : NNUE::evaluate(pos) * 5 / 4 + Tempo;
+ // If there is PSQ imbalance use classical eval, with small probability if it is small
+ Value psq = Value(abs(eg_value(pos.psq_score())));
+ int r50 = 16 + pos.rule50_count();
+ bool largePsq = psq * 16 > (NNUEThreshold1 + pos.non_pawn_material() / 64) * r50;
+ bool classical = largePsq || (psq > PawnValueMg / 4 && !(pos.this_thread()->nodes & 0xB));
- if (classical && Eval::useNNUE && abs(v) * 16 < NNUEThreshold2 * (16 + pos.rule50_count()))
- v = NNUE::evaluate(pos) * 5 / 4 + Tempo;
+ bool strongClassical = pos.non_pawn_material() < 2 * RookValueMg && pos.count() < 2;
+
+ v = classical || strongClassical ? Evaluation(pos).value() : adjusted_NNUE();
+
+ // If the classical eval is small and imbalance large, use NNUE nevertheless.
+ // For the case of opposite colored bishops, switch to NNUE eval with
+ // small probability if the classical eval is less than the threshold.
+ if ( largePsq && !strongClassical
+ && ( abs(v) * 16 < NNUEThreshold2 * r50
+ || ( pos.opposite_bishops()
+ && abs(v) * 16 < (NNUEThreshold1 + pos.non_pawn_material() / 64) * r50
+ && !(pos.this_thread()->nodes & 0xB))))
+ v = adjusted_NNUE();
+ }
// Damp down the evaluation linearly when shuffling
v = v * (100 - pos.rule50_count()) / 100;
@@ -1015,7 +1009,7 @@ std::string Eval::trace(const Position& pos) {
ss << "\nClassical evaluation: " << to_cp(v) << " (white side)\n";
- if (Eval::useNNUE)
+ if (NNUE::useNNUE != NNUE::UseNNUEMode::False)
{
v = NNUE::evaluate(pos);
v = pos.side_to_move() == WHITE ? v : -v;
diff --git a/src/evaluate.h b/src/evaluate.h
index e808068d..f5d3efa7 100644
--- a/src/evaluate.h
+++ b/src/evaluate.h
@@ -26,23 +26,13 @@
class Position;
namespace Eval {
-
std::string trace(const Position& pos);
Value evaluate(const Position& pos);
- extern bool useNNUE;
- extern std::string eval_file_loaded;
- void init_NNUE();
- void verify_NNUE();
-
- namespace NNUE {
-
- Value evaluate(const Position& pos);
- Value compute_eval(const Position& pos);
- void update_eval(const Position& pos);
- bool load_eval_file(const std::string& evalFile);
-
- } // namespace NNUE
+ // The default net name MUST follow the format nn-[SHA256 first 12 digits].nnue
+ // for the build process (profile-build and fishtest) to work. Do not change the
+ // name of the macro, as it is used in the Makefile.
+ #define EvalFileDefaultName "nn-c3ca321c51c9.nnue"
} // namespace Eval
diff --git a/src/extra/nnue_data_binpack_format.h b/src/extra/nnue_data_binpack_format.h
new file mode 100644
index 00000000..31c6f7bb
--- /dev/null
+++ b/src/extra/nnue_data_binpack_format.h
@@ -0,0 +1,7842 @@
+/*
+
+Copyright 2020 Tomasz Sobczyk
+
+Permission is hereby granted, free of charge,
+to any person obtaining a copy of this software
+and associated documentation files (the "Software"),
+to deal in the Software without restriction,
+including without limitation the rights to use, copy,
+modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall
+be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#if (defined(_MSC_VER) || defined(__INTEL_COMPILER)) && !defined(__clang__)
+#include
+#endif
+
+namespace chess
+{
+ #if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__)
+
+ #define FORCEINLINE __attribute__((always_inline))
+
+ #elif defined(_MSC_VER)
+
+ // NOTE: for some reason it breaks the profiler a little
+ // keep it on only when not profiling.
+ //#define FORCEINLINE __forceinline
+ #define FORCEINLINE
+
+ #else
+
+ #define FORCEINLINE inline
+
+ #endif
+
+ #if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__)
+
+ #define NOINLINE __attribute__((noinline))
+
+ #elif defined(_MSC_VER)
+
+ #define NOINLINE __declspec(noinline)
+
+ #else
+
+ #define NOINLINE
+
+ #endif
+
+ namespace intrin
+ {
+ [[nodiscard]] constexpr int popcount_constexpr(std::uint64_t value)
+ {
+ int r = 0;
+ while (value)
+ {
+ value &= value - 1;
+ ++r;
+ }
+ return r;
+ }
+
+ [[nodiscard]] constexpr int lsb_constexpr(std::uint64_t value)
+ {
+ int c = 0;
+ value &= ~value + 1; // leave only the lsb
+ if ((value & 0x00000000FFFFFFFFull) == 0) c += 32;
+ if ((value & 0x0000FFFF0000FFFFull) == 0) c += 16;
+ if ((value & 0x00FF00FF00FF00FFull) == 0) c += 8;
+ if ((value & 0x0F0F0F0F0F0F0F0Full) == 0) c += 4;
+ if ((value & 0x3333333333333333ull) == 0) c += 2;
+ if ((value & 0x5555555555555555ull) == 0) c += 1;
+ return c;
+ }
+
+ [[nodiscard]] constexpr int msb_constexpr(std::uint64_t value)
+ {
+ int c = 63;
+ if ((value & 0xFFFFFFFF00000000ull) == 0) { c -= 32; value <<= 32; }
+ if ((value & 0xFFFF000000000000ull) == 0) { c -= 16; value <<= 16; }
+ if ((value & 0xFF00000000000000ull) == 0) { c -= 8; value <<= 8; }
+ if ((value & 0xF000000000000000ull) == 0) { c -= 4; value <<= 4; }
+ if ((value & 0xC000000000000000ull) == 0) { c -= 2; value <<= 2; }
+ if ((value & 0x8000000000000000ull) == 0) { c -= 1; }
+ return c;
+ }
+ }
+
+ namespace intrin
+ {
+ [[nodiscard]] inline int popcount(std::uint64_t b)
+ {
+ #if (defined(_MSC_VER) || defined(__INTEL_COMPILER)) && !defined(__clang__)
+
+ return static_cast(_mm_popcnt_u64(b));
+
+ #else
+
+ return static_cast(__builtin_popcountll(b));
+
+ #endif
+ }
+
+ #if defined(_MSC_VER) && !defined(__clang__)
+
+ [[nodiscard]] inline int lsb(std::uint64_t value)
+ {
+ assert(value != 0);
+
+ unsigned long idx;
+ _BitScanForward64(&idx, value);
+ return static_cast(idx);
+ }
+
+ [[nodiscard]] inline int msb(std::uint64_t value)
+ {
+ assert(value != 0);
+
+ unsigned long idx;
+ _BitScanReverse64(&idx, value);
+ return static_cast(idx);
+ }
+
+ #else
+
+ [[nodiscard]] inline int lsb(std::uint64_t value)
+ {
+ assert(value != 0);
+
+ return __builtin_ctzll(value);
+ }
+
+ [[nodiscard]] inline int msb(std::uint64_t value)
+ {
+ assert(value != 0);
+
+ return 63 ^ __builtin_clzll(value);
+ }
+
+ #endif
+ }
+
+ template
+ [[nodiscard]] constexpr IntT floorLog2(IntT value)
+ {
+ return intrin::msb_constexpr(value);
+ }
+
+ template
+ constexpr auto computeMasks()
+ {
+ static_assert(std::is_unsigned_v);
+
+ constexpr std::size_t numBits = sizeof(IntT) * CHAR_BIT;
+ std::array nbitmasks{};
+
+ for (std::size_t i = 0; i < numBits; ++i)
+ {
+ nbitmasks[i] = (static_cast(1u) << i) - 1u;
+ }
+ nbitmasks[numBits] = ~static_cast(0u);
+
+ return nbitmasks;
+ }
+
+ template
+ constexpr auto nbitmask = computeMasks();
+
+ template >
+ inline ToT signExtend(FromT value)
+ {
+ static_assert(std::is_signed_v);
+ static_assert(std::is_unsigned_v);
+ static_assert(sizeof(ToT) == sizeof(FromT));
+
+ constexpr std::size_t totalBits = sizeof(FromT) * CHAR_BIT;
+
+ static_assert(N > 0 && N <= totalBits);
+
+ constexpr std::size_t unusedBits = totalBits - N;
+ if constexpr (ToT(~FromT(0)) >> 1 == ToT(~FromT(0)))
+ {
+ return ToT(value << unusedBits) >> ToT(unusedBits);
+ }
+ else
+ {
+ constexpr FromT mask = (~FromT(0)) >> unusedBits;
+ value &= mask;
+ if (value & (FromT(1) << (N - 1)))
+ {
+ value |= ~mask;
+ }
+ return static_cast(value);
+ }
+ }
+
+ namespace lookup
+ {
+ constexpr int nthSetBitIndexNaive(std::uint64_t value, int n)
+ {
+ for (int i = 0; i < n; ++i)
+ {
+ value &= value - 1;
+ }
+ return intrin::lsb_constexpr(value);
+ }
+
+ constexpr std::array, 256> nthSetBitIndex = []()
+ {
+ std::array, 256> t{};
+
+ for (int i = 0; i < 256; ++i)
+ {
+ for (int j = 0; j < 8; ++j)
+ {
+ t[i][j] = nthSetBitIndexNaive(i, j);
+ }
+ }
+
+ return t;
+ }();
+ }
+
+ inline int nthSetBitIndex(std::uint64_t v, std::uint64_t n)
+ {
+ std::uint64_t shift = 0;
+
+ std::uint64_t p = intrin::popcount(v & 0xFFFFFFFFull);
+ std::uint64_t pmask = static_cast(p > n) - 1ull;
+ v >>= 32 & pmask;
+ shift += 32 & pmask;
+ n -= p & pmask;
+
+ p = intrin::popcount(v & 0xFFFFull);
+ pmask = static_cast(p > n) - 1ull;
+ v >>= 16 & pmask;
+ shift += 16 & pmask;
+ n -= p & pmask;
+
+ p = intrin::popcount(v & 0xFFull);
+ pmask = static_cast(p > n) - 1ull;
+ shift += 8 & pmask;
+ v >>= 8 & pmask;
+ n -= p & pmask;
+
+ return static_cast(lookup::nthSetBitIndex[v & 0xFFull][n] + shift);
+ }
+
+ namespace util
+ {
+ inline std::size_t usedBits(std::size_t value)
+ {
+ if (value == 0) return 0;
+ return intrin::msb(value) + 1;
+ }
+ }
+
+ template
+ struct EnumTraits;
+
+ template
+ [[nodiscard]] constexpr auto hasEnumTraits() -> decltype(EnumTraits::cardinaliy, bool{})
+ {
+ return true;
+ }
+
+ template
+ [[nodiscard]] constexpr bool hasEnumTraits(...)
+ {
+ return false;
+ }
+
+ template
+ [[nodiscard]] constexpr bool isNaturalIndex() noexcept
+ {
+ return EnumTraits::isNaturalIndex;
+ }
+
+ template
+ [[nodiscard]] constexpr int cardinality() noexcept
+ {
+ return EnumTraits::cardinality;
+ }
+
+ template
+ [[nodiscard]] constexpr const std::array()>& values() noexcept
+ {
+ return EnumTraits::values;
+ }
+
+ template
+ [[nodiscard]] constexpr EnumT fromOrdinal(int id) noexcept
+ {
+ assert(!EnumTraits::isNaturalIndex || (id >= 0 && id < EnumTraits::cardinality));
+
+ return EnumTraits::fromOrdinal(id);
+ }
+
+ template
+ [[nodiscard]] constexpr typename EnumTraits::IdType ordinal(EnumT v) noexcept
+ {
+ return EnumTraits::ordinal(v);
+ }
+
+ template ()>>
+ [[nodiscard]] constexpr decltype(auto) toString(EnumT v, ArgsTs&&... args)
+ {
+ return EnumTraits::toString(v, std::forward(args)...);
+ }
+
+ template
+ [[nodiscard]] constexpr decltype(auto) toString(EnumT v)
+ {
+ return EnumTraits::toString(v);
+ }
+
+ template ()>>
+ [[nodiscard]] constexpr decltype(auto) toString(FormatT&& f, EnumT v)
+ {
+ return EnumTraits::toString(std::forward(f), v);
+ }
+
+ template
+ [[nodiscard]] constexpr decltype(auto) toChar(EnumT v)
+ {
+ return EnumTraits::toChar(v);
+ }
+
+ template
+ [[nodiscard]] constexpr decltype(auto) toChar(FormatT&& f, EnumT v)
+ {
+ return EnumTraits::toChar(std::forward(f), v);
+ }
+
+ template
+ [[nodiscard]] constexpr decltype(auto) fromString(ArgsTs&& ... args)
+ {
+ return EnumTraits::fromString(std::forward(args)...);
+ }
+
+ template
+ [[nodiscard]] constexpr decltype(auto) fromChar(ArgsTs&& ... args)
+ {
+ return EnumTraits::fromChar(std::forward(args)...);
+ }
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = bool;
+
+ static constexpr int cardinality = 2;
+ static constexpr bool isNaturalIndex = true;
+
+ static constexpr std::array values{
+ false,
+ true
+ };
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(IdType id) noexcept
+ {
+ return static_cast(id);
+ }
+ };
+
+ template ()>
+ struct EnumArray
+ {
+ static_assert(isNaturalIndex(), "Enum must start with 0 and end with cardinality-1.");
+
+ using value_type = ValueT;
+ using size_type = std::size_t;
+ using difference_type = std::ptrdiff_t;
+ using pointer = ValueT *;
+ using const_pointer = const ValueT*;
+ using reference = ValueT &;
+ using const_reference = const ValueT &;
+
+ using iterator = pointer;
+ using const_iterator = const_pointer;
+
+ using reverse_iterator = std::reverse_iterator;
+ using const_reverse_iterator = std::reverse_iterator;
+
+ using KeyType = EnumT;
+ using ValueType = ValueT;
+
+ constexpr void fill(const ValueType& init)
+ {
+ for (auto& v : elements)
+ {
+ v = init;
+ }
+ }
+
+ [[nodiscard]] constexpr ValueType& operator[](const KeyType& dir)
+ {
+ assert(static_cast(ordinal(dir)) < static_cast(SizeV));
+
+ return elements[ordinal(dir)];
+ }
+
+ [[nodiscard]] constexpr const ValueType& operator[](const KeyType& dir) const
+ {
+ assert(static_cast(ordinal(dir)) < static_cast(SizeV));
+
+ return elements[ordinal(dir)];
+ }
+
+ [[nodiscard]] constexpr ValueType& front()
+ {
+ return elements[0];
+ }
+
+ [[nodiscard]] constexpr const ValueType& front() const
+ {
+ return elements[0];
+ }
+
+ [[nodiscard]] constexpr ValueType& back()
+ {
+ return elements[SizeV - 1];
+ }
+
+ [[nodiscard]] constexpr const ValueType& back() const
+ {
+ return elements[SizeV - 1];
+ }
+
+ [[nodiscard]] constexpr pointer data()
+ {
+ return elements;
+ }
+
+ [[nodiscard]] constexpr const_pointer data() const
+ {
+ return elements;
+ }
+
+ [[nodiscard]] constexpr iterator begin() noexcept
+ {
+ return elements;
+ }
+
+ [[nodiscard]] constexpr const_iterator begin() const noexcept
+ {
+ return elements;
+ }
+
+ [[nodiscard]] constexpr iterator end() noexcept
+ {
+ return elements + SizeV;
+ }
+
+ [[nodiscard]] constexpr const_iterator end() const noexcept
+ {
+ return elements + SizeV;
+ }
+
+ [[nodiscard]] constexpr reverse_iterator rbegin() noexcept
+ {
+ return reverse_iterator(end());
+ }
+
+ [[nodiscard]] constexpr const_reverse_iterator rbegin() const noexcept
+ {
+ return const_reverse_iterator(end());
+ }
+
+ [[nodiscard]] constexpr reverse_iterator rend() noexcept
+ {
+ return reverse_iterator(begin());
+ }
+
+ [[nodiscard]] constexpr const_reverse_iterator rend() const noexcept
+ {
+ return const_reverse_iterator(begin());
+ }
+
+ [[nodiscard]] constexpr const_iterator cbegin() const noexcept
+ {
+ return begin();
+ }
+
+ [[nodiscard]] constexpr const_iterator cend() const noexcept
+ {
+ return end();
+ }
+
+ [[nodiscard]] constexpr const_reverse_iterator crbegin() const noexcept
+ {
+ return rbegin();
+ }
+
+ [[nodiscard]] constexpr const_reverse_iterator crend() const noexcept
+ {
+ return rend();
+ }
+
+ [[nodiscard]] constexpr size_type size() const noexcept
+ {
+ return SizeV;
+ }
+
+ ValueT elements[SizeV];
+ };
+
+ template (), std::size_t Size2V = cardinality()>
+ using EnumArray2 = EnumArray, Size1V>;
+
+ enum struct Color : std::uint8_t
+ {
+ White,
+ Black
+ };
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = Color;
+
+ static constexpr int cardinality = 2;
+ static constexpr bool isNaturalIndex = true;
+
+ static constexpr std::array values{
+ Color::White,
+ Color::Black
+ };
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(IdType id) noexcept
+ {
+ assert(id >= 0 && id < cardinality);
+
+ return static_cast(id);
+ }
+
+ [[nodiscard]] static constexpr std::string_view toString(EnumType c) noexcept
+ {
+ return std::string_view("wb" + ordinal(c), 1);
+ }
+
+ [[nodiscard]] static constexpr char toChar(EnumType c) noexcept
+ {
+ return "wb"[ordinal(c)];
+ }
+
+ [[nodiscard]] static constexpr std::optional fromChar(char c) noexcept
+ {
+ if (c == 'w') return Color::White;
+ if (c == 'b') return Color::Black;
+
+ return {};
+ }
+
+ [[nodiscard]] static constexpr std::optional fromString(std::string_view sv) noexcept
+ {
+ if (sv.size() != 1) return {};
+
+ return fromChar(sv[0]);
+ }
+ };
+
+ constexpr Color operator!(Color c)
+ {
+ return fromOrdinal(ordinal(c) ^ 1);
+ }
+
+ enum struct PieceType : std::uint8_t
+ {
+ Pawn,
+ Knight,
+ Bishop,
+ Rook,
+ Queen,
+ King,
+
+ None
+ };
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = PieceType;
+
+ static constexpr int cardinality = 7;
+ static constexpr bool isNaturalIndex = true;
+
+ static constexpr std::array values{
+ PieceType::Pawn,
+ PieceType::Knight,
+ PieceType::Bishop,
+ PieceType::Rook,
+ PieceType::Queen,
+ PieceType::King,
+ PieceType::None
+ };
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(IdType id) noexcept
+ {
+ assert(id >= 0 && id < cardinality);
+
+ return static_cast(id);
+ }
+
+ [[nodiscard]] static constexpr std::string_view toString(EnumType p, Color c) noexcept
+ {
+ return std::string_view("PpNnBbRrQqKk " + (chess::ordinal(p) * 2 + chess::ordinal(c)), 1);
+ }
+
+ [[nodiscard]] static constexpr char toChar(EnumType p, Color c) noexcept
+ {
+ return "PpNnBbRrQqKk "[chess::ordinal(p) * 2 + chess::ordinal(c)];
+ }
+
+ [[nodiscard]] static constexpr std::optional fromChar(char c) noexcept
+ {
+ auto it = std::string_view("PpNnBbRrQqKk ").find(c);
+ if (it == std::string::npos) return {};
+ else return static_cast(it/2);
+ }
+
+ [[nodiscard]] static constexpr std::optional fromString(std::string_view sv) noexcept
+ {
+ if (sv.size() != 1) return {};
+
+ return fromChar(sv[0]);
+ }
+ };
+
+ struct Piece
+ {
+ [[nodiscard]] static constexpr Piece fromId(int id)
+ {
+ return Piece(id);
+ }
+
+ [[nodiscard]] static constexpr Piece none()
+ {
+ return Piece(PieceType::None, Color::White);
+ }
+
+ constexpr Piece() noexcept :
+ Piece(PieceType::None, Color::White)
+ {
+
+ }
+
+ constexpr Piece(PieceType type, Color color) noexcept :
+ m_id((ordinal(type) << 1) | ordinal(color))
+ {
+ assert(type != PieceType::None || color == Color::White);
+ }
+
+ constexpr Piece& operator=(const Piece& other) = default;
+
+ [[nodiscard]] constexpr friend bool operator==(Piece lhs, Piece rhs) noexcept
+ {
+ return lhs.m_id == rhs.m_id;
+ }
+
+ [[nodiscard]] constexpr friend bool operator!=(Piece lhs, Piece rhs) noexcept
+ {
+ return !(lhs == rhs);
+ }
+
+ [[nodiscard]] constexpr PieceType type() const
+ {
+ return fromOrdinal(m_id >> 1);
+ }
+
+ [[nodiscard]] constexpr Color color() const
+ {
+ return fromOrdinal(m_id & 1);
+ }
+
+ [[nodiscard]] constexpr std::pair parts() const
+ {
+ return std::make_pair(type(), color());
+ }
+
+ [[nodiscard]] constexpr explicit operator int() const
+ {
+ return static_cast(m_id);
+ }
+
+ private:
+ constexpr Piece(int id) :
+ m_id(id)
+ {
+ }
+
+ std::uint8_t m_id; // lowest bit is a color, 7 highest bits are a piece type
+ };
+
+ [[nodiscard]] constexpr Piece operator|(PieceType type, Color color) noexcept
+ {
+ return Piece(type, color);
+ }
+
+ [[nodiscard]] constexpr Piece operator|(Color color, PieceType type) noexcept
+ {
+ return Piece(type, color);
+ }
+
+ constexpr Piece whitePawn = Piece(PieceType::Pawn, Color::White);
+ constexpr Piece whiteKnight = Piece(PieceType::Knight, Color::White);
+ constexpr Piece whiteBishop = Piece(PieceType::Bishop, Color::White);
+ constexpr Piece whiteRook = Piece(PieceType::Rook, Color::White);
+ constexpr Piece whiteQueen = Piece(PieceType::Queen, Color::White);
+ constexpr Piece whiteKing = Piece(PieceType::King, Color::White);
+
+ constexpr Piece blackPawn = Piece(PieceType::Pawn, Color::Black);
+ constexpr Piece blackKnight = Piece(PieceType::Knight, Color::Black);
+ constexpr Piece blackBishop = Piece(PieceType::Bishop, Color::Black);
+ constexpr Piece blackRook = Piece(PieceType::Rook, Color::Black);
+ constexpr Piece blackQueen = Piece(PieceType::Queen, Color::Black);
+ constexpr Piece blackKing = Piece(PieceType::King, Color::Black);
+
+ static_assert(Piece::none().type() == PieceType::None);
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = Piece;
+
+ static constexpr int cardinality = 13;
+ static constexpr bool isNaturalIndex = true;
+
+ static constexpr std::array values{
+ whitePawn,
+ blackPawn,
+ whiteKnight,
+ blackKnight,
+ whiteBishop,
+ blackBishop,
+ whiteRook,
+ blackRook,
+ whiteQueen,
+ blackQueen,
+ whiteKing,
+ blackKing,
+ Piece::none()
+ };
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(int id) noexcept
+ {
+ assert(id >= 0 && id < cardinality);
+
+ return Piece::fromId(id);
+ }
+
+ [[nodiscard]] static constexpr std::string_view toString(EnumType p) noexcept
+ {
+ return std::string_view("PpNnBbRrQqKk " + ordinal(p), 1);
+ }
+
+ [[nodiscard]] static constexpr char toChar(EnumType p) noexcept
+ {
+ return "PpNnBbRrQqKk "[ordinal(p)];
+ }
+
+ [[nodiscard]] static constexpr std::optional fromChar(char c) noexcept
+ {
+ auto it = std::string_view("PpNnBbRrQqKk ").find(c);
+ if (it == std::string::npos) return {};
+ else return Piece::fromId(static_cast(it));
+ }
+
+ [[nodiscard]] static constexpr std::optional fromString(std::string_view sv) noexcept
+ {
+ if (sv.size() != 1) return {};
+
+ return fromChar(sv[0]);
+ }
+ };
+
+ template
+ struct Coord
+ {
+ constexpr Coord() noexcept :
+ m_i(0)
+ {
+ }
+
+ constexpr explicit Coord(int i) noexcept :
+ m_i(i)
+ {
+ }
+
+ [[nodiscard]] constexpr explicit operator int() const
+ {
+ return static_cast(m_i);
+ }
+
+ constexpr friend Coord& operator++(Coord& c)
+ {
+ ++c.m_i;
+ return c;
+ }
+
+ constexpr friend Coord& operator--(Coord& c)
+ {
+ --c.m_i;
+ return c;
+ }
+
+ constexpr friend Coord& operator+=(Coord& c, int d)
+ {
+ c.m_i += d;
+ return c;
+ }
+
+ constexpr friend Coord& operator-=(Coord& c, int d)
+ {
+ c.m_i -= d;
+ return c;
+ }
+
+ constexpr friend Coord operator+(const Coord& c, int d)
+ {
+ Coord cpy(c);
+ cpy += d;
+ return cpy;
+ }
+
+ constexpr friend Coord operator-(const Coord& c, int d)
+ {
+ Coord cpy(c);
+ cpy -= d;
+ return cpy;
+ }
+
+ constexpr friend int operator-(const Coord& c1, const Coord& c2)
+ {
+ return c1.m_i - c2.m_i;
+ }
+
+ [[nodiscard]] constexpr friend bool operator==(const Coord& c1, const Coord& c2) noexcept
+ {
+ return c1.m_i == c2.m_i;
+ }
+
+ [[nodiscard]] constexpr friend bool operator!=(const Coord& c1, const Coord& c2) noexcept
+ {
+ return c1.m_i != c2.m_i;
+ }
+
+ [[nodiscard]] constexpr friend bool operator<(const Coord& c1, const Coord& c2) noexcept
+ {
+ return c1.m_i < c2.m_i;
+ }
+
+ [[nodiscard]] constexpr friend bool operator<=(const Coord& c1, const Coord& c2) noexcept
+ {
+ return c1.m_i <= c2.m_i;
+ }
+
+ [[nodiscard]] constexpr friend bool operator>(const Coord& c1, const Coord& c2) noexcept
+ {
+ return c1.m_i > c2.m_i;
+ }
+
+ [[nodiscard]] constexpr friend bool operator>=(const Coord& c1, const Coord& c2) noexcept
+ {
+ return c1.m_i >= c2.m_i;
+ }
+
+ private:
+ std::int8_t m_i;
+ };
+
+ struct FileTag;
+ struct RankTag;
+ using File = Coord;
+ using Rank = Coord;
+
+ constexpr File fileA = File(0);
+ constexpr File fileB = File(1);
+ constexpr File fileC = File(2);
+ constexpr File fileD = File(3);
+ constexpr File fileE = File(4);
+ constexpr File fileF = File(5);
+ constexpr File fileG = File(6);
+ constexpr File fileH = File(7);
+
+ constexpr Rank rank1 = Rank(0);
+ constexpr Rank rank2 = Rank(1);
+ constexpr Rank rank3 = Rank(2);
+ constexpr Rank rank4 = Rank(3);
+ constexpr Rank rank5 = Rank(4);
+ constexpr Rank rank6 = Rank(5);
+ constexpr Rank rank7 = Rank(6);
+ constexpr Rank rank8 = Rank(7);
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = File;
+
+ static constexpr int cardinality = 8;
+ static constexpr bool isNaturalIndex = true;
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(IdType id) noexcept
+ {
+ assert(id >= 0 && id < cardinality);
+
+ return static_cast(id);
+ }
+
+ [[nodiscard]] static constexpr std::string_view toString(EnumType c) noexcept
+ {
+ assert(ordinal(c) >= 0 && ordinal(c) < 8);
+
+ return std::string_view("abcdefgh" + ordinal(c), 1);
+ }
+
+ [[nodiscard]] static constexpr std::optional fromChar(char c) noexcept
+ {
+ if (c < 'a' || c > 'h') return {};
+ return static_cast(c - 'a');
+ }
+
+ [[nodiscard]] static constexpr std::optional fromString(std::string_view sv) noexcept
+ {
+ if (sv.size() != 1) return {};
+
+ return fromChar(sv[0]);
+ }
+ };
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = Rank;
+
+ static constexpr int cardinality = 8;
+ static constexpr bool isNaturalIndex = true;
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(IdType id) noexcept
+ {
+ assert(id >= 0 && id < cardinality);
+
+ return static_cast(id);
+ }
+
+ [[nodiscard]] static constexpr std::string_view toString(EnumType c) noexcept
+ {
+ assert(ordinal(c) >= 0 && ordinal(c) < 8);
+
+ return std::string_view("12345678" + ordinal(c), 1);
+ }
+
+ [[nodiscard]] static constexpr std::optional fromChar(char c) noexcept
+ {
+ if (c < '1' || c > '8') return {};
+ return static_cast(c - '1');
+ }
+
+ [[nodiscard]] static constexpr std::optional fromString(std::string_view sv) noexcept
+ {
+ if (sv.size() != 1) return {};
+
+ return fromChar(sv[0]);
+ }
+ };
+
+ // files east
+ // ranks north
+ struct FlatSquareOffset
+ {
+ std::int8_t value;
+
+ constexpr FlatSquareOffset() noexcept :
+ value(0)
+ {
+ }
+
+ constexpr FlatSquareOffset(int files, int ranks) noexcept :
+ value(files + ranks * cardinality())
+ {
+ assert(files + ranks * cardinality() >= std::numeric_limits::min());
+ assert(files + ranks * cardinality() <= std::numeric_limits::max());
+ }
+
+ constexpr FlatSquareOffset operator-() const noexcept
+ {
+ return FlatSquareOffset(-value);
+ }
+
+ private:
+ constexpr FlatSquareOffset(int v) noexcept :
+ value(v)
+ {
+ }
+ };
+
+ struct Offset
+ {
+ std::int8_t files;
+ std::int8_t ranks;
+
+ constexpr Offset() :
+ files(0),
+ ranks(0)
+ {
+ }
+
+ constexpr Offset(int files_, int ranks_) :
+ files(files_),
+ ranks(ranks_)
+ {
+ }
+
+ [[nodiscard]] constexpr FlatSquareOffset flat() const
+ {
+ return { files, ranks };
+ }
+
+ [[nodiscard]] constexpr Offset operator-() const
+ {
+ return { -files, -ranks };
+ }
+ };
+
+ struct SquareCoords
+ {
+ File file;
+ Rank rank;
+
+ constexpr SquareCoords() noexcept :
+ file{},
+ rank{}
+ {
+ }
+
+ constexpr SquareCoords(File f, Rank r) noexcept :
+ file(f),
+ rank(r)
+ {
+ }
+
+ constexpr friend SquareCoords& operator+=(SquareCoords& c, Offset offset)
+ {
+ c.file += offset.files;
+ c.rank += offset.ranks;
+ return c;
+ }
+
+ [[nodiscard]] constexpr friend SquareCoords operator+(const SquareCoords& c, Offset offset)
+ {
+ SquareCoords cpy(c);
+ cpy.file += offset.files;
+ cpy.rank += offset.ranks;
+ return cpy;
+ }
+
+ [[nodiscard]] constexpr bool isOk() const
+ {
+ return file >= fileA && file <= fileH && rank >= rank1 && rank <= rank8;
+ }
+ };
+
+ struct Square
+ {
+ private:
+ static constexpr std::int8_t m_noneId = cardinality() * cardinality();
+
+ static constexpr std::uint8_t fileMask = 0b111;
+ static constexpr std::uint8_t rankMask = 0b111000;
+ static constexpr std::uint8_t rankShift = 3;
+
+ public:
+ [[nodiscard]] static constexpr Square none()
+ {
+ return Square(m_noneId);
+ }
+
+ constexpr Square() noexcept :
+ m_id(0)
+ {
+ }
+
+ constexpr explicit Square(int idx) noexcept :
+ m_id(idx)
+ {
+ assert(isOk() || m_id == m_noneId);
+ }
+
+ constexpr Square(File file, Rank rank) noexcept :
+ m_id(ordinal(file) + ordinal(rank) * cardinality())
+ {
+ assert(isOk());
+ }
+
+ constexpr explicit Square(SquareCoords coords) noexcept :
+ Square(coords.file, coords.rank)
+ {
+ }
+
+ [[nodiscard]] constexpr friend bool operator<(Square lhs, Square rhs) noexcept
+ {
+ return lhs.m_id < rhs.m_id;
+ }
+
+ [[nodiscard]] constexpr friend bool operator>(Square lhs, Square rhs) noexcept
+ {
+ return lhs.m_id > rhs.m_id;
+ }
+
+ [[nodiscard]] constexpr friend bool operator<=(Square lhs, Square rhs) noexcept
+ {
+ return lhs.m_id <= rhs.m_id;
+ }
+
+ [[nodiscard]] constexpr friend bool operator>=(Square lhs, Square rhs) noexcept
+ {
+ return lhs.m_id >= rhs.m_id;
+ }
+
+ [[nodiscard]] constexpr friend bool operator==(Square lhs, Square rhs) noexcept
+ {
+ return lhs.m_id == rhs.m_id;
+ }
+
+ [[nodiscard]] constexpr friend bool operator!=(Square lhs, Square rhs) noexcept
+ {
+ return !(lhs == rhs);
+ }
+
+ constexpr friend Square& operator++(Square& sq)
+ {
+ ++sq.m_id;
+ return sq;
+ }
+
+ constexpr friend Square& operator--(Square& sq)
+ {
+ --sq.m_id;
+ return sq;
+ }
+
+ [[nodiscard]] constexpr friend Square operator+(Square sq, FlatSquareOffset offset)
+ {
+ Square sqCpy = sq;
+ sqCpy += offset;
+ return sqCpy;
+ }
+
+ constexpr friend Square& operator+=(Square& sq, FlatSquareOffset offset)
+ {
+ assert(sq.m_id + offset.value >= 0 && sq.m_id + offset.value < Square::m_noneId);
+ sq.m_id += offset.value;
+ return sq;
+ }
+
+ [[nodiscard]] constexpr friend Square operator+(Square sq, Offset offset)
+ {
+ assert(sq.file() + offset.files >= fileA);
+ assert(sq.file() + offset.files <= fileH);
+ assert(sq.rank() + offset.ranks >= rank1);
+ assert(sq.rank() + offset.ranks <= rank8);
+ return operator+(sq, offset.flat());
+ }
+
+ constexpr friend Square& operator+=(Square& sq, Offset offset)
+ {
+ return operator+=(sq, offset.flat());
+ }
+
+ [[nodiscard]] constexpr explicit operator int() const
+ {
+ return m_id;
+ }
+
+ [[nodiscard]] constexpr File file() const
+ {
+ assert(isOk());
+ return File(static_cast(m_id) & fileMask);
+ }
+
+ [[nodiscard]] constexpr Rank rank() const
+ {
+ assert(isOk());
+ return Rank(static_cast(m_id) >> rankShift);
+ }
+
+ [[nodiscard]] constexpr SquareCoords coords() const
+ {
+ return { file(), rank() };
+ }
+
+ [[nodiscard]] constexpr Color color() const
+ {
+ assert(isOk());
+ return !fromOrdinal((ordinal(rank()) + ordinal(file())) & 1);
+ }
+
+ constexpr void flipVertically()
+ {
+ m_id ^= rankMask;
+ }
+
+ constexpr void flipHorizontally()
+ {
+ m_id ^= fileMask;
+ }
+
+ constexpr Square flippedVertically() const
+ {
+ return Square(m_id ^ rankMask);
+ }
+
+ constexpr Square flippedHorizontally() const
+ {
+ return Square(m_id ^ fileMask);
+ }
+
+ [[nodiscard]] constexpr bool isOk() const
+ {
+ return m_id >= 0 && m_id < m_noneId;
+ }
+
+ private:
+ std::int8_t m_id;
+ };
+
+ constexpr Square a1(fileA, rank1);
+ constexpr Square a2(fileA, rank2);
+ constexpr Square a3(fileA, rank3);
+ constexpr Square a4(fileA, rank4);
+ constexpr Square a5(fileA, rank5);
+ constexpr Square a6(fileA, rank6);
+ constexpr Square a7(fileA, rank7);
+ constexpr Square a8(fileA, rank8);
+
+ constexpr Square b1(fileB, rank1);
+ constexpr Square b2(fileB, rank2);
+ constexpr Square b3(fileB, rank3);
+ constexpr Square b4(fileB, rank4);
+ constexpr Square b5(fileB, rank5);
+ constexpr Square b6(fileB, rank6);
+ constexpr Square b7(fileB, rank7);
+ constexpr Square b8(fileB, rank8);
+
+ constexpr Square c1(fileC, rank1);
+ constexpr Square c2(fileC, rank2);
+ constexpr Square c3(fileC, rank3);
+ constexpr Square c4(fileC, rank4);
+ constexpr Square c5(fileC, rank5);
+ constexpr Square c6(fileC, rank6);
+ constexpr Square c7(fileC, rank7);
+ constexpr Square c8(fileC, rank8);
+
+ constexpr Square d1(fileD, rank1);
+ constexpr Square d2(fileD, rank2);
+ constexpr Square d3(fileD, rank3);
+ constexpr Square d4(fileD, rank4);
+ constexpr Square d5(fileD, rank5);
+ constexpr Square d6(fileD, rank6);
+ constexpr Square d7(fileD, rank7);
+ constexpr Square d8(fileD, rank8);
+
+ constexpr Square e1(fileE, rank1);
+ constexpr Square e2(fileE, rank2);
+ constexpr Square e3(fileE, rank3);
+ constexpr Square e4(fileE, rank4);
+ constexpr Square e5(fileE, rank5);
+ constexpr Square e6(fileE, rank6);
+ constexpr Square e7(fileE, rank7);
+ constexpr Square e8(fileE, rank8);
+
+ constexpr Square f1(fileF, rank1);
+ constexpr Square f2(fileF, rank2);
+ constexpr Square f3(fileF, rank3);
+ constexpr Square f4(fileF, rank4);
+ constexpr Square f5(fileF, rank5);
+ constexpr Square f6(fileF, rank6);
+ constexpr Square f7(fileF, rank7);
+ constexpr Square f8(fileF, rank8);
+
+ constexpr Square g1(fileG, rank1);
+ constexpr Square g2(fileG, rank2);
+ constexpr Square g3(fileG, rank3);
+ constexpr Square g4(fileG, rank4);
+ constexpr Square g5(fileG, rank5);
+ constexpr Square g6(fileG, rank6);
+ constexpr Square g7(fileG, rank7);
+ constexpr Square g8(fileG, rank8);
+
+ constexpr Square h1(fileH, rank1);
+ constexpr Square h2(fileH, rank2);
+ constexpr Square h3(fileH, rank3);
+ constexpr Square h4(fileH, rank4);
+ constexpr Square h5(fileH, rank5);
+ constexpr Square h6(fileH, rank6);
+ constexpr Square h7(fileH, rank7);
+ constexpr Square h8(fileH, rank8);
+
+ static_assert(e1.color() == Color::Black);
+ static_assert(e8.color() == Color::White);
+
+ static_assert(e1.file() == fileE);
+ static_assert(e1.rank() == rank1);
+
+ static_assert(e1.flippedHorizontally() == d1);
+ static_assert(e1.flippedVertically() == e8);
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = Square;
+
+ static constexpr int cardinality = chess::cardinality() * chess::cardinality();
+ static constexpr bool isNaturalIndex = true;
+
+ static constexpr std::array values{
+ 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
+ };
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(IdType id) noexcept
+ {
+ assert(id >= 0 && id < cardinality + 1);
+
+ return static_cast(id);
+ }
+
+ [[nodiscard]] static constexpr std::string_view toString(Square sq)
+ {
+ assert(sq.isOk());
+
+ return
+ std::string_view(
+ "a1b1c1d1e1f1g1h1"
+ "a2b2c2d2e2f2g2h2"
+ "a3b3c3d3e3f3g3h3"
+ "a4b4c4d4e4f4g4h4"
+ "a5b5c5d5e5f5g5h5"
+ "a6b6c6d6e6f6g6h6"
+ "a7b7c7d7e7f7g7h7"
+ "a8b8c8d8e8f8g8h8"
+ + (ordinal(sq) * 2),
+ 2
+ );
+ }
+
+ [[nodiscard]] static constexpr std::optional fromString(std::string_view sv) noexcept
+ {
+ if (sv.size() != 2) return {};
+
+ const char f = sv[0];
+ const char r = sv[1];
+ if (f < 'a' || f > 'h') return {};
+ if (r < '1' || r > '8') return {};
+
+ return Square(static_cast(f - 'a'), static_cast(r - '1'));
+ }
+ };
+
+ static_assert(toString(d1) == std::string_view("d1"));
+ static_assert(values()[29] == f4);
+
+ enum struct MoveType : std::uint8_t
+ {
+ Normal,
+ Promotion,
+ Castle,
+ EnPassant
+ };
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = MoveType;
+
+ static constexpr int cardinality = 4;
+ static constexpr bool isNaturalIndex = true;
+
+ static constexpr std::array values{
+ MoveType::Normal,
+ MoveType::Promotion,
+ MoveType::Castle,
+ MoveType::EnPassant
+ };
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(IdType id) noexcept
+ {
+ assert(id >= 0 && id < cardinality);
+
+ return static_cast(id);
+ }
+ };
+
+ enum struct CastleType : std::uint8_t
+ {
+ Short,
+ Long
+ };
+
+ [[nodiscard]] constexpr CastleType operator!(CastleType ct)
+ {
+ return static_cast(static_cast(ct) ^ 1);
+ }
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = CastleType;
+
+ static constexpr int cardinality = 2;
+ static constexpr bool isNaturalIndex = true;
+
+ static constexpr std::array values{
+ CastleType::Short,
+ CastleType::Long
+ };
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(IdType id) noexcept
+ {
+ assert(id >= 0 && id < cardinality);
+
+ return static_cast(id);
+ }
+ };
+
+ struct CompressedMove;
+
+ // castling is encoded as a king capturing rook
+ // ep is encoded as a normal pawn capture (move.to is empty on the board)
+ struct Move
+ {
+ Square from;
+ Square to;
+ MoveType type = MoveType::Normal;
+ Piece promotedPiece = Piece::none();
+
+ [[nodiscard]] constexpr friend bool operator==(const Move& lhs, const Move& rhs) noexcept
+ {
+ return lhs.from == rhs.from
+ && lhs.to == rhs.to
+ && lhs.type == rhs.type
+ && lhs.promotedPiece == rhs.promotedPiece;
+ }
+
+ [[nodiscard]] constexpr friend bool operator!=(const Move& lhs, const Move& rhs) noexcept
+ {
+ return !(lhs == rhs);
+ }
+
+ [[nodiscard]] constexpr CompressedMove compress() const noexcept;
+
+ [[nodiscard]] constexpr static Move null()
+ {
+ return Move{ Square::none(), Square::none() };
+ }
+
+ [[nodiscard]] constexpr static Move castle(CastleType ct, Color c);
+
+ [[nodiscard]] constexpr static Move normal(Square from, Square to)
+ {
+ return Move{ from, to, MoveType::Normal, Piece::none() };
+ }
+
+ [[nodiscard]] constexpr static Move enPassant(Square from, Square to)
+ {
+ return Move{ from, to, MoveType::EnPassant, Piece::none() };
+ }
+
+ [[nodiscard]] constexpr static Move promotion(Square from, Square to, Piece piece)
+ {
+ return Move{ from, to, MoveType::Promotion, piece };
+ }
+ };
+
+ namespace detail::castle
+ {
+ constexpr EnumArray2 moves = { {
+ {{ { e1, h1, MoveType::Castle }, { e8, h8, MoveType::Castle } }},
+ {{ { e1, a1, MoveType::Castle }, { e8, a8, MoveType::Castle } }}
+ } };
+ }
+
+ [[nodiscard]] constexpr Move Move::castle(CastleType ct, Color c)
+ {
+ return detail::castle::moves[ct][c];
+ }
+
+ static_assert(sizeof(Move) == 4);
+
+ struct CompressedMove
+ {
+ private:
+ // from most significant bits
+ // 2 bits for move type
+ // 6 bits for from square
+ // 6 bits for to square
+ // 2 bits for promoted piece type
+ // 0 if not a promotion
+ static constexpr std::uint16_t squareMask = 0b111111u;
+ static constexpr std::uint16_t promotedPieceTypeMask = 0b11u;
+ static constexpr std::uint16_t moveTypeMask = 0b11u;
+
+ public:
+ [[nodiscard]] constexpr static CompressedMove readFromBigEndian(const unsigned char* data)
+ {
+ CompressedMove move{};
+ move.m_packed = (data[0] << 8) | data[1];
+ return move;
+ }
+
+ constexpr CompressedMove() noexcept :
+ m_packed(0)
+ {
+ }
+
+ // move must be either valid or a null move
+ constexpr CompressedMove(Move move) noexcept :
+ m_packed(0)
+ {
+ // else null move
+ if (move.from != move.to)
+ {
+ assert(move.from != Square::none());
+ assert(move.to != Square::none());
+
+ m_packed =
+ (static_cast(ordinal(move.type)) << (16 - 2))
+ | (static_cast(ordinal(move.from)) << (16 - 2 - 6))
+ | (static_cast(ordinal(move.to)) << (16 - 2 - 6 - 6));
+
+ if (move.type == MoveType::Promotion)
+ {
+ assert(move.promotedPiece != Piece::none());
+
+ m_packed |= ordinal(move.promotedPiece.type()) - ordinal(PieceType::Knight);
+ }
+ else
+ {
+ assert(move.promotedPiece == Piece::none());
+ }
+ }
+ }
+
+ void writeToBigEndian(unsigned char* data) const
+ {
+ *data++ = m_packed >> 8;
+ *data++ = m_packed & 0xFF;
+ }
+
+ [[nodiscard]] constexpr std::uint16_t packed() const
+ {
+ return m_packed;
+ }
+
+ [[nodiscard]] constexpr MoveType type() const
+ {
+ return fromOrdinal(m_packed >> (16 - 2));
+ }
+
+ [[nodiscard]] constexpr Square from() const
+ {
+ return fromOrdinal((m_packed >> (16 - 2 - 6)) & squareMask);
+ }
+
+ [[nodiscard]] constexpr Square to() const
+ {
+ return fromOrdinal((m_packed >> (16 - 2 - 6 - 6)) & squareMask);
+ }
+
+ [[nodiscard]] constexpr Piece promotedPiece() const
+ {
+ if (type() == MoveType::Promotion)
+ {
+ const Color color =
+ (to().rank() == rank1)
+ ? Color::Black
+ : Color::White;
+
+ const PieceType pt = fromOrdinal((m_packed & promotedPieceTypeMask) + ordinal(PieceType::Knight));
+ return color | pt;
+ }
+ else
+ {
+ return Piece::none();
+ }
+ }
+
+ [[nodiscard]] constexpr Move decompress() const noexcept
+ {
+ if (m_packed == 0)
+ {
+ return Move::null();
+ }
+ else
+ {
+ const MoveType type = fromOrdinal(m_packed >> (16 - 2));
+ const Square from = fromOrdinal((m_packed >> (16 - 2 - 6)) & squareMask);
+ const Square to = fromOrdinal((m_packed >> (16 - 2 - 6 - 6)) & squareMask);
+ const Piece promotedPiece = [&]() {
+ if (type == MoveType::Promotion)
+ {
+ const Color color =
+ (to.rank() == rank1)
+ ? Color::Black
+ : Color::White;
+
+ const PieceType pt = fromOrdinal((m_packed & promotedPieceTypeMask) + ordinal(PieceType::Knight));
+ return color | pt;
+ }
+ else
+ {
+ return Piece::none();
+ }
+ }();
+
+ return Move{ from, to, type, promotedPiece };
+ }
+ }
+
+ private:
+ std::uint16_t m_packed;
+ };
+
+ static_assert(sizeof(CompressedMove) == 2);
+
+ [[nodiscard]] constexpr CompressedMove Move::compress() const noexcept
+ {
+ return CompressedMove(*this);
+ }
+
+ static_assert(a4 + Offset{ 0, 1 } == a5);
+ static_assert(a4 + Offset{ 0, 2 } == a6);
+ static_assert(a4 + Offset{ 0, -2 } == a2);
+ static_assert(a4 + Offset{ 0, -1 } == a3);
+
+ static_assert(e4 + Offset{ 1, 0 } == f4);
+ static_assert(e4 + Offset{ 2, 0 } == g4);
+ static_assert(e4 + Offset{ -1, 0 } == d4);
+ static_assert(e4 + Offset{ -2, 0 } == c4);
+
+ enum struct CastlingRights : std::uint8_t
+ {
+ None = 0x0,
+ WhiteKingSide = 0x1,
+ WhiteQueenSide = 0x2,
+ BlackKingSide = 0x4,
+ BlackQueenSide = 0x8,
+ White = WhiteKingSide | WhiteQueenSide,
+ Black = BlackKingSide | BlackQueenSide,
+ All = WhiteKingSide | WhiteQueenSide | BlackKingSide | BlackQueenSide
+ };
+
+ [[nodiscard]] constexpr CastlingRights operator|(CastlingRights lhs, CastlingRights rhs)
+ {
+ return static_cast(static_cast(lhs) | static_cast(rhs));
+ }
+
+ [[nodiscard]] constexpr CastlingRights operator&(CastlingRights lhs, CastlingRights rhs)
+ {
+ return static_cast(static_cast(lhs) & static_cast(rhs));
+ }
+
+ [[nodiscard]] constexpr CastlingRights operator~(CastlingRights lhs)
+ {
+ return static_cast(~static_cast(lhs) & static_cast(CastlingRights::All));
+ }
+
+ constexpr CastlingRights& operator|=(CastlingRights& lhs, CastlingRights rhs)
+ {
+ lhs = static_cast(static_cast(lhs) | static_cast(rhs));
+ return lhs;
+ }
+
+ constexpr CastlingRights& operator&=(CastlingRights& lhs, CastlingRights rhs)
+ {
+ lhs = static_cast(static_cast(lhs) & static_cast(rhs));
+ return lhs;
+ }
+ // checks whether lhs contains rhs
+ [[nodiscard]] constexpr bool contains(CastlingRights lhs, CastlingRights rhs)
+ {
+ return (lhs & rhs) == rhs;
+ }
+
+ template <>
+ struct EnumTraits
+ {
+ using IdType = int;
+ using EnumType = CastlingRights;
+
+ static constexpr int cardinality = 4;
+ static constexpr bool isNaturalIndex = false;
+
+ static constexpr std::array values{
+ CastlingRights::WhiteKingSide,
+ CastlingRights::WhiteQueenSide,
+ CastlingRights::BlackKingSide,
+ CastlingRights::BlackQueenSide
+ };
+
+ [[nodiscard]] static constexpr int ordinal(EnumType c) noexcept
+ {
+ return static_cast(c);
+ }
+
+ [[nodiscard]] static constexpr EnumType fromOrdinal(IdType id) noexcept
+ {
+ return static_cast(id);
+ }
+ };
+
+ struct CompressedReverseMove;
+
+ struct ReverseMove
+ {
+ Move move;
+ Piece capturedPiece;
+ Square oldEpSquare;
+ CastlingRights oldCastlingRights;
+
+ // We need a well defined case for the starting position.
+ constexpr ReverseMove() :
+ move(Move::null()),
+ capturedPiece(Piece::none()),
+ oldEpSquare(Square::none()),
+ oldCastlingRights(CastlingRights::All)
+ {
+ }
+
+ constexpr ReverseMove(const Move& move_, Piece capturedPiece_, Square oldEpSquare_, CastlingRights oldCastlingRights_) :
+ move(move_),
+ capturedPiece(capturedPiece_),
+ oldEpSquare(oldEpSquare_),
+ oldCastlingRights(oldCastlingRights_)
+ {
+ }
+
+ constexpr bool isNull() const
+ {
+ return move.from == move.to;
+ }
+
+ [[nodiscard]] constexpr CompressedReverseMove compress() const noexcept;
+
+ [[nodiscard]] constexpr friend bool operator==(const ReverseMove& lhs, const ReverseMove& rhs) noexcept
+ {
+ return lhs.move == rhs.move
+ && lhs.capturedPiece == rhs.capturedPiece
+ && lhs.oldEpSquare == rhs.oldEpSquare
+ && lhs.oldCastlingRights == rhs.oldCastlingRights;
+ }
+
+ [[nodiscard]] constexpr friend bool operator!=(const ReverseMove& lhs, const ReverseMove& rhs) noexcept
+ {
+ return !(lhs == rhs);
+ }
+ };
+
+ static_assert(sizeof(ReverseMove) == 7);
+
+ struct CompressedReverseMove
+ {
+ private:
+ // we use 7 bits because it can be Square::none()
+ static constexpr std::uint32_t squareMask = 0b1111111u;
+ static constexpr std::uint32_t pieceMask = 0b1111u;
+ static constexpr std::uint32_t castlingRightsMask = 0b1111;
+ public:
+
+ constexpr CompressedReverseMove() noexcept :
+ m_move{},
+ m_oldState{}
+ {
+ }
+
+ constexpr CompressedReverseMove(const ReverseMove& rm) noexcept :
+ m_move(rm.move.compress()),
+ m_oldState{ static_cast(
+ ((ordinal(rm.capturedPiece) & pieceMask) << 11)
+ | ((ordinal(rm.oldCastlingRights) & castlingRightsMask) << 7)
+ | (ordinal(rm.oldEpSquare) & squareMask)
+ )
+ }
+ {
+ }
+
+ [[nodiscard]] constexpr Move move() const
+ {
+ return m_move.decompress();
+ }
+
+ [[nodiscard]] const CompressedMove& compressedMove() const
+ {
+ return m_move;
+ }
+
+ [[nodiscard]] constexpr Piece capturedPiece() const
+ {
+ return fromOrdinal(m_oldState >> 11);
+ }
+
+ [[nodiscard]] constexpr CastlingRights oldCastlingRights() const
+ {
+ return fromOrdinal((m_oldState >> 7) & castlingRightsMask);
+ }
+
+ [[nodiscard]] constexpr Square oldEpSquare() const
+ {
+ return fromOrdinal(m_oldState & squareMask);
+ }
+
+ [[nodiscard]] constexpr ReverseMove decompress() const noexcept
+ {
+ const Piece capturedPiece = fromOrdinal(m_oldState >> 11);
+ const CastlingRights castlingRights = fromOrdinal((m_oldState >> 7) & castlingRightsMask);
+ // We could pack the ep square more, but don't have to, because
+ // can't save another byte anyway.
+ const Square epSquare = fromOrdinal(m_oldState & squareMask);
+
+ return ReverseMove(m_move.decompress(), capturedPiece, epSquare, castlingRights);
+ }
+
+ private:
+ CompressedMove m_move;
+ std::uint16_t m_oldState;
+ };
+
+ static_assert(sizeof(CompressedReverseMove) == 4);
+
+ [[nodiscard]] constexpr CompressedReverseMove ReverseMove::compress() const noexcept
+ {
+ return CompressedReverseMove(*this);
+ }
+
+ // This can be regarded as a perfect hash. Going back is hard.
+ struct PackedReverseMove
+ {
+ static constexpr std::uint32_t mask = 0x7FFFFFFu;
+ static constexpr std::size_t numBits = 27;
+
+ private:
+ static constexpr std::uint32_t squareMask = 0b111111u;
+ static constexpr std::uint32_t pieceMask = 0b1111u;
+ static constexpr std::uint32_t pieceTypeMask = 0b111u;
+ static constexpr std::uint32_t castlingRightsMask = 0b1111;
+ static constexpr std::uint32_t fileMask = 0b111;
+
+ public:
+ constexpr PackedReverseMove(const std::uint32_t packed) :
+ m_packed(packed)
+ {
+
+ }
+
+ constexpr PackedReverseMove(const ReverseMove& reverseMove) :
+ m_packed(
+ 0u
+ // The only move when square is none() is null move and
+ // then both squares are none(). No other move is like that
+ // so we don't lose any information by storing only
+ // the 6 bits of each square.
+ | ((ordinal(reverseMove.move.from) & squareMask) << 21)
+ | ((ordinal(reverseMove.move.to) & squareMask) << 15)
+ // Other masks are just for code clarity, they should
+ // never change the values.
+ | ((ordinal(reverseMove.capturedPiece) & pieceMask) << 11)
+ | ((ordinal(reverseMove.oldCastlingRights) & castlingRightsMask) << 7)
+ | ((ordinal(reverseMove.move.promotedPiece.type()) & pieceTypeMask) << 4)
+ | (((reverseMove.oldEpSquare != Square::none()) & 1) << 3)
+ // We probably could omit the squareMask here but for clarity it's left.
+ | (ordinal(Square(ordinal(reverseMove.oldEpSquare) & squareMask).file()) & fileMask)
+ )
+ {
+ }
+
+ constexpr std::uint32_t packed() const
+ {
+ return m_packed;
+ }
+
+ constexpr ReverseMove unpack(Color sideThatMoved) const
+ {
+ ReverseMove rmove{};
+
+ rmove.move.from = fromOrdinal((m_packed >> 21) & squareMask);
+ rmove.move.to = fromOrdinal((m_packed >> 15) & squareMask);
+ rmove.capturedPiece = fromOrdinal((m_packed >> 11) & pieceMask);
+ rmove.oldCastlingRights = fromOrdinal((m_packed >> 7) & castlingRightsMask);
+ const PieceType promotedPieceType = fromOrdinal((m_packed >> 4) & pieceTypeMask);
+ if (promotedPieceType != PieceType::None)
+ {
+ rmove.move.promotedPiece = Piece(promotedPieceType, sideThatMoved);
+ rmove.move.type = MoveType::Promotion;
+ }
+ const bool hasEpSquare = static_cast((m_packed >> 3) & 1);
+ if (hasEpSquare)
+ {
+ // ep square is always where the opponent moved
+ const Rank rank =
+ sideThatMoved == Color::White
+ ? rank6
+ : rank3;
+ const File file = fromOrdinal