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