diff --git a/Chess-Challenge/src/Framework/Application/Core/ChallengeController.cs b/Chess-Challenge/src/Framework/Application/Core/ChallengeController.cs index e7e089a1b..e8e0be199 100644 --- a/Chess-Challenge/src/Framework/Application/Core/ChallengeController.cs +++ b/Chess-Challenge/src/Framework/Application/Core/ChallengeController.cs @@ -1,9 +1,8 @@ -using ChessChallenge.Chess; +using ChessChallenge.Chess; using ChessChallenge.Example; using Raylib_cs; using System; using System.IO; -using System.Linq; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; @@ -23,12 +22,11 @@ public enum PlayerType } // Game state - Random rng; int gameID; bool isPlaying; Board board; public ChessPlayer PlayerWhite { get; private set; } - public ChessPlayer PlayerBlack {get;private set;} + public ChessPlayer PlayerBlack { get; private set; } float lastMoveMadeTime; bool isWaitingToPlayMove; @@ -40,7 +38,7 @@ public enum PlayerType readonly string[] botMatchStartFens; int botMatchGameIndex; public BotMatchStats BotStatsA { get; private set; } - public BotMatchStats BotStatsB {get;private set;} + public BotMatchStats BotStatsB { get; private set; } bool botAPlaysWhite; @@ -54,6 +52,10 @@ public enum PlayerType readonly MoveGenerator moveGenerator; readonly int tokenCount; readonly StringBuilder pgns; + public bool fastForward; + + int totalMovesPlayed = 0; + public int trueTotalMovesPlayed = 0; public ChallengeController() { @@ -61,15 +63,15 @@ public ChallengeController() tokenCount = GetTokenCount(); Warmer.Warm(); - rng = new Random(); moveGenerator = new(); boardUI = new BoardUI(); board = new Board(); pgns = new(); + fastForward = false; BotStatsA = new BotMatchStats("IBot"); BotStatsB = new BotMatchStats("IBot"); - botMatchStartFens = FileHelper.ReadResourceFile("Fens.txt").Split('\n').Where(fen => fen.Length > 0).ToArray(); + botMatchStartFens = FileHelper.ReadResourceFile("Fens.txt").Split('\n'); botTaskWaitHandle = new AutoResetEvent(false); StartNewGame(PlayerType.Human, PlayerType.MyBot); @@ -79,7 +81,7 @@ public void StartNewGame(PlayerType whiteType, PlayerType blackType) { // End any ongoing game EndGame(GameResult.DrawByArbiter, log: false, autoStartNextBotMatch: false); - gameID = rng.Next(); + gameID++; // Stop prev task and create a new one if (RunBotsOnSeparateThread) @@ -142,7 +144,11 @@ void BotThinkerThread() Move GetBotMove() { + + totalMovesPlayed++; + API.Board botBoard = new(board); + try { API.Timer timer = new(PlayerToMove.TimeRemainingMs); @@ -271,15 +277,16 @@ void PlayMove(Move move) void EndGame(GameResult result, bool log = true, bool autoStartNextBotMatch = true) { + trueTotalMovesPlayed += totalMovesPlayed; + totalMovesPlayed = 0; if (isPlaying) { isPlaying = false; isWaitingToPlayMove = false; - gameID = -1; if (log) { - Log("Game Over: " + result, false, ConsoleColor.Blue); + Log("Game Over: " + result + " Match: " + CurrGameNumber, false, ConsoleColor.Blue); } string pgn = PGNCreator.CreatePGN(board, result, GetPlayerName(PlayerWhite), GetPlayerName(PlayerBlack)); @@ -295,16 +302,24 @@ void EndGame(GameResult result, bool log = true, bool autoStartNextBotMatch = tr if (botMatchGameIndex < numGamesToPlay && autoStartNextBotMatch) { botAPlaysWhite = !botAPlaysWhite; - const int startNextGameDelayMs = 600; - System.Timers.Timer autoNextTimer = new(startNextGameDelayMs); - int originalGameID = gameID; - autoNextTimer.Elapsed += (s, e) => AutoStartNextBotMatchGame(originalGameID, autoNextTimer); - autoNextTimer.AutoReset = false; - autoNextTimer.Start(); + if (fastForward) + { + StartNewGame(PlayerBlack.PlayerType, PlayerWhite.PlayerType); + } + else + { + const int startNextGameDelayMs = 600; + System.Timers.Timer autoNextTimer = new(startNextGameDelayMs); + int originalGameID = gameID; + autoNextTimer.Elapsed += (s, e) => AutoStartNextBotMatchGame(originalGameID, autoNextTimer); + autoNextTimer.AutoReset = false; + autoNextTimer.Start(); + } } else if (autoStartNextBotMatch) { + fastForward = false; Log("Match finished", false, ConsoleColor.Blue); } } @@ -350,31 +365,41 @@ void UpdateStats(BotMatchStats stats, bool isWhiteStats) public void Update() { - if (isPlaying) + + do { - PlayerWhite.Update(); - PlayerBlack.Update(); + if (isPlaying) + { + PlayerWhite.Update(); + PlayerBlack.Update(); - PlayerToMove.UpdateClock(Raylib.GetFrameTime()); - if (PlayerToMove.TimeRemainingMs <= 0) + PlayerToMove.UpdateClock(Raylib.GetFrameTime() + MinMoveDelay); + if (PlayerToMove.TimeRemainingMs <= 0) + { + EndGame(PlayerToMove == PlayerWhite ? GameResult.WhiteTimeout : GameResult.BlackTimeout); + } + else + { + if (isWaitingToPlayMove && (Raylib.GetTime() >= playMoveTime || fastForward)) + { + isWaitingToPlayMove = false; + PlayMove(moveToPlay); + } + } + } + + if (hasBotTaskException) { - EndGame(PlayerToMove == PlayerWhite ? GameResult.WhiteTimeout : GameResult.BlackTimeout); + hasBotTaskException = false; + botExInfo.Throw(); } - else + + if (PlayerWhite.IsHuman || PlayerBlack.IsHuman) { - if (isWaitingToPlayMove && Raylib.GetTime() > playMoveTime) - { - isWaitingToPlayMove = false; - PlayMove(moveToPlay); - } + fastForward = false; } - } - if (hasBotTaskException) - { - hasBotTaskException = false; - botExInfo.Throw(); - } + } while (fastForward && isWaitingToPlayMove); } public void Draw() diff --git a/Chess-Challenge/src/Framework/Application/Core/Settings.cs b/Chess-Challenge/src/Framework/Application/Core/Settings.cs index 4535230ac..c329a7ed1 100644 --- a/Chess-Challenge/src/Framework/Application/Core/Settings.cs +++ b/Chess-Challenge/src/Framework/Application/Core/Settings.cs @@ -9,12 +9,13 @@ public static class Settings // Game settings public const int GameDurationMilliseconds = 60 * 1000; public const float MinMoveDelay = 0; - public static readonly bool RunBotsOnSeparateThread = true; + public static bool RunBotsOnSeparateThread = true; // IF NOT IN FAST FORWARD, TURN THIS ON - It's no longer readonly // Display settings public const bool DisplayBoardCoordinates = true; public static readonly Vector2 ScreenSizeSmall = new(1280, 720); public static readonly Vector2 ScreenSizeBig = new(1920, 1080); + public static readonly Vector2 ScreenSizeXS = new (400, 200); // Other settings public const int MaxTokenCount = 1024; diff --git a/Chess-Challenge/src/Framework/Application/UI/MatchStatsUI.cs b/Chess-Challenge/src/Framework/Application/UI/MatchStatsUI.cs index fc22e4645..f68f4b34b 100644 --- a/Chess-Challenge/src/Framework/Application/UI/MatchStatsUI.cs +++ b/Chess-Challenge/src/Framework/Application/UI/MatchStatsUI.cs @@ -1,6 +1,7 @@ -using Raylib_cs; +using Raylib_cs; using System.Numerics; using System; +using static System.Formats.Asn1.AsnWriter; namespace ChessChallenge.Application { @@ -14,7 +15,10 @@ public static void DrawMatchStats(ChallengeController controller) int regularFontSize = UIHelper.ScaleInt(35); int headerFontSize = UIHelper.ScaleInt(45); Color col = new(180, 180, 180, 255); - Vector2 startPos = UIHelper.Scale(new Vector2(1500, 250)); + Color white = new(225, 225, 225, 225); + Color red = new Color(200, 0, 0, 255); + Color green = new Color(0, 200, 0, 255); + Vector2 startPos = UIHelper.Scale(new Vector2(1500, 150)); float spacingY = UIHelper.Scale(35); DrawNextText($"Game {controller.CurrGameNumber} of {controller.TotalGameCount}", headerFontSize, Color.WHITE); @@ -23,16 +27,28 @@ public static void DrawMatchStats(ChallengeController controller) DrawStats(controller.BotStatsA); startPos.Y += spacingY * 2; DrawStats(controller.BotStatsB); - + + startPos.Y += spacingY * 2; + + string eloDifference = CalculateElo(controller.BotStatsA.NumWins, controller.BotStatsA.NumDraws, controller.BotStatsA.NumLosses); + string errorMargin = CalculateErrorMargin(controller.BotStatsA.NumWins, controller.BotStatsA.NumDraws, controller.BotStatsA.NumLosses); + + DrawNextText($"Elo Difference:", headerFontSize, Color.WHITE); + DrawNextText($"{eloDifference} {errorMargin}", regularFontSize, Color.GRAY); void DrawStats(ChallengeController.BotMatchStats stats) { DrawNextText(stats.BotName + ":", nameFontSize, Color.WHITE); - DrawNextText($"Score: +{stats.NumWins} ={stats.NumDraws} -{stats.NumLosses}", regularFontSize, col); + DrawNextText($"Score: +{stats.NumWins} ={stats.NumDraws} -{stats.NumLosses}", regularFontSize, white); DrawNextText($"Num Timeouts: {stats.NumTimeouts}", regularFontSize, col); DrawNextText($"Num Illegal Moves: {stats.NumIllegalMoves}", regularFontSize, col); + DrawNextText($"Winrate: {(float)stats.NumWins / (controller.CurrGameNumber - 1) * 100}%", regularFontSize, green); + DrawNextText($"Draw rate: {(float)stats.NumDraws / (controller.CurrGameNumber - 1) * 100}%", regularFontSize, white); + DrawNextText($"Loss rate: {(float)stats.NumLosses / (controller.CurrGameNumber - 1) * 100}%", regularFontSize, red); } - + DrawNextText($"Average moves per game: {controller.trueTotalMovesPlayed / controller.CurrGameNumber - 1}", regularFontSize, white); + + void DrawNextText(string text, int fontSize, Color col) { UIHelper.DrawText(text, startPos, fontSize, 1, col); @@ -40,5 +56,66 @@ void DrawNextText(string text, int fontSize, Color col) } } } + + private static string CalculateElo(int wins, int draws, int losses) + { + double score = wins + draws / 2; + int totalGames = wins + draws + losses; + double difference = CalculateEloDifference(score / totalGames); + if ((int)difference == -2147483648) + { + if (difference > 0) return "+Inf"; + else return "-Inf"; + } + + return $"{(int)difference}"; + } + + private static double CalculateEloDifference(double percentage) + { + return -400 * Math.Log(1 / percentage - 1) / 2.302; + } + + private static string CalculateErrorMargin(int wins, int draws, int losses) + { + double total = wins + draws + losses; + double winP = wins / total; + double drawP = draws / total; + double lossP = losses / total; + + double percentage = (wins + draws / 2) / total; + double winDev = winP * Math.Pow(1 - percentage, 2); + double drawsDev = drawP * Math.Pow(0.5 - percentage, 2); + double lossesDev = lossP * Math.Pow(0 - percentage, 2); + + double stdDeviation = Math.Sqrt(winDev + drawsDev + lossesDev) / Math.Sqrt(total); + + double confidenceP = 0.95; + double minConfidenceP = (1 - confidenceP) / 2; + double maxConfidenceP = 1 - minConfidenceP; + double devMin = percentage + PhiInv(minConfidenceP) * stdDeviation; + double devMax = percentage + PhiInv(maxConfidenceP) * stdDeviation; + + double difference = CalculateEloDifference(devMax) - CalculateEloDifference(devMin); + double margin = Math.Round(difference / 2); + if (double.IsNaN(margin)) return ""; + return $"+/- {margin}"; + } + + private static double PhiInv(double p) + { + return Math.Sqrt(2) * CalculateInverseErrorFunction(2 * p - 1); + } + + private static double CalculateInverseErrorFunction(double x) + { + double a = 8 * (Math.PI - 3) / (3 * Math.PI * (4 - Math.PI)); + double y = Math.Log(1 - x * x); + double z = 2 / (Math.PI * a) + y / 2; + + double ret = Math.Sqrt(Math.Sqrt(z * z - y / a) - z); + if (x < 0) return -ret; + return ret; + } } -} \ No newline at end of file +} diff --git a/Chess-Challenge/src/Framework/Application/UI/MenuUI.cs b/Chess-Challenge/src/Framework/Application/UI/MenuUI.cs index 1e39fcf3f..4441d522e 100644 --- a/Chess-Challenge/src/Framework/Application/UI/MenuUI.cs +++ b/Chess-Challenge/src/Framework/Application/UI/MenuUI.cs @@ -65,10 +65,22 @@ public static void DrawButtons(ChallengeController controller) { Program.SetWindowSize(isBigWindow ? Settings.ScreenSizeSmall : Settings.ScreenSizeBig); } + + if(NextButtonInRow("Smallerer Window", ref buttonPos, spacing, buttonSize)) + { + Program.SetWindowsSize(Settings.ScreenSizeXS); + } + if (NextButtonInRow("Exit (ESC)", ref buttonPos, spacing, buttonSize)) { Environment.Exit(0); } + if (NextButtonInRow("Fast forward", ref buttonPos, spacing, buttonSize)) + { + controller.fastForward = !controller.fastForward; + if(controller.fastForward) Settings.RunBotsOnSeparateThread = false; + else Settings.RunBotsOnSeparateThread = true; + } bool NextButtonInRow(string name, ref Vector2 pos, float spacingY, Vector2 size) { @@ -78,4 +90,4 @@ bool NextButtonInRow(string name, ref Vector2 pos, float spacingY, Vector2 size) } } } -} \ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..657063ef3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM docker.io/archlinux:latest +RUN pacman-key --init +RUN pacman -Syu --noconfirm \ + dotnet-sdk-6.0 \ + mesa \ + && pacman -Scc --noconfirm +COPY . /app/ +WORKDIR /app/Chess-Challenge/ +RUN cd /app/ && dotnet build +CMD dotnet run diff --git a/README.md b/README.md index dcaace30a..40b5a35ab 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ All names (variables, functions, etc.) are counted as a single token, regardless * For example: `cd C:\Users\MyName\Desktop\Chess-Challenge\Chess-Challenge` * Now use the command: `dotnet run` * This should launch the project. If not, open an issue with any error messages and relevant info. -* [Running on Linux](https://github.com/SebLague/Chess-Challenge/discussions/3) +* [Running on Linux](https://github.com/SebLague/Chess-Challenge/discussions/3) Or with the Dockerfile provided (run: `./run.sh` (you need to have docker or podman installed on your machine)) * Issues with illegal moves or errors when making/undoing a move * 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. * How to tell what colour MyBot is playing diff --git a/run.sh b/run.sh new file mode 100755 index 000000000..570d7c81d --- /dev/null +++ b/run.sh @@ -0,0 +1,31 @@ +#!/bin/bash -e + +if grep -q 'Arch' /etc/os-release; then + unset DBUS_SESSION_BUS_ADDRESS +fi + +if [ -z "$WAYLAND_DISPLAY" ]; then + CRI_OPT+=( + --network host + -e XAUTHORITY=/app/.Xauthority + -v "$XAUTHORITY:/app/.Xauthority:ro" + ) +fi + +CRI="$(command -v podman || command -v docker)" + +if ! "${CRI[@]}" image ls &>/dev/null; then + CRI=(sudo "${CRI[@]}") +fi + +"${CRI[@]}" build . -t chess + +"${CRI[@]}" run --rm --name chess \ + "${CRI_OPT[@]}" \ + --device /dev/dri/ \ + -e DISPLAY \ + -e XDG_RUNTIME_DIR \ + -v /dev/shm/:/dev/shm/ \ + -v /tmp/.X11-unix/:/tmp/.X11-unix/ \ + -v "$XDG_RUNTIME_DIR:$XDG_RUNTIME_DIR" \ + chess