From b80e3fd46a4a10427764e62e59dd067621bfe461 Mon Sep 17 00:00:00 2001 From: Sargates Date: Thu, 10 Aug 2023 01:46:25 -0500 Subject: [PATCH] Added UCIPlayer.cs and UCI engine compatibility and integrated stockfish, partially standardized active player system. Other misc fixes --- Chess-Bot.csproj | 3 + license.md | 22 ++ resources/Fonts/sdf.fs | 3 +- src/Application/Core/Controller.cs | 222 +++++++++++++++++- src/Application/Core/Model.cs | 20 +- src/Application/Core/View.cs | 187 ++------------- src/Application/Gameplay/ChessPlayer.cs | 21 ++ src/Application/Gameplay/ComputerPlayer.cs | 44 ++++ src/Application/Gameplay/Evaluation.cs | 15 ++ src/Application/Gameplay/IPlayer.cs | 11 + src/Application/Gameplay/Player.cs | 25 +- src/Application/Gameplay/UCIEngine.cs | 259 +++++++++++++++++++++ src/Application/Gameplay/UCIPlayer.cs | 83 +++++++ src/Application/Gameplay/UCISettings.cs | 48 ++++ src/Application/Helpers/FileHelper.cs | 55 +++++ src/Application/Helpers/UIHelper.cs | 13 +- src/Application/UI/Animation.cs | 13 +- src/Application/UI/BoardAnimation.cs | 60 ++--- src/Application/UI/BoardUI.cs | 64 +++-- src/Application/UI/Button.cs | 9 +- src/Engine/Board/Board.cs | 62 ++++- src/Engine/Board/Fen.cs | 4 +- src/Engine/Board/Move.cs | 10 +- src/Engine/Helpers/BoardHelper.cs | 13 +- src/Engine/Stockfish.NET/Core/Stockfish.cs | 2 + 25 files changed, 984 insertions(+), 284 deletions(-) create mode 100644 license.md create mode 100644 src/Application/Gameplay/ChessPlayer.cs create mode 100644 src/Application/Gameplay/ComputerPlayer.cs create mode 100644 src/Application/Gameplay/Evaluation.cs create mode 100644 src/Application/Gameplay/IPlayer.cs create mode 100644 src/Application/Gameplay/UCIEngine.cs create mode 100644 src/Application/Gameplay/UCIPlayer.cs create mode 100644 src/Application/Gameplay/UCISettings.cs create mode 100644 src/Application/Helpers/FileHelper.cs diff --git a/Chess-Bot.csproj b/Chess-Bot.csproj index 8835a03..6181453 100644 --- a/Chess-Bot.csproj +++ b/Chess-Bot.csproj @@ -12,5 +12,8 @@ + + + diff --git a/license.md b/license.md new file mode 100644 index 0000000..dcf0982 --- /dev/null +++ b/license.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 Nicholas Glenn +Credits to Yaroslav Bondarev (Stockfish.NET) (https://github.com/Oremiro/Stockfish.NET) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/resources/Fonts/sdf.fs b/resources/Fonts/sdf.fs index 4d95381..c7fa5d8 100644 --- a/resources/Fonts/sdf.fs +++ b/resources/Fonts/sdf.fs @@ -13,8 +13,7 @@ out vec4 finalColor; // NOTE: Add here your custom variables -void main() -{ +void main() { // Texel color fetching from texture sampler // NOTE: Calculate alpha using signed distance field (SDF) float distanceFromOutline = texture(texture0, fragTexCoord).a - 0.5; diff --git a/src/Application/Core/Controller.cs b/src/Application/Core/Controller.cs index 3ce5fe9..003e2ed 100644 --- a/src/Application/Core/Controller.cs +++ b/src/Application/Core/Controller.cs @@ -1,11 +1,9 @@ using Raylib_cs; -using System; using System.IO; using System.Numerics; -using System.Globalization; +using System.Diagnostics; using ChessBot.Engine; using ChessBot.Helpers; -using ChessBot.Engine.Stockfish; namespace ChessBot.Application { @@ -17,6 +15,32 @@ public class Controller { static Camera2D cam; Model model; View view; + public static Random random = new Random(); + + public bool SuspendPlay = false; + ChessPlayer whitePlayer; + ChessPlayer blackPlayer; + + // Square selected on each interaction, -1 for invalid square + // { leftDown, leftUp, rightDown, rightUp } + public int[] mouseClickInfo = {-1, -1, -1, -1}; + // in the format of: leftReleased, leftPressed, rightPressed, rightReleased + public static int mouseButtonsClicked; // 0b1111 + public static int pressedKey=0; + + public static bool IsLeftPressed => (Controller.mouseButtonsClicked & 8) == 8; + public static bool IsLeftReleased => (Controller.mouseButtonsClicked & 4) == 4; + public static bool IsRightPressed => (Controller.mouseButtonsClicked & 2) == 2; + public static bool IsRightReleased => (Controller.mouseButtonsClicked & 1) == 1; + + + // Return active player based on color passed, throw error if invalid color + public ChessPlayer GetPlayerFromColor(char color) => ("wb".IndexOf(color) == -1) ? throw new Exception("Invalid Color") : (color == 'w') ? whitePlayer : blackPlayer; + + + public void ExitPlayerThreads() { whitePlayer.RaiseExitFlag(); blackPlayer.RaiseExitFlag(); } + public void Join() { whitePlayer.Join(); blackPlayer.Join(); } + public ChessPlayer ActivePlayer => model.board.whiteToMove ? whitePlayer : blackPlayer; public Controller() { @@ -25,6 +49,8 @@ public Controller() { Raylib.SetTraceLogLevel(TraceLogLevel.LOG_FATAL); // Ignore Raylib Errors unless fatal Raylib.InitWindow(1600, 900, "Chess"); + Debug.Assert(random != null); + cam = new Camera2D(); int screenWidth = Raylib.GetScreenWidth(); int screenHeight = Raylib.GetScreenHeight(); @@ -32,21 +58,30 @@ public Controller() { cam.offset = new Vector2(screenWidth / 2f, screenHeight / 2f); cam.zoom = 1.0f; - screenSize = new Vector2(Raylib.GetScreenWidth(), Raylib.GetScreenHeight()); model = new Model(); view = new View(screenSize, model, cam); + ChessPlayer.OnMoveChosen += MakeMove; + + whitePlayer = new ChessPlayer(); + blackPlayer = new UCIPlayer('b', "stockfish-windows-x86-64-avx2.exe", model.board); + + + } public void MainLoop() { float dt = 0f; - Stockfish stockfish = new Stockfish("./resources/stockfish-windows-x86-64-avx2.exe"); - - while (!Raylib.WindowShouldClose()) { dt = Raylib.GetFrameTime(); + mouseButtonsClicked = 0; + mouseButtonsClicked += Raylib.IsMouseButtonPressed(MouseButton.MOUSE_BUTTON_LEFT) ? 8 : 0; + mouseButtonsClicked += Raylib.IsMouseButtonReleased(MouseButton.MOUSE_BUTTON_LEFT) ? 4 : 0; + mouseButtonsClicked += Raylib.IsMouseButtonPressed(MouseButton.MOUSE_BUTTON_RIGHT) ? 2 : 0; + mouseButtonsClicked += Raylib.IsMouseButtonReleased(MouseButton.MOUSE_BUTTON_RIGHT) ? 1 : 0; + pressedKey = Raylib.GetKeyPressed(); if (Raylib.IsWindowResized()) { view.camera.offset = new Vector2(Raylib.GetScreenWidth() / 2f, Raylib.GetScreenHeight() / 2f); @@ -56,8 +91,20 @@ public void MainLoop() { Raylib.BeginDrawing(); Raylib.ClearBackground(new Color(22, 22, 22, 255)); Raylib.DrawFPS(10, 10); + + model.Update(); view.Update(dt); + if (! ActivePlayer.IsSearching && ! SuspendPlay) { + ActivePlayer.IsSearching = true; + } + + if (pressedKey != 0) { + HandleKeyboardInput(); + } + if (view.ui.activeAnimation is null && mouseButtonsClicked > 0) { + HandleMouseInput(); + } //* Draw menu here @@ -65,10 +112,171 @@ public void MainLoop() { } Raylib.CloseWindow(); + ExitPlayerThreads(); + Join(); + view.Release(); UIHelper.Release(); + } + + public void HandleMouseInput() { + // TODO Test how ineffective it would be to constantly update mousePos and check if mouse is on a square + Piece clickedPiece = Piece.None; + int squareClicked = -1; + Move validMove = new Move(0); + + Vector2 pos = Raylib.GetMousePosition() - screenSize/2; + + Vector2 boardPos = (pos/BoardUI.squareSize); + if (view.ui.isFlipped) { boardPos *= -1; } + boardPos += new Vector2(4); + + if ((0 <= boardPos.X && boardPos.X < 8 && 0 <= boardPos.Y && boardPos.Y < 8) ) { + squareClicked = 8*((int)(8-boardPos.Y))+(int)boardPos.X; + clickedPiece = model.board.GetSquare(squareClicked); + } // If the interaction (click/release) is in bounds, set square clicked and clicked piece, otherwise they will be -1 and {Piece.None} + + if (squareClicked == -1) { // Case 1 + view.ui.DeselectActiveSquare(); + return; + } // Passes guard clause if the click was in bounds + + if (view.ui.selectedIndex != -1) { + foreach (Move move in view.ui.movesForSelected) { + if (move == new Move(view.ui.selectedIndex, squareClicked)) { + validMove = move; + break; + } + } + } + + if (IsLeftPressed) { + mouseClickInfo[0] = squareClicked; + view.ui.highlightedSquares = new bool[64]; + if (! validMove.IsNull ) { // Case 3 + view.ui.DeselectActiveSquare(); + MakeMove(validMove); + //* ANIMATION HERE + } else + if (view.ui.selectedIndex != -1 && squareClicked == view.ui.selectedIndex) { // Case 5 + view.ui.isDraggingPiece = true; + } else + if (view.ui.selectedIndex == -1 && clickedPiece != Piece.None) { // Case 2 + if (model.enforceColorToMove && clickedPiece.Color == model.board.activeColor) { + view.ui.selectedIndex = squareClicked; + view.ui.movesForSelected = MoveGenerator.GetMoves(model.board, squareClicked); + view.ui.isDraggingPiece = true; + } + } else + if (validMove.IsNull && clickedPiece.Type != Piece.None) { // Case 6 + if (model.enforceColorToMove && clickedPiece.Color == model.board.activeColor) { + view.ui.selectedIndex = squareClicked; + view.ui.movesForSelected = MoveGenerator.GetMoves(model.board, squareClicked); + view.ui.isDraggingPiece = true; + } + } else + if (validMove.IsNull) { // Case 4 + view.ui.DeselectActiveSquare(); + } + } + + if (IsLeftReleased) { + mouseClickInfo[1] = squareClicked; + view.ui.isDraggingPiece = false; + + if (! validMove.IsNull) { + view.ui.DeselectActiveSquare(); + MakeMove(validMove, false); // Do not animate a move made on the release + } + mouseClickInfo[0] = -1; mouseClickInfo[1] = -1; + } + + if (IsRightPressed) { + mouseClickInfo[2] = squareClicked; + view.ui.DeselectActiveSquare(); + view.ui.isDraggingPiece = false; + } + + if (IsRightReleased) { + mouseClickInfo[3] = squareClicked; + if (view.ui.selectedIndex == -1 && mouseClickInfo[0] == -1) { + view.ui.highlightedSquares[squareClicked] = ! view.ui.highlightedSquares[squareClicked]; + } else + if (true) { + view.drawnArrows.Add((mouseClickInfo[2], mouseClickInfo[3])); + } + mouseClickInfo[2] = -1; mouseClickInfo[3] = -1; + } + + //* Case 1: No square is selected, and square clicked is out of bounds => call DeselectActiveSquare ✓ + //* Case 2: No square is selected and piece is clicked => Set selectedIndex to square clicked ✘ + //* Case 3: Square is selected and square clicked is a valid move => call model.board.MakeMove ✘ + //* Case 4: Square is selected and square clicked is not a valid move => Deselect piece and fallthrough to case 7 ✘ + //* Case 5: Square is selected and square clicked == selected index => set isDragging to true ✘ + //* Case 6: Square is selected and clicked piece is the same color => Subset of Case 7 ✓ + //* Case 7: Square is selected and clicked piece is not in the valid moves => Superset of case 4 ✘ + //* Case 7.1: If clicked square is a piece, select that square + } + + public void HandleKeyboardInput() { + switch (pressedKey) { + case (int) KeyboardKey.KEY_Z :{ + view.ui.DeselectActiveSquare(); + Piece[] old = model.board.board.ToArray(); + model.board.SetPrevState(); + model.board.SetPrevState(); + view.ui.activeAnimation = new BoardAnimation(old, model.board.board, 0.08f); + break; + } + case (int) KeyboardKey.KEY_X :{ + view.ui.DeselectActiveSquare(); + Piece[] old = model.board.board.ToArray(); + model.board.SetNextState(); + model.board.SetNextState(); + view.ui.activeAnimation = new BoardAnimation(old, model.board.board, 0.08f); + break; + } + case (int) KeyboardKey.KEY_C :{ + SuspendPlay = ! SuspendPlay; + break; + } + + case (int) KeyboardKey.KEY_P :{ + Console.WriteLine(); + Console.WriteLine(model.board.GetUCIGameFormat()); + break; + } + case (int) KeyboardKey.KEY_O :{ + foreach(Fen state in model.board.stateHistory) { + Console.WriteLine($"{state} {state.moveMade}"); + } + break; + } + case (int) KeyboardKey.KEY_I :{ + Console.WriteLine(model.board.currentStateNode.Value); + break; + } + default: { + break; + } + } + } + + public void MakeMove(Move move, bool animate=true) { + if (! move.IsNull) { // When null move is attempted, it's assumed it's checkmate, active color is the loser + ActivePlayer.IsSearching = false; + if (animate) { + Piece[] oldState = model.board.board.ToArray(); + model.board.MakeMove(move); + view.ui.activeAnimation = new BoardAnimation(oldState, model.board.board, .12f); + return; + } + model.board.MakeMove(move); + return; + } + // Handle Checkmate } } diff --git a/src/Application/Core/Model.cs b/src/Application/Core/Model.cs index 58bf1a1..00d8326 100644 --- a/src/Application/Core/Model.cs +++ b/src/Application/Core/Model.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using ChessBot.Engine; using ChessBot.Helpers; @@ -6,18 +7,18 @@ public class Model { public Board board; public bool enforceColorToMove = false; + public readonly string[] botMatchStartFens; + public Model() { - board = new Board("r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"); + StartNewGame(); - Player whitePlayer = new Player('w'); - Player blackPlayer = new Player('b'); + Debug.Assert(board != null); + botMatchStartFens = FileHelper.ReadResourceFile("Fens.txt").Split('\n'); } - - - + public void StartNewGame() { StartNewGame(Fen.startpos); } public void StartNewGame(string fenString) { //* Instantiate starting gamestate //* Instantiate new Board passing starting gamestate @@ -25,11 +26,16 @@ public void StartNewGame(string fenString) { //* //* - board = new Board(fenString); + ConsoleHelper.WriteLine($"\nNew game started\nFEN: {fenString}", ConsoleColor.Yellow); + } + + public void Update() { } + + } } \ No newline at end of file diff --git a/src/Application/Core/View.cs b/src/Application/Core/View.cs index d585cf6..3c47075 100644 --- a/src/Application/Core/View.cs +++ b/src/Application/Core/View.cs @@ -10,26 +10,11 @@ public class View { public Model model; public Camera2D camera; - // Square selected on each interaction, -1 for invalid square - // { leftDown, leftUp, rightDown, rightUp } - public int[] mouseClickInfo = {-1, -1, -1, -1}; - // in the format of: leftReleased, leftPressed, rightPressed, rightReleased - public static int mouseButtonsClicked; // 0b1111 - public static int pressedKey=0; - public List<(int tail, int head)> drawnArrows = new List<(int tail, int head)>(); public List pipeline; - public static bool IsLeftPressed => (View.mouseButtonsClicked & 8) == 8; - public static bool IsLeftReleased => (View.mouseButtonsClicked & 4) == 4; - public static bool IsRightPressed => (View.mouseButtonsClicked & 2) == 2; - public static bool IsRightReleased => (View.mouseButtonsClicked & 1) == 1; - - - - // Animation? public View(Vector2 screenSize, Model model, Camera2D cam) { ui = new BoardUI(); View.screenSize = screenSize; @@ -39,29 +24,15 @@ public View(Vector2 screenSize, Model model, Camera2D cam) { pipeline = new List(); - Button button = new Button(new Rectangle(40, 600, 200, 50), "Button"); - button.OnLeftPressed = () => { - Piece[] oldBoard = model.board.board.ToArray(); - model.StartNewGame(Fen.startpos); - ui.activeAnimation = new BoardAnimation(oldBoard, model.board.board, .12f); - }; - AddToPipeline(button); + AddButtons(); } public void AddToPipeline(IInteractable interactable) { pipeline.Add(interactable); } - - - public void Update(float dt) { - mouseButtonsClicked = 0; - mouseButtonsClicked += Raylib.IsMouseButtonPressed(MouseButton.MOUSE_BUTTON_LEFT) ? 8 : 0; - mouseButtonsClicked += Raylib.IsMouseButtonReleased(MouseButton.MOUSE_BUTTON_LEFT) ? 4 : 0; - mouseButtonsClicked += Raylib.IsMouseButtonPressed(MouseButton.MOUSE_BUTTON_RIGHT) ? 2 : 0; - mouseButtonsClicked += Raylib.IsMouseButtonReleased(MouseButton.MOUSE_BUTTON_RIGHT) ? 1 : 0; - pressedKey = Raylib.GetKeyPressed(); + Raylib.BeginMode2D(camera); @@ -72,7 +43,7 @@ public void Update(float dt) { if (ui.activeAnimation != null) { ui.activeAnimation.Update(dt); - ui.activeAnimation.Draw(); + ui.activeAnimation.Draw(ui.isFlipped); if (ui.activeAnimation.HasFinished) { ui.activeAnimation = null; } @@ -86,12 +57,7 @@ public void Update(float dt) { - if (pressedKey != 0) { - HandleKeyboardInput(); - } - if (ui.activeAnimation is null && mouseButtonsClicked > 0) { - HandleMouseInput(); - } + } public void GetAttackedSquares() { @@ -112,135 +78,30 @@ public void GetAttackedSquares() { } } - public void HandleKeyboardInput() { - switch (pressedKey) { - case (int) KeyboardKey.KEY_Z :{ - ui.DeselectActiveSquare(); - model.board.SetPrevState(); - break; - } - case (int) KeyboardKey.KEY_X :{ - ui.DeselectActiveSquare(); - model.board.SetNextState(); - break; - } - - case (int) KeyboardKey.KEY_P :{ - Console.WriteLine(Convert.ToString(model.board.currentFen.castlePrivsBin, 2)); - // foreach (Fen fenString in model.board.stateHistory) { - // ConsoleHelper.WriteLine($"{fenString}", ConsoleColor.Cyan); - // } - // Console.WriteLine(Raylib.GetMousePosition()); - // Console.WriteLine((new Vector2(ui.selectedIndex & 0b111, 7-(ui.selectedIndex >> 3)) - new Vector2(4, 4)) * BoardUI.squareSize); - break; - } - default: { - break; - } - } - } - - public void HandleMouseInput() { - // TODO Test how ineffective it would be to constantly update mousePos and check if mouse is on a square - Piece clickedPiece = Piece.None; - int squareClicked = -1; - Move validMove = new Move(0); + public void AddButtons() { - Vector2 pos = Raylib.GetMousePosition() - screenSize/2; - - Vector2 boardPos = (pos/BoardUI.squareSize)+new Vector2(4); - - if ((0 <= boardPos.X && boardPos.X < 8 && 0 <= boardPos.Y && boardPos.Y < 8) ) { - squareClicked = 8*((int)(8-boardPos.Y))+(int)boardPos.X; - clickedPiece = model.board.GetSquare(squareClicked); - } // If the interaction (click/release) is in bounds, set square clicked and clicked piece, otherwise they will be -1 and {Piece.None} - - if (squareClicked == -1) { // Case 1 - ui.DeselectActiveSquare(); - return; - } // Passes guard clause if the click was in bounds - - if (ui.selectedIndex != -1) { - foreach (Move move in ui.movesForSelected) { - if (move == new Move(ui.selectedIndex, squareClicked)) { - validMove = move; - break; - } - } - } - - if (IsLeftPressed) { - mouseClickInfo[0] = squareClicked; - ui.highlightedSquares = new bool[64]; - if (! validMove.IsNull ) { // Case 3 - ui.DeselectActiveSquare(); - Piece[] oldState = model.board.board.ToArray(); - model.board.MakeMove(validMove); - ui.activeAnimation = new BoardAnimation(oldState, model.board.board, .12f); - //* ANIMATION HERE - } else - if (ui.selectedIndex != -1 && squareClicked == ui.selectedIndex) { // Case 5 - ui.isDraggingPiece = true; - } else - if (ui.selectedIndex == -1 && clickedPiece != Piece.None) { // Case 2 - if (model.enforceColorToMove && clickedPiece.Color == model.board.activeColor) { - ui.selectedIndex = squareClicked; - ui.movesForSelected = MoveGenerator.GetMoves(model.board, squareClicked); - ui.isDraggingPiece = true; - } - } else - if (validMove.IsNull && clickedPiece.Type != Piece.None) { // Case 6 - if (model.enforceColorToMove && clickedPiece.Color == model.board.activeColor) { - ui.selectedIndex = squareClicked; - ui.movesForSelected = MoveGenerator.GetMoves(model.board, squareClicked); - ui.isDraggingPiece = true; - } - } else - if (validMove.IsNull) { // Case 4 - ui.DeselectActiveSquare(); - } - } - - if (IsLeftReleased) { - mouseClickInfo[1] = squareClicked; - ui.isDraggingPiece = false; - - if (! validMove.IsNull) { - ui.DeselectActiveSquare(); - model.board.MakeMove(validMove); - } - // Console.WriteLine(string.Join(", ", mouseClickInfo)); - mouseClickInfo[0] = -1; mouseClickInfo[1] = -1; - } - - if (IsRightPressed) { - mouseClickInfo[2] = squareClicked; - ui.DeselectActiveSquare(); - ui.isDraggingPiece = false; - } + Button button1 = new Button(new Rectangle(40, 600, 200, 50), "Reset Board"); + button1.OnLeftPressed = () => { + Piece[] oldBoard = model.board.board.ToArray(); + model.StartNewGame(Fen.startpos); + ui.activeAnimation = new BoardAnimation(oldBoard, model.board.board, 0.2f); + }; + AddToPipeline(button1); + Button button2 = new Button(new Rectangle(40, 540, 200, 50), "Random Position"); + button2.OnLeftPressed = () => { + Piece[] oldBoard = model.board.board.ToArray(); + model.StartNewGame(model.botMatchStartFens[Controller.random.Next(model.botMatchStartFens.Length)]); + ui.activeAnimation = new BoardAnimation(oldBoard, model.board.board, 0.2f); + }; + AddToPipeline(button2); - if (IsRightReleased) { - mouseClickInfo[3] = squareClicked; - if (ui.selectedIndex == -1 && mouseClickInfo[0] == -1) { - ui.highlightedSquares[squareClicked] = ! ui.highlightedSquares[squareClicked]; - } else - if (true) { - drawnArrows.Add((mouseClickInfo[2], mouseClickInfo[3])); - } - // Console.WriteLine(string.Join(", ", mouseClickInfo)); - mouseClickInfo[2] = -1; mouseClickInfo[3] = -1; - } + Button button3 = new Button(new Rectangle(40, 480, 200, 50), "Flip Board"); + button3.OnLeftPressed = () => { + ui.isFlipped = ! ui.isFlipped; + }; + AddToPipeline(button3); - //* Case 1: No square is selected, and square clicked is out of bounds => call DeselectActiveSquare ✓ - //* Case 2: No square is selected and piece is clicked => Set selectedIndex to square clicked ✘ - //* Case 3: Square is selected and square clicked is a valid move => call model.board.MakeMove ✘ - //* Case 4: Square is selected and square clicked is not a valid move => Deselect piece and fallthrough to case 7 ✘ - //* Case 5: Square is selected and square clicked == selected index => set isDragging to true ✘ - //* Case 6: Square is selected and clicked piece is the same color => Subset of Case 7 ✓ - //* Case 7: Square is selected and clicked piece is not in the valid moves => Superset of case 4 ✘ - //* Case 7.1: If clicked square is a piece, select that square } - public void Release() { ui.Release(); } diff --git a/src/Application/Gameplay/ChessPlayer.cs b/src/Application/Gameplay/ChessPlayer.cs new file mode 100644 index 0000000..44d5fbf --- /dev/null +++ b/src/Application/Gameplay/ChessPlayer.cs @@ -0,0 +1,21 @@ +using ChessBot.Engine; +namespace ChessBot.Application; + + +public class ChessPlayer { + public char color; + + public bool ExitFlag; + public bool IsSearching; + public delegate void VoidDel(Move move, bool animate=true); + public static VoidDel? OnMoveChosen; + + + + public virtual Move Think() { + return Move.NullMove; + } + public virtual void RaiseExitFlag() { ExitFlag = true; } + public virtual void SetShouldSearch(bool n) { IsSearching = n; } + public virtual void Join() {} +} \ No newline at end of file diff --git a/src/Application/Gameplay/ComputerPlayer.cs b/src/Application/Gameplay/ComputerPlayer.cs new file mode 100644 index 0000000..9dea0ef --- /dev/null +++ b/src/Application/Gameplay/ComputerPlayer.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; +using ChessBot.Helpers; +using ChessBot.Engine; + +namespace ChessBot.Application; +public class ComputerPlayer : ChessPlayer { + public Board board; + public bool IsThreaded; + public Thread thread; + + public ComputerPlayer(char color, Board board) { + IsThreaded = true; + this.color = color; + this.board = board; + + ThreadStart ths = new ThreadStart(Start); + thread = new Thread(ths); + thread.Start(); + } + + public void Start() { + Console.WriteLine($"Starting Thread {color}"); + while (true) { + if (ExitFlag) { + break; + } + if (OnMoveChosen == null) { + continue; + } + + + if (IsSearching) { + OnMoveChosen(Think()); + IsSearching = false; + } + } + Console.WriteLine($"Exiting Thread {color}"); + } + public override Move Think() { + return Move.NullMove; + } + + +} diff --git a/src/Application/Gameplay/Evaluation.cs b/src/Application/Gameplay/Evaluation.cs new file mode 100644 index 0000000..f872193 --- /dev/null +++ b/src/Application/Gameplay/Evaluation.cs @@ -0,0 +1,15 @@ +namespace ChessBot.Application { + public class Evaluation { + public string Type { get; set; } + public int Value { get; set; } + + public Evaluation() { + Type = ""; + } + + public Evaluation(string type, int value) { + Type = type; + Value = value; + } + } +} \ No newline at end of file diff --git a/src/Application/Gameplay/IPlayer.cs b/src/Application/Gameplay/IPlayer.cs new file mode 100644 index 0000000..66b804c --- /dev/null +++ b/src/Application/Gameplay/IPlayer.cs @@ -0,0 +1,11 @@ +using ChessBot.Engine; +namespace ChessBot.Application; + + +public interface IPlayer { + + public Move Think(); + public void Join(); + public void RaiseExitFlag(); + public void SetShouldSearch(bool n); +} \ No newline at end of file diff --git a/src/Application/Gameplay/Player.cs b/src/Application/Gameplay/Player.cs index ad15832..0ad3635 100644 --- a/src/Application/Gameplay/Player.cs +++ b/src/Application/Gameplay/Player.cs @@ -1,18 +1,9 @@ -using Raylib_cs; - - -namespace ChessBot.Engine { - public readonly struct Player { - public readonly char color; - public readonly char forward; - // public readonly char ; - - public Player(char color) { - this.color = color; - } - - public void Think() { - - } - } +using ChessBot.Engine; +namespace ChessBot.Application; + +public class Player { + + public bool ShouldSearch; + public bool ExitFlag; + public bool IsThreaded; } \ No newline at end of file diff --git a/src/Application/Gameplay/UCIEngine.cs b/src/Application/Gameplay/UCIEngine.cs new file mode 100644 index 0000000..f14456e --- /dev/null +++ b/src/Application/Gameplay/UCIEngine.cs @@ -0,0 +1,259 @@ +///////////////////////////////////////////////////////////////////// +// Most of this is taken from Stockfish.NET +// The only real changes are to make it compatible with my engine +///////////////////////////////////////////////////////////////////// + +using System.Diagnostics; +using ChessBot.Helpers; +using ChessBot.Engine; + +namespace ChessBot.Application; + +public class UCIEngine { + private const int MAX_TRIES = 200; + private int _skillLevel; + public int Depth; + public UCISettings Settings; + public int SkillLevel { + get => _skillLevel; + set { + _skillLevel = value; + Settings.SkillLevel = SkillLevel; + setOption("Skill level", SkillLevel.ToString()); + } + } + public char color; + private ProcessStartInfo _processStartInfo; + private Process _process; + + public bool ShouldSearch; + + public UCIEngine( + string pathToExe, + int depth = 14, + UCISettings? settings = null) { + _processStartInfo = new ProcessStartInfo { + FileName = FileHelper.GetResourcePath(pathToExe), + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardInput = true, + RedirectStandardOutput = true + }; + _process = new Process {StartInfo = _processStartInfo}; + + Depth = depth; + Settings = (settings ?? (new UCISettings())); + } + + public void Start() { + + _process.Start(); + ReadLine(); // Reads the buffer output when you launch stockfish + SkillLevel = Settings.SkillLevel; + + foreach (var property in Settings.GetPropertiesAsDictionary()) { + setOption(property.Key, property.Value); + } + } + + + private void send(string command, int estimatedTime = 100) { + WriteLine(command); + Wait(estimatedTime); + } + + private bool isReady() { + send("isready"); + var tries = 0; + while (tries < MAX_TRIES) { + ++tries; + + if (ReadLine() == "readyok") { + return true; + } + } + throw new Exception("Max tries Exceeded"); + } + + private void setOption(string name, string value) { + send($"setoption name {name} value {value}"); + if (!isReady()) { + throw new ApplicationException(); + } + } + + private void startNewGame() { + send("ucinewgame"); + if (!isReady()) { + throw new ApplicationException(); + } + } + + private void go() { + send($"go depth {Depth}"); + } + + private void goTime(int time) { + send($"go movetime {time}"); + } + + private List readLineAsList() { + var data = ReadLine(); + if (data == null) { data = ""; } + return data.Split(' ').ToList(); + } + + public void SetPosition(string uciGameFormat) { + startNewGame(); + send($"{uciGameFormat}"); + } + + public string GetFenPosition() { + send("d"); + var tries = 0; + while (true) { + if (tries > MAX_TRIES) { + throw new Exception("Max tries Exceeded"); + } + + var data = readLineAsList(); + if (data[0] == "Fen:") { + return string.Join(" ", data.GetRange(1, data.Count - 1)); + } + + tries++; + } + } + + public string GetBestMove() { + go(); + var tries = 0; + while (true) { + if (tries > MAX_TRIES) { + throw new Exception("Max tries Exceeded"); + } + + var data = readLineAsList(); + + if (data[0] == "bestmove") { + if (data[1] == "(none)") { + return "a1a1"; // Represents null move in my engine + } + + return data[1]; + } + + tries++; + } + } + + public string GetBestMoveTime(int time = 1000) { + goTime(time); + var tries = 0; + while (true) { + if (tries > MAX_TRIES) { + throw new Exception("Max tries Exceeded"); + } + + var data = readLineAsList(); + if (data[0] == "bestmove") { + if (data[1] == "(none)") { + return "a1a1"; + } + + return data[1]; + } + } + } + + public bool IsMoveCorrect(string moveValue) { + send($"go depth 1 searchmoves {moveValue}"); + var tries = 0; + while (true) { + if (tries > MAX_TRIES) { + throw new Exception("Max tries Exceeded"); + } + + var data = readLineAsList(); + if (data[0] == "bestmove") { + if (data[1] == "(none)") { + return false; + } + + return true; + } + + tries++; + } + } + + public Evaluation GetEvaluation() { + Evaluation evaluation = new Evaluation(); + var fen = GetFenPosition(); + char compare; + // fen sequence for white always contains w + if (fen.Contains("w")) { + compare = 'w'; + } + else { + compare = 'b'; + } + + // I'm not sure this is the good way to handle evaluation of position, but why not? + // Another way we need to somehow limit engine depth? + goTime(10000); + var tries = 0; + while (true) { + if (tries > MAX_TRIES) { + throw new Exception("Max tries Exceeded"); + } + + var data = readLineAsList(); + if (data[0] == "info") { + for (int i = 0; i < data.Count; i++) { + if (data[i] == "score") { + //don't use ternary operator here for readability + int k; + if (compare == 'w') { + k = 1; + } + else { + k = -1; + } + + evaluation = new Evaluation(data[i + 1], Convert.ToInt32(data[i + 2]) * k); + } + } + } + + if (data[0] == "bestmove") { + return evaluation; + } + + tries++; + } + } + public void Wait(int millisecond) { + this._process.WaitForExit(millisecond); + } + + public void WriteLine(string command) { + if (_process.StandardInput == null) { + throw new NullReferenceException(); + } + _process.StandardInput.WriteLine(command); + _process.StandardInput.Flush(); + } + + public string? ReadLine() { + if (_process.StandardOutput == null) { + throw new NullReferenceException(); + } + return _process.StandardOutput.ReadLine(); + } + + ~UCIEngine() { + //When process is going to be destructed => we are going to close stockfish process + _process.Close(); + } +} diff --git a/src/Application/Gameplay/UCIPlayer.cs b/src/Application/Gameplay/UCIPlayer.cs new file mode 100644 index 0000000..40d3316 --- /dev/null +++ b/src/Application/Gameplay/UCIPlayer.cs @@ -0,0 +1,83 @@ +///////////////////////////////////////////////////////////////////// +// Most of this is taken from Stockfish.NET +// The only real changes are to make it compatible with my engine +///////////////////////////////////////////////////////////////////// + +using System.Diagnostics; +using ChessBot.Helpers; +using ChessBot.Engine; + +namespace ChessBot.Application; + +public class UCIPlayer : ChessPlayer{ + // public char color; + UCIEngine engine; + Board board; + + public bool IsThreaded; + public Thread thread; + + public UCIPlayer( + char color, + string pathToExe, + Board board, + int depth = 22, + UCISettings? settings = null) { + + IsThreaded = true; + engine = new UCIEngine(pathToExe, depth, settings); + this.board = board; + this.color = color; + + // OnMoveChosen += board.MakeMove; + + ThreadStart ths = new ThreadStart(Start); + thread = new Thread(ths); + thread.Start(); + } + + public void Start() { + engine.Start(); + Console.WriteLine($"Starting Thread {color}"); + while (true) { + if (ExitFlag) { + break; + } + if (OnMoveChosen == null) { + continue; + } + + + if (IsSearching) { + OnMoveChosen(Think()); + IsSearching = false; + } + } + Console.WriteLine($"Exiting Thread {color}"); + } + + public override Move Think() { + engine.SetPosition(board.GetUCIGameFormat()); + string response = engine.GetBestMoveTime(400); + int promoChar = 0; + if (response.Length > 4) { // is Promotion + promoChar = response[4] switch { + 'q' => Move.PromoteToQueenFlag, + 'b' => Move.PromoteToBishopFlag, + 'n' => Move.PromoteToKnightFlag, + 'r' => Move.PromoteToRookFlag, + _ => 0 + }; + } + return new Move(BoardHelper.NameToSquareIndex(response.Substring(0, 2)), BoardHelper.NameToSquareIndex(response.Substring(2, 2)), promoChar); + } + + public override void Join() { thread.Join(); } + public override void RaiseExitFlag() { ExitFlag = true; } + public override void SetShouldSearch(bool n) { IsSearching = n; } + + ~UCIPlayer() { + ExitFlag = true; + thread.Join(); + } +} diff --git a/src/Application/Gameplay/UCISettings.cs b/src/Application/Gameplay/UCISettings.cs new file mode 100644 index 0000000..782ae8b --- /dev/null +++ b/src/Application/Gameplay/UCISettings.cs @@ -0,0 +1,48 @@ +// Ref: Stockfish.NET (see license) +using System.Collections.Generic; + +namespace ChessBot.Application { + public class UCISettings { + public int Contempt; + public int Threads; + public bool Ponder; + public int MultiPV; + public int SkillLevel; + public int MoveOverhead; + public int SlowMover; + public bool UCIChess960; + + public UCISettings( + int contempt = 0, + int threads = 0, + bool ponder = false, + int multiPV = 1, + int skillLevel = 2, + int moveOverhead = 30, + int slowMover = 10, + bool uciChess960 = false + ) { + Contempt = contempt; + Ponder = ponder; + Threads = threads; + MultiPV = multiPV; + SkillLevel = skillLevel; + MoveOverhead = moveOverhead; + SlowMover = slowMover; + UCIChess960 = uciChess960; + } + + public Dictionary GetPropertiesAsDictionary() { + return new Dictionary { + ["Contempt"] = Contempt.ToString(), + ["Threads"] = Threads.ToString(), + ["Ponder"] = Ponder.ToString(), + ["MultiPV"] = MultiPV.ToString(), + ["Skill Level"] = SkillLevel.ToString(), + ["Move Overhead"] = MoveOverhead.ToString(), + ["Slow Mover"] = SlowMover.ToString(), + ["UCI_Chess960"] = UCIChess960.ToString(), + }; + } + } +} \ No newline at end of file diff --git a/src/Application/Helpers/FileHelper.cs b/src/Application/Helpers/FileHelper.cs new file mode 100644 index 0000000..3751064 --- /dev/null +++ b/src/Application/Helpers/FileHelper.cs @@ -0,0 +1,55 @@ +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System; + +namespace ChessBot.Helpers { + public static class FileHelper { + + public static string GetUniqueFileName(string path, string fileName, string fileExtension) { + if (fileExtension[0] != '.') { + fileExtension = "." + fileExtension; + } + + string uniqueName = fileName; + int index = 0; + + while (File.Exists(Path.Combine(path, uniqueName + fileExtension))) { + index++; + uniqueName = fileName + index; + } + return uniqueName + fileExtension; + } + + public static string GetResourcePath(params string[] localPath) { + return Path.Combine(Directory.GetCurrentDirectory(), "resources", Path.Combine(localPath)); + } + + public static string ReadResourceFile(string localPath) { + return File.ReadAllText(GetResourcePath(localPath)); + } + + // Thanks to https://github.com/dotnet/runtime/issues/17938 + public static void OpenUrl(string url) { + try { + Process.Start(url); + } + catch { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + Process.Start("open", url); + } + else { + throw; + } + } + } + + } +} diff --git a/src/Application/Helpers/UIHelper.cs b/src/Application/Helpers/UIHelper.cs index db4c919..7e4c560 100644 --- a/src/Application/Helpers/UIHelper.cs +++ b/src/Application/Helpers/UIHelper.cs @@ -34,7 +34,7 @@ static UIHelper() { const int baseSize = 64; uint fileSize = 0; - var fileData = Raylib.LoadFileData(GetResourcePath("Fonts", fontName), ref fileSize); + var fileData = Raylib.LoadFileData(FileHelper.GetResourcePath("Fonts", fontName), ref fileSize); Font fontSdf = default; fontSdf.baseSize = baseSize; fontSdf.glyphCount = 95; @@ -48,9 +48,9 @@ static UIHelper() Raylib.SetTextureFilter(fontSdf.texture, TextureFilter.TEXTURE_FILTER_BILINEAR); UIHelper.fontSdf = fontSdf; } - shader = Raylib.LoadShader("", GetResourcePath("Fonts", "sdf.fs")); + shader = Raylib.LoadShader("", FileHelper.GetResourcePath("Fonts", "sdf.fs")); } - font = Raylib.LoadFontEx(GetResourcePath("Fonts", fontName), 128, null, 0); + font = Raylib.LoadFontEx(FileHelper.GetResourcePath("Fonts", fontName), 128, null, 0); } public static void DrawText(string text, Vector2 pos, int size, int spacing, Color col, AlignH alignH = AlignH.Left, AlignV alignV = AlignV.Centre) @@ -96,10 +96,6 @@ static bool MouseInRect(Rectangle rec) { return mousePos.X >= rec.x && mousePos.Y >= rec.y && mousePos.X <= rec.x + rec.width && mousePos.Y <= rec.y + rec.height; } - public static string GetResourcePath(params string[] localPath) { - return Path.Combine(Directory.GetCurrentDirectory(), "resources", Path.Combine(localPath)); - } - public static float Scale(float val, int referenceResolution = referenceResolution) { return Raylib.GetScreenWidth() / (float)referenceResolution * val; } @@ -114,8 +110,7 @@ public static Vector2 Scale(Vector2 vec, int referenceResolution = referenceReso return new Vector2(x, y); } - public static void Release() - { + public static void Release() { Raylib.UnloadFont(font); if (SDF_Enabled) { diff --git a/src/Application/UI/Animation.cs b/src/Application/UI/Animation.cs index f46abdb..4c26620 100644 --- a/src/Application/UI/Animation.cs +++ b/src/Application/UI/Animation.cs @@ -14,8 +14,8 @@ public Animation(int start, int end, Piece piece, float t) { StartIndex = start; EndIndex = end; - StartPos = BoardUI.squareSize*(new Vector2(start & 0b111, (7-(start>>3))) - new Vector2(4)); - EndPos = BoardUI.squareSize*(new Vector2(end & 0b111, (7-(end>>3))) - new Vector2(4)); + StartPos = BoardUI.squareSize*(new Vector2(start & 0b111, (7-(start>>3))) - new Vector2(3.5f)); + EndPos = BoardUI.squareSize*(new Vector2(end & 0b111, (7-(end>>3))) - new Vector2(3.5f)); TotalTime = t; ElapsedTime = 0f; @@ -24,12 +24,15 @@ public Animation(int start, int end, Piece piece, float t) { public float LerpTime => ElapsedTime/TotalTime; - public void Draw() { + public void Draw(bool isFlipped) { // Draw piece at position float t = LerpTime; + Vector2 end = EndPos; + Vector2 start = StartPos; + if (isFlipped) { start *= -1; end *= -1; } - // BoardUI.DrawPiece(Piece.Bishop, new Vector2(400,400)); - BoardUI.DrawPiece(piece, (t*EndPos)+(1-t)*StartPos); + + BoardUI.DrawPiece(piece, (t*end)+((1-t)*start)); } public void Update(float dt) { // Update T [0, 1] diff --git a/src/Application/UI/BoardAnimation.cs b/src/Application/UI/BoardAnimation.cs index afafa40..d25cf5a 100644 --- a/src/Application/UI/BoardAnimation.cs +++ b/src/Application/UI/BoardAnimation.cs @@ -13,45 +13,49 @@ public class BoardAnimation { List leftoverAnimations; - public BoardAnimation(Piece[] start, Piece[] end, float totalTime) { + public BoardAnimation(Piece[] oldBoard, Piece[] newBoard, float totalTime) { TotalTime = totalTime; - List<(Piece piece, int index)> startPiecesThatDontFit = new List<(Piece piece, int index)>(); - List<(Piece piece, int index)> endPiecesThatDontFit = new List<(Piece piece, int index)>(); + List<(Piece piece, int index)> oldPiecesThatDontFit = new List<(Piece piece, int index)>(); + List<(Piece piece, int index)> newPiecesThatDontFit = new List<(Piece piece, int index)>(); leftoverAnimations = new List(); - for (int i=0; i<64; i++) { // Iterate over each array to - Piece pieceOnStart = start[i]; Piece pieceOnEnd = end[i]; + for (int i=0; i<64; i++) { // Iterate over every square in board, if the piece on the old square matches the new square, cache that and don't do anything + Piece pieceOnStart = oldBoard[i]; Piece pieceOnEnd = newBoard[i]; + if ((! pieceOnStart.IsNull) && pieceOnStart == pieceOnEnd) { identicalPieces |= (1ul << i); continue; } - startPiecesThatDontFit.Add((pieceOnStart, i)); - endPiecesThatDontFit.Add((pieceOnEnd, i)); + if (! pieceOnStart.IsNull) oldPiecesThatDontFit.Add((pieceOnStart, i)); + if (! pieceOnEnd.IsNull) newPiecesThatDontFit.Add((pieceOnEnd, i)); + } + + if (oldPiecesThatDontFit.Count == 0 && newPiecesThatDontFit.Count == 0) { // Board states are the same, no reason to animate + HasFinished = true; + return; } - // for (int k=0; k<64; k++) { - // ulong square = 1ul << k; - // if ((square & identicalPieces) != 0) { - // // Console.WriteLine($"Ident square {k}"); - // } - // } - - for (int i=startPiecesThatDontFit.Count-1; i>-1; i--) { - Piece startPiece = startPiecesThatDontFit[i].piece; - for (int j=endPiecesThatDontFit.Count-1; j>-1; j--) { - Piece endPiece = endPiecesThatDontFit[j].piece; + + for (int i=oldPiecesThatDontFit.Count-1; i>-1; i--) { + Piece startPiece = oldPiecesThatDontFit[i].piece; + for (int j=newPiecesThatDontFit.Count-1; j>-1; j--) { + Piece endPiece = newPiecesThatDontFit[j].piece; if (startPiece == endPiece) { - leftoverAnimations.Add(new Animation(startPiecesThatDontFit[i].index, endPiecesThatDontFit[j].index, startPiece, totalTime)); - endPiecesThatDontFit.RemoveAt(j); - startPiecesThatDontFit.RemoveAt(i); + leftoverAnimations.Add(new Animation(oldPiecesThatDontFit[i].index, newPiecesThatDontFit[j].index, startPiece, totalTime)); + newPiecesThatDontFit.RemoveAt(j); + oldPiecesThatDontFit.RemoveAt(i); break; } } - } - if (endPiecesThatDontFit.Count > 0) { - - foreach ((Piece piece, int index) tup in endPiecesThatDontFit) { - Random r = new Random(); + } //* All that's left now are pieces on the old board that arent on the new board, and vice versa + + + + //* Pieces on the old board that don't fit do not matter because they won't have a place at the end of the animation + //* This iterates and adds new animations for "newly generated" pieces on the new state; Animates from a random index to add flavor + Random r = new Random(); + if (newPiecesThatDontFit.Count > 0) { + foreach ((Piece piece, int index) tup in newPiecesThatDontFit) { leftoverAnimations.Add(new Animation((int)(r.NextDouble()*64), tup.index, tup.piece, totalTime)); } } @@ -59,10 +63,10 @@ public BoardAnimation(Piece[] start, Piece[] end, float totalTime) { public float LerpTime => ElapsedTime/TotalTime; - public void Draw() { + public void Draw(bool isFlipped) { // Draw piece at position foreach (Animation anim in leftoverAnimations) { - anim.Draw(); + anim.Draw(isFlipped); } } diff --git a/src/Application/UI/BoardUI.cs b/src/Application/UI/BoardUI.cs index 30ea275..25b6fa3 100644 --- a/src/Application/UI/BoardUI.cs +++ b/src/Application/UI/BoardUI.cs @@ -19,10 +19,11 @@ public class BoardUI { public Color[] squareColors = new Color[64]; public bool isDraggingPiece = false; public BoardAnimation? activeAnimation; + public bool isFlipped; public BoardUI() { - piecesTexture = Raylib.LoadTexture(UIHelper.GetResourcePath("Pieces.png")); + piecesTexture = Raylib.LoadTexture(FileHelper.GetResourcePath("Pieces.png")); Raylib.GenTextureMipmaps(ref piecesTexture); Raylib.SetTextureWrap(piecesTexture, TextureWrap.TEXTURE_WRAP_CLAMP); Raylib.SetTextureFilter(piecesTexture, TextureFilter.TEXTURE_FILTER_BILINEAR); @@ -36,36 +37,46 @@ public BoardUI() { public void DrawBoardBorder() { - int boardStartX = -squareSize * 4; - int boardStartY = -squareSize * 4; int w = 12; - Raylib.DrawRectangle(boardStartX-w, boardStartY - w, 8*squareSize+2*w, 8*squareSize+2*w, BoardTheme.borderCol); + DrawRectangle(0, 0, 8*squareSize+2*w, 8*squareSize+2*w, BoardTheme.borderCol); } public void DrawBoardSquares() { - for (int i=0;i<64;i++) { - Vector2 squarePos = new Vector2(i%8, (7-i/8)); - Vector2 temp = squareSize * (squarePos - new Vector2(4)); - Raylib.DrawRectangle((int)( temp.X ), (int)( temp.Y ), squareSize, squareSize, squareColors[i]); - if (highlightedSquares[i]) { - Raylib.DrawRectangle((int)( squareSize * (squarePos.X-4) ), (int)( squareSize * (squarePos.Y-4) ), squareSize, squareSize, BoardTheme.selectedHighlight); + for (int i=0;i<64;i++) { + Vector2 squarePos = new Vector2(i & 0b111, 7-(i>>3)); + Vector2 temp = squareSize * (squarePos - new Vector2(3.5f)); + DrawRectangle(( temp.X ), ( temp.Y ), squareSize, squareSize, squareColors[i]); + if (highlightedSquares[isFlipped ? 63-i : i]) { + DrawRectangle(( temp.X ), ( temp.Y ), squareSize, squareSize, BoardTheme.selectedHighlight); } } foreach (Move move in movesForSelected) { Vector2 squarePos = new Vector2(move.TargetSquare%8, (7-move.TargetSquare/8)); + Vector2 temp = squareSize * (squarePos - new Vector2(3.5f)); + if (isFlipped) { + temp *= -1; + } // squareColors[move.TargetSquare] = IsLightSquare(move.TargetSquare) ? BoardTheme.legalLight : BoardTheme.legalDark; - Raylib.DrawRectangle((int)( squareSize * (squarePos.X-4) ), (int)( squareSize * (squarePos.Y-4) ), squareSize, squareSize, BoardTheme.legalHighlight); + DrawRectangle(( temp.X ), ( temp.Y ), squareSize, squareSize, BoardTheme.legalHighlight); } if (selectedIndex != -1) { Vector2 squarePos = new Vector2(selectedIndex%8, (7-selectedIndex/8)); + Vector2 temp = squareSize * (squarePos - new Vector2(3.5f)); + if (isFlipped) { + temp *= -1; + } // squareColors[selectedIndex] = IsLightSquare(selectedIndex) ? BoardTheme.selectedLight : BoardTheme.selectedLight; // TODO fix redundant line - Raylib.DrawRectangle((int)( squareSize * (squarePos.X-4) ), (int)( squareSize * (squarePos.Y-4) ), squareSize, squareSize, BoardTheme.movedFromHighlight); + DrawRectangle(( temp.X ), ( temp.Y ), squareSize, squareSize, BoardTheme.movedFromHighlight); } } + public void DrawRectangle(float x, float y, int width, int height, Color color) { + Raylib.DrawRectangle((int)x-(width/2), (int)y-(height/2), width, height, color); + } + public void ResetBoardColors() { for (int i=0;i<64;i++) { squareColors[i] = IsLightSquare(i) ? BoardTheme.lightCol : BoardTheme.darkCol; @@ -76,23 +87,26 @@ public void ResetBoardColors() { public void DrawPiecesOnBoard(Board board) { //* This is kind of nasty to have this inside of the draw method but it's the only way to //* add some QOL functionality without cluttering up other things - float snappingFactor = 0.375f; // Domain: [0, 1]; 0 for no snaping, 1 for snapping within 1 board square + // float snappingFactor = 0.675f; // Domain: [0, 1]; 0 for no snaping, 1 for snapping within 1 board square Vector2 cachedRenderPos = Vector2.Zero; for (int i=0; i<64;i++) { - int x = i & 0b111; int y = (7-(i >> 3)); + int x = i & 0b111; int y = 7-(i>>3); Vector2 indexVector = new Vector2(x, y); - Vector2 renderPosition = (indexVector - new Vector2(4, 4)) * squareSize; + Vector2 renderPosition = (indexVector - new Vector2(3.5f)) * squareSize; + if (isFlipped) { + renderPosition *= -1; + } if (selectedIndex == i && isDraggingPiece) { cachedRenderPos = renderPosition; continue; } - // this is the control if there is no animation render anyway + // this is the control if there is no animation render anyway // vvv vvv single out the 1st bit vvv - if (1ul != ((1ul) & ((activeAnimation?.identicalPieces>>i) ?? 1ul))) { + if (0ul == ((1ul) & ((activeAnimation?.identicalPieces>>i) ?? 1ul))) { continue; } @@ -100,14 +114,14 @@ public void DrawPiecesOnBoard(Board board) { } if (selectedIndex != -1 && isDraggingPiece) { Vector2 mousePos = Raylib.GetMousePosition() - View.screenSize/2; // Mouse position in camera space converted to worldspace (centered at the origin) - Vector2 renderedPosition = cachedRenderPos + new Vector2(squareSize/2); // center of selected square + Vector2 renderedPosition = cachedRenderPos; // center of selected square // Checking if either X or Y is greater than the snappingFactor, in terms of half the square size - if (Math.Max(Math.Abs((mousePos - renderedPosition).X), Math.Abs((mousePos - renderedPosition).Y)) < (squareSize/2) * snappingFactor) { - DrawPiece(board.GetSquare(selectedIndex), cachedRenderPos); - return; - } - DrawPiece(board.GetSquare(selectedIndex), Raylib.GetMousePosition()-View.screenSize/2-new Vector2(squareSize)/2); + // if (Math.Max(Math.Abs((mousePos - renderedPosition).X), Math.Abs((mousePos - renderedPosition).Y)) < (squareSize/2) * snappingFactor) { + // DrawPiece(board.GetSquare(selectedIndex), cachedRenderPos); + // return; + // } + DrawPiece(board.GetSquare(selectedIndex), Raylib.GetMousePosition()-View.screenSize/2); } } @@ -117,12 +131,12 @@ public void DeselectActiveSquare() { movesForSelected = new Move[0]; } - public static void DrawPiece(Piece piece, Vector2 posTopLeft, float alpha = 1) { //* Copied from SebLague + public static void DrawPiece(Piece piece, Vector2 posAbsCenter, float alpha = 1) { //* Copied from SebLague if (piece != Piece.None) { int type = piece.Type; bool white = piece.Color == Piece.White; Rectangle srcRect = GetPieceTextureRect(type, white); - Rectangle targRect = new Rectangle((int)posTopLeft.X, (int)posTopLeft.Y, squareSize, squareSize); + Rectangle targRect = new Rectangle((int)posAbsCenter.X-(squareSize/2), (int)posAbsCenter.Y-(squareSize/2), squareSize, squareSize); Color tint = new Color(255, 255, 255, (int)MathF.Round(255 * alpha)); Raylib.DrawTexturePro(piecesTexture, srcRect, targRect, new Vector2(0, 0), 0, tint); diff --git a/src/Application/UI/Button.cs b/src/Application/UI/Button.cs index 10a218e..cba83ce 100644 --- a/src/Application/UI/Button.cs +++ b/src/Application/UI/Button.cs @@ -23,6 +23,7 @@ public Button(Rectangle rect, string text, Color color) { this.text = text; this.color = color; } + public Button(Rectangle rect, string text, string color) : this(rect, text, ColorHelper.HexToColor(color)) {} public Vector2 Size => new Vector2(_Rect.width, _Rect.height); public Vector2 Position => new Vector2(_Rect.x, _Rect.y); @@ -40,16 +41,16 @@ public void Draw() { public void Update() { if (! this.IsHoveringOver) { return; } - if (View.IsLeftPressed) { + if (Controller.IsLeftPressed) { OnLeftPressed?.Invoke(); } - if (View.IsLeftReleased) { + if (Controller.IsLeftReleased) { OnLeftReleased?.Invoke(); } - if (View.IsRightPressed) { + if (Controller.IsRightPressed) { OnRightPressed?.Invoke(); } - if (View.IsRightReleased) { + if (Controller.IsRightReleased) { OnRightReleased?.Invoke(); } } diff --git a/src/Engine/Board/Board.cs b/src/Engine/Board/Board.cs index 6e9412f..17b8bb9 100644 --- a/src/Engine/Board/Board.cs +++ b/src/Engine/Board/Board.cs @@ -4,6 +4,7 @@ namespace ChessBot.Engine { public class Board { + public Piece[] prevBoard; public Piece[] board; public bool whiteToMove; @@ -19,6 +20,7 @@ public class Board { public Board(string fen="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") { + if (fen == "") fen = Fen.startpos; stateHistory = new LinkedList(); currentStateNode = new LinkedListNode(new Fen(fen)); @@ -27,12 +29,13 @@ public Board(string fen="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 // TODO: Add FEN string loading, StartNewGame should be in Controller/Model.cs, board should just be able to load a fen string in place board = FenToBoard(this.currentFen.fenBoard); + prevBoard = board; for (int i = 0; i < board.Length; i++) { if (board[i] == (Piece.White | Piece.King)) { whiteKingPos = i; } if (board[i] == (Piece.Black | Piece.King)) { blackKingPos = i; } } - Console.WriteLine(this.currentFen.ToFEN()); whiteToMove = this.currentFen.fenColor == 'w'; + } @@ -83,6 +86,35 @@ public static Piece[] FenToBoard(string fen) { public int opponentColour(int color) => whiteToMove ? Piece.Black : Piece.White; public int forwardDir(int color) => color == Piece.White ? 8 : -8; + public string GetUCIGameFormat() { + + LinkedListNode? currNode = stateHistory.First; + string o = ""; + if (currNode is null) { + Console.WriteLine("`GetUCIGameFormat` returned default"); + return "position startpos"; + } + + if (currNode.Value.isStartPos) { + o += $"position startpos "; + } else if (! currNode.Value.isStartPos) { + o += $"position fen {currNode.Value.ToFEN()} "; + } + if (! currNode.Value.moveMade.IsNull) { + o += $"moves {currNode.Value.moveMade}"; + } + currNode = currNode.Next; + + while (currNode != null) { + if (currNode.Value.moveMade.IsNull) break; + + o += $" {currNode.Value.moveMade}"; + currNode = currNode.Next; + } + + return o; + } + public void MakeMove(Move move, bool quiet = false) { //* Wrapper method for MovePiece, calls MovePiece and handles things like board history, 50 move rule, 3 move repition, int movedFrom = move.StartSquare; int movedTo = move.TargetSquare; @@ -105,8 +137,17 @@ public static Piece[] FenToBoard(string fen) { } // Is a promotion - if (pieceMoved.Type == Piece.Pawn && BoardHelper.RankIndex(movedTo) == (pieceMoved.Color==Piece.White ? 7 : 0)) { - board[movedTo] = Piece.Queen|pieceMoved.Color; + if (moveFlag == Move.PromoteToQueenFlag) { + board[movedTo] = pieceMoved.Color|Piece.Queen; + } + if (moveFlag == Move.PromoteToKnightFlag) { + board[movedTo] = pieceMoved.Color|Piece.Knight; + } + if (moveFlag == Move.PromoteToRookFlag) { + board[movedTo] = pieceMoved.Color|Piece.Rook; + } + if (moveFlag == Move.PromoteToBishopFlag) { + board[movedTo] = pieceMoved.Color|Piece.Bishop; } // If move is a castle, move rook @@ -164,9 +205,13 @@ public static Piece[] FenToBoard(string fen) { } } - whiteToMove = !whiteToMove; // ForwardDir / anything related to the active color will be the same up until this point + bool tempWhiteToMove = !whiteToMove; // ForwardDir / anything related to the active color will be the same up until this point if (! quiet) { - currentFen.moveMade = move; + Fen temp = currentStateNode.Value; + temp.moveMade = move; + currentStateNode.Value = temp; + + currentFen = new Fen(currentFen.ToFEN()); currentFen.castlePrivsBin &= castlesToRemove; currentFen.enpassantSquare = (enPassantIndex==-1) ? "-" : BoardHelper.IndexToSquareName(enPassantIndex); @@ -174,12 +219,12 @@ public static Piece[] FenToBoard(string fen) { else { currentFen.halfMoveCount += 1; } if (whiteToMove) currentFen.fullMoveCount += 1; - currentFen.fenColor = whiteToMove ? 'w' : 'b'; - + currentFen.fenColor = tempWhiteToMove ? 'w' : 'b'; BoardHelper.UpdateFenAttachedToBoard(this); - + PushNewState(this.currentFen); } + whiteToMove = tempWhiteToMove; // Need to change whiteToMove after pushing state to fix threading issues between two computer opponents } public void MovePiece(Piece piece, int movedFrom, int movedTo) { //* modify bitboards here @@ -201,6 +246,7 @@ public void PushNewState(Fen newFen) { public void SetPrevState() { if (currentStateNode.Previous == null) { Console.WriteLine("Cannot get previous state, is null"); return; } + currentStateNode = currentStateNode.Previous; currentFen = currentStateNode.Value; UpdateFromState(); diff --git a/src/Engine/Board/Fen.cs b/src/Engine/Board/Fen.cs index 5d7a0db..9c536bc 100644 --- a/src/Engine/Board/Fen.cs +++ b/src/Engine/Board/Fen.cs @@ -6,8 +6,9 @@ namespace ChessBot.Engine { public struct Fen { public static readonly string startpos = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + public readonly bool isStartPos = false; - public Move? moveMade; // Move made on current state, current state + moveMade yields new FEN string + public Move moveMade; // Move made on current state, current state + moveMade yields new FEN string public string fenBoard; public char fenColor; public int castlePrivsBin; @@ -34,6 +35,7 @@ public string castlePrivs { public const int blackQueenCastle = 0b0001; public Fen(string fenString) { + if (fenString == startpos) isStartPos = true; String[] splitFenString = fenString.Split(' '); try { diff --git a/src/Engine/Board/Move.cs b/src/Engine/Board/Move.cs index ae1d8dc..96489ba 100644 --- a/src/Engine/Board/Move.cs +++ b/src/Engine/Board/Move.cs @@ -50,7 +50,15 @@ public bool Equals(Move other) { } public override string ToString() { - return $"{BoardHelper.IndexToSquareName(StartSquare)}{BoardHelper.IndexToSquareName(TargetSquare)}"; + if (IsNull) return "null"; + string promoChar = MoveFlag switch { + PromoteToQueenFlag => "q", + PromoteToBishopFlag => "b", + PromoteToKnightFlag => "n", + PromoteToRookFlag => "r", + _ => "" + }; + return $"{BoardHelper.IndexToSquareName(StartSquare)}{BoardHelper.IndexToSquareName(TargetSquare)}{promoChar}"; } public static bool operator ==(Move a, Move b) => (a.moveValue & ~flagMask) == (b.moveValue & ~flagMask); diff --git a/src/Engine/Helpers/BoardHelper.cs b/src/Engine/Helpers/BoardHelper.cs index 4a2a969..724ab5e 100644 --- a/src/Engine/Helpers/BoardHelper.cs +++ b/src/Engine/Helpers/BoardHelper.cs @@ -94,20 +94,19 @@ public static void UpdateFenAttachedToBoard(Board board) { for (int i=0; i<8;i++) { for (int j=0; j<8; j++) { int index = 8*(7-i)+j; - if (index%8==0 && index!=56) { - if (gap != 0) { o += $"{gap}"; } - o += '/'; - gap = 0; - } int pieceEnum = board.GetSquare(index); if (pieceEnum == Piece.None) { gap += 1; continue; - } + } // Passes guard clause if square is not empty if (gap != 0) { o += $"{gap}"; } o += $"{BoardEnumToChar(pieceEnum)}"; gap = 0; - + } + if (gap != 0) { o += $"{gap}"; } + if (i!=7) { + o += '/'; + gap = 0; } } diff --git a/src/Engine/Stockfish.NET/Core/Stockfish.cs b/src/Engine/Stockfish.NET/Core/Stockfish.cs index b3996a2..e579832 100644 --- a/src/Engine/Stockfish.NET/Core/Stockfish.cs +++ b/src/Engine/Stockfish.NET/Core/Stockfish.cs @@ -3,6 +3,7 @@ using System.Linq; using ChessBot.Engine.Stockfish; using ChessBot.Engine.Stockfish.NET.Models; +using ChessBot.Helpers; namespace ChessBot.Engine.Stockfish { public class Stockfish : IStockfish { @@ -67,6 +68,7 @@ public Stockfish( string path, int depth = 14, Settings? settings = null) { + path = FileHelper.GetResourcePath(path); Depth = depth; _stockfish = new StockfishProcess(path);