├── .gitattributes ├── Chess-Challenge ├── src │ ├── API │ │ ├── Square.cs │ │ ├── IChessBot.cs │ │ ├── PieceType.cs │ │ ├── PieceList.cs │ │ ├── Piece.cs │ │ ├── Timer.cs │ │ ├── Move.cs │ │ ├── BitboardHelper.cs │ │ └── Board.cs │ ├── Framework │ │ ├── Application │ │ │ ├── Helpers │ │ │ │ ├── Token Counter │ │ │ │ │ ├── Microsoft.CodeAnalysis.dll │ │ │ │ │ ├── Microsoft.CodeAnalysis.CSharp.dll │ │ │ │ │ └── TokenCounter.cs │ │ │ │ ├── API Helpers │ │ │ │ │ ├── BitboardDebugState.cs │ │ │ │ │ └── MoveHelper.cs │ │ │ │ ├── Warmer.cs │ │ │ │ ├── ConsoleHelper.cs │ │ │ │ ├── FileHelper.cs │ │ │ │ └── UIHelper.cs │ │ │ ├── Core │ │ │ │ ├── Settings.cs │ │ │ │ ├── Program.cs │ │ │ │ └── ChallengeController.cs │ │ │ ├── UI │ │ │ │ ├── BoardTheme.cs │ │ │ │ ├── MatchStatsUI.cs │ │ │ │ ├── BotBrainCapacityUI.cs │ │ │ │ ├── MenuUI.cs │ │ │ │ └── BoardUI.cs │ │ │ └── Players │ │ │ │ ├── ChessPlayer.cs │ │ │ │ └── HumanPlayer.cs │ │ └── Chess │ │ │ ├── Result │ │ │ ├── GameResult.cs │ │ │ └── Arbiter.cs │ │ │ ├── Board │ │ │ ├── GameState.cs │ │ │ ├── RepetitionTable.cs │ │ │ ├── Coord.cs │ │ │ ├── PieceList.cs │ │ │ ├── Move.cs │ │ │ ├── PieceHelper.cs │ │ │ └── Zobrist.cs │ │ │ ├── Helpers │ │ │ ├── PGNCreator.cs │ │ │ ├── PGNLoader.cs │ │ │ ├── BoardHelper.cs │ │ │ ├── MoveUtility.cs │ │ │ └── FenUtility.cs │ │ │ └── Move Generation │ │ │ ├── Magics │ │ │ ├── Magic.cs │ │ │ ├── MagicHelper.cs │ │ │ └── PrecomputedMagics.cs │ │ │ ├── Bitboards │ │ │ ├── BitBoardUtility.cs │ │ │ └── Bits.cs │ │ │ └── PrecomputedMoveData.cs │ ├── My Bot │ │ └── MyBot.cs │ └── Evil Bot │ │ └── EvilBot.cs ├── resources │ ├── Pieces.png │ └── Fonts │ │ ├── OPENSANS-SEMIBOLD.TTF │ │ └── sdf.fs └── Chess-Challenge.csproj ├── .editorconfig ├── LICENSE ├── Chess-Challenge.sln ├── .gitignore └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Chess-Challenge/src/API/Square.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebLague/Chess-Challenge/HEAD/Chess-Challenge/src/API/Square.cs -------------------------------------------------------------------------------- /Chess-Challenge/resources/Pieces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebLague/Chess-Challenge/HEAD/Chess-Challenge/resources/Pieces.png -------------------------------------------------------------------------------- /Chess-Challenge/resources/Fonts/OPENSANS-SEMIBOLD.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebLague/Chess-Challenge/HEAD/Chess-Challenge/resources/Fonts/OPENSANS-SEMIBOLD.TTF -------------------------------------------------------------------------------- /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/Framework/Application/Helpers/Token Counter/Microsoft.CodeAnalysis.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebLague/Chess-Challenge/HEAD/Chess-Challenge/src/Framework/Application/Helpers/Token Counter/Microsoft.CodeAnalysis.dll -------------------------------------------------------------------------------- /Chess-Challenge/src/Framework/Application/Helpers/Token Counter/Microsoft.CodeAnalysis.CSharp.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebLague/Chess-Challenge/HEAD/Chess-Challenge/src/Framework/Application/Helpers/Token Counter/Microsoft.CodeAnalysis.CSharp.dll -------------------------------------------------------------------------------- /Chess-Challenge/src/My Bot/MyBot.cs: -------------------------------------------------------------------------------- 1 | using ChessChallenge.API; 2 | 3 | public class MyBot : IChessBot 4 | { 5 | public Move Think(Board board, Timer timer) 6 | { 7 | Move[] moves = board.GetLegalMoves(); 8 | return moves[0]; 9 | } 10 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/Framework/Application/Helpers/API Helpers/BitboardDebugState.cs: -------------------------------------------------------------------------------- 1 | namespace ChessChallenge.Application.APIHelpers 2 | { 3 | public static class BitboardDebugState 4 | { 5 | public static bool BitboardDebugVisualizationRequested { get; set; } 6 | public static ulong BitboardToVisualize {get; set;} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /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/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/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/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/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.20"; 8 | 9 | // Game settings 10 | public const int GameDurationMilliseconds = 60 * 1000; 11 | public const int IncrementMilliseconds = 0 * 1000; 12 | public const float MinMoveDelay = 0; 13 | public static readonly bool RunBotsOnSeparateThread = true; 14 | 15 | // Display settings 16 | public const bool DisplayBoardCoordinates = true; 17 | public static readonly Vector2 ScreenSizeSmall = new(1280, 720); 18 | public static readonly Vector2 ScreenSizeBig = new(1920, 1080); 19 | 20 | // Other settings 21 | public const int MaxTokenCount = 1024; 22 | public const LogType MessagesToLog = LogType.All; 23 | 24 | public enum LogType 25 | { 26 | None, 27 | ErrorOnly, 28 | All 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sebastian Lague 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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/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.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/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/Framework/Chess/Board/RepetitionTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace ChessChallenge.Chess 5 | { 6 | public class RepetitionTable 7 | { 8 | readonly ulong[] hashes; 9 | readonly int[] startIndices; 10 | int count; 11 | 12 | public RepetitionTable() 13 | { 14 | const int size = 1024; 15 | hashes = new ulong[size - 1]; 16 | startIndices = new int[size]; 17 | } 18 | 19 | public void Init(Board board) 20 | { 21 | ulong[] initialHashes = board.RepetitionPositionHistory.Reverse().ToArray(); 22 | count = initialHashes.Length; 23 | 24 | for (int i = 0; i < initialHashes.Length; i++) 25 | { 26 | hashes[i] = initialHashes[i]; 27 | startIndices[i] = 0; 28 | } 29 | startIndices[count] = 0; 30 | } 31 | 32 | 33 | public void Push(ulong hash, bool reset) 34 | { 35 | // Check bounds just in case 36 | if (count < hashes.Length) 37 | { 38 | hashes[count] = hash; 39 | startIndices[count + 1] = reset ? count : startIndices[count]; 40 | count++; 41 | } 42 | } 43 | 44 | public void TryPop() 45 | { 46 | count = Math.Max(0, count - 1); 47 | } 48 | 49 | public bool Contains(ulong h) 50 | { 51 | int s = startIndices[count]; 52 | // up to count-1 so that curr position is not counted 53 | for (int i = s; i < count - 1; i++) 54 | { 55 | if (hashes[i] == h) 56 | { 57 | return true; 58 | } 59 | } 60 | return false; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /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/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/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/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/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 incrementAddedMs; 16 | int baseTimeMs; 17 | 18 | public ChessPlayer(object instance, ChallengeController.PlayerType type, int baseTimeMs = int.MaxValue) 19 | { 20 | this.PlayerType = type; 21 | Bot = instance as IChessBot; 22 | Human = instance as HumanPlayer; 23 | this.baseTimeMs = baseTimeMs; 24 | 25 | } 26 | 27 | public bool IsHuman => Human != null; 28 | public bool IsBot => Bot != null; 29 | 30 | public void Update() 31 | { 32 | if (Human != null) 33 | { 34 | Human.Update(); 35 | } 36 | } 37 | 38 | public void UpdateClock(double dt) 39 | { 40 | secondsElapsed += dt; 41 | } 42 | 43 | public void AddIncrement(int incrementMs) 44 | { 45 | incrementAddedMs += incrementMs; 46 | } 47 | 48 | public int TimeRemainingMs 49 | { 50 | get 51 | { 52 | if (baseTimeMs == int.MaxValue) 53 | { 54 | return baseTimeMs; 55 | } 56 | return (int)Math.Ceiling(Math.Max(0, baseTimeMs - secondsElapsed * 1000.0 + incrementAddedMs)); 57 | } 58 | } 59 | 60 | public void SubscribeToMoveChosenEventIfHuman(Action action) 61 | { 62 | if (Human != null) 63 | { 64 | Human.MoveChosen += action; 65 | } 66 | } 67 | 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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/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 totalTokenCount, int debugTokenCount, int tokenLimit) 14 | { 15 | int activeTokenCount = totalTokenCount - debugTokenCount; 16 | 17 | int screenWidth = Raylib.GetScreenWidth(); 18 | int screenHeight = Raylib.GetScreenHeight(); 19 | int height = UIHelper.ScaleInt(48); 20 | int fontSize = UIHelper.ScaleInt(35); 21 | // Bg 22 | Raylib.DrawRectangle(0, screenHeight - height, screenWidth, height, background); 23 | // Bar 24 | double t = (double)activeTokenCount / tokenLimit; 25 | 26 | Color col; 27 | if (t <= 0.7) 28 | col = green; 29 | else if (t <= 0.85) 30 | col = yellow; 31 | else if (t <= 1) 32 | col = orange; 33 | else 34 | col = red; 35 | Raylib.DrawRectangle(0, screenHeight - height, (int)(screenWidth * t), height, col); 36 | 37 | var textPos = new System.Numerics.Vector2(screenWidth / 2, screenHeight - height / 2); 38 | string text = $"Bot Brain Capacity: {activeTokenCount}/{tokenLimit}"; 39 | if (activeTokenCount > tokenLimit) 40 | { 41 | text += " [LIMIT EXCEEDED]"; 42 | } 43 | else if (debugTokenCount != 0) 44 | { 45 | text += $" ({totalTokenCount} with Debugs included)"; 46 | } 47 | UIHelper.DrawText(text, textPos, fontSize, 1, Color.WHITE, UIHelper.AlignH.Centre); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /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/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/API/Timer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ChessChallenge.API 4 | { 5 | public sealed class Timer 6 | { 7 | /// 8 | /// The amount of time (in milliseconds) that each player started the game with 9 | /// 10 | public readonly int GameStartTimeMilliseconds; 11 | 12 | /// 13 | /// The amount of time (in milliseconds) that gets added to the clock after each turn 14 | /// 15 | public readonly int IncrementMilliseconds; 16 | 17 | /// 18 | /// Amount of time elapsed since the current player started thinking (in milliseconds) 19 | /// 20 | public int MillisecondsElapsedThisTurn => (int)sw.ElapsedMilliseconds; 21 | 22 | /// 23 | /// Amount of time left on the clock for the current player (in milliseconds) 24 | /// 25 | public int MillisecondsRemaining => Math.Max(0, millisRemainingAtStartOfTurn - MillisecondsElapsedThisTurn); 26 | 27 | /// 28 | /// Amount of time left on the clock for the other player (in milliseconds) 29 | /// 30 | public readonly int OpponentMillisecondsRemaining; 31 | 32 | readonly System.Diagnostics.Stopwatch sw; 33 | readonly int millisRemainingAtStartOfTurn; 34 | 35 | public Timer(int millisRemaining) 36 | { 37 | millisRemainingAtStartOfTurn = millisRemaining; 38 | sw = System.Diagnostics.Stopwatch.StartNew(); 39 | } 40 | 41 | public Timer(int remainingMs, int opponentRemainingMs, int startingMs, int incrementMs = 0) 42 | { 43 | millisRemainingAtStartOfTurn = remainingMs; 44 | sw = System.Diagnostics.Stopwatch.StartNew(); 45 | GameStartTimeMilliseconds = startingMs; 46 | OpponentMillisecondsRemaining = opponentRemainingMs; 47 | IncrementMilliseconds = incrementMs; 48 | } 49 | 50 | public override string ToString() 51 | { 52 | return $"Game start time: {GameStartTimeMilliseconds} ms. Turn elapsed time: {MillisecondsElapsedThisTurn} ms. My time remaining: {MillisecondsRemaining} Opponent time remaining: {OpponentMillisecondsRemaining} ms."; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /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/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/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/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 bool IsEnPassant => MoveFlag == EnPassantCaptureFlag; 54 | public int MoveFlag => moveValue >> 12; 55 | 56 | public int PromotionPieceType 57 | { 58 | get 59 | { 60 | switch (MoveFlag) 61 | { 62 | case PromoteToRookFlag: 63 | return PieceHelper.Rook; 64 | case PromoteToKnightFlag: 65 | return PieceHelper.Knight; 66 | case PromoteToBishopFlag: 67 | return PieceHelper.Bishop; 68 | case PromoteToQueenFlag: 69 | return PieceHelper.Queen; 70 | default: 71 | return PieceHelper.None; 72 | } 73 | } 74 | } 75 | 76 | public static Move NullMove => new Move(0); 77 | public static bool SameMove(Move a, Move b) => a.moveValue == b.moveValue; 78 | 79 | 80 | } 81 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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/Application/Helpers/Token Counter/TokenCounter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.Text; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System; 7 | 8 | namespace ChessChallenge.Application 9 | { 10 | public static class TokenCounter 11 | { 12 | // Including this in a comment will cause all tokens on the same line to be excluded from the count. 13 | // Note: these tokens will still be counted in your final submission. 14 | const string ignoreString = "#DEBUG"; 15 | 16 | static readonly HashSet tokensToIgnore = new(new SyntaxKind[] 17 | { 18 | SyntaxKind.PrivateKeyword, 19 | SyntaxKind.PublicKeyword, 20 | SyntaxKind.SemicolonToken, 21 | SyntaxKind.CommaToken, 22 | SyntaxKind.ReadOnlyKeyword, 23 | // only count open brace since I want to count the pair as a single token 24 | SyntaxKind.CloseBraceToken, 25 | SyntaxKind.CloseBracketToken, 26 | SyntaxKind.CloseParenToken 27 | }); 28 | 29 | public static (int totalCount, int debugCount) CountTokens(string code) 30 | { 31 | SyntaxTree tree = CSharpSyntaxTree.ParseText(code); 32 | SyntaxNode root = tree.GetRoot(); 33 | TextLineCollection lines = tree.GetText().Lines; 34 | IEnumerable allTokens = root.DescendantTokens(); 35 | 36 | // Total token count 37 | int totalTokenCount = CountTokens(allTokens); 38 | 39 | // Debug token count (tokens explicitly excluded by user for testing purposes) 40 | var ignoreInstructions = root.DescendantTrivia().Where(t => IsIgnoreTokenInstruction(t)); 41 | int debugTokenCount = 0; 42 | foreach(var ignore in ignoreInstructions) 43 | { 44 | int lineNumber = lines.GetLineFromPosition(ignore.SpanStart).LineNumber; 45 | var debugTokens = allTokens.Where(t => lines.GetLineFromPosition(t.SpanStart).LineNumber == lineNumber); 46 | debugTokenCount += CountTokens(debugTokens); 47 | } 48 | 49 | return (totalTokenCount, debugTokenCount); 50 | } 51 | 52 | static int CountTokens(IEnumerable tokens) 53 | { 54 | int tokenCount = 0; 55 | foreach (var token in tokens) 56 | { 57 | tokenCount += GetTokenCountValue(token); 58 | } 59 | return tokenCount; 60 | } 61 | 62 | static int GetTokenCountValue(SyntaxToken token) 63 | { 64 | SyntaxKind kind = token.Kind(); 65 | if (tokensToIgnore.Contains(kind)) 66 | { 67 | return 0; 68 | } 69 | 70 | // String literals count for as many chars as are in the string 71 | if (kind is SyntaxKind.StringLiteralToken or SyntaxKind.InterpolatedStringTextToken) 72 | { 73 | return token.ToString().Length; 74 | } 75 | 76 | // Regular tokens count as just one token 77 | return 1; 78 | } 79 | 80 | static bool IsIgnoreTokenInstruction(SyntaxTrivia trivia) 81 | { 82 | var compareType = StringComparison.InvariantCultureIgnoreCase; 83 | return trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) && trivia.ToString().Contains(ignoreString, compareType); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /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/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/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/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/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/Application/Helpers/UIHelper.cs: -------------------------------------------------------------------------------- 1 | using Raylib_cs; 2 | using System; 3 | using System.Numerics; 4 | using static ChessChallenge.Application.FileHelper; 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 float Scale(float val, int referenceResolution = referenceResolution) 108 | { 109 | return Raylib.GetScreenWidth() / (float)referenceResolution * val; 110 | } 111 | 112 | public static int ScaleInt(int val, int referenceResolution = referenceResolution) 113 | { 114 | return (int)Math.Round(Raylib.GetScreenWidth() / (float)referenceResolution * val); 115 | } 116 | 117 | public static Vector2 Scale(Vector2 vec, int referenceResolution = referenceResolution) 118 | { 119 | float x = Scale(vec.X, referenceResolution); 120 | float y = Scale(vec.Y, referenceResolution); 121 | return new Vector2(x, y); 122 | } 123 | 124 | public static void Release() 125 | { 126 | Raylib.UnloadFont(font); 127 | if (SDF_Enabled) 128 | { 129 | Raylib.UnloadFont(fontSdf); 130 | Raylib.UnloadShader(shader); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /.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/src/API/BitboardHelper.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace ChessChallenge.API 3 | { 4 | using ChessChallenge.Application.APIHelpers; 5 | using ChessChallenge.Chess; 6 | 7 | /// 8 | /// Helper class for working with bitboards. 9 | /// Bitboards are represented with the ulong type (unsigned 64 bit integer). 10 | /// 11 | public static class BitboardHelper 12 | { 13 | /// 14 | /// Set the given square on the bitboard to 1. 15 | /// 16 | public static void SetSquare(ref ulong bitboard, Square square) 17 | { 18 | bitboard |= 1ul << square.Index; 19 | } 20 | /// 21 | /// Clear the given square on the bitboard to 0. 22 | /// 23 | public static void ClearSquare(ref ulong bitboard, Square square) 24 | { 25 | bitboard &= ~(1ul << square.Index); 26 | } 27 | 28 | /// 29 | /// Toggle the given square on the bitboard between 0 and 1. 30 | /// 31 | public static void ToggleSquare(ref ulong bitboard, Square square) 32 | { 33 | bitboard ^= 1ul << square.Index; 34 | } 35 | 36 | /// 37 | /// Returns true if the given square is set to 1 on the bitboard, otherwise false. 38 | /// 39 | public static bool SquareIsSet(ulong bitboard, Square square) 40 | { 41 | return ((bitboard >> square.Index) & 1) != 0; 42 | } 43 | 44 | 45 | /// 46 | /// Returns index of the first bit that is set to 1. The bit will also be cleared to zero. 47 | /// This can be useful for efficiently iterating over all the set squares in a bitboard. 48 | /// 49 | public static int ClearAndGetIndexOfLSB(ref ulong bitboard) 50 | { 51 | return BitBoardUtility.PopLSB(ref bitboard); 52 | } 53 | 54 | /// 55 | /// Returns the number of bits that set to 1 in the given bitboard. 56 | /// 57 | public static int GetNumberOfSetBits(ulong bitboard) 58 | { 59 | return BitBoardUtility.PopCount(bitboard); 60 | } 61 | 62 | /// 63 | /// Returns a bitboard where each bit that is set to 1 represents a square that the given piece type is 64 | /// able to attack. These attacks are calculated from the given square, and take the given board state into 65 | /// account (so queen, rook, and bishop attacks will be blocked by pieces that are in the way). 66 | /// The isWhite parameter determines the direction of pawn captures. 67 | /// 68 | public static ulong GetPieceAttacks(PieceType pieceType, Square square, Board board, bool isWhite) 69 | { 70 | return pieceType switch 71 | { 72 | PieceType.Pawn => GetPawnAttacks(square, isWhite), 73 | PieceType.Knight => GetKnightAttacks(square), 74 | PieceType.Bishop => GetBishopAttacks(square, board.AllPiecesBitboard), 75 | PieceType.Rook => GetRookAttacks(square, board.AllPiecesBitboard), 76 | PieceType.Queen => GetQueenAttacks(square, board.AllPiecesBitboard), 77 | PieceType.King => GetKingAttacks(square), 78 | _ => 0 79 | }; 80 | } 81 | 82 | /// 83 | /// Returns a bitboard where each bit that is set to 1 represents a square that the given piece type is 84 | /// able to attack. These attacks are calculated from the given square, and take the given blockers into 85 | /// account (so queen, rook, and bishop attacks will be blocked by pieces that are in the way). 86 | /// The isWhite parameter determines the direction of pawn captures. 87 | /// 88 | public static ulong GetPieceAttacks(PieceType pieceType, Square square, ulong blockers, bool isWhite) 89 | { 90 | return pieceType switch 91 | { 92 | PieceType.Pawn => GetPawnAttacks(square, isWhite), 93 | PieceType.Knight => GetKnightAttacks(square), 94 | PieceType.Bishop => GetBishopAttacks(square, blockers), 95 | PieceType.Rook => GetRookAttacks(square, blockers), 96 | PieceType.Queen => GetQueenAttacks(square, blockers), 97 | PieceType.King => GetKingAttacks(square), 98 | _ => 0 99 | }; 100 | } 101 | 102 | /// 103 | /// Returns a bitboard where each bit that is set to 1 represents a square that the given 104 | /// sliding piece type is able to attack. These attacks are calculated from the given square, 105 | /// and take the given board state into account (so attacks will be blocked by pieces that are in the way). 106 | /// Valid only for sliding piece types (queen, rook, and bishop). 107 | /// 108 | public static ulong GetSliderAttacks(PieceType pieceType, Square square, Board board) 109 | { 110 | return pieceType switch 111 | { 112 | PieceType.Rook => GetRookAttacks(square, board.AllPiecesBitboard), 113 | PieceType.Bishop => GetBishopAttacks(square, board.AllPiecesBitboard), 114 | PieceType.Queen => GetQueenAttacks(square, board.AllPiecesBitboard), 115 | _ => 0 116 | }; 117 | } 118 | 119 | /// 120 | /// Returns a bitboard where each bit that is set to 1 represents a square that the given 121 | /// sliding piece type is able to attack. These attacks are calculated from the given square, 122 | /// and take the given blocker bitboard into account (so attacks will be blocked by pieces that are in the way). 123 | /// Valid only for sliding piece types (queen, rook, and bishop). 124 | /// 125 | public static ulong GetSliderAttacks(PieceType pieceType, Square square, ulong blockers) 126 | { 127 | return pieceType switch 128 | { 129 | PieceType.Rook => GetRookAttacks(square, blockers), 130 | PieceType.Bishop => GetBishopAttacks(square, blockers), 131 | PieceType.Queen => GetQueenAttacks(square, blockers), 132 | _ => 0 133 | }; 134 | } 135 | /// 136 | /// Gets a bitboard of squares that a knight can attack from the given square. 137 | /// 138 | public static ulong GetKnightAttacks(Square square) => Bits.KnightAttacks[square.Index]; 139 | /// 140 | /// Gets a bitboard of squares that a king can attack from the given square. 141 | /// 142 | public static ulong GetKingAttacks(Square square) => Bits.KingMoves[square.Index]; 143 | 144 | /// 145 | /// Gets a bitboard of squares that a pawn (of the given colour) can attack from the given square. 146 | /// 147 | public static ulong GetPawnAttacks(Square square, bool isWhite) 148 | { 149 | return isWhite ? Bits.WhitePawnAttacks[square.Index] : Bits.BlackPawnAttacks[square.Index]; 150 | } 151 | 152 | /// 153 | /// A debug function for visualizing bitboards. 154 | /// Highlights the squares that are set to 1 in the given bitboard with a red colour. 155 | /// Highlights the squares that are set to 0 in the given bitboard with a blue colour. 156 | /// 157 | public static void VisualizeBitboard(ulong bitboard) 158 | { 159 | BitboardDebugState.BitboardDebugVisualizationRequested = true; 160 | BitboardDebugState.BitboardToVisualize = bitboard; 161 | } 162 | 163 | /// 164 | /// Clears the bitboard debug visualization 165 | /// 166 | public static void StopVisualizingBitboard() => BitboardDebugState.BitboardDebugVisualizationRequested = false; 167 | 168 | static ulong GetRookAttacks(Square square, ulong blockers) 169 | { 170 | ulong mask = Magic.RookMask[square.Index]; 171 | ulong magic = PrecomputedMagics.RookMagics[square.Index]; 172 | int shift = PrecomputedMagics.RookShifts[square.Index]; 173 | 174 | ulong key = ((blockers & mask) * magic) >> shift; 175 | return Magic.RookAttacks[square.Index][key]; 176 | } 177 | 178 | static ulong GetBishopAttacks(Square square, ulong blockers) 179 | { 180 | ulong mask = Magic.BishopMask[square.Index]; 181 | ulong magic = PrecomputedMagics.BishopMagics[square.Index]; 182 | int shift = PrecomputedMagics.BishopShifts[square.Index]; 183 | 184 | ulong key = ((blockers & mask) * magic) >> shift; 185 | return Magic.BishopAttacks[square.Index][key]; 186 | } 187 | 188 | static ulong GetQueenAttacks(Square square, ulong blockers) 189 | { 190 | return GetRookAttacks(square, blockers) | GetBishopAttacks(square, blockers); 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chess Coding Challenge (C#) 2 | Welcome to the [chess coding challenge](https://youtu.be/iScy18pVR58)! This is a friendly competition in which your goal is to create a small chess bot (in C#) using the framework provided in this repository. 3 | Once submissions close, these bots will battle it out to discover which bot is best! 4 | 5 | I will then create a video exploring the implementations of the best and most unique/interesting bots. 6 | I also plan to make a small game that features these most interesting/challenging entries, so that everyone can try playing against them. 7 | 8 | ## Submissions are now closed 9 | Thank you so much to everyone who participated -- in total, 636 chess bots were submitted. Also, a huge extra thanks to everyone who contributed code, reported bugs in the framework (sorry about those!), and gave their time to help others getting started with the intricacies of chess programming. 10 | 11 | The results video is now out over [here](https://youtu.be/Ne40a5LkK6A). 12 | And the tournament data can be found [here](https://github.com/SebLague/Tiny-Chess-Bot-Challenge-Results). 13 | 14 | ## Change Log 15 | It has been necessary to make some bug fixes to the original project, and I've also been tempted (by some great suggestions from the community) into making a few non-breaking improvements/additions to the API. I realize that changes can be frustrating during a challenge though, and so will commit to freezing the API from August 1st. 16 | 17 | * V1.1 Fixed major bug affecting `board.GetPiece()` and `PieceList` functions. Added `Board.CreateBoardFromFEN()`. 18 | * V1.11 UI changes: Added coordinate names to board UI and fixed human player input bug. 19 | * V1.12 Small fixes to `board.IsDraw()`: Fifty move counter is now updated properly during search, and insufficient material is now detected for lone bishops on the same square colour. 20 | * V1.13 Fixed issue with `board.ZobristKey` where value would sometimes be different after making and undoing a move. Added an alternative function for getting moves `board.GetLegalMovesNonAlloc()` (see docs for more info). 21 | * V1.14 A handful of additions to the Board API: `board.IsInsufficientMaterial()`, `board.IsRepeatedPosition()`, `board.GameRepetitionHistory`, `board.FiftyMoveCounter`, `board.GameMoveHistory`, `board.GameStartFenString`. 22 | * V1.15 Fixed incorrect `move.CapturePieceType` for en-passant moves and moves in `board.GameMoveHistory`. Added `BitboardHelper.VisualizeBitboard()` to help with debugging bitboards. 23 | * V1.16 Added `timer.GameStartTimeMilliseconds`, `timer.OpponentMillisecondsRemaining`, `board.ForceSkipTurn()`. 24 | * V1.17 Added `BitboardHelper.GetPieceAttacks()` and optimized `board.SquareIsAttackedByOponent()`. Writing `#DEBUG` in a comment will now exclude code in that line from counting towards the token limit (for testing only of course). 25 | * V1.18 Added `timer.IncrementMilliseconds` (this will be 0 for the main tournament, but a small increment may be used in the final playoff games). Fixed a bug in the repetition handling, and optimized check/stalemate detection. 26 | * V1.19 Fixed potential out of bounds exception. Fixed bug in stalemate detection. 27 | * V1.20 Fixed (another) bug in the repetition detection. 28 | 29 | [There will be no API changes after August 1] 30 | 31 | ## How to Participate 32 | * Install an IDE such as [Visual Studio](https://visualstudio.microsoft.com/downloads/). 33 | * Install [.NET 6.0](https://dotnet.microsoft.com/en-us/download) 34 | * Download this repository and open the Chess-Challenge project in your IDE. 35 | * Try building and running the project. 36 | * If a window with a chess board appears — great! 37 | * If it doesn't work, take a look at the [FAQ/troubleshooting](#faq-and-troubleshooting) section at the bottom of the page. You can also search the [issues page](https://github.com/SebLague/Chess-Challenge/issues) to see if anyone is having a similar issue. If not, post about it there with any details such as error messages, operating system etc. 38 | * Open the MyBot.cs file _(located in src/MyBot)_ and write some code! 39 | * You might want to take a look at the [Documentation](https://seblague.github.io/chess-coding-challenge/documentation/) first, and the Rules too! 40 | * Build and run the program again to test your changes. 41 | * For testing, you have three options in the program: 42 | * You can play against the bot yourself (Human vs Bot) 43 | * The bot can play a match against itself (MyBot vs MyBot) 44 | * The bot can play a match against a simple example bot (MyBot vs EvilBot).
You could also replace the EvilBot code with your own code, to test two different versions of your bot against one another. 45 | * Once you're happy with your chess bot, head over to the [Submission Page](https://forms.gle/6jjj8jxNQ5Ln53ie6) to enter it into the competition. 46 | * You will be able to edit your entry up until the competition closes. 47 | 48 | ## Rules 49 | * You may participate alone, or in a group of any size. 50 | * You may submit a maximum of two entries. 51 | * Please only submit a second entry if it is significantly different from your first bot (not just a minor tweak). 52 | * Note: you will need to log in with a second Google account if you want submit a second entry. 53 | * Only the following namespaces are allowed: 54 | * `ChessChallenge.API` 55 | * `System` 56 | * `System.Numerics` 57 | * `System.Collections.Generic` 58 | * `System.Linq` 59 | * You may not use the `AsParallel()` function 60 | * As implied by the allowed namespaces, you may not read data from a file or access the internet, nor may you create any new threads or tasks to run code in parallel/in the background. 61 | * You may not use the unsafe keyword. 62 | * You may not store data inside the name of a variable/function/class etc (to be extracted with `nameof()`, `GetType().ToString()`, `Environment.StackTrace` and so on). Thank you to [#12](https://github.com/SebLague/Chess-Challenge/issues/12) and [#24](https://github.com/SebLague/Chess-Challenge/issues/24). 63 | * If your bot makes an illegal move or runs out of time, it will lose the game. 64 | * Games are played with 1 minute per side by default (this can be changed in the settings class). The final tournament time control is TBD, so your bot should not assume a particular time control, and instead respect the amount of time left on the timer (given in the Think function). 65 | * Your bot may not use more than 256mb of memory for creating look-up tables (such as a transposition table). 66 | * If you have added a constructor to MyBot (for generating look up tables, etc.) it may not take longer than 5 seconds to complete. 67 | * All of your code/data must be contained within the _MyBot.cs_ file. 68 | * Note: you may create additional scripts for testing/training your bot, but only the _MyBot.cs_ file will be submitted, so it must be able to run without them. 69 | * You may not rename the _MyBot_ struct or _Think_ function contained in the _MyBot.cs_ file. 70 | * The code in MyBot.cs may not exceed the _bot brain capacity_ of 1024 (see below). 71 | 72 | ## Bot Brain Capacity 73 | There is a size limit on the code you create called the _bot brain capacity_. This is measured in ‘tokens’ and may not exceed 1024. The number of tokens you have used so far is displayed on the bottom of the screen when running the program. 74 | 75 | All names (variables, functions, etc.) are counted as a single token, regardless of length. This means that both lines of code: `bool a = true;` and `bool myObscenelyLongVariableName = true;` count the same. Additionally, the following things do not count towards the limit: white space, new lines, comments, access modifiers, commas, and semicolons. 76 | 77 | ## FAQ and Troubleshooting 78 | * What is the format of the tournament? 79 | * The format may change depending on the number of entries, but the current plan is to run two tournaments, with the first being a large Swiss tournament in which all bots are able to receive a ranking. These games will be played from the standard starting position. Some percengtage of the top bots will then be promoted to a second knock-out tournament, which will use a selection of different opening positions. The exact number of rounds/games and time-control are TBD. 80 | * [Unable to build/run the project from my IDE/Code editor](https://github.com/SebLague/Chess-Challenge/issues/85) 81 | * After downloading the project and installing .Net 6.0, open a terminal / command prompt window. 82 | * Navigate to the folder where Chess-Challenge.csproj is located using the `cd` command. 83 | * For example: `cd C:\Users\MyName\Desktop\Chess-Challenge\Chess-Challenge` 84 | * Now use the command: `dotnet run` 85 | * This should launch the project. If not, open an issue with any error messages and relevant info. 86 | * [Running on Linux](https://github.com/SebLague/Chess-Challenge/discussions/3) 87 | * Issues with illegal moves or errors when making/undoing a move 88 | * Make sure that you are making and undoing moves in the correct order, and that you don't forget to undo a move when exiting early from a function for example. 89 | * How to tell what colour MyBot is playing 90 | * You can look at `board.IsWhiteToMove` when the Think function is called 91 | * `GetPiece()` function is giving a null piece after making a move 92 | * Please make sure you are using the latest version of the project, there was a bug with this function in the original version 93 | * There is a community-run discord server [over here](https://github.com/SebLague/Chess-Challenge/discussions/156). 94 | * There is also an unofficial [live leaderboard](https://chess.stjo.dev/) created by a member of the community (source code available [here](https://github.com/StanislavNikolov/chess-league)). 95 | 96 | -------------------------------------------------------------------------------- /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/FenUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace ChessChallenge.Chess 6 | { 7 | // Helper class for dealing with FEN strings 8 | public static class FenUtility 9 | { 10 | public const string StartPositionFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; 11 | 12 | // Load position from fen string 13 | public static PositionInfo PositionFromFen(string fen) 14 | { 15 | 16 | PositionInfo loadedPositionInfo = new(fen); 17 | return loadedPositionInfo; 18 | } 19 | 20 | /// 21 | /// Get the fen string of the current position 22 | /// When alwaysIncludeEPSquare is true the en passant square will be included 23 | /// in the fen string even if no enemy pawn is in a position to capture it. 24 | /// 25 | public static string CurrentFen(Board board, bool alwaysIncludeEPSquare = true) 26 | { 27 | string fen = ""; 28 | for (int rank = 7; rank >= 0; rank--) 29 | { 30 | int numEmptyFiles = 0; 31 | for (int file = 0; file < 8; file++) 32 | { 33 | int i = rank * 8 + file; 34 | int piece = board.Square[i]; 35 | if (piece != 0) 36 | { 37 | if (numEmptyFiles != 0) 38 | { 39 | fen += numEmptyFiles; 40 | numEmptyFiles = 0; 41 | } 42 | bool isBlack = PieceHelper.IsColour(piece, PieceHelper.Black); 43 | int pieceType = PieceHelper.PieceType(piece); 44 | char pieceChar = ' '; 45 | switch (pieceType) 46 | { 47 | case PieceHelper.Rook: 48 | pieceChar = 'R'; 49 | break; 50 | case PieceHelper.Knight: 51 | pieceChar = 'N'; 52 | break; 53 | case PieceHelper.Bishop: 54 | pieceChar = 'B'; 55 | break; 56 | case PieceHelper.Queen: 57 | pieceChar = 'Q'; 58 | break; 59 | case PieceHelper.King: 60 | pieceChar = 'K'; 61 | break; 62 | case PieceHelper.Pawn: 63 | pieceChar = 'P'; 64 | break; 65 | } 66 | fen += (isBlack) ? pieceChar.ToString().ToLower() : pieceChar.ToString(); 67 | } 68 | else 69 | { 70 | numEmptyFiles++; 71 | } 72 | 73 | } 74 | if (numEmptyFiles != 0) 75 | { 76 | fen += numEmptyFiles; 77 | } 78 | if (rank != 0) 79 | { 80 | fen += '/'; 81 | } 82 | } 83 | 84 | // Side to move 85 | fen += ' '; 86 | fen += (board.IsWhiteToMove) ? 'w' : 'b'; 87 | 88 | // Castling 89 | bool whiteKingside = (board.currentGameState.castlingRights & 1) == 1; 90 | bool whiteQueenside = (board.currentGameState.castlingRights >> 1 & 1) == 1; 91 | bool blackKingside = (board.currentGameState.castlingRights >> 2 & 1) == 1; 92 | bool blackQueenside = (board.currentGameState.castlingRights >> 3 & 1) == 1; 93 | fen += ' '; 94 | fen += (whiteKingside) ? "K" : ""; 95 | fen += (whiteQueenside) ? "Q" : ""; 96 | fen += (blackKingside) ? "k" : ""; 97 | fen += (blackQueenside) ? "q" : ""; 98 | fen += ((board.currentGameState.castlingRights) == 0) ? "-" : ""; 99 | 100 | // En-passant 101 | fen += ' '; 102 | int epFileIndex = board.currentGameState.enPassantFile - 1; 103 | int epRankIndex = (board.IsWhiteToMove) ? 5 : 2; 104 | 105 | bool isEnPassant = epFileIndex != -1; 106 | bool includeEP = alwaysIncludeEPSquare || EnPassantCanBeCaptured(epFileIndex, epRankIndex, board); 107 | if (isEnPassant && includeEP) 108 | { 109 | fen += BoardHelper.SquareNameFromCoordinate(epFileIndex, epRankIndex); 110 | } 111 | else 112 | { 113 | fen += '-'; 114 | } 115 | 116 | // 50 move counter 117 | fen += ' '; 118 | fen += board.currentGameState.fiftyMoveCounter; 119 | 120 | // Full-move count (should be one at start, and increase after each move by black) 121 | fen += ' '; 122 | fen += (board.plyCount / 2) + 1; 123 | 124 | return fen; 125 | } 126 | 127 | static bool EnPassantCanBeCaptured(int epFileIndex, int epRankIndex, Board board) 128 | { 129 | Coord captureFromA = new Coord(epFileIndex - 1, epRankIndex + (board.IsWhiteToMove ? -1 : 1)); 130 | Coord captureFromB = new Coord(epFileIndex + 1, epRankIndex + (board.IsWhiteToMove ? -1 : 1)); 131 | int epCaptureSquare = new Coord(epFileIndex, epRankIndex).SquareIndex; 132 | int friendlyPawn = PieceHelper.MakePiece(PieceHelper.Pawn, board.MoveColour); 133 | 134 | 135 | 136 | return CanCapture(captureFromA) || CanCapture(captureFromB); 137 | 138 | 139 | bool CanCapture(Coord from) 140 | { 141 | bool isPawnOnSquare = board.Square[from.SquareIndex] == friendlyPawn; 142 | if (from.IsValidSquare() && isPawnOnSquare) 143 | { 144 | Move move = new Move(from.SquareIndex, epCaptureSquare, Move.EnPassantCaptureFlag); 145 | board.MakeMove(move); 146 | board.MakeNullMove(); 147 | bool wasLegalMove = !board.CalculateInCheckState(); 148 | 149 | board.UnmakeNullMove(); 150 | board.UndoMove(move); 151 | return wasLegalMove; 152 | } 153 | 154 | return false; 155 | } 156 | } 157 | 158 | public static string FlipFen(string fen) 159 | { 160 | string flippedFen = ""; 161 | string[] sections = fen.Split(' '); 162 | 163 | List invertedFenChars = new(); 164 | string[] fenRanks = sections[0].Split('/'); 165 | 166 | for (int i = fenRanks.Length - 1; i >= 0; i--) 167 | { 168 | string rank = fenRanks[i]; 169 | foreach (char c in rank) 170 | { 171 | flippedFen += InvertCase(c); 172 | } 173 | if (i != 0) 174 | { 175 | flippedFen += '/'; 176 | } 177 | } 178 | 179 | flippedFen += " " + (sections[1][0] == 'w' ? 'b' : 'w'); 180 | string castlingRights = sections[2]; 181 | string flippedRights = ""; 182 | foreach (char c in "kqKQ") 183 | { 184 | if (castlingRights.Contains(c)) 185 | { 186 | flippedRights += InvertCase(c); 187 | } 188 | } 189 | flippedFen += " " + (flippedRights.Length == 0 ? "-" : flippedRights); 190 | 191 | string ep = sections[3]; 192 | string flippedEp = ep[0] + ""; 193 | if (ep.Length > 1) 194 | { 195 | flippedEp += ep[1] == '6' ? '3' : '6'; 196 | } 197 | flippedFen += " " + flippedEp; 198 | flippedFen += " " + sections[4] + " " + sections[5]; 199 | 200 | 201 | return flippedFen; 202 | 203 | char InvertCase(char c) 204 | { 205 | if (char.IsLower(c)) 206 | { 207 | return char.ToUpper(c); 208 | } 209 | return char.ToLower(c); 210 | } 211 | } 212 | 213 | public readonly struct PositionInfo 214 | { 215 | public readonly string fen; 216 | public readonly ReadOnlyCollection squares; 217 | 218 | // Castling rights 219 | public readonly bool whiteCastleKingside; 220 | public readonly bool whiteCastleQueenside; 221 | public readonly bool blackCastleKingside; 222 | public readonly bool blackCastleQueenside; 223 | // En passant file (1 is a-file, 8 is h-file, 0 means none) 224 | public readonly int epFile; 225 | public readonly bool whiteToMove; 226 | // Number of half-moves since last capture or pawn advance 227 | // (starts at 0 and increments after each player's move) 228 | public readonly int fiftyMovePlyCount; 229 | // Total number of moves played in the game 230 | // (starts at 1 and increments after black's move) 231 | public readonly int moveCount; 232 | 233 | public PositionInfo(string fen) 234 | { 235 | this.fen = fen; 236 | int[] squarePieces = new int[64]; 237 | 238 | string[] sections = fen.Split(' '); 239 | 240 | int file = 0; 241 | int rank = 7; 242 | 243 | foreach (char symbol in sections[0]) 244 | { 245 | if (symbol == '/') 246 | { 247 | file = 0; 248 | rank--; 249 | } 250 | else 251 | { 252 | if (char.IsDigit(symbol)) 253 | { 254 | file += (int)char.GetNumericValue(symbol); 255 | } 256 | else 257 | { 258 | int pieceColour = (char.IsUpper(symbol)) ? PieceHelper.White : PieceHelper.Black; 259 | int pieceType = char.ToLower(symbol) switch 260 | { 261 | 'k' => PieceHelper.King, 262 | 'p' => PieceHelper.Pawn, 263 | 'n' => PieceHelper.Knight, 264 | 'b' => PieceHelper.Bishop, 265 | 'r' => PieceHelper.Rook, 266 | 'q' => PieceHelper.Queen, 267 | _ => PieceHelper.None 268 | }; 269 | 270 | squarePieces[rank * 8 + file] = pieceType | pieceColour; 271 | file++; 272 | } 273 | } 274 | } 275 | 276 | squares = new(squarePieces); 277 | 278 | whiteToMove = (sections[1] == "w"); 279 | 280 | string castlingRights = sections[2]; 281 | whiteCastleKingside = castlingRights.Contains('K'); 282 | whiteCastleQueenside = castlingRights.Contains('Q'); 283 | blackCastleKingside = castlingRights.Contains('k'); 284 | blackCastleQueenside = castlingRights.Contains('q'); 285 | 286 | // Default values 287 | epFile = 0; 288 | fiftyMovePlyCount = 0; 289 | moveCount = 0; 290 | 291 | if (sections.Length > 3) 292 | { 293 | string enPassantFileName = sections[3][0].ToString(); 294 | if (BoardHelper.fileNames.Contains(enPassantFileName)) 295 | { 296 | epFile = BoardHelper.fileNames.IndexOf(enPassantFileName) + 1; 297 | } 298 | } 299 | 300 | // Half-move clock 301 | if (sections.Length > 4) 302 | { 303 | int.TryParse(sections[4], out fiftyMovePlyCount); 304 | } 305 | // Full move number 306 | if (sections.Length > 5) 307 | { 308 | int.TryParse(sections[5], out moveCount); 309 | } 310 | } 311 | } 312 | } 313 | } -------------------------------------------------------------------------------- /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/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 | readonly 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 int debugTokenCount; 57 | readonly StringBuilder pgns; 58 | 59 | public ChallengeController() 60 | { 61 | Log($"Launching Chess-Challenge version {Settings.Version}"); 62 | (tokenCount, debugTokenCount) = GetTokenCount(); 63 | Warmer.Warm(); 64 | 65 | rng = new Random(); 66 | moveGenerator = new(); 67 | boardUI = new BoardUI(); 68 | board = new Board(); 69 | pgns = new(); 70 | 71 | BotStatsA = new BotMatchStats("IBot"); 72 | BotStatsB = new BotMatchStats("IBot"); 73 | botMatchStartFens = FileHelper.ReadResourceFile("Fens.txt").Split('\n').Where(fen => fen.Length > 0).ToArray(); 74 | botTaskWaitHandle = new AutoResetEvent(false); 75 | 76 | StartNewGame(PlayerType.Human, PlayerType.MyBot); 77 | } 78 | 79 | public void StartNewGame(PlayerType whiteType, PlayerType blackType) 80 | { 81 | // End any ongoing game 82 | EndGame(GameResult.DrawByArbiter, log: false, autoStartNextBotMatch: false); 83 | gameID = rng.Next(); 84 | 85 | // Stop prev task and create a new one 86 | if (RunBotsOnSeparateThread) 87 | { 88 | // Allow task to terminate 89 | botTaskWaitHandle.Set(); 90 | // Create new task 91 | botTaskWaitHandle = new AutoResetEvent(false); 92 | Task.Factory.StartNew(BotThinkerThread, TaskCreationOptions.LongRunning); 93 | } 94 | // Board Setup 95 | board = new Board(); 96 | bool isGameWithHuman = whiteType is PlayerType.Human || blackType is PlayerType.Human; 97 | int fenIndex = isGameWithHuman ? 0 : botMatchGameIndex / 2; 98 | board.LoadPosition(botMatchStartFens[fenIndex]); 99 | 100 | // Player Setup 101 | PlayerWhite = CreatePlayer(whiteType); 102 | PlayerBlack = CreatePlayer(blackType); 103 | PlayerWhite.SubscribeToMoveChosenEventIfHuman(OnMoveChosen); 104 | PlayerBlack.SubscribeToMoveChosenEventIfHuman(OnMoveChosen); 105 | 106 | // UI Setup 107 | boardUI.UpdatePosition(board); 108 | boardUI.ResetSquareColours(); 109 | SetBoardPerspective(); 110 | 111 | // Start 112 | isPlaying = true; 113 | NotifyTurnToMove(); 114 | } 115 | 116 | void BotThinkerThread() 117 | { 118 | int threadID = gameID; 119 | //Console.WriteLine("Starting thread: " + threadID); 120 | 121 | while (true) 122 | { 123 | // Sleep thread until notified 124 | botTaskWaitHandle.WaitOne(); 125 | // Get bot move 126 | if (threadID == gameID) 127 | { 128 | var move = GetBotMove(); 129 | 130 | if (threadID == gameID) 131 | { 132 | OnMoveChosen(move); 133 | } 134 | } 135 | // Terminate if no longer playing this game 136 | if (threadID != gameID) 137 | { 138 | break; 139 | } 140 | } 141 | //Console.WriteLine("Exitting thread: " + threadID); 142 | } 143 | 144 | Move GetBotMove() 145 | { 146 | API.Board botBoard = new(board); 147 | try 148 | { 149 | API.Timer timer = new(PlayerToMove.TimeRemainingMs, PlayerNotOnMove.TimeRemainingMs, GameDurationMilliseconds, IncrementMilliseconds); 150 | API.Move move = PlayerToMove.Bot.Think(botBoard, timer); 151 | return new Move(move.RawValue); 152 | } 153 | catch (Exception e) 154 | { 155 | Log("An error occurred while bot was thinking.\n" + e.ToString(), true, ConsoleColor.Red); 156 | hasBotTaskException = true; 157 | botExInfo = ExceptionDispatchInfo.Capture(e); 158 | } 159 | return Move.NullMove; 160 | } 161 | 162 | 163 | 164 | void NotifyTurnToMove() 165 | { 166 | //playerToMove.NotifyTurnToMove(board); 167 | if (PlayerToMove.IsHuman) 168 | { 169 | PlayerToMove.Human.SetPosition(FenUtility.CurrentFen(board)); 170 | PlayerToMove.Human.NotifyTurnToMove(); 171 | } 172 | else 173 | { 174 | if (RunBotsOnSeparateThread) 175 | { 176 | botTaskWaitHandle.Set(); 177 | } 178 | else 179 | { 180 | double startThinkTime = Raylib.GetTime(); 181 | var move = GetBotMove(); 182 | double thinkDuration = Raylib.GetTime() - startThinkTime; 183 | PlayerToMove.UpdateClock(thinkDuration); 184 | OnMoveChosen(move); 185 | } 186 | } 187 | } 188 | 189 | void SetBoardPerspective() 190 | { 191 | // Board perspective 192 | if (PlayerWhite.IsHuman || PlayerBlack.IsHuman) 193 | { 194 | boardUI.SetPerspective(PlayerWhite.IsHuman); 195 | HumanWasWhiteLastGame = PlayerWhite.IsHuman; 196 | } 197 | else if (PlayerWhite.Bot is MyBot && PlayerBlack.Bot is MyBot) 198 | { 199 | boardUI.SetPerspective(true); 200 | } 201 | else 202 | { 203 | boardUI.SetPerspective(PlayerWhite.Bot is MyBot); 204 | } 205 | } 206 | 207 | ChessPlayer CreatePlayer(PlayerType type) 208 | { 209 | return type switch 210 | { 211 | PlayerType.MyBot => new ChessPlayer(new MyBot(), type, GameDurationMilliseconds), 212 | PlayerType.EvilBot => new ChessPlayer(new EvilBot(), type, GameDurationMilliseconds), 213 | _ => new ChessPlayer(new HumanPlayer(boardUI), type) 214 | }; 215 | } 216 | 217 | static (int totalTokenCount, int debugTokenCount) GetTokenCount() 218 | { 219 | string path = Path.Combine(Directory.GetCurrentDirectory(), "src", "My Bot", "MyBot.cs"); 220 | 221 | using StreamReader reader = new(path); 222 | string txt = reader.ReadToEnd(); 223 | return TokenCounter.CountTokens(txt); 224 | } 225 | 226 | void OnMoveChosen(Move chosenMove) 227 | { 228 | if (IsLegal(chosenMove)) 229 | { 230 | PlayerToMove.AddIncrement(IncrementMilliseconds); 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 | 390 | public void DrawOverlay() 391 | { 392 | BotBrainCapacityUI.Draw(tokenCount, debugTokenCount, MaxTokenCount); 393 | MenuUI.DrawButtons(this); 394 | MatchStatsUI.DrawMatchStats(this); 395 | } 396 | 397 | static string GetPlayerName(ChessPlayer player) => GetPlayerName(player.PlayerType); 398 | static string GetPlayerName(PlayerType type) => type.ToString(); 399 | 400 | public void StartNewBotMatch(PlayerType botTypeA, PlayerType botTypeB) 401 | { 402 | EndGame(GameResult.DrawByArbiter, log: false, autoStartNextBotMatch: false); 403 | botMatchGameIndex = 0; 404 | string nameA = GetPlayerName(botTypeA); 405 | string nameB = GetPlayerName(botTypeB); 406 | if (nameA == nameB) 407 | { 408 | nameA += " (A)"; 409 | nameB += " (B)"; 410 | } 411 | BotStatsA = new BotMatchStats(nameA); 412 | BotStatsB = new BotMatchStats(nameB); 413 | botAPlaysWhite = true; 414 | Log($"Starting new match: {nameA} vs {nameB}", false, ConsoleColor.Blue); 415 | StartNewGame(botTypeA, botTypeB); 416 | } 417 | 418 | 419 | ChessPlayer PlayerToMove => board.IsWhiteToMove ? PlayerWhite : PlayerBlack; 420 | ChessPlayer PlayerNotOnMove => board.IsWhiteToMove ? PlayerBlack : PlayerWhite; 421 | 422 | public int TotalGameCount => botMatchStartFens.Length * 2; 423 | public int CurrGameNumber => Math.Min(TotalGameCount, botMatchGameIndex + 1); 424 | public string AllPGNs => pgns.ToString(); 425 | 426 | 427 | bool IsLegal(Move givenMove) 428 | { 429 | var moves = moveGenerator.GenerateMoves(board); 430 | foreach (var legalMove in moves) 431 | { 432 | if (givenMove.Value == legalMove.Value) 433 | { 434 | return true; 435 | } 436 | } 437 | 438 | return false; 439 | } 440 | 441 | public class BotMatchStats 442 | { 443 | public string BotName; 444 | public int NumWins; 445 | public int NumLosses; 446 | public int NumDraws; 447 | public int NumTimeouts; 448 | public int NumIllegalMoves; 449 | 450 | public BotMatchStats(string name) => BotName = name; 451 | } 452 | 453 | public void Release() 454 | { 455 | boardUI.Release(); 456 | } 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /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 | using ChessChallenge.Application.APIHelpers; 9 | 10 | namespace ChessChallenge.Application 11 | { 12 | public class BoardUI 13 | { 14 | 15 | // Board settings 16 | const int squareSize = 100; 17 | const double moveAnimDuration = 0.15; 18 | bool whitePerspective = true; 19 | 20 | // Text colours 21 | static readonly Color activeTextCol = new(200, 200, 200, 255); 22 | static readonly Color inactiveTextCol = new(100, 100, 100, 255); 23 | static readonly Color nameCol = new(67, 204, 101, 255); 24 | 25 | // Bitboard debug mode 26 | static readonly Color bitboardColZERO = new(61, 121, 217, 200); 27 | static readonly Color bitboardColONE = new(252, 43, 92, 200); 28 | 29 | // Colour state 30 | Color topTextCol; 31 | Color bottomTextCol; 32 | 33 | // Drag state 34 | bool isDraggingPiece; 35 | int dragSquare; 36 | Vector2 dragPos; 37 | 38 | static readonly int[] pieceImageOrder = { 5, 3, 2, 4, 1, 0 }; 39 | Texture2D piecesTexture; 40 | BoardTheme theme; 41 | Dictionary squareColOverrides; 42 | Board board; 43 | Move lastMove; 44 | 45 | // Animate move state 46 | Board animateMoveTargetBoardState; 47 | Move moveToAnimate; 48 | double moveAnimStartTime; 49 | bool isAnimatingMove; 50 | 51 | 52 | public enum HighlightType 53 | { 54 | MoveFrom, 55 | MoveTo, 56 | LegalMove, 57 | Check 58 | } 59 | 60 | 61 | public BoardUI() 62 | { 63 | theme = new BoardTheme(); 64 | 65 | LoadPieceTexture(); 66 | 67 | board = new Board(); 68 | board.LoadStartPosition(); 69 | squareColOverrides = new Dictionary(); 70 | topTextCol = inactiveTextCol; 71 | bottomTextCol = inactiveTextCol; 72 | } 73 | 74 | public void SetPerspective(bool whitePerspective) 75 | { 76 | this.whitePerspective = whitePerspective; 77 | } 78 | 79 | public void UpdatePosition(Board board) 80 | { 81 | isAnimatingMove = false; 82 | 83 | // Update 84 | this.board = new(board); 85 | lastMove = Move.NullMove; 86 | if (board.IsInCheck()) 87 | { 88 | OverrideSquareColour(board.KingSquare[board.MoveColourIndex], HighlightType.Check); 89 | } 90 | } 91 | 92 | public void UpdatePosition(Board board, Move moveMade, bool animate = false) 93 | { 94 | // Interrupt prev animation 95 | if (isAnimatingMove) 96 | { 97 | UpdatePosition(animateMoveTargetBoardState); 98 | isAnimatingMove = false; 99 | } 100 | 101 | ResetSquareColours(); 102 | if (animate) 103 | { 104 | OverrideSquareColour(moveMade.StartSquareIndex, HighlightType.MoveFrom); 105 | animateMoveTargetBoardState = new Board(board); 106 | moveToAnimate = moveMade; 107 | moveAnimStartTime = Raylib.GetTime(); 108 | isAnimatingMove = true; 109 | } 110 | else 111 | { 112 | UpdatePosition(board); 113 | 114 | if (!moveMade.IsNull) 115 | { 116 | HighlightMove(moveMade); 117 | lastMove = moveMade; 118 | } 119 | } 120 | } 121 | 122 | void HighlightMove(Move move) 123 | { 124 | OverrideSquareColour(move.StartSquareIndex, HighlightType.MoveFrom); 125 | OverrideSquareColour(move.TargetSquareIndex, HighlightType.MoveTo); 126 | } 127 | 128 | public void DragPiece(int square, Vector2 worldPos) 129 | { 130 | isDraggingPiece = true; 131 | dragSquare = square; 132 | dragPos = worldPos; 133 | } 134 | 135 | public bool TryGetSquareAtPoint(Vector2 worldPos, out int squareIndex) 136 | { 137 | Vector2 boardStartPosWorld = new Vector2(squareSize, squareSize) * -4; 138 | Vector2 endPosWorld = boardStartPosWorld + new Vector2(8, 8) * squareSize; 139 | 140 | float tx = (worldPos.X - boardStartPosWorld.X) / (endPosWorld.X - boardStartPosWorld.X); 141 | float ty = (worldPos.Y - boardStartPosWorld.Y) / (endPosWorld.Y - boardStartPosWorld.Y); 142 | 143 | if (tx >= 0 && tx <= 1 && ty >= 0 && ty <= 1) 144 | { 145 | if (!whitePerspective) 146 | { 147 | tx = 1 - tx; 148 | ty = 1 - ty; 149 | } 150 | squareIndex = new Coord((int)(tx * 8), 7 - (int)(ty * 8)).SquareIndex; 151 | return true; 152 | } 153 | 154 | squareIndex = -1; 155 | return false; 156 | } 157 | 158 | public void OverrideSquareColour(int square, HighlightType hightlightType) 159 | { 160 | bool isLight = new Coord(square).IsLightSquare(); 161 | 162 | Color col = hightlightType switch 163 | { 164 | HighlightType.MoveFrom => isLight ? theme.MoveFromLight : theme.MoveFromDark, 165 | HighlightType.MoveTo => isLight ? theme.MoveToLight : theme.MoveToDark, 166 | HighlightType.LegalMove => isLight ? theme.LegalLight : theme.LegalDark, 167 | HighlightType.Check => isLight ? theme.CheckLight : theme.CheckDark, 168 | _ => Color.PINK 169 | }; 170 | 171 | if (squareColOverrides.ContainsKey(square)) 172 | { 173 | squareColOverrides[square] = col; 174 | } 175 | else 176 | { 177 | squareColOverrides.Add(square, col); 178 | } 179 | } 180 | 181 | public void HighlightLegalMoves(Board board, int square) 182 | { 183 | MoveGenerator moveGenerator = new(); 184 | var moves = moveGenerator.GenerateMoves(board); 185 | foreach (var move in moves) 186 | { 187 | if (move.StartSquareIndex == square) 188 | { 189 | OverrideSquareColour(move.TargetSquareIndex, HighlightType.LegalMove); 190 | } 191 | } 192 | } 193 | 194 | public void Draw() 195 | { 196 | double animT = (Raylib.GetTime() - moveAnimStartTime) / moveAnimDuration; 197 | 198 | if (isAnimatingMove && animT >= 1) 199 | { 200 | isAnimatingMove = false; 201 | UpdatePosition(animateMoveTargetBoardState, moveToAnimate, false); 202 | } 203 | 204 | DrawBorder(); 205 | ForEachSquare(DrawSquare); 206 | 207 | if (isAnimatingMove) 208 | { 209 | UpdateMoveAnimation(animT); 210 | } 211 | 212 | if (BitboardDebugState.BitboardDebugVisualizationRequested) 213 | { 214 | ForEachSquare(DrawBitboardDebugOverlaySquare); 215 | } 216 | 217 | if (isDraggingPiece) 218 | { 219 | DrawPiece(board.Square[dragSquare], dragPos - new Vector2(squareSize * 0.5f, squareSize * 0.5f)); 220 | } 221 | 222 | 223 | // Reset state 224 | isDraggingPiece = false; 225 | } 226 | 227 | static void ForEachSquare(Action action) 228 | { 229 | for (int y = 0; y < 8; y++) 230 | { 231 | for (int x = 0; x < 8; x++) 232 | { 233 | action(x, y); 234 | } 235 | } 236 | } 237 | 238 | void UpdateMoveAnimation(double animT) 239 | { 240 | Coord startCoord = new Coord(moveToAnimate.StartSquareIndex); 241 | Coord targetCoord = new Coord(moveToAnimate.TargetSquareIndex); 242 | Vector2 startPos = GetSquarePos(startCoord.fileIndex, startCoord.rankIndex, whitePerspective); 243 | Vector2 targetPos = GetSquarePos(targetCoord.fileIndex, targetCoord.rankIndex, whitePerspective); 244 | 245 | Vector2 animPos = Vector2.Lerp(startPos, targetPos, (float)animT); 246 | DrawPiece(board.Square[moveToAnimate.StartSquareIndex], animPos); 247 | } 248 | 249 | public void DrawPlayerNames(string nameWhite, string nameBlack, int timeWhite, int timeBlack, bool isPlaying) 250 | { 251 | string nameBottom = whitePerspective ? nameWhite : nameBlack; 252 | string nameTop = !whitePerspective ? nameWhite : nameBlack; 253 | int timeBottom = whitePerspective ? timeWhite : timeBlack; 254 | int timeTop = !whitePerspective ? timeWhite : timeBlack; 255 | bool bottomTurnToMove = whitePerspective == board.IsWhiteToMove && isPlaying; 256 | bool topTurnToMove = whitePerspective != board.IsWhiteToMove && isPlaying; 257 | 258 | string colNameBottom = whitePerspective ? "White" : "Black"; 259 | string colNameTop = !whitePerspective ? "White" : "Black"; 260 | 261 | int boardStartX = -squareSize * 4; 262 | int boardStartY = -squareSize * 4; 263 | const int spaceY = 35; 264 | 265 | 266 | Color textTopTargetCol = topTurnToMove ? activeTextCol : inactiveTextCol; 267 | Color textBottomTargetCol = bottomTurnToMove ? activeTextCol : inactiveTextCol; 268 | 269 | float colLerpSpeed = 16; 270 | topTextCol = LerpColour(topTextCol, textTopTargetCol, Raylib.GetFrameTime() * colLerpSpeed); 271 | bottomTextCol = LerpColour(bottomTextCol, textBottomTargetCol, Raylib.GetFrameTime() * colLerpSpeed); 272 | 273 | //Color textColTop = topTurnToMove ? activeTextCol : inactiveTextCol; 274 | 275 | Draw(boardStartY + squareSize * 8 + spaceY, colNameBottom, nameBottom, timeBottom, bottomTextCol); 276 | Draw(boardStartY - spaceY, colNameTop, nameTop, timeTop, topTextCol); 277 | 278 | void Draw(float y, string colName, string name, int timeMs, Color textCol) 279 | { 280 | const int fontSize = 36; 281 | const int fontSpacing = 1; 282 | var namePos = new Vector2(boardStartX, y); 283 | 284 | UIHelper.DrawText($"{colName}: {name}", namePos, fontSize, fontSpacing, nameCol); 285 | var timePos = new Vector2(boardStartX + squareSize * 8, y); 286 | string timeText; 287 | if (timeMs == int.MaxValue) 288 | { 289 | timeText = "Time: Unlimited"; 290 | } 291 | else 292 | { 293 | double secondsRemaining = timeMs / 1000.0; 294 | int numMinutes = (int)(secondsRemaining / 60); 295 | int numSeconds = (int)(secondsRemaining - numMinutes * 60); 296 | int dec = (int)((secondsRemaining - numMinutes * 60 - numSeconds) * 10); 297 | 298 | timeText = $"Time: {numMinutes:00}:{numSeconds:00}.{dec}"; 299 | } 300 | UIHelper.DrawText(timeText, timePos, fontSize, fontSpacing, textCol, UIHelper.AlignH.Right); 301 | } 302 | } 303 | 304 | public void ResetSquareColours(bool keepPrevMoveHighlight = false) 305 | { 306 | squareColOverrides.Clear(); 307 | if (keepPrevMoveHighlight && !lastMove.IsNull) 308 | { 309 | HighlightMove(lastMove); 310 | } 311 | } 312 | 313 | 314 | void DrawBorder() 315 | { 316 | int boardStartX = -squareSize * 4; 317 | int boardStartY = -squareSize * 4; 318 | int w = 12; 319 | Raylib.DrawRectangle(boardStartX - w, boardStartY - w, 8 * squareSize + w * 2, 8 * squareSize + w * 2, theme.BorderCol); 320 | } 321 | 322 | void DrawSquare(int file, int rank) 323 | { 324 | 325 | Coord coord = new Coord(file, rank); 326 | Color col = coord.IsLightSquare() ? theme.LightCol : theme.DarkCol; 327 | if (squareColOverrides.TryGetValue(coord.SquareIndex, out Color overrideCol)) 328 | { 329 | col = overrideCol; 330 | } 331 | 332 | // top left 333 | Vector2 pos = GetSquarePos(file, rank, whitePerspective); 334 | Raylib.DrawRectangle((int)pos.X, (int)pos.Y, squareSize, squareSize, col); 335 | int piece = board.Square[coord.SquareIndex]; 336 | float alpha = isDraggingPiece && dragSquare == coord.SquareIndex ? 0.3f : 1; 337 | if (!isAnimatingMove || coord.SquareIndex != moveToAnimate.StartSquareIndex) 338 | { 339 | DrawPiece(piece, new Vector2((int)pos.X, (int)pos.Y), alpha); 340 | } 341 | 342 | if (Settings.DisplayBoardCoordinates) 343 | { 344 | int textSize = 25; 345 | float xpadding = 5f; 346 | float ypadding = 2f; 347 | Color coordNameCol = coord.IsLightSquare() ? theme.DarkCoordCol : theme.LightCoordCol; 348 | 349 | if (rank == (whitePerspective ? 0 : 7)) 350 | { 351 | string fileName = BoardHelper.fileNames[file] + ""; 352 | Vector2 drawPos = pos + new Vector2(xpadding, squareSize - ypadding); 353 | DrawText(fileName, drawPos, textSize, 0, coordNameCol, AlignH.Left, AlignV.Bottom); 354 | } 355 | if (file == (whitePerspective ? 7 : 0)) 356 | { 357 | string rankName = (rank + 1) + ""; 358 | Vector2 drawPos = pos + new Vector2(squareSize - xpadding, ypadding); 359 | DrawText(rankName, drawPos, textSize, 0, coordNameCol, AlignH.Right, AlignV.Top); 360 | } 361 | } 362 | } 363 | 364 | void DrawBitboardDebugOverlaySquare(int file, int rank) 365 | { 366 | ulong bitboard = BitboardDebugState.BitboardToVisualize; 367 | bool isSet = BitBoardUtility.ContainsSquare(bitboard, new Coord(file,rank).SquareIndex); 368 | Color col = isSet ? bitboardColONE : bitboardColZERO; 369 | 370 | Vector2 squarePos = GetSquarePos(file, rank, whitePerspective); 371 | Raylib.DrawRectangle((int)squarePos.X, (int)squarePos.Y, squareSize, squareSize, col); 372 | Vector2 textPos = squarePos + new Vector2(squareSize, squareSize) / 2; 373 | DrawText(isSet ? "1" : "0", textPos, 50, 0, Color.WHITE, AlignH.Centre); 374 | } 375 | 376 | static Vector2 GetSquarePos(int file, int rank, bool whitePerspective) 377 | { 378 | const int boardStartX = -squareSize * 4; 379 | const int boardStartY = -squareSize * 4; 380 | 381 | if (!whitePerspective) 382 | { 383 | file = 7 - file; 384 | rank = 7 - rank; 385 | } 386 | 387 | int posX = boardStartX + file * squareSize; 388 | int posY = boardStartY + (7 - rank) * squareSize; 389 | return new Vector2(posX, posY); 390 | } 391 | 392 | void DrawPiece(int piece, Vector2 posTopLeft, float alpha = 1) 393 | { 394 | if (piece != PieceHelper.None) 395 | { 396 | int type = PieceHelper.PieceType(piece); 397 | bool white = PieceHelper.IsWhite(piece); 398 | Rectangle srcRect = GetPieceTextureRect(type, white); 399 | Rectangle targRect = new Rectangle((int)posTopLeft.X, (int)posTopLeft.Y, squareSize, squareSize); 400 | 401 | Color tint = new Color(255, 255, 255, (int)MathF.Round(255 * alpha)); 402 | Raylib.DrawTexturePro(piecesTexture, srcRect, targRect, new Vector2(0, 0), 0, tint); 403 | } 404 | } 405 | 406 | static Color LerpColour(Color a, Color b, float t) 407 | { 408 | int newR = (int)(Math.Round(Lerp(a.r, b.r, t))); 409 | int newG = (int)(Math.Round(Lerp(a.g, b.g, t))); 410 | int newB = (int)(Math.Round(Lerp(a.b, b.b, t))); 411 | int newA = (int)(Math.Round(Lerp(a.a, b.a, t))); 412 | return new Color(newR, newG, newB, newA); 413 | 414 | float Lerp(float a, float b, float t) 415 | { 416 | t = Math.Min(1, Math.Max(t, 0)); 417 | return a + (b - a) * t; 418 | } 419 | } 420 | 421 | void LoadPieceTexture() 422 | { 423 | // Workaround for Raylib.LoadTexture() not working when path contains non-ascii chars 424 | byte[] pieceImgBytes = File.ReadAllBytes(FileHelper.GetResourcePath("Pieces.png")); 425 | Image pieceImg = Raylib.LoadImageFromMemory(".png", pieceImgBytes); 426 | piecesTexture = Raylib.LoadTextureFromImage(pieceImg); 427 | Raylib.UnloadImage(pieceImg); 428 | 429 | Raylib.GenTextureMipmaps(ref piecesTexture); 430 | Raylib.SetTextureWrap(piecesTexture, TextureWrap.TEXTURE_WRAP_CLAMP); 431 | Raylib.SetTextureFilter(piecesTexture, TextureFilter.TEXTURE_FILTER_BILINEAR); 432 | } 433 | 434 | public void Release() 435 | { 436 | Raylib.UnloadTexture(piecesTexture); 437 | } 438 | 439 | static Rectangle GetPieceTextureRect(int pieceType, bool isWhite) 440 | { 441 | const int size = 333; 442 | return new Rectangle(size * pieceImageOrder[pieceType - 1], isWhite ? 0 : size, size, size); 443 | } 444 | } 445 | } -------------------------------------------------------------------------------- /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 | using System.Text; 9 | 10 | public sealed class Board 11 | { 12 | readonly Chess.Board board; 13 | readonly APIMoveGen moveGen; 14 | readonly RepetitionTable repetitionTable; 15 | 16 | readonly PieceList[] allPieceLists; 17 | readonly PieceList[] validPieceLists; 18 | 19 | readonly Move[] movesDest; 20 | Move[] cachedLegalMoves; 21 | bool hasCachedMoves; 22 | Move[] cachedLegalCaptureMoves; 23 | bool hasCachedCaptureMoves; 24 | bool hasCachedMoveCount; 25 | int cachedMoveCount; 26 | int depth; 27 | 28 | /// 29 | /// Create a new board. Note: this should not be used in the challenge, 30 | /// use the board provided in the Think method instead. 31 | /// 32 | public Board(Chess.Board boardSource) 33 | { 34 | // Clone board and create game move history 35 | board = new Chess.Board(); 36 | board.LoadPosition(boardSource.StartPositionInfo); 37 | GameMoveHistory = new Move[boardSource.AllGameMoves.Count]; 38 | repetitionTable = new(); 39 | 40 | for (int i = 0; i < boardSource.AllGameMoves.Count; i ++) 41 | { 42 | Chess.Move move = boardSource.AllGameMoves[i]; 43 | int movePieceType = PieceHelper.PieceType(board.Square[move.StartSquareIndex]); 44 | int capturePieceType = move.IsEnPassant ? PieceHelper.Pawn : PieceHelper.PieceType(board.Square[move.TargetSquareIndex]); 45 | GameMoveHistory[i] = new Move(move, movePieceType, capturePieceType); 46 | board.MakeMove(move, false); 47 | } 48 | 49 | // Init move gen 50 | moveGen = new APIMoveGen(); 51 | cachedLegalMoves = Array.Empty(); 52 | cachedLegalCaptureMoves = Array.Empty(); 53 | movesDest = new Move[APIMoveGen.MaxMoves]; 54 | 55 | // Init piece lists 56 | List validPieceLists = new(); 57 | allPieceLists = new PieceList[board.pieceLists.Length]; 58 | for (int i = 0; i < board.pieceLists.Length; i++) 59 | { 60 | if (board.pieceLists[i] != null) 61 | { 62 | allPieceLists[i] = new PieceList(board.pieceLists[i], this, i); 63 | validPieceLists.Add(allPieceLists[i]); 64 | } 65 | } 66 | this.validPieceLists = validPieceLists.ToArray(); 67 | 68 | // Init rep history 69 | GameRepetitionHistory = board.RepetitionPositionHistory.ToArray(); 70 | repetitionTable.Init(board); 71 | } 72 | 73 | /// 74 | /// Updates the board state with the given move. 75 | /// The move is assumed to be legal, and may result in errors if it is not. 76 | /// Can be undone with the UndoMove method. 77 | /// 78 | public void MakeMove(Move move) 79 | { 80 | if (!move.IsNull) 81 | { 82 | OnPositionChanged(); 83 | board.MakeMove(new Chess.Move(move.RawValue), inSearch: true); 84 | repetitionTable.Push(ZobristKey, move.IsCapture || move.MovePieceType == PieceType.Pawn); 85 | depth++; 86 | 87 | } 88 | } 89 | 90 | /// 91 | /// Undo a move that was made with the MakeMove method 92 | /// 93 | public void UndoMove(Move move) 94 | { 95 | if (!move.IsNull) 96 | { 97 | repetitionTable.TryPop(); 98 | board.UndoMove(new Chess.Move(move.RawValue), inSearch: true); 99 | OnPositionChanged(); 100 | depth--; 101 | } 102 | } 103 | 104 | /// 105 | /// Try skip the current turn. 106 | /// This will fail and return false if in check. 107 | /// Note: skipping a turn is not allowed in the game, but it can be used as a search technique. 108 | /// Skipped turns can be undone with UndoSkipTurn() 109 | /// 110 | public bool TrySkipTurn() 111 | { 112 | if (IsInCheck()) 113 | { 114 | return false; 115 | } 116 | board.MakeNullMove(); 117 | OnPositionChanged(); 118 | return true; 119 | } 120 | 121 | /// 122 | /// Forcibly skips the current turn. 123 | /// Unlike TrySkipTurn(), this will work even when in check, which has some dangerous side-effects if done: 124 | /// 1) Generating 'legal' moves will now include the illegal capture of the king. 125 | /// 2) If the skipped turn is undone, the board will now incorrectly report that the position is not check. 126 | /// Note: skipping a turn is not allowed in the game, but it can be used as a search technique. 127 | /// Skipped turns can be undone with UndoSkipTurn() 128 | /// 129 | public void ForceSkipTurn() 130 | { 131 | board.MakeNullMove(); 132 | OnPositionChanged(); 133 | } 134 | 135 | /// 136 | /// Undo a turn that was succesfully skipped with TrySkipTurn() or ForceSkipTurn() 137 | /// 138 | public void UndoSkipTurn() 139 | { 140 | board.UnmakeNullMove(); 141 | OnPositionChanged(); 142 | } 143 | 144 | /// 145 | /// Gets an array of the legal moves in the current position. 146 | /// Can choose to get only capture moves with the optional 'capturesOnly' parameter. 147 | /// 148 | public Move[] GetLegalMoves(bool capturesOnly = false) 149 | { 150 | if (capturesOnly) 151 | { 152 | return GetLegalCaptureMoves(); 153 | } 154 | 155 | if (!hasCachedMoves) 156 | { 157 | Span moveSpan = movesDest.AsSpan(); 158 | moveGen.GenerateMoves(ref moveSpan, board, includeQuietMoves: true); 159 | cachedLegalMoves = moveSpan.ToArray(); 160 | hasCachedMoves = true; 161 | hasCachedMoveCount = true; 162 | cachedMoveCount = moveSpan.Length; 163 | } 164 | 165 | return cachedLegalMoves; 166 | } 167 | 168 | /// 169 | /// Fills the given move span with legal moves, and slices it to the correct length. 170 | /// Can choose to get only capture moves with the optional 'capturesOnly' parameter. 171 | /// This gives the same result as the GetLegalMoves function, but allows you to be more 172 | /// efficient with memory by allocating moves on the stack rather than the heap. 173 | /// 174 | public void GetLegalMovesNonAlloc(ref Span moveList, bool capturesOnly = false) 175 | { 176 | bool includeQuietMoves = !capturesOnly; 177 | moveGen.GenerateMoves(ref moveList, board, includeQuietMoves); 178 | 179 | if (!capturesOnly) 180 | { 181 | hasCachedMoveCount = true; 182 | cachedMoveCount = moveList.Length; 183 | } 184 | } 185 | 186 | 187 | Move[] GetLegalCaptureMoves() 188 | { 189 | if (!hasCachedCaptureMoves) 190 | { 191 | Span moveSpan = movesDest.AsSpan(); 192 | moveGen.GenerateMoves(ref moveSpan, board, includeQuietMoves: false); 193 | cachedLegalCaptureMoves = moveSpan.ToArray(); 194 | hasCachedCaptureMoves = true; 195 | } 196 | return cachedLegalCaptureMoves; 197 | } 198 | 199 | /// 200 | /// Test if the player to move is in check in the current position. 201 | /// 202 | public bool IsInCheck() => moveGen.IsInitialized ? moveGen.InCheck() : board.IsInCheck(); 203 | 204 | /// 205 | /// Test if the current position is checkmate 206 | /// 207 | public bool IsInCheckmate() => IsInCheck() && HasZeroLegalMoves(); 208 | 209 | /// 210 | /// Test if the current position is a draw due stalemate, repetition, insufficient material, or 50-move rule. 211 | /// Note: this function will return true if the same position has occurred twice on the board (rather than 3 times, 212 | /// which is when the game is actually drawn). This quirk is to help bots avoid repeating positions unnecessarily. 213 | /// 214 | public bool IsDraw() 215 | { 216 | return IsFiftyMoveDraw() || IsInsufficientMaterial() || IsInStalemate() || IsRepeatedPosition(); 217 | } 218 | 219 | /// 220 | /// Test if the current position is a draw due to stalemate 221 | /// 222 | public bool IsInStalemate() => !IsInCheck() && HasZeroLegalMoves(); 223 | 224 | /// 225 | /// Test if the current position is a draw due to the fifty move rule 226 | /// 227 | public bool IsFiftyMoveDraw() => board.currentGameState.fiftyMoveCounter >= 100; 228 | 229 | /// 230 | /// Test if the current position has occurred at least once before on the board. 231 | /// This includes both positions in the actual game, and positions reached by 232 | /// making moves while the bot is thinking. 233 | /// 234 | public bool IsRepeatedPosition() => depth > 0 && repetitionTable.Contains(board.ZobristKey); 235 | 236 | /// 237 | /// Test if there are sufficient pieces remaining on the board to potentially deliver checkmate. 238 | /// If not, the game is automatically a draw. 239 | /// 240 | public bool IsInsufficientMaterial() => Arbiter.InsufficentMaterial(board); 241 | 242 | /// 243 | /// Does the given player still have the right to castle kingside? 244 | /// Note that having the right to castle doesn't necessarily mean castling is legal right now 245 | /// (for example, a piece might be in the way, or player might be in check, etc). 246 | /// 247 | public bool HasKingsideCastleRight(bool white) => board.currentGameState.HasKingsideCastleRight(white); 248 | 249 | /// 250 | /// Does the given player still have the right to castle queenside? 251 | /// Note that having the right to castle doesn't necessarily mean castling is legal right now 252 | /// (for example, a piece might be in the way, or player might be in check, etc). 253 | /// 254 | public bool HasQueensideCastleRight(bool white) => board.currentGameState.HasQueensideCastleRight(white); 255 | 256 | /// 257 | /// Gets the square that the king (of the given colour) is currently on. 258 | /// 259 | public Square GetKingSquare(bool white) 260 | { 261 | int colIndex = white ? Chess.Board.WhiteIndex : Chess.Board.BlackIndex; 262 | return new Square(board.KingSquare[colIndex]); 263 | } 264 | 265 | /// 266 | /// Gets the piece on the given square. If the square is empty, the piece will have a PieceType of None. 267 | /// 268 | public Piece GetPiece(Square square) 269 | { 270 | int p = board.Square[square.Index]; 271 | bool white = PieceHelper.IsWhite(p); 272 | return new Piece((PieceType)PieceHelper.PieceType(p), white, square); 273 | } 274 | 275 | /// 276 | /// Gets a list of pieces of the given type and colour 277 | /// 278 | public PieceList GetPieceList(PieceType pieceType, bool white) 279 | { 280 | return allPieceLists[PieceHelper.MakePiece((int)pieceType, white)]; 281 | } 282 | /// 283 | /// Gets an array of all the piece lists. In order these are: 284 | /// Pawns(white), Knights (white), Bishops (white), Rooks (white), Queens (white), King (white), 285 | /// Pawns (black), Knights (black), Bishops (black), Rooks (black), Queens (black), King (black) 286 | /// 287 | public PieceList[] GetAllPieceLists() 288 | { 289 | return validPieceLists; 290 | } 291 | 292 | /// 293 | /// Is the given square attacked by the opponent? 294 | /// (opponent being whichever player doesn't currently have the right to move) 295 | /// 296 | public bool SquareIsAttackedByOpponent(Square square) 297 | { 298 | return BitboardHelper.SquareIsSet(moveGen.GetOpponentAttackMap(board), square); 299 | } 300 | 301 | 302 | /// 303 | /// FEN representation of the current position 304 | /// 305 | public string GetFenString() => FenUtility.CurrentFen(board); 306 | 307 | /// 308 | /// 64-bit number where each bit that is set to 1 represents a 309 | /// square that contains a piece of the given type and colour. 310 | /// 311 | public ulong GetPieceBitboard(PieceType pieceType, bool white) 312 | { 313 | return board.pieceBitboards[PieceHelper.MakePiece((int)pieceType, white)]; 314 | } 315 | /// 316 | /// 64-bit number where each bit that is set to 1 represents a square that contains any type of white piece. 317 | /// 318 | public ulong WhitePiecesBitboard => board.colourBitboards[Chess.Board.WhiteIndex]; 319 | /// 320 | /// 64-bit number where each bit that is set to 1 represents a square that contains any type of black piece. 321 | /// 322 | public ulong BlackPiecesBitboard => board.colourBitboards[Chess.Board.BlackIndex]; 323 | 324 | /// 325 | /// 64-bit number where each bit that is set to 1 represents a 326 | /// square that contains a piece of any type or colour. 327 | /// 328 | public ulong AllPiecesBitboard => board.allPiecesBitboard; 329 | 330 | 331 | public bool IsWhiteToMove => board.IsWhiteToMove; 332 | 333 | /// 334 | /// Number of ply (a single move by either white or black) played so far 335 | /// 336 | public int PlyCount => board.plyCount; 337 | 338 | /// 339 | /// Number of ply (a single move by either white or black) since the last pawn move or capture. 340 | /// If this value reaches a hundred (meaning 50 full moves without a pawn move or capture), the game is drawn. 341 | /// 342 | public int FiftyMoveCounter => board.currentGameState.fiftyMoveCounter; 343 | 344 | /// 345 | /// 64-bit hash of the current position 346 | /// 347 | public ulong ZobristKey => board.ZobristKey; 348 | 349 | /// 350 | /// Zobrist keys for all the positions played in the game so far. This is reset whenever a 351 | /// pawn move or capture is made, as previous positions are now impossible to reach again. 352 | /// Note that this is not updated when your bot makes moves on the board while thinking, 353 | /// but rather only when moves are actually played in the game. 354 | /// 355 | public ulong[] GameRepetitionHistory { get; private set; } 356 | 357 | /// 358 | /// FEN representation of the game's starting position. 359 | /// 360 | public string GameStartFenString => board.GameStartFen; 361 | 362 | /// 363 | /// All the moves played in the game so far. 364 | /// This only includes moves played in the actual game, not moves made on the board while the bot is thinking. 365 | /// 366 | public Move[] GameMoveHistory { get; private set; } 367 | 368 | /// 369 | /// Creates an ASCII-diagram of the current position. 370 | /// The capital letters are the white pieces, while the lowercase letters are the black ones. 371 | /// NOTE: To distinguish kings from knights, kings are represented by K/k and knights by N/n. 372 | /// 373 | public string CreateDiagram(bool blackAtTop = true, bool includeFen = true, bool includeZobristKey = true, Square? highlightedSquare = null) 374 | { 375 | StringBuilder result = new(); 376 | 377 | for (int y = 0; y < 8; y++) 378 | { 379 | int rankIndex = blackAtTop ? 7 - y : y; 380 | result.AppendLine("+---+---+---+---+---+---+---+---+"); 381 | 382 | for (int x = 0; x < 8; x++) 383 | { 384 | int fileIndex = blackAtTop ? x : 7 - x; 385 | Square square = new Square(fileIndex, rankIndex); 386 | Piece pieceInSquare = GetPiece(square); 387 | if (square != highlightedSquare) 388 | { 389 | result.Append($"| {GetPieceSymbol(pieceInSquare)} "); 390 | } 391 | else 392 | { 393 | // To highlight this square, we add brackets around its piece 394 | result.Append($"|({GetPieceSymbol(pieceInSquare)})"); 395 | } 396 | 397 | if (x == 7) 398 | { 399 | // Show rank number on the right side 400 | result.AppendLine($"| {rankIndex + 1}"); 401 | } 402 | } 403 | 404 | if (y == 7) 405 | { 406 | // Show files at the bottom 407 | result.AppendLine("+---+---+---+---+---+---+---+---+"); 408 | const string fileNames = " a b c d e f g h "; 409 | const string fileNamesRev = " h g f e d c b a "; 410 | result.AppendLine(blackAtTop ? fileNames : fileNamesRev); 411 | result.AppendLine(); 412 | 413 | if (includeFen) 414 | { 415 | result.AppendLine($"Fen : {FenUtility.CurrentFen(board)}"); 416 | } 417 | if (includeZobristKey) 418 | { 419 | result.AppendLine($"Zobrist Key : {board.ZobristKey}"); 420 | } 421 | } 422 | } 423 | 424 | return result.ToString(); 425 | 426 | static char GetPieceSymbol(Piece piece) 427 | { 428 | if (piece.IsNull) 429 | { 430 | return ' '; 431 | } 432 | char pieceSymbol = piece.IsKnight ? 'N' : piece.PieceType.ToString()[0]; 433 | return piece.IsWhite ? char.ToUpper(pieceSymbol) : char.ToLower(pieceSymbol); 434 | } 435 | } 436 | 437 | public override string ToString() => CreateDiagram(); 438 | 439 | /// 440 | /// Creates a board from the given fen string. Please note that this is quite slow, and so it is advised 441 | /// to use the board given in the Think function, and update it using MakeMove and UndoMove instead. 442 | /// 443 | public static Board CreateBoardFromFEN(string fen) 444 | { 445 | Chess.Board boardCore = new Chess.Board(); 446 | boardCore.LoadPosition(fen); 447 | return new Board(boardCore); 448 | } 449 | 450 | void OnPositionChanged() 451 | { 452 | moveGen.NotifyPositionChanged(); 453 | hasCachedMoves = false; 454 | hasCachedCaptureMoves = false; 455 | hasCachedMoveCount = false; 456 | } 457 | 458 | bool HasZeroLegalMoves() 459 | { 460 | if (hasCachedMoveCount) 461 | { 462 | return cachedMoveCount == 0; 463 | } 464 | return moveGen.NoLegalMovesInPosition(board); 465 | } 466 | 467 | } 468 | } --------------------------------------------------------------------------------