├── .github └── workflows │ └── release-pipeline.yaml ├── .gitignore ├── LICENSE ├── OpenBench ├── Instructions.txt ├── Test │ └── Makefile └── Tune │ └── Makefile ├── README.md ├── Scripts ├── build_linux_avx256.sh ├── build_linux_avx512.sh ├── build_osx_avx256.sh ├── build_osx_avx512.sh ├── build_windows_avx256.bat ├── build_windows_avx512.bat └── sprt.bat ├── sapling-banner.png └── src ├── README.md ├── Sapling.Engine.Tests ├── BitboardHelperTests.cs ├── InsufficientMaterialTests.cs ├── MoveTests.cs ├── NNUETests.cs ├── PerftTests.cs ├── PgnTests.cs ├── PromotionThreatTests.cs ├── Sapling.Engine.Tests.csproj ├── SquareHelperTests.cs ├── StaticExchangeTests.cs └── ZobristTests.cs ├── Sapling.Engine ├── BitboardHelpers.cs ├── BoardState.cs ├── BoardStateExtensions.cs ├── Constants.cs ├── DataGen │ ├── Bench.cs │ ├── BulletFormat.cs │ ├── Chess960.cs │ └── DataGenerator.cs ├── Evaluation │ ├── NnueEvaluator.cs │ ├── NnueExtensions.cs │ ├── NnueWeights.cs │ ├── StaticExchangeEvaluator.cs │ └── Unrolled.cs ├── GameState.cs ├── GlobalUsings.cs ├── MagicBitBoard.cs ├── MathHelpers.cs ├── MoveGen │ ├── AttackTables.cs │ ├── MoveExtensions.cs │ ├── MoveGenerator.cs │ ├── MoveScoring.cs │ └── MoveType.cs ├── OutputHelpers.cs ├── Pgn │ ├── Lexer.cs │ ├── PgnParser.cs │ ├── PgnSplitter.cs │ ├── Token.cs │ └── TokenType.cs ├── Piece.cs ├── PieceValues.cs ├── RepetitionDetector.cs ├── Resources │ ├── WeightsHistory │ │ ├── 00_hl256_random.bin │ │ ├── 01_hl256.bin │ │ ├── 02_hl256.bin │ │ ├── 03_hl256.bin │ │ ├── 04_hl256.bin │ │ ├── 05_hl256.bin │ │ ├── 06_hl256.bin │ │ ├── 07_hl256.bin │ │ ├── 08_hl256.bin │ │ ├── 09_hl256.bin │ │ ├── 10_hl256.bin │ │ ├── 11_hl256.bin │ │ ├── 12_hl512.bin │ │ ├── 13_hl768.bin │ │ ├── 14_hl1024.bin │ │ ├── 15_hl1024.bin │ │ ├── 16_hl1024.bin │ │ ├── 17_(768x4-1024)x2-8.bin │ │ ├── 18_(768x8-1024)x2-8.bin │ │ ├── 19_(768x4-1024)x2-8.bin │ │ ├── 20_(768x4-1024)x2-8.bin │ │ └── log.txt │ └── sapling.nnue ├── Sapling.Engine.csproj ├── Search │ ├── CorrectionHistory.cs │ ├── HistoryHeuristicExtensions.cs │ ├── NegaMaxSearch.cs │ ├── PVTable.cs │ ├── ParallelSearcher.cs │ ├── Perft.cs │ ├── QuiescenceSearch.cs │ └── Searcher.cs ├── SkipLocalsInit.cs ├── SquareHelpers.cs ├── Transpositions │ ├── Transposition.cs │ ├── TranspositionTableExtensions.cs │ └── TranspositionTableFlag.cs ├── Tuning │ └── SpsaOptions.cs ├── Zobrist.cs ├── logo.ico └── logo.png ├── Sapling.SourceGenerators ├── Class1.cs └── Sapling.SourceGenerators.csproj ├── Sapling.Utils ├── Program - Copy.cs ├── Program.cs └── Sapling.Utils.csproj ├── Sapling.sln ├── Sapling.sln.DotSettings ├── Sapling ├── App.config ├── Program.cs ├── Sapling.csproj ├── UciEngine.cs ├── logo.ico └── logo.png ├── logo.ico └── logo.png /.github/workflows/release-pipeline.yaml: -------------------------------------------------------------------------------- 1 | name: Release Pipeline 2 | 3 | on: 4 | workflow_dispatch: 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | defaults: 12 | run: 13 | working-directory: src/Sapling 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup .NET Core 19 | uses: actions/setup-dotnet@v1 20 | with: 21 | dotnet-version: 9.0.x 22 | 23 | # Get the version from the .csproj file 24 | - name: Get .NET application version 25 | working-directory: src/Sapling 26 | id: get_version 27 | run: | 28 | VERSION=1.2.7 29 | echo "Application version: $VERSION" 30 | echo "::set-output name=version::$VERSION" 31 | 32 | # Build and Package AVX512 for all platforms 33 | - name: Build and Package AVX512 for Windows 34 | run: | 35 | dotnet restore 36 | dotnet publish -c Release -r win-x64 --self-contained /p:Release=true /p:PublishSingleFile=true -p:DefineConstants="AVX512" -o ../output/win-x64-avx512 37 | 38 | - name: Build and Package Non-AVX512 for Windows 39 | run: | 40 | dotnet restore 41 | dotnet publish -c Release -r win-x64 --self-contained /p:Release=true /p:PublishSingleFile=true -o ../output/win-x64 42 | 43 | - name: Build and Package AVX512 for Linux 44 | run: | 45 | dotnet restore 46 | dotnet publish -c Release -r linux-x64 --self-contained /p:Release=true /p:PublishSingleFile=true -p:DefineConstants="AVX512" -o ../output/linux-x64-avx512 47 | 48 | - name: Build and Package Non-AVX512 for Linux 49 | run: | 50 | dotnet restore 51 | dotnet publish -c Release -r linux-x64 --self-contained /p:Release=true /p:PublishSingleFile=true -o ../output/linux-x64 52 | 53 | - name: Build and Package AVX512 for OSX 54 | run: | 55 | dotnet restore 56 | dotnet publish -c Release -r osx-x64 --self-contained /p:Release=true /p:PublishSingleFile=true -p:DefineConstants="AVX512" -o ../output/osx-x64-avx512 57 | 58 | - name: Build and Package Non-AVX512 for OSX 59 | run: | 60 | dotnet restore 61 | dotnet publish -c Release -r osx-x64 --self-contained /p:Release=true /p:PublishSingleFile=true -o ../output/osx-x64 62 | 63 | # Rename the output files for consistency 64 | - name: Rename output files 65 | run: | 66 | mv ../output/win-x64-avx512/Sapling.exe ../output/Sapling_win_x64_avx512.exe 67 | mv ../output/win-x64/Sapling.exe ../output/Sapling_win_x64.exe 68 | mv ../output/linux-x64-avx512/Sapling ../output/Sapling_linux_x64_avx512 69 | mv ../output/linux-x64/Sapling ../output/Sapling_linux_x64 70 | mv ../output/osx-x64-avx512/Sapling ../output/Sapling_osx_x64_avx512 71 | mv ../output/osx-x64/Sapling ../output/Sapling_osx_x64 72 | 73 | # List files in output directory to verify they exist 74 | - name: Verify output files 75 | run: | 76 | ls -R ../output 77 | 78 | # Generate release tag based on version 79 | - name: Generate release tag 80 | id: tag 81 | run: | 82 | echo "::set-output name=release_tag::Sapling-${{ steps.get_version.outputs.version }}" 83 | 84 | # Create a single release with all artifacts 85 | - name: Create GitHub release 86 | uses: softprops/action-gh-release@v1 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | with: 90 | tag_name: ${{ steps.tag.outputs.release_tag }} 91 | files: | 92 | ${{ github.workspace }}/src/output/Sapling_win_x64_avx512.exe 93 | ${{ github.workspace }}/src/output/Sapling_win_x64.exe 94 | ${{ github.workspace }}/src/output/Sapling_linux_x64_avx512 95 | ${{ github.workspace }}/src/output/Sapling_linux_x64 96 | ${{ github.workspace }}/src/output/Sapling_osx_x64_avx512 97 | ${{ github.workspace }}/src/output/Sapling_osx_x64 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /OpenBench/Instructions.txt: -------------------------------------------------------------------------------- 1 | git clone https://github.com/AndyGrant/OpenBench.git 2 | cd OpenBench 3 | python -m pip install --upgrade pip 4 | python -m pip install virtualenv 5 | python -m venv venv 6 | python -m pip install -r Client/requirements.txt 7 | venv/Scripts/activate 8 | cd ./Client 9 | python client.py -U -P -S -T -N 1 -I 10 | 11 | sudo apt-get install -y dotnet-sdk-8.0 12 | apt-get install git 13 | apt-get install python3-pip 14 | apt-get install python3-venv 15 | source ./venv/bin/activate 16 | -------------------------------------------------------------------------------- /OpenBench/Test/Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := publish 2 | 3 | ifndef EXE 4 | EXE = Sapling 5 | endif 6 | 7 | # Set a default output directory if not already defined 8 | OUTPUT_DIR ?= ./ 9 | 10 | # Detect OS and Architecture 11 | UNAME_S := $(shell uname -s) 12 | UNAME_P := $(shell uname -p) 13 | 14 | ifeq ($(OS),Windows_NT) 15 | RUNTIME=win-x64 16 | SHELL := cmd.exe 17 | MKDIR_CMD := if not exist "$(subst /,\,$(OUTPUT_DIR))" mkdir "$(subst /,\,$(OUTPUT_DIR))" 18 | else 19 | # Default runtime for Linux/MacOS 20 | ifeq ($(UNAME_S),Linux) 21 | RUNTIME=linux-x64 22 | ifneq ($(filter aarch64% armv8% arm%,$(UNAME_P)),) 23 | RUNTIME=linux-arm64 24 | endif 25 | else ifeq ($(UNAME_S),Darwin) 26 | RUNTIME=osx-x64 27 | ifneq ($(filter arm%,$(UNAME_P)),) 28 | RUNTIME=osx-arm64 29 | endif 30 | endif 31 | SHELL := /bin/sh 32 | MKDIR_CMD := mkdir -p $(OUTPUT_DIR) 33 | endif 34 | 35 | # Publish target 36 | publish: 37 | $(MKDIR_CMD) 38 | dotnet publish ../../src/Sapling/Sapling.csproj -c Release --runtime $(RUNTIME) --self-contained \ 39 | -p:PublishSingleFile=true -p:DeterministicBuild=true \ 40 | -o $(OUTPUT_DIR) -p:ExecutableName=$(EXE) -------------------------------------------------------------------------------- /OpenBench/Tune/Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := publish 2 | 3 | ifndef EXE 4 | EXE = Sapling 5 | endif 6 | 7 | # Set a default output directory if not already defined 8 | OUTPUT_DIR ?= ./ 9 | 10 | # Detect OS and Architecture 11 | UNAME_S := $(shell uname -s) 12 | UNAME_P := $(shell uname -p) 13 | 14 | ifeq ($(OS),Windows_NT) 15 | RUNTIME=win-x64 16 | SHELL := cmd.exe 17 | MKDIR_CMD := if not exist "$(subst /,\,$(OUTPUT_DIR))" mkdir "$(subst /,\,$(OUTPUT_DIR))" 18 | else 19 | # Default runtime for Linux/MacOS 20 | ifeq ($(UNAME_S),Linux) 21 | RUNTIME=linux-x64 22 | ifneq ($(filter aarch64% armv8% arm%,$(UNAME_P)),) 23 | RUNTIME=linux-arm64 24 | endif 25 | else ifeq ($(UNAME_S),Darwin) 26 | RUNTIME=osx-x64 27 | ifneq ($(filter arm%,$(UNAME_P)),) 28 | RUNTIME=osx-arm64 29 | endif 30 | endif 31 | SHELL := /bin/sh 32 | MKDIR_CMD := mkdir -p $(OUTPUT_DIR) 33 | endif 34 | 35 | # Publish target 36 | publish: 37 | $(MKDIR_CMD) 38 | dotnet publish ../../src/Sapling/Sapling.csproj -c Release --runtime $(RUNTIME) --self-contained \ 39 | -p:PublishSingleFile=true -p:DeterministicBuild=true \ 40 | -o $(OUTPUT_DIR) -p:ExecutableName=$(EXE) -p:DefineConstants="OpenBench" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 |
5 |

6 | 7 | ### A strong dotnet UCI Chess engine - My leaf nodes are growing 8 | 9 | Play it here -> https://iblunder.com/ 10 | 11 | Or challenge it on Lichess -> https://lichess.org/@/sapling-bot 12 | 13 | 14 | | Rating pool | Version | ELO | 15 | |-------------------------------------------------------------------------------|---------|------| 16 | | [CCRL 40/15](https://www.computerchess.org.uk/ccrl/4040/rating_list_all.html) | 1.1.2 | 3378 | 17 | | [Lichess Bullet](https://lichess.org/@/Sapling-Bot/perf/bullet) | 1.2.2 | 2890 | 18 | | [Lichess Blitz](https://lichess.org/@/Sapling-Bot/perf/blitz) | 1.2.2 | 2786 | 19 | | [Lichess Rapid](https://lichess.org/@/Sapling-Bot/perf/rapid) | 1.2.2 | 2797 | 20 | | [Lichess Chess960](https://lichess.org/@/Sapling-Bot/perf/chess960) | 1.2.2 | 2209 | 21 | 22 | | Tournament | Version | Result | 23 | |----------------------------------------------------------------------------------------------------|---------|------------------| 24 | | [CCRL 112th Amateur Series Division 5](https://talkchess.com/viewtopic.php?p=969661&hilit=sapling) | 1.1.8 | drawed 3rd place | 25 | | [Blitz Tournament 3'+2" (48th Edition)](https://talkchess.com/viewtopic.php?t=84301&hilit=sapling) | 1.2.0 | Division 4 6th place | 26 | | [FRC Tournament (35th Edition)](https://talkchess.com/viewtopic.php?p=970724&hilit=sapling#p970724) | 1.2.2 | TBD | 27 | 28 | ## Releases 29 | You can browse all windows, linux or mac releases [here](https://github.com/Timmoth/Sapling/releases) 30 | 31 | ### Latest Release [v1.2.4 21/04/2025](https://github.com/Timmoth/Sapling/releases/tag/Sapling-1.2.4) 32 | 33 | ## Requirements 34 | - Sapling makes use of hardware intrinsics to improve performance. Currently your CPU must support: `Avx2`, `Bmi1`, `Bmi2`, `Popcnt`, `Sse`. Most modern hardware shipped after 2013 should be supported. 35 | - The releases come with a bundled version of the dotnet runtime, however if you want to run from source you'll need the dotnet 9 SDK installed. 36 | 37 | ## Running from source 38 | ```bash 39 | cd ./scripts 40 | 41 | // Windows 42 | ./build_windows_avx256.bat 43 | ./build_windows_avx512.bat 44 | 45 | // Linux 46 | ./build_linux_avx256.sh 47 | ./build_linux_avx512.sh 48 | 49 | // Osx 50 | ./build_osx_avx256.sh 51 | ./build_osx_avx512.sh 52 | ``` 53 | 54 | ## Commands 55 | - `quit` : exit the program 56 | - `setoption name threads value 8` : sets the number of threads to use 57 | - `ucinewgame` : initializes a new game 58 | - `position startpos` : sets the engine to the starting chess position 59 | - `position startpos moves a2a3 a7a6` : sets the engine to the starting position then applies a set of moves 60 | - `position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1` : sets the engine to the position given by the fen string 61 | - `d` : outputs a diagram of the current position 62 | - `go perft 6` : Runs a pert test to a specific depth 63 | - `go depth 10` : Returns the best move after searching for the given depth 64 | - `go wtime 10000 btime 10000 winc 100 binc 100` : returns the best move after searching with the given time control 65 | - `go see a2a3` : [Dev] returns a the result of static exchange evaluation for a given move 66 | - `go eval` : [Dev] returns the static evaluation of the current position 67 | - `datagen` : [Dev] starts generating data in the bullet format, used when training a new NNUE network 68 | 69 | ## Features 70 | 71 |
72 | General 73 | 74 | - Bitboards 75 | - NNUE (768 -> 1024)x2 -> 8 76 | - Horizontal mirroring 77 | - Output buckets x8 78 | - Transposition table 79 | - Lazy SMP 80 | - Pondering 81 |
82 | 83 |
84 | Search 85 | 86 | - Negamax 87 | - Quiescence 88 | - Alpha-Beta pruning 89 | - Iterative Deepening 90 | - Asperation windows 91 | - Null move pruning 92 | - Late Move Pruning 93 | - Futility Pruning 94 | - Razoring 95 | - Principal Variation Search 96 | - Check extensions 97 | - Internal Iterative Reduction 98 | - Late Move Reductions 99 | - Cuckoo filter repetition detection 100 |
101 | 102 |
103 | Move generation / ordering 104 | 105 | - Pseudo-legal movegen 106 | - Static exchange evaluation 107 | - Killer move heuristic 108 | - Counter move heuristic 109 | - History heuristic with malus 110 | - Incremental sorting 111 | - Magic bitboards 112 | - PEXT bitboards 113 |
114 | 115 | ## SPRT 116 | After any changes to the engine a SPRT test must be ran to ensure that the changes have a positive effect. 117 | 118 | There is a script `sprt.bat` which contains the command to run a cutechess-cli SPRT test. Ensure that you've configured CuteChess to point to both `dev` and `base` engines before hand, and also update the opening book + endgame table base to point to one on your system. 119 | 120 | ## NNUE 121 | I'm in the process of training a (768x8->1024)x2-8 network starting from random weights using self play data generation and bullet trainer. Expect the engine to get much stronger as I improve the network. Check [here](https://github.com/Timmoth/Sapling/tree/main/Sapling.Engine/Resources/WeightsHistory) to see the sequence of networks starting from scratch and the training logs. 122 | 123 | ## Resources: 124 | - [Chess Programming Wiki](https://www.chessprogramming.org/) 125 | - [Talk Chess Forum](https://talkchess.com/) 126 | - [Coding Adventure](https://www.youtube.com/watch?v=U4ogK0MIzqk) 127 | -------------------------------------------------------------------------------- /Scripts/build_linux_avx256.sh: -------------------------------------------------------------------------------- 1 | dotnet publish ../src/Sapling/Sapling.csproj -c Release --runtime linux-x64 --self-contained -p:PublishSingleFile=true -p:DeterministicBuild=true -o ./ -p:ExecutableName=Sapling_linux_avx256 -------------------------------------------------------------------------------- /Scripts/build_linux_avx512.sh: -------------------------------------------------------------------------------- 1 | dotnet publish ../src/Sapling/Sapling.csproj -c Release --runtime linux-x64 --self-contained -p:PublishSingleFile=true -p:DeterministicBuild=true -o ./ -p:ExecutableName=Sapling_linux_avx512 -p:DefineConstants="AVX512" -------------------------------------------------------------------------------- /Scripts/build_osx_avx256.sh: -------------------------------------------------------------------------------- 1 | dotnet publish ../src/Sapling/Sapling.csproj -c Release --runtime osx-x64 --self-contained -p:PublishSingleFile=true -p:DeterministicBuild=true -o ./ -p:ExecutableName=Sapling_osx_avx256 -------------------------------------------------------------------------------- /Scripts/build_osx_avx512.sh: -------------------------------------------------------------------------------- 1 | dotnet publish ../src/Sapling/Sapling.csproj -c Release --runtime osx-x64 --self-contained -p:PublishSingleFile=true -p:DeterministicBuild=true -o ./ -p:ExecutableName=Sapling_osx_avx512 -p:DefineConstants="AVX512" -------------------------------------------------------------------------------- /Scripts/build_windows_avx256.bat: -------------------------------------------------------------------------------- 1 | dotnet publish ../src/Sapling/Sapling.csproj -c Release --runtime win-x64 --self-contained -p:PublishSingleFile=true -p:DeterministicBuild=true -o ./ -p:ExecutableName=Sapling_win_avx256 -------------------------------------------------------------------------------- /Scripts/build_windows_avx512.bat: -------------------------------------------------------------------------------- 1 | dotnet publish ../src/Sapling/Sapling.csproj -c Release --runtime win-x64 --self-contained -p:PublishSingleFile=true -p:DeterministicBuild=true -o ./ -p:ExecutableName=Sapling_win_avx512 -p:DefineConstants="AVX512" -------------------------------------------------------------------------------- /Scripts/sprt.bat: -------------------------------------------------------------------------------- 1 | cutechess-cli ^ 2 | -engine conf=dev name="Dev" ^ 3 | -engine conf=base name="Base" ^ 4 | -each tc=10+0.1 restart=on timemargin=10 ^ 5 | -games 2500 -repeat 2 -resultformat wide -recover -wait 20 ^ 6 | -maxmoves 200 7 | -ratinginterval 10 -variant standard -concurrency 8 ^ 8 | -sprt elo0=0 elo1=10 alpha=0.05 beta=0.05 ^ 9 | -event sprt-test -pgnout "./sprt.pgn" -site "UK" -tournament round-robin 10 | -openings file=".\engines\Opening\Cerebellum3Merge.bin" format=pgn ^ 11 | -tb "./syzygy/3-4-5" -tbpieces 5 ^ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sapling-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/sapling-banner.png -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ### A strong dotnet UCI Chess engine - My leaf nodes are growing 2 | 3 | Play it here -> https://iblunder.com/ 4 | 5 | Or challenge it on Lichess -> https://lichess.org/@/sapling-bot 6 | 7 | ## Releases 8 | You can browse all windows, linux or mac releases [here](https://github.com/Timmoth/Sapling/releases) 9 | 10 | ## Requirements 11 | - Sapling makes use of hardware intrinsics to improve performance. Currently your CPU must support: `Avx2`, `Bmi1`, `Bmi2`, `Popcnt`, `Sse`. Most modern hardware shipped after 2013 should be supported. 12 | - The releases come with a bundled version of the dotnet runtime, however if you want to run from source you'll need the dotnet 8 SDK installed. 13 | 14 | ## Running from source 15 | ```bash 16 | dotnet run --project .\Sapling\Sapling.csproj --configuration Release 17 | ``` 18 | 19 | ## Commands 20 | - `quit` : exit the program 21 | - `setoption name threads value 8` : sets the number of threads to use 22 | - `ucinewgame` : initializes a new game 23 | - `position startpos` : sets the engine to the starting chess position 24 | - `position startpos moves a2a3 a7a6` : sets the engine to the starting position then applies a set of moves 25 | - `position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1` : sets the engine to the position given by the fen string 26 | - `d` : outputs a diagram of the current position 27 | - `go perft 6` : Runs a pert test to a specific depth 28 | - `go depth 10` : Returns the best move after searching for the given depth 29 | - `go wtime 10000 btime 10000 winc 100 binc 100` : returns the best move after searching with the given time control 30 | - `go see a2a3` : [Dev] returns a the result of static exchange evaluation for a given move 31 | - `go eval` : [Dev] returns the static evaluation of the current position 32 | - `datagen` : [Dev] starts generating data in the bullet format, used when training a new NNUE network 33 | 34 | ## Features 35 | 36 |
37 | General 38 | 39 | - Bitboards 40 | - NNUE (768 -> 1024)x2 -> 8 41 | - Horizontal mirroring 42 | - Output buckets x8 43 | - Transposition table 44 | - Lazy SMP 45 | - Pondering 46 |
47 | 48 |
49 | Search 50 | 51 | - Negamax 52 | - Quiescence 53 | - Alpha-Beta pruning 54 | - Iterative Deepening 55 | - Asperation windows 56 | - Null move pruning 57 | - Late Move Pruning 58 | - Futility Pruning 59 | - Razoring 60 | - Principal Variation Search 61 | - Check extensions 62 | - Internal Iterative Reduction 63 | - Late Move Reductions 64 | - Cuckoo filter repetition detection 65 |
66 | 67 |
68 | Move generation / ordering 69 | 70 | - Pseudo-legal movegen 71 | - Static exchange evaluation 72 | - Killer move heuristic 73 | - Counter move heuristic 74 | - History heuristic with malus 75 | - Incremental sorting 76 | - Magic bitboards 77 | - PEXT bitboards 78 |
79 | 80 | ## SPRT 81 | After any changes to the engine a SPRT test must be ran to ensure that the changes have a positive effect. 82 | 83 | There is a script `sprt.bat` which contains the command to run a cutechess-cli SPRT test. Ensure that you've configured CuteChess to point to both `dev` and `base` engines before hand, and also update the opening book + endgame table base to point to one on your system. 84 | 85 | ## NNUE 86 | I'm in the process of training a (768x8->1024)x2-8 network starting from random weights using self play data generation and bullet trainer. Expect the engine to get much stronger as I improve the network. Check [here](https://github.com/Timmoth/Sapling/tree/main/Sapling.Engine/Resources/WeightsHistory) to see the sequence of networks starting from scratch and the training logs. 87 | 88 | ## Resources: 89 | - [Chess Programming Wiki](https://www.chessprogramming.org/) 90 | - [Talk Chess Forum](https://talkchess.com/) 91 | - [Coding Adventure](https://www.youtube.com/watch?v=U4ogK0MIzqk) 92 | -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/BitboardHelperTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | 3 | namespace Sapling.Engine.Tests; 4 | 5 | public class BitboardHelperTests 6 | { 7 | [Fact] 8 | public void ShiftTests() 9 | { 10 | for (var rank = 0; rank < 8; rank++) 11 | { 12 | for (var file = 0; file < 8; file++) 13 | { 14 | var bitboard = 1UL << (rank * 8 + file); 15 | var canMoveUp = rank < 7; 16 | var canMoveDown = rank > 0; 17 | var canMoveLeft = file > 0; 18 | var canMoveRight = file < 7; 19 | 20 | var leftSquare = canMoveLeft ? BitboardHelpers.RankFileToBitboard(rank, file - 1) : 0; 21 | var rightSquare = canMoveRight ? BitboardHelpers.RankFileToBitboard(rank, file + 1) : 0; 22 | var upLeft = canMoveUp && canMoveLeft ? BitboardHelpers.RankFileToBitboard(rank + 1, file - 1) : 0; 23 | var upSquare = canMoveUp ? BitboardHelpers.RankFileToBitboard(rank + 1, file) : 0; 24 | var upRight = canMoveUp && canMoveRight ? BitboardHelpers.RankFileToBitboard(rank + 1, file + 1) : 0; 25 | var downLeft = canMoveDown && canMoveLeft ? BitboardHelpers.RankFileToBitboard(rank - 1, file - 1) : 0; 26 | var downSquare = canMoveDown ? BitboardHelpers.RankFileToBitboard(rank - 1, file) : 0; 27 | var downRight = canMoveDown && canMoveRight 28 | ? BitboardHelpers.RankFileToBitboard(rank - 1, file + 1) 29 | : 0; 30 | 31 | bitboard.ShiftUp().PeekLSB().Should().Be(upSquare.PeekLSB()); 32 | bitboard.ShiftDown().PeekLSB().Should().Be(downSquare.PeekLSB()); 33 | bitboard.ShiftLeft().PeekLSB().Should().Be(leftSquare.PeekLSB()); 34 | bitboard.ShiftRight().PeekLSB().Should().Be(rightSquare.PeekLSB()); 35 | bitboard.ShiftUpLeft().PeekLSB().Should().Be(upLeft.PeekLSB()); 36 | bitboard.ShiftUpRight().PeekLSB().Should().Be(upRight.PeekLSB()); 37 | bitboard.ShiftDownLeft().PeekLSB().Should().Be(downLeft.PeekLSB()); 38 | bitboard.ShiftDownRight().PeekLSB().Should().Be(downRight.PeekLSB()); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/InsufficientMaterialTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | 3 | namespace Sapling.Engine.Tests; 4 | 5 | public class InsufficientMaterialTests 6 | { 7 | [Theory] 8 | [InlineData("7K/8/8/8/8/2p5/3P4/7k b - - 0 1", false)] 9 | [InlineData("7K/8/8/8/8/8/8/7k w - - 0 1", true)] 10 | [InlineData("7K/8/5n2/6N1/8/8/8/7k w - - 0 1", true)] 11 | [InlineData("7K/8/5n2/6N1/4B3/8/8/7k w - - 0 1", false)] 12 | [InlineData("7K/8/6B1/8/8/8/8/7k w - - 0 1", true)] 13 | [InlineData("7K/4b3/6B1/8/8/8/8/7k w - - 0 1", true)] 14 | [InlineData("7K/8/4N3/5N2/8/8/8/7k w - - 0 1", true)] 15 | [InlineData("8/8/8/7K/8/3k4/8/8 w - - 0 157", true)] 16 | [InlineData("5B2/8/8/8/2K5/8/8/2k5 b - - 0 176", true)] 17 | [InlineData("8/b7/8/8/7K/8/2k5/8 w - - 0 57", true)] 18 | [InlineData("8/8/8/8/8/6k1/NK6/8 b - - 0 96", true)] 19 | public void ApplyUnapplyMatchesInitialState(string fen, bool isInsufficientMaterial) 20 | { 21 | var board = BoardStateExtensions.CreateBoardFromFen(fen); 22 | board.InsufficientMatingMaterial().Should().Be(isInsufficientMaterial); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/MoveTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Sapling.Engine.MoveGen; 3 | 4 | namespace Sapling.Engine.Tests; 5 | 6 | public class MoveTests 7 | { 8 | [Theory] 9 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.Normal)] 10 | [InlineData(Piece.WhiteKing, 0, Piece.None, 0, MoveType.Normal)] 11 | [InlineData(Piece.None, 2, Piece.None, 0, MoveType.Normal)] 12 | [InlineData(Piece.None, 0, Piece.BlackKing, 0, MoveType.Normal)] 13 | [InlineData(Piece.None, 0, Piece.None, 4, MoveType.Normal)] 14 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.EnPassant)] 15 | [InlineData(Piece.WhiteKing, 2, Piece.BlackKnight, 3, MoveType.EnPassant)] 16 | [InlineData(Piece.WhiteKing, 2, Piece.BlackKnight, 3, MoveType.PawnBishopPromotion)] 17 | public void GetKingPosition_Returns_CorrectPosition(Piece movedPiece, byte fromSquare, Piece capturedPiece, 18 | byte toSquare, MoveType moveType) 19 | { 20 | var move = MoveExtensions.EncodeCapturePromotionMove((byte)movedPiece, fromSquare, (byte)capturedPiece, 21 | toSquare, 22 | (byte)moveType); 23 | move.GetMovedPiece().Should().Be((byte)movedPiece); 24 | move.GetFromSquare().Should().Be(fromSquare); 25 | move.GetCapturedPiece().Should().Be((byte)capturedPiece); 26 | move.GetToSquare().Should().Be(toSquare); 27 | move.GetMoveType().Should().Be((byte)moveType); 28 | } 29 | 30 | [Theory] 31 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.Normal, true)] 32 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.Castle, true)] 33 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.EnPassant, true)] 34 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.DoublePush, true)] 35 | [InlineData(Piece.WhiteQueen, 0, Piece.BlackQueen, 0, MoveType.Normal, false)] 36 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.PawnBishopPromotion, false)] 37 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.PawnRookPromotion, false)] 38 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.PawnKnightPromotion, false)] 39 | [InlineData(Piece.None, 0, Piece.None, 0, MoveType.PawnQueenPromotion, false)] 40 | [InlineData(Piece.WhitePawn, 0, Piece.BlackPawn, 0, MoveType.PawnQueenPromotion, false)] 41 | public void Is_Quiet(Piece movedPiece, byte fromSquare, Piece capturedPiece, byte toSquare, MoveType moveType, 42 | bool expected) 43 | { 44 | var move = MoveExtensions.EncodeCapturePromotionMove((byte)movedPiece, fromSquare, (byte)capturedPiece, 45 | toSquare, 46 | (byte)moveType); 47 | move.IsQuiet().Should().Be(expected); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/NNUETests.cs: -------------------------------------------------------------------------------- 1 | //using FluentAssertions; 2 | //using Sapling.Engine.Evaluation; 3 | //using Sapling.Engine.MoveGen; 4 | //using Sapling.Engine.Search; 5 | //using Sapling.Engine.Transpositions; 6 | 7 | //namespace Sapling.Engine.Tests; 8 | 9 | //public class NNUETests 10 | //{ 11 | // [Theory] 12 | // [InlineData( 13 | // "c2c3 c7c6 a2a4 d7d5 g1f3 e7e6 h2h4 g8e7 e2e4 d5e4 b1a3 e4f3 g2g4 h7h5 g4g5 e7g6 d1f3 f8a3 a1a3 e6e5 a3b3 e8g8 a4a5 d8a5 f1g2 b8d7 f3h5 d7c5 h5d1 c5d3 e1f1 a5c5 d1f3 d3c1 b3b4 c1a2 d2d4 e5d4 b4d4 a2c1 g2h3 c8h3 h1h3 a8d8 d4b4 c1b3 f3e2 f8e8 e2g4 b3d2 f1g1 a7a5 b4d4 d8d4 g4d4 c5d4 c3d4 e8e1 g1g2 g6f4 g2g3 f4h3 f2f4 e1e3 g3h2 e3b3 f4f5 a5a4 f5f6 g7g6 h4h5 g6h5 g5g6 f7g6 d4d5 c6d5 f6f7 g8f8 h2g2 h5h4 g2h1 b3g3 h1h2 d2f1 h2h1 h3f2", 14 | // "5k2/1p3P2/6p1/3p4/p6p/6r1/1P3n2/5n1K w - - 6 87")] 15 | // [InlineData( 16 | // "b2b3 c7c6 g1f3 g7g6 c1b2 g8f6 b1c3 d7d5 e2e3 f8g7 f1d3 e8g8 e1g1 b8d7 a2a4 d7c5 d3e2 a7a5 d2d4 c5a6 e2a6 a8a6 f3e5 a6a8 f1e1 d8c7 c3e2 f6e8 h2h4 g7e5 d4e5 c6c5 d1d5 c8e6 d5f3 a8d8 e2f4 c7b6 b2c3 e8c7 f4e6 c7e6 e1b1 g8g7 b1b2 g6g5 b3b4 c5b4 a1b1 b6c7 c3e1 g5h4 f3g4 g7h8 e1b4 a5b4 b2b4 c7c2 b4b2 c2c5 g4h4 f8g8 b2b7 g8g5 g2g3 h8g7 h4b4 g5e5 b4c5 e6c5 b7b5 d8d6 a4a5 c5d7 b5e5 d7e5 b1b7 d6a6 b7e7 a6a5 e7b7 h7h5 b7b2 g7g6 e3e4 a5a3 b2b8 g6g7 b8b7 e5g4 b7b2 a3c3 g1g2 c3c4 g2h3 c4a4 f2f3 g4e5 b2b5 e5f3 b5h5 a4e4 h5a5 g7g6 h3g2 f3g5 g2f2 g6h5 a5a8 h5g4 a8a3 e4b4 f2e2 f7f5 a3a6 g4g3 a6a3 g3g2 a3a5 g5f3 e2d3 f5f4 a5a2 g2g3 a2a5 b4d4 d3c3 g3f2 a5a1 f2e2 a1a2 d4d2 a2a8 d2d3 c3b2 f3d4 a8e8 e2d2", 17 | // "4R3/8/8/8/3n1p2/3r4/1K1k4/8 w - - 16 139")] 18 | // public void ApplyMatchesFen(string moves, string fen) 19 | // { 20 | // // Given 21 | // var moveList = moves.Split(' '); 22 | // var board = BoardStateExtensions.CreateBoardFromFen(fen); 23 | 24 | // // When 25 | // Apply(board, moveList, 0); 26 | 27 | // // Then 28 | // board.Data.ToFen().Should().Be(fen); 29 | // } 30 | 31 | 32 | // public unsafe void Apply(ref BoardStateData board, string[] moves, int moveIndex) 33 | // { 34 | // if (moveIndex >= moves.Length) 35 | // { 36 | // return; 37 | // } 38 | 39 | // var oldEnpassant = board.EnPassantFile; 40 | // var prevCastleRights = board.CastleRights; 41 | 42 | 43 | // var validMoves = new List(); 44 | // board.GenerateLegalMoves(validMoves, false); 45 | // var moveString = moves[moveIndex]; 46 | 47 | // var move = validMoves.FirstOrDefault(m => m.ToUciMoveName() == moveString); 48 | // board.PartialApply(move); 49 | // board.UpdateCheckStatus(); 50 | // var hashHistory = stackalloc ulong[100]; 51 | // board.FinishApply(board.WhiteAccumulator, board.BlackAccumulator, hashHistory, move, oldEnpassant, prevCastleRights); 52 | // Apply(board, moves, moveIndex + 1); 53 | // } 54 | //} -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/PerftTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Sapling.Engine.Search; 3 | 4 | namespace Sapling.Engine.Tests; 5 | 6 | public class PerftTests 7 | { 8 | [Theory] 9 | [InlineData("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 5, 4_865_609)] 10 | [InlineData("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -", 4, 4_085_603)] 11 | [InlineData("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -", 5, 674_624)] 12 | [InlineData("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1", 4, 422_333)] 13 | [InlineData("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", 4, 2_103_487)] 14 | [InlineData("r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10", 4, 3_894_594)] 15 | public void PerftResults(string fen, int depth, ulong expectedNodes) 16 | { 17 | // Given 18 | var board = BoardStateExtensions.CreateBoardFromFen(fen); 19 | 20 | // When 21 | var result = board.PerftRootSequential(depth); 22 | 23 | // Then 24 | ulong sum = 0; 25 | foreach (var (nodes, move) in result) 26 | { 27 | sum += nodes; 28 | } 29 | 30 | sum.Should().Be(expectedNodes); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/PgnTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Sapling.Engine.Pgn; 3 | 4 | namespace Sapling.Engine.Tests; 5 | 6 | public class PgnTests 7 | { 8 | [Theory] 9 | [InlineData(0, "a1")] 10 | [InlineData(1, "b1")] 11 | [InlineData(2, "c1")] 12 | [InlineData(3, "d1")] 13 | [InlineData(4, "e1")] 14 | [InlineData(5, "f1")] 15 | [InlineData(6, "g1")] 16 | [InlineData(7, "h1")] 17 | [InlineData(8, "a2")] 18 | [InlineData(9, "b2")] 19 | [InlineData(10, "c2")] 20 | [InlineData(11, "d2")] 21 | [InlineData(12, "e2")] 22 | [InlineData(13, "f2")] 23 | [InlineData(14, "g2")] 24 | [InlineData(15, "h2")] 25 | [InlineData(16, "a3")] 26 | [InlineData(17, "b3")] 27 | [InlineData(18, "c3")] 28 | [InlineData(19, "d3")] 29 | [InlineData(20, "e3")] 30 | [InlineData(21, "f3")] 31 | [InlineData(22, "g3")] 32 | [InlineData(23, "h3")] 33 | [InlineData(24, "a4")] 34 | [InlineData(25, "b4")] 35 | [InlineData(26, "c4")] 36 | [InlineData(27, "d4")] 37 | [InlineData(28, "e4")] 38 | [InlineData(29, "f4")] 39 | [InlineData(30, "g4")] 40 | [InlineData(31, "h4")] 41 | [InlineData(32, "a5")] 42 | [InlineData(33, "b5")] 43 | [InlineData(34, "c5")] 44 | [InlineData(35, "d5")] 45 | [InlineData(36, "e5")] 46 | [InlineData(37, "f5")] 47 | [InlineData(38, "g5")] 48 | [InlineData(39, "h5")] 49 | [InlineData(40, "a6")] 50 | [InlineData(41, "b6")] 51 | [InlineData(42, "c6")] 52 | [InlineData(43, "d6")] 53 | [InlineData(44, "e6")] 54 | [InlineData(45, "f6")] 55 | [InlineData(46, "g6")] 56 | [InlineData(47, "h6")] 57 | [InlineData(48, "a7")] 58 | [InlineData(49, "b7")] 59 | [InlineData(50, "c7")] 60 | [InlineData(51, "d7")] 61 | [InlineData(52, "e7")] 62 | [InlineData(53, "f7")] 63 | [InlineData(54, "g7")] 64 | [InlineData(55, "h7")] 65 | [InlineData(56, "a8")] 66 | [InlineData(57, "b8")] 67 | [InlineData(58, "c8")] 68 | [InlineData(59, "d8")] 69 | [InlineData(60, "e8")] 70 | [InlineData(61, "f8")] 71 | [InlineData(62, "g8")] 72 | [InlineData(63, "h8")] 73 | public void ConvertPosition(byte position, string expected) 74 | { 75 | PgnSplitter.ConvertPosition(position).Should().Be(expected); 76 | } 77 | } -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/PromotionThreatTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Sapling.Engine.MoveGen; 3 | using Sapling.Engine.Search; 4 | using Sapling.Engine.Transpositions; 5 | 6 | namespace Sapling.Engine.Tests; 7 | 8 | public class PromotionThreatTests 9 | { 10 | [Theory] 11 | [InlineData("7K/8/8/8/8/8/2p5/7k b - -", "c2c1b", true)] 12 | [InlineData("7K/8/8/8/8/2p5/3P4/7k b - - 0 1", "c3c2", true)] 13 | public void IsPromotionThreat_Tests(string fen, string uciMove, bool expected) 14 | { 15 | // Given 16 | var board = BoardStateExtensions.CreateBoardFromFen(fen); 17 | var moves = new List(); 18 | board.GenerateLegalMoves(moves, false); 19 | 20 | var move = Assert.Single(moves.Where(m => m.ToUciMoveName() == uciMove)); 21 | 22 | // When 23 | var actual = move.IsPromotionThreat(); 24 | 25 | // Then 26 | actual.Should().Be(expected); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/Sapling.Engine.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | true 11 | AnyCPU;x64 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/SquareHelperTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | 3 | namespace Sapling.Engine.Tests; 4 | 5 | public class SquareHelperTests 6 | { 7 | [Fact] 8 | public void ShiftTests() 9 | { 10 | for (var rank = 0; rank < 8; rank++) 11 | { 12 | for (var file = 0; file < 8; file++) 13 | { 14 | var square = (byte)(rank * 8 + file); 15 | 16 | var leftSquare = (byte)(square - 1); 17 | var rightSquare = (byte)(square + 1); 18 | var upLeft = (byte)(square + 8 - 1); 19 | var upSquare = (byte)(square + 8); 20 | var upRight = (byte)(square + 8 + 1); 21 | var downLeft = (byte)(square - 8 - 1); 22 | var downSquare = (byte)(square - 8); 23 | var downRight = (byte)(square - 8 + 1); 24 | 25 | square.ShiftUp().Should().Be(upSquare); 26 | square.ShiftDown().Should().Be(downSquare); 27 | square.ShiftLeft().Should().Be(leftSquare); 28 | square.ShiftRight().Should().Be(rightSquare); 29 | square.ShiftUpLeft().Should().Be(upLeft); 30 | square.ShiftUpRight().Should().Be(upRight); 31 | square.ShiftDownLeft().Should().Be(downLeft); 32 | square.ShiftDownRight().Should().Be(downRight); 33 | } 34 | } 35 | } 36 | 37 | [Theory] 38 | [InlineData(0, 0, 0)] 39 | [InlineData(1, 0, 1)] 40 | [InlineData(2, 0, 2)] 41 | [InlineData(3, 0, 3)] 42 | [InlineData(4, 0, 4)] 43 | [InlineData(5, 0, 5)] 44 | [InlineData(6, 0, 6)] 45 | [InlineData(7, 0, 7)] 46 | [InlineData(8, 1, 0)] 47 | [InlineData(9, 1, 1)] 48 | [InlineData(10, 1, 2)] 49 | [InlineData(11, 1, 3)] 50 | [InlineData(12, 1, 4)] 51 | [InlineData(13, 1, 5)] 52 | [InlineData(14, 1, 6)] 53 | [InlineData(15, 1, 7)] 54 | [InlineData(16, 2, 0)] 55 | [InlineData(17, 2, 1)] 56 | [InlineData(18, 2, 2)] 57 | [InlineData(19, 2, 3)] 58 | [InlineData(20, 2, 4)] 59 | [InlineData(21, 2, 5)] 60 | [InlineData(22, 2, 6)] 61 | [InlineData(23, 2, 7)] 62 | [InlineData(24, 3, 0)] 63 | [InlineData(25, 3, 1)] 64 | [InlineData(26, 3, 2)] 65 | [InlineData(27, 3, 3)] 66 | [InlineData(28, 3, 4)] 67 | [InlineData(29, 3, 5)] 68 | [InlineData(30, 3, 6)] 69 | [InlineData(31, 3, 7)] 70 | [InlineData(32, 4, 0)] 71 | [InlineData(33, 4, 1)] 72 | [InlineData(34, 4, 2)] 73 | [InlineData(35, 4, 3)] 74 | [InlineData(36, 4, 4)] 75 | [InlineData(37, 4, 5)] 76 | [InlineData(38, 4, 6)] 77 | [InlineData(39, 4, 7)] 78 | [InlineData(40, 5, 0)] 79 | [InlineData(41, 5, 1)] 80 | [InlineData(42, 5, 2)] 81 | [InlineData(43, 5, 3)] 82 | [InlineData(44, 5, 4)] 83 | [InlineData(45, 5, 5)] 84 | [InlineData(46, 5, 6)] 85 | [InlineData(47, 5, 7)] 86 | [InlineData(48, 6, 0)] 87 | [InlineData(49, 6, 1)] 88 | [InlineData(50, 6, 2)] 89 | [InlineData(51, 6, 3)] 90 | [InlineData(52, 6, 4)] 91 | [InlineData(53, 6, 5)] 92 | [InlineData(54, 6, 6)] 93 | [InlineData(55, 6, 7)] 94 | [InlineData(56, 7, 0)] 95 | [InlineData(57, 7, 1)] 96 | [InlineData(58, 7, 2)] 97 | [InlineData(59, 7, 3)] 98 | [InlineData(60, 7, 4)] 99 | [InlineData(61, 7, 5)] 100 | [InlineData(62, 7, 6)] 101 | [InlineData(63, 7, 7)] 102 | public void GetSquareRankAndFile(int square, int rank, int file) 103 | { 104 | square.GetRankIndex().Should().Be(rank); 105 | square.GetFileIndex().Should().Be(file); 106 | } 107 | } -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/StaticExchangeTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Sapling.Engine.Evaluation; 3 | using Sapling.Engine.MoveGen; 4 | using Sapling.Engine.Search; 5 | using Sapling.Engine.Transpositions; 6 | 7 | namespace Sapling.Engine.Tests; 8 | 9 | public class StaticExchangeTests 10 | { 11 | [Theory] 12 | [InlineData("5rk1/1pp2q1p/p1pb4/8/3P1NP1/2P5/1P1BQ1P1/5RK1 b - -", "d6f4", "Nb")] 13 | [InlineData("2r1r1k1/pp1bppbp/3p1np1/q3P3/2P2P2/1P2B3/P1N1B1PP/2RQ1RK1 b - -", "d6e5", "PpP")] 14 | [InlineData("4q3/1p1pr1k1/1B2rp2/6p1/p3PP2/P3R1P1/1P2R1K1/4Q3 b - -", "e6e4", "PrRr")] 15 | //[InlineData("3r3k/3r4/2n1n3/8/3p4/2PR4/1B1Q4/3R3K w - -", "d3d4", "pRnPnB")] 16 | [InlineData("3N4/2K5/2n5/1k6/8/8/8/8 b - -", "c6d8", "nN")] 17 | [InlineData("r1bqkb1r/2pp1ppp/p1n5/1p2p3/3Pn3/1B3N2/PPP2PPP/RNBQ1RK1 b kq -", "c6d4", "PnNp")] 18 | [InlineData("6k1/1pp4p/p1pb4/6q1/3P1pRr/2P4P/PP1Br1P1/5RKN w - -", "f1f4", "pR")] 19 | [InlineData("4R3/2r3p1/5bk1/1p1r3p/p2PR1P1/P1BK1P2/1P6/8 b - -", "h5g4", "Pp")] 20 | public unsafe void GetKingPosition_Returns_CorrectPosition(string fen, string uciMove, string exchange) 21 | { 22 | // Given 23 | var board = BoardStateExtensions.CreateBoardFromFen(fen); 24 | 25 | var moves = new List(); 26 | board.GenerateLegalMoves(moves, true); 27 | var move = Assert.Single(moves.Where(m => m.ToUciMoveName() == uciMove)); 28 | 29 | var occupancyBitBoards = stackalloc ulong[8] 30 | { 31 | board.Occupancy[Constants.WhitePieces], 32 | board.Occupancy[Constants.BlackPieces], 33 | board.Occupancy[Constants.BlackPawn] | board.Occupancy[Constants.WhitePawn], 34 | board.Occupancy[Constants.BlackKnight] | board.Occupancy[Constants.WhiteKnight], 35 | board.Occupancy[Constants.BlackBishop] | board.Occupancy[Constants.WhiteBishop], 36 | board.Occupancy[Constants.BlackRook] | board.Occupancy[Constants.WhiteRook], 37 | board.Occupancy[Constants.BlackQueen] | board.Occupancy[Constants.WhiteQueen], 38 | board.Occupancy[Constants.BlackKing] | board.Occupancy[Constants.WhiteKing] 39 | }; 40 | 41 | var captures = stackalloc short[32]; 42 | 43 | // When 44 | var seeScore = board.StaticExchangeEvaluation(occupancyBitBoards, captures, move); 45 | 46 | // Then 47 | 48 | var expected = 0; 49 | for (var i = 0; i < exchange.Length; i++) 50 | { 51 | var v = PieceValues.PieceValue[ChatToPiece(exchange[i])]; 52 | expected += i % 2 == 0 ? v : -v; 53 | } 54 | 55 | seeScore.Should().Be(expected); 56 | } 57 | 58 | public static byte ChatToPiece(char c) 59 | { 60 | return c switch 61 | { 62 | 'p' => 1, 63 | 'P' => 2, 64 | 'n' => 3, 65 | 'N' => 4, 66 | 'b' => 5, 67 | 'B' => 6, 68 | 'r' => 7, 69 | 'R' => 8, 70 | 'q' => 9, 71 | 'Q' => 10, 72 | 'k' => 11, 73 | 'K' => 12 74 | }; 75 | } 76 | } -------------------------------------------------------------------------------- /src/Sapling.Engine.Tests/ZobristTests.cs: -------------------------------------------------------------------------------- 1 | //using FluentAssertions; 2 | //using Sapling.Engine.Pgn; 3 | 4 | //namespace Sapling.Engine.Tests; 5 | 6 | //public class ZobristTests 7 | //{ 8 | // [Fact] 9 | // public void InitialHash_Is_Correct() 10 | // { 11 | // var board = BoardStateExtensions.CreateBoardFromFen(Constants.InitialState); 12 | // board.Hash.Should().Be(10825574554103633524UL); 13 | // } 14 | 15 | // [Theory] 16 | // [InlineData(2385735843766215098UL, 17 | // "1.d4 Nf6 2.c4 c5 3.d5 e6 4.Nc3 exd5 5.cxd5 d6 6.e4 a6 7.a4 g6 8.Nf3 Bg7 9.Be2 O-O\n10.O-O Re8 11.Qc2 Nbd7 12.Nd2 Ne5 13.h3 g5 14.Nc4 g4 15.Nxe5 Rxe5 16.h4 Nh5\n17.g3 Re8 18.Bd3 Qe7 19.Be3 Bd7 20.a5 Rac8 21.b3 Qe5 22.Ne2 Bb5 23.Bxb5 axb5\n24.a6 Ra8 25.a7 Qxe4 26.Qxe4 Rxe4 27.Ra5 Rb4 28.Rb1 c4 29.Bd2 Rxb3 30.Rxb3 cxb3\n31.Rxb5 Rxa7 32.Rxb3 Ra2 33.Rd3 Nf6 34.Nd4 Nd7 35.Bf4 Nc5 36.Rd1 Ne4 37.Re1 Bxd4\n38.Rxe4 Bxf2+ 39.Kf1 h5 40.Bxd6 Ba7 41.Bf4 Kg7 42.d6 Ra5 43.Rb4 Rd5 44.Rxb7 Bc5\n45.Rb5 Kf6 46.d7 Ke7 47.Be3 Kxd7 48.Rxc5 Rd3 49.Ke2 Ra3 50.Rxh5 Ke6 51.Rb5 Ra2+\n52.Kd3 Ra3+ 53.Ke4 Ra4+ 54.Bd4 Ra3 55.Re5+ Kd7 56.Be3 f6 57.Rb5 Ke6 58.Rb6+ Kf7\n59.Kf4 Kg6 60.Bd4 Kh5 61.Rb5+ Kg6 62.Kxg4 Ra4 63.Rd5 f5+ 64.Kf4 Rb4 65.Rd6+ Kh5\n66.Kxf5 Rb3 67.Be5 Rxg3 68.Bf4 1-0")] 18 | // [InlineData(1974259010437475510UL, 19 | // "1.e4 e5 2.f4 exf4 3.Nf3 Nf6 4.Nc3 d5 5.exd5 Nxd5 6.Nxd5 Qxd5 7.d4 Be7 8.c4 Qe6+\n9.Kf2 Qf6 10.Bd3 c5 11.d5 O-O 12.Qc2 Qh6 13.Bd2 f5 14.Rae1 Bd6 15.Re2 Nd7\n16.Rhe1 Nf6 17.Bxf5 Ng4+ 18.Bxg4 Bxg4 19.Kg1 Qh5 20.Qd3 Bf5 21.Qb3 b6 22.Bc3 Rae8\n23.Be5 Bxe5 24.Rxe5 Rxe5 25.Rxe5 Re8 26.Qc3 Qf7 27.Qe1 Bd7 28.Qe4 Rxe5 29.Qxe5 Qf5\n30.Qb8+ Kf7 31.h4 Ke7 32.Kh2 Qf6 33.b3 a6 34.a4 h6 35.a5 bxa5 36.Qc7 Qd6\n37.Qxa5 g5 38.Qe1+ Kf6 39.Qc3+ Kg6 40.Qe5 Qf6 41.Qc7 Bf5 42.Ne5+ Kh5 43.hxg5 hxg5\n44.g4+ fxg3+ 45.Kxg3 g4 46.Qf7+ Qxf7 47.Nxf7 Kg6 48.Ne5+ Kg7 49.Kf4 Bc2 50.Kxg4 Bxb3\n51.Kf5 Kf8 52.d6 Ke8 53.Ke6 1-0")] 20 | // public void ZorbristHashMatches(ulong hash, string pgn) 21 | // { 22 | // var (gameState, searcher) = ChessboardHelpers.InitialState(); 23 | 24 | // pgn = pgn.Replace("1-0", "") 25 | // .Replace("0-1", "") 26 | // .Replace("1/2-1/2", "") 27 | // .Replace("*", "").Trim(); 28 | 29 | // foreach (var move in PgnSplitter.SplitPgnIntoMoves(pgn)) 30 | // { 31 | // var turns = move.Split(' '); 32 | 33 | // var mov = PgnParser.Parse(turns[0], gameState.LegalMoves); 34 | 35 | // gameState.Apply(mov); 36 | 37 | // if (turns.Length == 1) 38 | // { 39 | // break; 40 | // } 41 | 42 | // mov = PgnParser.Parse(turns[1], gameState.LegalMoves); 43 | // gameState.Apply(mov); 44 | // } 45 | 46 | // gameState.Board.Data.Hash.Should().Be(hash); 47 | // } 48 | 49 | // [Theory] 50 | // [InlineData("4k3/8/p2PK3/2p1N3/2P5/1b6/8/8", 51 | // "1.e4 e5 2.f4 exf4 3.Nf3 Nf6 4.Nc3 d5 5.exd5 Nxd5 6.Nxd5 Qxd5 7.d4 Be7 8.c4 Qe6+\n9.Kf2 Qf6 10.Bd3 c5 11.d5 O-O 12.Qc2 Qh6 13.Bd2 f5 14.Rae1 Bd6 15.Re2 Nd7\n16.Rhe1 Nf6 17.Bxf5 Ng4+ 18.Bxg4 Bxg4 19.Kg1 Qh5 20.Qd3 Bf5 21.Qb3 b6 22.Bc3 Rae8\n23.Be5 Bxe5 24.Rxe5 Rxe5 25.Rxe5 Re8 26.Qc3 Qf7 27.Qe1 Bd7 28.Qe4 Rxe5 29.Qxe5 Qf5\n30.Qb8+ Kf7 31.h4 Ke7 32.Kh2 Qf6 33.b3 a6 34.a4 h6 35.a5 bxa5 36.Qc7 Qd6\n37.Qxa5 g5 38.Qe1+ Kf6 39.Qc3+ Kg6 40.Qe5 Qf6 41.Qc7 Bf5 42.Ne5+ Kh5 43.hxg5 hxg5\n44.g4+ fxg3+ 45.Kxg3 g4 46.Qf7+ Qxf7 47.Nxf7 Kg6 48.Ne5+ Kg7 49.Kf4 Bc2 50.Kxg4 Bxb3\n51.Kf5 Kf8 52.d6 Ke8 53.Ke6 1-0")] 52 | // [InlineData("8/5r2/6k1/8/8/1R6/1p4r1/1K5R", 53 | // "1.d4 d5 2.c4 c6 3.Nf3 e6 4.Nc3 Nd7 5.cxd5 exd5 6.Bf4 Ne7 7.e3 Ng6 8.Bg3 Nf6\n9.Bd3 Be7 10.h3 O-O 11.Qc2 Bd6 12.Bxd6 Qxd6 13.O-O Ne8 14.Rfe1 Qf6 15.Nh2 Nd6\n16.b4 Bd7 17.b5 Nxb5 18.Bxb5 cxb5 19.Nxd5 Qg5 20.e4 f5 21.Nf3 Qd8 22.Ne5 Rc8\n23.Qe2 Bc6 24.Nxg6 hxg6 25.Nf4 Re8 26.Nxg6 Qg5 27.Ne5 Bxe4 28.g3 Rc2 29.Qxb5 Rec8\n30.Qb3+ Kh7 31.h4 Qd2 32.Qe3 Qxe3 33.Rxe3 Rxa2 34.Rf1 b5 35.Nf7 b4 36.Rd1 Rd2\n37.Ng5+ Kg6 38.Ra1 Ra2 39.Rd1 Bd5 40.Re5 Rd8 41.Nh3 Kf6 42.Rde1 Ra6 43.Nf4 Bf7\n44.d5 Rb6 45.f3 Rd7 46.g4 g6 47.h5 Kg5 48.hxg6 Bxg6 49.Ne6+ Kf6 50.Nf8 Rdd6\n51.Nxg6 Kxg6 52.gxf5+ Kg5 53.Kf2 b3 54.f4+ Kf6 55.Re6+ Kxf5 56.R1e5+ Kg4\n57.Re1 a5 58.f5 Kg5 59.R1e3 b2 60.Rg3+ Kh4 61.Rg1 a4 62.Re4+ Kh5 63.Re3 Rb4\n64.Rh3+ Rh4 65.Rf3 Rxd5 66.f6 Rd2+ 67.Ke3 Rd8 68.f7 Rf8 69.Kd3 Rg4 70.Rh1+ Kg6\n71.Kc2 Rg2+ 72.Kb1 a3 73.Rxa3 Rxf7 74.Rb3 1/2-1/2")] 54 | // [InlineData("8/5rk1/6r1/p4K2/1p4R1/8/PP6/3R4", 55 | // "1.d4 Nf6 2.c4 g6 3.Nc3 Bg7 4.e4 d6 5.f3 O-O 6.Be3 e5 7.Nge2 c6 8.Qd2 exd4\n9.Bxd4 Be6 10.Nf4 c5 11.Be3 Nc6 12.Nb5 Ne8 13.O-O-O Qb6 14.Nd5 Bxd5 15.cxd5 Ne5\n16.Bh6 a6 17.Nc3 Qa5 18.Kb1 b5 19.Rc1 c4 20.Bxg7 Nxg7 21.f4 Nd7 22.Qd4 Rfe8\n23.g4 Qb6 24.Qxb6 Nxb6 25.Re1 b4 26.Nd1 Nxd5 27.Bxc4 Nxf4 28.Rhf1 g5 29.h4 h6\n30.hxg5 hxg5 31.Ne3 Ra7 32.Nd5 Nxd5 33.Bxd5 Re5 34.Rc1 Ne8 35.Rc6 Kg7 36.Rfc1 Nf6\n37.Rxd6 Nxg4 38.Rg1 Nf6 39.Kc2 g4 40.Kd3 Rg5 41.Ke3 Nh5 42.Bc4 a5 43.Be2 Nf6\n44.Kf4 Rg6 45.e5 Nh5+ 46.Kf5 Re7 47.Bxg4 f6 48.exf6+ Nxf6 49.Rdd1 Nxg4 50.Rxg4 Rf7+ 0-1")] 56 | // [InlineData("8/p1r5/k6p/1P1Q4/2N4P/3K4/8/8", 57 | // "1. d4 d5 2. Nc3 Bf5 3. Bf4 e6 4. f3 Bd6 5. Qd2 Ne7 6. g4 Bg6 7. O-O-O Nbc6 8. Bxd6 Qxd6 9. e4 O-O-O 10. e5 Qb4 11. a3 Qa5 12. Nge2 Kb8 13. Nf4 Nc8 14. h4 h6 15. Nxg6 fxg6 16. Ne2 Nb6 17. Qxa5 Nxa5 18. Nf4 Rhe8 19. Nxg6 Nac4 20. b3 Ne3 21. Rd3 Nxf1 22. Rxf1 Nd7 23. f4 c5 24. c3 Rc8 25. Kb2 c4 26. Rdf3 cxb3 27. Kxb3 Nb6 28. f5 Nc4 29. R1f2 Rc6 30. Ra2 exf5 31. gxf5 Rb6+ 32. Kc2 Ra6 33. a4 b5 34. a5 Rxa5 35. Rxa5 Nxa5 36. f6 gxf6 37. exf6 b4 38. cxb4 Nc6 39. f7 Rc8 40. f8=Q Nxd4+ 41. Kd3 Nxf3 42. Qf4+ Kb7 43. Qxf3 Kb6 44. Qxd5 Rc6 45. Ne5 Rc7 46. Nc4+ Ka6 47. b5#")] 58 | // [InlineData("8/8/8/3K4/3B4/7p/7k/6Q1", 59 | // "1. e4 g6 2. d4 Bg7 3. Nc3 c6 4. f4 d5 5. e5 Nh6 6. Bd3 O-O 7. Nf3 Bf5 8. O-O f6 9. Be3 Nd7 10. Bxf5 Nxf5 11. Bf2 fxe5 12. fxe5 e6 13. Qd2 c5 14. Rae1 cxd4 15. Nxd4 Bh6 16. Qe2 Nxd4 17. Bxd4 Qa5 18. Qg4 Qa6 19. Nxd5 Rxf1+ 20. Rxf1 Nxe5 21. Bxe5 exd5 22. Bd4 Qd6 23. Rf6 Qe7 24. Re6 Qf7 25. Qe2 Qf4 26. Re8+ Rxe8 27. Qxe8+ Qf8 28. Qe6+ Qf7 29. Qc8+ Bf8 30. Qd8 Qe6 31. h3 Kf7 32. Qc7+ Qe7 33. Qxe7+ Bxe7 34. Bxa7 Ke6 35. Bd4 Kf5 36. Kf2 Ke4 37. Bg7 d4 38. Ke2 Bg5 39. Bf8 Bc1 40. b3 b5 41. Bb4 Bf4 42. a4 bxa4 43. bxa4 Bb8 44. a5 Ba7 45. a6 Kf4 46. Kd3 Kg3 47. Kc4 Kxg2 48. Bc5 Bb8 49. a7 Bxa7 50. Bxa7 Kxh3 51. Bxd4 g5 52. Kd5 g4 53. c4 h5 54. c5 h4 55. c6 Kh2 56. c7 g3 57. c8=Q h3 58. Qc1 g2 59. Qe1 g1=B 60. Qxg1#")] 60 | // public void Produces_Correct_Fen_String(string fen, string pgn) 61 | // { 62 | // var (gameState, searcher) = ChessboardHelpers.InitialState(); 63 | 64 | // pgn = pgn.Replace("1-0", "") 65 | // .Replace("0-1", "") 66 | // .Replace("1/2-1/2", "") 67 | // .Replace("*", "").Trim(); 68 | 69 | // foreach (var move in PgnSplitter.SplitPgnIntoMoves(pgn)) 70 | // { 71 | // var turns = move.Split(' '); 72 | 73 | // var mov = PgnParser.Parse(turns[0], gameState.LegalMoves); 74 | 75 | // gameState.Apply(mov); 76 | 77 | // if (turns.Length == 1) 78 | // { 79 | // break; 80 | // } 81 | 82 | // mov = PgnParser.Parse(turns[1], gameState.LegalMoves); 83 | 84 | // gameState.Apply(mov); 85 | // } 86 | 87 | // // gameState.Board.Pieces.ArrayToFen().Should().Be(fen); 88 | // } 89 | //} -------------------------------------------------------------------------------- /src/Sapling.Engine/BitboardHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.Intrinsics.X86; 3 | 4 | namespace Sapling.Engine; 5 | 6 | public static class BitboardHelpers 7 | { 8 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 9 | public static byte PopLSB(this ref ulong b) 10 | { 11 | var i = (byte)Bmi1.X64.TrailingZeroCount(b); 12 | b &= b - 1; 13 | 14 | return i; 15 | } 16 | 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public static ulong PeekLSB(this ulong bitBoard) 19 | { 20 | return Bmi1.X64.TrailingZeroCount(bitBoard); 21 | } 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public static byte PopCount(ulong bitBoard) 25 | { 26 | return (byte)Popcnt.X64.PopCount(bitBoard); 27 | } 28 | 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public static ulong RankFileToBitboard(int rank, int file) 31 | { 32 | return 1UL << (rank * 8 + file); 33 | } 34 | 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public static ulong SquareToBitboard(this int square) 37 | { 38 | return 1UL << square; 39 | } 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public static ulong ShiftUp(this ulong board) 43 | { 44 | return board << 8; 45 | } 46 | 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public static ulong ShiftDown(this ulong board) 49 | { 50 | return board >> 8; 51 | } 52 | 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | public static ulong ShiftLeft(this ulong board) 55 | { 56 | return (board >> 1) & Constants.NotHFile; 57 | } 58 | 59 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 60 | public static ulong ShiftRight(this ulong board) 61 | { 62 | return (board << 1) & Constants.NotAFile; 63 | } 64 | 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | public static ulong ShiftUpRight(this ulong board) 67 | { 68 | // Combined shift up (<< 8) and right (<< 1) with a mask to prevent overflow on the left side. 69 | return (board << 9) & Constants.NotAFile; 70 | } 71 | 72 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 73 | public static ulong ShiftUpLeft(this ulong board) 74 | { 75 | // Combined shift up (<< 8) and left (>> 1) with a mask to prevent overflow on the right side. 76 | return (board << 7) & Constants.NotHFile; 77 | } 78 | 79 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 80 | public static ulong ShiftDownRight(this ulong board) 81 | { 82 | // Combined shift down (>> 8) and right (<< 1) with a mask to prevent overflow on the left side. 83 | return (board >> 7) & Constants.NotAFile; 84 | } 85 | 86 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 87 | public static ulong ShiftDownLeft(this ulong board) 88 | { 89 | // Combined shift down (>> 8) and left (>> 1) with a mask to prevent overflow on the right side. 90 | return (board >> 9) & Constants.NotHFile; 91 | } 92 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/BoardState.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Sapling.Engine.Evaluation; 3 | using System.Runtime.InteropServices; 4 | using Sapling.Engine.Transpositions; 5 | 6 | namespace Sapling.Engine; 7 | 8 | [StructLayout(LayoutKind.Explicit, Size = 168)] // Adjusted size to fit more efficiently 9 | public unsafe struct BoardStateData 10 | { 11 | public const uint BoardStateSize = 168; 12 | 13 | // 15 * 8 = 120 bytes (occupancy array) 14 | [FieldOffset(0)] public fixed ulong Occupancy[15]; // 15 ulong values, no padding needed. 15 | 16 | // 8 bytes, aligned at 120 17 | [FieldOffset(120)] public ulong Hash; // 64-bit aligned naturally. 18 | 19 | // Combine all 1-byte fields (byte and bool) together to avoid unnecessary padding. 20 | // 8 bytes in total (TurnCount + HalfMoveClock + WhiteToMove + InCheck + CastleRights + WhiteKingSquare + BlackKingSquare + EnPassantFile) 21 | [FieldOffset(128)] public ushort TurnCount; // 16-bit 22 | [FieldOffset(130)] public byte HalfMoveClock; // 8-bit 23 | [FieldOffset(131)] public bool WhiteToMove; // 8-bit 24 | [FieldOffset(132)] public bool InCheck; // 8-bit 25 | [FieldOffset(133)] public CastleRights CastleRights; // 8-bit enum 26 | [FieldOffset(134)] public byte WhiteKingSquare; // 8-bit 27 | [FieldOffset(135)] public byte BlackKingSquare; // 8-bit 28 | [FieldOffset(136)] public byte EnPassantFile; // 8-bit 29 | [FieldOffset(137)] public byte PieceCount; // 8-bit 30 | 31 | // Group additional fields that fit in the next available space: 32 | // 8 bytes for PawnHash, aligned at 138. 33 | [FieldOffset(138)] public ulong PawnHash; // 64-bit aligned naturally. 34 | 35 | // 8 bytes for WhiteMaterialHash, aligned at 146. 36 | [FieldOffset(146)] public ulong WhiteMaterialHash; // 64-bit 37 | 38 | // 8 bytes for BlackMaterialHash, aligned at 154. 39 | [FieldOffset(154)] public ulong BlackMaterialHash; // 64-bit 40 | 41 | // Group remaining small fields together (total 4 bytes): 42 | [FieldOffset(162)] public byte WhiteKingSideTargetSquare; // 1 byte 43 | [FieldOffset(163)] public byte WhiteQueenSideTargetSquare; // 1 byte 44 | [FieldOffset(164)] public byte BlackKingSideTargetSquare; // 1 byte 45 | [FieldOffset(165)] public byte BlackQueenSideTargetSquare; // 1 byte 46 | [FieldOffset(166)] public bool Is960; // 1 byte 47 | 48 | // Add padding if necessary to make the total size a multiple of 8 bytes. 49 | [FieldOffset(167)] private fixed byte _padding[1]; // Padding to align total size to 168 bytes. 50 | } 51 | 52 | 53 | public static unsafe class AccumulatorStateExtensions 54 | { 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public static void CloneTo(this ref BoardStateData board, ref BoardStateData copy) 57 | { 58 | fixed (BoardStateData* sourcePtr = &board) 59 | fixed (BoardStateData* destPtr = ©) 60 | { 61 | // Copy the memory block from source to destination 62 | Buffer.MemoryCopy(sourcePtr, destPtr, sizeof(BoardStateData), sizeof(BoardStateData)); 63 | } 64 | } 65 | 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public static void UpdateTo(this ref AccumulatorState state, BoardStateData* board) 68 | { 69 | state.Evaluation = TranspositionTableExtensions.NoHashEntry; 70 | state.WhiteNeedsRefresh = state.BlackNeedsRefresh = false; 71 | state.WhiteMirrored = board->WhiteKingSquare.IsMirroredSide(); 72 | state.WhiteInputBucket = *(NnueWeights.BucketLayout + board->WhiteKingSquare); 73 | state.BlackMirrored = board->BlackKingSquare.IsMirroredSide(); 74 | state.BlackInputBucket = *(NnueWeights.BucketLayout + (board->BlackKingSquare ^ 0x38)); 75 | 76 | state.WhiteAccumulatorUpToDate = state.BlackAccumulatorUpToDate = false; 77 | state.ChangeType = AccumulatorChangeType.None; 78 | state.Move = default; 79 | } 80 | 81 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 82 | public static void UpdateToParent(this ref AccumulatorState state, AccumulatorState* other, BoardStateData* board) 83 | { 84 | state.Evaluation = TranspositionTableExtensions.NoHashEntry; 85 | state.WhiteMirrored = board->WhiteKingSquare.IsMirroredSide(); 86 | state.WhiteInputBucket = *(NnueWeights.BucketLayout + board->WhiteKingSquare); 87 | state.BlackMirrored = board->BlackKingSquare.IsMirroredSide(); 88 | state.BlackInputBucket = *(NnueWeights.BucketLayout + (board->BlackKingSquare ^ 0x38)); 89 | 90 | state.WhiteAccumulatorUpToDate = state.BlackAccumulatorUpToDate = false; 91 | state.ChangeType = AccumulatorChangeType.None; 92 | 93 | state.WhiteNeedsRefresh = other->WhiteMirrored != state.WhiteMirrored || other->WhiteInputBucket != state.WhiteInputBucket; 94 | state.BlackNeedsRefresh = other->BlackMirrored != state.BlackMirrored || other->BlackInputBucket != state.BlackInputBucket; 95 | state.Move = default; 96 | } 97 | } 98 | 99 | public enum AccumulatorChangeType : byte 100 | { 101 | None = 0, 102 | SubAdd = 1, 103 | SubSubAdd = 2, 104 | SubSubAddAdd = 3, 105 | } 106 | 107 | [StructLayout(LayoutKind.Explicit, Size = 56)] // Corrected size based on alignment needs 108 | public unsafe struct AccumulatorState 109 | { 110 | // 32-bit integers (4 bytes each) 111 | [FieldOffset(0)] public int WhiteAddFeatureUpdatesA; 112 | [FieldOffset(4)] public int WhiteAddFeatureUpdatesB; 113 | [FieldOffset(8)] public int WhiteSubFeatureUpdatesA; 114 | [FieldOffset(12)] public int WhiteSubFeatureUpdatesB; 115 | [FieldOffset(16)] public int BlackAddFeatureUpdatesA; 116 | [FieldOffset(20)] public int BlackAddFeatureUpdatesB; 117 | [FieldOffset(24)] public int BlackSubFeatureUpdatesA; 118 | [FieldOffset(28)] public int BlackSubFeatureUpdatesB; 119 | 120 | // 1-byte fields start after 32 bytes 121 | [FieldOffset(32)] public AccumulatorChangeType ChangeType; // Enum type (1 byte) 122 | [FieldOffset(33)] public bool BlackAccumulatorUpToDate; // 1 byte 123 | [FieldOffset(34)] public bool WhiteAccumulatorUpToDate; // 1 byte 124 | [FieldOffset(35)] public bool WhiteMirrored; // 1 byte 125 | [FieldOffset(36)] public bool BlackMirrored; // 1 byte 126 | [FieldOffset(37)] public byte WhiteInputBucket; // 1 byte 127 | [FieldOffset(38)] public byte BlackInputBucket; // 1 byte 128 | [FieldOffset(39)] public bool WhiteNeedsRefresh; // 1 byte 129 | [FieldOffset(40)] public bool BlackNeedsRefresh; // 1 byte 130 | 131 | // Padding to align the next field (int) to a 4-byte boundary 132 | [FieldOffset(41)] private fixed byte _padding1[3]; // 3 bytes of padding 133 | 134 | // 32-bit fields aligned properly (starting at 44 bytes) 135 | [FieldOffset(44)] public int Evaluation; // Changed nullable int to regular int for simplicity 136 | 137 | // 32-bit unsigned int (4 bytes) 138 | [FieldOffset(48)] public uint Move; // 32-bit aligned correctly at 48 bytes 139 | } 140 | -------------------------------------------------------------------------------- /src/Sapling.Engine/Constants.cs: -------------------------------------------------------------------------------- 1 | using Sapling.Engine; 2 | 3 | namespace Sapling.Engine; 4 | 5 | [Flags] 6 | public enum CastleRights : byte 7 | { 8 | None = 0, 9 | WhiteKingSide = 1, 10 | WhiteQueenSide = 2, 11 | BlackKingSide = 4, 12 | BlackQueenSide = 8 13 | } 14 | 15 | public static class Constants 16 | { 17 | public const short ImmediateMateScore = 29_000; 18 | 19 | #region Bitboards 20 | 21 | public const ulong NotAFile = 0xFEFEFEFEFEFEFEFE; // All squares except column 'A' 22 | public const ulong NotHFile = 0x7F7F7F7F7F7F7F7F; // All squares except column 'H' 23 | 24 | public const byte None = 0; 25 | 26 | public const byte Occupancy = 0; 27 | public const byte BlackPawn = 1; 28 | public const byte BlackKnight = 3; 29 | public const byte BlackBishop = 5; 30 | public const byte BlackRook = 7; 31 | public const byte BlackQueen = 9; 32 | public const byte BlackKing = 11; 33 | 34 | public const byte WhitePawn = 2; 35 | public const byte WhiteKnight = 4; 36 | public const byte WhiteBishop = 6; 37 | public const byte WhiteRook = 8; 38 | public const byte WhiteQueen = 10; 39 | public const byte WhiteKing = 12; 40 | 41 | public const byte WhitePieces = 13; 42 | public const byte BlackPieces = 14; 43 | 44 | public const byte Castle = 1; 45 | public const byte DoublePush = 2; 46 | public const byte EnPassant = 3; 47 | public const byte PawnKnightPromotion = 4; 48 | public const byte PawnBishopPromotion = 5; 49 | public const byte PawnRookPromotion = 6; 50 | public const byte PawnQueenPromotion = 7; 51 | 52 | public const CastleRights AllCastleRights = CastleRights.WhiteKingSide | CastleRights.WhiteQueenSide | 53 | CastleRights.BlackKingSide | CastleRights.BlackQueenSide; 54 | 55 | public const CastleRights WhiteCastleRights = CastleRights.WhiteKingSide | CastleRights.WhiteQueenSide; 56 | 57 | public const CastleRights BlackCastleRights = CastleRights.BlackKingSide | CastleRights.BlackQueenSide; 58 | 59 | 60 | public const int BlackPawnZobristOffset = 1 * 64; 61 | public const int BlackKnightZobristOffset = 3 * 64; 62 | public const int BlackBishopZobristOffset = 5 * 64; 63 | public const int BlackRookZobristOffset = 7 * 64; 64 | public const int BlackQueenZobristOffset = 9 * 64; 65 | public const int BlackKingZobristOffset = 11 * 64; 66 | public const int WhitePawnZobristOffset = 2 * 64; 67 | public const int WhiteKnightZobristOffset = 4 * 64; 68 | public const int WhiteBishopZobristOffset = 6 * 64; 69 | public const int WhiteRookZobristOffset = 8 * 64; 70 | public const int WhiteQueenZobristOffset = 10 * 64; 71 | public const int WhiteKingZobristOffset = 12 * 64; 72 | 73 | public const int BlackPawnFeatureIndexOffset = 1 * 128; 74 | public const int BlackKnightFeatureIndexOffset = 3 * 128; 75 | public const int BlackBishopFeatureIndexOffset = 5 * 128; 76 | public const int BlackRookFeatureIndexOffset = 7 * 128; 77 | public const int BlackQueenFeatureIndexOffset = 9 * 128; 78 | public const int BlackKingFeatureIndexOffset = 11 * 128; 79 | public const int WhitePawnFeatureIndexOffset = 2 * 128; 80 | public const int WhiteKnightFeatureIndexOffset = 4 * 128; 81 | public const int WhiteBishopFeatureIndexOffset = 6 * 128; 82 | public const int WhiteRookFeatureIndexOffset = 8 * 128; 83 | public const int WhiteQueenFeatureIndexOffset = 10 * 128; 84 | public const int WhiteKingFeatureIndexOffset = 12 * 128; 85 | 86 | public const int BlackKingSideCastleKingFromIndex = BlackKingFeatureIndexOffset + (60 << 1); 87 | public const int BlackKingSideCastleRookFromIndex = BlackRookFeatureIndexOffset + (63 << 1); 88 | public const int BlackKingSideCastleKingToIndex = BlackKingFeatureIndexOffset + (62 << 1); 89 | public const int BlackKingSideCastleRookToIndex = BlackRookFeatureIndexOffset + (61 << 1); 90 | public const int BlackQueenSideCastleKingFromIndex = BlackKingFeatureIndexOffset + (60 << 1); 91 | public const int BlackQueenSideCastleRookFromIndex = BlackRookFeatureIndexOffset + (56 << 1); 92 | public const int BlackQueenSideCastleKingToIndex = BlackKingFeatureIndexOffset + (58 << 1); 93 | public const int BlackQueenSideCastleRookToIndex = BlackRookFeatureIndexOffset + (59 << 1); 94 | 95 | public const int WhiteKingSideCastleKingFromIndex = WhiteKingFeatureIndexOffset + (4 << 1); 96 | public const int WhiteKingSideCastleRookFromIndex = WhiteRookFeatureIndexOffset + (7 << 1); 97 | public const int WhiteKingSideCastleKingToIndex = WhiteKingFeatureIndexOffset + (6 << 1); 98 | public const int WhiteKingSideCastleRookToIndex = WhiteRookFeatureIndexOffset + (5 << 1); 99 | public const int WhiteQueenSideCastleKingFromIndex = WhiteKingFeatureIndexOffset + (4 << 1); 100 | public const int WhiteQueenSideCastleRookFromIndex = WhiteRookFeatureIndexOffset; 101 | public const int WhiteQueenSideCastleKingToIndex = WhiteKingFeatureIndexOffset + (2 << 1); 102 | public const int WhiteQueenSideCastleRookToIndex = WhiteRookFeatureIndexOffset + (3 << 1); 103 | 104 | #endregion 105 | 106 | #region PieceValue 107 | 108 | public const int PawnValue = 100; 109 | public const int KnightValue = 450; 110 | public const int BishopValue = 450; 111 | public const int RookValue = 650; 112 | public const int QueenValue = 1250; 113 | public const int KingValue = 5000; 114 | 115 | public const int MaxSearchDepth = 120; 116 | public const int MaxScore = 99999999; 117 | public const int MinScore = -99999999; 118 | 119 | public static readonly string InitialState = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; 120 | public static readonly BoardStateData InitialBoard = BoardStateExtensions.CreateBoardFromFen(InitialState); 121 | 122 | #endregion 123 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/DataGen/Bench.cs: -------------------------------------------------------------------------------- 1 | using Sapling.Engine.Search; 2 | using Sapling.Engine.Transpositions; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Sapling.Engine.DataGen 7 | { 8 | public static class Bench 9 | { 10 | private static readonly string[] BenchFens = new string[] 11 | { 12 | "r3k2r/2pb1ppp/2pp1q2/p7/1nP1B3/1P2P3/P2N1PPP/R2QK2R w KQkq a6 0 14", 13 | "4rrk1/2p1b1p1/p1p3q1/4p3/2P2n1p/1P1NR2P/PB3PP1/3R1QK1 b - - 2 24", 14 | "r3qbrk/6p1/2b2pPp/p3pP1Q/PpPpP2P/3P1B2/2PB3K/R5R1 w - - 16 42", 15 | "6k1/1R3p2/6p1/2Bp3p/3P2q1/P7/1P2rQ1K/5R2 b - - 4 44", 16 | "8/8/1p2k1p1/3p3p/1p1P1P1P/1P2PK2/8/8 w - - 3 54", 17 | "7r/2p3k1/1p1p1qp1/1P1Bp3/p1P2r1P/P7/4R3/Q4RK1 w - - 0 36", 18 | "r1bq1rk1/pp2b1pp/n1pp1n2/3P1p2/2P1p3/2N1P2N/PP2BPPP/R1BQ1RK1 b - - 2 10", 19 | "3r3k/2r4p/1p1b3q/p4P2/P2Pp3/1B2P3/3BQ1RP/6K1 w - - 3 87", 20 | "2r4r/1p4k1/1Pnp4/3Qb1pq/8/4BpPp/5P2/2RR1BK1 w - - 0 42", 21 | "4q1bk/6b1/7p/p1p4p/PNPpP2P/KN4P1/3Q4/4R3 b - - 0 37", 22 | "2q3r1/1r2pk2/pp3pp1/2pP3p/P1Pb1BbP/1P4Q1/R3NPP1/4R1K1 w - - 2 34", 23 | "1r2r2k/1b4q1/pp5p/2pPp1p1/P3Pn2/1P1B1Q1P/2R3P1/4BR1K b - - 1 37", 24 | "r3kbbr/pp1n1p1P/3ppnp1/q5N1/1P1pP3/P1N1B3/2P1QP2/R3KB1R b KQkq b3 0 17", 25 | "8/6pk/2b1Rp2/3r4/1R1B2PP/P5K1/8/2r5 b - - 16 42", 26 | "1r4k1/4ppb1/2n1b1qp/pB4p1/1n1BP1P1/7P/2PNQPK1/3RN3 w - - 8 29", 27 | "8/p2B4/PkP5/4p1pK/4Pb1p/5P2/8/8 w - - 29 68", 28 | "3r4/ppq1ppkp/4bnp1/2pN4/2P1P3/1P4P1/PQ3PBP/R4K2 b - - 2 20", 29 | "5rr1/4n2k/4q2P/P1P2n2/3B1p2/4pP2/2N1P3/1RR1K2Q w - - 1 49", 30 | "1r5k/2pq2p1/3p3p/p1pP4/4QP2/PP1R3P/6PK/8 w - - 1 51", 31 | "q5k1/5ppp/1r3bn1/1B6/P1N2P2/BQ2P1P1/5K1P/8 b - - 2 34", 32 | "r1b2k1r/5n2/p4q2/1ppn1Pp1/3pp1p1/NP2P3/P1PPBK2/1RQN2R1 w - - 0 22", 33 | "r1bqk2r/pppp1ppp/5n2/4b3/4P3/P1N5/1PP2PPP/R1BQKB1R w KQkq - 0 5", 34 | "r1bqr1k1/pp1p1ppp/2p5/8/3N1Q2/P2BB3/1PP2PPP/R3K2n b Q - 1 12", 35 | "r1bq2k1/p4r1p/1pp2pp1/3p4/1P1B3Q/P2B1N2/2P3PP/4R1K1 b - - 2 19", 36 | "r4qk1/6r1/1p4p1/2ppBbN1/1p5Q/P7/2P3PP/5RK1 w - - 2 25", 37 | "r7/6k1/1p6/2pp1p2/7Q/8/p1P2K1P/8 w - - 0 32", 38 | "r3k2r/ppp1pp1p/2nqb1pn/3p4/4P3/2PP4/PP1NBPPP/R2QK1NR w KQkq - 1 5", 39 | "3r1rk1/1pp1pn1p/p1n1q1p1/3p4/Q3P3/2P5/PP1NBPPP/4RRK1 w - - 0 12", 40 | "5rk1/1pp1pn1p/p3Brp1/8/1n6/5N2/PP3PPP/2R2RK1 w - - 2 20", 41 | "8/1p2pk1p/p1p1r1p1/3n4/8/5R2/PP3PPP/4R1K1 b - - 3 27", 42 | "8/4pk2/1p1r2p1/p1p4p/Pn5P/3R4/1P3PP1/4RK2 w - - 1 33", 43 | "8/5k2/1pnrp1p1/p1p4p/P6P/4R1PK/1P3P2/4R3 b - - 1 38", 44 | "8/8/1p1kp1p1/p1pr1n1p/P6P/1R4P1/1P3PK1/1R6 b - - 15 45", 45 | "8/8/1p1k2p1/p1prp2p/P2n3P/6P1/1P1R1PK1/4R3 b - - 5 49", 46 | "8/8/1p4p1/p1p2k1p/P2npP1P/4K1P1/1P6/3R4 w - - 6 54", 47 | "8/8/1p4p1/p1p2k1p/P2n1P1P/4K1P1/1P6/6R1 b - - 6 59", 48 | "8/5k2/1p4p1/p1pK3p/P2n1P1P/6P1/1P6/4R3 b - - 14 63", 49 | "8/1R6/1p1K1kp1/p6p/P1p2P1P/6P1/1Pn5/8 w - - 0 67", 50 | "1rb1rn1k/p3q1bp/2p3p1/2p1p3/2P1P2N/PP1RQNP1/1B3P2/4R1K1 b - - 4 23", 51 | "4rrk1/pp1n1pp1/q5p1/P1pP4/2n3P1/7P/1P3PB1/R1BQ1RK1 w - - 3 22", 52 | "r2qr1k1/pb1nbppp/1pn1p3/2ppP3/3P4/2PB1NN1/PP3PPP/R1BQR1K1 w - - 4 12", 53 | "2r2k2/8/4P1R1/1p6/8/P4K1N/7b/2B5 b - - 0 55", 54 | "6k1/5pp1/8/2bKP2P/2P5/p4PNb/B7/8 b - - 1 44", 55 | "2rqr1k1/1p3p1p/p2p2p1/P1nPb3/2B1P3/5P2/1PQ2NPP/R1R4K w - - 3 25", 56 | "r1b2rk1/p1q1ppbp/6p1/2Q5/8/4BP2/PPP3PP/2KR1B1R b - - 2 14", 57 | "6r1/5k2/p1b1r2p/1pB1p1p1/1Pp3PP/2P1R1K1/2P2P2/3R4 w - - 1 36", 58 | "rnbqkb1r/pppppppp/5n2/8/2PP4/8/PP2PPPP/RNBQKBNR b KQkq c3 0 2", 59 | "2rr2k1/1p4bp/p1q1p1p1/4Pp1n/2PB4/1PN3P1/P3Q2P/2RR2K1 w - f6 0 20", 60 | "3br1k1/p1pn3p/1p3n2/5pNq/2P1p3/1PN3PP/P2Q1PB1/4R1K1 w - - 0 23", 61 | "2r2b2/5p2/5k2/p1r1pP2/P2pB3/1P3P2/K1P3R1/7R w - - 23 93", 62 | "5k2/4q1p1/3P1pQb/1p1B4/pP5p/P1PR4/5PP1/1K6 b - - 0 38", 63 | "6k1/6p1/8/6KQ/1r6/q2b4/8/8 w - - 0 32", 64 | "5rk1/1rP3pp/p4n2/3Pp3/1P2Pq2/2Q4P/P5P1/R3R1K1 b - - 0 32", 65 | "4r1k1/4r1p1/8/p2R1P1K/5P1P/1QP3q1/1P6/3R4 b - - 0 1", 66 | "R4r2/4q1k1/2p1bb1p/2n2B1Q/1N2pP2/1r2P3/1P5P/2B2KNR w - - 3 31", 67 | "r6k/pbR5/1p2qn1p/P2pPr2/4n2Q/1P2RN1P/5PBK/8 w - - 2 31", 68 | "rn2k3/4r1b1/pp1p1n2/1P1q1p1p/3P4/P3P1RP/1BQN1PR1/1K6 w - - 6 28", 69 | "3q1k2/3P1rb1/p6r/1p2Rp2/1P5p/P1N2pP1/5B1P/3QRK2 w - - 1 42", 70 | "4r2k/1p3rbp/2p1N1p1/p3n3/P2NB1nq/1P6/4R1P1/B1Q2RK1 b - - 4 32", 71 | "4r1k1/1q1r3p/2bPNb2/1p1R3Q/pB3p2/n5P1/6B1/4R1K1 w - - 2 36", 72 | "3qr2k/1p3rbp/2p3p1/p7/P2pBNn1/1P3n2/6P1/B1Q1RR1K b - - 1 30", 73 | "3qk1b1/1p4r1/1n4r1/2P1b2B/p3N2p/P2Q3P/8/1R3R1K w - - 2 39", 74 | }; 75 | 76 | public static unsafe Transposition* AllocateTranspositions(nuint items) 77 | { 78 | const nuint alignment = 64; 79 | 80 | nuint bytes = ((nuint)sizeof(Transposition) * (nuint)items); 81 | void* block = NativeMemory.AlignedAlloc(bytes, alignment); 82 | NativeMemory.Clear(block, bytes); 83 | 84 | return (Transposition*)block; 85 | } 86 | 87 | public static unsafe void Run(int depth = 14) 88 | { 89 | var transpositionSize = (int)TranspositionTableExtensions.CalculateTranspositionTableSize(256); 90 | var transpositions = AllocateTranspositions((nuint)transpositionSize); 91 | 92 | var gameState = GameState.InitialState(); 93 | var searcher = new Searcher(transpositions, transpositionSize); 94 | var stopwatch = new Stopwatch(); 95 | long totalNodes = 0; 96 | long totalTime = 0; 97 | foreach (var fen in BenchFens) 98 | { 99 | Console.WriteLine(fen); 100 | gameState.ResetToFen(fen); 101 | stopwatch.Restart(); 102 | searcher.Reset(gameState); 103 | var result = searcher.Search(gameState, searcher.Stop, depthLimit: depth, threadId: 0); 104 | 105 | totalTime += stopwatch.ElapsedMilliseconds; 106 | totalNodes += result.nodes; 107 | } 108 | 109 | Console.WriteLine($"{totalNodes} nodes {(int)(totalNodes / (float)totalTime) * 1000} nps"); 110 | NativeMemory.AlignedFree(transpositions); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Sapling.Engine/DataGen/BulletFormat.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Sapling.Engine.DataGen; 6 | 7 | [StructLayout(LayoutKind.Explicit, Size = 32)] 8 | public unsafe struct BulletFormat 9 | { 10 | public const int Size = 32; 11 | 12 | [FieldOffset(0)] public ulong Occupancy = 0; 13 | [FieldOffset(8)] public fixed byte Pieces[16]; 14 | [FieldOffset(24)] public short Score = 0; 15 | [FieldOffset(26)] public byte Result = 0; 16 | [FieldOffset(27)] public byte KingSquare = 0; 17 | [FieldOffset(28)] public byte OppKingSquare = 0; 18 | [FieldOffset(29)] public fixed byte _pad[3]; 19 | 20 | public byte[] GetPieces() 21 | { 22 | var buffer = new byte[16]; 23 | 24 | fixed (byte* piecesPtr = Pieces) // This gets the pointer to the Pieces field 25 | { 26 | // Copy the data from the Pieces field to the new array 27 | new Span(piecesPtr, 16).CopyTo(buffer); 28 | } 29 | 30 | return buffer; 31 | } 32 | 33 | public BulletFormat() 34 | { 35 | } 36 | 37 | public static bool IsEqual(BulletFormat a, BulletFormat b) 38 | { 39 | // Cast the structs to byte pointers 40 | var aPtr = (byte*)&a; 41 | var bPtr = (byte*)&b; 42 | 43 | // Iterate over each byte in the struct 44 | for (var i = 0; i < Size; i++) 45 | { 46 | if (*(aPtr + i) != *(bPtr + i)) 47 | { 48 | return false; 49 | } 50 | } 51 | 52 | return true; 53 | } 54 | 55 | public void Read(BinaryReader reader) 56 | { 57 | // Create a temporary buffer to read the data from the BinaryReader 58 | Span buffer = stackalloc byte[Size]; 59 | 60 | // Read the data into the buffer 61 | reader.Read(buffer); 62 | 63 | // Copy the data from the buffer into this struct 64 | fixed (void* buffPtr = &buffer[0], thisPtr = &this) 65 | { 66 | Unsafe.CopyBlock(thisPtr, buffPtr, Size); 67 | } 68 | } 69 | 70 | public void Write(BinaryWriter writer) 71 | { 72 | // Create a temporary buffer to hold the struct data 73 | Span buffer = stackalloc byte[Size]; 74 | 75 | // Copy the struct data into the buffer 76 | fixed (void* buffPtr = &buffer[0], thisPtr = &this) 77 | { 78 | Unsafe.CopyBlock(buffPtr, thisPtr, Size); 79 | } 80 | 81 | // Write the buffer to the BinaryWriter's underlying stream 82 | writer.Write(buffer); 83 | } 84 | 85 | public static BulletFormat Pack(ref BoardStateData board, short score, byte wdl) 86 | { 87 | var data = new BulletFormat(); 88 | Span pieces = stackalloc byte[16]; 89 | 90 | if (board.WhiteToMove) 91 | { 92 | data.KingSquare = board.WhiteKingSquare; 93 | data.OppKingSquare = (byte)(board.BlackKingSquare ^ 0x38); 94 | data.Score = score; 95 | data.Result = wdl; 96 | data.Occupancy = board.Occupancy[Constants.Occupancy]; 97 | 98 | var nextPiece = 0; 99 | 100 | var bits = data.Occupancy; 101 | while (bits != 0) 102 | { 103 | var index = bits.PopLSB(); 104 | var piece = board.GetPiece(index); 105 | var pieceBits = (piece % 2) << 3; 106 | pieceBits |= (piece - 1) / 2; 107 | var offset = 4 * (nextPiece & 1); 108 | pieces[nextPiece >> 1] |= (byte)(pieceBits << offset); 109 | nextPiece++; 110 | } 111 | } 112 | else 113 | { 114 | data.KingSquare = (byte)(board.BlackKingSquare ^ 0x38); 115 | data.OppKingSquare = board.WhiteKingSquare; 116 | data.Score = score; 117 | data.Result = (byte)(-1 * (wdl - 1) + 1); 118 | data.Occupancy = BinaryPrimitives.ReverseEndianness(board.Occupancy[Constants.Occupancy]); 119 | 120 | var nextPiece = 0; 121 | 122 | var bits = data.Occupancy; 123 | while (bits != 0) 124 | { 125 | var index = bits.PopLSB(); 126 | var piece = board.GetPiece(index ^ 0x38); 127 | var pieceBits = ((piece % 2) ^ 1) << 3; 128 | pieceBits |= (piece - 1) / 2; 129 | var offset = 4 * (nextPiece & 1); 130 | pieces[nextPiece >> 1] |= (byte)(pieceBits << offset); 131 | nextPiece++; 132 | } 133 | } 134 | 135 | for (var i = 0; i < pieces.Length; i++) 136 | { 137 | data.Pieces[i] = pieces[i]; 138 | } 139 | 140 | return data; 141 | } 142 | 143 | public void UpdateWdl(bool whiteToMove, byte wdl) 144 | { 145 | // Input 146 | // wdl 0 -> draw 147 | // wdl 1 -> black win 148 | // wdl 2 -> white win 149 | 150 | // Output 151 | // 0 -> Stm looses 152 | // 1 -> Draw 153 | // 2 -> Stm wins 154 | 155 | if (whiteToMove) 156 | { 157 | Result = wdl switch 158 | { 159 | 0 => 1, 160 | 1 => 0, 161 | _ => 2 162 | }; 163 | } 164 | else 165 | { 166 | Result = wdl switch 167 | { 168 | 0 => 1, 169 | 2 => 0, 170 | _ => 2 171 | }; 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/DataGen/Chess960.cs: -------------------------------------------------------------------------------- 1 | namespace Sapling.Engine.DataGen; 2 | 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | public static class Chess960 7 | { 8 | public static string[] Fens; 9 | 10 | static Chess960() 11 | { 12 | char[] board = new char[8]; 13 | var fens = new HashSet(); 14 | GenerateBishops(board, fens); 15 | Fens = fens.ToArray(); 16 | } 17 | 18 | // Step 1: Place bishops on opposite-colored squares 19 | private static void GenerateBishops(char[] board, HashSet fenList) 20 | { 21 | for (int b1 = 0; b1 < 8; b1 += 2) // Dark square bishop 22 | { 23 | for (int b2 = 1; b2 < 8; b2 += 2) // Light square bishop 24 | { 25 | board[b1] = 'B'; 26 | board[b2] = 'B'; 27 | GenerateRooksAndKing(board, fenList); // Proceed to place rooks and king 28 | board[b1] = '\0'; 29 | board[b2] = '\0'; 30 | } 31 | } 32 | } 33 | 34 | // Step 2: Place the king between the two rooks 35 | private static void GenerateRooksAndKing(char[] board, HashSet fenList) 36 | { 37 | for (int r1 = 0; r1 < 8; r1++) 38 | { 39 | if (board[r1] != '\0') continue; // Skip occupied squares (bishops) 40 | 41 | for (int r2 = r1 + 1; r2 < 8; r2++) 42 | { 43 | if (board[r2] != '\0') continue; 44 | 45 | // Ensure the king is placed between the two rooks 46 | for (int k = r1 + 1; k < r2; k++) 47 | { 48 | if (board[k] == '\0') // The square must be free for the king 49 | { 50 | board[r1] = 'R'; 51 | board[r2] = 'R'; 52 | board[k] = 'K'; 53 | GenerateKnightsAndQueen(board, fenList); // Proceed to place knights and queen 54 | board[r1] = '\0'; 55 | board[r2] = '\0'; 56 | board[k] = '\0'; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | // Step 3: Place knights and queen in remaining empty squares 64 | private static void GenerateKnightsAndQueen(char[] board, HashSet fenList) 65 | { 66 | List emptySquares = new List(); 67 | for (int i = 0; i < 8; i++) 68 | { 69 | if (board[i] == '\0') 70 | { 71 | emptySquares.Add(i); 72 | } 73 | } 74 | 75 | // There are 3 empty squares remaining, so permute 'N', 'N', 'Q' 76 | char[] remainingPieces = { 'N', 'N', 'Q' }; 77 | Permute(remainingPieces, 0, board, emptySquares, fenList); 78 | } 79 | 80 | // Step 4: Permute remaining knights and queen, placing them on the board 81 | private static void Permute(char[] pieces, int index, char[] board, List emptySquares, HashSet fenList) 82 | { 83 | if (index == pieces.Length) 84 | { 85 | // Fill the empty squares with the current permutation 86 | for (int i = 0; i < emptySquares.Count; i++) 87 | { 88 | board[emptySquares[i]] = pieces[i]; 89 | } 90 | 91 | // Generate the FEN string with Shredder-style castling rights 92 | var castlingRights = GetShredderCastlingRights(board); 93 | var fen = string.Join("", board).ToLower() + "/pppppppp/8/8/8/8/PPPPPPPP/" + string.Join("", board).ToUpper() + " w " + castlingRights.ToUpper()+ castlingRights.ToLower() + " - 0 1"; 94 | fenList.Add(fen); 95 | 96 | // Clear the board for the next permutation 97 | foreach (var indexToClear in emptySquares) 98 | { 99 | board[indexToClear] = '\0'; 100 | } 101 | 102 | return; 103 | } 104 | 105 | for (int i = index; i < pieces.Length; i++) 106 | { 107 | Swap(ref pieces[index], ref pieces[i]); 108 | Permute(pieces, index + 1, board, emptySquares, fenList); 109 | Swap(ref pieces[index], ref pieces[i]); // Backtrack 110 | } 111 | } 112 | 113 | // Helper function to swap pieces in the permutation 114 | private static void Swap(ref char a, ref char b) 115 | { 116 | (a, b) = (b, a); 117 | } 118 | 119 | // Generate castling rights in Shredder FEN format by checking rook positions 120 | private static string GetShredderCastlingRights(char[] board) 121 | { 122 | string castlingRights = ""; 123 | 124 | // Find the positions of the rooks (R) and convert them to file letters (a-h) 125 | for (int i = 0; i < 8; i++) 126 | { 127 | if (board[i] == 'R') 128 | { 129 | castlingRights += (char)('a' + i); // Convert index to file letter 130 | } 131 | } 132 | 133 | return castlingRights; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Sapling.Engine/DataGen/DataGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.CompilerServices; 3 | using Sapling.Engine.MoveGen; 4 | using Sapling.Engine.Search; 5 | using Sapling.Engine.Transpositions; 6 | 7 | namespace Sapling.Engine.DataGen; 8 | 9 | using System; 10 | 11 | public class DataGeneratorStats 12 | { 13 | public BoardStateData InitialBoard = default; 14 | public readonly object OutputLock = new(); 15 | public int Draws; 16 | public int Looses; 17 | public ulong Positions; 18 | public int Wins; 19 | public Stopwatch Stopwatch = Stopwatch.StartNew(); 20 | public BinaryWriter Writer; 21 | public DataGeneratorStats(BinaryWriter writer) 22 | { 23 | Writer = writer; 24 | InitialBoard.ResetToFen(Constants.InitialState); 25 | } 26 | 27 | public unsafe void Output(Span positions, int positionCount, byte result) 28 | { 29 | // Calculate the total size needed to store all BulletFormat structures 30 | var totalSize = positionCount * BulletFormat.Size; 31 | 32 | // Allocate a buffer on the stack to hold all positions 33 | Span buffer = stackalloc byte[totalSize]; 34 | 35 | // Copy all BulletFormat structures to the buffer 36 | fixed (void* bufferPtr = buffer, positionsPtr = positions) 37 | { 38 | Unsafe.CopyBlock(bufferPtr, positionsPtr, (uint)totalSize); 39 | } 40 | 41 | lock (OutputLock) 42 | { 43 | Positions += (ulong)positionCount; 44 | switch (result) 45 | { 46 | case 2: 47 | Wins++; 48 | break; 49 | case 1: 50 | Looses++; 51 | break; 52 | default: 53 | Draws++; 54 | break; 55 | } 56 | 57 | // Write the entire buffer to the writer 58 | Writer.Write(buffer); 59 | } 60 | } 61 | 62 | 63 | public void Output() 64 | { 65 | lock (OutputLock) 66 | { 67 | Writer.Flush(); 68 | } 69 | 70 | var games = Wins + Draws + Looses; 71 | var elapsed = Stopwatch.Elapsed; 72 | Console.WriteLine( 73 | $"{DateTime.Now:t} duration: {(int)elapsed.TotalHours:D2}:{elapsed.Minutes:D2} Wins: {(Wins * 100 / (float)games).RoundToSignificantFigures(3)}% Draws: {(Draws * 100 / (float)games).RoundToSignificantFigures(3)}% Loses: {(Looses * 100 / (float)games).RoundToSignificantFigures(3)}% Games: {games.FormatBigNumber()} Positions: {Positions.FormatBigNumber()} {(Positions / (float)elapsed.TotalSeconds).RoundToSignificantFigures(3).FormatBigNumber()}/s"); 74 | } 75 | } 76 | public class DataGenerator 77 | { 78 | public const bool Is960 = true; 79 | public const int RandomMoves = 8; 80 | 81 | public const int MaxTurnCount = 500; 82 | 83 | public bool Cancelled = false; 84 | public void Start() 85 | { 86 | Cancelled = false; 87 | #if AVX512 88 | Console.WriteLine("Using Avx-512"); 89 | #else 90 | Console.WriteLine("Using Avx-256"); 91 | #endif 92 | 93 | using var fileStream = new FileStream("./out.bullet", FileMode.Append, FileAccess.Write); 94 | using var writer = new BinaryWriter(fileStream); 95 | var stats = new DataGeneratorStats(writer); 96 | 97 | using var timer = new Timer(_ => 98 | { 99 | if (!Cancelled) 100 | { 101 | stats.Output(); 102 | } 103 | }, null, 10_000, 60_000); 104 | 105 | var threads = Environment.ProcessorCount; 106 | Parallel.For(0, threads, new ParallelOptions() 107 | { 108 | MaxDegreeOfParallelism = threads 109 | }, 110 | _ => 111 | { 112 | RunWorker(stats); 113 | }); 114 | 115 | stats.Output(); 116 | Console.WriteLine("Datagen finished"); 117 | } 118 | 119 | static bool IsAdjudicatedDraw(GameState gameState, int drawScoreCount) 120 | { 121 | return gameState.Board.TurnCount >= 60 && gameState.Board.HalfMoveClock >= 20 && drawScoreCount >= 4; 122 | } 123 | 124 | private unsafe void RunWorker(DataGeneratorStats stats) 125 | { 126 | var ttSize = (int)TranspositionTableExtensions.CalculateTranspositionTableSize(256); 127 | var transpositionTable = MemoryHelpers.Allocate(ttSize); 128 | var searcher = new Searcher(transpositionTable, ttSize); 129 | 130 | BoardStateData boardState = default; 131 | stats.InitialBoard.CloneTo(ref boardState); 132 | 133 | Span turns = stackalloc bool[MaxTurnCount]; 134 | Span dataGenPositions = stackalloc BulletFormat[MaxTurnCount]; 135 | var gameState = new GameState(boardState); 136 | var initialLegalMoves = gameState.LegalMoves.ToArray(); 137 | while (!Cancelled) 138 | { 139 | try 140 | { 141 | var (result, positions) = RunGame(gameState, searcher, turns, dataGenPositions); 142 | for (var index = 0; index < positions; index++) 143 | { 144 | dataGenPositions[index].UpdateWdl(turns[index], result); 145 | } 146 | 147 | stats.Output(dataGenPositions, positions, result); 148 | if (Is960) 149 | { 150 | gameState.ResetToFen(Chess960.Fens[Random.Shared.Next(0, 960)]); 151 | } 152 | else 153 | { 154 | gameState.ResetTo(ref stats.InitialBoard, initialLegalMoves); 155 | } 156 | } 157 | catch (Exception ex) 158 | { 159 | Console.WriteLine(ex); 160 | } 161 | } 162 | 163 | } 164 | 165 | public (byte result, int positions) RunGame(GameState gameState, Searcher searcher, Span turns, Span dataGenPositions) 166 | { 167 | var randomMoveCount = 0; 168 | var positions = 0; 169 | var adjudicationCounter = 0; 170 | var score = 0; 171 | var drawScoreCount = 0; 172 | while (!gameState.GameOver() && gameState.Board.TurnCount < MaxTurnCount && !IsAdjudicatedDraw(gameState, drawScoreCount)) 173 | { 174 | uint move; 175 | if (randomMoveCount <= RandomMoves) 176 | { 177 | move = gameState.LegalMoves[Random.Shared.Next(0, gameState.LegalMoves.Count)]; 178 | randomMoveCount++; 179 | } 180 | else 181 | { 182 | searcher.Reset(gameState); 183 | var (pv, _, s, _) = searcher.Search(gameState, searcher.Stop, nodeLimit: 6500, depthLimit: 60); 184 | move = pv[0]; 185 | score = s; 186 | 187 | if (score == 0) 188 | { 189 | drawScoreCount++; 190 | } 191 | else 192 | { 193 | drawScoreCount = 0; 194 | } 195 | 196 | if (move.IsQuiet() && !gameState.Board.InCheck) 197 | { 198 | turns[positions] = gameState.Board.WhiteToMove; 199 | dataGenPositions[positions] = BulletFormat.Pack(ref gameState.Board, (short)score, 0); 200 | positions++; 201 | } 202 | } 203 | 204 | gameState.Apply(move); 205 | 206 | if (Math.Abs(score) >= 2500) 207 | { 208 | if (++adjudicationCounter > 4) 209 | { 210 | break; 211 | } 212 | } 213 | else 214 | { 215 | adjudicationCounter = 0; 216 | } 217 | } 218 | 219 | byte result; 220 | if (adjudicationCounter > 4) 221 | { 222 | if (gameState.Board.WhiteToMove) 223 | { 224 | result = score > 0 ? (byte)1 : (byte)2; 225 | } 226 | else 227 | { 228 | result = score > 0 ? (byte)2 : (byte)1; 229 | } 230 | } 231 | else if (IsAdjudicatedDraw(gameState, drawScoreCount)) 232 | { 233 | result = 0; 234 | } 235 | else 236 | { 237 | result = gameState.WinDrawLoose(); 238 | } 239 | 240 | return (result, positions); 241 | } 242 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Evaluation/NnueExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Sapling.Engine.Evaluation; 3 | 4 | namespace Sapling.Engine.Search; 5 | 6 | public static unsafe class NnueExtensions 7 | { 8 | #if AVX512 9 | const int VectorSize = 32; // AVX2 operates on 16 shorts (256 bits = 16 x 16 bits) 10 | #else 11 | public const int VectorSize = 16; // AVX2 operates on 16 shorts (256 bits = 16 x 16 bits) 12 | #endif 13 | 14 | public const int AccumulatorSize = NnueWeights.Layer1Size / VectorSize; 15 | 16 | public const int L1ByteSize = sizeof(short) * NnueWeights.Layer1Size; 17 | public const int InputBucketWeightCount = NnueWeights.InputSize * AccumulatorSize; 18 | 19 | public const int BucketDivisor = (32 + NnueWeights.OutputBuckets - 1) / NnueWeights.OutputBuckets; 20 | 21 | private const int ColorStride = 64 * 6; 22 | private const int PieceStride = 64; 23 | 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public static void ApplyQuiet(this ref AccumulatorState accumulatorState, int fromIndex, int toIndex) 26 | { 27 | // Precompute mirroring as integers 28 | var whiteMirrored = accumulatorState.WhiteMirrored ? 1 : 0; 29 | var blackMirrored = accumulatorState.BlackMirrored ? 1 : 0; 30 | 31 | // Precompute the bucket offsets 32 | var wBucketOffset = accumulatorState.WhiteInputBucket * InputBucketWeightCount; 33 | var bBucketOffset = accumulatorState.BlackInputBucket * InputBucketWeightCount; 34 | 35 | // Update the accumulator state with calculated feature updates 36 | accumulatorState.WhiteSubFeatureUpdatesA = wBucketOffset + *(WhiteFeatureIndexes + fromIndex + whiteMirrored); 37 | accumulatorState.BlackSubFeatureUpdatesA = bBucketOffset + *(BlackFeatureIndexes + fromIndex + blackMirrored); 38 | accumulatorState.WhiteAddFeatureUpdatesA = wBucketOffset + *(WhiteFeatureIndexes + toIndex + whiteMirrored); 39 | accumulatorState.BlackAddFeatureUpdatesA = bBucketOffset + *(BlackFeatureIndexes + toIndex + blackMirrored); 40 | 41 | // Set the change type 42 | accumulatorState.ChangeType = AccumulatorChangeType.SubAdd; 43 | } 44 | 45 | 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | public static void ApplyCapture(this ref AccumulatorState accumulatorState, 48 | int fromIndex, int toIndex, 49 | int capIndex) 50 | { 51 | // Precompute mirroring as integers 52 | var whiteMirrored = accumulatorState.WhiteMirrored ? 1 : 0; 53 | var blackMirrored = accumulatorState.BlackMirrored ? 1 : 0; 54 | 55 | // Precompute the bucket offsets 56 | var wBucketOffset = accumulatorState.WhiteInputBucket * InputBucketWeightCount; 57 | var bBucketOffset = accumulatorState.BlackInputBucket * InputBucketWeightCount; 58 | 59 | // Update the accumulator state with calculated feature updates 60 | accumulatorState.WhiteSubFeatureUpdatesA = wBucketOffset + *(WhiteFeatureIndexes + fromIndex + whiteMirrored); 61 | accumulatorState.BlackSubFeatureUpdatesA = bBucketOffset + *(BlackFeatureIndexes + fromIndex + blackMirrored); 62 | accumulatorState.WhiteSubFeatureUpdatesB = wBucketOffset + *(WhiteFeatureIndexes + capIndex + whiteMirrored); 63 | accumulatorState.BlackSubFeatureUpdatesB = bBucketOffset + *(BlackFeatureIndexes + capIndex + blackMirrored); 64 | accumulatorState.WhiteAddFeatureUpdatesA = wBucketOffset + *(WhiteFeatureIndexes + toIndex + whiteMirrored); 65 | accumulatorState.BlackAddFeatureUpdatesA = bBucketOffset + *(BlackFeatureIndexes + toIndex + blackMirrored); 66 | 67 | // Set the change type 68 | accumulatorState.ChangeType = AccumulatorChangeType.SubSubAdd; 69 | } 70 | 71 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 72 | public static void ApplyCastle(this ref AccumulatorState accumulatorState, 73 | int kingFromIndex, int kingToIndex, int rookFromIndex, int rookToIndex) 74 | { 75 | // Precompute mirroring as integers 76 | var whiteMirrored = accumulatorState.WhiteMirrored ? 1 : 0; 77 | var blackMirrored = accumulatorState.BlackMirrored ? 1 : 0; 78 | 79 | // Precompute bucket offsets 80 | var wBucketOffset = accumulatorState.WhiteInputBucket * InputBucketWeightCount; 81 | var bBucketOffset = accumulatorState.BlackInputBucket * InputBucketWeightCount; 82 | 83 | // Update the accumulator state with calculated feature updates 84 | accumulatorState.WhiteSubFeatureUpdatesA = wBucketOffset + *(WhiteFeatureIndexes + kingFromIndex + whiteMirrored); 85 | accumulatorState.BlackSubFeatureUpdatesA = bBucketOffset + *(BlackFeatureIndexes + kingFromIndex + blackMirrored); 86 | accumulatorState.WhiteSubFeatureUpdatesB = wBucketOffset + *(WhiteFeatureIndexes + rookFromIndex + whiteMirrored); 87 | accumulatorState.BlackSubFeatureUpdatesB = bBucketOffset + *(BlackFeatureIndexes + rookFromIndex + blackMirrored); 88 | 89 | accumulatorState.WhiteAddFeatureUpdatesA = wBucketOffset + *(WhiteFeatureIndexes + kingToIndex + whiteMirrored); 90 | accumulatorState.BlackAddFeatureUpdatesA = bBucketOffset + *(BlackFeatureIndexes + kingToIndex + blackMirrored); 91 | accumulatorState.WhiteAddFeatureUpdatesB = wBucketOffset + *(WhiteFeatureIndexes + rookToIndex + whiteMirrored); 92 | accumulatorState.BlackAddFeatureUpdatesB = bBucketOffset + *(BlackFeatureIndexes + rookToIndex + blackMirrored); 93 | 94 | // Set the change type 95 | accumulatorState.ChangeType = AccumulatorChangeType.SubSubAddAdd; 96 | } 97 | 98 | 99 | public static readonly int* WhiteFeatureIndexes; 100 | public static readonly int* BlackFeatureIndexes; 101 | static NnueExtensions() 102 | { 103 | WhiteFeatureIndexes = MemoryHelpers.Allocate(13 * 64 * 2); 104 | BlackFeatureIndexes = MemoryHelpers.Allocate(13 * 64 * 2); 105 | for (byte i = 0; i <= Constants.WhiteKing; i++) 106 | { 107 | for (byte j = 0; j < 64; j++) 108 | { 109 | WhiteFeatureIndexes[i * 64 * 2 + j * 2 + 0] = WhiteFeatureIndices(0, i, j) * AccumulatorSize; 110 | WhiteFeatureIndexes[i * 64 * 2 + j * 2 + 1] = WhiteFeatureIndices(7, i, j) * AccumulatorSize; 111 | 112 | BlackFeatureIndexes[i * 64 * 2 + j * 2 + 0] = BlackFeatureIndices(0, i, j) * AccumulatorSize; 113 | BlackFeatureIndexes[i * 64 * 2 + j * 2 + 1] = BlackFeatureIndices(7, i, j) * AccumulatorSize; 114 | } 115 | } 116 | } 117 | 118 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 119 | private static int WhiteFeatureIndices(int mirrored, int piece, byte square) 120 | { 121 | var white = piece & 1 ^ 1; 122 | var type = (piece >> 1) - white; 123 | 124 | return (white ^ 1) * ColorStride + type * PieceStride + square ^ mirrored; 125 | } 126 | 127 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 128 | private static int BlackFeatureIndices(int mirrored, int piece, byte square) 129 | { 130 | var white = piece & 1 ^ 1; 131 | 132 | var type = (piece >> 1) - white; 133 | return white * ColorStride + type * PieceStride + square ^ mirrored ^ 0x38; 134 | } 135 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Evaluation/NnueWeights.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace Sapling.Engine.Evaluation; 6 | public static unsafe class NnueWeights 7 | { 8 | public const int InputSize = 768; 9 | public const int Layer1Size = 1024; 10 | 11 | public const byte InputBuckets = 4; 12 | public const byte OutputBuckets = 8; 13 | public static readonly unsafe VectorShort* FeatureWeights; 14 | public static readonly unsafe VectorShort* FeatureBiases; 15 | public static readonly unsafe VectorShort* OutputWeights; 16 | public static readonly short[] OutputBiases = new short[OutputBuckets]; 17 | 18 | public static readonly byte* BucketLayout; 19 | 20 | 21 | //public static byte[] BucketLayout = new byte[64] 22 | //{ 23 | // 0, 1, 2, 3, 3, 2, 1, 0, 24 | // 4, 4, 5, 5, 5, 5, 4, 4, 25 | // 6, 6, 6, 6, 6, 6, 6, 6, 26 | // 6, 6, 6, 6, 6, 6, 6, 6, 27 | // 7, 7, 7, 7, 7, 7, 7, 7, 28 | // 7, 7, 7, 7, 7, 7, 7, 7, 29 | // 7, 7, 7, 7, 7, 7, 7, 7, 30 | // 7, 7, 7, 7, 7, 7, 7, 7, 31 | //}; 32 | 33 | static unsafe NnueWeights() 34 | { 35 | var kingBuckets = new byte[64] 36 | { 37 | 0, 0, 1, 1, 1, 1, 0, 0, 38 | 2, 2, 2, 2, 2, 2, 2, 2, 39 | 3, 3, 3, 3, 3, 3, 3, 3, 40 | 3, 3, 3, 3, 3, 3, 3, 3, 41 | 3, 3, 3, 3, 3, 3, 3, 3, 42 | 3, 3, 3, 3, 3, 3, 3, 3, 43 | 3, 3, 3, 3, 3, 3, 3, 3, 44 | 3, 3, 3, 3, 3, 3, 3, 3, 45 | }; 46 | 47 | BucketLayout = AlignedAllocZeroedByte(64); 48 | fixed (byte* kingBucketsPtr = kingBuckets) // Pin the array in memory 49 | { 50 | // Copy the data from kingBucketsPtr to BucketLayout 51 | Buffer.MemoryCopy(kingBucketsPtr, BucketLayout, 64, 64); 52 | } 53 | 54 | 55 | var assembly = Assembly.GetAssembly(typeof(GameState)); 56 | var info = assembly.GetName(); 57 | var name = info.Name; 58 | using var stream = assembly 59 | .GetManifestResourceStream($"{name}.Resources.sapling.nnue")!; 60 | 61 | var featureWeights = new short[InputSize * Layer1Size * InputBuckets]; 62 | var featureBiases = new short[Layer1Size]; 63 | var outputWeights = new short[Layer1Size * 2 * OutputBuckets]; 64 | 65 | using var reader = new BinaryReader(stream, Encoding.UTF8, false); 66 | for (var i = 0; i < featureWeights.Length; i++) 67 | { 68 | featureWeights[i] = reader.ReadInt16(); 69 | } 70 | 71 | for (var i = 0; i < featureBiases.Length; i++) 72 | { 73 | featureBiases[i] = reader.ReadInt16(); 74 | } 75 | 76 | for (var i = 0; i < outputWeights.Length; i++) 77 | { 78 | outputWeights[i] = reader.ReadInt16(); 79 | } 80 | 81 | var transposedWeights = new short[outputWeights.Length]; 82 | 83 | // Transposing logic 84 | for (var i = 0; i < 2 * Layer1Size; i++) 85 | { 86 | for (var j = 0; j < OutputBuckets; j++) 87 | { 88 | // Original index calculation 89 | var originalIndex = i * OutputBuckets + j; 90 | 91 | // Transposed index calculation 92 | var transposedIndex = j * 2 * Layer1Size + i; 93 | 94 | // Assign value to transposed position 95 | transposedWeights[transposedIndex] = outputWeights[originalIndex]; 96 | } 97 | } 98 | 99 | outputWeights = transposedWeights; 100 | 101 | for (var i = 0; i < OutputBiases.Length; i++) 102 | { 103 | OutputBiases[i] = reader.ReadInt16(); 104 | } 105 | 106 | //var result = Encoding.UTF8.GetString(reader.ReadBytes((int)(reader.BaseStream.Length - reader.BaseStream.Position))); 107 | // Console.WriteLine(result); 108 | 109 | // Allocate unmanaged memory 110 | FeatureWeights = AlignedAllocZeroedShort((nuint)featureWeights.Length); 111 | FeatureBiases = AlignedAllocZeroedShort((nuint)featureBiases.Length); 112 | OutputWeights = AlignedAllocZeroedShort((nuint)outputWeights.Length); 113 | 114 | // Copy managed array to unmanaged memory 115 | fixed (short* sourcePtr = featureWeights) 116 | { 117 | Buffer.MemoryCopy(sourcePtr, FeatureWeights, featureWeights.Length * sizeof(short), 118 | featureWeights.Length * sizeof(short)); 119 | } 120 | 121 | fixed (short* sourcePtr = featureBiases) 122 | { 123 | Buffer.MemoryCopy(sourcePtr, FeatureBiases, featureBiases.Length * sizeof(short), 124 | featureBiases.Length * sizeof(short)); 125 | } 126 | 127 | fixed (short* sourcePtr = outputWeights) 128 | { 129 | Buffer.MemoryCopy(sourcePtr, OutputWeights, outputWeights.Length * sizeof(short), 130 | outputWeights.Length * sizeof(short)); 131 | } 132 | } 133 | 134 | public static unsafe byte* AlignedAllocZeroedByte(nuint items) 135 | { 136 | const nuint alignment = 64; 137 | var bytes = sizeof(short) * items; 138 | var block = NativeMemory.AlignedAlloc(bytes, alignment); 139 | if (block == null) 140 | { 141 | throw new OutOfMemoryException("Failed to allocate aligned memory."); 142 | } 143 | 144 | NativeMemory.Clear(block, bytes); 145 | return (byte*)block; 146 | } 147 | public static unsafe VectorShort* AlignedAllocZeroedShort(nuint items) 148 | { 149 | const nuint alignment = 64; 150 | var bytes = sizeof(short) * items; 151 | var block = NativeMemory.AlignedAlloc(bytes, alignment); 152 | if (block == null) 153 | { 154 | throw new OutOfMemoryException("Failed to allocate aligned memory."); 155 | } 156 | 157 | NativeMemory.Clear(block, bytes); 158 | return (VectorShort*)block; 159 | } 160 | 161 | public static unsafe void Dispose() 162 | { 163 | if (FeatureWeights != null) 164 | { 165 | Marshal.FreeHGlobal((IntPtr)FeatureWeights); 166 | } 167 | 168 | if (FeatureBiases != null) 169 | { 170 | Marshal.FreeHGlobal((IntPtr)FeatureBiases); 171 | } 172 | 173 | if (OutputWeights != null) 174 | { 175 | Marshal.FreeHGlobal((IntPtr)OutputWeights); 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Evaluation/StaticExchangeEvaluator.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.Intrinsics.X86; 3 | using Sapling.Engine.MoveGen; 4 | 5 | namespace Sapling.Engine.Evaluation; 6 | 7 | public static class StaticExchangeEvaluator 8 | { 9 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 10 | public static unsafe int StaticExchangeEvaluation(this ref BoardStateData board, ulong* occupancyBitBoards, 11 | short* captures, uint move) 12 | { 13 | *captures = *(PieceValues.PieceValue + move.GetCapturedPiece()); 14 | var lastPieceValue = *(PieceValues.PieceValue + move.GetMovedPiece()); 15 | 16 | if (*captures - lastPieceValue > 0) 17 | { 18 | // Return if we give up our piece but the exchange is still positive. 19 | return *captures - lastPieceValue; 20 | } 21 | 22 | var targetSquare = move.GetToSquare(); 23 | // all pieces except the two involved in the initial move 24 | var occupancy = board.Occupancy[Constants.Occupancy] & ~((1UL << move.GetFromSquare()) | (1UL << targetSquare)); 25 | 26 | // Bit boards by piece type 27 | var pawns = *(occupancyBitBoards + 2); 28 | var knights = *(occupancyBitBoards + 3); 29 | var bishops = *(occupancyBitBoards + 4); 30 | var rooks = *(occupancyBitBoards + 5); 31 | var queens = *(occupancyBitBoards + 6); 32 | var kings = *(occupancyBitBoards + 7); 33 | 34 | // Calculate all squares that can attack the target square 35 | var attackers = ((AttackTables.PextBishopAttacks(occupancy, targetSquare) & (queens | bishops)) | 36 | (AttackTables.PextRookAttacks(occupancy, targetSquare) & (queens | rooks)) | 37 | (*(AttackTables.KnightAttackTable + targetSquare) & knights) | 38 | (*(AttackTables.WhitePawnAttackTable + targetSquare) & board.Occupancy[Constants.BlackPawn]) | 39 | (*(AttackTables.BlackPawnAttackTable + targetSquare) & board.Occupancy[Constants.WhitePawn]) | 40 | (*(AttackTables.KingAttackTable + targetSquare) & kings)) & occupancy; 41 | 42 | // Starts off as the opponents turn 43 | var turn = board.WhiteToMove ? 1 : 0; 44 | var cIndex = 1; 45 | do 46 | { 47 | var remainingAttackers = attackers & *(occupancyBitBoards +turn) & occupancy; 48 | 49 | if (remainingAttackers == 0) 50 | { 51 | // No attacks left on the target square 52 | break; 53 | } 54 | 55 | *(captures + cIndex) = (short)(-*(captures + cIndex - 1) + lastPieceValue); 56 | 57 | // Remove the least valuable attacker 58 | ulong attacker; 59 | if ((attacker = pawns & remainingAttackers) != 0) 60 | { 61 | lastPieceValue = Constants.PawnValue; 62 | } 63 | else if ((attacker = knights & remainingAttackers) != 0) 64 | { 65 | lastPieceValue = Constants.KnightValue; 66 | } 67 | else if ((attacker = bishops & remainingAttackers) != 0) 68 | { 69 | lastPieceValue = Constants.BishopValue; 70 | } 71 | else if ((attacker = rooks & remainingAttackers) != 0) 72 | { 73 | lastPieceValue = Constants.RookValue; 74 | } 75 | else if ((attacker = queens & remainingAttackers) != 0) 76 | { 77 | lastPieceValue = Constants.QueenValue; 78 | } 79 | else if ((attacker = kings & remainingAttackers) != 0) 80 | { 81 | lastPieceValue = Constants.KingValue; 82 | } 83 | 84 | // Attacker is removed from occupancy 85 | occupancy &= ~(1UL << (byte)Bmi1.X64.TrailingZeroCount(attacker)); 86 | 87 | // Update attackers with any discovered attacks 88 | attackers |= ((AttackTables.PextBishopAttacks(occupancy, targetSquare) & (bishops | queens)) | 89 | (AttackTables.PextRookAttacks(occupancy, targetSquare) & (rooks | queens))) & occupancy; 90 | 91 | // Flip turn 92 | turn ^= 1; 93 | } while (*(captures + cIndex++) - lastPieceValue <= 0); 94 | 95 | for (var n = cIndex - 1; n > 0; n--) 96 | { 97 | *(captures + n - 1) = (short)-Math.Max(-*(captures + n - 1), *(captures + n)); 98 | } 99 | 100 | return *captures; 101 | } 102 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/GameState.cs: -------------------------------------------------------------------------------- 1 | using Sapling.Engine.MoveGen; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Sapling.Engine; 5 | 6 | public sealed unsafe class GameState 7 | { 8 | public readonly List History; 9 | public readonly List LegalMoves; 10 | public readonly ulong* HashHistory; 11 | public BoardStateData Board = default; 12 | 13 | ~GameState() 14 | { 15 | NativeMemory.AlignedFree(HashHistory); 16 | } 17 | 18 | public GameState(BoardStateData board) 19 | { 20 | HashHistory = MemoryHelpers.Allocate(800); 21 | Board = board; 22 | History = new List(); 23 | LegalMoves = new List(); 24 | Board.GenerateLegalMoves(LegalMoves, false); 25 | *(HashHistory + Board.TurnCount - 1) = Board.Hash; 26 | } 27 | 28 | public static GameState InitialState() 29 | { 30 | return new GameState(BoardStateExtensions.CreateBoardFromFen(Constants.InitialState)); 31 | } 32 | 33 | public void ResetTo(ref BoardStateData newBoard) 34 | { 35 | History.Clear(); 36 | newBoard.CloneTo(ref Board); 37 | Board.GenerateLegalMoves(LegalMoves, false); 38 | *(HashHistory + Board.TurnCount - 1) = Board.Hash; 39 | } 40 | public void ResetToFen(string fen) 41 | { 42 | History.Clear(); 43 | var state = BoardStateExtensions.CreateBoardFromFen(fen); 44 | state.CloneTo(ref Board); 45 | Board.GenerateLegalMoves(LegalMoves, false); 46 | *(HashHistory + Board.TurnCount - 1) = Board.Hash; 47 | } 48 | 49 | public void Reset() 50 | { 51 | History.Clear(); 52 | var state = Constants.InitialBoard; 53 | state.CloneTo(ref Board); 54 | Board.GenerateLegalMoves(LegalMoves, false); 55 | *(HashHistory + Board.TurnCount - 1) = Board.Hash; 56 | } 57 | 58 | public void ResetTo(ref BoardStateData newBoard, uint[] legalMoves) 59 | { 60 | History.Clear(); 61 | newBoard.CloneTo(ref Board); 62 | LegalMoves.Clear(); 63 | LegalMoves.AddRange(legalMoves); 64 | *(HashHistory + Board.TurnCount - 1) = Board.Hash; 65 | } 66 | public void Apply(uint move) 67 | { 68 | var oldEnpassant = Board.EnPassantFile; 69 | var oldCastle = Board.CastleRights; 70 | 71 | AccumulatorState emptyAccumulator = default; 72 | var whiteToMove = Board.WhiteToMove; 73 | if (whiteToMove) 74 | { 75 | Board.PartialApplyWhite(move); 76 | } 77 | else 78 | { 79 | Board.PartialApplyBlack(move); 80 | } 81 | 82 | Board.UpdateCheckStatus(); 83 | 84 | fixed (BoardStateData* boardPtr = &Board) 85 | { 86 | // Copy the memory block from source to destination 87 | 88 | if (whiteToMove) 89 | { 90 | boardPtr->FinishApplyWhite(ref emptyAccumulator, move, oldEnpassant, oldCastle); 91 | } 92 | else 93 | { 94 | boardPtr->FinishApplyBlack(ref emptyAccumulator, move, oldEnpassant, oldCastle); 95 | } 96 | } 97 | 98 | Board.GenerateLegalMoves(LegalMoves, false); 99 | History.Add(move); 100 | *(HashHistory + Board.TurnCount - 1) = Board.Hash; 101 | } 102 | 103 | public bool GameOver() 104 | { 105 | return LegalMoves.Count == 0 || Board.HalfMoveClock >= 100 || Board.InsufficientMatingMaterial(); 106 | } 107 | 108 | public byte WinDrawLoose() 109 | { 110 | if (LegalMoves.Count != 0) 111 | { 112 | // Draw 113 | return 0; 114 | } 115 | 116 | if (Board.WhiteToMove) 117 | { 118 | // Black wins 119 | return 1; 120 | } 121 | 122 | // White wins 123 | return 2; 124 | } 125 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // GlobalUsings.cs 2 | #if AVX512 3 | global using AvxIntrinsics = System.Runtime.Intrinsics.X86.Avx512BW; 4 | global using VectorType = System.Runtime.Intrinsics.Vector512; 5 | global using VectorInt = System.Runtime.Intrinsics.Vector512; 6 | global using VectorShort = System.Runtime.Intrinsics.Vector512; 7 | #else 8 | global using AvxIntrinsics = System.Runtime.Intrinsics.X86.Avx2; 9 | global using VectorType = System.Runtime.Intrinsics.Vector256; 10 | global using VectorInt = System.Runtime.Intrinsics.Vector256; 11 | global using VectorShort = System.Runtime.Intrinsics.Vector256; 12 | #endif -------------------------------------------------------------------------------- /src/Sapling.Engine/MagicBitBoard.cs: -------------------------------------------------------------------------------- 1 | namespace Sapling.Engine; 2 | 3 | public sealed record MagicBitBoard(ulong MagicNumber, ulong MovementMask, int Position, ulong[] Moves) 4 | { 5 | public ulong GetMoves(ulong blockers) 6 | { 7 | return Moves[((MovementMask & blockers) * MagicNumber) >> Position]; 8 | } 9 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/MathHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Sapling.Engine 5 | { 6 | public static unsafe class MemoryHelpers 7 | { 8 | public static unsafe T* Allocate(long count) where T : unmanaged 9 | { 10 | if (count <= 0) 11 | throw new ArgumentOutOfRangeException(nameof(count), "Count must be positive."); 12 | 13 | const ulong alignment = 64; 14 | 15 | // Use ulong to prevent overflow when calculating size 16 | ulong elementSize = (ulong)sizeof(T); 17 | ulong totalSize = elementSize * (ulong)count; 18 | 19 | // Basic safety check: 128 TB max (adjust based on your app domain) 20 | const ulong maxAllowedSize = 128UL * 1024 * 1024 * 1024 * 1024; // 128 TB 21 | if (totalSize > maxAllowedSize) 22 | throw new ArgumentOutOfRangeException(nameof(count), $"Requested size exceeds {maxAllowedSize / (1024 * 1024 * 1024)} GB."); 23 | 24 | void* block = NativeMemory.AlignedAlloc((nuint)totalSize, (nuint)alignment); 25 | 26 | if (block == null) 27 | throw new OutOfMemoryException($"Failed to allocate {(totalSize / (1024 * 1024))} MB for {typeof(T).Name}."); 28 | 29 | NativeMemory.Clear(block, (nuint)totalSize); 30 | 31 | return (T*)block; 32 | } 33 | 34 | public static unsafe T* AllocateMultiThreaded(long count) where T : unmanaged 35 | { 36 | if (count <= 0) 37 | throw new ArgumentOutOfRangeException(nameof(count), "Count must be positive."); 38 | 39 | const ulong alignment = 64; 40 | ulong elementSize = (ulong)sizeof(T); 41 | ulong totalSize = elementSize * (ulong)count; 42 | 43 | const ulong maxAllowedSize = 128UL * 1024 * 1024 * 1024 * 1024; // 128 TB 44 | if (totalSize > maxAllowedSize) 45 | throw new ArgumentOutOfRangeException(nameof(count), $"Requested size exceeds {maxAllowedSize / (1024 * 1024 * 1024)} GB."); 46 | 47 | void* block = NativeMemory.AlignedAlloc((nuint)totalSize, (nuint)alignment); 48 | if (block == null) 49 | throw new OutOfMemoryException($"Failed to allocate {(totalSize / (1024 * 1024))} MB for {typeof(T).Name}."); 50 | 51 | // Parallel zero-init 52 | int threadCount = Environment.ProcessorCount; 53 | long chunkSize = count / threadCount; 54 | var tasks = new Task[threadCount]; 55 | 56 | for (int i = 0; i < threadCount; i++) 57 | { 58 | long start = i * chunkSize; 59 | long length = (i == threadCount - 1) ? count - start : chunkSize; 60 | 61 | tasks[i] = Task.Run(() => 62 | { 63 | byte* byteStart = (byte*)block + start * sizeof(T); 64 | Unsafe.InitBlockUnaligned(byteStart, 0, (uint)(length * sizeof(T))); 65 | }); 66 | } 67 | 68 | Task.WaitAll(tasks); 69 | return (T*)block; 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Sapling.Engine/MoveGen/MoveExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Sapling.Engine.MoveGen; 4 | 5 | public static class MoveExtensions 6 | { 7 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 8 | public static byte GetMovedPiece(this uint move) 9 | { 10 | return (byte)(move & 0x0F); 11 | } 12 | 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public static uint GetCounterMoveIndex(this uint move) 15 | { 16 | return ((move & 0x0F) << 6) + ((move >> 10) & 0x3F); 17 | } 18 | 19 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 20 | public static bool IsPawn(this uint move) 21 | { 22 | uint piece = move & 0x0F; // 0x0F masks the lower 4 bits 23 | return piece == Constants.WhitePawn || piece == Constants.BlackPawn; 24 | } 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public static bool IsRook(this uint move) 28 | { 29 | uint piece = move & 0x0F; // 0x0F masks the lower 4 bits 30 | return piece == Constants.WhiteRook || piece == Constants.BlackRook; 31 | } 32 | 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public static bool IsKnight(this uint move) 35 | { 36 | uint piece = move & 0x0F; // 0x0F masks the lower 4 bits 37 | return piece == Constants.WhiteKnight || piece == Constants.BlackKnight; 38 | } 39 | 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | public static bool IsBishop(this uint move) 42 | { 43 | uint piece = move & 0x0F; // 0x0F masks the lower 4 bits 44 | return piece == Constants.WhiteBishop || piece == Constants.BlackBishop; 45 | } 46 | 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public static bool IsQueen(this uint move) 49 | { 50 | uint piece = move & 0x0F; // 0x0F masks the lower 4 bits 51 | return piece == Constants.WhiteQueen || piece == Constants.BlackQueen; 52 | } 53 | 54 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 55 | public static bool IsKing(this uint move) 56 | { 57 | uint piece = move & 0x0F; // 0x0F masks the lower 4 bits 58 | return piece == Constants.WhiteKing || piece == Constants.BlackKing; 59 | } 60 | 61 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 62 | public static bool IsEnPassant(this uint move) 63 | { 64 | return ((move >> 20) & 0x0F) == Constants.EnPassant; 65 | } 66 | 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | public static bool IsCastle(this uint move) 69 | { 70 | return ((move >> 20) & 0x0F) == Constants.Castle; 71 | } 72 | 73 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 74 | public static bool IsPromotion(this uint move) 75 | { 76 | return ((move >> 20) & 0x0F) >= 4; 77 | } 78 | 79 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 80 | public static bool IsRookPromotion(this uint move) 81 | { 82 | return ((move >> 20) & 0x0F) == Constants.PawnRookPromotion; 83 | } 84 | 85 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 86 | public static bool IsKnightPromotion(this uint move) 87 | { 88 | return ((move >> 20) & 0x0F) == Constants.PawnKnightPromotion; 89 | } 90 | 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | public static bool IsBishopPromotion(this uint move) 93 | { 94 | return ((move >> 20) & 0x0F) == Constants.PawnBishopPromotion; 95 | } 96 | 97 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 98 | public static bool IsQueenPromotion(this uint move) 99 | { 100 | return ((move >> 20) & 0x0F) == Constants.PawnQueenPromotion; 101 | } 102 | 103 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 104 | public static byte GetFromSquare(this uint move) 105 | { 106 | return (byte)((move >> 4) & 0x3F); 107 | } 108 | 109 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 110 | public static byte GetToSquare(this uint move) 111 | { 112 | return (byte)((move >> 10) & 0x3F); 113 | } 114 | 115 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 116 | public static byte GetCapturedPiece(this uint move) 117 | { 118 | return (byte)((move >> 16) & 0x0F); 119 | } 120 | 121 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 122 | public static byte GetMoveType(this uint move) 123 | { 124 | return (byte)((move >> 20) & 0x0F); 125 | } 126 | 127 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 128 | public static bool IsPromotionThreat(this uint move) 129 | { 130 | var movedPiece = move.GetMovedPiece(); 131 | 132 | if (movedPiece == Constants.WhitePawn) 133 | { 134 | return move.GetToSquare().GetRankIndex() >= 5; 135 | } 136 | 137 | if (movedPiece == Constants.BlackPawn) 138 | { 139 | return move.GetToSquare().GetRankIndex() <= 3; 140 | } 141 | 142 | return false; 143 | } 144 | 145 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 146 | public static uint EncodeCastleMove( 147 | byte movedPiece, 148 | byte fromSquare, 149 | byte toSquare) 150 | { 151 | return (uint)(movedPiece | 152 | (fromSquare << 4) | 153 | (toSquare << 10) | 154 | (Constants.Castle << 20)); 155 | } 156 | 157 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 158 | public static uint EncodeCaptureMove( 159 | byte movedPiece, 160 | byte fromSquare, 161 | byte capturedPiece, 162 | byte toSquare) 163 | { 164 | return (uint)(movedPiece | 165 | (fromSquare << 4) | 166 | (toSquare << 10) | 167 | (capturedPiece << 16)); 168 | } 169 | 170 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 171 | public static uint EncodeWhiteDoublePushMove( 172 | byte fromSquare, 173 | byte toSquare) 174 | { 175 | return (uint)(Constants.WhitePawn | 176 | (fromSquare << 4) | 177 | (toSquare << 10) | 178 | (Constants.DoublePush << 20)); 179 | } 180 | 181 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 182 | public static uint EncodeBlackDoublePushMove( 183 | byte fromSquare, 184 | byte toSquare) 185 | { 186 | return (uint)(Constants.BlackPawn | 187 | (fromSquare << 4) | 188 | (toSquare << 10) | 189 | (Constants.DoublePush << 20)); 190 | } 191 | 192 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 193 | public static uint EncodeCapturePromotionMove( 194 | byte movedPiece, 195 | byte fromSquare, 196 | byte capturedPiece, 197 | byte toSquare, 198 | byte moveType) 199 | { 200 | return (uint)(movedPiece | 201 | (fromSquare << 4) | 202 | (toSquare << 10) | 203 | (capturedPiece << 16) | 204 | (moveType << 20)); 205 | } 206 | 207 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 208 | public static uint EncodePromotionMove( 209 | byte movedPiece, 210 | byte fromSquare, 211 | byte toSquare, 212 | byte moveType) 213 | { 214 | return (uint)(movedPiece | 215 | (fromSquare << 4) | 216 | (toSquare << 10) | 217 | (moveType << 20)); 218 | } 219 | 220 | private const int whiteEnpassantOffset = 5 * 8; 221 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 222 | public static uint EncodeWhiteEnpassantMove( 223 | byte fromSquare, 224 | byte enpassantFile) 225 | { 226 | return (uint)(Constants.WhitePawn | 227 | (fromSquare << 4) | 228 | ((whiteEnpassantOffset + enpassantFile) << 10) | 229 | (Constants.BlackPawn << 16) | 230 | (Constants.EnPassant << 20)); 231 | } 232 | 233 | private const int blackEnpassantOffset = 2 * 8; 234 | 235 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 236 | public static uint EncodeBlackEnpassantMove( 237 | byte fromSquare, 238 | byte enpassantFile) 239 | { 240 | return (uint)(Constants.BlackPawn | 241 | (fromSquare << 4) | 242 | ((blackEnpassantOffset + enpassantFile) << 10) | 243 | (Constants.WhitePawn << 16) | 244 | (Constants.EnPassant << 20)); 245 | } 246 | 247 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 248 | public static uint EncodeNormalMove( 249 | int movedPiece, 250 | int fromSquare, 251 | int toSquare) 252 | { 253 | return (uint)(movedPiece | 254 | (fromSquare << 4) | 255 | (toSquare << 10)); 256 | } 257 | 258 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 259 | public static bool IsReset(this uint move) 260 | { 261 | return move.IsCapture() || move.IsPawn(); 262 | } 263 | 264 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 265 | public static bool IsCapture(this uint move) 266 | { 267 | return ((move >> 16) & 0x0F) != Constants.None; 268 | } 269 | 270 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 271 | public static bool IsQuiet(this uint move) 272 | { 273 | // Not a capture or promotion 274 | return ((move >> 16) & 0x0F) == Constants.None && ((move >> 20) & 0x0F) < 4; 275 | } 276 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/MoveGen/MoveScoring.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Sapling.Engine.MoveGen; 4 | 5 | public static class MoveScoring 6 | { 7 | private const short MaxMateDepth = 1000; 8 | 9 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 10 | public static short EvaluateFinalPosition(int ply, bool isInCheck) 11 | { 12 | if (isInCheck) 13 | { 14 | return (short)(-Constants.ImmediateMateScore + ply); 15 | } 16 | 17 | return 0; 18 | } 19 | 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public static bool IsMateScore(int score) 22 | { 23 | if (score == int.MinValue) 24 | { 25 | return false; 26 | } 27 | 28 | return Math.Abs(score) > Constants.ImmediateMateScore - MaxMateDepth; 29 | } 30 | 31 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 32 | public static int GetMateDistance(int score) 33 | { 34 | return (Constants.ImmediateMateScore - Math.Abs(score) + 1) / 2; 35 | } 36 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/MoveGen/MoveType.cs: -------------------------------------------------------------------------------- 1 | namespace Sapling.Engine.MoveGen; 2 | 3 | public enum MoveType : byte 4 | { 5 | Normal = 0, 6 | Castle = 1, 7 | DoublePush = 2, 8 | EnPassant = 3, 9 | PawnRookPromotion = 4, 10 | PawnKnightPromotion = 5, 11 | PawnBishopPromotion = 6, 12 | PawnQueenPromotion = 7 13 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Pgn/Lexer.cs: -------------------------------------------------------------------------------- 1 | namespace Sapling.Engine.Pgn; 2 | 3 | public static class Lexer 4 | { 5 | public const string QueenSideCastleKeyword = "O-O-O"; 6 | public const string KingSideCastleKeyword = "O-O"; 7 | 8 | internal static TokenType? MapToken(char token) 9 | { 10 | return token switch 11 | { 12 | '1' => TokenType.Rank, 13 | '2' => TokenType.Rank, 14 | '3' => TokenType.Rank, 15 | '4' => TokenType.Rank, 16 | '5' => TokenType.Rank, 17 | '6' => TokenType.Rank, 18 | '7' => TokenType.Rank, 19 | '8' => TokenType.Rank, 20 | 'a' => TokenType.File, 21 | 'b' => TokenType.File, 22 | 'c' => TokenType.File, 23 | 'd' => TokenType.File, 24 | 'e' => TokenType.File, 25 | 'f' => TokenType.File, 26 | 'g' => TokenType.File, 27 | 'h' => TokenType.File, 28 | 'x' => TokenType.Capture, 29 | '+' => TokenType.Check, 30 | 'P' => TokenType.Pawn, 31 | 'N' => TokenType.Knight, 32 | 'R' => TokenType.Rook, 33 | 'B' => TokenType.Bishop, 34 | 'Q' => TokenType.Queen, 35 | 'K' => TokenType.King, 36 | '=' => TokenType.Promotion, 37 | '#' => TokenType.Checkmate, 38 | '\n' => TokenType.NewLine, 39 | _ => null 40 | }; 41 | } 42 | 43 | internal static TokenType MapKeyword(ReadOnlySpan token) 44 | { 45 | if (token.SequenceEqual(QueenSideCastleKeyword.AsSpan())) 46 | { 47 | return TokenType.QueenSideCastle; 48 | } 49 | 50 | if (token.SequenceEqual(KingSideCastleKeyword.AsSpan())) 51 | { 52 | return TokenType.KingSideCastle; 53 | } 54 | 55 | throw new Exception("Unrecognized keyword"); 56 | } 57 | 58 | public static Token NextToken(ReadOnlySpan input, ref int position) 59 | { 60 | if (position >= input.Length) 61 | { 62 | // End of file 63 | return new Token 64 | { 65 | Start = -1, 66 | Length = -1, 67 | TokenType = TokenType.Eof 68 | }; 69 | } 70 | 71 | if (input[position] == ' ') 72 | { 73 | // Ignore all white space 74 | while (position < input.Length && input[position] == ' ') 75 | // Advance until current character is no longer white space 76 | { 77 | position++; 78 | } 79 | 80 | if (position >= input.Length) 81 | { 82 | // End of file 83 | return new Token 84 | { 85 | Start = -1, 86 | Length = -1, 87 | TokenType = TokenType.Eof 88 | }; 89 | } 90 | } 91 | 92 | // Next token must be a single character token 93 | var token = MapToken(input[position]); 94 | 95 | if (token != null) 96 | { 97 | position++; 98 | return new Token 99 | { 100 | Start = position - 1, 101 | Length = 1, 102 | TokenType = token.Value 103 | }; 104 | } 105 | 106 | var start = position; 107 | while (position < input.Length && input[position] is 'O' or '-') 108 | // Advance until the end of the key word 109 | { 110 | position++; 111 | } 112 | 113 | if (position == start) 114 | { 115 | return new Token 116 | { 117 | Start = -1, 118 | Length = -1, 119 | TokenType = TokenType.Eof 120 | }; 121 | } 122 | 123 | return new Token 124 | { 125 | Start = start, 126 | Length = position - start, 127 | TokenType = MapKeyword(input.Slice(start, position - start)) 128 | }; 129 | } 130 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Pgn/PgnParser.cs: -------------------------------------------------------------------------------- 1 | using Sapling.Engine.MoveGen; 2 | 3 | namespace Sapling.Engine.Pgn; 4 | 5 | public static class PgnParser 6 | { 7 | public static uint Parse(ReadOnlySpan input, IEnumerable moves) 8 | { 9 | var positions = new List(); 10 | var position = 0; 11 | var token = Lexer.NextToken(input, ref position); 12 | var positionOnly = true; 13 | 14 | while (token.TokenType != TokenType.Eof) 15 | { 16 | switch (token.TokenType) 17 | { 18 | case TokenType.Capture: 19 | moves = moves.Where(m => m.IsCapture()); 20 | break; 21 | case TokenType.Pawn: 22 | moves = moves.Where(m => m.IsPawn()); 23 | break; 24 | case TokenType.Knight: 25 | positionOnly = false; 26 | moves = moves.Where(m => m.IsKnight()); 27 | break; 28 | case TokenType.Rook: 29 | positionOnly = false; 30 | moves = moves.Where(m => m.IsRook()); 31 | break; 32 | case TokenType.Bishop: 33 | positionOnly = false; 34 | moves = moves.Where(m => m.IsBishop()); 35 | break; 36 | case TokenType.Queen: 37 | positionOnly = false; 38 | moves = moves.Where(m => m.IsQueen()); 39 | break; 40 | case TokenType.King: 41 | positionOnly = false; 42 | moves = moves.Where(m => m.IsKing()); 43 | break; 44 | case TokenType.File: 45 | case TokenType.Rank: 46 | positions.Add(input[token.Start]); 47 | break; 48 | case TokenType.KingSideCastle: 49 | positionOnly = false; 50 | moves = moves.Where(m => m.IsCastle() && m.GetToSquare() % 8 == 6); 51 | break; 52 | case TokenType.QueenSideCastle: 53 | positionOnly = false; 54 | moves = moves.Where(m => m.IsCastle() && m.GetToSquare() % 8 == 2); 55 | break; 56 | case TokenType.Promotion: 57 | { 58 | token = Lexer.NextToken(input, ref position); 59 | moves = token.TokenType switch 60 | { 61 | TokenType.Bishop => moves.Where(m => m.IsBishopPromotion()), 62 | TokenType.Knight => moves.Where(m => m.IsKnightPromotion()), 63 | TokenType.Rook => moves.Where(m => m.IsRookPromotion()), 64 | TokenType.Queen => moves.Where(m => m.IsQueenPromotion()), 65 | _ => moves 66 | }; 67 | 68 | break; 69 | } 70 | } 71 | 72 | token = Lexer.NextToken(input, ref position); 73 | } 74 | 75 | if (positionOnly) 76 | { 77 | moves = moves.Where(m => m.IsPawn()); 78 | } 79 | 80 | if (positions.Count <= 0) 81 | { 82 | return moves.FirstOrDefault(); 83 | } 84 | 85 | var toSquare = PgnSplitter.GetSquare(positions[^2], positions[^1]); 86 | moves = moves.Where(m => m.GetToSquare() == toSquare); 87 | switch (positions.Count) 88 | { 89 | case 3: 90 | { 91 | var c = positions[0]; 92 | if (char.IsLetter(c)) 93 | { 94 | var file = PgnSplitter.GetFile(c); 95 | moves = moves.Where(m => m.GetFromSquare() % 8 == file); 96 | } 97 | else 98 | { 99 | var rank = PgnSplitter.GetRank(c); 100 | moves = moves.Where(m => m.GetFromSquare() / 8 == rank); 101 | } 102 | 103 | break; 104 | } 105 | case 4: 106 | { 107 | var fromSquare = PgnSplitter.GetSquare(positions[0], positions[1]); 108 | moves = moves.Where(m => m.GetFromSquare() == fromSquare); 109 | break; 110 | } 111 | } 112 | 113 | return moves.FirstOrDefault(); 114 | } 115 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Pgn/PgnSplitter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Sapling.Engine.MoveGen; 3 | 4 | namespace Sapling.Engine.Pgn; 5 | 6 | public static class PgnSplitter 7 | { 8 | public const string FileNames = "abcdefgh"; 9 | public const string RankNames = "87654321"; 10 | 11 | public static IEnumerable SplitPgnIntoMoves(string pgn) 12 | { 13 | // Define the regex pattern to match move numbers (e.g., "1.", "2.", etc.) 14 | var pattern = @"\d+\.\s?"; 15 | // Use regex to split the PGN string by move numbers 16 | var splitPgn = Regex.Split(pgn, pattern); 17 | 18 | 19 | // Loop through the split parts and combine them with the respective move numbers 20 | for (var i = 1; i < splitPgn.Length; i++) 21 | { 22 | yield return splitPgn[i].Trim(); 23 | } 24 | } 25 | 26 | public static string ConvertPosition(this byte position) 27 | { 28 | var rank = position.GetRankIndex(); 29 | var file = position.GetFileIndex(); 30 | return $"{(char)('a' + file)}{(char)('1' + rank)}"; 31 | } 32 | 33 | public static string ConvertPosition(this uint position) 34 | { 35 | var rank = position.GetRankIndex(); 36 | var file = position.GetFileIndex(); 37 | return $"{(char)('a' + file)}{(char)('1' + rank)}"; 38 | } 39 | 40 | public static (int file, int rank) GetPosition(this string name) 41 | { 42 | return (FileNames.IndexOf(name[0]), RankNames.IndexOf(name[1])); 43 | } 44 | 45 | public static byte GetSquare(char file, char rank) 46 | { 47 | return (byte)(RankNames.IndexOf(rank) * 8 + FileNames.IndexOf(file)); 48 | } 49 | 50 | public static byte GetRank(char rank) 51 | { 52 | return (byte)RankNames.IndexOf(rank); 53 | } 54 | 55 | public static byte GetFile(char file) 56 | { 57 | return (byte)FileNames.IndexOf(file); 58 | } 59 | 60 | public static (int start, int target, MoveType flag) GetMoveFromUCIName(string moveName, Piece[] board) 61 | { 62 | var (sFile, sRank) = GetPosition(moveName.Substring(0, 2)); 63 | var startSquare = sRank * 8 + sFile; 64 | var (tFile, tRank) = GetPosition(moveName.Substring(2, 2)); 65 | var targetSquare = tRank * 8 + tFile; 66 | 67 | var movedPieceType = board[startSquare]; 68 | 69 | // Figure out move flag 70 | var flag = MoveType.Normal; 71 | 72 | if (movedPieceType is Piece.WhitePawn or Piece.BlackPawn) 73 | { 74 | // Promotion 75 | if (moveName.Length > 4) 76 | { 77 | flag = moveName[^1] switch 78 | { 79 | 'q' => MoveType.PawnQueenPromotion, 80 | 'r' => MoveType.PawnRookPromotion, 81 | 'n' => MoveType.PawnKnightPromotion, 82 | 'b' => MoveType.PawnBishopPromotion, 83 | _ => MoveType.Normal 84 | }; 85 | } 86 | // Double pawn push 87 | else if (Math.Abs(tRank - sRank) == 2) 88 | { 89 | flag = MoveType.DoublePush; 90 | } 91 | } 92 | else if (movedPieceType is Piece.WhiteKing or Piece.BlackKing) 93 | { 94 | if (Math.Abs(sFile - tFile) > 1) 95 | { 96 | flag = MoveType.Castle; 97 | } 98 | } 99 | 100 | return (startSquare, targetSquare, flag); 101 | } 102 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Pgn/Token.cs: -------------------------------------------------------------------------------- 1 | namespace Sapling.Engine.Pgn; 2 | 3 | public readonly record struct Token(TokenType TokenType, int Start, int Length); -------------------------------------------------------------------------------- /src/Sapling.Engine/Pgn/TokenType.cs: -------------------------------------------------------------------------------- 1 | namespace Sapling.Engine.Pgn; 2 | 3 | public enum TokenType 4 | { 5 | Pawn, 6 | Rook, 7 | Bishop, 8 | Knight, 9 | Queen, 10 | King, 11 | Rank, 12 | File, 13 | QueenSideCastle, 14 | KingSideCastle, 15 | Capture, 16 | Check, 17 | Mate, 18 | Promotion, 19 | Checkmate, 20 | NewLine, 21 | Eof 22 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Piece.cs: -------------------------------------------------------------------------------- 1 | namespace Sapling.Engine; 2 | 3 | public enum Piece : byte 4 | { 5 | None = 0, 6 | 7 | BlackPawn = Constants.BlackPawn, 8 | BlackKnight = Constants.BlackKnight, 9 | BlackBishop = Constants.BlackBishop, 10 | BlackRook = Constants.BlackRook, 11 | BlackQueen = Constants.BlackQueen, 12 | BlackKing = Constants.BlackKing, 13 | 14 | WhitePawn = Constants.WhitePawn, 15 | WhiteKnight = Constants.WhiteKnight, 16 | WhiteBishop = Constants.WhiteBishop, 17 | WhiteRook = Constants.WhiteRook, 18 | WhiteQueen = Constants.WhiteQueen, 19 | WhiteKing = Constants.WhiteKing 20 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/PieceValues.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Sapling.Engine; 4 | 5 | public static unsafe class PieceValues 6 | { 7 | public static readonly short* PieceValue; 8 | 9 | static PieceValues() 10 | { 11 | PieceValue = MemoryHelpers.Allocate(13); 12 | 13 | PieceValue[Constants.WhitePawn] = Constants.PawnValue; 14 | PieceValue[Constants.WhiteKnight] = Constants.KnightValue; 15 | PieceValue[Constants.WhiteBishop] = Constants.BishopValue; 16 | PieceValue[Constants.WhiteRook] = Constants.RookValue; 17 | PieceValue[Constants.WhiteQueen] = Constants.QueenValue; 18 | PieceValue[Constants.WhiteKing] = Constants.KingValue; 19 | 20 | PieceValue[Constants.BlackPawn] = Constants.PawnValue; 21 | PieceValue[Constants.BlackKnight] = Constants.KnightValue; 22 | PieceValue[Constants.BlackBishop] = Constants.BishopValue; 23 | PieceValue[Constants.BlackRook] = Constants.RookValue; 24 | PieceValue[Constants.BlackQueen] = Constants.QueenValue; 25 | PieceValue[Constants.BlackKing] = Constants.KingValue; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/RepetitionDetector.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Sapling.Engine.MoveGen; 3 | 4 | namespace Sapling.Engine; 5 | 6 | public static unsafe class RepetitionDetector 7 | { 8 | private static readonly uint* Moves; 9 | private static readonly ulong* Keys; 10 | 11 | private const int TableSize = 8192; 12 | 13 | 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | private static int Hash1(ulong key) => (int)(key & 0x1FFF); 16 | 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | private static int Hash2(ulong key) => (int)((key >> 16) & 0x1FFF); 19 | 20 | static RepetitionDetector() 21 | { 22 | Moves = MemoryHelpers.Allocate(TableSize); 23 | Keys = MemoryHelpers.Allocate(TableSize); 24 | new Span(Moves, TableSize).Fill(0); 25 | 26 | for (var piece = 3; piece <= 12; piece++) 27 | { 28 | for (var fromSquare = 0; fromSquare < 64; fromSquare++) 29 | { 30 | var fromHash = Zobrist.PiecesArray[piece * 64 + fromSquare]; 31 | var attackMask = AttackMask(fromSquare, piece); 32 | 33 | for (var toSquare = fromSquare + 1; toSquare < 64; toSquare++) 34 | { 35 | var destSquare = (1UL << toSquare); 36 | 37 | if ((attackMask & destSquare) == 0) 38 | { 39 | continue; 40 | } 41 | 42 | var toHash = Zobrist.PiecesArray[piece * 64 + toSquare]; 43 | var move = MoveExtensions.EncodeNormalMove(piece, fromSquare, toSquare); 44 | var key = fromHash ^ toHash ^ Zobrist.SideToMove; 45 | 46 | var i = Hash1(key); 47 | while (true) 48 | { 49 | (Keys[i], key) = (key, Keys[i]); 50 | (Moves[i], move) = (move, Moves[i]); 51 | 52 | if (i == 0) 53 | break; 54 | 55 | i = i == Hash1(key) ? Hash2(key) : Hash1(key); 56 | } 57 | } 58 | } 59 | } 60 | 61 | ulong AttackMask(int idx, int piece) 62 | { 63 | return piece switch 64 | { 65 | Constants.WhiteKnight => AttackTables.KnightAttackTable[idx], 66 | Constants.BlackKnight => AttackTables.KnightAttackTable[idx], 67 | Constants.WhiteBishop => AttackTables.BishopAttackMasksAll[idx], 68 | Constants.BlackBishop => AttackTables.BishopAttackMasksAll[idx], 69 | Constants.WhiteRook => AttackTables.RookAttackMasksAll[idx], 70 | Constants.BlackRook => AttackTables.RookAttackMasksAll[idx], 71 | Constants.WhiteQueen => AttackTables.BishopAttackMasksAll[idx] | AttackTables.RookAttackMasksAll[idx], 72 | Constants.BlackQueen => AttackTables.BishopAttackMasksAll[idx] | AttackTables.RookAttackMasksAll[idx], 73 | Constants.WhiteKing => AttackTables.KingAttackTable[idx], 74 | Constants.BlackKing => AttackTables.KingAttackTable[idx], 75 | }; 76 | } 77 | } 78 | 79 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 80 | public static bool IsThreefoldRepetition(ushort turnCount, ushort halfMoveClock, ulong* hashHistory) 81 | { 82 | if (halfMoveClock < 3) 83 | return false; 84 | 85 | var currHash = hashHistory + turnCount - 1; 86 | 87 | var initialHash = *(currHash); 88 | 89 | var count = 1; 90 | for (var i = 2; i < halfMoveClock; i+=2) 91 | { 92 | currHash -= 2; 93 | if (*(currHash) != initialHash) 94 | { 95 | continue; 96 | } 97 | 98 | if (++count >= 3) 99 | { 100 | return true; 101 | } 102 | } 103 | 104 | return false; 105 | } 106 | 107 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 108 | public static bool HasRepetition(this ref BoardStateData pos, ulong* hashHistory, int depthFromRoot) 109 | { 110 | if (pos.HalfMoveClock < 3) 111 | return false; 112 | 113 | var lastMoveIndex = pos.TurnCount - 1; 114 | var occupancy = pos.Occupancy[Constants.Occupancy]; 115 | int slot; 116 | for (var i = 3; i <= pos.HalfMoveClock && i < lastMoveIndex; i += 2) 117 | { 118 | var diff = pos.Hash ^ *(hashHistory + lastMoveIndex - i); 119 | 120 | if (diff != Keys[(slot = Hash1(diff))] && 121 | diff != Keys[(slot = Hash2(diff))]) 122 | continue; 123 | 124 | var m = Moves[slot]; 125 | int moveFrom = (int)m.GetFromSquare(); 126 | int moveTo = (int)m.GetToSquare(); 127 | 128 | if ((occupancy & *(AttackTables.LineBitBoards+((moveFrom << 6) + moveTo))) != 0) 129 | { 130 | continue; 131 | } 132 | 133 | if (depthFromRoot > i) 134 | return true; 135 | 136 | var isWhite = false; 137 | if ((occupancy & (1ul << moveFrom)) != 0) 138 | { 139 | isWhite = (pos.Occupancy[Constants.WhitePieces] & (1ul << moveFrom)) != 0; 140 | } 141 | else 142 | { 143 | isWhite = (pos.Occupancy[Constants.WhitePieces] & (1ul << moveTo)) != 0; 144 | } 145 | 146 | return isWhite == pos.WhiteToMove; 147 | } 148 | 149 | return false; 150 | } 151 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/00_hl256_random.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/00_hl256_random.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/01_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/01_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/02_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/02_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/03_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/03_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/04_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/04_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/05_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/05_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/06_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/06_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/07_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/07_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/08_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/08_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/09_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/09_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/10_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/10_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/11_hl256.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/11_hl256.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/12_hl512.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/12_hl512.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/13_hl768.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/13_hl768.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/14_hl1024.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/14_hl1024.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/15_hl1024.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/15_hl1024.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/16_hl1024.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/16_hl1024.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/17_(768x4-1024)x2-8.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/17_(768x4-1024)x2-8.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/18_(768x8-1024)x2-8.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/18_(768x8-1024)x2-8.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/19_(768x4-1024)x2-8.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/19_(768x4-1024)x2-8.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/20_(768x4-1024)x2-8.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/WeightsHistory/20_(768x4-1024)x2-8.bin -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/WeightsHistory/log.txt: -------------------------------------------------------------------------------- 1 | ---------------------------------- 2 | 00_hl256_random.bin 3 | ---------------------------------- 4 | Data: randomly generated weights 5 | ```python 6 | import os 7 | 8 | file_size = 394816 # Size in bytes 9 | output_file = "random.bin" 10 | 11 | with open(output_file, "wb") as f: 12 | f.write(os.urandom(file_size)) 13 | ``` 14 | WDL: 1.0 15 | LR: 0.001, 0.3, 6 16 | SuperBatches: 24 17 | 18 | ---------------------------------- 19 | 01_hl256.bin 20 | ---------------------------------- 21 | Data: 10m positions 22 | WDL: 1.0 23 | LR: 0.001, 0.3, 6 24 | SuperBatches: 24 25 | 26 | ---------------------------------- 27 | 02_hl256.bin 28 | ---------------------------------- 29 | Data: 15m positions 30 | WDL: 0.9 31 | LR: 0.001, 0.3, 6 32 | SuperBatches: 24 33 | 34 | ---------------------------------- 35 | 03_hl256.bin 36 | ---------------------------------- 37 | Data: 20m positions 38 | WDL: 0.9 39 | LR: 0.001, 0.3, 6 40 | SuperBatches: 24 41 | 42 | ---------------------------------- 43 | 04_hl256.bin 44 | ---------------------------------- 45 | Data: 30m positions 46 | WDL: 0.9 47 | LR: 0.001, 0.3, 6 48 | SuperBatches: 24 49 | 50 | ---------------------------------- 51 | 05_hl256.bin 52 | ---------------------------------- 53 | Data: 55m positions 54 | WDL: 0.8 55 | LR: 0.001, 0.3, 12 56 | SuperBatches: 48 57 | 58 | ---------------------------------- 59 | 06_hl256.bin 60 | ---------------------------------- 61 | Data: 110m positions 62 | WDL: 0.75 63 | LR: 0.001, 0.3, 16 64 | SuperBatches: 64 65 | 66 | ---------------------------------- 67 | 07_hl256.bin 68 | ---------------------------------- 69 | Data: 175m positions 70 | WDL: 0.7 71 | LR: 0.001, 0.3, 16 72 | SuperBatches: 64 73 | 74 | ---------------------------------- 75 | 08_hl256.bin 76 | ---------------------------------- 77 | Data: 380m positions 78 | WDL: 0.7 79 | LR: 0.001, 0.3, 16 80 | SuperBatches: 64 81 | 82 | ---------------------------------- 83 | 09_hl256.bin 84 | ---------------------------------- 85 | Data: 600m positions 86 | WDL: 0.7 87 | LR: 0.001, 0.3, 20 88 | SuperBatches: 80 89 | 90 | ---------------------------------- 91 | 10_hl256.bin 92 | ---------------------------------- 93 | Data: 750m positions 94 | WDL: 0.7 95 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 96 | SuperBatches: 90 97 | 98 | ---------------------------------- 99 | 11_hl256.bin 100 | ---------------------------------- 101 | Data: 650m positions 102 | WDL: 0.7 103 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 104 | SuperBatches: 90 105 | 106 | ---------------------------------- 107 | 12_hl512.bin 108 | ---------------------------------- 109 | Data: 1.5bn positions 110 | WDL: 0.4 111 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 112 | SuperBatches: 130 113 | 114 | ---------------------------------- 115 | 13_hl768.bin 116 | ---------------------------------- 117 | Data: 1.2bn positions 118 | WDL: 0.4 119 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 120 | SuperBatches: 180 121 | 122 | ---------------------------------- 123 | 14_hl1024.bin 124 | ---------------------------------- 125 | Data: 1.3bn positions 126 | WDL: 0.4 127 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 128 | SuperBatches: 180 129 | 130 | ---------------------------------- 131 | 15_hl1024.bin 132 | ---------------------------------- 133 | Data: 1bn positions 134 | WDL: 0.4 135 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 136 | SuperBatches: 180 137 | 138 | ---------------------------------- 139 | 16_hl1024.bin 140 | ---------------------------------- 141 | Data: 1.5bn positions 142 | WDL: 0.4 143 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 144 | SuperBatches: 200 145 | 146 | ---------------------------------- 147 | 17_(768x4-1024)x2-8.bin 148 | ---------------------------------- 149 | Data: 1.5bn positions 150 | WDL: 0.3 151 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 152 | SuperBatches: 200 153 | 154 | ---------------------------------- 155 | 18_(768x8-1024)x2-8.bin 156 | ---------------------------------- 157 | Data: 1.5bn positions 158 | WDL: 0.3 159 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 160 | SuperBatches: 240 161 | 162 | ---------------------------------- 163 | 19_(768x4-1024)x2-8 164 | ---------------------------------- 165 | Data: 1.7bn positions 166 | WDL: 0.3 167 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 168 | SuperBatches: 260 169 | 170 | ---------------------------------- 171 | 20_(768x4-1024)x2-8 172 | ---------------------------------- 173 | Data: 2bn positions (400m FRC) 174 | WDL: 0.3 175 | LR: CosineDecayLR 0.001 * 0.3 * 0.3 * 0.3 176 | SuperBatches: 300 -------------------------------------------------------------------------------- /src/Sapling.Engine/Resources/sapling.nnue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/Resources/sapling.nnue -------------------------------------------------------------------------------- /src/Sapling.Engine/Sapling.Engine.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | AnyCPU;x64 8 | true 9 | true 10 | true 11 | preview 12 | 13 | true 14 | SaplingEngine 15 | 1.2.7 16 | Tim Jones 17 | Aptacode 18 | A strong UCI chess engine 19 | https://github.com/Timmoth/Sapling 20 | https://github.com/Timmoth/Sapling 21 | git 22 | Chess Engine 23 | Sapling 24 | true 25 | logo.ico 26 | logo.png 27 | README.md 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | True 44 | \ 45 | 46 | 47 | True 48 | \ 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Sapling.Engine/Search/CorrectionHistory.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Sapling.Engine.Search; 4 | 5 | public partial class Searcher 6 | { 7 | public const int TableSize = 16384; 8 | public const int TableElementsSize = TableSize * 2; 9 | public const int CorrectionTableMask = TableSize - 1; 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public int CorrectionIndex(ulong pawnHash, bool whiteToMove) 12 | { 13 | return (whiteToMove ? 0 : 1 * TableSize) + (int)(pawnHash & CorrectionTableMask); 14 | } 15 | public const int CorrectionScale = 256; 16 | public const int CorrectionGrain = 256; 17 | public const short CorrectionMax = CorrectionGrain * 32; 18 | 19 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 20 | static void Update(ref int entry, int newWeight, int scaledDiff) 21 | { 22 | int update = entry * (CorrectionScale - newWeight) + scaledDiff * newWeight; 23 | entry = Math.Clamp(update / CorrectionScale, -CorrectionMax, CorrectionMax); 24 | } 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | private unsafe void UpdateCorrectionHistory(int pawnChIndex, int whiteMaterialChIndex, int blackMaterialChIndex, int diff, int depth) 28 | { 29 | int scaledDiff = diff * CorrectionGrain; 30 | int newWeight = Math.Min(16, 1 + depth); 31 | 32 | Update(ref *(PawnCorrHist + pawnChIndex), newWeight, scaledDiff); 33 | Update(ref *(WhiteMaterialCorrHist + whiteMaterialChIndex), newWeight, scaledDiff); 34 | Update(ref *(BlackMaterialCorrHist + blackMaterialChIndex), newWeight, scaledDiff); 35 | 36 | } 37 | 38 | const int MinMateScore = Constants.ImmediateMateScore - Constants.MaxSearchDepth; 39 | 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | private unsafe int AdjustEval(int pawnChIndex, int whiteMaterialChIndex, int blackMaterialChIndex, int rawEval) 42 | { 43 | var pch = *(PawnCorrHist + pawnChIndex); 44 | var mchW = *(WhiteMaterialCorrHist + whiteMaterialChIndex); 45 | var mchB = *(BlackMaterialCorrHist + blackMaterialChIndex); 46 | 47 | return rawEval + (int)((pch + mchW + mchB) / CorrectionGrain); 48 | 49 | } 50 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Search/HistoryHeuristicExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Sapling.Engine.MoveGen; 3 | using Sapling.Engine.Tuning; 4 | 5 | namespace Sapling.Engine.Search; 6 | 7 | public static unsafe class HistoryHeuristicExtensions 8 | { 9 | public static short* BonusTable; 10 | static HistoryHeuristicExtensions() 11 | { 12 | BonusTable = MemoryHelpers.Allocate(Constants.MaxSearchDepth); 13 | UpdateBonusTable(); 14 | } 15 | 16 | public static void UpdateBonusTable() 17 | { 18 | for (var i = 0; i < Constants.MaxSearchDepth; i++) 19 | { 20 | BonusTable[i] = Math.Min((short)SpsaOptions.HistoryHeuristicBonusMax, (short)(SpsaOptions.HistoryHeuristicBonusCoeff * (i - 1))); 21 | } 22 | } 23 | 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public static void UpdateMovesHistory(int* history, uint* moves, int quietCount, uint m, int depth) 26 | { 27 | var bonus = *(BonusTable + depth); 28 | 29 | // Directly update the history array 30 | var index = m.GetCounterMoveIndex(); 31 | 32 | *(history + index) += bonus - (*(history + index) * bonus) / SpsaOptions.HistoryHeuristicMaxHistory; 33 | 34 | var malus = (short)-bonus; 35 | 36 | // Process quiet moves 37 | for (var n = 0; n < quietCount; n++) 38 | { 39 | var quiet = *(moves + n); 40 | if (!quiet.IsQuiet() || quiet == default) 41 | { 42 | continue; 43 | } 44 | 45 | var quietIndex = quiet.GetCounterMoveIndex(); 46 | *(history + quietIndex) += malus - (*(history + quietIndex) * bonus) / SpsaOptions.HistoryHeuristicMaxHistory; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Search/PVTable.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using Sapling.Engine; 3 | using Sapling.Engine.Transpositions; 4 | 5 | namespace Sapling; 6 | 7 | public static unsafe class PVTable 8 | { 9 | public static readonly int* Indexes; 10 | public const int IndexCount = Constants.MaxSearchDepth + 16; 11 | static PVTable() 12 | { 13 | Indexes = MemoryHelpers.Allocate(IndexCount); 14 | var previousPvIndex = 0; 15 | Indexes[0] = previousPvIndex; 16 | 17 | for (var depth = 0; depth < IndexCount - 1; ++depth) 18 | { 19 | Indexes[depth + 1] = previousPvIndex + Constants.MaxSearchDepth - depth; 20 | previousPvIndex = Indexes[depth + 1]; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Search/Perft.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Sapling.Engine.MoveGen; 3 | using Sapling.Engine.Pgn; 4 | 5 | namespace Sapling.Engine.Search; 6 | 7 | public static class Perft 8 | { 9 | private static unsafe ulong PerftInternal(this ref BoardStateData board, int depth) 10 | { 11 | var moves = stackalloc uint[218]; 12 | var moveCount = board.GeneratePseudoLegalMoves(moves, false); 13 | BoardStateData copy = default; 14 | var whiteToMove = board.WhiteToMove; 15 | ulong nodeCount = 0; 16 | for (var index = 0; index < moveCount; index++) 17 | { 18 | var m = moves[index]; 19 | board.CloneTo(ref copy); 20 | if (whiteToMove ? !copy.PartialApplyWhite(m) : !copy.PartialApplyBlack(m)) 21 | { 22 | continue; 23 | } 24 | 25 | if (depth <= 1) 26 | { 27 | // Leaf node, don't search any deeper 28 | nodeCount++; 29 | } 30 | else 31 | { 32 | copy.UpdateCheckStatus(); 33 | nodeCount += copy.PerftInternal(depth - 1); 34 | } 35 | } 36 | 37 | return nodeCount; 38 | } 39 | 40 | public static unsafe List<(ulong nodes, string move)> PerftRootSequential(this ref BoardStateData board, int depth) 41 | { 42 | var moves = stackalloc uint[218]; 43 | var moveCount = board.GeneratePseudoLegalMoves(moves, false); 44 | BoardStateData copy = default; 45 | var whiteToMove = board.WhiteToMove; 46 | 47 | var rootMoves = new ConcurrentBag<(ulong nodes, string move)>(); 48 | for (var i = 0; i < moveCount; i++) 49 | { 50 | var m = moves[i]; 51 | board.CloneTo(ref copy); 52 | 53 | if (whiteToMove ? !copy.PartialApplyWhite(m) : !copy.PartialApplyBlack(m)) 54 | { 55 | // Illegal move 56 | continue; 57 | } 58 | 59 | copy.UpdateCheckStatus(); 60 | var nodeCount = copy.PerftInternal(depth - 1); 61 | 62 | Console.WriteLine( 63 | $"{m.GetFromSquare().ConvertPosition()}{m.GetToSquare().ConvertPosition()} {nodeCount}"); 64 | rootMoves.Add((nodeCount, 65 | $"{m.GetFromSquare().ConvertPosition()}{m.GetToSquare().ConvertPosition()}")); 66 | } 67 | 68 | return rootMoves.ToList(); 69 | } 70 | 71 | public static unsafe List<(ulong nodes, string move)> PerftRootParallel(this BoardStateData board, int depth) 72 | { 73 | var moves = stackalloc uint[218]; 74 | var moveCount = board.GeneratePseudoLegalMoves(moves, false); 75 | var whiteToMove = board.WhiteToMove; 76 | 77 | var rootMoves = new ConcurrentBag<(ulong nodes, string move)>(); 78 | Parallel.For(0, moveCount, new ParallelOptions 79 | { 80 | MaxDegreeOfParallelism = Environment.ProcessorCount 81 | }, i => 82 | { 83 | BoardStateData copy = default; 84 | board.CloneTo(ref copy); 85 | 86 | var m = moves[i]; 87 | 88 | if (whiteToMove ? !copy.PartialApplyWhite(m) : !copy.PartialApplyBlack(m)) 89 | { 90 | // Illegal move 91 | return; 92 | } 93 | 94 | copy.UpdateCheckStatus(); 95 | var nodeCount = copy.PerftInternal(depth - 1); 96 | 97 | Console.WriteLine( 98 | $"{m.GetFromSquare().ConvertPosition()}{m.GetToSquare().ConvertPosition()} {nodeCount}"); 99 | rootMoves.Add((nodeCount, 100 | $"{m.GetFromSquare().ConvertPosition()}{m.GetToSquare().ConvertPosition()}")); 101 | }); 102 | 103 | return rootMoves.ToList(); 104 | } 105 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/SkipLocalsInit.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [module: SkipLocalsInit] // Applies to all methods in this module 4 | -------------------------------------------------------------------------------- /src/Sapling.Engine/SquareHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Sapling.Engine; 4 | 5 | public static class SquareHelpers 6 | { 7 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 8 | public static int GetFileIndex(this int square) 9 | { 10 | // File is the last 3 bits of the square index 11 | return square & 7; // Equivalent to square % 8 12 | } 13 | 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public static int GetRankIndex(this int square) 16 | { 17 | // Rank is obtained by shifting right by 3 bits 18 | return square >> 3; // Equivalent to square / 8 19 | } 20 | 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | public static byte GetFileIndex(this byte square) 23 | { 24 | // File is the last 3 bits of the square index 25 | return (byte)(square & 7); // Equivalent to square % 8 26 | } 27 | 28 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 29 | public static uint GetFileIndex(this uint square) 30 | { 31 | // File is the last 3 bits of the square index 32 | return (square & 7); // Equivalent to square % 8 33 | } 34 | 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public static bool IsMirroredSide(this byte square) 37 | { 38 | return (square & 7) >= 4; 39 | } 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public static byte GetRankIndex(this byte square) 43 | { 44 | // Rank is obtained by shifting right by 3 bits 45 | return (byte)(square >> 3); // Equivalent to square / 8 46 | } 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public static uint GetRankIndex(this uint square) 49 | { 50 | // Rank is obtained by shifting right by 3 bits 51 | return square >> 3; // Equivalent to square / 8 52 | } 53 | 54 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 55 | public static ushort GetRankIndex(this ushort square) 56 | { 57 | // Rank is obtained by shifting right by 3 bits 58 | return (ushort)(square >> 3); // Equivalent to square / 8 59 | } 60 | 61 | 62 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 63 | public static bool IsSecondRank(this byte rankIndex) 64 | { 65 | return rankIndex == 1; 66 | } 67 | 68 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 69 | public static bool IsSeventhRank(this byte rankIndex) 70 | { 71 | return rankIndex == 6; 72 | } 73 | 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | public static bool IsWhiteEnPassantRankIndex(this byte rankIndex) 76 | { 77 | return rankIndex == 4; 78 | } 79 | 80 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 81 | public static bool IsBlackEnPassantRankIndex(this byte rankIndex) 82 | { 83 | return rankIndex == 3; 84 | } 85 | 86 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 87 | public static byte ShiftUp(this byte board) 88 | { 89 | return (byte)(board + 8); 90 | } 91 | 92 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 93 | public static byte ShiftDown(this byte board) 94 | { 95 | return (byte)(board - 8); 96 | } 97 | 98 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 | public static byte ShiftLeft(this byte board) 100 | { 101 | return (byte)(board - 1); 102 | } 103 | 104 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 105 | public static byte ShiftRight(this byte board) 106 | { 107 | return (byte)(board + 1); 108 | } 109 | 110 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 111 | public static byte ShiftUpRight(this byte board) 112 | { 113 | return (byte)(board + 9); 114 | } 115 | 116 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 117 | public static byte ShiftUpLeft(this byte board) 118 | { 119 | return (byte)(board + 7); 120 | } 121 | 122 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 123 | public static byte ShiftDownRight(this byte board) 124 | { 125 | return (byte)(board - 7); 126 | } 127 | 128 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 129 | public static byte ShiftDownLeft(this byte board) 130 | { 131 | return (byte)(board - 9); 132 | } 133 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Transpositions/Transposition.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Sapling.Engine.Transpositions; 4 | 5 | [StructLayout(LayoutKind.Explicit, Size = 20)] // Size aligned to 20 bytes 6 | public unsafe struct Transposition 7 | { 8 | // 8 bytes for ulong, aligned at offset 0 9 | [FieldOffset(0)] public ulong FullHash; 10 | 11 | // 4 bytes for int, aligned at offset 8 12 | [FieldOffset(8)] public int Evaluation; 13 | 14 | // 4 bytes for uint, aligned at offset 12 15 | [FieldOffset(12)] public uint Move; 16 | 17 | // 1 byte for TranspositionTableFlag, no alignment needed 18 | [FieldOffset(16)] public TranspositionTableFlag Flag; 19 | 20 | // 1 byte for Depth, packed right after Flag 21 | [FieldOffset(17)] public byte Depth; 22 | 23 | // 2 bytes of padding to align the size to 20 bytes 24 | [FieldOffset(18)] private fixed byte _padding[2]; // Padding for alignment 25 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Transpositions/TranspositionTableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Sapling.Engine.Transpositions; 5 | 6 | public static class TranspositionTableExtensions 7 | { 8 | public const int NoHashEntry = 25_000; 9 | 10 | public const int 11 | PositiveCheckmateDetectionLimit = 12 | 27_000; 13 | 14 | public const int 15 | NegativeCheckmateDetectionLimit = 16 | -27_000; 17 | 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | internal static int RecalculateMateScores(int score, int ply) 20 | { 21 | if (score > PositiveCheckmateDetectionLimit) 22 | { 23 | return score - ply; // Positive checkmate, reduce score by ply 24 | } 25 | if (score < NegativeCheckmateDetectionLimit) 26 | { 27 | return score + ply; // Negative checkmate, increase score by ply 28 | } 29 | return score; // No change 30 | } 31 | 32 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 33 | public static unsafe (int Evaluation, uint BestMove, TranspositionTableFlag NodeType) Get(Transposition* tt, 34 | ulong ttMask, ulong hash, int depth, int ply, int alpha, int beta) 35 | { 36 | ref var entry = ref tt[hash & ttMask]; 37 | 38 | if (hash != entry.FullHash) 39 | { 40 | return (NoHashEntry, default, default); 41 | } 42 | 43 | var eval = NoHashEntry; 44 | 45 | if (entry.Depth < depth) 46 | { 47 | return (eval, entry.Move, entry.Flag); 48 | } 49 | 50 | // We want to translate the checkmate position relative to the saved node to our root position from which we're searching 51 | // If the recorded score is a checkmate in 3 and we are at depth 5, we want to read checkmate in 8 52 | var score = RecalculateMateScores(entry.Evaluation, ply); 53 | 54 | eval = entry.Flag switch 55 | { 56 | TranspositionTableFlag.Exact => score, 57 | TranspositionTableFlag.Alpha when score <= alpha => alpha, 58 | TranspositionTableFlag.Beta when score >= beta => beta, 59 | _ => NoHashEntry 60 | }; 61 | 62 | return (eval, entry.Move, entry.Flag); 63 | } 64 | 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | public static void Set(this ref Transposition entry, ulong hash, byte depth, int ply, 67 | int eval, TranspositionTableFlag nodeType, uint move = default) 68 | { 69 | var shouldReplace = 70 | entry.FullHash == 0 // No actual entry 71 | || hash != entry.FullHash // Different key: collision 72 | || nodeType == TranspositionTableFlag.Exact // Entering PV data 73 | || depth >= entry.Depth; // Higher depth 74 | 75 | if (!shouldReplace) 76 | { 77 | return; 78 | } 79 | 80 | entry.FullHash = hash; 81 | entry.Evaluation = RecalculateMateScores(eval, -ply); 82 | entry.Depth = depth; 83 | entry.Flag = nodeType; 84 | entry.Move = move != 0 ? move : entry.Move; //Don't clear TT move if no best move is provided: keep old one 85 | } 86 | 87 | public static unsafe long CalculateTranspositionTableSize(long sizeInMb) 88 | { 89 | if (sizeInMb <= 0) 90 | throw new ArgumentOutOfRangeException(nameof(sizeInMb), "Size must be positive."); 91 | 92 | const long EightGB = 8L * 1024; 93 | const long TwoGB = 2L * 1024; 94 | 95 | ulong estimatedCount = (ulong)sizeInMb * 1024 * 1024 / (ulong)sizeof(Transposition); 96 | 97 | if (estimatedCount == 0) 98 | throw new OverflowException("Requested size is too small to store even one transposition."); 99 | 100 | // Find powers of two above and below the estimated count 101 | ulong lower = BitOperations.IsPow2(estimatedCount) 102 | ? estimatedCount 103 | : BitOperations.RoundUpToPowerOf2(estimatedCount) >> 1; 104 | 105 | ulong upper = lower << 1; 106 | 107 | // Calculate size in MB for the rounded counts 108 | long lowerMb = (long)(lower * (ulong)sizeof(Transposition) / (1024 * 1024)); 109 | long upperMb = (long)(upper * (ulong)sizeof(Transposition) / (1024 * 1024)); 110 | 111 | long chosenMb; 112 | 113 | if (sizeInMb <= EightGB) 114 | { 115 | // Always round up under 8GB 116 | chosenMb = upperMb; 117 | } 118 | else if ((upperMb - sizeInMb) <= TwoGB) 119 | { 120 | // Round up if it's within 2GB 121 | chosenMb = upperMb; 122 | } 123 | else 124 | { 125 | // Otherwise round down 126 | chosenMb = lowerMb; 127 | } 128 | 129 | ulong finalCount = (ulong)chosenMb * 1024 * 1024 / (ulong)sizeof(Transposition); 130 | 131 | if (finalCount == 0 || finalCount > long.MaxValue) 132 | throw new OverflowException("Final transposition table size is too large."); 133 | 134 | return (long)finalCount; 135 | } 136 | 137 | public static unsafe int CalculateSizeInMb(uint transpositionCount) 138 | { 139 | // If transpositionCount is less than 2, the original function would have shifted it 140 | // to a power of 2 and then shifted it back down by >> 1, so adjust it. 141 | if (transpositionCount < 2 || !BitOperations.IsPow2(transpositionCount)) 142 | { 143 | throw new ArgumentException("Invalid transposition count, must be a power of 2."); 144 | } 145 | 146 | // Reverse the potential shift caused by rounding up in the original function 147 | ulong adjustedTranspositionCount = transpositionCount << 1; 148 | 149 | // Calculate the size in MB 150 | var sizeInMb = (int)(adjustedTranspositionCount * (ulong)sizeof(Transposition) / (1024 * 1024)); 151 | 152 | // Check if the original function would have produced the same transposition count 153 | // for this sizeInMb, if not, decrement the size until it matches. 154 | while (CalculateTranspositionTableSize(sizeInMb) != transpositionCount) 155 | { 156 | sizeInMb--; 157 | if (sizeInMb < 0) 158 | { 159 | throw new ArgumentException("Could not invert the function, invalid input."); 160 | } 161 | } 162 | 163 | return sizeInMb; 164 | } 165 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/Transpositions/TranspositionTableFlag.cs: -------------------------------------------------------------------------------- 1 | namespace Sapling.Engine.Transpositions; 2 | 3 | public enum TranspositionTableFlag : byte 4 | { 5 | None = 0, 6 | Alpha = 1, 7 | Exact = 2, 8 | Beta = 3 9 | } -------------------------------------------------------------------------------- /src/Sapling.Engine/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/logo.ico -------------------------------------------------------------------------------- /src/Sapling.Engine/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling.Engine/logo.png -------------------------------------------------------------------------------- /src/Sapling.SourceGenerators/Class1.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Text; 2 | using Microsoft.CodeAnalysis; 3 | using System.Text; 4 | 5 | namespace Sapling.SourceGenerators 6 | { 7 | 8 | [Generator] 9 | public sealed class ExampleGenerator : IIncrementalGenerator 10 | { 11 | public void Initialize(IncrementalGeneratorInitializationContext context) 12 | { 13 | 14 | 15 | 16 | context.RegisterPostInitializationOutput(ctx => 17 | { 18 | ctx.AddSource("ExampleGenerator.g", SourceText.From(source, Encoding.UTF8)); 19 | }); 20 | } 21 | } 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Sapling.SourceGenerators/Sapling.SourceGenerators.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | enable 6 | true 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Sapling.Utils/Program - Copy.cs: -------------------------------------------------------------------------------- 1 | //using System.Buffers.Binary; 2 | //using System.Text; 3 | //using Sapling.Engine; 4 | //using Sapling.Engine.MoveGen; 5 | 6 | //namespace Sapling.Utils 7 | //{ 8 | 9 | // internal class Program 10 | // { 11 | // public static string PolyGlotToUciMove(ushort move) 12 | // { 13 | // // Extract components using bit-shifting and masking 14 | 15 | // // To file (0-2 bits) 16 | // int toFile = (move & 0b0000000000000111); 17 | 18 | // // To rank (3-5 bits) 19 | // int toRank = (move >> 3) & 0b0000000000000111; 20 | 21 | // // From file (6-8 bits) 22 | // int fromFile = (move >> 6) & 0b0000000000000111; 23 | 24 | // // From rank (9-11 bits) 25 | // int fromRank = (move >> 9) & 0b0000000000000111; 26 | 27 | // // Promotion piece (12-14 bits) 28 | // int promotionPiece = (move >> 12) & 0b0000000000000111; 29 | 30 | // // Convert the file (0-7) to a letter ('a' = 0, 'b' = 1, ..., 'h' = 7) 31 | // char fromFileChar = (char)('a' + fromFile); 32 | // char toFileChar = (char)('a' + toFile); 33 | 34 | // // Convert the rank (0-7) to a number ('1' = 0, '2' = 1, ..., '8' = 7) 35 | // char fromRankChar = (char)('1' + fromRank); 36 | // char toRankChar = (char)('1' + toRank); 37 | 38 | // // Create the basic move string (like "e2e4") 39 | // string moveStr = $"{fromFileChar}{fromRankChar}{toFileChar}{toRankChar}"; 40 | 41 | // // Handle promotion if present (promotionPiece > 0 means promotion) 42 | // if (promotionPiece > 0) 43 | // { 44 | // // Convert promotion piece (1=q, 2=r, 3=b, 4=n) 45 | // char promotionChar = promotionPiece switch 46 | // { 47 | // 1 => 'q', // Queen 48 | // 2 => 'r', // Rook 49 | // 3 => 'b', // Bishop 50 | // 4 => 'n', // Knight 51 | // _ => throw new InvalidOperationException("Invalid promotion piece") 52 | // }; 53 | 54 | // // Append the promotion character to the move string 55 | // moveStr += promotionChar; 56 | // } 57 | 58 | // return moveStr; 59 | // } 60 | 61 | // public static ushort ToOpeningMove(uint move) 62 | // { 63 | // var moveType = move.GetMoveType(); 64 | 65 | // var promotion = 0; 66 | // if (moveType >= Constants.PawnKnightPromotion) 67 | // { 68 | // promotion = moveType - 3; 69 | // } 70 | 71 | // var from = move.GetFromSquare(); 72 | // var to = move.GetToSquare(); 73 | 74 | // return (ushort)(to.GetFileIndex() | 75 | // to.GetRankIndex() << 3 | 76 | // from.GetFileIndex() << 6 | 77 | // from.GetRankIndex() << 9 | 78 | // promotion << 12 79 | // ); 80 | // } 81 | 82 | // static void Main(string[] args) 83 | // { 84 | // var fileName = "./Human.bin"; 85 | // var entrySize = sizeof(ulong) + sizeof(ushort) + sizeof(ushort) + sizeof(uint); 86 | // var entryCount = (int)new FileInfo(fileName).Length / entrySize; 87 | 88 | // var openingMoves = new Dictionary>(); 89 | // using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) 90 | // { 91 | // using var reader = new BinaryReader(fs, Encoding.UTF8, false); 92 | // for (var i = 0; i < entryCount; i++) 93 | // { 94 | // ulong hash; 95 | // ushort move; 96 | 97 | // if (BitConverter.IsLittleEndian) 98 | // { 99 | // hash = BinaryPrimitives.ReverseEndianness(reader.ReadUInt64()); 100 | // move = BinaryPrimitives.ReverseEndianness(reader.ReadUInt16()); 101 | // } 102 | // else 103 | // { 104 | // hash = reader.ReadUInt64(); 105 | // move = reader.ReadUInt16(); 106 | // } 107 | 108 | // // Skip weight & learn 109 | // reader.ReadUInt16(); 110 | // reader.ReadUInt32(); 111 | 112 | // if (!openingMoves.TryGetValue(hash, out var moveList)) 113 | // { 114 | // openingMoves[hash] = moveList = new List(); 115 | // } 116 | 117 | // moveList.Add(move); 118 | // } 119 | // } 120 | 121 | // Console.WriteLine("Size: " + entryCount); 122 | 123 | // var gameHashes = new HashSet(); 124 | // var openingBookBulder = new StringBuilder(); 125 | 126 | // var validFirstMoves = new Dictionary 127 | // { 128 | // // Pawn moves 129 | // {"a2a3", 0}, {"a2a4", 0}, 130 | // {"b2b3", 0}, {"b2b4", 0}, 131 | // {"c2c3", 0}, {"c2c4", 0}, 132 | // {"d2d3", 0}, {"d2d4", 0}, 133 | // {"e2e3", 0}, {"e2e4", 0}, 134 | // {"f2f3", 0}, {"f2f4", 0}, 135 | // {"g2g3", 0}, {"g2g4", 0}, 136 | // {"h2h3", 0}, {"h2h4", 0}, 137 | 138 | // // Knight moves 139 | // {"b1a3", 0}, {"b1c3", 0}, 140 | // {"g1f3", 0}, {"g1h3", 0} 141 | // }; 142 | 143 | // var initialGameState = new GameState(BoardStateExtensions.CreateBoardFromArray(Constants.InitialState)); 144 | // var gameState = new GameState(BoardStateExtensions.CreateBoardFromArray(Constants.InitialState)); 145 | // for (var i = 0; i < 5000000; i++) 146 | // { 147 | // gameState.ResetTo(initialGameState.Board); 148 | // var firstMove = gameState.LegalMoves[Random.Shared.Next(0, gameState.LegalMoves.Count)]; 149 | // gameState.Apply(firstMove); 150 | 151 | // var openingMovesBuilder = new StringBuilder(); 152 | // openingMovesBuilder.Append(firstMove.ToUciMoveName()); 153 | // var gameOk = true; 154 | // var j = 1; 155 | 156 | // for (j = 1; j < 12; j++) 157 | // { 158 | // var hash = Zobrist.CalculatePolyGlotKey(ref gameState.Board.Data); 159 | // if (!openingMoves.TryGetValue(hash, out var openingMoveList)) 160 | // { 161 | // gameOk = false; 162 | // break; 163 | // } 164 | 165 | // var randomOpeningMove = openingMoveList[Random.Shared.Next(0, openingMoveList.Count)]; 166 | // var mv = gameState.LegalMoves.FirstOrDefault(m => ToOpeningMove(m) == randomOpeningMove); 167 | // if (mv == default) 168 | // { 169 | // var uciMove = PolyGlotToUciMove(randomOpeningMove); 170 | // if (uciMove == "e1h1") 171 | // { 172 | // mv = gameState.LegalMoves.FirstOrDefault(m => m.ToUciMoveName() == "e1g1"); 173 | // } 174 | // else if (uciMove == "e1a1") 175 | // { 176 | // mv = gameState.LegalMoves.FirstOrDefault(m => m.ToUciMoveName() == "e1b1"); 177 | // } 178 | // else if (uciMove == "e8h8") 179 | // { 180 | // mv = gameState.LegalMoves.FirstOrDefault(m => m.ToUciMoveName() == "e8g8"); 181 | // } 182 | // else if (uciMove == "e8a8") 183 | // { 184 | // mv = gameState.LegalMoves.FirstOrDefault(m => m.ToUciMoveName() == "e8b8"); 185 | // } 186 | // } 187 | 188 | // if (mv == default) 189 | // { 190 | // if (j < 8) 191 | // { 192 | // gameOk = false; 193 | // } 194 | 195 | // break; 196 | // } 197 | 198 | // openingMovesBuilder.Append(" "); 199 | // openingMovesBuilder.Append(mv.ToUciMoveName()); 200 | // gameState.Apply(mv); 201 | // } 202 | 203 | // if (!gameHashes.Contains(gameState.Board.Data.Hash) && gameOk) 204 | // { 205 | // gameHashes.Add(gameState.Board.Data.Hash); 206 | // validFirstMoves[firstMove.ToUciMoveName()]++; 207 | // openingBookBulder.AppendLine(openingMovesBuilder.ToString()); 208 | // } 209 | // } 210 | 211 | // var total = 0; 212 | // foreach (var (move, count) in validFirstMoves) 213 | // { 214 | // total += count; 215 | // Console.WriteLine($"move: {move} count: {count}"); 216 | // } 217 | 218 | // Console.WriteLine($"Total games: {total}"); 219 | 220 | // File.WriteAllText("./book.csv",openingBookBulder.ToString()); 221 | // Console.WriteLine("Fin"); 222 | // } 223 | // } 224 | //} 225 | -------------------------------------------------------------------------------- /src/Sapling.Utils/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using System.Text; 3 | using Sapling.Engine; 4 | using Sapling.Engine.Evaluation; 5 | using Sapling.Engine.MoveGen; 6 | 7 | namespace Sapling.Utils 8 | { 9 | internal class Program 10 | { 11 | public static string UnrollAndInsert(string source, string replace, Func line) 12 | { 13 | var simdCopySourceBuilder = new StringBuilder(); 14 | 15 | var avx512Elements = NnueWeights.Layer1Size / 32; 16 | var avx256Elements = NnueWeights.Layer1Size / 16; 17 | var i = 0; 18 | for (; i < avx512Elements; i++) 19 | { 20 | simdCopySourceBuilder.AppendLine(line(i)); 21 | } 22 | 23 | simdCopySourceBuilder.AppendLine("#if !AVX512"); 24 | for (; i < avx256Elements; i++) 25 | { 26 | simdCopySourceBuilder.AppendLine(line(i)); 27 | } 28 | simdCopySourceBuilder.AppendLine("#endif"); 29 | 30 | return source.Replace(replace, simdCopySourceBuilder.ToString()); 31 | } 32 | 33 | static void Main(string[] args) 34 | { 35 | 36 | var source = @" 37 | using System.Runtime.CompilerServices; 38 | using Sapling.Engine.Evaluation; 39 | 40 | namespace Sapling.Engine.Search; 41 | 42 | public unsafe partial class Searcher 43 | { 44 | private static readonly VectorShort Ceil = VectorType.Create(255); 45 | private static readonly VectorShort Floor = VectorType.Create(0); 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | public static void SimdResetAccumulators(VectorShort* whiteAcc, VectorShort* blackAcc) 48 | { 49 | @SimdResetAccumulators@ 50 | } 51 | 52 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 53 | public static void SimdResetAccumulator(VectorShort* acc) 54 | { 55 | @SimdResetAccumulator@ 56 | } 57 | 58 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 59 | public static void SimdCopy(VectorShort* dest, VectorShort* src) 60 | { 61 | @SimdCopy@ 62 | } 63 | 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | public static void Sub( 66 | VectorShort* source, 67 | VectorShort* dest, 68 | VectorShort* sub) 69 | { 70 | @Sub@ 71 | } 72 | 73 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 74 | public static void Add( 75 | VectorShort* source, 76 | VectorShort* dest, 77 | VectorShort* add) 78 | { 79 | @Add@ 80 | } 81 | 82 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 83 | private static void AddWeights(VectorShort* accuPtr, VectorShort* featurePtr) 84 | { 85 | @AddWeights@ 86 | } 87 | 88 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 89 | private static int ForwardCReLU(VectorShort* usAcc, VectorShort* themAcc, int bucket) 90 | { 91 | var sum = VectorInt.Zero; 92 | var featureWeightsPtr = NnueWeights.OutputWeights + bucket * AccumulatorSize * 2; 93 | var themWeightsPtr = featureWeightsPtr + AccumulatorSize; 94 | 95 | @ForwardCReLU@ 96 | 97 | return VectorType.Sum(sum); 98 | } 99 | 100 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 101 | public static void SubAdd( 102 | VectorShort* source, 103 | VectorShort* dest, 104 | VectorShort* sub1, VectorShort* add1) 105 | { 106 | @SubAdd@ 107 | } 108 | 109 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 110 | public static void SubSubAdd(VectorShort* source, VectorShort* dest, VectorShort* sub1, VectorShort* sub2, VectorShort* add1) 111 | { 112 | @SubSubAdd@ 113 | } 114 | 115 | 116 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 117 | public static void SubSubAddAdd(VectorShort* source, VectorShort* dest, VectorShort* sub1, VectorShort* sub2, VectorShort* add1, VectorShort* add2) 118 | { 119 | @SubSubAddAdd@ 120 | } 121 | } 122 | "; 123 | 124 | source = UnrollAndInsert(source, "@SimdResetAccumulator@", (i) => $"*(acc+{i}) = *(NnueWeights.FeatureBiases+{i});"); 125 | source = UnrollAndInsert(source, "@SimdResetAccumulators@", (i) => $"*(whiteAcc+{i}) = *(blackAcc+{i}) = *(NnueWeights.FeatureBiases+{i});"); 126 | source = UnrollAndInsert(source, "@SimdCopy@", (i) => $"*(dest+{i}) = *(src+{i});"); 127 | source = UnrollAndInsert(source, "@Sub@", (i) => $"*(dest + {i}) = *(source + {i}) - *(sub + {i});"); 128 | source = UnrollAndInsert(source, "@Add@", (i) => $"*(dest + {i}) = *(source + {i}) + *(add + {i});"); 129 | source = UnrollAndInsert(source, "@AddWeights@", (i) => $"*(accuPtr + {i}) += *(featurePtr + {i});"); 130 | source = UnrollAndInsert(source, "@ForwardCReLU@", (i) => $" sum += AvxIntrinsics.MultiplyAddAdjacent(AvxIntrinsics.Max(AvxIntrinsics.Min(*(usAcc + {i}), Ceil), Floor), *(featureWeightsPtr + {i})) + AvxIntrinsics.MultiplyAddAdjacent(AvxIntrinsics.Max(AvxIntrinsics.Min(*(themAcc + {i}), Ceil), Floor), *(themWeightsPtr + {i}));"); 131 | source = UnrollAndInsert(source, "@SubAdd@", (i) => $" *(dest + {i}) = *(source + {i}) - *(sub1 + {i}) + *(add1 + {i});"); 132 | source = UnrollAndInsert(source, "@SubSubAdd@", (i) => $"*(dest + {i}) = *(source + {i}) - *(sub1 + {i}) + *(add1 + {i}) - *(sub2 + {i});"); 133 | source = UnrollAndInsert(source, "@SubSubAddAdd@", (i) => $"*(dest + { i}) = *(source + { i}) -*(sub1 + { i}) + *(add1 + { i}) - *(sub2 + { i}) + *(add2 + {i});"); 134 | 135 | 136 | File.WriteAllText("./unrolled.cs", source.ToString()); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Sapling.Utils/Sapling.Utils.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | AnyCPU;x64 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Sapling.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34728.123 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sapling.Engine", "Sapling.Engine\Sapling.Engine.csproj", "{DBF3F812-1153-4D69-BEEF-F4D887A69232}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sapling.Engine.Tests", "Sapling.Engine.Tests\Sapling.Engine.Tests.csproj", "{C6CF5E56-BF12-4558-89CD-1373CE25AA4B}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sapling", "Sapling\Sapling.csproj", "{F889F19A-4106-4765-A531-C9F96D966DE8}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sapling.Utils", "Sapling.Utils\Sapling.Utils.csproj", "{F0836F77-0512-4AF6-B0D1-4AF34B6CAE91}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Release|Any CPU = Release|Any CPU 19 | Release|x64 = Release|x64 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {DBF3F812-1153-4D69-BEEF-F4D887A69232}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {DBF3F812-1153-4D69-BEEF-F4D887A69232}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {DBF3F812-1153-4D69-BEEF-F4D887A69232}.Debug|x64.ActiveCfg = Debug|x64 25 | {DBF3F812-1153-4D69-BEEF-F4D887A69232}.Debug|x64.Build.0 = Debug|x64 26 | {DBF3F812-1153-4D69-BEEF-F4D887A69232}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {DBF3F812-1153-4D69-BEEF-F4D887A69232}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {DBF3F812-1153-4D69-BEEF-F4D887A69232}.Release|x64.ActiveCfg = Release|x64 29 | {DBF3F812-1153-4D69-BEEF-F4D887A69232}.Release|x64.Build.0 = Release|x64 30 | {C6CF5E56-BF12-4558-89CD-1373CE25AA4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {C6CF5E56-BF12-4558-89CD-1373CE25AA4B}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {C6CF5E56-BF12-4558-89CD-1373CE25AA4B}.Debug|x64.ActiveCfg = Debug|x64 33 | {C6CF5E56-BF12-4558-89CD-1373CE25AA4B}.Debug|x64.Build.0 = Debug|x64 34 | {C6CF5E56-BF12-4558-89CD-1373CE25AA4B}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {C6CF5E56-BF12-4558-89CD-1373CE25AA4B}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {C6CF5E56-BF12-4558-89CD-1373CE25AA4B}.Release|x64.ActiveCfg = Release|x64 37 | {C6CF5E56-BF12-4558-89CD-1373CE25AA4B}.Release|x64.Build.0 = Release|x64 38 | {F889F19A-4106-4765-A531-C9F96D966DE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {F889F19A-4106-4765-A531-C9F96D966DE8}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {F889F19A-4106-4765-A531-C9F96D966DE8}.Debug|x64.ActiveCfg = Debug|x64 41 | {F889F19A-4106-4765-A531-C9F96D966DE8}.Debug|x64.Build.0 = Debug|x64 42 | {F889F19A-4106-4765-A531-C9F96D966DE8}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {F889F19A-4106-4765-A531-C9F96D966DE8}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {F889F19A-4106-4765-A531-C9F96D966DE8}.Release|x64.ActiveCfg = Release|x64 45 | {F889F19A-4106-4765-A531-C9F96D966DE8}.Release|x64.Build.0 = Release|x64 46 | {F0836F77-0512-4AF6-B0D1-4AF34B6CAE91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {F0836F77-0512-4AF6-B0D1-4AF34B6CAE91}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {F0836F77-0512-4AF6-B0D1-4AF34B6CAE91}.Debug|x64.ActiveCfg = Debug|x64 49 | {F0836F77-0512-4AF6-B0D1-4AF34B6CAE91}.Debug|x64.Build.0 = Debug|x64 50 | {F0836F77-0512-4AF6-B0D1-4AF34B6CAE91}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {F0836F77-0512-4AF6-B0D1-4AF34B6CAE91}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {F0836F77-0512-4AF6-B0D1-4AF34B6CAE91}.Release|x64.ActiveCfg = Release|x64 53 | {F0836F77-0512-4AF6-B0D1-4AF34B6CAE91}.Release|x64.Build.0 = Release|x64 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(ExtensibilityGlobals) = postSolution 59 | SolutionGuid = {917B47EC-DE4D-4276-A4A0-3EE498CFE6C7} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /src/Sapling.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | Required 3 | Required 4 | Required 5 | Required -------------------------------------------------------------------------------- /src/Sapling/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Sapling/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Reflection; 3 | using System.Runtime.Intrinsics.X86; 4 | using System.Text; 5 | using Sapling.Engine; 6 | using Sapling.Engine.DataGen; 7 | using Sapling.Engine.Evaluation; 8 | using Sapling.Engine.MoveGen; 9 | using Sapling.Engine.Search; 10 | using Sapling.Engine.Tuning; 11 | 12 | namespace Sapling; 13 | 14 | public static class UciOptions 15 | { 16 | public static bool IsDebug = false; 17 | } 18 | internal class Program 19 | { 20 | private static readonly ConcurrentQueue CommandQueue = new(); 21 | private static readonly ManualResetEventSlim CommandAvailable = new(false); 22 | private static bool hasQuit = false; 23 | 24 | private static FileStream _fileStream; 25 | private static StreamWriter _logWriter; 26 | 27 | private static void Main(string[] args) 28 | { 29 | Console.SetIn(new StreamReader(Console.OpenStandardInput(), Encoding.UTF8, false, 2048 * 4)); 30 | 31 | if (args.Length > 0 && args[0] == "--version") 32 | { 33 | // Get the version from the assembly information 34 | var version = Assembly.GetExecutingAssembly().GetName().Version; 35 | Console.WriteLine($"{version.Major}-{version.Minor}-{version.Build}"); 36 | return; 37 | } 38 | 39 | #if AVX512 40 | if (!Avx512BW.IsSupported) 41 | { 42 | Console.WriteLine("[Error] Avx512BW is not supported on this system"); 43 | return; 44 | } 45 | #else 46 | if (!Avx2.IsSupported) 47 | { 48 | Console.WriteLine("[Error] Avx2 is not supported on this system"); 49 | return; 50 | } 51 | #endif 52 | 53 | if (!Bmi1.IsSupported) 54 | { 55 | Console.WriteLine("[Error] Bmi1 is not supported on this system"); 56 | return; 57 | } 58 | 59 | if (!Bmi2.IsSupported) 60 | { 61 | Console.WriteLine("[Error] Bmi2 is not supported on this system"); 62 | return; 63 | } 64 | 65 | if (!Popcnt.IsSupported) 66 | { 67 | Console.WriteLine("[Error] Popcnt is not supported on this system"); 68 | return; 69 | } 70 | 71 | if (!Sse.IsSupported) 72 | { 73 | Console.WriteLine("[Error] Sse is not supported on this system"); 74 | return; 75 | } 76 | var logDirectory = Path.Combine(Environment.CurrentDirectory, "logs"); 77 | if (!Directory.Exists(logDirectory)) 78 | { 79 | Directory.CreateDirectory(logDirectory); 80 | } 81 | 82 | var fileName = (DateTime.Now.ToString("g") + Guid.NewGuid()).Replace("/", "-").Replace(" ", "_") 83 | .Replace(":", "-"); 84 | var logFilePath = Path.Combine(logDirectory, $"{fileName}.txt"); 85 | _fileStream = new FileStream(logFilePath, FileMode.Append, FileAccess.Write); 86 | _logWriter = new StreamWriter(_fileStream); 87 | 88 | 89 | AppDomain.CurrentDomain.UnhandledException += (sender, e) => 90 | { 91 | // Log the exception or take appropriate action 92 | Console.WriteLine("Unhandled Exception: " + ((Exception)e.ExceptionObject).Message); 93 | _logWriter.WriteLine("Unhandled Exception: " + ((Exception)e.ExceptionObject).Message); 94 | _logWriter.Flush(); 95 | _logWriter.Close(); 96 | 97 | }; 98 | 99 | // Force the static constructors to be called 100 | var tasks = new[] 101 | { 102 | Task.Run(() => System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(NnueWeights).TypeHandle)), 103 | Task.Run(() => System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(NnueExtensions).TypeHandle)), 104 | Task.Run(() => System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(AttackTables).TypeHandle)), 105 | Task.Run(() => System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(PieceValues).TypeHandle)), 106 | Task.Run(() => System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(RepetitionDetector).TypeHandle)), 107 | Task.Run(() => System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(HistoryHeuristicExtensions).TypeHandle)), 108 | Task.Run(() => System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(PVTable).TypeHandle)), 109 | Task.Run(() => System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(Zobrist).TypeHandle)), 110 | Task.Run(() => System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(SpsaOptions).TypeHandle)) 111 | }; 112 | // Wait for all tasks to complete 113 | Task.WaitAll(tasks); 114 | 115 | GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true); 116 | GC.WaitForPendingFinalizers(); 117 | 118 | if (args.Contains("bench")) 119 | { 120 | Bench.Run(); 121 | return; 122 | } 123 | 124 | UciOptions.IsDebug = args.Contains("debug"); 125 | 126 | 127 | try 128 | { 129 | UciEngine engine = new(_logWriter); 130 | 131 | // Start the command reading task 132 | _ = Task.Run(() => 133 | { 134 | ReadCommands(engine); 135 | }); 136 | 137 | // Process commands in the main loop 138 | ProcessCommands(engine); 139 | } 140 | catch (Exception ex) 141 | { 142 | _logWriter.WriteLine("[FATAL ERROR]"); 143 | _logWriter.WriteLine("----------"); 144 | _logWriter.WriteLine(ex.ToString()); 145 | _logWriter.WriteLine("----------"); 146 | } 147 | finally 148 | { 149 | _logWriter.Flush(); 150 | _logWriter.Flush(); 151 | _logWriter.Close(); 152 | } 153 | } 154 | 155 | private static void ReadCommands(UciEngine engine) 156 | { 157 | while (true) 158 | { 159 | var command = Console.ReadLine(); 160 | if (string.IsNullOrEmpty(command)) 161 | { 162 | continue; // Skip empty commands 163 | } 164 | 165 | if (command.Contains("quit", StringComparison.OrdinalIgnoreCase)) 166 | { 167 | hasQuit = true; 168 | engine.ReceiveCommand("stop"); 169 | Environment.Exit(0); 170 | break; 171 | } 172 | 173 | if (command.Contains("stop", StringComparison.OrdinalIgnoreCase)) 174 | { 175 | // Process the stop command immediately 176 | engine.ReceiveCommand(command); 177 | continue; 178 | } 179 | 180 | CommandQueue.Enqueue(command); 181 | CommandAvailable.Set(); // Signal that a command is available 182 | } 183 | } 184 | 185 | private static void ProcessCommands(UciEngine engine) 186 | { 187 | while (!hasQuit) 188 | { 189 | CommandAvailable.Wait(); // Wait until a command is available 190 | CommandAvailable.Reset(); // Reset the event for the next wait 191 | 192 | while (CommandQueue.TryDequeue(out var command)) 193 | { 194 | engine.ReceiveCommand(command); 195 | } 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /src/Sapling/Sapling.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | logo.ico 9 | Sapling 10 | 1.2.7.0 11 | 1.2.7.0 12 | 1.2.7.0 13 | true 14 | true 15 | true 16 | preview 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Release 25 | true 26 | true 27 | true 28 | true 29 | true 30 | false 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | .exe 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/Sapling/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling/logo.ico -------------------------------------------------------------------------------- /src/Sapling/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/Sapling/logo.png -------------------------------------------------------------------------------- /src/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/logo.ico -------------------------------------------------------------------------------- /src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Timmoth/Sapling/11b04b3ce0689139471118ae6b2d8293c5a80475/src/logo.png --------------------------------------------------------------------------------