├── .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
--------------------------------------------------------------------------------