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