├── .editorconfig
├── .gitattributes
├── .gitignore
├── Chess-Challenge.sln
├── Chess-Challenge
├── Chess-Challenge.csproj
├── resources
│ ├── Fens.txt
│ ├── Fonts
│ │ ├── OPENSANS-SEMIBOLD.TTF
│ │ └── sdf.fs
│ └── Pieces.png
└── src
│ ├── API
│ ├── BitboardHelper.cs
│ ├── Board.cs
│ ├── IChessBot.cs
│ ├── Move.cs
│ ├── Piece.cs
│ ├── PieceList.cs
│ ├── PieceType.cs
│ ├── Square.cs
│ └── Timer.cs
│ ├── Evil Bot
│ └── EvilBot.cs
│ ├── Framework
│ ├── Application
│ │ ├── Core
│ │ │ ├── ChallengeController.cs
│ │ │ ├── Program.cs
│ │ │ └── Settings.cs
│ │ ├── Helpers
│ │ │ ├── API Helpers
│ │ │ │ ├── APIMoveGen.cs
│ │ │ │ └── MoveHelper.cs
│ │ │ ├── ConsoleHelper.cs
│ │ │ ├── FileHelper.cs
│ │ │ ├── Tester.cs
│ │ │ ├── Token Counter
│ │ │ │ ├── Microsoft.CodeAnalysis.CSharp.dll
│ │ │ │ ├── Microsoft.CodeAnalysis.dll
│ │ │ │ └── TokenCounter.cs
│ │ │ ├── UIHelper.cs
│ │ │ └── Warmer.cs
│ │ ├── Players
│ │ │ ├── ChessPlayer.cs
│ │ │ └── HumanPlayer.cs
│ │ └── UI
│ │ │ ├── BoardTheme.cs
│ │ │ ├── BoardUI.cs
│ │ │ ├── BotBrainCapacityUI.cs
│ │ │ ├── MatchStatsUI.cs
│ │ │ └── MenuUI.cs
│ └── Chess
│ │ ├── Board
│ │ ├── Board.cs
│ │ ├── Coord.cs
│ │ ├── GameState.cs
│ │ ├── Move.cs
│ │ ├── PieceHelper.cs
│ │ ├── PieceList.cs
│ │ └── Zobrist.cs
│ │ ├── Helpers
│ │ ├── BoardHelper.cs
│ │ ├── FenUtility.cs
│ │ ├── MoveUtility.cs
│ │ ├── PGNCreator.cs
│ │ └── PGNLoader.cs
│ │ ├── Move Generation
│ │ ├── Bitboards
│ │ │ ├── BitBoardUtility.cs
│ │ │ └── Bits.cs
│ │ ├── Magics
│ │ │ ├── Magic.cs
│ │ │ ├── MagicHelper.cs
│ │ │ └── PrecomputedMagics.cs
│ │ ├── MoveGenerator.cs
│ │ └── PrecomputedMoveData.cs
│ │ └── Result
│ │ ├── Arbiter.cs
│ │ └── GameResult.cs
│ └── My Bot
│ └── MyBot.cs
└── README.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # CS8602: Dereference of a possibly null reference.
4 | dotnet_diagnostic.CS8602.severity = silent
5 | # CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
6 | dotnet_diagnostic.CS8618.severity = silent
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/Chess-Challenge.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32901.215
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Chess-Challenge", "Chess-Challenge\Chess-Challenge.csproj", "{2803E64F-15AC-430B-A5A2-69C00EA7505F}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9FC5D849-F0B6-4C89-A541-C36A341AE67C}"
9 | ProjectSection(SolutionItems) = preProject
10 | .editorconfig = .editorconfig
11 | EndProjectSection
12 | EndProject
13 | Global
14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
15 | Debug|Any CPU = Debug|Any CPU
16 | Release|Any CPU = Release|Any CPU
17 | EndGlobalSection
18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
19 | {2803E64F-15AC-430B-A5A2-69C00EA7505F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20 | {2803E64F-15AC-430B-A5A2-69C00EA7505F}.Debug|Any CPU.Build.0 = Debug|Any CPU
21 | {2803E64F-15AC-430B-A5A2-69C00EA7505F}.Release|Any CPU.ActiveCfg = Release|Any CPU
22 | {2803E64F-15AC-430B-A5A2-69C00EA7505F}.Release|Any CPU.Build.0 = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(SolutionProperties) = preSolution
25 | HideSolutionNode = FALSE
26 | EndGlobalSection
27 | GlobalSection(ExtensibilityGlobals) = postSolution
28 | SolutionGuid = {DC54E848-1F03-426F-9B00-9CBC391363F6}
29 | EndGlobalSection
30 | EndGlobal
31 |
--------------------------------------------------------------------------------
/Chess-Challenge/Chess-Challenge.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | Chess_Challenge
7 | disable
8 | enable
9 | True
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | src\Token Counter\Microsoft.CodeAnalysis.dll
19 |
20 |
21 | src\Token Counter\Microsoft.CodeAnalysis.CSharp.dll
22 |
23 |
24 |
25 |
26 |
27 | Always
28 |
29 |
30 | Never
31 |
32 |
33 | Always
34 |
35 |
36 |
37 |
38 |
39 | PreserveNewest
40 |
41 |
42 | Always
43 |
44 |
45 | Always
46 |
47 |
48 | PreserveNewest
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Chess-Challenge/resources/Fonts/OPENSANS-SEMIBOLD.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jw1912/Chess-Challenge/e65b3b821f4179980397e4e62fec9227b984b850/Chess-Challenge/resources/Fonts/OPENSANS-SEMIBOLD.TTF
--------------------------------------------------------------------------------
/Chess-Challenge/resources/Fonts/sdf.fs:
--------------------------------------------------------------------------------
1 | #version 330
2 |
3 | // Input vertex attributes (from vertex shader)
4 | in vec2 fragTexCoord;
5 | in vec4 fragColor;
6 |
7 | // Input uniform values
8 | uniform sampler2D texture0;
9 | uniform vec4 colDiffuse;
10 |
11 | // Output fragment color
12 | out vec4 finalColor;
13 |
14 | // NOTE: Add here your custom variables
15 |
16 | void main()
17 | {
18 | // Texel color fetching from texture sampler
19 | // NOTE: Calculate alpha using signed distance field (SDF)
20 | float distanceFromOutline = texture(texture0, fragTexCoord).a - 0.5;
21 | float distanceChangePerFragment = length(vec2(dFdx(distanceFromOutline), dFdy(distanceFromOutline)));
22 | float alpha = smoothstep(-distanceChangePerFragment, distanceChangePerFragment, distanceFromOutline);
23 |
24 | // Calculate final fragment color
25 | finalColor = vec4(fragColor.rgb, fragColor.a*alpha);
26 | }
--------------------------------------------------------------------------------
/Chess-Challenge/resources/Pieces.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jw1912/Chess-Challenge/e65b3b821f4179980397e4e62fec9227b984b850/Chess-Challenge/resources/Pieces.png
--------------------------------------------------------------------------------
/Chess-Challenge/src/API/BitboardHelper.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace ChessChallenge.API
3 | {
4 | using ChessChallenge.Chess;
5 |
6 | ///
7 | /// Helper class for working with bitboards.
8 | /// Bitboards are represented with the ulong type (unsigned 64 bit integer).
9 | ///
10 | public static class BitboardHelper
11 | {
12 | ///
13 | /// Set the given square on the bitboard to 1.
14 | ///
15 | public static void SetSquare(ref ulong bitboard, Square square)
16 | {
17 | bitboard |= 1ul << square.Index;
18 | }
19 | ///
20 | /// Clear the given square on the bitboard to 0.
21 | ///
22 | public static void ClearSquare(ref ulong bitboard, Square square)
23 | {
24 | bitboard &= ~(1ul << square.Index);
25 | }
26 |
27 | ///
28 | /// Toggle the given square on the bitboard between 0 and 1.
29 | ///
30 | public static void ToggleSquare(ref ulong bitboard, Square square)
31 | {
32 | bitboard ^= 1ul << square.Index;
33 | }
34 |
35 | ///
36 | /// Returns true if the given square is set to 1 on the bitboard, otherwise false.
37 | ///
38 | public static bool SquareIsSet(ulong bitboard, Square square)
39 | {
40 | return ((bitboard >> square.Index) & 1) != 0;
41 | }
42 |
43 | public static int ClearAndGetIndexOfLSB(ref ulong bitboard)
44 | {
45 | return BitBoardUtility.PopLSB(ref bitboard);
46 | }
47 |
48 | public static int GetNumberOfSetBits(ulong bitboard)
49 | {
50 | return BitBoardUtility.PopCount(bitboard);
51 | }
52 |
53 | ///
54 | /// Returns a bitboard where each bit that is set to 1 represents a square that the given
55 | /// sliding piece type is able to attack. These attacks are calculated from the given square,
56 | /// and take the given board state into account (so attacks will be blocked by pieces that are in the way).
57 | /// Valid only for sliding piece types (queen, rook, and bishop).
58 | ///
59 | public static ulong GetSliderAttacks(PieceType pieceType, Square square, Board board)
60 | {
61 | return pieceType switch
62 | {
63 | PieceType.Rook => GetRookAttacks(square, board.AllPiecesBitboard),
64 | PieceType.Bishop => GetBishopAttacks(square, board.AllPiecesBitboard),
65 | PieceType.Queen => GetQueenAttacks(square, board.AllPiecesBitboard),
66 | _ => 0
67 | };
68 | }
69 |
70 | ///
71 | /// Returns a bitboard where each bit that is set to 1 represents a square that the given
72 | /// sliding piece type is able to attack. These attacks are calculated from the given square,
73 | /// and take the given blocker bitboard into account (so attacks will be blocked by pieces that are in the way).
74 | /// Valid only for sliding piece types (queen, rook, and bishop).
75 | ///
76 | public static ulong GetSliderAttacks(PieceType pieceType, Square square, ulong blockers)
77 | {
78 | return pieceType switch
79 | {
80 | PieceType.Rook => GetRookAttacks(square, blockers),
81 | PieceType.Bishop => GetBishopAttacks(square, blockers),
82 | PieceType.Queen => GetQueenAttacks(square, blockers),
83 | _ => 0
84 | };
85 | }
86 | ///
87 | /// Gets a bitboard of squares that a knight can attack from the given square.
88 | ///
89 | public static ulong GetKnightAttacks(Square square) => Bits.KnightAttacks[square.Index];
90 | ///
91 | /// Gets a bitboard of squares that a king can attack from the given square.
92 | ///
93 | public static ulong GetKingAttacks(Square square) => Bits.KingMoves[square.Index];
94 |
95 | ///
96 | /// Gets a bitboard of squares that a pawn (of the given colour) can attack from the given square.
97 | ///
98 | public static ulong GetPawnAttacks(Square square, bool isWhite)
99 | {
100 | return isWhite ? Bits.WhitePawnAttacks[square.Index] : Bits.BlackPawnAttacks[square.Index];
101 | }
102 | static ulong GetRookAttacks(Square square, ulong blockers)
103 | {
104 | ulong mask = Magic.RookMask[square.Index];
105 | ulong magic = PrecomputedMagics.RookMagics[square.Index];
106 | int shift = PrecomputedMagics.RookShifts[square.Index];
107 |
108 | ulong key = ((blockers & mask) * magic) >> shift;
109 | return Magic.RookAttacks[square.Index][key];
110 | }
111 |
112 | static ulong GetBishopAttacks(Square square, ulong blockers)
113 | {
114 | ulong mask = Magic.BishopMask[square.Index];
115 | ulong magic = PrecomputedMagics.BishopMagics[square.Index];
116 | int shift = PrecomputedMagics.BishopShifts[square.Index];
117 |
118 | ulong key = ((blockers & mask) * magic) >> shift;
119 | return Magic.BishopAttacks[square.Index][key];
120 | }
121 |
122 | static ulong GetQueenAttacks(Square square, ulong blockers)
123 | {
124 | return GetRookAttacks(square, blockers) | GetBishopAttacks(square, blockers);
125 | }
126 | }
127 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/API/Board.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.API
2 | {
3 | using ChessChallenge.Application.APIHelpers;
4 | using ChessChallenge.Chess;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 |
9 | public sealed class Board
10 | {
11 | readonly Chess.Board board;
12 | readonly APIMoveGen moveGen;
13 |
14 | readonly HashSet repetitionHistory;
15 | readonly PieceList[] allPieceLists;
16 | readonly PieceList[] validPieceLists;
17 |
18 | Move[] cachedLegalMoves;
19 | bool hasCachedMoves;
20 | Move[] cachedLegalCaptureMoves;
21 | bool hasCachedCaptureMoves;
22 | readonly Move[] movesDest;
23 |
24 | ///
25 | /// Create a new board. Note: this should not be used in the challenge,
26 | /// use the board provided in the Think method instead.
27 | ///
28 | public Board(Chess.Board board)
29 | {
30 | this.board = board;
31 | moveGen = new APIMoveGen();
32 | cachedLegalMoves = Array.Empty();
33 | cachedLegalCaptureMoves = Array.Empty();
34 | movesDest = new Move[APIMoveGen.MaxMoves];
35 |
36 | // Init piece lists
37 | List validPieceLists = new();
38 | allPieceLists = new PieceList[board.pieceLists.Length];
39 | for (int i = 0; i < board.pieceLists.Length; i++)
40 | {
41 | if (board.pieceLists[i] != null)
42 | {
43 | allPieceLists[i] = new PieceList(board.pieceLists[i], this, i);
44 | validPieceLists.Add(allPieceLists[i]);
45 | }
46 | }
47 | this.validPieceLists = validPieceLists.ToArray();
48 |
49 | // Init rep history
50 | repetitionHistory = new HashSet(board.RepetitionPositionHistory);
51 | GameRepetitionHistory = repetitionHistory.ToArray();
52 | GameRepetitionHistory.Reverse();
53 | repetitionHistory.Remove(board.ZobristKey);
54 |
55 | // Init game moves history
56 | GameMoveHistory = board.AllGameMoves.Select(m => new Move(MoveUtility.GetMoveNameUCI(m), this)).ToArray();
57 | }
58 |
59 | ///
60 | /// Updates the board state with the given move.
61 | /// The move is assumed to be legal, and may result in errors if it is not.
62 | /// Can be undone with the UndoMove method.
63 | ///
64 | public void MakeMove(Move move)
65 | {
66 | hasCachedMoves = false;
67 | hasCachedCaptureMoves = false;
68 | if (!move.IsNull)
69 | {
70 | repetitionHistory.Add(board.ZobristKey);
71 | board.MakeMove(new Chess.Move(move.RawValue), inSearch: true);
72 | }
73 | }
74 |
75 | ///
76 | /// Undo a move that was made with the MakeMove method
77 | ///
78 | public void UndoMove(Move move)
79 | {
80 | hasCachedMoves = false;
81 | hasCachedCaptureMoves = false;
82 | if (!move.IsNull)
83 | {
84 | board.UndoMove(new Chess.Move(move.RawValue), inSearch: true);
85 | repetitionHistory.Remove(board.ZobristKey);
86 | }
87 | }
88 |
89 | ///
90 | /// Try skip the current turn
91 | /// This will fail and return false if in check
92 | /// Note: skipping a turn is not allowed in the game, but it can be used as a search technique
93 | ///
94 | public bool TrySkipTurn()
95 | {
96 | if (IsInCheck())
97 | {
98 | return false;
99 | }
100 | hasCachedMoves = false;
101 | hasCachedCaptureMoves = false;
102 | board.MakeNullMove();
103 | return true;
104 | }
105 |
106 | ///
107 | /// Undo a turn that was succesfully skipped with the TrySkipTurn method
108 | ///
109 | public void UndoSkipTurn()
110 | {
111 | hasCachedMoves = false;
112 | hasCachedCaptureMoves = false;
113 | board.UnmakeNullMove();
114 | }
115 |
116 | ///
117 | /// Gets an array of the legal moves in the current position.
118 | /// Can choose to get only capture moves with the optional 'capturesOnly' parameter.
119 | ///
120 | public Move[] GetLegalMoves(bool capturesOnly = false)
121 | {
122 | if (capturesOnly)
123 | {
124 | return GetLegalCaptureMoves();
125 | }
126 |
127 | if (!hasCachedMoves)
128 | {
129 | Span moveSpan = movesDest.AsSpan();
130 | moveGen.GenerateMoves(ref moveSpan, board, includeQuietMoves: true);
131 | cachedLegalMoves = moveSpan.ToArray();
132 | hasCachedMoves = true;
133 | }
134 |
135 | return cachedLegalMoves;
136 | }
137 |
138 | ///
139 | /// Fills the given move span with legal moves, and slices it to the correct length.
140 | /// Can choose to get only capture moves with the optional 'capturesOnly' parameter.
141 | /// This gives the same result as the GetLegalMoves function, but allows you to be more
142 | /// efficient with memory by allocating moves on the stack rather than the heap.
143 | ///
144 | public void GetLegalMovesNonAlloc(ref Span moveList, bool capturesOnly = false)
145 | {
146 | bool includeQuietMoves = !capturesOnly;
147 | moveGen.GenerateMoves(ref moveList, board, includeQuietMoves);
148 | }
149 |
150 |
151 | Move[] GetLegalCaptureMoves()
152 | {
153 | if (!hasCachedCaptureMoves)
154 | {
155 | Span moveSpan = movesDest.AsSpan();
156 | moveGen.GenerateMoves(ref moveSpan, board, includeQuietMoves: false);
157 | cachedLegalCaptureMoves = moveSpan.ToArray();
158 | hasCachedCaptureMoves = true;
159 | }
160 | return cachedLegalCaptureMoves;
161 | }
162 |
163 | ///
164 | /// Test if the player to move is in check in the current position.
165 | ///
166 | public bool IsInCheck() => board.IsInCheck();
167 |
168 | ///
169 | /// Test if the current position is checkmate
170 | ///
171 | public bool IsInCheckmate() => IsInCheck() && GetLegalMoves().Length == 0;
172 |
173 | ///
174 | /// Test if the current position is a draw due stalemate, repetition, insufficient material, or 50-move rule.
175 | /// Note: this function will return true if the same position has occurred twice on the board (rather than 3 times,
176 | /// which is when the game is actually drawn). This quirk is to help bots avoid repeating positions unnecessarily.
177 | ///
178 | public bool IsDraw()
179 | {
180 | return IsFiftyMoveDraw() || IsInsufficientMaterial() || IsInStalemate() || IsRepeatedPosition();
181 |
182 | bool IsInStalemate() => !IsInCheck() && GetLegalMoves().Length == 0;
183 | bool IsFiftyMoveDraw() => board.currentGameState.fiftyMoveCounter >= 100;
184 | }
185 |
186 | ///
187 | /// Test if the current position has occurred at least once before on the board.
188 | /// This includes both positions in the actual game, and positions reached by
189 | /// making moves while the bot is thinking.
190 | ///
191 | public bool IsRepeatedPosition() => repetitionHistory.Contains(board.ZobristKey);
192 |
193 | ///
194 | /// Test if there are sufficient pieces remaining on the board to potentially deliver checkmate.
195 | /// If not, the game is automatically a draw.
196 | ///
197 | public bool IsInsufficientMaterial() => Arbiter.InsufficentMaterial(board);
198 |
199 | ///
200 | /// Does the given player still have the right to castle kingside?
201 | /// Note that having the right to castle doesn't necessarily mean castling is legal right now
202 | /// (for example, a piece might be in the way, or player might be in check, etc).
203 | ///
204 | public bool HasKingsideCastleRight(bool white) => board.currentGameState.HasKingsideCastleRight(white);
205 |
206 | ///
207 | /// Does the given player still have the right to castle queenside?
208 | /// Note that having the right to castle doesn't necessarily mean castling is legal right now
209 | /// (for example, a piece might be in the way, or player might be in check, etc).
210 | ///
211 | public bool HasQueensideCastleRight(bool white) => board.currentGameState.HasQueensideCastleRight(white);
212 |
213 | ///
214 | /// Gets the square that the king (of the given colour) is currently on.
215 | ///
216 | public Square GetKingSquare(bool white)
217 | {
218 | int colIndex = white ? Chess.Board.WhiteIndex : Chess.Board.BlackIndex;
219 | return new Square(board.KingSquare[colIndex]);
220 | }
221 |
222 | ///
223 | /// Gets the piece on the given square. If the square is empty, the piece will have a PieceType of None.
224 | ///
225 | public Piece GetPiece(Square square)
226 | {
227 | int p = board.Square[square.Index];
228 | bool white = PieceHelper.IsWhite(p);
229 | return new Piece((PieceType)PieceHelper.PieceType(p), white, square);
230 | }
231 |
232 | ///
233 | /// Gets a list of pieces of the given type and colour
234 | ///
235 | public PieceList GetPieceList(PieceType pieceType, bool white)
236 | {
237 | return allPieceLists[PieceHelper.MakePiece((int)pieceType, white)];
238 | }
239 | ///
240 | /// Gets an array of all the piece lists. In order these are:
241 | /// Pawns(white), Knights (white), Bishops (white), Rooks (white), Queens (white), King (white),
242 | /// Pawns (black), Knights (black), Bishops (black), Rooks (black), Queens (black), King (black)
243 | ///
244 | public PieceList[] GetAllPieceLists()
245 | {
246 | return validPieceLists;
247 | }
248 |
249 | ///
250 | /// Is the given square attacked by the opponent?
251 | /// (opponent being whichever player doesn't currently have the right to move)
252 | ///
253 | public bool SquareIsAttackedByOpponent(Square square)
254 | {
255 | if (!hasCachedMoves)
256 | {
257 | GetLegalMoves();
258 | }
259 | return BitboardHelper.SquareIsSet(moveGen.opponentAttackMap, square);
260 | }
261 |
262 |
263 | ///
264 | /// FEN representation of the current position
265 | ///
266 | public string GetFenString() => FenUtility.CurrentFen(board);
267 |
268 | ///
269 | /// 64-bit number where each bit that is set to 1 represents a
270 | /// square that contains a piece of the given type and colour.
271 | ///
272 | public ulong GetPieceBitboard(PieceType pieceType, bool white)
273 | {
274 | return board.pieceBitboards[PieceHelper.MakePiece((int)pieceType, white)];
275 | }
276 | ///
277 | /// 64-bit number where each bit that is set to 1 represents a square that contains any type of white piece.
278 | ///
279 | public ulong WhitePiecesBitboard => board.colourBitboards[Chess.Board.WhiteIndex];
280 | ///
281 | /// 64-bit number where each bit that is set to 1 represents a square that contains any type of black piece.
282 | ///
283 | public ulong BlackPiecesBitboard => board.colourBitboards[Chess.Board.BlackIndex];
284 |
285 | ///
286 | /// 64-bit number where each bit that is set to 1 represents a
287 | /// square that contains a piece of any type or colour.
288 | ///
289 | public ulong AllPiecesBitboard => board.allPiecesBitboard;
290 |
291 |
292 | public bool IsWhiteToMove => board.IsWhiteToMove;
293 |
294 | ///
295 | /// Number of ply (a single move by either white or black) played so far
296 | ///
297 | public int PlyCount => board.plyCount;
298 |
299 | ///
300 | /// Number of ply (a single move by either white or black) since the last pawn move or capture.
301 | /// If this value reaches a hundred (meaning 50 full moves without a pawn move or capture), the game is drawn.
302 | ///
303 | public int FiftyMoveCounter => board.currentGameState.fiftyMoveCounter;
304 |
305 | ///
306 | /// 64-bit hash of the current position
307 | ///
308 | public ulong ZobristKey => board.ZobristKey;
309 |
310 | ///
311 | /// Zobrist keys for all the positions played in the game so far. This is reset whenever a
312 | /// pawn move or capture is made, as previous positions are now impossible to reach again.
313 | /// Note that this is not updated when your bot makes moves on the board while thinking,
314 | /// but rather only when moves are actually played in the game.
315 | ///
316 | public ulong[] GameRepetitionHistory { get; private set; }
317 |
318 | ///
319 | /// FEN representation of the game's starting position.
320 | ///
321 | public string GameStartFenString => board.GameStartFen;
322 |
323 | ///
324 | /// All the moves played in the game so far.
325 | /// This only includes moves played in the actual game, not moves made on the board while the bot is thinking.
326 | ///
327 | public Move[] GameMoveHistory { get; private set; }
328 |
329 | ///
330 | /// Creates a board from the given fen string. Please note that this is quite slow, and so it is advised
331 | /// to use the board given in the Think function, and update it using MakeMove and UndoMove instead.
332 | ///
333 | public static Board CreateBoardFromFEN(string fen)
334 | {
335 | Chess.Board boardCore = new Chess.Board();
336 | boardCore.LoadPosition(fen);
337 | return new Board(boardCore);
338 | }
339 |
340 | }
341 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/API/IChessBot.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace ChessChallenge.API
3 | {
4 | public interface IChessBot
5 | {
6 | Move Think(Board board, Timer timer);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/API/Move.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.Chess;
2 | using System;
3 |
4 | namespace ChessChallenge.API
5 | {
6 | public readonly struct Move : IEquatable
7 | {
8 | public Square StartSquare => new Square(move.StartSquareIndex);
9 | public Square TargetSquare => new Square(move.TargetSquareIndex);
10 | public PieceType MovePieceType => (PieceType)(pieceTypeData & 0b111);
11 | public PieceType CapturePieceType => (PieceType)(pieceTypeData >> 3);
12 | public PieceType PromotionPieceType => (PieceType)move.PromotionPieceType;
13 | public bool IsCapture => (pieceTypeData >> 3) != 0;
14 | public bool IsEnPassant => move.MoveFlag == Chess.Move.EnPassantCaptureFlag;
15 |
16 | public bool IsPromotion => move.IsPromotion;
17 | public bool IsCastles => move.MoveFlag == Chess.Move.CastleFlag;
18 | public bool IsNull => move.IsNull;
19 | public ushort RawValue => move.Value;
20 | public static readonly Move NullMove = new();
21 |
22 | readonly Chess.Move move;
23 | readonly ushort pieceTypeData;
24 |
25 | ///
26 | /// Create a null/invalid move.
27 | /// This is simply an invalid move that can be used as a placeholder until a valid move has been found
28 | ///
29 | public Move()
30 | {
31 | move = Chess.Move.NullMove;
32 | pieceTypeData = 0;
33 | }
34 |
35 | ///
36 | /// Create a move from UCI notation, for example: "e2e4" to move a piece from e2 to e4.
37 | /// If promoting, piece type must be included, for example: "d7d8q".
38 | ///
39 | public Move(string moveName, Board board)
40 | {
41 | var data = Application.APIHelpers.MoveHelper.CreateMoveFromName(moveName, board);
42 | move = data.move;
43 | pieceTypeData = (ushort)((int)data.pieceType | ((int)data.captureType << 3));
44 |
45 | }
46 |
47 | ///
48 | /// Internal move constructor. Do not use.
49 | ///
50 | public Move(Chess.Move move, int movePieceType, int capturePieceType)
51 | {
52 | this.move = move;
53 | pieceTypeData = (ushort)(movePieceType | (capturePieceType << 3));
54 | }
55 |
56 | public override string ToString()
57 | {
58 | string moveName = MoveUtility.GetMoveNameUCI(move);
59 | return $"Move: '{moveName}'";
60 | }
61 |
62 | ///
63 | /// Tests if two moves are the same.
64 | /// This is true if they move to/from the same square, and move/capture/promote the same piece type
65 | ///
66 | public bool Equals(Move other)
67 | {
68 | return RawValue == other.RawValue && pieceTypeData == other.pieceTypeData;
69 | }
70 |
71 | public static bool operator ==(Move lhs, Move rhs) => lhs.Equals(rhs);
72 | public static bool operator !=(Move lhs, Move rhs) => !lhs.Equals(rhs);
73 | public override bool Equals(object? obj) => base.Equals(obj);
74 | public override int GetHashCode() => RawValue;
75 | }
76 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/API/Piece.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ChessChallenge.API
4 | {
5 | public readonly struct Piece : IEquatable
6 | {
7 | public readonly bool IsWhite;
8 | public readonly PieceType PieceType;
9 | ///
10 | /// The square that the piece is on. Note that this value will not be updated if the
11 | /// piece is moved, it is a snapshot of the state of the piece when it was looked up.
12 | ///
13 | public readonly Square Square;
14 |
15 | public bool IsNull => PieceType is PieceType.None;
16 | public bool IsRook => PieceType is PieceType.Rook;
17 | public bool IsKnight => PieceType is PieceType.Knight;
18 | public bool IsBishop => PieceType is PieceType.Bishop;
19 | public bool IsQueen => PieceType is PieceType.Queen;
20 | public bool IsKing => PieceType is PieceType.King;
21 | public bool IsPawn => PieceType is PieceType.Pawn;
22 |
23 | ///
24 | /// Create a piece from its type, colour, and square
25 | ///
26 | public Piece(PieceType pieceType, bool isWhite, Square square)
27 | {
28 | PieceType = pieceType;
29 | Square = square;
30 | IsWhite = isWhite;
31 | }
32 |
33 | public override string ToString()
34 | {
35 | if (IsNull)
36 | {
37 | return "Null";
38 | }
39 | string col = IsWhite ? "White" : "Black";
40 | return $"{col} {PieceType}";
41 | }
42 |
43 | // Comparisons:
44 | public static bool operator ==(Piece lhs, Piece rhs) => lhs.Equals(rhs);
45 | public static bool operator !=(Piece lhs, Piece rhs) => !lhs.Equals(rhs);
46 | public override bool Equals(object? obj) => base.Equals(obj);
47 | public override int GetHashCode() => base.GetHashCode();
48 |
49 | public bool Equals(Piece other)
50 | {
51 | return IsWhite == other.IsWhite && PieceType == other.PieceType && Square == other.Square;
52 | }
53 |
54 | }
55 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/API/PieceList.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Collections.Generic;
3 |
4 | namespace ChessChallenge.API
5 | {
6 | ///
7 | /// A special list for storing pieces of a particular type and colour
8 | ///
9 | public sealed class PieceList : IEnumerable
10 | {
11 | public int Count => list.Count;
12 | public readonly bool IsWhitePieceList;
13 | public readonly PieceType TypeOfPieceInList;
14 | public Piece GetPiece(int index) => this[index];
15 |
16 | readonly Chess.PieceList list;
17 | readonly Board board;
18 |
19 | ///
20 | /// Piece List constructor (you shouldn't be creating your own piece lists in
21 | /// this challenge, but rather accessing the existing lists from the board).
22 | ///
23 | public PieceList(Chess.PieceList list, Board board, int piece)
24 | {
25 | this.board = board;
26 | this.list = list;
27 | TypeOfPieceInList = (PieceType)Chess.PieceHelper.PieceType(piece);
28 | IsWhitePieceList = Chess.PieceHelper.IsWhite(piece);
29 | }
30 |
31 |
32 | public Piece this[int index] => board.GetPiece(new Square(list[index]));
33 |
34 | // Allow piece list to be iterated over with 'foreach'
35 | public IEnumerator GetEnumerator()
36 | {
37 | for (int i = 0; i < Count; i++)
38 | {
39 | yield return GetPiece(i);
40 | }
41 | }
42 |
43 | IEnumerator IEnumerable.GetEnumerator()
44 | {
45 | return GetEnumerator();
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/API/PieceType.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.API
2 | {
3 | public enum PieceType
4 | {
5 | None, // 0
6 | Pawn, // 1
7 | Knight, // 2
8 | Bishop, // 3
9 | Rook, // 4
10 | Queen, // 5
11 | King // 6
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/API/Square.cs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jw1912/Chess-Challenge/e65b3b821f4179980397e4e62fec9227b984b850/Chess-Challenge/src/API/Square.cs
--------------------------------------------------------------------------------
/Chess-Challenge/src/API/Timer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ChessChallenge.API
4 | {
5 | public sealed class Timer
6 | {
7 | ///
8 | /// Amount of time left on clock for current player (in milliseconds)
9 | ///
10 | public int MillisecondsRemaining => Math.Max(0, initialMillisRemaining - (int)sw.ElapsedMilliseconds);
11 | ///
12 | /// Amount of time elapsed since current player started thinking (in milliseconds)
13 | ///
14 | public int MillisecondsElapsedThisTurn => (int)sw.ElapsedMilliseconds;
15 |
16 | System.Diagnostics.Stopwatch sw;
17 | readonly int initialMillisRemaining;
18 |
19 | public Timer(int millisRemaining)
20 | {
21 | initialMillisRemaining = millisRemaining;
22 | sw = System.Diagnostics.Stopwatch.StartNew();
23 |
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Evil Bot/EvilBot.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.API;
2 | using System;
3 |
4 | namespace ChessChallenge.Example
5 | {
6 | // A simple bot that can spot mate in one, and always captures the most valuable piece it can.
7 | // Plays randomly otherwise.
8 | public class EvilBot : IChessBot
9 | {
10 | // Piece values: null, pawn, knight, bishop, rook, queen, king
11 | int[] pieceValues = { 0, 100, 300, 300, 500, 900, 10000 };
12 |
13 | public Move Think(Board board, Timer timer)
14 | {
15 | Move[] allMoves = board.GetLegalMoves();
16 |
17 | // Pick a random move to play if nothing better is found
18 | Random rng = new();
19 | Move moveToPlay = allMoves[rng.Next(allMoves.Length)];
20 | int highestValueCapture = 0;
21 |
22 | foreach (Move move in allMoves)
23 | {
24 | // Always play checkmate in one
25 | if (MoveIsCheckmate(board, move))
26 | {
27 | moveToPlay = move;
28 | break;
29 | }
30 |
31 | // Find highest value capture
32 | Piece capturedPiece = board.GetPiece(move.TargetSquare);
33 | int capturedPieceValue = pieceValues[(int)capturedPiece.PieceType];
34 |
35 | if (capturedPieceValue > highestValueCapture)
36 | {
37 | moveToPlay = move;
38 | highestValueCapture = capturedPieceValue;
39 | }
40 | }
41 |
42 | return moveToPlay;
43 | }
44 |
45 | // Test if this move gives checkmate
46 | bool MoveIsCheckmate(Board board, Move move)
47 | {
48 | board.MakeMove(move);
49 | bool isMate = board.IsInCheckmate();
50 | board.UndoMove(move);
51 | return isMate;
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Core/ChallengeController.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.Chess;
2 | using ChessChallenge.Example;
3 | using Raylib_cs;
4 | using System;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Runtime.ExceptionServices;
8 | using System.Text;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using static ChessChallenge.Application.Settings;
12 | using static ChessChallenge.Application.ConsoleHelper;
13 |
14 | namespace ChessChallenge.Application
15 | {
16 | public class ChallengeController
17 | {
18 | public enum PlayerType
19 | {
20 | Human,
21 | MyBot,
22 | EvilBot
23 | }
24 |
25 | // Game state
26 | Random rng;
27 | int gameID;
28 | bool isPlaying;
29 | Board board;
30 | public ChessPlayer PlayerWhite { get; private set; }
31 | public ChessPlayer PlayerBlack {get;private set;}
32 |
33 | float lastMoveMadeTime;
34 | bool isWaitingToPlayMove;
35 | Move moveToPlay;
36 | float playMoveTime;
37 | public bool HumanWasWhiteLastGame { get; private set; }
38 |
39 | // Bot match state
40 | readonly string[] botMatchStartFens;
41 | int botMatchGameIndex;
42 | public BotMatchStats BotStatsA { get; private set; }
43 | public BotMatchStats BotStatsB {get;private set;}
44 | bool botAPlaysWhite;
45 |
46 |
47 | // Bot task
48 | AutoResetEvent botTaskWaitHandle;
49 | bool hasBotTaskException;
50 | ExceptionDispatchInfo botExInfo;
51 |
52 | // Other
53 | readonly BoardUI boardUI;
54 | readonly MoveGenerator moveGenerator;
55 | readonly int tokenCount;
56 | readonly StringBuilder pgns;
57 |
58 | public ChallengeController()
59 | {
60 | Log($"Launching Chess-Challenge version {Settings.Version}");
61 | tokenCount = GetTokenCount();
62 | Warmer.Warm();
63 |
64 | rng = new Random();
65 | moveGenerator = new();
66 | boardUI = new BoardUI();
67 | board = new Board();
68 | pgns = new();
69 |
70 | BotStatsA = new BotMatchStats("IBot");
71 | BotStatsB = new BotMatchStats("IBot");
72 | botMatchStartFens = FileHelper.ReadResourceFile("Fens.txt").Split('\n').Where(fen => fen.Length > 0).ToArray();
73 | botTaskWaitHandle = new AutoResetEvent(false);
74 |
75 | StartNewGame(PlayerType.Human, PlayerType.MyBot);
76 | }
77 |
78 | public void StartNewGame(PlayerType whiteType, PlayerType blackType)
79 | {
80 | // End any ongoing game
81 | EndGame(GameResult.DrawByArbiter, log: false, autoStartNextBotMatch: false);
82 | gameID = rng.Next();
83 |
84 | // Stop prev task and create a new one
85 | if (RunBotsOnSeparateThread)
86 | {
87 | // Allow task to terminate
88 | botTaskWaitHandle.Set();
89 | // Create new task
90 | botTaskWaitHandle = new AutoResetEvent(false);
91 | Task.Factory.StartNew(BotThinkerThread, TaskCreationOptions.LongRunning);
92 | }
93 | // Board Setup
94 | board = new Board();
95 | bool isGameWithHuman = whiteType is PlayerType.Human || blackType is PlayerType.Human;
96 | int fenIndex = isGameWithHuman ? 0 : botMatchGameIndex / 2;
97 | board.LoadPosition(botMatchStartFens[fenIndex]);
98 |
99 | // Player Setup
100 | PlayerWhite = CreatePlayer(whiteType);
101 | PlayerBlack = CreatePlayer(blackType);
102 | PlayerWhite.SubscribeToMoveChosenEventIfHuman(OnMoveChosen);
103 | PlayerBlack.SubscribeToMoveChosenEventIfHuman(OnMoveChosen);
104 |
105 | // UI Setup
106 | boardUI.UpdatePosition(board);
107 | boardUI.ResetSquareColours();
108 | SetBoardPerspective();
109 |
110 | // Start
111 | isPlaying = true;
112 | NotifyTurnToMove();
113 | }
114 |
115 | void BotThinkerThread()
116 | {
117 | int threadID = gameID;
118 | //Console.WriteLine("Starting thread: " + threadID);
119 |
120 | while (true)
121 | {
122 | // Sleep thread until notified
123 | botTaskWaitHandle.WaitOne();
124 | // Get bot move
125 | if (threadID == gameID)
126 | {
127 | var move = GetBotMove();
128 |
129 | if (threadID == gameID)
130 | {
131 | OnMoveChosen(move);
132 | }
133 | }
134 | // Terminate if no longer playing this game
135 | if (threadID != gameID)
136 | {
137 | break;
138 | }
139 | }
140 | //Console.WriteLine("Exitting thread: " + threadID);
141 | }
142 |
143 | Move GetBotMove()
144 | {
145 | // Board b = new Board();
146 | // b.LoadPosition(FenUtility.CurrentFen(board));
147 | API.Board botBoard = new(new(board));
148 | try
149 | {
150 | API.Timer timer = new(PlayerToMove.TimeRemainingMs);
151 | API.Move move = PlayerToMove.Bot.Think(botBoard, timer);
152 | return new Move(move.RawValue);
153 | }
154 | catch (Exception e)
155 | {
156 | Log("An error occurred while bot was thinking.\n" + e.ToString(), true, ConsoleColor.Red);
157 | hasBotTaskException = true;
158 | botExInfo = ExceptionDispatchInfo.Capture(e);
159 | }
160 | return Move.NullMove;
161 | }
162 |
163 |
164 |
165 | void NotifyTurnToMove()
166 | {
167 | //playerToMove.NotifyTurnToMove(board);
168 | if (PlayerToMove.IsHuman)
169 | {
170 | PlayerToMove.Human.SetPosition(FenUtility.CurrentFen(board));
171 | PlayerToMove.Human.NotifyTurnToMove();
172 | }
173 | else
174 | {
175 | if (RunBotsOnSeparateThread)
176 | {
177 | botTaskWaitHandle.Set();
178 | }
179 | else
180 | {
181 | double startThinkTime = Raylib.GetTime();
182 | var move = GetBotMove();
183 | double thinkDuration = Raylib.GetTime() - startThinkTime;
184 | PlayerToMove.UpdateClock(thinkDuration);
185 | OnMoveChosen(move);
186 | }
187 | }
188 | }
189 |
190 | void SetBoardPerspective()
191 | {
192 | // Board perspective
193 | if (PlayerWhite.IsHuman || PlayerBlack.IsHuman)
194 | {
195 | boardUI.SetPerspective(PlayerWhite.IsHuman);
196 | HumanWasWhiteLastGame = PlayerWhite.IsHuman;
197 | }
198 | else if (PlayerWhite.Bot is MyBot && PlayerBlack.Bot is MyBot)
199 | {
200 | boardUI.SetPerspective(true);
201 | }
202 | else
203 | {
204 | boardUI.SetPerspective(PlayerWhite.Bot is MyBot);
205 | }
206 | }
207 |
208 | ChessPlayer CreatePlayer(PlayerType type)
209 | {
210 | return type switch
211 | {
212 | PlayerType.MyBot => new ChessPlayer(new MyBot(), type, GameDurationMilliseconds),
213 | PlayerType.EvilBot => new ChessPlayer(new EvilBot(), type, GameDurationMilliseconds),
214 | _ => new ChessPlayer(new HumanPlayer(boardUI), type)
215 | };
216 | }
217 |
218 | static int GetTokenCount()
219 | {
220 | string path = Path.Combine(Directory.GetCurrentDirectory(), "src", "My Bot", "MyBot.cs");
221 |
222 | using StreamReader reader = new(path);
223 | string txt = reader.ReadToEnd();
224 | return TokenCounter.CountTokens(txt);
225 | }
226 |
227 | void OnMoveChosen(Move chosenMove)
228 | {
229 | if (IsLegal(chosenMove))
230 | {
231 | if (PlayerToMove.IsBot)
232 | {
233 | moveToPlay = chosenMove;
234 | isWaitingToPlayMove = true;
235 | playMoveTime = lastMoveMadeTime + MinMoveDelay;
236 | }
237 | else
238 | {
239 | PlayMove(chosenMove);
240 | }
241 | }
242 | else
243 | {
244 | string moveName = MoveUtility.GetMoveNameUCI(chosenMove);
245 | string log = $"Illegal move: {moveName} in position: {FenUtility.CurrentFen(board)}";
246 | Log(log, true, ConsoleColor.Red);
247 | GameResult result = PlayerToMove == PlayerWhite ? GameResult.WhiteIllegalMove : GameResult.BlackIllegalMove;
248 | EndGame(result);
249 | }
250 | }
251 |
252 | void PlayMove(Move move)
253 | {
254 | if (isPlaying)
255 | {
256 | bool animate = PlayerToMove.IsBot;
257 | lastMoveMadeTime = (float)Raylib.GetTime();
258 |
259 | board.MakeMove(move, false);
260 | boardUI.UpdatePosition(board, move, animate);
261 |
262 | GameResult result = Arbiter.GetGameState(board);
263 | if (result == GameResult.InProgress)
264 | {
265 | NotifyTurnToMove();
266 | }
267 | else
268 | {
269 | EndGame(result);
270 | }
271 | }
272 | }
273 |
274 | void EndGame(GameResult result, bool log = true, bool autoStartNextBotMatch = true)
275 | {
276 | if (isPlaying)
277 | {
278 | isPlaying = false;
279 | isWaitingToPlayMove = false;
280 | gameID = -1;
281 |
282 | if (log)
283 | {
284 | Log("Game Over: " + result, false, ConsoleColor.Blue);
285 | }
286 |
287 | string pgn = PGNCreator.CreatePGN(board, result, GetPlayerName(PlayerWhite), GetPlayerName(PlayerBlack));
288 | pgns.AppendLine(pgn);
289 |
290 | // If 2 bots playing each other, start next game automatically.
291 | if (PlayerWhite.IsBot && PlayerBlack.IsBot)
292 | {
293 | UpdateBotMatchStats(result);
294 | botMatchGameIndex++;
295 | int numGamesToPlay = botMatchStartFens.Length * 2;
296 |
297 | if (botMatchGameIndex < numGamesToPlay && autoStartNextBotMatch)
298 | {
299 | botAPlaysWhite = !botAPlaysWhite;
300 | const int startNextGameDelayMs = 600;
301 | System.Timers.Timer autoNextTimer = new(startNextGameDelayMs);
302 | int originalGameID = gameID;
303 | autoNextTimer.Elapsed += (s, e) => AutoStartNextBotMatchGame(originalGameID, autoNextTimer);
304 | autoNextTimer.AutoReset = false;
305 | autoNextTimer.Start();
306 |
307 | }
308 | else if (autoStartNextBotMatch)
309 | {
310 | Log("Match finished", false, ConsoleColor.Blue);
311 | }
312 | }
313 | }
314 | }
315 |
316 | private void AutoStartNextBotMatchGame(int originalGameID, System.Timers.Timer timer)
317 | {
318 | if (originalGameID == gameID)
319 | {
320 | StartNewGame(PlayerBlack.PlayerType, PlayerWhite.PlayerType);
321 | }
322 | timer.Close();
323 | }
324 |
325 |
326 | void UpdateBotMatchStats(GameResult result)
327 | {
328 | UpdateStats(BotStatsA, botAPlaysWhite);
329 | UpdateStats(BotStatsB, !botAPlaysWhite);
330 |
331 | void UpdateStats(BotMatchStats stats, bool isWhiteStats)
332 | {
333 | // Draw
334 | if (Arbiter.IsDrawResult(result))
335 | {
336 | stats.NumDraws++;
337 | }
338 | // Win
339 | else if (Arbiter.IsWhiteWinsResult(result) == isWhiteStats)
340 | {
341 | stats.NumWins++;
342 | }
343 | // Loss
344 | else
345 | {
346 | stats.NumLosses++;
347 | stats.NumTimeouts += (result is GameResult.WhiteTimeout or GameResult.BlackTimeout) ? 1 : 0;
348 | stats.NumIllegalMoves += (result is GameResult.WhiteIllegalMove or GameResult.BlackIllegalMove) ? 1 : 0;
349 | }
350 | }
351 | }
352 |
353 | public void Update()
354 | {
355 | if (isPlaying)
356 | {
357 | PlayerWhite.Update();
358 | PlayerBlack.Update();
359 |
360 | PlayerToMove.UpdateClock(Raylib.GetFrameTime());
361 | if (PlayerToMove.TimeRemainingMs <= 0)
362 | {
363 | EndGame(PlayerToMove == PlayerWhite ? GameResult.WhiteTimeout : GameResult.BlackTimeout);
364 | }
365 | else
366 | {
367 | if (isWaitingToPlayMove && Raylib.GetTime() > playMoveTime)
368 | {
369 | isWaitingToPlayMove = false;
370 | PlayMove(moveToPlay);
371 | }
372 | }
373 | }
374 |
375 | if (hasBotTaskException)
376 | {
377 | hasBotTaskException = false;
378 | botExInfo.Throw();
379 | }
380 | }
381 |
382 | public void Draw()
383 | {
384 | boardUI.Draw();
385 | string nameW = GetPlayerName(PlayerWhite);
386 | string nameB = GetPlayerName(PlayerBlack);
387 | boardUI.DrawPlayerNames(nameW, nameB, PlayerWhite.TimeRemainingMs, PlayerBlack.TimeRemainingMs, isPlaying);
388 | }
389 | public void DrawOverlay()
390 | {
391 | BotBrainCapacityUI.Draw(tokenCount, MaxTokenCount);
392 | MenuUI.DrawButtons(this);
393 | MatchStatsUI.DrawMatchStats(this);
394 | }
395 |
396 | static string GetPlayerName(ChessPlayer player) => GetPlayerName(player.PlayerType);
397 | static string GetPlayerName(PlayerType type) => type.ToString();
398 |
399 | public void StartNewBotMatch(PlayerType botTypeA, PlayerType botTypeB)
400 | {
401 | EndGame(GameResult.DrawByArbiter, log: false, autoStartNextBotMatch: false);
402 | botMatchGameIndex = 0;
403 | string nameA = GetPlayerName(botTypeA);
404 | string nameB = GetPlayerName(botTypeB);
405 | if (nameA == nameB)
406 | {
407 | nameA += " (A)";
408 | nameB += " (B)";
409 | }
410 | BotStatsA = new BotMatchStats(nameA);
411 | BotStatsB = new BotMatchStats(nameB);
412 | botAPlaysWhite = true;
413 | Log($"Starting new match: {nameA} vs {nameB}", false, ConsoleColor.Blue);
414 | StartNewGame(botTypeA, botTypeB);
415 | }
416 |
417 |
418 | ChessPlayer PlayerToMove => board.IsWhiteToMove ? PlayerWhite : PlayerBlack;
419 | public int TotalGameCount => botMatchStartFens.Length * 2;
420 | public int CurrGameNumber => Math.Min(TotalGameCount, botMatchGameIndex + 1);
421 | public string AllPGNs => pgns.ToString();
422 |
423 |
424 | bool IsLegal(Move givenMove)
425 | {
426 | var moves = moveGenerator.GenerateMoves(board);
427 | foreach (var legalMove in moves)
428 | {
429 | if (givenMove.Value == legalMove.Value)
430 | {
431 | return true;
432 | }
433 | }
434 |
435 | return false;
436 | }
437 |
438 | public class BotMatchStats
439 | {
440 | public string BotName;
441 | public int NumWins;
442 | public int NumLosses;
443 | public int NumDraws;
444 | public int NumTimeouts;
445 | public int NumIllegalMoves;
446 |
447 | public BotMatchStats(string name) => BotName = name;
448 | }
449 |
450 | public void Release()
451 | {
452 | boardUI.Release();
453 | }
454 | }
455 | }
456 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Core/Program.cs:
--------------------------------------------------------------------------------
1 | using Raylib_cs;
2 | using System.IO;
3 | using System.Numerics;
4 | using System.Runtime.InteropServices;
5 |
6 | namespace ChessChallenge.Application
7 | {
8 | static class Program
9 | {
10 | const bool hideRaylibLogs = true;
11 | static Camera2D cam;
12 |
13 | public static void Main()
14 | {
15 | Vector2 loadedWindowSize = GetSavedWindowSize();
16 | int screenWidth = (int)loadedWindowSize.X;
17 | int screenHeight = (int)loadedWindowSize.Y;
18 |
19 | if (hideRaylibLogs)
20 | {
21 | unsafe
22 | {
23 | Raylib.SetTraceLogCallback(&LogCustom);
24 | }
25 | }
26 |
27 | Raylib.InitWindow(screenWidth, screenHeight, "Chess Coding Challenge");
28 | Raylib.SetTargetFPS(60);
29 |
30 | UpdateCamera(screenWidth, screenHeight);
31 |
32 | ChallengeController controller = new();
33 |
34 | while (!Raylib.WindowShouldClose())
35 | {
36 | Raylib.BeginDrawing();
37 | Raylib.ClearBackground(new Color(22, 22, 22, 255));
38 | Raylib.BeginMode2D(cam);
39 |
40 | controller.Update();
41 | controller.Draw();
42 |
43 | Raylib.EndMode2D();
44 |
45 | controller.DrawOverlay();
46 |
47 | Raylib.EndDrawing();
48 | }
49 |
50 | Raylib.CloseWindow();
51 |
52 | controller.Release();
53 | UIHelper.Release();
54 | }
55 |
56 | public static void SetWindowSize(Vector2 size)
57 | {
58 | Raylib.SetWindowSize((int)size.X, (int)size.Y);
59 | UpdateCamera((int)size.X, (int)size.Y);
60 | SaveWindowSize();
61 | }
62 |
63 | public static Vector2 ScreenToWorldPos(Vector2 screenPos) => Raylib.GetScreenToWorld2D(screenPos, cam);
64 |
65 | static void UpdateCamera(int screenWidth, int screenHeight)
66 | {
67 | cam = new Camera2D();
68 | cam.target = new Vector2(0, 15);
69 | cam.offset = new Vector2(screenWidth / 2f, screenHeight / 2f);
70 | cam.zoom = screenWidth / 1280f * 0.7f;
71 | }
72 |
73 |
74 | [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
75 | private static unsafe void LogCustom(int logLevel, sbyte* text, sbyte* args)
76 | {
77 | }
78 |
79 | static Vector2 GetSavedWindowSize()
80 | {
81 | if (File.Exists(FileHelper.PrefsFilePath))
82 | {
83 | string prefs = File.ReadAllText(FileHelper.PrefsFilePath);
84 | if (!string.IsNullOrEmpty(prefs))
85 | {
86 | if (prefs[0] == '0')
87 | {
88 | return Settings.ScreenSizeSmall;
89 | }
90 | else if (prefs[0] == '1')
91 | {
92 | return Settings.ScreenSizeBig;
93 | }
94 | }
95 | }
96 | return Settings.ScreenSizeSmall;
97 | }
98 |
99 | static void SaveWindowSize()
100 | {
101 | Directory.CreateDirectory(FileHelper.AppDataPath);
102 | bool isBigWindow = Raylib.GetScreenWidth() > Settings.ScreenSizeSmall.X;
103 | File.WriteAllText(FileHelper.PrefsFilePath, isBigWindow ? "1" : "0");
104 | }
105 |
106 |
107 |
108 | }
109 |
110 |
111 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Core/Settings.cs:
--------------------------------------------------------------------------------
1 | using System.Numerics;
2 |
3 | namespace ChessChallenge.Application
4 | {
5 | public static class Settings
6 | {
7 | public const string Version = "1.14";
8 |
9 | // Game settings
10 | public const int GameDurationMilliseconds = 60 * 1000;
11 | public const float MinMoveDelay = 0;
12 | public static readonly bool RunBotsOnSeparateThread = true;
13 |
14 | // Display settings
15 | public const bool DisplayBoardCoordinates = true;
16 | public static readonly Vector2 ScreenSizeSmall = new(1280, 720);
17 | public static readonly Vector2 ScreenSizeBig = new(1920, 1080);
18 |
19 | // Other settings
20 | public const int MaxTokenCount = 1024;
21 | public const LogType MessagesToLog = LogType.All;
22 |
23 | public enum LogType
24 | {
25 | None,
26 | ErrorOnly,
27 | All
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Helpers/API Helpers/MoveHelper.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.Chess;
2 | using System;
3 | using ChessChallenge.API;
4 |
5 | namespace ChessChallenge.Application.APIHelpers
6 | {
7 |
8 | public class MoveHelper
9 | {
10 | public static (Chess.Move move, PieceType pieceType, PieceType captureType) CreateMoveFromName(string moveNameUCI, API.Board board)
11 | {
12 | int indexStart = BoardHelper.SquareIndexFromName(moveNameUCI[0] + "" + moveNameUCI[1]);
13 | int indexTarget = BoardHelper.SquareIndexFromName(moveNameUCI[2] + "" + moveNameUCI[3]);
14 | char promoteChar = moveNameUCI.Length > 3 ? moveNameUCI[^1] : ' ';
15 |
16 | PieceType promotePieceType = promoteChar switch
17 | {
18 | 'q' => PieceType.Queen,
19 | 'r' => PieceType.Rook,
20 | 'n' => PieceType.Knight,
21 | 'b' => PieceType.Bishop,
22 | _ => PieceType.None
23 | };
24 |
25 | Square startSquare = new Square(indexStart);
26 | Square targetSquare = new Square(indexTarget);
27 |
28 |
29 | PieceType movedPieceType = board.GetPiece(startSquare).PieceType;
30 | PieceType capturedPieceType = board.GetPiece(targetSquare).PieceType;
31 |
32 | // Figure out move flag
33 | int flag = Chess.Move.NoFlag;
34 |
35 | if (movedPieceType == PieceType.Pawn)
36 | {
37 | if (targetSquare.Rank is 7 or 0)
38 | {
39 | flag = promotePieceType switch
40 | {
41 | PieceType.Queen => Chess.Move.PromoteToQueenFlag,
42 | PieceType.Rook => Chess.Move.PromoteToRookFlag,
43 | PieceType.Knight => Chess.Move.PromoteToKnightFlag,
44 | PieceType.Bishop => Chess.Move.PromoteToBishopFlag,
45 | _ => 0
46 | };
47 | }
48 | else
49 | {
50 | if (Math.Abs(targetSquare.Rank - startSquare.Rank) == 2)
51 | {
52 | flag = Chess.Move.PawnTwoUpFlag;
53 | }
54 | // En-passant
55 | else if (startSquare.File != targetSquare.File && board.GetPiece(targetSquare).IsNull)
56 | {
57 | flag = Chess.Move.EnPassantCaptureFlag;
58 | }
59 | }
60 | }
61 | else if (movedPieceType == PieceType.King)
62 | {
63 | if (Math.Abs(startSquare.File - targetSquare.File) > 1)
64 | {
65 | flag = Chess.Move.CastleFlag;
66 | }
67 | }
68 |
69 | Chess.Move coreMove = new Chess.Move(startSquare.Index, targetSquare.Index, flag);
70 | return (coreMove, movedPieceType, capturedPieceType);
71 | }
72 |
73 |
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Helpers/ConsoleHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using static ChessChallenge.Application.Settings;
3 |
4 | namespace ChessChallenge.Application
5 | {
6 | public static class ConsoleHelper
7 | {
8 | public static void Log(string msg, bool isError = false, ConsoleColor col = ConsoleColor.White)
9 | {
10 | bool log = MessagesToLog == LogType.All || (isError && MessagesToLog == LogType.ErrorOnly);
11 |
12 | if (log)
13 | {
14 | Console.ForegroundColor = col;
15 | Console.WriteLine(msg);
16 | Console.ResetColor();
17 | }
18 | }
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Helpers/FileHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.IO;
3 | using System.Runtime.InteropServices;
4 | using System;
5 |
6 | namespace ChessChallenge.Application
7 | {
8 | public static class FileHelper
9 | {
10 |
11 | public static string AppDataPath
12 | {
13 | get
14 | {
15 | string dir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
16 | return Path.Combine(dir, "ChessCodingChallenge");
17 | }
18 | }
19 |
20 | public static string SavedGamesPath => Path.Combine(AppDataPath, "Games");
21 | public static string PrefsFilePath => Path.Combine(AppDataPath, "prefs.txt");
22 |
23 | public static string GetUniqueFileName(string path, string fileName, string fileExtension)
24 | {
25 | if (fileExtension[0] != '.')
26 | {
27 | fileExtension = "." + fileExtension;
28 | }
29 |
30 | string uniqueName = fileName;
31 | int index = 0;
32 |
33 | while (File.Exists(Path.Combine(path, uniqueName + fileExtension)))
34 | {
35 | index++;
36 | uniqueName = fileName + index;
37 | }
38 | return uniqueName + fileExtension;
39 | }
40 |
41 | public static string GetResourcePath(params string[] localPath)
42 | {
43 | return Path.Combine(Directory.GetCurrentDirectory(), "resources", Path.Combine(localPath));
44 | }
45 |
46 | public static string ReadResourceFile(string localPath)
47 | {
48 | return File.ReadAllText(GetResourcePath(localPath));
49 | }
50 |
51 | // Thanks to https://github.com/dotnet/runtime/issues/17938
52 | public static void OpenUrl(string url)
53 | {
54 | try
55 | {
56 | Process.Start(url);
57 | }
58 | catch
59 | {
60 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
61 | {
62 | url = url.Replace("&", "^&");
63 | Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
64 | }
65 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
66 | {
67 | Process.Start("xdg-open", url);
68 | }
69 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
70 | {
71 | Process.Start("open", url);
72 | }
73 | else
74 | {
75 | throw;
76 | }
77 | }
78 | }
79 |
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Helpers/Tester.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.API;
2 | using ChessChallenge.Chess;
3 | using System;
4 |
5 | namespace ChessChallenge.Application
6 | {
7 | public static class Tester
8 | {
9 | static MoveGenerator moveGen;
10 | static API.Board boardAPI;
11 |
12 | static bool anyFailed;
13 |
14 | public static void Run(bool runPerft)
15 | {
16 | anyFailed = false;
17 |
18 | MoveGenTest();
19 | PieceListTest();
20 | DrawTest();
21 | CheckTest();
22 | MiscTest();
23 | TestBitboards();
24 | TestMoveCreate();
25 |
26 | if (runPerft)
27 | {
28 | RunPerft(true);
29 | RunPerft(false);
30 | }
31 |
32 | if (anyFailed)
33 | {
34 | WriteWithCol("TEST FAILED");
35 | }
36 | else
37 | {
38 | WriteWithCol("ALL TESTS PASSED", ConsoleColor.Green);
39 | }
40 |
41 | }
42 |
43 | public static void RunPerft(bool useStackalloc = true)
44 | {
45 | Warmer.Warm();
46 | int[] depths = { 5, 5, 6, 5, 5, 4, 5, 4, 6, 6, 6, 7, 4, 5, 6, 5, 6, 6, 10, 7, 6, 5, 4, 5, 4, 6, 6, 9, 4, 5 };
47 | ulong[] expectedNodes = { 4865609, 5617302, 11030083, 15587335, 89941194, 3894594, 193690690, 497787, 1134888, 1440467, 661072, 15594314, 1274206, 58773923, 3821001, 1004658, 217342, 92683, 5966690, 567584, 3114998, 42761834, 3050662, 10574719, 6871272, 71179139, 28859283, 7618365, 28181, 6323457 };
48 | string[] fens = { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", "2b1b3/1r1P4/3K3p/1p6/2p5/6k1/1P3p2/4B3 w - - 0 42", "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -", "r3k2r/pp3pp1/PN1pr1p1/4p1P1/4P3/3P4/P1P2PP1/R3K2R w KQkq - 4 4", "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10", "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -", "r3k1nr/p2pp1pp/b1n1P1P1/1BK1Pp1q/8/8/2PP1PPP/6N1 w kq - 0 1", "3k4/3p4/8/K1P4r/8/8/8/8 b - - 0 1", "8/8/1k6/2b5/2pP4/8/5K2/8 b - d3 0 1", "5k2/8/8/8/8/8/8/4K2R w K - 0 1", "3k4/8/8/8/8/8/8/R3K3 w Q - 0 1", "r3k2r/1b4bq/8/8/8/8/7B/R3K2R w KQkq - 0 1", "r3k2r/8/3Q4/8/8/5q2/8/R3K2R b KQkq - 0 1", "2K2r2/4P3/8/8/8/8/8/3k4 w - - 0 1", "8/8/1P2K3/8/2n5/1q6/8/5k2 b - - 0 1", "4k3/1P6/8/8/8/8/K7/8 w - - 0 1", "8/P1k5/K7/8/8/8/8/8 w - - 0 1", "K1k5/8/P7/8/8/8/8/8 w - - 0 1", "8/k1P5/8/1K6/8/8/8/8 w - - 0 1", "8/8/2k5/5q2/5n2/8/5K2/8 b - - 0 1", "r1bq2r1/1pppkppp/1b3n2/pP1PP3/2n5/2P5/P3QPPP/RNB1K2R w KQ a6 0 12", "r3k2r/pppqbppp/3p1n1B/1N2p3/1nB1P3/3P3b/PPPQNPPP/R3K2R w KQkq - 11 10", "4k2r/1pp1n2p/6N1/1K1P2r1/4P3/P5P1/1Pp4P/R7 w k - 0 6", "1Bb3BN/R2Pk2r/1Q5B/4q2R/2bN4/4Q1BK/1p6/1bq1R1rb w - - 0 1", "n1n5/PPPk4/8/8/8/8/4Kppp/5N1N b - - 0 1", "8/PPPk4/8/8/8/8/4Kppp/8 b - - 0 1", "8/2k1p3/3pP3/3P2K1/8/8/8/8 w - - 0 1", "3r4/2p1p3/8/1P1P1P2/3K4/5k2/8/8 b - - 0 1", "8/1p4p1/8/q1PK1P1r/3p1k2/8/4P3/4Q3 b - - 0 1" };
49 | Console.WriteLine($"Running perft (useStackalloc={useStackalloc})");
50 |
51 | var board = new Chess.Board();
52 | long timeTotal = 0;
53 |
54 | for (int i = 0; i < fens.Length; i++)
55 | {
56 | board.LoadPosition(fens[i]);
57 | boardAPI = new(board);
58 | System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
59 |
60 | ulong result;
61 | if (useStackalloc)
62 | {
63 | result = SearchStackalloc(depths[i]);
64 | }
65 | else
66 | {
67 | result = Search(depths[i]);
68 | }
69 |
70 | if (result != expectedNodes[i])
71 | {
72 | Console.WriteLine("Error");
73 | anyFailed = true;
74 | break;
75 | }
76 | else
77 | {
78 | sw.Stop();
79 | timeTotal += sw.ElapsedMilliseconds;
80 | Console.WriteLine(i + " successful: " + sw.ElapsedMilliseconds + " ms");
81 | }
82 |
83 | }
84 | Console.WriteLine("Time Total: " + timeTotal + " ms.");
85 |
86 | }
87 |
88 | static void TestMoveCreate()
89 | {
90 | Console.WriteLine("Testing move create");
91 | var board = new Chess.Board();
92 | board.LoadPosition("2rqk2r/1p3p1p/p2p1n2/2PPn3/8/3B1QP1/PR1K1P1p/2B1R3 b k - 1 27");
93 | boardAPI = new(board);
94 |
95 | var move = new API.Move("b7b5", boardAPI);
96 | boardAPI.MakeMove(move);
97 | move = new API.Move("c5b6", boardAPI);
98 | Assert(move.IsEnPassant, "En passant wrong");
99 | move = new API.Move("h2h1q", boardAPI);
100 | Assert(move.IsPromotion && move.PromotionPieceType == PieceType.Queen, "Promotion wrong");
101 | move = new API.Move("e8g8", boardAPI);
102 | Assert(move.IsCastles && move.MovePieceType == PieceType.King, "Castles wrong");
103 | }
104 |
105 | static void TestBitboards()
106 | {
107 |
108 | Console.WriteLine("Testing Bitboards");
109 | var board = new Chess.Board();
110 | board.LoadPosition("r2q2k1/pp2rppp/3p1n2/1R1Pn3/8/2PB1Q1P/P4PP1/2B2RK1 w - - 7 16");
111 | boardAPI = new(board);
112 |
113 | ulong rookTest = BitboardHelper.GetSliderAttacks(PieceType.Rook, new Square("b5"), boardAPI);
114 | Assert(BitboardHelper.GetNumberOfSetBits(rookTest) == 9, "Bitboard error");
115 | ulong queenTest = BitboardHelper.GetSliderAttacks(PieceType.Queen, new Square("f3"), boardAPI);
116 | Assert(BitboardHelper.GetNumberOfSetBits(queenTest) == 15, "Bitboard error");
117 | ulong bishopTest = BitboardHelper.GetSliderAttacks(PieceType.Bishop, new Square("d3"), boardAPI);
118 | Assert(BitboardHelper.GetNumberOfSetBits(bishopTest) == 10, "Bitboard error");
119 | ulong pawnTest = BitboardHelper.GetPawnAttacks(new Square("c3"), true);
120 | Assert(BitboardHelper.SquareIsSet(pawnTest, new Square("b4")), "Pawn bitboard error");
121 | Assert(BitboardHelper.SquareIsSet(pawnTest, new Square("d4")), "Pawn bitboard error");
122 | ulong knightTest = BitboardHelper.GetKnightAttacks(new Square("a1"));
123 | Assert(BitboardHelper.GetNumberOfSetBits(knightTest) == 2, "Knight bb wrong");
124 | ulong king = BitboardHelper.GetKingAttacks(new Square("a1"));
125 | Assert(BitboardHelper.GetNumberOfSetBits(king) == 3, "King bb wrong");
126 |
127 | Assert(boardAPI.SquareIsAttackedByOpponent(new Square("a6")), "Square attacked wrong");
128 | Assert(boardAPI.SquareIsAttackedByOpponent(new Square("f3")), "Square attacked wrong");
129 | Assert(!boardAPI.SquareIsAttackedByOpponent(new Square("c3")), "Square attacked wrong");
130 | Assert(!boardAPI.SquareIsAttackedByOpponent(new Square("h4")), "Square attacked wrong");
131 | boardAPI.MakeMove(new API.Move("b5b7", boardAPI));
132 | Assert(boardAPI.SquareIsAttackedByOpponent(new Square("e7")), "Square attacked wrong");
133 | Assert(boardAPI.SquareIsAttackedByOpponent(new Square("b8")), "Square attacked wrong");
134 | Assert(!boardAPI.SquareIsAttackedByOpponent(new Square("a5")), "Square attacked wrong");
135 | Assert(!boardAPI.SquareIsAttackedByOpponent(new Square("e8")), "Square attacked wrong");
136 | }
137 |
138 | static void CheckTest()
139 | {
140 | Console.WriteLine("Testing Checks");
141 | var board = new Chess.Board();
142 | board.LoadPosition("r2q1rk1/pp3ppp/3p1n2/3Pn3/8/2PB1Q1P/P4PP1/R1B2RK1 w - - 3 14");
143 | boardAPI = new(board);
144 |
145 | Assert(!boardAPI.IsInCheck(), "Check wrong");
146 | API.Move move = new API.Move("d3h7", boardAPI);
147 | boardAPI.MakeMove(move);
148 | Assert(boardAPI.IsInCheck(), "Check wrong");
149 | boardAPI.UndoMove(move);
150 | Assert(!boardAPI.IsInCheck(), "Check wrong");
151 |
152 | boardAPI.MakeMove(new API.Move("f3h5", boardAPI));
153 | boardAPI.MakeMove(new API.Move("f6d7", boardAPI));
154 | Assert(!boardAPI.IsInCheckmate(), "Checkmate wrong");
155 | boardAPI.MakeMove(new API.Move("h5h7", boardAPI));
156 | Assert(boardAPI.IsInCheckmate(), "Checkmate wrong");
157 |
158 | }
159 | static void PieceListTest()
160 | {
161 | Console.WriteLine("Piece Lists Tests");
162 | var board = new Chess.Board();
163 | board.LoadPosition("1q3rk1/P5p1/4p2p/2ppP1N1/5Qb1/1PP5/7P/2R2RK1 w - - 0 28");
164 | boardAPI = new(board);
165 |
166 | API.PieceList[] pieceLists = boardAPI.GetAllPieceLists();
167 | int[] counts = { 5, 1, 0, 2, 1, 1, 5, 0, 1, 1, 1, 1 };
168 | for (int i = 0; i < pieceLists.Length; i++)
169 | {
170 | string msg = $"Wrong piece count: {pieceLists[i].Count} Type: {pieceLists[i].TypeOfPieceInList}";
171 | Assert(pieceLists[i].Count == counts[i], msg);
172 | }
173 |
174 | Assert(boardAPI.GetKingSquare(true) == boardAPI.GetPieceList(PieceType.King, true)[0].Square, "King square wrong");
175 | Assert(boardAPI.GetKingSquare(true) == new Square(6, 0), "King square wrong");
176 | Assert(boardAPI.GetKingSquare(false) == boardAPI.GetPieceList(PieceType.King, false)[0].Square, "King square wrong");
177 | Assert(boardAPI.GetKingSquare(false) == new Square(6, 7), "King square wrong");
178 | Assert(boardAPI.GetPiece(new Square(4, 5)).IsPawn, "Wrong piece");
179 | Assert(!boardAPI.GetPiece(new Square(4, 5)).IsWhite, "Wrong colour");
180 |
181 | API.Move testMove = new("g5e6", boardAPI);
182 | boardAPI.MakeMove(testMove);
183 | Assert(boardAPI.GetPiece(new Square("g5")).IsNull, "Wrong piece");
184 | Assert(boardAPI.GetPiece(new Square("e6")).IsKnight, "Wrong piece");
185 | Assert(boardAPI.GetPiece(new Square("e6")).IsWhite, "Wrong piece col");
186 | boardAPI.UndoMove(testMove);
187 | Assert(boardAPI.GetPiece(new Square("e6")).IsPawn, "Wrong piece");
188 | Assert(!boardAPI.GetPiece(new Square("e6")).IsWhite, "Wrong piece col");
189 | Assert(boardAPI.GetPiece(new Square("g5")).IsKnight, "Wrong piece");
190 |
191 | }
192 |
193 | static void DrawTest()
194 | {
195 | Console.WriteLine("Draw test");
196 |
197 | // Repetition test
198 | var board = new Chess.Board();
199 | board.LoadPosition("r1r3k1/p1q5/3p2pQ/1p1Pp1N1/2B5/1PP2P2/K1b3P1/7R b - - 2 24");
200 | boardAPI = new(board);
201 |
202 | Assert(!boardAPI.IsDraw(), "Draw wrong");
203 | boardAPI.MakeMove(new API.Move("c7g7", boardAPI));
204 | Assert(!boardAPI.IsDraw(), "Draw wrong");
205 | boardAPI.MakeMove(new API.Move("h6h4", boardAPI));
206 | Assert(!boardAPI.IsDraw(), "Draw wrong");
207 | boardAPI.MakeMove(new API.Move("g7c7", boardAPI));
208 | Assert(!boardAPI.IsDraw(), "Draw wrong");
209 | boardAPI.MakeMove(new API.Move("h4h6", boardAPI));
210 | Assert(boardAPI.IsDraw(), "Draw wrong");
211 | boardAPI.UndoMove(new API.Move("h4h6", boardAPI));
212 | Assert(!boardAPI.IsDraw(), "Draw wrong");
213 |
214 | // Stalemate test
215 | board = new Chess.Board();
216 | board.LoadPosition("7K/8/6k1/5q2/8/8/8/8 b - - 0 1");
217 | boardAPI = new(board);
218 | Assert(!boardAPI.IsDraw(), "Draw wrong");
219 | boardAPI.MakeMove(new API.Move("f5f7", boardAPI));
220 | Assert(boardAPI.IsDraw(), "Draw wrong");
221 |
222 | // Insufficient material
223 | board = new Chess.Board();
224 | board.LoadPosition("7K/3N4/6k1/2n5/8/8/8/8 b - - 0 1");
225 | boardAPI = new(board);
226 | Assert(!boardAPI.IsDraw(), "Draw wrong");
227 | boardAPI.MakeMove(new API.Move("c5d7", boardAPI));
228 | Assert(boardAPI.IsDraw(), "Draw wrong");
229 |
230 | string[] notInsufficient =
231 | {
232 | "3k4/4b3/8/8/8/3B4/1K6/8 w - - 0 1",
233 | "3k4/3b4/8/8/8/4B3/1K6/8 w - - 0 1",
234 | "3k4/3b4/8/8/8/2N5/1K6/8 w - - 0 1",
235 | "3k4/3n4/8/8/8/2N5/1K6/8 w - - 0 1",
236 | "8/4k3/8/8/8/2NN4/1K6/8 w - - 0 1",
237 | "8/4k3/8/8/8/8/PK6/8 w - - 0 1",
238 | "8/4k3/8/8/8/8/1K1R4/8 w - - 0 1",
239 | "8/4k3/8/8/8/8/1KQ5/8 w - - 0 1"
240 | };
241 |
242 | string[] insufficient =
243 | {
244 | "3k4/8/8/8/8/8/1K6/8 w - - 0 1",
245 | "3k4/8/8/8/8/2B5/1K6/8 w - - 0 1",
246 | "3k4/8/8/8/8/8/1KN5/8 w - - 0 1",
247 | "3k4/2b5/8/8/8/2B5/1K6/8 w - - 0 1",
248 | "3k4/3b4/8/8/8/3B4/1K6/8 w - - 0 1"
249 | };
250 |
251 | foreach (string drawPos in insufficient)
252 | {
253 | boardAPI = API.Board.CreateBoardFromFEN(drawPos);
254 | Assert(boardAPI.IsDraw(), "Draw wrong, position is insufficient mat");
255 | boardAPI = API.Board.CreateBoardFromFEN(FenUtility.FlipFen(drawPos));
256 | Assert(boardAPI.IsDraw(), "Draw wrong, position is insufficient mat");
257 | }
258 |
259 | foreach (string winnablePos in notInsufficient)
260 | {
261 | boardAPI = API.Board.CreateBoardFromFEN(winnablePos);
262 | Assert(!boardAPI.IsDraw(), "Draw wrong, position is winnable");
263 | boardAPI = API.Board.CreateBoardFromFEN(FenUtility.FlipFen(winnablePos));
264 | Assert(!boardAPI.IsDraw(), "Draw wrong, position is winnable");
265 | }
266 | }
267 |
268 | static void MiscTest()
269 | {
270 | Console.WriteLine("Running Misc Tests");
271 | var board = new Chess.Board();
272 | board.LoadPosition("1q3rk1/P5p1/4p2p/2ppP1N1/5Qb1/1PP5/7P/2R2RK1 w - - 0 28");
273 | boardAPI = new(board);
274 | Assert(boardAPI.IsWhiteToMove, "Colour to move wrong");
275 |
276 | //var moves = boardAPI.GetLegalMoves();
277 | var captures = boardAPI.GetLegalMoves(true);
278 | Assert(captures.Length == 4, "Captures wrong");
279 | int numTested = 0;
280 | foreach (var c in captures)
281 | {
282 | if (c.TargetSquare.Index == 57)
283 | {
284 | Assert(c.StartSquare == new Square(0, 6), "Start square wrong");
285 | Assert(c.CapturePieceType == PieceType.Queen, "Capture type wrong");
286 | Assert(c.MovePieceType == PieceType.Pawn, "Move piece type wrong");
287 | Assert(c.PromotionPieceType == PieceType.Queen, "Promote type wrong");
288 | numTested++;
289 | }
290 | if (c.TargetSquare.Index == 44)
291 | {
292 | Assert(c.StartSquare == new Square(6, 4), "Start square wrong");
293 | Assert(c.CapturePieceType == PieceType.Pawn, "Capture type wrong");
294 | Assert(c.MovePieceType == PieceType.Knight, "Move piece type wrong");
295 | numTested++;
296 | }
297 | if (c.TargetSquare.Index == 61)
298 | {
299 | Assert(c.CapturePieceType == PieceType.Rook, "Capture type wrong");
300 | Assert(c.MovePieceType == PieceType.Queen, "Move piece type wrong");
301 | numTested++;
302 | }
303 | if (c.TargetSquare.Index == 30)
304 | {
305 | Assert(c.CapturePieceType == PieceType.Bishop, "Capture type wrong");
306 | Assert(c.MovePieceType == PieceType.Queen, "Move piece type wrong");
307 | numTested++;
308 | }
309 | }
310 | Assert(numTested == 4, "Target square wrong");
311 | }
312 |
313 | static void MoveGenTest()
314 | {
315 | Console.WriteLine("Running move gen tests");
316 | Chess.Board board = new();
317 | moveGen = new();
318 |
319 | string[] testFens =
320 | {
321 | "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -",
322 | "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -",
323 | "r1bq2r1/1pppkppp/1b3n2/pP1PP3/2n5/2P5/P3QPPP/RNB1K2R w KQ a6 0 12",
324 | "2b1b3/1r1P4/3K3p/1p6/2p5/6k1/1P3p2/4B3 w - - 0 42"
325 | };
326 |
327 | int[] testDepths = { 3, 4, 4, 5 };
328 | ulong[] testResults = { 2812, 4085603, 1280017, 5617302 };
329 |
330 |
331 | for (int i = 0; i < testFens.Length; i++)
332 | {
333 | board.LoadPosition(testFens[i]);
334 | boardAPI = new API.Board(board);
335 | ulong result = Search(testDepths[i]);
336 | Assert(result == testResults[i], "TEST FAILED");
337 | }
338 |
339 | }
340 |
341 |
342 | static ulong Search(int depth)
343 | {
344 | var moves = boardAPI.GetLegalMoves();
345 |
346 | if (depth == 1)
347 | {
348 | return (ulong)moves.Length;
349 | }
350 | ulong numLocalNodes = 0;
351 | for (int i = 0; i < moves.Length; i++)
352 | {
353 |
354 | boardAPI.MakeMove(moves[i]);
355 |
356 | ulong numNodesFromThisPosition = Search(depth - 1);
357 | numLocalNodes += numNodesFromThisPosition;
358 |
359 | boardAPI.UndoMove(moves[i]);
360 |
361 | }
362 | return numLocalNodes;
363 | }
364 |
365 | static ulong SearchStackalloc(int depth)
366 | {
367 | Span moves = stackalloc API.Move[128];
368 | boardAPI.GetLegalMovesNonAlloc(ref moves);
369 |
370 | if (depth == 1)
371 | {
372 | return (ulong)moves.Length;
373 | }
374 | ulong numLocalNodes = 0;
375 | for (int i = 0; i < moves.Length; i++)
376 | {
377 |
378 | boardAPI.MakeMove(moves[i]);
379 |
380 | ulong numNodesFromThisPosition = SearchStackalloc(depth - 1);
381 | numLocalNodes += numNodesFromThisPosition;
382 |
383 | boardAPI.UndoMove(moves[i]);
384 |
385 | }
386 | return numLocalNodes;
387 | }
388 |
389 | static void Assert(bool condition, string msg)
390 | {
391 | if (!condition)
392 | {
393 | WriteWithCol(msg);
394 | anyFailed = true;
395 | }
396 | }
397 |
398 |
399 | static void WriteWithCol(string msg, ConsoleColor col = ConsoleColor.Red)
400 | {
401 | Console.ForegroundColor = col;
402 | Console.WriteLine(msg);
403 | Console.ResetColor();
404 | }
405 |
406 | }
407 | }
408 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Helpers/Token Counter/Microsoft.CodeAnalysis.CSharp.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jw1912/Chess-Challenge/e65b3b821f4179980397e4e62fec9227b984b850/Chess-Challenge/src/Framework/Application/Helpers/Token Counter/Microsoft.CodeAnalysis.CSharp.dll
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Helpers/Token Counter/Microsoft.CodeAnalysis.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jw1912/Chess-Challenge/e65b3b821f4179980397e4e62fec9227b984b850/Chess-Challenge/src/Framework/Application/Helpers/Token Counter/Microsoft.CodeAnalysis.dll
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Helpers/Token Counter/TokenCounter.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using System.Collections.Generic;
4 |
5 | namespace ChessChallenge.Application
6 | {
7 | public static class TokenCounter
8 | {
9 |
10 | static readonly HashSet tokensToIgnore = new(new SyntaxKind[]
11 | {
12 | SyntaxKind.PrivateKeyword,
13 | SyntaxKind.PublicKeyword,
14 | SyntaxKind.SemicolonToken,
15 | SyntaxKind.CommaToken,
16 | SyntaxKind.ReadOnlyKeyword,
17 | // only count open brace since I want to count the pair as a single token
18 | SyntaxKind.CloseBraceToken,
19 | SyntaxKind.CloseBracketToken,
20 | SyntaxKind.CloseParenToken
21 | });
22 |
23 | public static int CountTokens(string code)
24 | {
25 | SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
26 | SyntaxNode root = tree.GetRoot();
27 | return CountTokens(root);
28 | }
29 |
30 | static int CountTokens(SyntaxNodeOrToken syntaxNode)
31 | {
32 | SyntaxKind kind = syntaxNode.Kind();
33 | int numTokensInChildren = 0;
34 |
35 |
36 | foreach (var child in syntaxNode.ChildNodesAndTokens())
37 | {
38 | numTokensInChildren += CountTokens(child);
39 | }
40 |
41 | if (syntaxNode.IsToken && !tokensToIgnore.Contains(kind))
42 | {
43 | //Console.WriteLine(kind + " " + syntaxNode.ToString());
44 |
45 | // String literals count for as many chars as are in the string
46 | if (kind is SyntaxKind.StringLiteralToken or SyntaxKind.InterpolatedStringTextToken)
47 | {
48 | return syntaxNode.ToString().Length;
49 | }
50 |
51 | // Regular tokens count as just one token
52 | return 1;
53 | }
54 |
55 | return numTokensInChildren;
56 | }
57 |
58 | }
59 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Helpers/UIHelper.cs:
--------------------------------------------------------------------------------
1 | using Raylib_cs;
2 | using System;
3 | using System.IO;
4 | using System.Numerics;
5 |
6 | namespace ChessChallenge.Application
7 | {
8 | public static class UIHelper
9 | {
10 | static readonly bool SDF_Enabled = true;
11 | const string fontName = "OPENSANS-SEMIBOLD.TTF";
12 | const int referenceResolution = 1920;
13 |
14 | static Font font;
15 | static Font fontSdf;
16 | static Shader shader;
17 |
18 | public enum AlignH
19 | {
20 | Left,
21 | Centre,
22 | Right
23 | }
24 | public enum AlignV
25 | {
26 | Top,
27 | Centre,
28 | Bottom
29 | }
30 |
31 | static UIHelper()
32 | {
33 | if (SDF_Enabled)
34 | {
35 | unsafe
36 | {
37 | const int baseSize = 64;
38 | uint fileSize = 0;
39 | var fileData = Raylib.LoadFileData(GetResourcePath("Fonts", fontName), ref fileSize);
40 | Font fontSdf = default;
41 | fontSdf.baseSize = baseSize;
42 | fontSdf.glyphCount = 95;
43 | fontSdf.glyphs = Raylib.LoadFontData(fileData, (int)fileSize, baseSize, null, 0, FontType.FONT_SDF);
44 |
45 | Image atlas = Raylib.GenImageFontAtlas(fontSdf.glyphs, &fontSdf.recs, 95, baseSize, 0, 1);
46 | fontSdf.texture = Raylib.LoadTextureFromImage(atlas);
47 | Raylib.UnloadImage(atlas);
48 | Raylib.UnloadFileData(fileData);
49 |
50 | Raylib.SetTextureFilter(fontSdf.texture, TextureFilter.TEXTURE_FILTER_BILINEAR);
51 | UIHelper.fontSdf = fontSdf;
52 |
53 | }
54 | shader = Raylib.LoadShader("", GetResourcePath("Fonts", "sdf.fs"));
55 | }
56 | font = Raylib.LoadFontEx(GetResourcePath("Fonts", fontName), 128, null, 0);
57 |
58 | }
59 |
60 | public static void DrawText(string text, Vector2 pos, int size, int spacing, Color col, AlignH alignH = AlignH.Left, AlignV alignV = AlignV.Centre)
61 | {
62 | Vector2 boundSize = Raylib.MeasureTextEx(font, text, size, spacing);
63 | float offsetX = alignH == AlignH.Left ? 0 : (alignH == AlignH.Centre ? -boundSize.X / 2 : -boundSize.X);
64 | float offsetY = alignV == AlignV.Top ? 0 : (alignV == AlignV.Centre ? -boundSize.Y / 2 : -boundSize.Y);
65 | Vector2 offset = new(offsetX, offsetY);
66 |
67 | if (SDF_Enabled)
68 | {
69 | Raylib.BeginShaderMode(shader);
70 | Raylib.DrawTextEx(fontSdf, text, pos + offset, size, spacing, col);
71 | Raylib.EndShaderMode();
72 | }
73 | else
74 | {
75 | Raylib.DrawTextEx(font, text, pos + offset, size, spacing, col);
76 | }
77 | }
78 |
79 | public static bool Button(string text, Vector2 centre, Vector2 size)
80 | {
81 | Rectangle rec = new(centre.X - size.X / 2, centre.Y - size.Y / 2, size.X, size.Y);
82 |
83 | Color normalCol = new(40, 40, 40, 255);
84 | Color hoverCol = new(3, 173, 252, 255);
85 | Color pressCol = new(2, 119, 173, 255);
86 |
87 | bool mouseOver = MouseInRect(rec);
88 | bool pressed = mouseOver && Raylib.IsMouseButtonDown(MouseButton.MOUSE_BUTTON_LEFT);
89 | bool pressedThisFrame = pressed && Raylib.IsMouseButtonPressed(MouseButton.MOUSE_BUTTON_LEFT);
90 | Color col = mouseOver ? (pressed ? pressCol : hoverCol) : normalCol;
91 |
92 | Raylib.DrawRectangleRec(rec, col);
93 | Color textCol = mouseOver ? Color.WHITE : new Color(180, 180, 180, 255);
94 | int fontSize = ScaleInt(32);
95 |
96 | DrawText(text, centre, fontSize, 1, textCol, AlignH.Centre);
97 |
98 | return pressedThisFrame;
99 | }
100 |
101 | static bool MouseInRect(Rectangle rec)
102 | {
103 | Vector2 mousePos = Raylib.GetMousePosition();
104 | return mousePos.X >= rec.x && mousePos.Y >= rec.y && mousePos.X <= rec.x + rec.width && mousePos.Y <= rec.y + rec.height;
105 | }
106 |
107 | public static string GetResourcePath(params string[] localPath)
108 | {
109 | return Path.Combine(Directory.GetCurrentDirectory(), "resources", Path.Combine(localPath));
110 | }
111 |
112 | public static float Scale(float val, int referenceResolution = referenceResolution)
113 | {
114 | return Raylib.GetScreenWidth() / (float)referenceResolution * val;
115 | }
116 |
117 | public static int ScaleInt(int val, int referenceResolution = referenceResolution)
118 | {
119 | return (int)Math.Round(Raylib.GetScreenWidth() / (float)referenceResolution * val);
120 | }
121 |
122 | public static Vector2 Scale(Vector2 vec, int referenceResolution = referenceResolution)
123 | {
124 | float x = Scale(vec.X, referenceResolution);
125 | float y = Scale(vec.Y, referenceResolution);
126 | return new Vector2(x, y);
127 | }
128 |
129 | public static void Release()
130 | {
131 | Raylib.UnloadFont(font);
132 | if (SDF_Enabled)
133 | {
134 | Raylib.UnloadFont(fontSdf);
135 | Raylib.UnloadShader(shader);
136 | }
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Helpers/Warmer.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.API;
2 |
3 | namespace ChessChallenge.Application
4 | {
5 | public static class Warmer
6 | {
7 |
8 | public static void Warm()
9 | {
10 | Chess.Board b = new();
11 | b.LoadStartPosition();
12 | Board board = new Board(b);
13 | Move[] moves = board.GetLegalMoves();
14 |
15 | board.MakeMove(moves[0]);
16 | board.UndoMove(moves[0]);
17 | }
18 |
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Players/ChessPlayer.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.API;
2 | using System;
3 |
4 | namespace ChessChallenge.Application
5 | {
6 | public class ChessPlayer
7 | {
8 | // public event Action? MoveChosen;
9 |
10 | public readonly ChallengeController.PlayerType PlayerType;
11 | public readonly IChessBot? Bot;
12 | public readonly HumanPlayer? Human;
13 |
14 | double secondsElapsed;
15 | int baseTimeMS;
16 |
17 | public ChessPlayer(object instance, ChallengeController.PlayerType type, int baseTimeMS = int.MaxValue)
18 | {
19 | this.PlayerType = type;
20 | Bot = instance as IChessBot;
21 | Human = instance as HumanPlayer;
22 | this.baseTimeMS = baseTimeMS;
23 |
24 | }
25 |
26 | public bool IsHuman => Human != null;
27 | public bool IsBot => Bot != null;
28 |
29 | public void Update()
30 | {
31 | if (Human != null)
32 | {
33 | Human.Update();
34 | }
35 | }
36 |
37 | public void UpdateClock(double dt)
38 | {
39 | secondsElapsed += dt;
40 | }
41 |
42 | public int TimeRemainingMs
43 | {
44 | get
45 | {
46 | if (baseTimeMS == int.MaxValue)
47 | {
48 | return baseTimeMS;
49 | }
50 | return (int)Math.Round(Math.Max(0, baseTimeMS - secondsElapsed * 1000.0));
51 | }
52 | }
53 |
54 | public void SubscribeToMoveChosenEventIfHuman(Action action)
55 | {
56 | if (Human != null)
57 | {
58 | Human.MoveChosen += action;
59 | }
60 | }
61 |
62 |
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/Players/HumanPlayer.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.Chess;
2 | using Raylib_cs;
3 | using System.Numerics;
4 |
5 | namespace ChessChallenge.Application
6 | {
7 | public class HumanPlayer
8 | {
9 | public event System.Action? MoveChosen;
10 |
11 | readonly Board board;
12 | readonly BoardUI boardUI;
13 |
14 | // State
15 | bool isDragging;
16 | int selectedSquare;
17 | bool isTurnToMove;
18 |
19 |
20 | public HumanPlayer(BoardUI boardUI)
21 | {
22 | board = new();
23 | board.LoadStartPosition();
24 | this.boardUI = boardUI;
25 | }
26 |
27 | public void NotifyTurnToMove()
28 | {
29 | isTurnToMove = true;
30 | }
31 |
32 | public void SetPosition(string fen)
33 | {
34 | board.LoadPosition(fen);
35 | }
36 |
37 | public void Update()
38 | {
39 | if (!isTurnToMove)
40 | {
41 | return;
42 | }
43 | Vector2 mouseScreenPos = Raylib.GetMousePosition();
44 | Vector2 mouseWorldPos = Program.ScreenToWorldPos(mouseScreenPos);
45 |
46 | if (LeftMousePressedThisFrame())
47 | {
48 | if (boardUI.TryGetSquareAtPoint(mouseWorldPos, out int square))
49 | {
50 | int piece = board.Square[square];
51 | if (PieceHelper.IsColour(piece, board.IsWhiteToMove ? PieceHelper.White : PieceHelper.Black))
52 | {
53 | isDragging = true;
54 | selectedSquare = square;
55 | boardUI.HighlightLegalMoves(board, square);
56 | }
57 | }
58 | }
59 |
60 | if (isDragging)
61 | {
62 | if (LeftMouseReleasedThisFrame())
63 | {
64 | CancelDrag();
65 | if (boardUI.TryGetSquareAtPoint(mouseWorldPos, out int square))
66 | {
67 | TryMakeMove(selectedSquare, square);
68 | }
69 | }
70 | else if (RightMousePressedThisFrame())
71 | {
72 | CancelDrag();
73 | }
74 | else
75 | {
76 | boardUI.DragPiece(selectedSquare, mouseWorldPos);
77 | }
78 | }
79 | }
80 |
81 | void CancelDrag()
82 | {
83 | isDragging = false;
84 | boardUI.ResetSquareColours(true);
85 | }
86 |
87 | void TryMakeMove(int startSquare, int targetSquare)
88 | {
89 | bool isLegal = false;
90 | Move move = Move.NullMove;
91 |
92 | MoveGenerator generator = new();
93 | var legalMoves = generator.GenerateMoves(board);
94 | foreach (var legalMove in legalMoves)
95 | {
96 | if (legalMove.StartSquareIndex == startSquare && legalMove.TargetSquareIndex == targetSquare)
97 | {
98 | isLegal = true;
99 | move = legalMove;
100 | break;
101 | }
102 | }
103 |
104 | if (isLegal)
105 | {
106 | isTurnToMove = false;
107 | MoveChosen?.Invoke(move);
108 | }
109 | }
110 |
111 | static bool LeftMousePressedThisFrame() => Raylib.IsMouseButtonPressed(MouseButton.MOUSE_BUTTON_LEFT);
112 | static bool LeftMouseReleasedThisFrame() => Raylib.IsMouseButtonReleased(MouseButton.MOUSE_BUTTON_LEFT);
113 | static bool RightMousePressedThisFrame() => Raylib.IsMouseButtonPressed(MouseButton.MOUSE_BUTTON_RIGHT);
114 |
115 | }
116 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/UI/BoardTheme.cs:
--------------------------------------------------------------------------------
1 | using Raylib_cs;
2 |
3 | namespace ChessChallenge.Application
4 | {
5 | public class BoardTheme
6 | {
7 | public Color LightCol = new Color(238, 216, 192, 255);
8 | public Color DarkCol = new Color(171, 121, 101, 255);
9 |
10 | public Color selectedLight = new Color(236, 197, 123, 255);
11 | public Color selectedDark = new Color(200, 158, 80, 255);
12 |
13 | public Color MoveFromLight = new Color(207, 172, 106, 255);
14 | public Color MoveFromDark = new Color(197, 158, 54, 255);
15 |
16 | public Color MoveToLight = new Color(221, 208, 124, 255);
17 | public Color MoveToDark = new Color(197, 173, 96, 255);
18 |
19 | public Color LegalLight = new Color(89, 171, 221, 255);
20 | public Color LegalDark = new Color(62, 144, 195, 255);
21 |
22 | public Color CheckLight = new Color(234, 74, 74, 255);
23 | public Color CheckDark = new Color(207, 39, 39, 255);
24 |
25 | public Color BorderCol = new Color(44, 44, 44, 255);
26 |
27 | public Color LightCoordCol = new Color(255, 240, 220, 255);
28 | public Color DarkCoordCol = new Color(140, 100, 80, 255);
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/UI/BoardUI.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.Chess;
2 | using Raylib_cs;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Numerics;
6 | using System.IO;
7 | using static ChessChallenge.Application.UIHelper;
8 |
9 | namespace ChessChallenge.Application
10 | {
11 | public class BoardUI
12 | {
13 | const int squareSize = 100;
14 | const double moveAnimDuration = 0.15;
15 | bool whitePerspective = true;
16 |
17 | // Text colours
18 | static readonly Color activeTextCol = new(200, 200, 200, 255);
19 | static readonly Color inactiveTextCol = new(100, 100, 100, 255);
20 | static readonly Color nameCol = new(67, 204, 101, 255);
21 |
22 | // Colour state
23 | Color topTextCol;
24 | Color bottomTextCol;
25 |
26 | // Drag state
27 | bool isDraggingPiece;
28 | int dragSquare;
29 | Vector2 dragPos;
30 |
31 | static readonly int[] pieceImageOrder = { 5, 3, 2, 4, 1, 0 };
32 | Texture2D piecesTexture;
33 | BoardTheme theme;
34 | Dictionary squareColOverrides;
35 | Board board;
36 | Move lastMove;
37 |
38 | // Animate move state
39 | Board animateMoveTargetBoardState;
40 | Move moveToAnimate;
41 | double moveAnimStartTime;
42 | bool isAnimatingMove;
43 |
44 |
45 | public enum HighlightType
46 | {
47 | MoveFrom,
48 | MoveTo,
49 | LegalMove,
50 | Check
51 | }
52 |
53 |
54 | public BoardUI()
55 | {
56 | theme = new BoardTheme();
57 |
58 | LoadPieceTexture();
59 |
60 | board = new Board();
61 | board.LoadStartPosition();
62 | squareColOverrides = new Dictionary();
63 | topTextCol = inactiveTextCol;
64 | bottomTextCol = inactiveTextCol;
65 | }
66 |
67 | public void SetPerspective(bool whitePerspective)
68 | {
69 | this.whitePerspective = whitePerspective;
70 | }
71 |
72 | public void UpdatePosition(Board board)
73 | {
74 | isAnimatingMove = false;
75 |
76 | // Update
77 | this.board = new(board);
78 | lastMove = Move.NullMove;
79 | if (board.IsInCheck())
80 | {
81 | OverrideSquareColour(board.KingSquare[board.MoveColourIndex], HighlightType.Check);
82 | }
83 | }
84 |
85 | public void UpdatePosition(Board board, Move moveMade, bool animate = false)
86 | {
87 | // Interrupt prev animation
88 | if (isAnimatingMove)
89 | {
90 | UpdatePosition(animateMoveTargetBoardState);
91 | isAnimatingMove = false;
92 | }
93 |
94 | ResetSquareColours();
95 | if (animate)
96 | {
97 | OverrideSquareColour(moveMade.StartSquareIndex, HighlightType.MoveFrom);
98 | animateMoveTargetBoardState = new Board(board);
99 | moveToAnimate = moveMade;
100 | moveAnimStartTime = Raylib.GetTime();
101 | isAnimatingMove = true;
102 | }
103 | else
104 | {
105 | UpdatePosition(board);
106 |
107 | if (!moveMade.IsNull)
108 | {
109 | HighlightMove(moveMade);
110 | lastMove = moveMade;
111 | }
112 | }
113 | }
114 |
115 | void HighlightMove(Move move)
116 | {
117 | OverrideSquareColour(move.StartSquareIndex, HighlightType.MoveFrom);
118 | OverrideSquareColour(move.TargetSquareIndex, HighlightType.MoveTo);
119 | }
120 |
121 | public void DragPiece(int square, Vector2 worldPos)
122 | {
123 | isDraggingPiece = true;
124 | dragSquare = square;
125 | dragPos = worldPos;
126 | }
127 |
128 | public bool TryGetSquareAtPoint(Vector2 worldPos, out int squareIndex)
129 | {
130 | Vector2 boardStartPosWorld = new Vector2(squareSize, squareSize) * -4;
131 | Vector2 endPosWorld = boardStartPosWorld + new Vector2(8, 8) * squareSize;
132 |
133 | float tx = (worldPos.X - boardStartPosWorld.X) / (endPosWorld.X - boardStartPosWorld.X);
134 | float ty = (worldPos.Y - boardStartPosWorld.Y) / (endPosWorld.Y - boardStartPosWorld.Y);
135 |
136 | if (tx >= 0 && tx <= 1 && ty >= 0 && ty <= 1)
137 | {
138 | if (!whitePerspective)
139 | {
140 | tx = 1 - tx;
141 | ty = 1 - ty;
142 | }
143 | squareIndex = new Coord((int)(tx * 8), 7 - (int)(ty * 8)).SquareIndex;
144 | return true;
145 | }
146 |
147 | squareIndex = -1;
148 | return false;
149 | }
150 |
151 | public void OverrideSquareColour(int square, HighlightType hightlightType)
152 | {
153 | bool isLight = new Coord(square).IsLightSquare();
154 |
155 | Color col = hightlightType switch
156 | {
157 | HighlightType.MoveFrom => isLight ? theme.MoveFromLight : theme.MoveFromDark,
158 | HighlightType.MoveTo => isLight ? theme.MoveToLight : theme.MoveToDark,
159 | HighlightType.LegalMove => isLight ? theme.LegalLight : theme.LegalDark,
160 | HighlightType.Check => isLight ? theme.CheckLight : theme.CheckDark,
161 | _ => Color.PINK
162 | };
163 |
164 | if (squareColOverrides.ContainsKey(square))
165 | {
166 | squareColOverrides[square] = col;
167 | }
168 | else
169 | {
170 | squareColOverrides.Add(square, col);
171 | }
172 | }
173 |
174 | public void HighlightLegalMoves(Board board, int square)
175 | {
176 | MoveGenerator moveGenerator = new();
177 | var moves = moveGenerator.GenerateMoves(board);
178 | foreach (var move in moves)
179 | {
180 | if (move.StartSquareIndex == square)
181 | {
182 | OverrideSquareColour(move.TargetSquareIndex, HighlightType.LegalMove);
183 | }
184 | }
185 | }
186 |
187 | public void Draw()
188 | {
189 | double animT = (Raylib.GetTime() - moveAnimStartTime) / moveAnimDuration;
190 |
191 | if (isAnimatingMove && animT >= 1)
192 | {
193 | isAnimatingMove = false;
194 | UpdatePosition(animateMoveTargetBoardState, moveToAnimate, false);
195 | }
196 |
197 | DrawBorder();
198 | for (int y = 0; y < 8; y++)
199 | {
200 | for (int x = 0; x < 8; x++)
201 | {
202 | DrawSquare(x, y);
203 | }
204 | }
205 |
206 | if (isDraggingPiece)
207 | {
208 | DrawPiece(board.Square[dragSquare], dragPos - new Vector2(squareSize * 0.5f, squareSize * 0.5f));
209 | }
210 | if (isAnimatingMove)
211 | {
212 | Coord startCoord = new Coord(moveToAnimate.StartSquareIndex);
213 | Coord targetCoord = new Coord(moveToAnimate.TargetSquareIndex);
214 | Vector2 startPos = GetSquarePos(startCoord.fileIndex, startCoord.rankIndex, whitePerspective);
215 | Vector2 targetPos = GetSquarePos(targetCoord.fileIndex, targetCoord.rankIndex, whitePerspective);
216 |
217 | Vector2 animPos = Vector2.Lerp(startPos, targetPos, (float)animT);
218 | DrawPiece(board.Square[moveToAnimate.StartSquareIndex], animPos);
219 |
220 | }
221 |
222 | // Reset state
223 | isDraggingPiece = false;
224 | }
225 |
226 | public void DrawPlayerNames(string nameWhite, string nameBlack, int timeWhite, int timeBlack, bool isPlaying)
227 | {
228 | string nameBottom = whitePerspective ? nameWhite : nameBlack;
229 | string nameTop = !whitePerspective ? nameWhite : nameBlack;
230 | int timeBottom = whitePerspective ? timeWhite : timeBlack;
231 | int timeTop = !whitePerspective ? timeWhite : timeBlack;
232 | bool bottomTurnToMove = whitePerspective == board.IsWhiteToMove && isPlaying;
233 | bool topTurnToMove = whitePerspective != board.IsWhiteToMove && isPlaying;
234 |
235 | string colNameBottom = whitePerspective ? "White" : "Black";
236 | string colNameTop = !whitePerspective ? "White" : "Black";
237 |
238 | int boardStartX = -squareSize * 4;
239 | int boardStartY = -squareSize * 4;
240 | const int spaceY = 35;
241 |
242 |
243 | Color textTopTargetCol = topTurnToMove ? activeTextCol : inactiveTextCol;
244 | Color textBottomTargetCol = bottomTurnToMove ? activeTextCol : inactiveTextCol;
245 |
246 | float colLerpSpeed = 16;
247 | topTextCol = LerpColour(topTextCol, textTopTargetCol, Raylib.GetFrameTime() * colLerpSpeed);
248 | bottomTextCol = LerpColour(bottomTextCol, textBottomTargetCol, Raylib.GetFrameTime() * colLerpSpeed);
249 |
250 | //Color textColTop = topTurnToMove ? activeTextCol : inactiveTextCol;
251 |
252 | Draw(boardStartY + squareSize * 8 + spaceY, colNameBottom, nameBottom, timeBottom, bottomTextCol);
253 | Draw(boardStartY - spaceY, colNameTop, nameTop, timeTop, topTextCol);
254 |
255 | void Draw(float y, string colName, string name, int timeMs, Color textCol)
256 | {
257 | const int fontSize = 36;
258 | const int fontSpacing = 1;
259 | var namePos = new Vector2(boardStartX, y);
260 |
261 | UIHelper.DrawText($"{colName}: {name}", namePos, fontSize, fontSpacing, nameCol);
262 | var timePos = new Vector2(boardStartX + squareSize * 8, y);
263 | string timeText;
264 | if (timeMs == int.MaxValue)
265 | {
266 | timeText = "Time: Unlimited";
267 | }
268 | else
269 | {
270 | double secondsRemaining = timeMs / 1000.0;
271 | int numMinutes = (int)(secondsRemaining / 60);
272 | int numSeconds = (int)(secondsRemaining - numMinutes * 60);
273 | int dec = (int)((secondsRemaining - numMinutes * 60 - numSeconds) * 10);
274 |
275 | timeText = $"Time: {numMinutes:00}:{numSeconds:00}.{dec}";
276 | }
277 | UIHelper.DrawText(timeText, timePos, fontSize, fontSpacing, textCol, UIHelper.AlignH.Right);
278 | }
279 | }
280 |
281 | public void ResetSquareColours(bool keepPrevMoveHighlight = false)
282 | {
283 | squareColOverrides.Clear();
284 | if (keepPrevMoveHighlight && !lastMove.IsNull)
285 | {
286 | HighlightMove(lastMove);
287 | }
288 | }
289 |
290 |
291 | void DrawBorder()
292 | {
293 | int boardStartX = -squareSize * 4;
294 | int boardStartY = -squareSize * 4;
295 | int w = 12;
296 | Raylib.DrawRectangle(boardStartX - w, boardStartY - w, 8 * squareSize + w * 2, 8 * squareSize + w * 2, theme.BorderCol);
297 | }
298 |
299 | void DrawSquare(int file, int rank)
300 | {
301 |
302 | Coord coord = new Coord(file, rank);
303 | Color col = coord.IsLightSquare() ? theme.LightCol : theme.DarkCol;
304 | if (squareColOverrides.TryGetValue(coord.SquareIndex, out Color overrideCol))
305 | {
306 | col = overrideCol;
307 | }
308 |
309 | // top left
310 | Vector2 pos = GetSquarePos(file, rank, whitePerspective);
311 | Raylib.DrawRectangle((int)pos.X, (int)pos.Y, squareSize, squareSize, col);
312 | int piece = board.Square[coord.SquareIndex];
313 | float alpha = isDraggingPiece && dragSquare == coord.SquareIndex ? 0.3f : 1;
314 | if (!isAnimatingMove || coord.SquareIndex != moveToAnimate.StartSquareIndex)
315 | {
316 | DrawPiece(piece, new Vector2((int)pos.X, (int)pos.Y), alpha);
317 | }
318 |
319 | if (Settings.DisplayBoardCoordinates)
320 | {
321 | int textSize = 25;
322 | float xpadding = 5f;
323 | float ypadding = 2f;
324 | Color coordNameCol = coord.IsLightSquare() ? theme.DarkCoordCol : theme.LightCoordCol;
325 |
326 | if (rank == (whitePerspective ? 0 : 7))
327 | {
328 | string fileName = BoardHelper.fileNames[file] + "";
329 | Vector2 drawPos = pos + new Vector2(xpadding, squareSize - ypadding);
330 | DrawText(fileName, drawPos, textSize, 0, coordNameCol, AlignH.Left, AlignV.Bottom);
331 | }
332 | if (file == (whitePerspective ? 7 : 0))
333 | {
334 | string rankName = (rank + 1) + "";
335 | Vector2 drawPos = pos + new Vector2(squareSize - xpadding, ypadding);
336 | DrawText(rankName, drawPos, textSize, 0, coordNameCol, AlignH.Right, AlignV.Top);
337 | }
338 | }
339 | }
340 |
341 | static Vector2 GetSquarePos(int file, int rank, bool whitePerspective)
342 | {
343 | const int boardStartX = -squareSize * 4;
344 | const int boardStartY = -squareSize * 4;
345 |
346 | if (!whitePerspective)
347 | {
348 | file = 7 - file;
349 | rank = 7 - rank;
350 | }
351 |
352 | int posX = boardStartX + file * squareSize;
353 | int posY = boardStartY + (7 - rank) * squareSize;
354 | return new Vector2(posX, posY);
355 | }
356 |
357 | void DrawPiece(int piece, Vector2 posTopLeft, float alpha = 1)
358 | {
359 | if (piece != PieceHelper.None)
360 | {
361 | int type = PieceHelper.PieceType(piece);
362 | bool white = PieceHelper.IsWhite(piece);
363 | Rectangle srcRect = GetPieceTextureRect(type, white);
364 | Rectangle targRect = new Rectangle((int)posTopLeft.X, (int)posTopLeft.Y, squareSize, squareSize);
365 |
366 | Color tint = new Color(255, 255, 255, (int)MathF.Round(255 * alpha));
367 | Raylib.DrawTexturePro(piecesTexture, srcRect, targRect, new Vector2(0, 0), 0, tint);
368 | }
369 | }
370 |
371 | static Color LerpColour(Color a, Color b, float t)
372 | {
373 | int newR = (int)(Math.Round(Lerp(a.r, b.r, t)));
374 | int newG = (int)(Math.Round(Lerp(a.g, b.g, t)));
375 | int newB = (int)(Math.Round(Lerp(a.b, b.b, t)));
376 | int newA = (int)(Math.Round(Lerp(a.a, b.a, t)));
377 | return new Color(newR, newG, newB, newA);
378 |
379 | float Lerp(float a, float b, float t)
380 | {
381 | t = Math.Min(1, Math.Max(t, 0));
382 | return a + (b - a) * t;
383 | }
384 | }
385 |
386 | void LoadPieceTexture()
387 | {
388 | // Workaround for Raylib.LoadTexture() not working when path contains non-ascii chars
389 | byte[] pieceImgBytes = File.ReadAllBytes(GetResourcePath("Pieces.png"));
390 | Image pieceImg = Raylib.LoadImageFromMemory(".png", pieceImgBytes);
391 | piecesTexture = Raylib.LoadTextureFromImage(pieceImg);
392 | Raylib.UnloadImage(pieceImg);
393 |
394 | Raylib.GenTextureMipmaps(ref piecesTexture);
395 | Raylib.SetTextureWrap(piecesTexture, TextureWrap.TEXTURE_WRAP_CLAMP);
396 | Raylib.SetTextureFilter(piecesTexture, TextureFilter.TEXTURE_FILTER_BILINEAR);
397 | }
398 |
399 | public void Release()
400 | {
401 | Raylib.UnloadTexture(piecesTexture);
402 | }
403 |
404 | static Rectangle GetPieceTextureRect(int pieceType, bool isWhite)
405 | {
406 | const int size = 333;
407 | return new Rectangle(size * pieceImageOrder[pieceType - 1], isWhite ? 0 : size, size, size);
408 | }
409 | }
410 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/UI/BotBrainCapacityUI.cs:
--------------------------------------------------------------------------------
1 | using Raylib_cs;
2 |
3 | namespace ChessChallenge.Application
4 | {
5 | public static class BotBrainCapacityUI
6 | {
7 | static readonly Color green = new(17, 212, 73, 255);
8 | static readonly Color yellow = new(219, 161, 24, 255);
9 | static readonly Color orange = new(219, 96, 24, 255);
10 | static readonly Color red = new(219, 9, 9, 255);
11 | static readonly Color background = new Color(40, 40, 40, 255);
12 |
13 | public static void Draw(int numTokens, int tokenLimit)
14 | {
15 |
16 | int screenWidth = Raylib.GetScreenWidth();
17 | int screenHeight = Raylib.GetScreenHeight();
18 | int height = UIHelper.ScaleInt(48);
19 | int fontSize = UIHelper.ScaleInt(35);
20 | // Bg
21 | Raylib.DrawRectangle(0, screenHeight - height, screenWidth, height, background);
22 | // Bar
23 | double t = (double)numTokens / tokenLimit;
24 |
25 | Color col;
26 | if (t <= 0.7)
27 | col = green;
28 | else if (t <= 0.85)
29 | col = yellow;
30 | else if (t <= 1)
31 | col = orange;
32 | else
33 | col = red;
34 | Raylib.DrawRectangle(0, screenHeight - height, (int)(screenWidth * t), height, col);
35 |
36 | var textPos = new System.Numerics.Vector2(screenWidth / 2, screenHeight - height / 2);
37 | string text = $"Bot Brain Capacity: {numTokens}/{tokenLimit}";
38 | if (numTokens > tokenLimit)
39 | {
40 | text += " [LIMIT EXCEEDED]";
41 | }
42 | UIHelper.DrawText(text, textPos, fontSize, 1, Color.WHITE, UIHelper.AlignH.Centre);
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/UI/MatchStatsUI.cs:
--------------------------------------------------------------------------------
1 | using Raylib_cs;
2 | using System.Numerics;
3 | using System;
4 |
5 | namespace ChessChallenge.Application
6 | {
7 | public static class MatchStatsUI
8 | {
9 | public static void DrawMatchStats(ChallengeController controller)
10 | {
11 | if (controller.PlayerWhite.IsBot && controller.PlayerBlack.IsBot)
12 | {
13 | int nameFontSize = UIHelper.ScaleInt(40);
14 | int regularFontSize = UIHelper.ScaleInt(35);
15 | int headerFontSize = UIHelper.ScaleInt(45);
16 | Color col = new(180, 180, 180, 255);
17 | Vector2 startPos = UIHelper.Scale(new Vector2(1500, 250));
18 | float spacingY = UIHelper.Scale(35);
19 |
20 | DrawNextText($"Game {controller.CurrGameNumber} of {controller.TotalGameCount}", headerFontSize, Color.WHITE);
21 | startPos.Y += spacingY * 2;
22 |
23 | DrawStats(controller.BotStatsA);
24 | startPos.Y += spacingY * 2;
25 | DrawStats(controller.BotStatsB);
26 |
27 |
28 | void DrawStats(ChallengeController.BotMatchStats stats)
29 | {
30 | DrawNextText(stats.BotName + ":", nameFontSize, Color.WHITE);
31 | DrawNextText($"Score: +{stats.NumWins} ={stats.NumDraws} -{stats.NumLosses}", regularFontSize, col);
32 | DrawNextText($"Num Timeouts: {stats.NumTimeouts}", regularFontSize, col);
33 | DrawNextText($"Num Illegal Moves: {stats.NumIllegalMoves}", regularFontSize, col);
34 | }
35 |
36 | void DrawNextText(string text, int fontSize, Color col)
37 | {
38 | UIHelper.DrawText(text, startPos, fontSize, 1, col);
39 | startPos.Y += spacingY;
40 | }
41 | }
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Application/UI/MenuUI.cs:
--------------------------------------------------------------------------------
1 | using Raylib_cs;
2 | using System.Numerics;
3 | using System;
4 | using System.IO;
5 |
6 | namespace ChessChallenge.Application
7 | {
8 | public static class MenuUI
9 | {
10 | public static void DrawButtons(ChallengeController controller)
11 | {
12 | Vector2 buttonPos = UIHelper.Scale(new Vector2(260, 210));
13 | Vector2 buttonSize = UIHelper.Scale(new Vector2(260, 55));
14 | float spacing = buttonSize.Y * 1.2f;
15 | float breakSpacing = spacing * 0.6f;
16 |
17 | // Game Buttons
18 | if (NextButtonInRow("Human vs MyBot", ref buttonPos, spacing, buttonSize))
19 | {
20 | var whiteType = controller.HumanWasWhiteLastGame ? ChallengeController.PlayerType.MyBot : ChallengeController.PlayerType.Human;
21 | var blackType = !controller.HumanWasWhiteLastGame ? ChallengeController.PlayerType.MyBot : ChallengeController.PlayerType.Human;
22 | controller.StartNewGame(whiteType, blackType);
23 | }
24 | if (NextButtonInRow("MyBot vs MyBot", ref buttonPos, spacing, buttonSize))
25 | {
26 | controller.StartNewBotMatch(ChallengeController.PlayerType.MyBot, ChallengeController.PlayerType.MyBot);
27 | }
28 | if (NextButtonInRow("MyBot vs EvilBot", ref buttonPos, spacing, buttonSize))
29 | {
30 | controller.StartNewBotMatch(ChallengeController.PlayerType.MyBot, ChallengeController.PlayerType.EvilBot);
31 | }
32 |
33 | // Page buttons
34 | buttonPos.Y += breakSpacing;
35 |
36 | if (NextButtonInRow("Save Games", ref buttonPos, spacing, buttonSize))
37 | {
38 | string pgns = controller.AllPGNs;
39 | string directoryPath = Path.Combine(FileHelper.AppDataPath, "Games");
40 | Directory.CreateDirectory(directoryPath);
41 | string fileName = FileHelper.GetUniqueFileName(directoryPath, "games", ".txt");
42 | string fullPath = Path.Combine(directoryPath, fileName);
43 | File.WriteAllText(fullPath, pgns);
44 | ConsoleHelper.Log("Saved games to " + fullPath, false, ConsoleColor.Blue);
45 | }
46 | if (NextButtonInRow("Rules & Help", ref buttonPos, spacing, buttonSize))
47 | {
48 | FileHelper.OpenUrl("https://github.com/SebLague/Chess-Challenge");
49 | }
50 | if (NextButtonInRow("Documentation", ref buttonPos, spacing, buttonSize))
51 | {
52 | FileHelper.OpenUrl("https://seblague.github.io/chess-coding-challenge/documentation/");
53 | }
54 | if (NextButtonInRow("Submission Page", ref buttonPos, spacing, buttonSize))
55 | {
56 | FileHelper.OpenUrl("https://forms.gle/6jjj8jxNQ5Ln53ie6");
57 | }
58 |
59 | // Window and quit buttons
60 | buttonPos.Y += breakSpacing;
61 |
62 | bool isBigWindow = Raylib.GetScreenWidth() > Settings.ScreenSizeSmall.X;
63 | string windowButtonName = isBigWindow ? "Smaller Window" : "Bigger Window";
64 | if (NextButtonInRow(windowButtonName, ref buttonPos, spacing, buttonSize))
65 | {
66 | Program.SetWindowSize(isBigWindow ? Settings.ScreenSizeSmall : Settings.ScreenSizeBig);
67 | }
68 | if (NextButtonInRow("Exit (ESC)", ref buttonPos, spacing, buttonSize))
69 | {
70 | Environment.Exit(0);
71 | }
72 |
73 | bool NextButtonInRow(string name, ref Vector2 pos, float spacingY, Vector2 size)
74 | {
75 | bool pressed = UIHelper.Button(name, pos, size);
76 | pos.Y += spacingY;
77 | return pressed;
78 | }
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Board/Coord.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | namespace ChessChallenge.Chess
3 | {
4 | // Structure for representing squares on the chess board as file/rank integer pairs.
5 | // (0, 0) = a1, (7, 7) = h8.
6 | // Coords can also be used as offsets. For example, while a Coord of (-1, 0) is not
7 | // a valid square, it can be used to represent the concept of moving 1 square left.
8 |
9 | public readonly struct Coord : IComparable
10 | {
11 | public readonly int fileIndex;
12 | public readonly int rankIndex;
13 |
14 | public Coord(int fileIndex, int rankIndex)
15 | {
16 | this.fileIndex = fileIndex;
17 | this.rankIndex = rankIndex;
18 | }
19 |
20 | public Coord(int squareIndex)
21 | {
22 | this.fileIndex = BoardHelper.FileIndex(squareIndex);
23 | this.rankIndex = BoardHelper.RankIndex(squareIndex);
24 | }
25 |
26 | public bool IsLightSquare()
27 | {
28 | return (fileIndex + rankIndex) % 2 != 0;
29 | }
30 |
31 | public int CompareTo(Coord other)
32 | {
33 | return (fileIndex == other.fileIndex && rankIndex == other.rankIndex) ? 0 : 1;
34 | }
35 |
36 | public static Coord operator +(Coord a, Coord b) => new Coord(a.fileIndex + b.fileIndex, a.rankIndex + b.rankIndex);
37 | public static Coord operator -(Coord a, Coord b) => new Coord(a.fileIndex - b.fileIndex, a.rankIndex - b.rankIndex);
38 | public static Coord operator *(Coord a, int m) => new Coord(a.fileIndex * m, a.rankIndex * m);
39 | public static Coord operator *(int m, Coord a) => a * m;
40 |
41 | public bool IsValidSquare() => fileIndex >= 0 && fileIndex < 8 && rankIndex >= 0 && rankIndex < 8;
42 | public int SquareIndex => BoardHelper.IndexFromCoord(this);
43 | }
44 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Board/GameState.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | public readonly struct GameState
4 | {
5 | public readonly int capturedPieceType;
6 | public readonly int enPassantFile;
7 | public readonly int castlingRights;
8 | public readonly int fiftyMoveCounter;
9 | public readonly ulong zobristKey;
10 |
11 | public const int ClearWhiteKingsideMask = 0b1110;
12 | public const int ClearWhiteQueensideMask = 0b1101;
13 | public const int ClearBlackKingsideMask = 0b1011;
14 | public const int ClearBlackQueensideMask = 0b0111;
15 |
16 | public GameState(int capturedPieceType, int enPassantFile, int castlingRights, int fiftyMoveCounter, ulong zobristKey)
17 | {
18 | this.capturedPieceType = capturedPieceType;
19 | this.enPassantFile = enPassantFile;
20 | this.castlingRights = castlingRights;
21 | this.fiftyMoveCounter = fiftyMoveCounter;
22 | this.zobristKey = zobristKey;
23 | }
24 |
25 | public bool HasKingsideCastleRight(bool white)
26 | {
27 | int mask = white ? 1 : 4;
28 | return (castlingRights & mask) != 0;
29 | }
30 |
31 | public bool HasQueensideCastleRight(bool white)
32 | {
33 | int mask = white ? 2 : 8;
34 | return (castlingRights & mask) != 0;
35 | }
36 |
37 | }
38 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Board/Move.cs:
--------------------------------------------------------------------------------
1 | /*
2 | Compact (16bit) move representation to preserve memory during search.
3 |
4 | The format is as follows (ffffttttttssssss)
5 | Bits 0-5: start square index
6 | Bits 6-11: target square index
7 | Bits 12-15: flag (promotion type, etc)
8 | */
9 | namespace ChessChallenge.Chess
10 | {
11 | public readonly struct Move
12 | {
13 | // 16bit move value
14 | readonly ushort moveValue;
15 |
16 | // Flags
17 | public const int NoFlag = 0b0000;
18 | public const int EnPassantCaptureFlag = 0b0001;
19 | public const int CastleFlag = 0b0010;
20 | public const int PawnTwoUpFlag = 0b0011;
21 |
22 | public const int PromoteToQueenFlag = 0b0100;
23 | public const int PromoteToKnightFlag = 0b0101;
24 | public const int PromoteToRookFlag = 0b0110;
25 | public const int PromoteToBishopFlag = 0b0111;
26 |
27 | // Masks
28 | const ushort startSquareMask = 0b0000000000111111;
29 | const ushort targetSquareMask = 0b0000111111000000;
30 | const ushort flagMask = 0b1111000000000000;
31 |
32 | public Move(ushort moveValue)
33 | {
34 | this.moveValue = moveValue;
35 | }
36 |
37 | public Move(int startSquare, int targetSquare)
38 | {
39 | moveValue = (ushort)(startSquare | targetSquare << 6);
40 | }
41 |
42 | public Move(int startSquare, int targetSquare, int flag)
43 | {
44 | moveValue = (ushort)(startSquare | targetSquare << 6 | flag << 12);
45 | }
46 |
47 | public ushort Value => moveValue;
48 | public bool IsNull => moveValue == 0;
49 |
50 | public int StartSquareIndex => moveValue & startSquareMask;
51 | public int TargetSquareIndex => (moveValue & targetSquareMask) >> 6;
52 | public bool IsPromotion => MoveFlag >= PromoteToQueenFlag;
53 | public int MoveFlag => moveValue >> 12;
54 |
55 | public int PromotionPieceType
56 | {
57 | get
58 | {
59 | switch (MoveFlag)
60 | {
61 | case PromoteToRookFlag:
62 | return PieceHelper.Rook;
63 | case PromoteToKnightFlag:
64 | return PieceHelper.Knight;
65 | case PromoteToBishopFlag:
66 | return PieceHelper.Bishop;
67 | case PromoteToQueenFlag:
68 | return PieceHelper.Queen;
69 | default:
70 | return PieceHelper.None;
71 | }
72 | }
73 | }
74 |
75 | public static Move NullMove => new Move(0);
76 | public static bool SameMove(Move a, Move b) => a.moveValue == b.moveValue;
77 |
78 |
79 | }
80 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Board/PieceHelper.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | // Contains definitions for each piece type (represented as integers),
4 | // as well as various helper functions for dealing with pieces.
5 | public static class PieceHelper
6 | {
7 | // Piece Types
8 | public const int None = 0;
9 | public const int Pawn = 1;
10 | public const int Knight = 2;
11 | public const int Bishop = 3;
12 | public const int Rook = 4;
13 | public const int Queen = 5;
14 | public const int King = 6;
15 |
16 | // Piece Colours
17 | public const int White = 0;
18 | public const int Black = 8;
19 |
20 | // Pieces
21 | public const int WhitePawn = Pawn | White; // 1
22 | public const int WhiteKnight = Knight | White; // 2
23 | public const int WhiteBishop = Bishop | White; // 3
24 | public const int WhiteRook = Rook | White; // 4
25 | public const int WhiteQueen = Queen | White; // 5
26 | public const int WhiteKing = King | White; // 6
27 |
28 | public const int BlackPawn = Pawn | Black; // 9
29 | public const int BlackKnight = Knight | Black; // 10
30 | public const int BlackBishop = Bishop | Black; // 11
31 | public const int BlackRook = Rook | Black; // 12
32 | public const int BlackQueen = Queen | Black; // 13
33 | public const int BlackKing = King | Black; // 14
34 |
35 | public const int MaxPieceIndex = BlackKing;
36 |
37 | public static readonly int[] PieceIndices =
38 | {
39 | WhitePawn, WhiteKnight, WhiteBishop, WhiteRook, WhiteQueen, WhiteKing,
40 | BlackPawn, BlackKnight, BlackBishop, BlackRook, BlackQueen, BlackKing
41 | };
42 |
43 | // Bit Masks
44 | const int typeMask = 0b0111;
45 | const int colourMask = 0b1000;
46 |
47 | public static int MakePiece(int pieceType, int pieceColour) => pieceType | pieceColour;
48 |
49 | public static int MakePiece(int pieceType, bool pieceIsWhite) => MakePiece(pieceType, pieceIsWhite ? White : Black);
50 |
51 | // Returns true if given piece matches the given colour. If piece is of type 'none', result will always be false.
52 | public static bool IsColour(int piece, int colour) => (piece & colourMask) == colour && piece != 0;
53 |
54 | public static bool IsWhite(int piece) => IsColour(piece, White);
55 |
56 | public static int PieceColour(int piece) => piece & colourMask;
57 |
58 | public static int PieceType(int piece) => piece & typeMask;
59 |
60 | // Rook or Queen
61 | public static bool IsOrthogonalSlider(int piece) => PieceType(piece) is Queen or Rook;
62 |
63 | // Bishop or Queen
64 | public static bool IsDiagonalSlider(int piece) => PieceType(piece) is Queen or Bishop;
65 |
66 | // Bishop, Rook, or Queen
67 | public static bool IsSlidingPiece(int piece) => PieceType(piece) is Queen or Bishop or Rook;
68 |
69 | }
70 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Board/PieceList.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | public class PieceList
4 | {
5 |
6 | // Indices of squares occupied by given piece type (only elements up to Count are valid, the rest are unused/garbage)
7 | public int[] occupiedSquares;
8 | // Map to go from index of a square, to the index in the occupiedSquares array where that square is stored
9 | int[] map;
10 | int numPieces;
11 |
12 | public PieceList(int maxPieceCount = 16)
13 | {
14 | occupiedSquares = new int[maxPieceCount];
15 | map = new int[64];
16 | numPieces = 0;
17 | }
18 |
19 | public int Count
20 | {
21 | get
22 | {
23 | return numPieces;
24 | }
25 | }
26 |
27 | public void AddPieceAtSquare(int square)
28 | {
29 | occupiedSquares[numPieces] = square;
30 | map[square] = numPieces;
31 | numPieces++;
32 | }
33 |
34 | public void RemovePieceAtSquare(int square)
35 | {
36 | int pieceIndex = map[square]; // get the index of this element in the occupiedSquares array
37 | occupiedSquares[pieceIndex] = occupiedSquares[numPieces - 1]; // move last element in array to the place of the removed element
38 | map[occupiedSquares[pieceIndex]] = pieceIndex; // update map to point to the moved element's new location in the array
39 | numPieces--;
40 | }
41 |
42 | public void MovePiece(int startSquare, int targetSquare)
43 | {
44 | int pieceIndex = map[startSquare]; // get the index of this element in the occupiedSquares array
45 | occupiedSquares[pieceIndex] = targetSquare;
46 | map[targetSquare] = pieceIndex;
47 | }
48 |
49 | public int this[int index] => occupiedSquares[index];
50 |
51 | }
52 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Board/Zobrist.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | // Helper class for the calculation of zobrist hash.
4 | // This is a single 64bit value that (non-uniquely) represents the current state of the game.
5 |
6 | // It is mainly used for quickly detecting positions that have already been evaluated, to avoid
7 | // potentially performing lots of duplicate work during game search.
8 |
9 | public static class Zobrist
10 | {
11 | // Random numbers are generated for each aspect of the game state, and are used for calculating the hash:
12 |
13 | // piece type, colour, square index
14 | public static readonly ulong[,] piecesArray = new ulong[PieceHelper.MaxPieceIndex + 1, 64];
15 | // Each player has 4 possible castling right states: none, queenside, kingside, both.
16 | // So, taking both sides into account, there are 16 possible states.
17 | public static readonly ulong[] castlingRights = new ulong[16];
18 | // En passant file (0 = no ep).
19 | // Rank does not need to be specified since side to move is included in key
20 | public static readonly ulong[] enPassantFile = new ulong[9];
21 | public static readonly ulong sideToMove;
22 |
23 |
24 | static Zobrist()
25 | {
26 |
27 | const int seed = 29426028;
28 | System.Random rng = new System.Random(seed);
29 |
30 | for (int squareIndex = 0; squareIndex < 64; squareIndex++)
31 | {
32 | foreach (int piece in PieceHelper.PieceIndices)
33 | {
34 | piecesArray[piece, squareIndex] = RandomUnsigned64BitNumber(rng);
35 | }
36 | }
37 |
38 |
39 | for (int i = 0; i < castlingRights.Length; i++)
40 | {
41 | castlingRights[i] = RandomUnsigned64BitNumber(rng);
42 | }
43 |
44 | for (int i = 0; i < enPassantFile.Length; i++)
45 | {
46 | enPassantFile[i] = i == 0 ? 0 : RandomUnsigned64BitNumber(rng);
47 | }
48 |
49 | sideToMove = RandomUnsigned64BitNumber(rng);
50 | }
51 |
52 | // Calculate zobrist key from current board position.
53 | // NOTE: this function is slow and should only be used when the board is initially set up from fen.
54 | // During search, the key should be updated incrementally instead.
55 | public static ulong CalculateZobristKey(Board board)
56 | {
57 | ulong zobristKey = 0;
58 |
59 | for (int squareIndex = 0; squareIndex < 64; squareIndex++)
60 | {
61 | int piece = board.Square[squareIndex];
62 |
63 | if (PieceHelper.PieceType(piece) != PieceHelper.None)
64 | {
65 | zobristKey ^= piecesArray[piece, squareIndex];
66 | }
67 | }
68 |
69 | zobristKey ^= enPassantFile[board.currentGameState.enPassantFile];
70 |
71 | if (board.MoveColour == PieceHelper.Black)
72 | {
73 | zobristKey ^= sideToMove;
74 | }
75 |
76 | zobristKey ^= castlingRights[board.currentGameState.castlingRights];
77 |
78 | return zobristKey;
79 | }
80 |
81 | static ulong RandomUnsigned64BitNumber(System.Random rng)
82 | {
83 | byte[] buffer = new byte[8];
84 | rng.NextBytes(buffer);
85 | return System.BitConverter.ToUInt64(buffer, 0);
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Helpers/BoardHelper.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | public static class BoardHelper
4 | {
5 |
6 | public static readonly Coord[] RookDirections = { new Coord(-1, 0), new Coord(1, 0), new Coord(0, 1), new Coord(0, -1) };
7 | public static readonly Coord[] BishopDirections = { new Coord(-1, 1), new Coord(1, 1), new Coord(1, -1), new Coord(-1, -1) };
8 |
9 | public const string fileNames = "abcdefgh";
10 | public const string rankNames = "12345678";
11 |
12 | public const int a1 = 0;
13 | public const int b1 = 1;
14 | public const int c1 = 2;
15 | public const int d1 = 3;
16 | public const int e1 = 4;
17 | public const int f1 = 5;
18 | public const int g1 = 6;
19 | public const int h1 = 7;
20 |
21 | public const int a8 = 56;
22 | public const int b8 = 57;
23 | public const int c8 = 58;
24 | public const int d8 = 59;
25 | public const int e8 = 60;
26 | public const int f8 = 61;
27 | public const int g8 = 62;
28 | public const int h8 = 63;
29 |
30 |
31 | // Rank (0 to 7) of square
32 | public static int RankIndex(int squareIndex)
33 | {
34 | return squareIndex >> 3;
35 | }
36 |
37 | // File (0 to 7) of square
38 | public static int FileIndex(int squareIndex)
39 | {
40 | return squareIndex & 0b000111;
41 | }
42 |
43 | public static int IndexFromCoord(int fileIndex, int rankIndex)
44 | {
45 | return rankIndex * 8 + fileIndex;
46 | }
47 |
48 | public static int IndexFromCoord(Coord coord)
49 | {
50 | return IndexFromCoord(coord.fileIndex, coord.rankIndex);
51 | }
52 |
53 | public static Coord CoordFromIndex(int squareIndex)
54 | {
55 | return new Coord(FileIndex(squareIndex), RankIndex(squareIndex));
56 | }
57 |
58 | public static bool LightSquare(int fileIndex, int rankIndex)
59 | {
60 | return (fileIndex + rankIndex) % 2 != 0;
61 | }
62 |
63 | public static bool LightSquare(int squareIndex)
64 | {
65 | return LightSquare(FileIndex(squareIndex), RankIndex(squareIndex));
66 | }
67 |
68 | public static string SquareNameFromCoordinate(int fileIndex, int rankIndex)
69 | {
70 | return fileNames[fileIndex] + "" + (rankIndex + 1);
71 | }
72 |
73 | public static string SquareNameFromIndex(int squareIndex)
74 | {
75 | return SquareNameFromCoordinate(CoordFromIndex(squareIndex));
76 | }
77 |
78 | public static string SquareNameFromCoordinate(Coord coord)
79 | {
80 | return SquareNameFromCoordinate(coord.fileIndex, coord.rankIndex);
81 | }
82 |
83 | public static int SquareIndexFromName(string name)
84 | {
85 | char fileName = name[0];
86 | char rankName = name[1];
87 | int fileIndex = fileNames.IndexOf(fileName);
88 | int rankIndex = rankNames.IndexOf(rankName);
89 | return IndexFromCoord(fileIndex, rankIndex);
90 | }
91 |
92 | public static bool IsValidCoordinate(int x, int y) => x >= 0 && x < 8 && y >= 0 && y < 8;
93 |
94 | }
95 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Helpers/FenUtility.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace ChessChallenge.Chess
4 | {
5 | // Helper class for dealing with FEN strings
6 | public static class FenUtility
7 | {
8 | public const string StartPositionFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
9 |
10 | // Load position from fen string
11 | public static PositionInfo PositionFromFen(string fen)
12 | {
13 |
14 | PositionInfo loadedPositionInfo = new PositionInfo();
15 | string[] sections = fen.Split(' ');
16 |
17 | int file = 0;
18 | int rank = 7;
19 |
20 | foreach (char symbol in sections[0])
21 | {
22 | if (symbol == '/')
23 | {
24 | file = 0;
25 | rank--;
26 | }
27 | else
28 | {
29 | if (char.IsDigit(symbol))
30 | {
31 | file += (int)char.GetNumericValue(symbol);
32 | }
33 | else
34 | {
35 | int pieceColour = (char.IsUpper(symbol)) ? PieceHelper.White : PieceHelper.Black;
36 | int pieceType = char.ToLower(symbol) switch
37 | {
38 | 'k' => PieceHelper.King,
39 | 'p' => PieceHelper.Pawn,
40 | 'n' => PieceHelper.Knight,
41 | 'b' => PieceHelper.Bishop,
42 | 'r' => PieceHelper.Rook,
43 | 'q' => PieceHelper.Queen,
44 | _ => PieceHelper.None
45 | };
46 |
47 | loadedPositionInfo.squares[rank * 8 + file] = pieceType | pieceColour;
48 | file++;
49 | }
50 | }
51 | }
52 |
53 | loadedPositionInfo.whiteToMove = (sections[1] == "w");
54 |
55 | string castlingRights = sections[2];
56 | loadedPositionInfo.whiteCastleKingside = castlingRights.Contains("K");
57 | loadedPositionInfo.whiteCastleQueenside = castlingRights.Contains("Q");
58 | loadedPositionInfo.blackCastleKingside = castlingRights.Contains("k");
59 | loadedPositionInfo.blackCastleQueenside = castlingRights.Contains("q");
60 |
61 | if (sections.Length > 3)
62 | {
63 | string enPassantFileName = sections[3][0].ToString();
64 | if (BoardHelper.fileNames.Contains(enPassantFileName))
65 | {
66 | loadedPositionInfo.epFile = BoardHelper.fileNames.IndexOf(enPassantFileName) + 1;
67 | }
68 | }
69 |
70 | // Half-move clock
71 | if (sections.Length > 4)
72 | {
73 | int.TryParse(sections[4], out loadedPositionInfo.fiftyMovePlyCount);
74 | }
75 | // Full move number
76 | if (sections.Length > 5)
77 | {
78 | int.TryParse(sections[5], out loadedPositionInfo.moveCount);
79 | }
80 | return loadedPositionInfo;
81 | }
82 |
83 | ///
84 | /// Get the fen string of the current position
85 | /// When alwaysIncludeEPSquare is true the en passant square will be included
86 | /// in the fen string even if no enemy pawn is in a position to capture it.
87 | ///
88 | public static string CurrentFen(Board board, bool alwaysIncludeEPSquare = true)
89 | {
90 | string fen = "";
91 | for (int rank = 7; rank >= 0; rank--)
92 | {
93 | int numEmptyFiles = 0;
94 | for (int file = 0; file < 8; file++)
95 | {
96 | int i = rank * 8 + file;
97 | int piece = board.Square[i];
98 | if (piece != 0)
99 | {
100 | if (numEmptyFiles != 0)
101 | {
102 | fen += numEmptyFiles;
103 | numEmptyFiles = 0;
104 | }
105 | bool isBlack = PieceHelper.IsColour(piece, PieceHelper.Black);
106 | int pieceType = PieceHelper.PieceType(piece);
107 | char pieceChar = ' ';
108 | switch (pieceType)
109 | {
110 | case PieceHelper.Rook:
111 | pieceChar = 'R';
112 | break;
113 | case PieceHelper.Knight:
114 | pieceChar = 'N';
115 | break;
116 | case PieceHelper.Bishop:
117 | pieceChar = 'B';
118 | break;
119 | case PieceHelper.Queen:
120 | pieceChar = 'Q';
121 | break;
122 | case PieceHelper.King:
123 | pieceChar = 'K';
124 | break;
125 | case PieceHelper.Pawn:
126 | pieceChar = 'P';
127 | break;
128 | }
129 | fen += (isBlack) ? pieceChar.ToString().ToLower() : pieceChar.ToString();
130 | }
131 | else
132 | {
133 | numEmptyFiles++;
134 | }
135 |
136 | }
137 | if (numEmptyFiles != 0)
138 | {
139 | fen += numEmptyFiles;
140 | }
141 | if (rank != 0)
142 | {
143 | fen += '/';
144 | }
145 | }
146 |
147 | // Side to move
148 | fen += ' ';
149 | fen += (board.IsWhiteToMove) ? 'w' : 'b';
150 |
151 | // Castling
152 | bool whiteKingside = (board.currentGameState.castlingRights & 1) == 1;
153 | bool whiteQueenside = (board.currentGameState.castlingRights >> 1 & 1) == 1;
154 | bool blackKingside = (board.currentGameState.castlingRights >> 2 & 1) == 1;
155 | bool blackQueenside = (board.currentGameState.castlingRights >> 3 & 1) == 1;
156 | fen += ' ';
157 | fen += (whiteKingside) ? "K" : "";
158 | fen += (whiteQueenside) ? "Q" : "";
159 | fen += (blackKingside) ? "k" : "";
160 | fen += (blackQueenside) ? "q" : "";
161 | fen += ((board.currentGameState.castlingRights) == 0) ? "-" : "";
162 |
163 | // En-passant
164 | fen += ' ';
165 | int epFileIndex = board.currentGameState.enPassantFile - 1;
166 | int epRankIndex = (board.IsWhiteToMove) ? 5 : 2;
167 |
168 | bool isEnPassant = epFileIndex != -1;
169 | bool includeEP = alwaysIncludeEPSquare || EnPassantCanBeCaptured(epFileIndex, epRankIndex, board);
170 | if (isEnPassant && includeEP)
171 | {
172 | fen += BoardHelper.SquareNameFromCoordinate(epFileIndex, epRankIndex);
173 | }
174 | else
175 | {
176 | fen += '-';
177 | }
178 |
179 | // 50 move counter
180 | fen += ' ';
181 | fen += board.currentGameState.fiftyMoveCounter;
182 |
183 | // Full-move count (should be one at start, and increase after each move by black)
184 | fen += ' ';
185 | fen += (board.plyCount / 2) + 1;
186 |
187 | return fen;
188 | }
189 |
190 | static bool EnPassantCanBeCaptured(int epFileIndex, int epRankIndex, Board board)
191 | {
192 | Coord captureFromA = new Coord(epFileIndex - 1, epRankIndex + (board.IsWhiteToMove ? -1 : 1));
193 | Coord captureFromB = new Coord(epFileIndex + 1, epRankIndex + (board.IsWhiteToMove ? -1 : 1));
194 | int epCaptureSquare = new Coord(epFileIndex, epRankIndex).SquareIndex;
195 | int friendlyPawn = PieceHelper.MakePiece(PieceHelper.Pawn, board.MoveColour);
196 |
197 |
198 |
199 | return CanCapture(captureFromA) || CanCapture(captureFromB);
200 |
201 |
202 | bool CanCapture(Coord from)
203 | {
204 | bool isPawnOnSquare = board.Square[from.SquareIndex] == friendlyPawn;
205 | if (from.IsValidSquare() && isPawnOnSquare)
206 | {
207 | Move move = new Move(from.SquareIndex, epCaptureSquare, Move.EnPassantCaptureFlag);
208 | board.MakeMove(move);
209 | board.MakeNullMove();
210 | bool wasLegalMove = !board.CalculateInCheckState();
211 |
212 | board.UnmakeNullMove();
213 | board.UndoMove(move);
214 | return wasLegalMove;
215 | }
216 |
217 | return false;
218 | }
219 | }
220 |
221 | public static string FlipFen(string fen)
222 | {
223 | string flippedFen = "";
224 | string[] sections = fen.Split(' ');
225 |
226 | List invertedFenChars = new();
227 | string[] fenRanks = sections[0].Split('/');
228 |
229 | for (int i = fenRanks.Length - 1; i >= 0; i--)
230 | {
231 | string rank = fenRanks[i];
232 | foreach (char c in rank)
233 | {
234 | flippedFen += InvertCase(c);
235 | }
236 | if (i != 0)
237 | {
238 | flippedFen += '/';
239 | }
240 | }
241 |
242 | flippedFen += " " + (sections[1][0] == 'w' ? 'b' : 'w');
243 | string castlingRights = sections[2];
244 | string flippedRights = "";
245 | foreach (char c in "kqKQ")
246 | {
247 | if (castlingRights.Contains(c))
248 | {
249 | flippedRights += InvertCase(c);
250 | }
251 | }
252 | flippedFen += " " + (flippedRights.Length == 0 ? "-" : flippedRights);
253 |
254 | string ep = sections[3];
255 | string flippedEp = ep[0] + "";
256 | if (ep.Length > 1)
257 | {
258 | flippedEp += ep[1] == '6' ? '3' : '6';
259 | }
260 | flippedFen += " " + flippedEp;
261 | flippedFen += " " + sections[4] + " " + sections[5];
262 |
263 |
264 | return flippedFen;
265 |
266 | char InvertCase(char c)
267 | {
268 | if (char.IsLower(c))
269 | {
270 | return char.ToUpper(c);
271 | }
272 | return char.ToLower(c);
273 | }
274 | }
275 |
276 | public class PositionInfo
277 | {
278 | public int[] squares;
279 | // Castling rights
280 | public bool whiteCastleKingside;
281 | public bool whiteCastleQueenside;
282 | public bool blackCastleKingside;
283 | public bool blackCastleQueenside;
284 | // En passant file (1 is a-file, 8 is h-file, 0 means none)
285 | public int epFile;
286 | public bool whiteToMove;
287 | // Number of half-moves since last capture or pawn advance
288 | // (starts at 0 and increments after each player's move)
289 | public int fiftyMovePlyCount;
290 | // Total number of moves played in the game
291 | // (starts at 1 and increments after black's move)
292 | public int moveCount;
293 |
294 | public PositionInfo()
295 | {
296 | squares = new int[64];
297 | }
298 | }
299 | }
300 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Helpers/MoveUtility.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | // Helper class for converting between various move representations:
4 | // UCI: move represented by string, e.g. "e2e4"
5 | // SAN: move represented in standard notation e.g. "Nxe7+"
6 | // Move: internal move representation
7 | public static class MoveUtility
8 | {
9 | // Converts a moveName into internal move representation
10 | // Name is expected in UCI format: "e2e4"
11 | // Promotions can be written with or without equals sign, for example: "e7e8=q" or "e7e8q"
12 | public static Move GetMoveFromUCIName(string moveName, Board board)
13 | {
14 |
15 | int startSquare = BoardHelper.SquareIndexFromName(moveName.Substring(0, 2));
16 | int targetSquare = BoardHelper.SquareIndexFromName(moveName.Substring(2, 2));
17 |
18 | int movedPieceType = PieceHelper.PieceType(board.Square[startSquare]);
19 | Coord startCoord = new Coord(startSquare);
20 | Coord targetCoord = new Coord(targetSquare);
21 |
22 | // Figure out move flag
23 | int flag = Move.NoFlag;
24 |
25 | if (movedPieceType == PieceHelper.Pawn)
26 | {
27 | // Promotion
28 | if (moveName.Length > 4)
29 | {
30 | flag = moveName[^1] switch
31 | {
32 | 'q' => Move.PromoteToQueenFlag,
33 | 'r' => Move.PromoteToRookFlag,
34 | 'n' => Move.PromoteToKnightFlag,
35 | 'b' => Move.PromoteToBishopFlag,
36 | _ => Move.NoFlag
37 | };
38 | }
39 | // Double pawn push
40 | else if (System.Math.Abs(targetCoord.rankIndex - startCoord.rankIndex) == 2)
41 | {
42 | flag = Move.PawnTwoUpFlag;
43 | }
44 | // En-passant
45 | else if (startCoord.fileIndex != targetCoord.fileIndex && board.Square[targetSquare] == PieceHelper.None)
46 | {
47 | flag = Move.EnPassantCaptureFlag;
48 | }
49 | }
50 | else if (movedPieceType == PieceHelper.King)
51 | {
52 | if (System.Math.Abs(startCoord.fileIndex - targetCoord.fileIndex) > 1)
53 | {
54 | flag = Move.CastleFlag;
55 | }
56 | }
57 |
58 | return new Move(startSquare, targetSquare, flag);
59 | }
60 |
61 | // Get name of move in UCI format
62 | // Examples: "e2e4", "e7e8q"
63 | public static string GetMoveNameUCI(Move move)
64 | {
65 | if (move.IsNull)
66 | {
67 | return "Null";
68 | }
69 | string startSquareName = BoardHelper.SquareNameFromIndex(move.StartSquareIndex);
70 | string endSquareName = BoardHelper.SquareNameFromIndex(move.TargetSquareIndex);
71 | string moveName = startSquareName + endSquareName;
72 | if (move.IsPromotion)
73 | {
74 | switch (move.MoveFlag)
75 | {
76 | case Move.PromoteToRookFlag:
77 | moveName += "r";
78 | break;
79 | case Move.PromoteToKnightFlag:
80 | moveName += "n";
81 | break;
82 | case Move.PromoteToBishopFlag:
83 | moveName += "b";
84 | break;
85 | case Move.PromoteToQueenFlag:
86 | moveName += "q";
87 | break;
88 | }
89 | }
90 | return moveName;
91 | }
92 |
93 | // Get name of move in Standard Algebraic Notation (SAN)
94 | // Examples: "e4", "Bxf7+", "O-O", "Rh8#", "Nfd2"
95 | // Note, the move must not yet have been made on the board
96 | public static string GetMoveNameSAN(Move move, Board board)
97 | {
98 | if (move.IsNull)
99 | {
100 | return "Null";
101 | }
102 | int movePieceType = PieceHelper.PieceType(board.Square[move.StartSquareIndex]);
103 | int capturedPieceType = PieceHelper.PieceType(board.Square[move.TargetSquareIndex]);
104 |
105 | if (move.MoveFlag == Move.CastleFlag)
106 | {
107 | int delta = move.TargetSquareIndex - move.StartSquareIndex;
108 | if (delta == 2)
109 | {
110 | return "O-O";
111 | }
112 | else if (delta == -2)
113 | {
114 | return "O-O-O";
115 | }
116 | }
117 |
118 | MoveGenerator moveGen = new MoveGenerator();
119 | string moveNotation = GetSymbolFromPieceType(movePieceType);
120 |
121 | // check if any ambiguity exists in notation (e.g if e2 can be reached via Nfe2 and Nbe2)
122 | if (movePieceType != PieceHelper.Pawn && movePieceType != PieceHelper.King)
123 | {
124 | var allMoves = moveGen.GenerateMoves(board);
125 |
126 | foreach (Move altMove in allMoves)
127 | {
128 |
129 | if (altMove.StartSquareIndex != move.StartSquareIndex && altMove.TargetSquareIndex == move.TargetSquareIndex)
130 | { // if moving to same square from different square
131 | if (PieceHelper.PieceType(board.Square[altMove.StartSquareIndex]) == movePieceType)
132 | { // same piece type
133 | int fromFileIndex = BoardHelper.FileIndex(move.StartSquareIndex);
134 | int alternateFromFileIndex = BoardHelper.FileIndex(altMove.StartSquareIndex);
135 | int fromRankIndex = BoardHelper.RankIndex(move.StartSquareIndex);
136 | int alternateFromRankIndex = BoardHelper.RankIndex(altMove.StartSquareIndex);
137 |
138 | if (fromFileIndex != alternateFromFileIndex)
139 | { // pieces on different files, thus ambiguity can be resolved by specifying file
140 | moveNotation += BoardHelper.fileNames[fromFileIndex];
141 | break; // ambiguity resolved
142 | }
143 | else if (fromRankIndex != alternateFromRankIndex)
144 | {
145 | moveNotation += BoardHelper.rankNames[fromRankIndex];
146 | break; // ambiguity resolved
147 | }
148 | }
149 | }
150 |
151 | }
152 | }
153 |
154 | if (capturedPieceType != 0)
155 | { // add 'x' to indicate capture
156 | if (movePieceType == PieceHelper.Pawn)
157 | {
158 | moveNotation += BoardHelper.fileNames[BoardHelper.FileIndex(move.StartSquareIndex)];
159 | }
160 | moveNotation += "x";
161 | }
162 | else
163 | { // check if capturing ep
164 | if (move.MoveFlag == Move.EnPassantCaptureFlag)
165 | {
166 | moveNotation += BoardHelper.fileNames[BoardHelper.FileIndex(move.StartSquareIndex)] + "x";
167 | }
168 | }
169 |
170 | moveNotation += BoardHelper.fileNames[BoardHelper.FileIndex(move.TargetSquareIndex)];
171 | moveNotation += BoardHelper.rankNames[BoardHelper.RankIndex(move.TargetSquareIndex)];
172 |
173 | // add promotion piece
174 | if (move.IsPromotion)
175 | {
176 | int promotionPieceType = move.PromotionPieceType;
177 | moveNotation += "=" + GetSymbolFromPieceType(promotionPieceType);
178 | }
179 |
180 | board.MakeMove(move, inSearch: true);
181 | var legalResponses = moveGen.GenerateMoves(board);
182 | // add check/mate symbol if applicable
183 | if (moveGen.InCheck())
184 | {
185 | if (legalResponses.Length == 0)
186 | {
187 | moveNotation += "#";
188 | }
189 | else
190 | {
191 | moveNotation += "+";
192 | }
193 | }
194 | board.UndoMove(move, inSearch: true);
195 |
196 | return moveNotation;
197 |
198 | string GetSymbolFromPieceType(int pieceType)
199 | {
200 | switch (pieceType)
201 | {
202 | case PieceHelper.Rook:
203 | return "R";
204 | case PieceHelper.Knight:
205 | return "N";
206 | case PieceHelper.Bishop:
207 | return "B";
208 | case PieceHelper.Queen:
209 | return "Q";
210 | case PieceHelper.King:
211 | return "K";
212 | default:
213 | return "";
214 | }
215 | }
216 | }
217 |
218 | public static Move GetMoveFromSAN(Board board, string algebraicMove)
219 | {
220 | MoveGenerator moveGenerator = new MoveGenerator();
221 |
222 | // Remove unrequired info from move string
223 | algebraicMove = algebraicMove.Replace("+", "").Replace("#", "").Replace("x", "").Replace("-", "");
224 | var allMoves = moveGenerator.GenerateMoves(board);
225 |
226 | Move move = new Move();
227 |
228 | foreach (Move moveToTest in allMoves)
229 | {
230 | move = moveToTest;
231 |
232 | int moveFromIndex = move.StartSquareIndex;
233 | int moveToIndex = move.TargetSquareIndex;
234 | int movePieceType = PieceHelper.PieceType(board.Square[moveFromIndex]);
235 | Coord fromCoord = BoardHelper.CoordFromIndex(moveFromIndex);
236 | Coord toCoord = BoardHelper.CoordFromIndex(moveToIndex);
237 | if (algebraicMove == "OO")
238 | { // castle kingside
239 | if (movePieceType == PieceHelper.King && moveToIndex - moveFromIndex == 2)
240 | {
241 | return move;
242 | }
243 | }
244 | else if (algebraicMove == "OOO")
245 | { // castle queenside
246 | if (movePieceType == PieceHelper.King && moveToIndex - moveFromIndex == -2)
247 | {
248 | return move;
249 | }
250 | }
251 | // Is pawn move if starts with any file indicator (e.g. 'e'4. Note that uppercase B is used for bishops)
252 | else if (BoardHelper.fileNames.Contains(algebraicMove[0].ToString()))
253 | {
254 | if (movePieceType != PieceHelper.Pawn)
255 | {
256 | continue;
257 | }
258 | if (BoardHelper.fileNames.IndexOf(algebraicMove[0]) == fromCoord.fileIndex)
259 | { // correct starting file
260 | if (algebraicMove.Contains("="))
261 | { // is promotion
262 | if (toCoord.rankIndex == 0 || toCoord.rankIndex == 7)
263 | {
264 |
265 | if (algebraicMove.Length == 5) // pawn is capturing to promote
266 | {
267 | char targetFile = algebraicMove[1];
268 | if (BoardHelper.fileNames.IndexOf(targetFile) != toCoord.fileIndex)
269 | {
270 | // Skip if not moving to correct file
271 | continue;
272 | }
273 | }
274 | char promotionChar = algebraicMove[algebraicMove.Length - 1];
275 |
276 | if (move.PromotionPieceType != GetPieceTypeFromSymbol(promotionChar))
277 | {
278 | continue; // skip this move, incorrect promotion type
279 | }
280 |
281 | return move;
282 | }
283 | }
284 | else
285 | {
286 |
287 | char targetFile = algebraicMove[algebraicMove.Length - 2];
288 | char targetRank = algebraicMove[algebraicMove.Length - 1];
289 |
290 | if (BoardHelper.fileNames.IndexOf(targetFile) == toCoord.fileIndex)
291 | { // correct ending file
292 | if (targetRank.ToString() == (toCoord.rankIndex + 1).ToString())
293 | { // correct ending rank
294 | break;
295 | }
296 | }
297 | }
298 | }
299 | }
300 | else
301 | { // regular piece move
302 |
303 | char movePieceChar = algebraicMove[0];
304 | if (GetPieceTypeFromSymbol(movePieceChar) != movePieceType)
305 | {
306 | continue; // skip this move, incorrect move piece type
307 | }
308 |
309 | char targetFile = algebraicMove[algebraicMove.Length - 2];
310 | char targetRank = algebraicMove[algebraicMove.Length - 1];
311 | if (BoardHelper.fileNames.IndexOf(targetFile) == toCoord.fileIndex)
312 | { // correct ending file
313 | if (targetRank.ToString() == (toCoord.rankIndex + 1).ToString())
314 | { // correct ending rank
315 |
316 | if (algebraicMove.Length == 4)
317 | { // addition char present for disambiguation (e.g. Nbd7 or R7e2)
318 | char disambiguationChar = algebraicMove[1];
319 |
320 | if (BoardHelper.fileNames.Contains(disambiguationChar.ToString()))
321 | { // is file disambiguation
322 | if (BoardHelper.fileNames.IndexOf(disambiguationChar) != fromCoord.fileIndex)
323 | { // incorrect starting file
324 | continue;
325 | }
326 | }
327 | else
328 | { // is rank disambiguation
329 | if (disambiguationChar.ToString() != (fromCoord.rankIndex + 1).ToString())
330 | { // incorrect starting rank
331 | continue;
332 | }
333 |
334 | }
335 | }
336 | break;
337 | }
338 | }
339 | }
340 | }
341 | return move;
342 |
343 | int GetPieceTypeFromSymbol(char symbol)
344 | {
345 | switch (symbol)
346 | {
347 | case 'R':
348 | return PieceHelper.Rook;
349 | case 'N':
350 | return PieceHelper.Knight;
351 | case 'B':
352 | return PieceHelper.Bishop;
353 | case 'Q':
354 | return PieceHelper.Queen;
355 | case 'K':
356 | return PieceHelper.King;
357 | default:
358 | return PieceHelper.None;
359 | }
360 | }
361 | }
362 |
363 | }
364 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Helpers/PGNCreator.cs:
--------------------------------------------------------------------------------
1 |
2 | using System.Text;
3 |
4 | namespace ChessChallenge.Chess
5 | {
6 |
7 | public static class PGNCreator
8 | {
9 |
10 | public static string CreatePGN(Move[] moves)
11 | {
12 | return CreatePGN(moves, GameResult.InProgress, FenUtility.StartPositionFEN);
13 | }
14 |
15 | public static string CreatePGN(Board board, GameResult result, string whiteName = "", string blackName = "")
16 | {
17 | return CreatePGN(board.AllGameMoves.ToArray(), result, board.GameStartFen, whiteName, blackName);
18 | }
19 |
20 | public static string CreatePGN(Move[] moves, GameResult result, string startFen, string whiteName = "", string blackName = "")
21 | {
22 | startFen = startFen.Replace("\n", "").Replace("\r", "");
23 |
24 | StringBuilder pgn = new();
25 | Board board = new Board();
26 | board.LoadPosition(startFen);
27 | // Headers
28 | if (!string.IsNullOrEmpty(whiteName))
29 | {
30 | pgn.AppendLine($"[White \"{whiteName}\"]");
31 | }
32 | if (!string.IsNullOrEmpty(blackName))
33 | {
34 | pgn.AppendLine($"[Black \"{blackName}\"]");
35 | }
36 |
37 | if (startFen != FenUtility.StartPositionFEN)
38 | {
39 | pgn.AppendLine($"[FEN \"{startFen}\"]");
40 | }
41 | if (result is not GameResult.NotStarted or GameResult.InProgress)
42 | {
43 | pgn.AppendLine($"[Result \"{result}\"]");
44 | }
45 |
46 | for (int plyCount = 0; plyCount < moves.Length; plyCount++)
47 | {
48 | string moveString = MoveUtility.GetMoveNameSAN(moves[plyCount], board);
49 | board.MakeMove(moves[plyCount]);
50 |
51 | if (plyCount % 2 == 0)
52 | {
53 | pgn.Append((plyCount / 2 + 1) + ". ");
54 | }
55 | pgn.Append(moveString + " ");
56 | }
57 |
58 | return pgn.ToString();
59 | }
60 |
61 | }
62 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Helpers/PGNLoader.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace ChessChallenge.Chess
4 | {
5 | public static class PGNLoader
6 | {
7 |
8 | public static Move[] MovesFromPGN(string pgn, int maxPlyCount = int.MaxValue)
9 | {
10 | List algebraicMoves = new List();
11 |
12 | string[] entries = pgn.Replace("\n", " ").Split(' ');
13 | for (int i = 0; i < entries.Length; i++)
14 | {
15 | // Reached move limit, so exit.
16 | // (This is used for example when creating book, where only interested in first n moves of game)
17 | if (algebraicMoves.Count == maxPlyCount)
18 | {
19 | break;
20 | }
21 |
22 | string entry = entries[i].Trim();
23 |
24 | if (entry.Contains(".") || entry == "1/2-1/2" || entry == "1-0" || entry == "0-1")
25 | {
26 | continue;
27 | }
28 |
29 | if (!string.IsNullOrEmpty(entry))
30 | {
31 | algebraicMoves.Add(entry);
32 | }
33 | }
34 |
35 | return MovesFromAlgebraic(algebraicMoves.ToArray());
36 | }
37 |
38 | static Move[] MovesFromAlgebraic(string[] algebraicMoves)
39 | {
40 | Board board = new Board();
41 | board.LoadStartPosition();
42 | var moves = new List();
43 |
44 | for (int i = 0; i < algebraicMoves.Length; i++)
45 | {
46 | Move move = MoveUtility.GetMoveFromSAN(board, algebraicMoves[i].Trim());
47 | if (move.IsNull)
48 | { // move is illegal; discard and return moves up to this point
49 | string pgn = "";
50 | foreach (string s in algebraicMoves)
51 | {
52 | pgn += s + " ";
53 | }
54 | moves.ToArray();
55 | }
56 | else
57 | {
58 | moves.Add(move);
59 | }
60 | board.MakeMove(move);
61 | }
62 | return moves.ToArray();
63 | }
64 |
65 | }
66 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Move Generation/Bitboards/BitBoardUtility.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | using System.Numerics;
4 |
5 | public static class BitBoardUtility
6 | {
7 |
8 | // Get index of least significant set bit in given 64bit value. Also clears the bit to zero.
9 | public static int PopLSB(ref ulong b)
10 | {
11 | int i = BitOperations.TrailingZeroCount(b);
12 | b &= (b - 1);
13 | return i;
14 | }
15 |
16 | public static int PopCount(ulong x)
17 | {
18 | return BitOperations.PopCount(x);
19 | }
20 |
21 | public static void SetSquare(ref ulong bitboard, int squareIndex)
22 | {
23 | bitboard |= 1ul << squareIndex;
24 | }
25 |
26 | public static void ClearSquare(ref ulong bitboard, int squareIndex)
27 | {
28 | bitboard &= ~(1ul << squareIndex);
29 | }
30 |
31 |
32 | public static void ToggleSquare(ref ulong bitboard, int squareIndex)
33 | {
34 | bitboard ^= 1ul << squareIndex;
35 | }
36 |
37 | public static void ToggleSquares(ref ulong bitboard, int squareA, int squareB)
38 | {
39 | bitboard ^= (1ul << squareA | 1ul << squareB);
40 | }
41 |
42 | public static bool ContainsSquare(ulong bitboard, int square)
43 | {
44 | return ((bitboard >> square) & 1) != 0;
45 | }
46 |
47 | public static ulong PawnAttacks(ulong pawnBitboard, bool isWhite)
48 | {
49 | // Pawn attacks are calculated like so: (example given with white to move)
50 |
51 | // The first half of the attacks are calculated by shifting all pawns north-east: northEastAttacks = pawnBitboard << 9
52 | // Note that pawns on the h file will be wrapped around to the a file, so then mask out the a file: northEastAttacks &= notAFile
53 | // (Any pawns that were originally on the a file will have been shifted to the b file, so a file should be empty).
54 |
55 | // The other half of the attacks are calculated by shifting all pawns north-west. This time the h file must be masked out.
56 | // Combine the two halves to get a bitboard with all the pawn attacks: northEastAttacks | northWestAttacks
57 |
58 | if (isWhite)
59 | {
60 | return ((pawnBitboard << 9) & Bits.NotAFile) | ((pawnBitboard << 7) & Bits.NotHFile);
61 | }
62 |
63 | return ((pawnBitboard >> 7) & Bits.NotAFile) | ((pawnBitboard >> 9) & Bits.NotHFile);
64 | }
65 |
66 |
67 | public static ulong Shift(ulong bitboard, int numSquaresToShift)
68 | {
69 | if (numSquaresToShift > 0)
70 | {
71 | return bitboard << numSquaresToShift;
72 | }
73 | else
74 | {
75 | return bitboard >> -numSquaresToShift;
76 | }
77 |
78 | }
79 |
80 | public static ulong ProtectedPawns(ulong pawns, bool isWhite)
81 | {
82 | ulong attacks = PawnAttacks(pawns, isWhite);
83 | return attacks & pawns;
84 | }
85 |
86 | public static ulong LockedPawns(ulong whitePawns, ulong blackPawns)
87 | {
88 | ulong pushUp = whitePawns << 8;
89 | ulong pushDown = blackPawns >> 8;
90 | return (whitePawns & pushDown) | (blackPawns & pushUp);
91 | }
92 |
93 |
94 | static BitBoardUtility()
95 | {
96 |
97 | }
98 |
99 |
100 | }
101 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Move Generation/Bitboards/Bits.cs:
--------------------------------------------------------------------------------
1 | using static System.Math;
2 |
3 | namespace ChessChallenge.Chess
4 | {
5 | // A collection of precomputed bitboards for use during movegen, search, etc.
6 | public static class Bits
7 | {
8 | public const ulong FileA = 0x101010101010101;
9 | public const ulong FileH = FileA << 7;
10 | public const ulong NotAFile = ~FileA;
11 | public const ulong NotHFile = ~FileH;
12 |
13 | public const ulong Rank1 = 0b11111111;
14 | public const ulong Rank2 = Rank1 << (8 * 1);
15 | public const ulong Rank3 = Rank1 << (8 * 2);
16 | public const ulong Rank4 = Rank1 << (8 * 3);
17 | public const ulong Rank5 = Rank1 << (8 * 4);
18 | public const ulong Rank6 = Rank1 << (8 * 5);
19 | public const ulong Rank7 = Rank1 << (8 * 6);
20 | public const ulong Rank8 = Rank1 << (8 * 7);
21 |
22 | public const ulong WhiteKingsideMask = 1ul << BoardHelper.f1 | 1ul << BoardHelper.g1;
23 | public const ulong BlackKingsideMask = 1ul << BoardHelper.f8 | 1ul << BoardHelper.g8;
24 |
25 | public const ulong WhiteQueensideMask2 = 1ul << BoardHelper.d1 | 1ul << BoardHelper.c1;
26 | public const ulong BlackQueensideMask2 = 1ul << BoardHelper.d8 | 1ul << BoardHelper.c8;
27 |
28 | public const ulong WhiteQueensideMask = WhiteQueensideMask2 | 1ul << BoardHelper.b1;
29 | public const ulong BlackQueensideMask = BlackQueensideMask2 | 1ul << BoardHelper.b8;
30 |
31 | public static readonly ulong[] WhitePassedPawnMask;
32 | public static readonly ulong[] BlackPassedPawnMask;
33 |
34 | // A pawn on 'e4' for example, is considered supported by any pawn on
35 | // squares: d3, d4, f3, f4
36 | public static readonly ulong[] WhitePawnSupportMask;
37 | public static readonly ulong[] BlackPawnSupportMask;
38 |
39 | public static readonly ulong[] FileMask;
40 | public static readonly ulong[] AdjacentFileMasks;
41 |
42 | // 3x3 mask (except along edges of course)
43 | public static readonly ulong[] KingSafetyMask;
44 |
45 | // Mask of 'forward' square. For example, from e4 the forward squares for white are: [e5, e6, e7, e8]
46 | public static readonly ulong[] WhiteForwardFileMask;
47 | public static readonly ulong[] BlackForwardFileMask;
48 |
49 | // Mask of three consecutive files centred at given file index.
50 | // For example, given file '3', the mask would contains files [2,3,4].
51 | // Note that for edge files, such as file 0, it would contain files [0,1,2]
52 | public static readonly ulong[] TripleFileMask;
53 |
54 |
55 | public static readonly ulong[] KnightAttacks;
56 | public static readonly ulong[] KingMoves;
57 | public static readonly ulong[] WhitePawnAttacks;
58 | public static readonly ulong[] BlackPawnAttacks;
59 |
60 |
61 | static Bits()
62 | {
63 | FileMask = new ulong[8];
64 | AdjacentFileMasks = new ulong[8];
65 |
66 | for (int i = 0; i < 8; i++)
67 | {
68 | FileMask[i] = FileA << i;
69 | ulong left = i > 0 ? FileA << (i - 1) : 0;
70 | ulong right = i < 7 ? FileA << (i + 1) : 0;
71 | AdjacentFileMasks[i] = left | right;
72 | }
73 |
74 | TripleFileMask = new ulong[8];
75 | for (int i = 0; i < 8; i++)
76 | {
77 | int clampedFile = System.Math.Clamp(i, 1, 6);
78 | TripleFileMask[i] = FileMask[clampedFile] | AdjacentFileMasks[clampedFile];
79 | }
80 |
81 | WhitePassedPawnMask = new ulong[64];
82 | BlackPassedPawnMask = new ulong[64];
83 | WhitePawnSupportMask = new ulong[64];
84 | BlackPawnSupportMask = new ulong[64];
85 | WhiteForwardFileMask = new ulong[64];
86 | BlackForwardFileMask = new ulong[64];
87 |
88 | for (int square = 0; square < 64; square++)
89 | {
90 | int file = BoardHelper.FileIndex(square);
91 | int rank = BoardHelper.RankIndex(square);
92 | ulong adjacentFiles = FileA << Max(0, file - 1) | FileA << Min(7, file + 1);
93 | // Passed pawn mask
94 | ulong whiteForwardMask = ~(ulong.MaxValue >> (64 - 8 * (rank + 1)));
95 | ulong blackForwardMask = ((1ul << 8 * rank) - 1);
96 |
97 | WhitePassedPawnMask[square] = (FileA << file | adjacentFiles) & whiteForwardMask;
98 | BlackPassedPawnMask[square] = (FileA << file | adjacentFiles) & blackForwardMask;
99 | // Pawn support mask
100 | ulong adjacent = (1ul << (square - 1) | 1ul << (square + 1)) & adjacentFiles;
101 | WhitePawnSupportMask[square] = adjacent | BitBoardUtility.Shift(adjacent, -8);
102 | BlackPawnSupportMask[square] = adjacent | BitBoardUtility.Shift(adjacent, +8);
103 |
104 | WhiteForwardFileMask[square] = whiteForwardMask & FileMask[file];
105 | BlackForwardFileMask[square] = blackForwardMask & FileMask[file];
106 | }
107 |
108 |
109 |
110 |
111 | KnightAttacks = new ulong[64];
112 | KingMoves = new ulong[64];
113 | WhitePawnAttacks = new ulong[64];
114 | BlackPawnAttacks = new ulong[64];
115 |
116 | (int x, int y)[] orthoDir = { (-1, 0), (0, 1), (1, 0), (0, -1) };
117 | (int x, int y)[] diagDir = { (-1, -1), (-1, 1), (1, 1), (1, -1) };
118 | (int x, int y)[] knightJumps = { (-2, -1), (-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (1, -2), (-1, -2) };
119 |
120 | for (int y = 0; y < 8; y++)
121 | {
122 | for (int x = 0; x < 8; x++)
123 | {
124 | ProcessSquare(x, y);
125 | }
126 | }
127 |
128 |
129 | KingSafetyMask = new ulong[64];
130 | for (int i = 0; i < 64; i++)
131 | {
132 | KingSafetyMask[i] = KingMoves[i] | (1ul << i);
133 | }
134 |
135 | void ProcessSquare(int x, int y)
136 | {
137 | int squareIndex = y * 8 + x;
138 |
139 | for (int dirIndex = 0; dirIndex < 4; dirIndex++)
140 | {
141 | // Orthogonal and diagonal directions
142 | for (int dst = 1; dst < 8; dst++)
143 | {
144 | int orthoX = x + orthoDir[dirIndex].x * dst;
145 | int orthoY = y + orthoDir[dirIndex].y * dst;
146 | int diagX = x + diagDir[dirIndex].x * dst;
147 | int diagY = y + diagDir[dirIndex].y * dst;
148 |
149 | if (ValidSquareIndex(orthoX, orthoY, out int orthoTargetIndex))
150 | {
151 | if (dst == 1)
152 | {
153 | KingMoves[squareIndex] |= 1ul << orthoTargetIndex;
154 | }
155 | }
156 |
157 | if (ValidSquareIndex(diagX, diagY, out int diagTargetIndex))
158 | {
159 | if (dst == 1)
160 | {
161 | KingMoves[squareIndex] |= 1ul << diagTargetIndex;
162 | }
163 | }
164 | }
165 |
166 | // Knight jumps
167 | for (int i = 0; i < knightJumps.Length; i++)
168 | {
169 | int knightX = x + knightJumps[i].x;
170 | int knightY = y + knightJumps[i].y;
171 | if (ValidSquareIndex(knightX, knightY, out int knightTargetSquare))
172 | {
173 | KnightAttacks[squareIndex] |= 1ul << knightTargetSquare;
174 | }
175 | }
176 |
177 | // Pawn attacks
178 |
179 | if (ValidSquareIndex(x + 1, y + 1, out int whitePawnRight))
180 | {
181 | WhitePawnAttacks[squareIndex] |= 1ul << whitePawnRight;
182 | }
183 | if (ValidSquareIndex(x - 1, y + 1, out int whitePawnLeft))
184 | {
185 | WhitePawnAttacks[squareIndex] |= 1ul << whitePawnLeft;
186 | }
187 |
188 |
189 | if (ValidSquareIndex(x + 1, y - 1, out int blackPawnAttackRight))
190 | {
191 | BlackPawnAttacks[squareIndex] |= 1ul << blackPawnAttackRight;
192 | }
193 | if (ValidSquareIndex(x - 1, y - 1, out int blackPawnAttackLeft))
194 | {
195 | BlackPawnAttacks[squareIndex] |= 1ul << blackPawnAttackLeft;
196 | }
197 |
198 |
199 | }
200 |
201 | }
202 |
203 | bool ValidSquareIndex(int x, int y, out int index)
204 | {
205 | index = y * 8 + x;
206 | return x >= 0 && x < 8 && y >= 0 && y < 8;
207 | }
208 |
209 | }
210 | }
211 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Move Generation/Magics/Magic.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | using static PrecomputedMagics;
4 |
5 | // Helper class for magic bitboards.
6 | // This is a technique where bishop and rook moves are precomputed
7 | // for any configuration of origin square and blocking pieces.
8 | public static class Magic
9 | {
10 | // Rook and bishop mask bitboards for each origin square.
11 | // A mask is simply the legal moves available to the piece from the origin square
12 | // (on an empty board), except that the moves stop 1 square before the edge of the board.
13 | public static readonly ulong[] RookMask;
14 | public static readonly ulong[] BishopMask;
15 |
16 | public static readonly ulong[][] RookAttacks;
17 | public static readonly ulong[][] BishopAttacks;
18 |
19 |
20 | public static ulong GetSliderAttacks(int square, ulong blockers, bool ortho)
21 | {
22 | return ortho ? GetRookAttacks(square, blockers) : GetBishopAttacks(square, blockers);
23 | }
24 |
25 | public static ulong GetRookAttacks(int square, ulong blockers)
26 | {
27 | ulong key = ((blockers & RookMask[square]) * RookMagics[square]) >> RookShifts[square];
28 | return RookAttacks[square][key];
29 | }
30 |
31 | public static ulong GetBishopAttacks(int square, ulong blockers)
32 | {
33 | ulong key = ((blockers & BishopMask[square]) * BishopMagics[square]) >> BishopShifts[square];
34 | return BishopAttacks[square][key];
35 | }
36 |
37 |
38 | static Magic()
39 | {
40 | RookMask = new ulong[64];
41 | BishopMask = new ulong[64];
42 |
43 | for (int squareIndex = 0; squareIndex < 64; squareIndex++)
44 | {
45 | RookMask[squareIndex] = MagicHelper.CreateMovementMask(squareIndex, true);
46 | BishopMask[squareIndex] = MagicHelper.CreateMovementMask(squareIndex, false);
47 | }
48 |
49 | RookAttacks = new ulong[64][];
50 | BishopAttacks = new ulong[64][];
51 |
52 | for (int i = 0; i < 64; i++)
53 | {
54 | RookAttacks[i] = CreateTable(i, true, RookMagics[i], RookShifts[i]);
55 | BishopAttacks[i] = CreateTable(i, false, BishopMagics[i], BishopShifts[i]);
56 | }
57 |
58 | ulong[] CreateTable(int square, bool rook, ulong magic, int leftShift)
59 | {
60 | int numBits = 64 - leftShift;
61 | int lookupSize = 1 << numBits;
62 | ulong[] table = new ulong[lookupSize];
63 |
64 | ulong movementMask = MagicHelper.CreateMovementMask(square, rook);
65 | ulong[] blockerPatterns = MagicHelper.CreateAllBlockerBitboards(movementMask);
66 |
67 | foreach (ulong pattern in blockerPatterns)
68 | {
69 | ulong index = (pattern * magic) >> leftShift;
70 | ulong moves = MagicHelper.LegalMoveBitboardFromBlockers(square, pattern, rook);
71 | table[index] = moves;
72 | }
73 |
74 | return table;
75 | }
76 | }
77 |
78 | }
79 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Move Generation/Magics/MagicHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace ChessChallenge.Chess
4 | {
5 | public static class MagicHelper
6 | {
7 | public static ulong[] CreateAllBlockerBitboards(ulong movementMask)
8 | {
9 | // Create a list of the indices of the bits that are set in the movement mask
10 | List moveSquareIndices = new();
11 | for (int i = 0; i < 64; i++)
12 | {
13 | if (((movementMask >> i) & 1) == 1)
14 | {
15 | moveSquareIndices.Add(i);
16 | }
17 | }
18 |
19 | // Calculate total number of different bitboards (one for each possible arrangement of pieces)
20 | int numPatterns = 1 << moveSquareIndices.Count; // 2^n
21 | ulong[] blockerBitboards = new ulong[numPatterns];
22 |
23 | // Create all bitboards
24 | for (int patternIndex = 0; patternIndex < numPatterns; patternIndex++)
25 | {
26 | for (int bitIndex = 0; bitIndex < moveSquareIndices.Count; bitIndex++)
27 | {
28 | int bit = (patternIndex >> bitIndex) & 1;
29 | blockerBitboards[patternIndex] |= (ulong)bit << moveSquareIndices[bitIndex];
30 | }
31 | }
32 |
33 | return blockerBitboards;
34 | }
35 |
36 |
37 | public static ulong CreateMovementMask(int squareIndex, bool ortho)
38 | {
39 | ulong mask = 0;
40 | Coord[] directions = ortho ? BoardHelper.RookDirections : BoardHelper.BishopDirections;
41 | Coord startCoord = new Coord(squareIndex);
42 |
43 | foreach (Coord dir in directions)
44 | {
45 | for (int dst = 1; dst < 8; dst++)
46 | {
47 | Coord coord = startCoord + dir * dst;
48 | Coord nextCoord = startCoord + dir * (dst + 1);
49 |
50 | if (nextCoord.IsValidSquare())
51 | {
52 | BitBoardUtility.SetSquare(ref mask, coord.SquareIndex);
53 | }
54 | else { break; }
55 | }
56 | }
57 | return mask;
58 | }
59 |
60 | public static ulong LegalMoveBitboardFromBlockers(int startSquare, ulong blockerBitboard, bool ortho)
61 | {
62 | ulong bitboard = 0;
63 |
64 | Coord[] directions = ortho ? BoardHelper.RookDirections : BoardHelper.BishopDirections;
65 | Coord startCoord = new Coord(startSquare);
66 |
67 | foreach (Coord dir in directions)
68 | {
69 | for (int dst = 1; dst < 8; dst++)
70 | {
71 | Coord coord = startCoord + dir * dst;
72 |
73 | if (coord.IsValidSquare())
74 | {
75 | BitBoardUtility.SetSquare(ref bitboard, coord.SquareIndex);
76 | if (BitBoardUtility.ContainsSquare(blockerBitboard, coord.SquareIndex))
77 | {
78 | break;
79 | }
80 | }
81 | else { break; }
82 | }
83 | }
84 |
85 | return bitboard;
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Move Generation/Magics/PrecomputedMagics.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | public static class PrecomputedMagics
4 | {
5 | public static readonly int[] RookShifts = { 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 53, 54, 53, 53, 54, 53, 53, 54, 54, 54, 53, 53, 54, 53, 53, 54, 53, 53, 54, 54, 54, 53, 52, 54, 53, 53, 53, 53, 54, 53, 52, 53, 54, 54, 53, 53, 54, 53, 53, 54, 54, 54, 53, 53, 54, 53, 52, 53, 53, 53, 53, 53, 53, 52 };
6 | public static readonly int[] BishopShifts = { 58, 60, 59, 59, 59, 59, 60, 58, 60, 59, 59, 59, 59, 59, 59, 60, 59, 59, 57, 57, 57, 57, 59, 59, 59, 59, 57, 55, 55, 57, 59, 59, 59, 59, 57, 55, 55, 57, 59, 59, 59, 59, 57, 57, 57, 57, 59, 59, 60, 60, 59, 59, 59, 59, 60, 60, 58, 60, 59, 59, 59, 59, 59, 58 };
7 |
8 | public static readonly ulong[] RookMagics = { 468374916371625120, 18428729537625841661, 2531023729696186408, 6093370314119450896, 13830552789156493815, 16134110446239088507, 12677615322350354425, 5404321144167858432, 2111097758984580, 18428720740584907710, 17293734603602787839, 4938760079889530922, 7699325603589095390, 9078693890218258431, 578149610753690728, 9496543503900033792, 1155209038552629657, 9224076274589515780, 1835781998207181184, 509120063316431138, 16634043024132535807, 18446673631917146111, 9623686630121410312, 4648737361302392899, 738591182849868645, 1732936432546219272, 2400543327507449856, 5188164365601475096, 10414575345181196316, 1162492212166789136, 9396848738060210946, 622413200109881612, 7998357718131801918, 7719627227008073923, 16181433497662382080, 18441958655457754079, 1267153596645440, 18446726464209379263, 1214021438038606600, 4650128814733526084, 9656144899867951104, 18444421868610287615, 3695311799139303489, 10597006226145476632, 18436046904206950398, 18446726472933277663, 3458977943764860944, 39125045590687766, 9227453435446560384, 6476955465732358656, 1270314852531077632, 2882448553461416064, 11547238928203796481, 1856618300822323264, 2573991788166144, 4936544992551831040, 13690941749405253631, 15852669863439351807, 18302628748190527413, 12682135449552027479, 13830554446930287982, 18302628782487371519, 7924083509981736956, 4734295326018586370 };
9 | public static readonly ulong[] BishopMagics = { 16509839532542417919, 14391803910955204223, 1848771770702627364, 347925068195328958, 5189277761285652493, 3750937732777063343, 18429848470517967340, 17870072066711748607, 16715520087474960373, 2459353627279607168, 7061705824611107232, 8089129053103260512, 7414579821471224013, 9520647030890121554, 17142940634164625405, 9187037984654475102, 4933695867036173873, 3035992416931960321, 15052160563071165696, 5876081268917084809, 1153484746652717320, 6365855841584713735, 2463646859659644933, 1453259901463176960, 9808859429721908488, 2829141021535244552, 576619101540319252, 5804014844877275314, 4774660099383771136, 328785038479458864, 2360590652863023124, 569550314443282, 17563974527758635567, 11698101887533589556, 5764964460729992192, 6953579832080335136, 1318441160687747328, 8090717009753444376, 16751172641200572929, 5558033503209157252, 17100156536247493656, 7899286223048400564, 4845135427956654145, 2368485888099072, 2399033289953272320, 6976678428284034058, 3134241565013966284, 8661609558376259840, 17275805361393991679, 15391050065516657151, 11529206229534274423, 9876416274250600448, 16432792402597134585, 11975705497012863580, 11457135419348969979, 9763749252098620046, 16960553411078512574, 15563877356819111679, 14994736884583272463, 9441297368950544394, 14537646123432199168, 9888547162215157388, 18140215579194907366, 18374682062228545019 };
10 | }
11 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Move Generation/PrecomputedMoveData.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | using System.Collections.Generic;
4 | using static System.Math;
5 |
6 | public static class PrecomputedMoveData
7 | {
8 |
9 |
10 | public static readonly ulong[,] alignMask;
11 | public static readonly ulong[,] dirRayMask;
12 |
13 | // First 4 are orthogonal, last 4 are diagonals (N, S, W, E, NW, SE, NE, SW)
14 | public static readonly int[] directionOffsets = { 8, -8, -1, 1, 7, -7, 9, -9 };
15 |
16 | static readonly Coord[] dirOffsets2D =
17 | {
18 | new Coord(0, 1),
19 | new Coord(0, -1),
20 | new Coord(-1, 0),
21 | new Coord(1, 0),
22 | new Coord(-1, 1),
23 | new Coord(1, -1),
24 | new Coord(1, 1),
25 | new Coord(-1, -1)
26 | };
27 |
28 |
29 | // Stores number of moves available in each of the 8 directions for every square on the board
30 | // Order of directions is: N, S, W, E, NW, SE, NE, SW
31 | // So for example, if availableSquares[0][1] == 7...
32 | // that means that there are 7 squares to the north of b1 (the square with index 1 in board array)
33 | public static readonly int[][] numSquaresToEdge;
34 |
35 | // Stores array of indices for each square a knight can land on from any square on the board
36 | // So for example, knightMoves[0] is equal to {10, 17}, meaning a knight on a1 can jump to c2 and b3
37 | public static readonly byte[][] knightMoves;
38 | public static readonly byte[][] kingMoves;
39 |
40 | // Pawn attack directions for white and black (NW, NE; SW SE)
41 | public static readonly byte[][] pawnAttackDirections = {
42 | new byte[] { 4, 6 },
43 | new byte[] { 7, 5 }
44 | };
45 |
46 | public static readonly int[][] pawnAttacksWhite;
47 | public static readonly int[][] pawnAttacksBlack;
48 | public static readonly int[] directionLookup;
49 |
50 | public static readonly ulong[] kingAttackBitboards;
51 | public static readonly ulong[] knightAttackBitboards;
52 | public static readonly ulong[][] pawnAttackBitboards;
53 |
54 | public static readonly ulong[] rookMoves;
55 | public static readonly ulong[] bishopMoves;
56 | public static readonly ulong[] queenMoves;
57 |
58 | // Aka manhattan distance (answers how many moves for a rook to get from square a to square b)
59 | public static int[,] OrthogonalDistance;
60 | // Aka chebyshev distance (answers how many moves for a king to get from square a to square b)
61 | public static int[,] kingDistance;
62 | public static int[] CentreManhattanDistance;
63 |
64 | public static int NumRookMovesToReachSquare(int startSquare, int targetSquare)
65 | {
66 | return OrthogonalDistance[startSquare, targetSquare];
67 | }
68 |
69 | public static int NumKingMovesToReachSquare(int startSquare, int targetSquare)
70 | {
71 | return kingDistance[startSquare, targetSquare];
72 | }
73 |
74 | // Initialize lookup data
75 | static PrecomputedMoveData()
76 | {
77 | pawnAttacksWhite = new int[64][];
78 | pawnAttacksBlack = new int[64][];
79 | numSquaresToEdge = new int[8][];
80 | knightMoves = new byte[64][];
81 | kingMoves = new byte[64][];
82 | numSquaresToEdge = new int[64][];
83 |
84 | rookMoves = new ulong[64];
85 | bishopMoves = new ulong[64];
86 | queenMoves = new ulong[64];
87 |
88 | // Calculate knight jumps and available squares for each square on the board.
89 | // See comments by variable definitions for more info.
90 | int[] allKnightJumps = { 15, 17, -17, -15, 10, -6, 6, -10 };
91 | knightAttackBitboards = new ulong[64];
92 | kingAttackBitboards = new ulong[64];
93 | pawnAttackBitboards = new ulong[64][];
94 |
95 | for (int squareIndex = 0; squareIndex < 64; squareIndex++)
96 | {
97 |
98 | int y = squareIndex / 8;
99 | int x = squareIndex - y * 8;
100 |
101 | int north = 7 - y;
102 | int south = y;
103 | int west = x;
104 | int east = 7 - x;
105 | numSquaresToEdge[squareIndex] = new int[8];
106 | numSquaresToEdge[squareIndex][0] = north;
107 | numSquaresToEdge[squareIndex][1] = south;
108 | numSquaresToEdge[squareIndex][2] = west;
109 | numSquaresToEdge[squareIndex][3] = east;
110 | numSquaresToEdge[squareIndex][4] = System.Math.Min(north, west);
111 | numSquaresToEdge[squareIndex][5] = System.Math.Min(south, east);
112 | numSquaresToEdge[squareIndex][6] = System.Math.Min(north, east);
113 | numSquaresToEdge[squareIndex][7] = System.Math.Min(south, west);
114 |
115 | // Calculate all squares knight can jump to from current square
116 | var legalKnightJumps = new List();
117 | ulong knightBitboard = 0;
118 | foreach (int knightJumpDelta in allKnightJumps)
119 | {
120 | int knightJumpSquare = squareIndex + knightJumpDelta;
121 | if (knightJumpSquare >= 0 && knightJumpSquare < 64)
122 | {
123 | int knightSquareY = knightJumpSquare / 8;
124 | int knightSquareX = knightJumpSquare - knightSquareY * 8;
125 | // Ensure knight has moved max of 2 squares on x/y axis (to reject indices that have wrapped around side of board)
126 | int maxCoordMoveDst = System.Math.Max(System.Math.Abs(x - knightSquareX), System.Math.Abs(y - knightSquareY));
127 | if (maxCoordMoveDst == 2)
128 | {
129 | legalKnightJumps.Add((byte)knightJumpSquare);
130 | knightBitboard |= 1ul << knightJumpSquare;
131 | }
132 | }
133 | }
134 | knightMoves[squareIndex] = legalKnightJumps.ToArray();
135 | knightAttackBitboards[squareIndex] = knightBitboard;
136 |
137 | // Calculate all squares king can move to from current square (not including castling)
138 | var legalKingMoves = new List();
139 | foreach (int kingMoveDelta in directionOffsets)
140 | {
141 | int kingMoveSquare = squareIndex + kingMoveDelta;
142 | if (kingMoveSquare >= 0 && kingMoveSquare < 64)
143 | {
144 | int kingSquareY = kingMoveSquare / 8;
145 | int kingSquareX = kingMoveSquare - kingSquareY * 8;
146 | // Ensure king has moved max of 1 square on x/y axis (to reject indices that have wrapped around side of board)
147 | int maxCoordMoveDst = System.Math.Max(System.Math.Abs(x - kingSquareX), System.Math.Abs(y - kingSquareY));
148 | if (maxCoordMoveDst == 1)
149 | {
150 | legalKingMoves.Add((byte)kingMoveSquare);
151 | kingAttackBitboards[squareIndex] |= 1ul << kingMoveSquare;
152 | }
153 | }
154 | }
155 | kingMoves[squareIndex] = legalKingMoves.ToArray();
156 |
157 | // Calculate legal pawn captures for white and black
158 | List pawnCapturesWhite = new List();
159 | List pawnCapturesBlack = new List();
160 | pawnAttackBitboards[squareIndex] = new ulong[2];
161 | if (x > 0)
162 | {
163 | if (y < 7)
164 | {
165 | pawnCapturesWhite.Add(squareIndex + 7);
166 | pawnAttackBitboards[squareIndex][Board.WhiteIndex] |= 1ul << (squareIndex + 7);
167 | }
168 | if (y > 0)
169 | {
170 | pawnCapturesBlack.Add(squareIndex - 9);
171 | pawnAttackBitboards[squareIndex][Board.BlackIndex] |= 1ul << (squareIndex - 9);
172 | }
173 | }
174 | if (x < 7)
175 | {
176 | if (y < 7)
177 | {
178 | pawnCapturesWhite.Add(squareIndex + 9);
179 | pawnAttackBitboards[squareIndex][Board.WhiteIndex] |= 1ul << (squareIndex + 9);
180 | }
181 | if (y > 0)
182 | {
183 | pawnCapturesBlack.Add(squareIndex - 7);
184 | pawnAttackBitboards[squareIndex][Board.BlackIndex] |= 1ul << (squareIndex - 7);
185 | }
186 | }
187 | pawnAttacksWhite[squareIndex] = pawnCapturesWhite.ToArray();
188 | pawnAttacksBlack[squareIndex] = pawnCapturesBlack.ToArray();
189 |
190 | // Rook moves
191 | for (int directionIndex = 0; directionIndex < 4; directionIndex++)
192 | {
193 | int currentDirOffset = directionOffsets[directionIndex];
194 | for (int n = 0; n < numSquaresToEdge[squareIndex][directionIndex]; n++)
195 | {
196 | int targetSquare = squareIndex + currentDirOffset * (n + 1);
197 | rookMoves[squareIndex] |= 1ul << targetSquare;
198 | }
199 | }
200 | // Bishop moves
201 | for (int directionIndex = 4; directionIndex < 8; directionIndex++)
202 | {
203 | int currentDirOffset = directionOffsets[directionIndex];
204 | for (int n = 0; n < numSquaresToEdge[squareIndex][directionIndex]; n++)
205 | {
206 | int targetSquare = squareIndex + currentDirOffset * (n + 1);
207 | bishopMoves[squareIndex] |= 1ul << targetSquare;
208 | }
209 | }
210 | queenMoves[squareIndex] = rookMoves[squareIndex] | bishopMoves[squareIndex];
211 | }
212 |
213 | directionLookup = new int[127];
214 | for (int i = 0; i < 127; i++)
215 | {
216 | int offset = i - 63;
217 | int absOffset = System.Math.Abs(offset);
218 | int absDir = 1;
219 | if (absOffset % 9 == 0)
220 | {
221 | absDir = 9;
222 | }
223 | else if (absOffset % 8 == 0)
224 | {
225 | absDir = 8;
226 | }
227 | else if (absOffset % 7 == 0)
228 | {
229 | absDir = 7;
230 | }
231 |
232 | directionLookup[i] = absDir * System.Math.Sign(offset);
233 | }
234 |
235 | // Distance lookup
236 | OrthogonalDistance = new int[64, 64];
237 | kingDistance = new int[64, 64];
238 | CentreManhattanDistance = new int[64];
239 | for (int squareA = 0; squareA < 64; squareA++)
240 | {
241 | Coord coordA = BoardHelper.CoordFromIndex(squareA);
242 | int fileDstFromCentre = Max(3 - coordA.fileIndex, coordA.fileIndex - 4);
243 | int rankDstFromCentre = Max(3 - coordA.rankIndex, coordA.rankIndex - 4);
244 | CentreManhattanDistance[squareA] = fileDstFromCentre + rankDstFromCentre;
245 |
246 | for (int squareB = 0; squareB < 64; squareB++)
247 | {
248 |
249 | Coord coordB = BoardHelper.CoordFromIndex(squareB);
250 | int rankDistance = Abs(coordA.rankIndex - coordB.rankIndex);
251 | int fileDistance = Abs(coordA.fileIndex - coordB.fileIndex);
252 | OrthogonalDistance[squareA, squareB] = fileDistance + rankDistance;
253 | kingDistance[squareA, squareB] = Max(fileDistance, rankDistance);
254 | }
255 | }
256 |
257 | alignMask = new ulong[64, 64];
258 | for (int squareA = 0; squareA < 64; squareA++)
259 | {
260 | for (int squareB = 0; squareB < 64; squareB++)
261 | {
262 | Coord cA = BoardHelper.CoordFromIndex(squareA);
263 | Coord cB = BoardHelper.CoordFromIndex(squareB);
264 | Coord delta = cB - cA;
265 | Coord dir = new Coord(System.Math.Sign(delta.fileIndex), System.Math.Sign(delta.rankIndex));
266 | //Coord dirOffset = dirOffsets2D[dirIndex];
267 |
268 | for (int i = -8; i < 8; i++)
269 | {
270 | Coord coord = BoardHelper.CoordFromIndex(squareA) + dir * i;
271 | if (coord.IsValidSquare())
272 | {
273 | alignMask[squareA, squareB] |= 1ul << (BoardHelper.IndexFromCoord(coord));
274 | }
275 | }
276 | }
277 | }
278 |
279 |
280 | dirRayMask = new ulong[8, 64];
281 | for (int dirIndex = 0; dirIndex < dirOffsets2D.Length; dirIndex++)
282 | {
283 | for (int squareIndex = 0; squareIndex < 64; squareIndex++)
284 | {
285 | Coord square = BoardHelper.CoordFromIndex(squareIndex);
286 |
287 | for (int i = 0; i < 8; i++)
288 | {
289 | Coord coord = square + dirOffsets2D[dirIndex] * i;
290 | if (coord.IsValidSquare())
291 | {
292 | dirRayMask[dirIndex, squareIndex] |= 1ul << (BoardHelper.IndexFromCoord(coord));
293 | }
294 | else
295 | {
296 | break;
297 | }
298 | }
299 | }
300 | }
301 | }
302 | }
303 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Result/Arbiter.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | using System.Linq;
4 |
5 | public static class Arbiter
6 | {
7 | public static bool IsDrawResult(GameResult result)
8 | {
9 | return result is GameResult.DrawByArbiter or GameResult.FiftyMoveRule or
10 | GameResult.Repetition or GameResult.Stalemate or GameResult.InsufficientMaterial;
11 | }
12 |
13 | public static bool IsWinResult(GameResult result)
14 | {
15 | return IsWhiteWinsResult(result) || IsBlackWinsResult(result);
16 | }
17 |
18 | public static bool IsWhiteWinsResult(GameResult result)
19 | {
20 | return result is GameResult.BlackIsMated or GameResult.BlackTimeout or GameResult.BlackIllegalMove;
21 | }
22 |
23 | public static bool IsBlackWinsResult(GameResult result)
24 | {
25 | return result is GameResult.WhiteIsMated or GameResult.WhiteTimeout or GameResult.WhiteIllegalMove;
26 | }
27 |
28 |
29 | public static GameResult GetGameState(Board board)
30 | {
31 | MoveGenerator moveGenerator = new MoveGenerator();
32 | var moves = moveGenerator.GenerateMoves(board);
33 |
34 | // Look for mate/stalemate
35 | if (moves.Length == 0)
36 | {
37 | if (moveGenerator.InCheck())
38 | {
39 | return (board.IsWhiteToMove) ? GameResult.WhiteIsMated : GameResult.BlackIsMated;
40 | }
41 | return GameResult.Stalemate;
42 | }
43 |
44 | // Fifty move rule
45 | if (board.currentGameState.fiftyMoveCounter >= 100)
46 | {
47 | return GameResult.FiftyMoveRule;
48 | }
49 |
50 | // Threefold repetition
51 | int repCount = board.RepetitionPositionHistory.Count((x => x == board.currentGameState.zobristKey));
52 | if (repCount == 3)
53 | {
54 | return GameResult.Repetition;
55 | }
56 |
57 | // Look for insufficient material
58 | if (InsufficentMaterial(board))
59 | {
60 | return GameResult.InsufficientMaterial;
61 | }
62 | return GameResult.InProgress;
63 | }
64 |
65 | // Test for insufficient material (Note: not all cases are implemented)
66 | public static bool InsufficentMaterial(Board board)
67 | {
68 | // Can't have insufficient material with pawns on the board
69 | if (board.pawns[Board.WhiteIndex].Count > 0 || board.pawns[Board.BlackIndex].Count > 0)
70 | {
71 | return false;
72 | }
73 |
74 | // Can't have insufficient material with queens/rooks on the board
75 | if (board.FriendlyOrthogonalSliders != 0 || board.EnemyOrthogonalSliders != 0)
76 | {
77 | return false;
78 | }
79 |
80 | // If no pawns, queens, or rooks on the board, then consider knight and bishop cases
81 | int numWhiteBishops = board.bishops[Board.WhiteIndex].Count;
82 | int numBlackBishops = board.bishops[Board.BlackIndex].Count;
83 | int numWhiteKnights = board.knights[Board.WhiteIndex].Count;
84 | int numBlackKnights = board.knights[Board.BlackIndex].Count;
85 | int numWhiteMinors = numWhiteBishops + numWhiteKnights;
86 | int numBlackMinors = numBlackBishops + numBlackKnights;
87 | int numMinors = numWhiteMinors + numBlackMinors;
88 |
89 | // Lone kings or King vs King + single minor: is insuffient
90 | if (numMinors <= 1)
91 | {
92 | return true;
93 | }
94 |
95 | // Bishop vs bishop: is insufficient when bishops are same colour complex
96 | if (numMinors == 2 && numWhiteBishops == 1 && numBlackBishops == 1)
97 | {
98 | bool whiteBishopIsLightSquare = BoardHelper.LightSquare(board.bishops[Board.WhiteIndex][0]);
99 | bool blackBishopIsLightSquare = BoardHelper.LightSquare(board.bishops[Board.BlackIndex][0]);
100 | return whiteBishopIsLightSquare == blackBishopIsLightSquare;
101 | }
102 |
103 | return false;
104 |
105 |
106 | }
107 | }
108 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/Framework/Chess/Result/GameResult.cs:
--------------------------------------------------------------------------------
1 | namespace ChessChallenge.Chess
2 | {
3 | public enum GameResult
4 | {
5 | NotStarted,
6 | InProgress,
7 | WhiteIsMated,
8 | BlackIsMated,
9 | Stalemate,
10 | Repetition,
11 | FiftyMoveRule,
12 | InsufficientMaterial,
13 | DrawByArbiter,
14 | WhiteTimeout,
15 | BlackTimeout,
16 | WhiteIllegalMove,
17 | BlackIllegalMove
18 | }
19 | }
--------------------------------------------------------------------------------
/Chess-Challenge/src/My Bot/MyBot.cs:
--------------------------------------------------------------------------------
1 | using ChessChallenge.API;
2 | using System;
3 |
4 | public class MyBot : IChessBot
5 | {
6 | Move bestmoveRoot = Move.NullMove;
7 |
8 | // https://www.chessprogramming.org/PeSTO%27s_Evaluation_Function
9 | int[] pieceVal = {0, 100, 310, 330, 500, 1000, 10000 };
10 | int[] piecePhase = {0, 0, 1, 1, 2, 4, 0};
11 | ulong[] psts = {657614902731556116, 420894446315227099, 384592972471695068, 312245244820264086, 364876803783607569, 366006824779723922, 366006826859316500, 786039115310605588, 421220596516513823, 366011295806342421, 366006826859316436, 366006896669578452, 162218943720801556, 440575073001255824, 657087419459913430, 402634039558223453, 347425219986941203, 365698755348489557, 311382605788951956, 147850316371514514, 329107007234708689, 402598430990222677, 402611905376114006, 329415149680141460, 257053881053295759, 291134268204721362, 492947507967247313, 367159395376767958, 384021229732455700, 384307098409076181, 402035762391246293, 328847661003244824, 365712019230110867, 366002427738801364, 384307168185238804, 347996828560606484, 329692156834174227, 365439338182165780, 386018218798040211, 456959123538409047, 347157285952386452, 365711880701965780, 365997890021704981, 221896035722130452, 384289231362147538, 384307167128540502, 366006826859320596, 366006826876093716, 366002360093332756, 366006824694793492, 347992428333053139, 457508666683233428, 329723156783776785, 329401687190893908, 366002356855326100, 366288301819245844, 329978030930875600, 420621693221156179, 422042614449657239, 384602117564867863, 419505151144195476, 366274972473194070, 329406075454444949, 275354286769374224, 366855645423297932, 329991151972070674, 311105941360174354, 256772197720318995, 365993560693875923, 258219435335676691, 383730812414424149, 384601907111998612, 401758895947998613, 420612834953622999, 402607438610388375, 329978099633296596, 67159620133902};
12 |
13 | // https://www.chessprogramming.org/Transposition_Table
14 | struct TTEntry {
15 | public ulong key;
16 | public Move move;
17 | public int depth, score, bound;
18 | public TTEntry(ulong _key, Move _move, int _depth, int _score, int _bound) {
19 | key = _key; move = _move; depth = _depth; score = _score; bound = _bound;
20 | }
21 | }
22 |
23 | const int entries = (1 << 20);
24 | TTEntry[] tt = new TTEntry[entries];
25 |
26 | public int getPstVal(int psq) {
27 | return (int)(((psts[psq / 10] >> (6 * (psq % 10))) & 63) - 20) * 8;
28 | }
29 |
30 | public int Evaluate(Board board) {
31 | int mg = 0, eg = 0, phase = 0;
32 |
33 | foreach(bool stm in new[] {true, false}) {
34 | for(var p = PieceType.Pawn; p <= PieceType.King; p++) {
35 | int piece = (int)p, ind;
36 | ulong mask = board.GetPieceBitboard(p, stm);
37 | while(mask != 0) {
38 | phase += piecePhase[piece];
39 | ind = 128 * (piece - 1) + BitboardHelper.ClearAndGetIndexOfLSB(ref mask) ^ (stm ? 56 : 0);
40 | mg += getPstVal(ind) + pieceVal[piece];
41 | eg += getPstVal(ind + 64) + pieceVal[piece];
42 | }
43 | }
44 |
45 | mg = -mg;
46 | eg = -eg;
47 | }
48 |
49 | return (mg * phase + eg * (24 - phase)) / 24 * (board.IsWhiteToMove ? 1 : -1);
50 | }
51 |
52 | // https://www.chessprogramming.org/Negamax
53 | // https://www.chessprogramming.org/Quiescence_Search
54 | public int Search(Board board, Timer timer, int alpha, int beta, int depth, int ply) {
55 | ulong key = board.ZobristKey;
56 | bool qsearch = depth <= 0;
57 | bool notRoot = ply > 0;
58 | int best = -30000;
59 |
60 | // Check for repetition (this is much more important than material and 50 move rule draws)
61 | if(notRoot && board.IsRepeatedPosition())
62 | return 0;
63 |
64 | TTEntry entry = tt[key % entries];
65 |
66 | // TT cutoffs
67 | if(notRoot && entry.key == key && entry.depth >= depth && (
68 | entry.bound == 3 // exact score
69 | || entry.bound == 2 && entry.score >= beta // lower bound, fail high
70 | || entry.bound == 1 && entry.score <= alpha // upper bound, fail low
71 | )) return entry.score;
72 |
73 | int eval = Evaluate(board);
74 |
75 | // Quiescence search is in the same function as negamax to save tokens
76 | if(qsearch) {
77 | best = eval;
78 | if(best >= beta) return best;
79 | alpha = Math.Max(alpha, best);
80 | }
81 |
82 | // Generate moves, only captures in qsearch
83 | Move[] moves = board.GetLegalMoves(qsearch);
84 | int[] scores = new int[moves.Length];
85 |
86 | // Score moves
87 | for(int i = 0; i < moves.Length; i++) {
88 | Move move = moves[i];
89 | // TT move
90 | if(move == entry.move) scores[i] = 1000000;
91 | // https://www.chessprogramming.org/MVV-LVA
92 | else if(move.IsCapture) scores[i] = 100 * (int)move.CapturePieceType - (int)move.MovePieceType;
93 | }
94 |
95 | Move bestMove = Move.NullMove;
96 | int origAlpha = alpha;
97 |
98 | // Search moves
99 | for(int i = 0; i < moves.Length; i++) {
100 | if(timer.MillisecondsElapsedThisTurn >= timer.MillisecondsRemaining / 30) return 30000;
101 |
102 | // Incrementally sort moves
103 | for(int j = i + 1; j < moves.Length; j++) {
104 | if(scores[j] > scores[i])
105 | (scores[i], scores[j], moves[i], moves[j]) = (scores[j], scores[i], moves[j], moves[i]);
106 | }
107 |
108 | Move move = moves[i];
109 | board.MakeMove(move);
110 | int score = -Search(board, timer, -beta, -alpha, depth - 1, ply + 1);
111 | board.UndoMove(move);
112 |
113 | // New best move
114 | if(score > best) {
115 | best = score;
116 | bestMove = move;
117 | if(ply == 0) bestmoveRoot = move;
118 |
119 | // Improve alpha
120 | alpha = Math.Max(alpha, score);
121 |
122 | // Fail-high
123 | if(alpha >= beta) break;
124 |
125 | }
126 | }
127 |
128 | // (Check/Stale)mate
129 | if(!qsearch && moves.Length == 0) return board.IsInCheck() ? -30000 + ply : 0;
130 |
131 | // Did we fail high/low or get an exact score?
132 | int bound = best >= beta ? 2 : best > origAlpha ? 3 : 1;
133 |
134 | // Push to TT
135 | tt[key % entries] = new TTEntry(key, bestMove, depth, best, bound);
136 |
137 | return best;
138 | }
139 |
140 | public Move Think(Board board, Timer timer)
141 | {
142 | bestmoveRoot = Move.NullMove;
143 | // https://www.chessprogramming.org/Iterative_Deepening
144 | for(int depth = 1; depth <= 50; depth++) {
145 | int score = Search(board, timer, -30000, 30000, depth, 0);
146 |
147 | // Out of time
148 | if(timer.MillisecondsElapsedThisTurn >= timer.MillisecondsRemaining / 30)
149 | break;
150 | }
151 | return bestmoveRoot.IsNull ? board.GetLegalMoves()[0] : bestmoveRoot;
152 | }
153 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chess Coding Challenge (C#) Example
2 |
3 | Also known as Negamax Tier 2, this is an example bot for Seb Lague's
4 | [Chess Coding Challenge](https://youtu.be/iScy18pVR58), it implements
5 | only the most basic features for a functional chess engine. Little effort
6 | has been made to optimise for tokens, apart from implementing Quiescence
7 | Search inside the normal search function (rather than in a separate function).
8 |
9 | Additionally this repository contains my Neural Network bot, on [this](https://github.com/jw1912/Chess-Challenge/tree/nn) branch.
10 |
11 | ### Search
12 | - Alpha-Beta Negamax
13 | - Quiescence Search
14 | - Iterative Deepening
15 | - Transposition Table (Ordering & Cutoffs)
16 | - MVV-LVA for Captures
17 |
18 | ### Evaluation
19 | - Quantised & Compressed PeSTO Piece-Square Tables
20 |
--------------------------------------------------------------------------------