diff --git a/.gitignore b/.gitignore index 8626380..7837a5d 100644 --- a/.gitignore +++ b/.gitignore @@ -347,6 +347,13 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# Custom /cloc.exe /cloc.bat /Cosette.Arbiter/settings.json +/Cosette.Arbiter/book.bin +/Cosette.Tuner/settings.json +/Cosette.Tuner/book.bin +/Cosette.Tuner.Web/Database.sqlite +/Cosette.Tuner.Web/wwwroot/lib/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cedb88c..57e3d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +# Version 3.0 (Luna), 12.12.2020 + - Added better time control for Arbiter + - Added more UCI options + - Added Tuner project + - Added insufficient material detection + - Added executable hash generator + - Added abort when search lasts too long + - Added legality checking of the hash table moves + - Added SEE pruning in the quiescence search + - Added fianchetto evaluation + - Added internal iterative deepening + - Added a lot of UCI options, allowing for full engine customization + - Added multi-stage move ordering + - Added multi-stage move generating + - Fixed FEN parser when input didn't have halfmove clock and moves count + - Fixed crash when the engine was receiving invalid position in UCI mode + - Fixed UCI statistics + - Improved time scheduler when incrementation time is present + - Improved mobility calculation by rewarding for center control + - Improved late move reduction conditions + - Improved SEE accuracy (now includes x-ray attacks) + - Improved king safety evaluation + - Changed maximal moves count from 128 to 218 (according to Internet sources) + - Reduced size of transposition table entry (from 12 bytes to 8 bytes) + - Disabled most of the evaluation functions when the game is near to end + - Disabled returning of exact transposition table entries in the PV nodes + - Adjusted evaluation scores + - Optimized king safety evaluation + - Updated .NET Core runtime version to 5.0.100 + +Estimated strength: 2000 ELO + # Version 2.0 (Darkness), 19.10.2020 - Added fifty-move rule detection - Added new evaluation functions: pawn shield, bishop pair, doubled rooks, a rook on open file diff --git a/Cosette.Arbiter/Cosette.Arbiter.csproj b/Cosette.Arbiter/Cosette.Arbiter.csproj index aedbf75..b5bd21c 100644 --- a/Cosette.Arbiter/Cosette.Arbiter.csproj +++ b/Cosette.Arbiter/Cosette.Arbiter.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net5.0 @@ -17,6 +17,10 @@ + + + + PreserveNewest diff --git a/Cosette.Arbiter/Engine/EngineOperator.cs b/Cosette.Arbiter/Engine/EngineOperator.cs index 291bec0..ca17a52 100644 --- a/Cosette.Arbiter/Engine/EngineOperator.cs +++ b/Cosette.Arbiter/Engine/EngineOperator.cs @@ -1,20 +1,27 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Security.Cryptography; +using System.Text; using Cosette.Arbiter.Settings; namespace Cosette.Arbiter.Engine { public class EngineOperator { - private string _name; + public Lazy ExecutableHash; + private string _enginePath; + private string _engineArguments; private Process _engineProcess; - public EngineOperator(string name, string path) + public EngineOperator(string path, string arguments) { - _name = name; _enginePath = path; + _engineArguments = arguments; + + ExecutableHash = new Lazy(GetExecutableHash); } public void Init() @@ -22,6 +29,7 @@ public void Init() _engineProcess = Process.Start(new ProcessStartInfo { FileName = _enginePath, + Arguments = _engineArguments, CreateNoWindow = true, RedirectStandardInput = true, RedirectStandardOutput = true @@ -30,12 +38,23 @@ public void Init() Write("uci"); WaitForMessage("uciok"); - SettingsLoader.Data.Options.ForEach(Write); + ApplyOptions(); Write("isready"); WaitForMessage("readyok"); } + public void Restart() + { + if (!_engineProcess.HasExited) + { + _engineProcess.Close(); + } + + Init(); + ApplyOptions(); + } + public void InitNewGame() { Write("ucinewgame"); @@ -43,7 +62,12 @@ public void InitNewGame() WaitForMessage("readyok"); } - public BestMoveData Go(List moves) + public void ApplyOptions() + { + SettingsLoader.Data.Options.ForEach(Write); + } + + public BestMoveData Go(List moves, int whiteClock, int blackClock) { var bestMoveData = new BestMoveData(); var movesJoined = string.Join(' ', moves); @@ -53,26 +77,22 @@ public BestMoveData Go(List moves) Write($"position startpos moves {movesJoined}"); } - Write($"go movetime {SettingsLoader.Data.MillisecondsPerMove}"); + Write($"go wtime {whiteClock} btime {blackClock} winc {SettingsLoader.Data.IncTime} binc {SettingsLoader.Data.IncTime}"); while (true) { - try + var response = Read(); + if (response.StartsWith("info depth")) { - var response = Read(); - if (response.StartsWith("info depth")) - { - bestMoveData.LastInfoData = InfoData.FromString(response); - } - else if (response.StartsWith("bestmove")) - { - bestMoveData.BestMove = response.Split(' ')[1]; - break; - } + bestMoveData.LastInfoData = InfoData.FromString(response); } - catch + else if (response.StartsWith("bestmove")) + { + bestMoveData.BestMove = response.Split(' ')[1]; + break; + } + else if (response.StartsWith("error")) { - Init(); return null; } } @@ -94,5 +114,24 @@ public void WaitForMessage(string message) { while (Read() != message) ; } + + private string GetExecutableHash() + { + var md5 = new MD5CryptoServiceProvider(); + var path = _enginePath == "dotnet" ? _engineArguments : _enginePath; + + using (var streamReader = new StreamReader(path)) + { + md5.ComputeHash(streamReader.BaseStream); + } + + var hashBuilder = new StringBuilder(); + foreach (var b in md5.Hash) + { + hashBuilder.Append(b.ToString("x2")); + } + + return hashBuilder.ToString(); + } } } diff --git a/Cosette.Arbiter/Settings/EngineData.cs b/Cosette.Arbiter/Settings/EngineData.cs index be15300..2cc0dc9 100644 --- a/Cosette.Arbiter/Settings/EngineData.cs +++ b/Cosette.Arbiter/Settings/EngineData.cs @@ -4,6 +4,7 @@ public class EngineData { public string Name { get; set; } public string Path { get; set; } + public string Arguments { get; set; } public int Rating { get; set; } } } diff --git a/Cosette.Arbiter/Settings/SettingsModel.cs b/Cosette.Arbiter/Settings/SettingsModel.cs index 16668bc..bccc896 100644 --- a/Cosette.Arbiter/Settings/SettingsModel.cs +++ b/Cosette.Arbiter/Settings/SettingsModel.cs @@ -8,15 +8,18 @@ public class SettingsModel public List Engines { get; set; } public List Options { get; set; } + [JsonProperty("base_time")] + public int BaseTime { get; set; } + + [JsonProperty("inc_time")] + public int IncTime { get; set; } + [JsonProperty("polyglot_opening_book")] public string PolyglotOpeningBook { get; set; } [JsonProperty("polyglot_max_moves")] public int PolyglotMaxMoves { get; set; } - [JsonProperty("milliseconds_per_move")] - public int MillisecondsPerMove { get; set; } - [JsonProperty("max_moves_count")] public int MaxMovesCount { get; set; } diff --git a/Cosette.Arbiter/Tournament/ArchivedGame.cs b/Cosette.Arbiter/Tournament/ArchivedGame.cs index da20c27..ce23040 100644 --- a/Cosette.Arbiter/Tournament/ArchivedGame.cs +++ b/Cosette.Arbiter/Tournament/ArchivedGame.cs @@ -5,12 +5,14 @@ public class ArchivedGame public GameData GameData { get; } public TournamentParticipant Opponent { get; } public GameResult Result { get; } + public bool TimeFlag { get; } - public ArchivedGame(GameData gameData, TournamentParticipant opponent, GameResult result) + public ArchivedGame(GameData gameData, TournamentParticipant opponent, GameResult result, bool timeFlag = false) { GameData = gameData; Opponent = opponent; Result = result; + TimeFlag = timeFlag; } } } diff --git a/Cosette.Arbiter/Tournament/GameData.cs b/Cosette.Arbiter/Tournament/GameData.cs index ac1ca9a..e14f675 100644 --- a/Cosette.Arbiter/Tournament/GameData.cs +++ b/Cosette.Arbiter/Tournament/GameData.cs @@ -10,12 +10,18 @@ public class GameData public bool GameIsDone { get; private set; } public Color Winner { get; private set; } + public int WhiteClock { get; private set; } + public int BlackClock { get; private set; } + private BestMoveData _lastBestMove; private Color _colorToMove; public GameData(List opening) { - HalfMovesDone = opening; + HalfMovesDone = new List(opening); + WhiteClock = SettingsLoader.Data.BaseTime; + BlackClock = SettingsLoader.Data.BaseTime; + _colorToMove = Color.White; } @@ -23,7 +29,25 @@ public void MakeMove(BestMoveData bestMoveData) { HalfMovesDone.Add(bestMoveData.BestMove); _lastBestMove = bestMoveData; - + + var clock = _colorToMove == Color.White ? WhiteClock : BlackClock; + clock -= bestMoveData.LastInfoData.Time; + + if (clock <= 0) + { + GameIsDone = true; + WhiteClock = _colorToMove == Color.White ? clock : WhiteClock; + BlackClock = _colorToMove == Color.Black ? clock : BlackClock; + + Winner = _colorToMove == Color.White ? Color.Black : Color.White; + return; + } + + clock += SettingsLoader.Data.IncTime; + + WhiteClock = _colorToMove == Color.White ? clock : WhiteClock; + BlackClock = _colorToMove == Color.Black ? clock : BlackClock; + if (IsCheckmate() || bestMoveData.LastInfoData.ScoreCp >= 2000) { GameIsDone = true; diff --git a/Cosette.Arbiter/Tournament/TournamentArbiter.cs b/Cosette.Arbiter/Tournament/TournamentArbiter.cs index fbed0b2..7077453 100644 --- a/Cosette.Arbiter/Tournament/TournamentArbiter.cs +++ b/Cosette.Arbiter/Tournament/TournamentArbiter.cs @@ -1,27 +1,30 @@ using System; using System.Collections.Generic; -using Cosette.Arbiter.Book; +using System.Diagnostics; +using System.Linq; using Cosette.Arbiter.Engine; using Cosette.Arbiter.Settings; +using Cosette.Polyglot; namespace Cosette.Arbiter.Tournament { public class TournamentArbiter { private List _participants; + private List _gamesDuration; private TournamentScheduler _scheduler; private PolyglotBook _polyglotBook; - private int _errors; public TournamentArbiter() { _participants = new List(); + _gamesDuration = new List(); _scheduler = new TournamentScheduler(); - _polyglotBook = new PolyglotBook(); + _polyglotBook = new PolyglotBook(SettingsLoader.Data.PolyglotOpeningBook); foreach (var engineData in SettingsLoader.Data.Engines) { - var engineOperator = new EngineOperator(engineData.Name, engineData.Path); + var engineOperator = new EngineOperator(engineData.Path, engineData.Arguments); var tournamentParticipant = new TournamentParticipant(engineData, engineOperator); _participants.Add(tournamentParticipant); @@ -33,69 +36,95 @@ public TournamentArbiter() public void Run() { _participants.ForEach(p => p.EngineOperator.Init()); - for (var gameIndex = 0; gameIndex < SettingsLoader.Data.GamesCount; gameIndex++) + for (var gameIndex = 0; gameIndex < SettingsLoader.Data.GamesCount;) { - var gameData = new GameData(_polyglotBook.GetRandomOpening()); - var (playerA, playerB) = _scheduler.GetPair(gameIndex); + var openingBookMoves = _polyglotBook.GetRandomOpening(SettingsLoader.Data.PolyglotMaxMoves); + var (playerA, playerB) = _scheduler.GetPair(gameIndex / 2); if (playerA >= _participants.Count || playerB >= _participants.Count) { + gameIndex++; continue; } - var participantA = _participants[playerA]; - var participantB = _participants[playerB]; + var whitePlayer = _participants[playerA]; + var blackPlayer = _participants[playerB]; - var whitePlayer = DateTime.UtcNow.Ticks % 2 == 0 ? participantA : participantB; - var blackPlayer = whitePlayer == participantA ? participantB : participantA; - var (playerToMove, opponent) = (whitePlayer, blackPlayer); - - Console.Clear(); - WriteResults(); - - Console.WriteLine($"Game {gameIndex} ({whitePlayer.EngineData.Name} vs. {blackPlayer.EngineData.Name})"); - Console.Write("Moves: "); - Console.Write(string.Join(' ', gameData.HalfMovesDone)); - Console.Write(" "); - - participantA.EngineOperator.InitNewGame(); - participantB.EngineOperator.InitNewGame(); - - - while (true) + for (var i = 0; i < 2; i++) { - var bestMoveData = playerToMove.EngineOperator.Go(gameData.HalfMovesDone); - if (bestMoveData == null) - { - _errors++; - break; - } + var gameData = new GameData(openingBookMoves); + var (playerToMove, opponent) = (whitePlayer, blackPlayer); - playerToMove.Logs.Add(bestMoveData.LastInfoData); - gameData.MakeMove(bestMoveData); + Console.Clear(); + WriteResults(); + WriteTournamentStatistics(); - Console.Write(bestMoveData.BestMove); + Console.WriteLine($"Game {gameIndex} ({whitePlayer.EngineData.Name} vs. {blackPlayer.EngineData.Name}), {openingBookMoves.Count} opening book moves:"); + Console.Write("Moves: "); + Console.Write(string.Join(' ', gameData.HalfMovesDone)); Console.Write(" "); - if (gameData.GameIsDone) + whitePlayer.EngineOperator.InitNewGame(); + blackPlayer.EngineOperator.InitNewGame(); + + var gameStopwatch = Stopwatch.StartNew(); + while (true) { - if (gameData.Winner == Color.None) + try { - playerToMove.History.Add(new ArchivedGame(gameData, opponent, GameResult.Draw)); - opponent.History.Add(new ArchivedGame(gameData, playerToMove, GameResult.Draw)); + var bestMoveData = playerToMove.EngineOperator.Go(gameData.HalfMovesDone, gameData.WhiteClock, gameData.BlackClock); + if (bestMoveData == null || bestMoveData.LastInfoData == null || bestMoveData.BestMove == "h1h1") + { + playerToMove.History.Add(new ArchivedGame(gameData, opponent, GameResult.Draw)); + opponent.History.Add(new ArchivedGame(gameData, playerToMove, GameResult.Draw)); + break; + } + + playerToMove.Logs.Add(bestMoveData.LastInfoData); + gameData.MakeMove(bestMoveData); + + Console.Write(bestMoveData.BestMove); + Console.Write(" "); + + if (gameData.GameIsDone) + { + if (gameData.WhiteClock <= 0 || gameData.BlackClock <= 0) + { + playerToMove.History.Add(new ArchivedGame(gameData, opponent, GameResult.Loss, true)); + opponent.History.Add(new ArchivedGame(gameData, playerToMove, GameResult.Win, true)); + } + else if (gameData.Winner == Color.None) + { + playerToMove.History.Add(new ArchivedGame(gameData, opponent, GameResult.Draw)); + opponent.History.Add(new ArchivedGame(gameData, playerToMove, GameResult.Draw)); + } + else + { + playerToMove.History.Add(new ArchivedGame(gameData, opponent, GameResult.Win)); + opponent.History.Add(new ArchivedGame(gameData, playerToMove, GameResult.Loss)); + } + + break; + } + + (playerToMove, opponent) = (opponent, playerToMove); } - else + catch { - playerToMove.History.Add(new ArchivedGame(gameData, opponent, GameResult.Win)); - opponent.History.Add(new ArchivedGame(gameData, playerToMove, GameResult.Loss)); + whitePlayer.EngineOperator.Restart(); + blackPlayer.EngineOperator.Restart(); } - - break; } - (playerToMove, opponent) = (opponent, playerToMove); + _gamesDuration.Add(gameStopwatch.ElapsedMilliseconds); + (whitePlayer, blackPlayer) = (blackPlayer, whitePlayer); + gameIndex++; } } + + Console.WriteLine(); + Console.WriteLine("Test ended, press any key to close Arbiter"); + Console.ReadLine(); } private void WriteResults() @@ -105,14 +134,25 @@ private void WriteResults() var originalRating = participant.EngineData.Rating; var performance = participant.CalculatePerformanceRating() - originalRating; var wonGamesPercent = participant.WonGamesPercent(); + var winsByTime = participant.History.Count(p => p.Result == GameResult.Win && p.TimeFlag); + var lossesByTime = participant.History.Count(p => p.Result == GameResult.Loss && p.TimeFlag); Console.WriteLine($"{participant.EngineData.Name} {originalRating} ELO ({performance:+0;-#}, {wonGamesPercent}%): " + - $"{participant.Wins} wins, {participant.Losses} losses, " + - $"{participant.Draws} draws, {_errors} errors"); + $"{participant.Wins} wins ({winsByTime} by time), {participant.Losses} losses ({lossesByTime} by time), " + + $"{participant.Draws} draws"); Console.WriteLine($" === {participant.AverageDepth:F1} average depth, {participant.AverageNodesCount} average nodes, " + $"{participant.AverageNps} average nodes per second"); + Console.WriteLine($"Executable hash: {participant.EngineOperator.ExecutableHash.Value}"); Console.WriteLine(); } } + + private void WriteTournamentStatistics() + { + var averageGameTime = (_gamesDuration.Count != 0 ? _gamesDuration.Average() : 0.0) / 1000; + + Console.WriteLine($"Tournament statistics: {averageGameTime:F} s per average game"); + Console.WriteLine(); + } } } diff --git a/Cosette.Arbiter/book.bin b/Cosette.Arbiter/book.bin deleted file mode 100644 index 9670b17..0000000 Binary files a/Cosette.Arbiter/book.bin and /dev/null differ diff --git a/Cosette.Arbiter.Tests/Cosette.Arbiter.Tests.csproj b/Cosette.Polyglot.Tests/Cosette.Polyglot.Tests.csproj similarity index 72% rename from Cosette.Arbiter.Tests/Cosette.Arbiter.Tests.csproj rename to Cosette.Polyglot.Tests/Cosette.Polyglot.Tests.csproj index 776d0eb..9b12b49 100644 --- a/Cosette.Arbiter.Tests/Cosette.Arbiter.Tests.csproj +++ b/Cosette.Polyglot.Tests/Cosette.Polyglot.Tests.csproj @@ -1,7 +1,7 @@ - + - netcoreapp3.1 + net5.0 false @@ -14,7 +14,7 @@ - + diff --git a/Cosette.Arbiter.Tests/HashKeyTests.cs b/Cosette.Polyglot.Tests/HashKeyTests.cs similarity index 91% rename from Cosette.Arbiter.Tests/HashKeyTests.cs rename to Cosette.Polyglot.Tests/HashKeyTests.cs index ca7d7b6..28e053d 100644 --- a/Cosette.Arbiter.Tests/HashKeyTests.cs +++ b/Cosette.Polyglot.Tests/HashKeyTests.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using Cosette.Arbiter.Book; +using Cosette.Polyglot.Book; using Xunit; -namespace Cosette.Arbiter.Tests +namespace Cosette.Polyglot.Tests { public class HashKeyTests { diff --git a/Cosette.Arbiter/Book/CastlingFlags.cs b/Cosette.Polyglot/Book/CastlingFlags.cs similarity index 91% rename from Cosette.Arbiter/Book/CastlingFlags.cs rename to Cosette.Polyglot/Book/CastlingFlags.cs index 7d51a9a..10bfa59 100644 --- a/Cosette.Arbiter/Book/CastlingFlags.cs +++ b/Cosette.Polyglot/Book/CastlingFlags.cs @@ -1,6 +1,6 @@ using System; -namespace Cosette.Arbiter.Book +namespace Cosette.Polyglot.Book { [Flags] public enum CastlingFlags diff --git a/Cosette.Arbiter/Book/ColorType.cs b/Cosette.Polyglot/Book/ColorType.cs similarity index 66% rename from Cosette.Arbiter/Book/ColorType.cs rename to Cosette.Polyglot/Book/ColorType.cs index ae23ffc..4aadb7b 100644 --- a/Cosette.Arbiter/Book/ColorType.cs +++ b/Cosette.Polyglot/Book/ColorType.cs @@ -1,4 +1,4 @@ -namespace Cosette.Arbiter.Book +namespace Cosette.Polyglot.Book { public enum ColorType { diff --git a/Cosette.Arbiter/Book/PieceType.cs b/Cosette.Polyglot/Book/PieceType.cs similarity index 89% rename from Cosette.Arbiter/Book/PieceType.cs rename to Cosette.Polyglot/Book/PieceType.cs index 08abb29..e960c21 100644 --- a/Cosette.Arbiter/Book/PieceType.cs +++ b/Cosette.Polyglot/Book/PieceType.cs @@ -1,4 +1,4 @@ -namespace Cosette.Arbiter.Book +namespace Cosette.Polyglot.Book { public enum PieceType { diff --git a/Cosette.Arbiter/Book/PolyglotBoard.cs b/Cosette.Polyglot/Book/PolyglotBoard.cs similarity index 99% rename from Cosette.Arbiter/Book/PolyglotBoard.cs rename to Cosette.Polyglot/Book/PolyglotBoard.cs index df62541..40364c1 100644 --- a/Cosette.Arbiter/Book/PolyglotBoard.cs +++ b/Cosette.Polyglot/Book/PolyglotBoard.cs @@ -1,6 +1,6 @@ using System; -namespace Cosette.Arbiter.Book +namespace Cosette.Polyglot.Book { public class PolyglotBoard { diff --git a/Cosette.Arbiter/Book/PolyglotBookEntry.cs b/Cosette.Polyglot/Book/PolyglotBookEntry.cs similarity index 90% rename from Cosette.Arbiter/Book/PolyglotBookEntry.cs rename to Cosette.Polyglot/Book/PolyglotBookEntry.cs index e344ace..5796180 100644 --- a/Cosette.Arbiter/Book/PolyglotBookEntry.cs +++ b/Cosette.Polyglot/Book/PolyglotBookEntry.cs @@ -1,4 +1,4 @@ -namespace Cosette.Arbiter.Book +namespace Cosette.Polyglot.Book { public struct PolyglotBookEntry { diff --git a/Cosette.Arbiter/Book/PolyglotBookMove.cs b/Cosette.Polyglot/Book/PolyglotBookMove.cs similarity index 98% rename from Cosette.Arbiter/Book/PolyglotBookMove.cs rename to Cosette.Polyglot/Book/PolyglotBookMove.cs index e20af91..95469e6 100644 --- a/Cosette.Arbiter/Book/PolyglotBookMove.cs +++ b/Cosette.Polyglot/Book/PolyglotBookMove.cs @@ -1,6 +1,6 @@ #pragma warning disable 649 -namespace Cosette.Arbiter.Book +namespace Cosette.Polyglot.Book { public struct PolyglotBookMove { diff --git a/Cosette.Arbiter/Book/PolyglotConstants.cs b/Cosette.Polyglot/Book/PolyglotConstants.cs similarity index 99% rename from Cosette.Arbiter/Book/PolyglotConstants.cs rename to Cosette.Polyglot/Book/PolyglotConstants.cs index 368833f..872ac12 100644 --- a/Cosette.Arbiter/Book/PolyglotConstants.cs +++ b/Cosette.Polyglot/Book/PolyglotConstants.cs @@ -1,4 +1,4 @@ -namespace Cosette.Arbiter.Book +namespace Cosette.Polyglot.Book { public static class PolyglotConstants { diff --git a/Cosette.Polyglot/Cosette.Polyglot.csproj b/Cosette.Polyglot/Cosette.Polyglot.csproj new file mode 100644 index 0000000..1b18b81 --- /dev/null +++ b/Cosette.Polyglot/Cosette.Polyglot.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + false + true + + + + true + + + diff --git a/Cosette.Arbiter/Book/PolyglotBook.cs b/Cosette.Polyglot/PolyglotBook.cs similarity index 88% rename from Cosette.Arbiter/Book/PolyglotBook.cs rename to Cosette.Polyglot/PolyglotBook.cs index e46bb73..9e35bc6 100644 --- a/Cosette.Arbiter/Book/PolyglotBook.cs +++ b/Cosette.Polyglot/PolyglotBook.cs @@ -3,26 +3,28 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; -using Cosette.Arbiter.Settings; +using Cosette.Polyglot.Book; -namespace Cosette.Arbiter.Book +namespace Cosette.Polyglot { public class PolyglotBook { + private string _bookFile; private Random _random; - public PolyglotBook() + public PolyglotBook(string bookFile) { + _bookFile = bookFile; _random = new Random(); } - public List GetRandomOpening() + public List GetRandomOpening(int movesCount) { var movesList = new List(); var polyglotBoard = new PolyglotBoard(); polyglotBoard.InitDefaultState(); - for (var moveIndex = 0; moveIndex < SettingsLoader.Data.PolyglotMaxMoves; moveIndex++) + for (var moveIndex = 0; moveIndex < movesCount; moveIndex++) { var availableMoves = GetBookEntries(polyglotBoard.CalculateHash()); if (availableMoves.Count == 0) @@ -51,6 +53,11 @@ public List GetRandomOpening() } } + if (movesList.Count % 2 != 0) + { + movesList.Remove(movesList.Last()); + } + return movesList.Select(p => p.Move.ToString()).ToList(); } @@ -59,12 +66,12 @@ public unsafe List GetBookEntries(ulong hash) var foundEntries = new List(); var entrySize = sizeof(PolyglotBookEntry); - var bookInfo = new FileInfo(SettingsLoader.Data.PolyglotOpeningBook); + var bookInfo = new FileInfo(_bookFile); var entriesCount = bookInfo.Length / entrySize; long left = 0; long right = entriesCount - 1; - using (var binaryReader = new BinaryReader(new FileStream(SettingsLoader.Data.PolyglotOpeningBook, FileMode.Open))) + using (var binaryReader = new BinaryReader(new FileStream(_bookFile, FileMode.Open))) { var buffer = new byte[16]; var bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); diff --git a/Cosette.Tests/Cosette.Tests.csproj b/Cosette.Tests/Cosette.Tests.csproj index 36db1c9..4650cbe 100644 --- a/Cosette.Tests/Cosette.Tests.csproj +++ b/Cosette.Tests/Cosette.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 false diff --git a/Cosette.Tests/StaticExchangeEvaluationTests.cs b/Cosette.Tests/StaticExchangeEvaluationTests.cs index 3b5a226..f8f3069 100644 --- a/Cosette.Tests/StaticExchangeEvaluationTests.cs +++ b/Cosette.Tests/StaticExchangeEvaluationTests.cs @@ -12,14 +12,17 @@ public StaticExchangeEvaluationTests() } [Theory] - [InlineData(Piece.Pawn, Piece.Pawn, (1 << Piece.Pawn), 0, 100)] // 8/8/8/3p4/4P3/8/8/8 w - - 0 1 - [InlineData(Piece.Pawn, Piece.Pawn, (1 << Piece.Pawn) | (1 << Piece.Knight), (1 << Piece.Pawn) | (1 << Piece.Knight), 0)] // 8/2n5/4p3/3p4/4P3/4N3/8/8 w - - 0 1 - [InlineData(Piece.Knight, Piece.Pawn, (1 << Piece.Pawn) | (1 << Piece.Knight), (1 << Piece.Pawn) | (1 << Piece.Knight), -220)] // 8/2n5/4p3/3p4/4P3/4N3/8/8 w - - 0 1 - [InlineData(Piece.Knight, Piece.Pawn, (1 << Piece.Pawn) | (1 << Piece.Knight) | (1 << Piece.Queen), (1 << Piece.Pawn) | (1 << Piece.Knight), -120)] // 8/2n5/4p3/3p4/4P3/4N3/Q7/8 w - - 0 1 - [InlineData(Piece.Pawn, Piece.Pawn, (1 << Piece.Knight) | (1 << Piece.Bishop), (1 << Piece.Queen), 100)] // 3q4/8/8/3p4/8/2N2B2/8/8 w - - 0 1 - [InlineData(Piece.Knight, Piece.Pawn, (1 << Piece.Knight) | (1 << Piece.Bishop), (1 << Piece.Queen), 100)] // 3q4/8/8/3p4/8/2N2B2/8/8 w - - 0 1 - [InlineData(Piece.Queen, Piece.Pawn, (1 << Piece.Knight) | (1 << Piece.Bishop) | (1 << Piece.Queen), (1 << Piece.Rook), -500)] // 3r4/8/8/3p4/8/2N2B2/8/3Q4 w - - 0 1 - [InlineData(Piece.Knight, Piece.Pawn, (1 << Piece.Knight) | (1 << Piece.Rook) | (1 << Piece.Queen), (1 << Piece.Knight) | (1 << Piece.Bishop) | (1 << Piece.Queen), -220)] // 7q/3n4/5b2/4p3/8/3N4/4R3/4Q3 w - - 0 1 + [InlineData(Piece.Pawn, Piece.Pawn, (1 << SeePiece.Pawn), 0, 100)] // 8/8/8/3p4/4P3/8/8/8 w - - 0 1 + [InlineData(Piece.Pawn, Piece.Pawn, (1 << SeePiece.Pawn) | (1 << SeePiece.Knight1), (1 << SeePiece.Pawn) | (1 << SeePiece.Knight1), 0)] // 8/2n5/4p3/3p4/4P3/4N3/8/8 w - - 0 1 + [InlineData(Piece.Knight, Piece.Pawn, (1 << SeePiece.Pawn) | (1 << SeePiece.Knight1), (1 << SeePiece.Pawn) | (1 << SeePiece.Knight1), -220)] // 8/2n5/4p3/3p4/4P3/4N3/8/8 w - - 0 1 + [InlineData(Piece.Knight, Piece.Pawn, (1 << SeePiece.Pawn) | (1 << SeePiece.Knight1) | (1 << SeePiece.Queen), (1 << SeePiece.Pawn) | (1 << Piece.Knight), -120)] // 8/2n5/4p3/3p4/4P3/4N3/Q7/8 w - - 0 1 + [InlineData(Piece.Pawn, Piece.Pawn, (1 << SeePiece.Pawn) | (1 << SeePiece.Knight1) | (1 << SeePiece.Bishop), (1 << SeePiece.Queen), 100)] // 3q4/8/8/3p4/8/2N2B2/8/8 w - - 0 1 + [InlineData(Piece.Knight, Piece.Pawn, (1 << SeePiece.Knight1) | (1 << SeePiece.Bishop), (1 << SeePiece.Queen), 100)] // 3q4/8/8/3p4/8/2N2B2/8/8 w - - 0 1 + [InlineData(Piece.Queen, Piece.Pawn, (1 << SeePiece.Knight1) | (1 << SeePiece.Bishop) | (1 << SeePiece.Queen), (1 << SeePiece.Rook1), -500)] // 3r4/8/8/3p4/8/2N2B2/8/3Q4 w - - 0 1 + [InlineData(Piece.Knight, Piece.Pawn, (1 << SeePiece.Knight1) | (1 << SeePiece.Rook1) | (1 << SeePiece.Queen), (1 << SeePiece.Knight1) | (1 << SeePiece.Bishop) | (1 << SeePiece.Queen), -220)] // 7q/3n4/5b2/4p3/8/3N4/4R3/4Q3 w - - 0 1 + [InlineData(Piece.Rook, Piece.Pawn, (1 << SeePiece.Rook1) | (1 << SeePiece.Rook2) | (1 << SeePiece.Queen), (1 << SeePiece.Rook1) | (1 << SeePiece.Rook2), 100)] // 8/4r3/4r3/4p3/8/4R3/4R3/4Q3 w - - 0 1 + [InlineData(Piece.Rook, Piece.Pawn, (1 << SeePiece.Rook1) | (1 << SeePiece.Rook2) | (1 << SeePiece.Queen), (1 << SeePiece.Rook1) | (1 << SeePiece.Rook2) | (1 << SeePiece.Queen), -400)] // 4q3/4r3/4r3/4p3/8/4R3/4R3/4Q3 w - - 0 1 + [InlineData(Piece.Knight, Piece.Knight, (1 << SeePiece.Knight1) | (1 << SeePiece.Knight2), (1 << SeePiece.Pawn), 100)] // 8/8/2n5/5n2/3N4/2P5/8/8 w - - 0 1 public void StaticExchangeEvaluation_NormalValues(int attackingPiece, int capturedPiece, int attacker, int defender, int expectedScore) { var score = StaticExchangeEvaluation.Evaluate(attackingPiece, capturedPiece, attacker, defender); diff --git a/Cosette.Tuner.Common/Cosette.Tuner.Common.csproj b/Cosette.Tuner.Common/Cosette.Tuner.Common.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/Cosette.Tuner.Common/Cosette.Tuner.Common.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/Cosette.Tuner.Common/Requests/ChromosomeDataRequest.cs b/Cosette.Tuner.Common/Requests/ChromosomeDataRequest.cs new file mode 100644 index 0000000..5c0e0b9 --- /dev/null +++ b/Cosette.Tuner.Common/Requests/ChromosomeDataRequest.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Cosette.Tuner.Common.Requests +{ + public class ChromosomeDataRequest + { + public int TestId { get; set; } + public double ElapsedTime { get; set; } + public int Fitness { get; set; } + public int ReferenceEngineWins { get; set; } + public int ExperimentalEngineWins { get; set; } + public int Draws { get; set; } + + public EngineStatisticsDataRequest ReferenceEngineStatistics { get; set; } + public EngineStatisticsDataRequest ExperimentalEngineStatistics { get; set; } + public List Genes { get; set; } + } +} diff --git a/Cosette.Tuner.Common/Requests/EngineStatisticsDataRequest.cs b/Cosette.Tuner.Common/Requests/EngineStatisticsDataRequest.cs new file mode 100644 index 0000000..d8aef7d --- /dev/null +++ b/Cosette.Tuner.Common/Requests/EngineStatisticsDataRequest.cs @@ -0,0 +1,10 @@ +namespace Cosette.Tuner.Common.Requests +{ + public class EngineStatisticsDataRequest + { + public double AverageTimePerGame { get; set; } + public double AverageDepth { get; set; } + public double AverageNodesCount { get; set; } + public double AverageNodesPerSecond { get; set; } + } +} diff --git a/Cosette.Tuner.Common/Requests/GeneDataRequest.cs b/Cosette.Tuner.Common/Requests/GeneDataRequest.cs new file mode 100644 index 0000000..445d777 --- /dev/null +++ b/Cosette.Tuner.Common/Requests/GeneDataRequest.cs @@ -0,0 +1,8 @@ +namespace Cosette.Tuner.Common.Requests +{ + public class GeneDataRequest + { + public string Name { get; set; } + public int Value { get; set; } + } +} diff --git a/Cosette.Tuner.Common/Requests/GenerationDataRequest.cs b/Cosette.Tuner.Common/Requests/GenerationDataRequest.cs new file mode 100644 index 0000000..f43e7f3 --- /dev/null +++ b/Cosette.Tuner.Common/Requests/GenerationDataRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Cosette.Tuner.Common.Requests +{ + public class GenerationDataRequest + { + public int TestId { get; set; } + public double ElapsedTime { get; set; } + public int BestFitness { get; set; } + + public List BestChromosomeGenes { get; set; } + } +} diff --git a/Cosette.Tuner.Common/Responses/TestDataResponse.cs b/Cosette.Tuner.Common/Responses/TestDataResponse.cs new file mode 100644 index 0000000..afb5e27 --- /dev/null +++ b/Cosette.Tuner.Common/Responses/TestDataResponse.cs @@ -0,0 +1,7 @@ +namespace Cosette.Tuner.Common.Responses +{ + public class TestDataResponse + { + public int Id { get; set; } + } +} diff --git a/Cosette.Tuner.Web/Controllers/ApiController.cs b/Cosette.Tuner.Web/Controllers/ApiController.cs new file mode 100644 index 0000000..a50d80b --- /dev/null +++ b/Cosette.Tuner.Web/Controllers/ApiController.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Cosette.Tuner.Common.Requests; +using Cosette.Tuner.Common.Responses; +using Cosette.Tuner.Web.Database.Models; +using Cosette.Tuner.Web.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Cosette.Tuner.Web.Controllers +{ + public class ApiController : Controller + { + private IMapper _mapper; + private TestService _testService; + private ChromosomeService _chromosomeService; + private GenerationService _generationService; + + public ApiController(IMapper mapper, TestService testService, ChromosomeService chromosomeService, GenerationService generationService) + { + _mapper = mapper; + _testService = testService; + _chromosomeService = chromosomeService; + _generationService = generationService; + } + + [HttpGet] + [Route("api/ping")] + public async Task Ping() + { + return new OkResult(); + } + + [HttpPost] + [Route("api/test/register")] + public async Task RegisterTest() + { + var response = new TestDataResponse + { + Id = await _testService.GenerateNewTest() + }; + + return new JsonResult(response); + } + + [HttpPost] + [Route("api/generation")] + public async Task Generation([FromBody] GenerationDataRequest requestData) + { + var generationModel = _mapper.Map(requestData); + await _generationService.Add(generationModel); + + return new OkResult(); + } + + [HttpPost] + [Route("api/chromosome")] + public async Task Chromosome([FromBody] ChromosomeDataRequest requestData) + { + var chromosomeModel = _mapper.Map(requestData); + await _chromosomeService.Add(chromosomeModel); + + return new OkResult(); + } + } +} diff --git a/Cosette.Tuner.Web/Controllers/HomeController.cs b/Cosette.Tuner.Web/Controllers/HomeController.cs new file mode 100644 index 0000000..a3af084 --- /dev/null +++ b/Cosette.Tuner.Web/Controllers/HomeController.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Cosette.Tuner.Web.Database; +using Cosette.Tuner.Web.Services; +using Cosette.Tuner.Web.ViewModels; +using Cosette.Tuner.Web.ViewModels.ChartJs; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace Cosette.Tuner.Web.Controllers +{ + public class HomeController : Controller + { + private IMapper _mapper; + private TestService _testService; + private ChromosomeService _chromosomeService; + private GenerationService _generationService; + private ChartJsService _chartJsService; + + public HomeController(IMapper mapper, TestService testService, ChromosomeService chromosomeService, GenerationService generationService, ChartJsService chartJsService) + { + _mapper = mapper; + _testService = testService; + _chromosomeService = chromosomeService; + _generationService = generationService; + _chartJsService = chartJsService; + } + + [HttpGet] + [Route("{id?}")] + public async Task Index(int? id) + { + var test = id.HasValue ? await _testService.GetTestById(id.Value) : await _testService.GetLastTest(); + var allTests = await _testService.GetAll(); + var allGenerations = await _generationService.GetAll(test.Id); + var bestGenerations = await _generationService.GetBest(test.Id, 15); + var allChromosomes = await _chromosomeService.GetAll(test.Id); + var bestChromosomes = await _chromosomeService.GetBest(test.Id, 15); + + var generationFitnessData = _chartJsService.GenerateGenerationFitnessData(allGenerations); + var chromosomeFitnessData = _chartJsService.GenerateChromosomeFitnessData(allChromosomes); + var averageElapsedTimeData = _chartJsService.GenerateAverageElapsedTimeData(allGenerations); + var averageDepthData = _chartJsService.GenerateAverageDepthData(allChromosomes); + var averageNodesData = _chartJsService.GenerateAverageNodesData(allChromosomes); + var averageTimePerGameData = _chartJsService.GenerateAverageTimePerGameData(allChromosomes); + + return View(new MainViewModel + { + LastTest = _mapper.Map(test), + Tests = _mapper.Map>(allTests), + AllGenerations = _mapper.Map>(allGenerations), + BestGenerations = _mapper.Map>(bestGenerations), + AllChromosomes = _mapper.Map>(allChromosomes), + BestChromosomes = _mapper.Map>(bestChromosomes), + + GenerationFitnessChartJson = JsonConvert.SerializeObject(generationFitnessData), + ChromosomeFitnessChartJson = JsonConvert.SerializeObject(chromosomeFitnessData), + AverageElapsedTimeChartJson = JsonConvert.SerializeObject(averageElapsedTimeData), + AverageDepthChartJson = JsonConvert.SerializeObject(averageDepthData), + AverageNodesChartJson = JsonConvert.SerializeObject(averageNodesData), + AverageTimePerGameChartJson = JsonConvert.SerializeObject(averageTimePerGameData) + }); + } + } +} diff --git a/Cosette.Tuner.Web/Cosette.Tuner.Web.csproj b/Cosette.Tuner.Web/Cosette.Tuner.Web.csproj new file mode 100644 index 0000000..cb0fac5 --- /dev/null +++ b/Cosette.Tuner.Web/Cosette.Tuner.Web.csproj @@ -0,0 +1,28 @@ + + + + net5.0 + 85ff807e-9a87-4541-a1d4-10a70003fecc + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Cosette.Tuner.Web/Database/DatabaseContext.cs b/Cosette.Tuner.Web/Database/DatabaseContext.cs new file mode 100644 index 0000000..1e6cfc4 --- /dev/null +++ b/Cosette.Tuner.Web/Database/DatabaseContext.cs @@ -0,0 +1,29 @@ +using System; +using Cosette.Tuner.Web.Database.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Cosette.Tuner.Web.Database +{ + public class DatabaseContext : DbContext + { + public virtual DbSet ChromosomeGenes { get; set; } + public virtual DbSet Chromosomes { get; set; } + public virtual DbSet EngineStatistics { get; set; } + public virtual DbSet GenerationGenes { get; set; } + public virtual DbSet Generations { get; set; } + public virtual DbSet Tests { get; set; } + + public static readonly ILoggerFactory DatabaseLoggerFactory = LoggerFactory.Create(builder => { builder.AddDebug(); }); + + public DatabaseContext(DbContextOptions options) : base(options) + { + + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseLoggerFactory(DatabaseLoggerFactory); + } + } +} diff --git a/Cosette.Tuner.Web/Database/Models/ChromosomeGeneModel.cs b/Cosette.Tuner.Web/Database/Models/ChromosomeGeneModel.cs new file mode 100644 index 0000000..2da0ce1 --- /dev/null +++ b/Cosette.Tuner.Web/Database/Models/ChromosomeGeneModel.cs @@ -0,0 +1,8 @@ +namespace Cosette.Tuner.Web.Database.Models +{ + public class ChromosomeGeneModel : GeneModel + { + public int ChromosomeId { get; set; } + public virtual ChromosomeModel Chromosome { get; set; } + } +} diff --git a/Cosette.Tuner.Web/Database/Models/ChromosomeModel.cs b/Cosette.Tuner.Web/Database/Models/ChromosomeModel.cs new file mode 100644 index 0000000..713c855 --- /dev/null +++ b/Cosette.Tuner.Web/Database/Models/ChromosomeModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Cosette.Tuner.Web.Database.Models +{ + public class ChromosomeModel + { + public int Id { get; set; } + + public int TestId { get; set; } + public virtual TestModel Test { get; set; } + + public DateTime CreationTimeUtc { get; set; } + public double ElapsedTime { get; set; } + public int Fitness { get; set; } + public int ReferenceEngineWins { get; set; } + public int ExperimentalEngineWins { get; set; } + public int Draws { get; set; } + + public virtual List EnginesStatistics { get; set; } + public virtual List Genes { get; set; } + } +} diff --git a/Cosette.Tuner.Web/Database/Models/EngineStatisticsModel.cs b/Cosette.Tuner.Web/Database/Models/EngineStatisticsModel.cs new file mode 100644 index 0000000..23fe67e --- /dev/null +++ b/Cosette.Tuner.Web/Database/Models/EngineStatisticsModel.cs @@ -0,0 +1,19 @@ +using System; + +namespace Cosette.Tuner.Web.Database.Models +{ + public class EngineStatisticsModel + { + public int Id { get; set; } + + public int ChromosomeId { get; set; } + public virtual ChromosomeModel Chromosome { get; set; } + + public DateTime CreationTimeUtc { get; set; } + public bool IsReferenceEngine { get; set; } + public double AverageTimePerGame { get; set; } + public double AverageDepth { get; set; } + public double AverageNodesCount { get; set; } + public double AverageNodesPerSecond { get; set; } + } +} diff --git a/Cosette.Tuner.Web/Database/Models/GeneModel.cs b/Cosette.Tuner.Web/Database/Models/GeneModel.cs new file mode 100644 index 0000000..c5ad65a --- /dev/null +++ b/Cosette.Tuner.Web/Database/Models/GeneModel.cs @@ -0,0 +1,9 @@ +namespace Cosette.Tuner.Web.Database.Models +{ + public class GeneModel + { + public int Id { get; set; } + public string Name { get; set; } + public int Value { get; set; } + } +} diff --git a/Cosette.Tuner.Web/Database/Models/GenerationGeneModel.cs b/Cosette.Tuner.Web/Database/Models/GenerationGeneModel.cs new file mode 100644 index 0000000..b159c2e --- /dev/null +++ b/Cosette.Tuner.Web/Database/Models/GenerationGeneModel.cs @@ -0,0 +1,8 @@ +namespace Cosette.Tuner.Web.Database.Models +{ + public class GenerationGeneModel : GeneModel + { + public int GenerationId { get; set; } + public virtual GenerationModel Generation { get; set; } + } +} diff --git a/Cosette.Tuner.Web/Database/Models/GenerationModel.cs b/Cosette.Tuner.Web/Database/Models/GenerationModel.cs new file mode 100644 index 0000000..12e5569 --- /dev/null +++ b/Cosette.Tuner.Web/Database/Models/GenerationModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace Cosette.Tuner.Web.Database.Models +{ + public class GenerationModel + { + public int Id { get; set; } + + public int TestId { get; set; } + public virtual TestModel Test { get; set; } + + public DateTime CreationTimeUtc { get; set; } + public double ElapsedTime { get; set; } + public int BestFitness { get; set; } + + public virtual List BestGenes { get; set; } + } +} diff --git a/Cosette.Tuner.Web/Database/Models/TestModel.cs b/Cosette.Tuner.Web/Database/Models/TestModel.cs new file mode 100644 index 0000000..1858a06 --- /dev/null +++ b/Cosette.Tuner.Web/Database/Models/TestModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace Cosette.Tuner.Web.Database.Models +{ + public class TestModel + { + public int Id { get; set; } + public DateTime CreationTimeUtc { get; set; } + + public virtual List Generation { get; set; } + } +} diff --git a/Cosette.Tuner.Web/MapperProfile.cs b/Cosette.Tuner.Web/MapperProfile.cs new file mode 100644 index 0000000..098b411 --- /dev/null +++ b/Cosette.Tuner.Web/MapperProfile.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Cosette.Tuner.Common.Requests; +using Cosette.Tuner.Web.Database.Models; +using Cosette.Tuner.Web.ViewModels; + +namespace Cosette.Tuner.Web +{ + public class MapperProfile : Profile + { + public MapperProfile() + { + CreateMap() + .ForMember(p => p.TestId, p => p.MapFrom(q => q.TestId)) + .ForMember(p => p.CreationTimeUtc, p => p.MapFrom(q => DateTime.UtcNow)) + .ForMember(p => p.ElapsedTime, p => p.MapFrom(q => q.ElapsedTime)) + .ForMember(p => p.Fitness, p => p.MapFrom(q => q.Fitness)) + .ForMember(p => p.ReferenceEngineWins, p => p.MapFrom(q => q.ReferenceEngineWins)) + .ForMember(p => p.ExperimentalEngineWins, p => p.MapFrom(q => q.ExperimentalEngineWins)) + .ForMember(p => p.Draws, p => p.MapFrom(q => q.Draws)) + .ForMember(p => p.EnginesStatistics, p => p.MapFrom((src, dest, order, context) => + { + var referenceEngineStatistics = context.Mapper.Map(src.ReferenceEngineStatistics); + var experimentalEngineStatistics = context.Mapper.Map(src.ExperimentalEngineStatistics); + + referenceEngineStatistics.IsReferenceEngine = true; + experimentalEngineStatistics.IsReferenceEngine = false; + + var outputList = new List + { + referenceEngineStatistics, + experimentalEngineStatistics + }; + + return outputList; + })) + .ForMember(p => p.Genes, p => p.MapFrom(q => q.Genes)); + + CreateMap() + .ForMember(p => p.Name, p => p.MapFrom(q => q.Name)) + .ForMember(p => p.Value, p => p.MapFrom(q => q.Value)); + + CreateMap() + .ForMember(p => p.Name, p => p.MapFrom(q => q.Name)) + .ForMember(p => p.Value, p => p.MapFrom(q => q.Value)); + + CreateMap() + .ForMember(p => p.CreationTimeUtc, p => p.MapFrom(q => DateTime.UtcNow)) + .ForMember(p => p.AverageTimePerGame, p => p.MapFrom(q => q.AverageTimePerGame)) + .ForMember(p => p.AverageDepth, p => p.MapFrom(q => q.AverageDepth)) + .ForMember(p => p.AverageNodesCount, p => p.MapFrom(q => q.AverageNodesCount)) + .ForMember(p => p.AverageNodesPerSecond, p => p.MapFrom(q => q.AverageNodesPerSecond)); + + CreateMap() + .ForMember(p => p.TestId, p => p.MapFrom(q => q.TestId)) + .ForMember(p => p.CreationTimeUtc, p => p.MapFrom(q => DateTime.UtcNow)) + .ForMember(p => p.ElapsedTime, p => p.MapFrom(q => q.ElapsedTime)) + .ForMember(p => p.BestFitness, p => p.MapFrom(q => q.BestFitness)) + .ForMember(p => p.BestGenes, p => p.MapFrom(q => q.BestChromosomeGenes)) + .ForMember(p => p.BestGenes, p => p.MapFrom(q => q.BestChromosomeGenes)); + + CreateMap() + .ForMember(p => p.Id, p => p.MapFrom(q => q.Id)) + .ForMember(p => p.CreationTimeUtc, p => p.MapFrom(q => q.CreationTimeUtc)); + + CreateMap() + .ForMember(p => p.Id, p => p.MapFrom(q => q.Id)) + .ForMember(p => p.CreationTimeUtc, p => p.MapFrom(q => q.CreationTimeUtc)) + .ForMember(p => p.ElapsedTime, p => p.MapFrom(q => q.ElapsedTime)) + .ForMember(p => p.BestFitness, p => p.MapFrom(q => q.BestFitness)) + .ForMember(p => p.BestGenes, p => p.MapFrom(q => q.BestGenes)); + + CreateMap() + .ForMember(p => p.Id, p => p.MapFrom(q => q.Id)) + .ForMember(p => p.Name, p => p.MapFrom(q => q.Name)) + .ForMember(p => p.Value, p => p.MapFrom(q => q.Value)); + + CreateMap() + .ForMember(p => p.Id, p => p.MapFrom(q => q.Id)) + .ForMember(p => p.Name, p => p.MapFrom(q => q.Name)) + .ForMember(p => p.Value, p => p.MapFrom(q => q.Value)); + + CreateMap() + .ForMember(p => p.Id, p => p.MapFrom(q => q.Id)) + .ForMember(p => p.CreationTimeUtc, p => p.MapFrom(q => q.CreationTimeUtc)) + .ForMember(p => p.ElapsedTime, p => p.MapFrom(q => q.ElapsedTime)) + .ForMember(p => p.Fitness, p => p.MapFrom(q => q.Fitness)) + .ForMember(p => p.ReferenceEngineWins, p => p.MapFrom(q => q.ReferenceEngineWins)) + .ForMember(p => p.ExperimentalEngineWins, p => p.MapFrom(q => q.ExperimentalEngineWins)) + .ForMember(p => p.Draws, p => p.MapFrom(q => q.Draws)) + .ForMember(p => p.EnginesStatistics, p => p.MapFrom(q => q.EnginesStatistics)) + .ForMember(p => p.Genes, p => p.MapFrom(q => q.Genes)); + + CreateMap() + .ForMember(p => p.Id, p => p.MapFrom(q => q.Id)) + .ForMember(p => p.CreationTimeUtc, p => p.MapFrom(q => q.CreationTimeUtc)) + .ForMember(p => p.IsReferenceEngine, p => p.MapFrom(q => q.IsReferenceEngine)) + .ForMember(p => p.AverageTimePerGame, p => p.MapFrom(q => q.AverageTimePerGame)) + .ForMember(p => p.AverageDepth, p => p.MapFrom(q => q.AverageDepth)) + .ForMember(p => p.AverageNodesCount, p => p.MapFrom(q => q.AverageNodesCount)) + .ForMember(p => p.AverageNodesPerSecond, p => p.MapFrom(q => q.AverageNodesPerSecond)); + } + } +} diff --git a/Cosette.Tuner.Web/Program.cs b/Cosette.Tuner.Web/Program.cs new file mode 100644 index 0000000..d746b1e --- /dev/null +++ b/Cosette.Tuner.Web/Program.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Cosette.Tuner.Web +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseKestrel(); + webBuilder.UseStartup(); + webBuilder.UseUrls("http://0.0.0.0:42000/"); + }); + } + } +} diff --git a/Cosette.Tuner.Web/Properties/launchSettings.json b/Cosette.Tuner.Web/Properties/launchSettings.json new file mode 100644 index 0000000..72cbe1e --- /dev/null +++ b/Cosette.Tuner.Web/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:42000", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Cosette.Tuner.Web": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:42000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Cosette.Tuner.Web/Services/ChartJsService.cs b/Cosette.Tuner.Web/Services/ChartJsService.cs new file mode 100644 index 0000000..ae9ad19 --- /dev/null +++ b/Cosette.Tuner.Web/Services/ChartJsService.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Cosette.Tuner.Web.Database.Models; +using Cosette.Tuner.Web.ViewModels.ChartJs; + +namespace Cosette.Tuner.Web.Services +{ + public class ChartJsService + { + private const string ReferenceColor = "#4dc9f6"; + private const string ExperimentalColor = "#ff0000"; + + public ChartJsData GenerateGenerationFitnessData(List generations) + { + var labels = Enumerable.Range(0, generations.Count).Select(p => p.ToString()).ToList(); + var values = generations.Select(p => p.BestFitness).ToList(); + var datasets = new List> + { + GenerateDataset("Fitness", values, ReferenceColor) + }; + + return GenerateData(labels, datasets); + } + + public ChartJsData GenerateChromosomeFitnessData(List chromosomes) + { + var labels = Enumerable.Range(0, chromosomes.Count).Select(p => p.ToString()).ToList(); + var values = chromosomes.Select(p => p.Fitness).ToList(); + var datasets = new List> + { + GenerateDataset("Fitness", values, ReferenceColor) + }; + + return GenerateData(labels, datasets); + } + + public ChartJsData GenerateAverageElapsedTimeData(List generations) + { + var labels = Enumerable.Range(0, generations.Count).Select(p => p.ToString()).ToList(); + var values = generations.Select(p => p.ElapsedTime).ToList(); + var datasets = new List> + { + GenerateDataset("Time", values, ReferenceColor) + }; + + return GenerateData(labels, datasets); + } + + public ChartJsData GenerateAverageDepthData(List chromosomes) + { + var labels = Enumerable.Range(0, chromosomes.Count).Select(p => p.ToString()).ToList(); + var referenceValues = chromosomes + .SelectMany(p => p.EnginesStatistics) + .Where(p => p.IsReferenceEngine) + .Select(p => p.AverageDepth) + .ToList(); + var experimentalValues = chromosomes + .SelectMany(p => p.EnginesStatistics) + .Where(p => !p.IsReferenceEngine) + .Select(p => p.AverageDepth) + .ToList(); + + var datasets = new List> + { + GenerateDataset("Reference", referenceValues, ReferenceColor), + GenerateDataset("Experimental", experimentalValues, ExperimentalColor), + }; + + return GenerateData(labels, datasets); + } + + public ChartJsData GenerateAverageNodesData(List chromosomes) + { + var labels = Enumerable.Range(0, chromosomes.Count).Select(p => p.ToString()).ToList(); + var referenceValues = chromosomes + .SelectMany(p => p.EnginesStatistics) + .Where(p => p.IsReferenceEngine) + .Select(p => p.AverageNodesCount) + .ToList(); + var experimentalValues = chromosomes + .SelectMany(p => p.EnginesStatistics) + .Where(p => !p.IsReferenceEngine) + .Select(p => p.AverageNodesCount) + .ToList(); + + var datasets = new List> + { + GenerateDataset("Reference", referenceValues, ReferenceColor), + GenerateDataset("Experimental", experimentalValues, ExperimentalColor), + }; + + return GenerateData(labels, datasets); + } + + public ChartJsData GenerateAverageTimePerGameData(List chromosomes) + { + var labels = Enumerable.Range(0, chromosomes.Count).Select(p => p.ToString()).ToList(); + var values = chromosomes + .SelectMany(p => p.EnginesStatistics) + .Where(p => p.IsReferenceEngine) + .Select(p => p.AverageTimePerGame) + .ToList(); + + var datasets = new List> + { + GenerateDataset("Time", values, ReferenceColor) + }; + + return GenerateData(labels, datasets); + } + + private ChartJsData GenerateData(List labels, List> datasets) + { + return new ChartJsData + { + Labels = labels, + Datasets = datasets + }; + } + + private ChartJsDataset GenerateDataset(string label, List values, string color) + { + return new ChartJsDataset + { + Label = label, + Data = values, + BorderColor = color, + BackgroundColor = color, + Fill = "false" + }; + } + } +} diff --git a/Cosette.Tuner.Web/Services/ChromosomeService.cs b/Cosette.Tuner.Web/Services/ChromosomeService.cs new file mode 100644 index 0000000..1637fe6 --- /dev/null +++ b/Cosette.Tuner.Web/Services/ChromosomeService.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Cosette.Tuner.Web.Database; +using Cosette.Tuner.Web.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace Cosette.Tuner.Web.Services +{ + public class ChromosomeService + { + private DatabaseContext _databaseContext; + + public ChromosomeService(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + } + + public async Task Add(ChromosomeModel chromosomeModel) + { + await _databaseContext.Chromosomes.AddAsync(chromosomeModel); + await _databaseContext.SaveChangesAsync(); + } + + public async Task> GetAll(int testId) + { + return await _databaseContext.Chromosomes + .Where(p => p.TestId == testId) + .Include(p => p.EnginesStatistics) + .Include(p => p.Genes) + .ToListAsync(); + } + + public async Task> GetBest(int testId, int count) + { + return await _databaseContext.Chromosomes + .Where(p => p.TestId == testId) + .OrderByDescending(p => p.Fitness) + .Include(p => p.Genes) + .Take(count) + .ToListAsync(); + } + } +} \ No newline at end of file diff --git a/Cosette.Tuner.Web/Services/GenerationService.cs b/Cosette.Tuner.Web/Services/GenerationService.cs new file mode 100644 index 0000000..bc7a59c --- /dev/null +++ b/Cosette.Tuner.Web/Services/GenerationService.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Cosette.Tuner.Web.Database; +using Cosette.Tuner.Web.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace Cosette.Tuner.Web.Services +{ + public class GenerationService + { + private DatabaseContext _databaseContext; + + public GenerationService(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + } + + public async Task Add(GenerationModel generationModel) + { + await _databaseContext.Generations.AddAsync(generationModel); + await _databaseContext.SaveChangesAsync(); + } + + public async Task> GetAll(int testId) + { + return await _databaseContext.Generations + .Where(p => p.TestId == testId) + .Include(p => p.BestGenes) + .ToListAsync(); + } + + public async Task> GetBest(int testId, int count) + { + return await _databaseContext.Generations + .Where(p => p.TestId == testId) + .OrderByDescending(p => p.BestFitness) + .Include(p => p.BestGenes) + .Take(count) + .ToListAsync(); + } + } +} diff --git a/Cosette.Tuner.Web/Services/TestService.cs b/Cosette.Tuner.Web/Services/TestService.cs new file mode 100644 index 0000000..e7b8b8b --- /dev/null +++ b/Cosette.Tuner.Web/Services/TestService.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Cosette.Tuner.Web.Database; +using Cosette.Tuner.Web.Database.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; + +namespace Cosette.Tuner.Web.Services +{ + public class TestService + { + private DatabaseContext _databaseContext; + + public TestService(DatabaseContext databaseContext) + { + _databaseContext = databaseContext; + } + + public async Task GenerateNewTest() + { + var entityTracking = await _databaseContext.Tests.AddAsync(new TestModel + { + CreationTimeUtc = DateTime.Now, + }); + + await _databaseContext.SaveChangesAsync(); + return entityTracking.Entity.Id; + } + + public async Task> GetAll() + { + return await _databaseContext.Tests.ToListAsync(); + } + + public async Task GetLastTest() + { + return await _databaseContext.Tests.OrderByDescending(p => p.Id).FirstOrDefaultAsync(); + } + + public async Task GetTestById(int testId) + { + return await _databaseContext.Tests.FirstOrDefaultAsync(p => p.Id == testId); + } + } +} \ No newline at end of file diff --git a/Cosette.Tuner.Web/Startup.cs b/Cosette.Tuner.Web/Startup.cs new file mode 100644 index 0000000..6682c75 --- /dev/null +++ b/Cosette.Tuner.Web/Startup.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Cosette.Tuner.Web.Database; +using Cosette.Tuner.Web.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Cosette.Tuner.Web +{ + public class Startup + { + private IConfiguration _configuration; + + public Startup(IConfiguration configuration) + { + _configuration = configuration; + } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddAutoMapper(typeof(Startup)); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddDbContext(options => + { + options.UseSqlite(_configuration.GetConnectionString("DefaultConnection")); + }); + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + app.UseStaticFiles(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}"); + }); + } + } +} diff --git a/Cosette.Tuner.Web/ViewModels/ChartJs/ChartJsData.cs b/Cosette.Tuner.Web/ViewModels/ChartJs/ChartJsData.cs new file mode 100644 index 0000000..5991b8a --- /dev/null +++ b/Cosette.Tuner.Web/ViewModels/ChartJs/ChartJsData.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Cosette.Tuner.Web.ViewModels.ChartJs +{ + public class ChartJsData + { + [JsonProperty("labels")] + public List Labels { get; set; } + + [JsonProperty("datasets")] + public List> Datasets { get; set; } + } +} diff --git a/Cosette.Tuner.Web/ViewModels/ChartJs/ChartJsDataset.cs b/Cosette.Tuner.Web/ViewModels/ChartJs/ChartJsDataset.cs new file mode 100644 index 0000000..c93bd2e --- /dev/null +++ b/Cosette.Tuner.Web/ViewModels/ChartJs/ChartJsDataset.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Cosette.Tuner.Web.ViewModels.ChartJs +{ + public class ChartJsDataset + { + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("borderColor")] + public string BorderColor { get; set; } + + [JsonProperty("backgroundColor")] + public string BackgroundColor { get; set; } + + [JsonProperty("fill")] + public string Fill { get; set; } + + [JsonProperty("data")] + public List Data { get; set; } + } +} diff --git a/Cosette.Tuner.Web/ViewModels/ChromosomeViewModel.cs b/Cosette.Tuner.Web/ViewModels/ChromosomeViewModel.cs new file mode 100644 index 0000000..06a8ffa --- /dev/null +++ b/Cosette.Tuner.Web/ViewModels/ChromosomeViewModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Cosette.Tuner.Web.ViewModels +{ + public class ChromosomeViewModel + { + public int Id { get; set; } + + public DateTime CreationTimeUtc { get; set; } + public double ElapsedTime { get; set; } + public int Fitness { get; set; } + public int ReferenceEngineWins { get; set; } + public int ExperimentalEngineWins { get; set; } + public int Draws { get; set; } + + public virtual List EnginesStatistics { get; set; } + public virtual List Genes { get; set; } + } +} diff --git a/Cosette.Tuner.Web/ViewModels/EngineStatisticsViewModel.cs b/Cosette.Tuner.Web/ViewModels/EngineStatisticsViewModel.cs new file mode 100644 index 0000000..0a7081f --- /dev/null +++ b/Cosette.Tuner.Web/ViewModels/EngineStatisticsViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Cosette.Tuner.Web.ViewModels +{ + public class EngineStatisticsViewModel + { + public int Id { get; set; } + + public DateTime CreationTimeUtc { get; set; } + public bool IsReferenceEngine { get; set; } + public double AverageTimePerGame { get; set; } + public double AverageDepth { get; set; } + public double AverageNodesCount { get; set; } + public double AverageNodesPerSecond { get; set; } + } +} diff --git a/Cosette.Tuner.Web/ViewModels/GeneViewModel.cs b/Cosette.Tuner.Web/ViewModels/GeneViewModel.cs new file mode 100644 index 0000000..0f616d9 --- /dev/null +++ b/Cosette.Tuner.Web/ViewModels/GeneViewModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Cosette.Tuner.Web.ViewModels +{ + public class GeneViewModel + { + public int Id { get; set; } + public string Name { get; set; } + public int Value { get; set; } + } +} diff --git a/Cosette.Tuner.Web/ViewModels/GenerationViewModel.cs b/Cosette.Tuner.Web/ViewModels/GenerationViewModel.cs new file mode 100644 index 0000000..351d98e --- /dev/null +++ b/Cosette.Tuner.Web/ViewModels/GenerationViewModel.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Cosette.Tuner.Web.ViewModels +{ + public class GenerationViewModel + { + public int Id { get; set; } + + public DateTime CreationTimeUtc { get; set; } + public double ElapsedTime { get; set; } + public int BestFitness { get; set; } + + public virtual List BestGenes { get; set; } + } +} diff --git a/Cosette.Tuner.Web/ViewModels/MainViewModel.cs b/Cosette.Tuner.Web/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..f5bfc6a --- /dev/null +++ b/Cosette.Tuner.Web/ViewModels/MainViewModel.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace Cosette.Tuner.Web.ViewModels +{ + public class MainViewModel + { + public TestViewModel LastTest { get; set; } + public List Tests { get; set; } + public List AllGenerations { get; set; } + public List BestGenerations { get; set; } + public List AllChromosomes { get; set; } + public List BestChromosomes { get; set; } + + public string GenerationFitnessChartJson { get; set; } + public string ChromosomeFitnessChartJson { get; set; } + public string AverageElapsedTimeChartJson { get; set; } + public string AverageDepthChartJson { get; set; } + public string AverageNodesChartJson { get; set; } + public string AverageTimePerGameChartJson { get; set; } + } +} diff --git a/Cosette.Tuner.Web/ViewModels/TestViewModel.cs b/Cosette.Tuner.Web/ViewModels/TestViewModel.cs new file mode 100644 index 0000000..09a934e --- /dev/null +++ b/Cosette.Tuner.Web/ViewModels/TestViewModel.cs @@ -0,0 +1,10 @@ +using System; + +namespace Cosette.Tuner.Web.ViewModels +{ + public class TestViewModel + { + public int Id { get; set; } + public DateTime CreationTimeUtc { get; set; } + } +} diff --git a/Cosette.Tuner.Web/Views/Home/Index.cshtml b/Cosette.Tuner.Web/Views/Home/Index.cshtml new file mode 100644 index 0000000..85d0637 --- /dev/null +++ b/Cosette.Tuner.Web/Views/Home/Index.cshtml @@ -0,0 +1,220 @@ +@model Cosette.Tuner.Web.ViewModels.MainViewModel +@{ + Layout = "~/Views/Shared/_Layout.cshtml"; +} + +
+ + +
+ + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + @for (var i = Model.AllGenerations.Count - 1; i >= 0; i--) + { + + + + + + + + } +
IDCreation time UTCElapsed timeFitnessGenes
@Model.AllGenerations[i].Id@Model.AllGenerations[i].CreationTimeUtc@Model.AllGenerations[i].ElapsedTime.ToString("F1")@Model.AllGenerations[i].BestFitness + @string.Join(", ", Model.AllGenerations[i].BestGenes.Select(p => $"{p.Name}={p.Value}")) +
+
+
+
+
+ + + + + + + + + + + + @for (var i = 0; i < Model.BestGenerations.Count; i++) + { + + + + + + + + } + +
IDCreation time UTCElapsed timeFitnessGenes
@Model.BestGenerations[i].Id@Model.BestGenerations[i].CreationTimeUtc@Model.BestGenerations[i].ElapsedTime.ToString("F1")@Model.BestGenerations[i].BestFitness + @string.Join(", ", Model.BestGenerations[i].BestGenes.Select(p => $"{p.Name}={p.Value}")) +
+
+
+
+
+ + + + + + + + + + + + + + + @for (var i = 0; i < Model.BestChromosomes.Count; i++) + { + + + + + + + + + + + } + +
IDCreation time UTCElapsed timeFitnessEE winsEE lossesEE drawsGenes
@Model.BestChromosomes[i].Id@Model.BestChromosomes[i].CreationTimeUtc@Model.BestChromosomes[i].ElapsedTime.ToString("F1")@Model.BestChromosomes[i].Fitness@Model.BestChromosomes[i].ExperimentalEngineWins@Model.BestChromosomes[i].ReferenceEngineWins@Model.BestChromosomes[i].Draws + @string.Join(", ", Model.BestChromosomes[i].Genes.Select(p => $"{p.Name}={p.Value}")) +
+
+
+
+
+ + + + + + + + + + + + + + + @for (var i = Model.AllChromosomes.Count - 1; i >= 0; i--) + { + + + + + + + + + + + } + +
IDCreation time UTCElapsed timeFitnessEE winsEE lossesEE drawsGenes
@Model.AllChromosomes[i].Id@Model.AllChromosomes[i].CreationTimeUtc@Model.AllChromosomes[i].ElapsedTime.ToString("F1")@Model.AllChromosomes[i].Fitness@Model.AllChromosomes[i].ExperimentalEngineWins@Model.AllChromosomes[i].ReferenceEngineWins@Model.AllChromosomes[i].Draws + @string.Join(", ", Model.AllChromosomes[i].Genes.Select(p => $"{p.Name}={p.Value}")) +
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/Cosette.Tuner.Web/Views/Shared/_Layout.cshtml b/Cosette.Tuner.Web/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000..bfd3ce8 --- /dev/null +++ b/Cosette.Tuner.Web/Views/Shared/_Layout.cshtml @@ -0,0 +1,18 @@ + + + + + @ViewBag.Title + + + + + + + + + + + @RenderBody() + + diff --git a/Cosette.Tuner.Web/appsettings.Development.json b/Cosette.Tuner.Web/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/Cosette.Tuner.Web/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/Cosette.Tuner.Web/appsettings.json b/Cosette.Tuner.Web/appsettings.json new file mode 100644 index 0000000..801de92 --- /dev/null +++ b/Cosette.Tuner.Web/appsettings.json @@ -0,0 +1,13 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=Database.sqlite" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/Cosette.Tuner.Web/libman.json b/Cosette.Tuner.Web/libman.json new file mode 100644 index 0000000..539f7e9 --- /dev/null +++ b/Cosette.Tuner.Web/libman.json @@ -0,0 +1,26 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "Chart.js@2.9.4", + "destination": "wwwroot/lib/Chart.js/", + "files": [ + "Chart.bundle.min.js", + "Chart.min.css" + ] + }, + { + "library": "twitter-bootstrap@4.5.3", + "destination": "wwwroot/lib/twitter-bootstrap/", + "files": [ + "js/bootstrap.bundle.min.js", + "css/bootstrap.min.css" + ] + }, + { + "library": "jquery@3.5.1", + "destination": "wwwroot/lib/jquery/" + } + ] +} \ No newline at end of file diff --git a/Cosette.Tuner.Web/wwwroot/css/Site.css b/Cosette.Tuner.Web/wwwroot/css/Site.css new file mode 100644 index 0000000..7658007 --- /dev/null +++ b/Cosette.Tuner.Web/wwwroot/css/Site.css @@ -0,0 +1,62 @@ +/*! + * Start Bootstrap - Simple Sidebar (https://startbootstrap.com/template/simple-sidebar) + * Copyright 2013-2020 Start Bootstrap + * Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-simple-sidebar/blob/master/LICENSE) + */ + +#wrapper { + overflow-x: hidden; +} + +#sidebar-wrapper { + min-height: 100vh; + margin-left: -15rem; + -webkit-transition: margin .25s ease-out; + -moz-transition: margin .25s ease-out; + -o-transition: margin .25s ease-out; + transition: margin .25s ease-out; +} + +#sidebar-wrapper .sidebar-heading { + padding: 0.875rem 1.25rem; + font-size: 1.2rem; +} + +#sidebar-wrapper .list-group { + width: 15rem; +} + +#page-content-wrapper { + min-width: 100vw; +} + +#wrapper.toggled #sidebar-wrapper { + margin-left: 0; +} + +@media (min-width: 768px) { + #sidebar-wrapper { + margin-left: 0; + } + + #page-content-wrapper { + min-width: 0; + width: 100%; + } + + #wrapper.toggled #sidebar-wrapper { + margin-left: -15rem; + } +} + +.row { + margin-top: 20px; +} + +.chart-container { + min-height: 250px; +} + +th { + white-space: nowrap; +} \ No newline at end of file diff --git a/Cosette.Tuner.Web/wwwroot/img/icon.png b/Cosette.Tuner.Web/wwwroot/img/icon.png new file mode 100644 index 0000000..9a07d89 Binary files /dev/null and b/Cosette.Tuner.Web/wwwroot/img/icon.png differ diff --git a/Cosette.Tuner.Web/wwwroot/js/Site.js b/Cosette.Tuner.Web/wwwroot/js/Site.js new file mode 100644 index 0000000..ce6c5f3 --- /dev/null +++ b/Cosette.Tuner.Web/wwwroot/js/Site.js @@ -0,0 +1,59 @@ +$(document).ready(function () { + $("#menu-toggle").click(function (e) { + e.preventDefault(); + $("#wrapper").toggleClass("toggled"); + }); + + createChart('generation-fitness-canvas', 'Generation fitness', window.generationFitnessChartData); + createChart('chromosome-fitness-canvas', 'Chromosome fitness', window.chromosomeFitnessChartData); + createChart('average-elapsed-time-canvas', 'Average elapsed time', window.averageElapsedTimeChartData); + createChart('average-depth-canvas', 'Average depth', window.averageDepthChartData); + createChart('average-nodes-count-canvas', 'Average nodes count', window.averageNodesChartData); + createChart('average-time-per-game-canvas', 'Average time per game', window.averageTimePerGameChartData); + + function createChart(canvasId, title, data) { + var config = { + type: 'line', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + title: { + display: true, + text: title, + padding: 0 + }, + tooltips: { + mode: 'index', + intersect: false, + callbacks: { + label: function (tooltipItem, data) { + var label = data.datasets[tooltipItem.datasetIndex].label || ''; + if (label) { + label += ': '; + } + + label += Math.round(tooltipItem.yLabel * 100) / 100; + return label; + } + } + }, + hover: { + mode: 'nearest', + intersect: true + }, + scales: { + xAxes: [{ + display: true + }], + yAxes: [{ + display: true + }] + } + } + }; + + var ctx = document.getElementById(canvasId).getContext('2d'); + window.myLine = new Chart(ctx, config); + } +}); \ No newline at end of file diff --git a/Cosette.Tuner/Cosette.Tuner.csproj b/Cosette.Tuner/Cosette.Tuner.csproj new file mode 100644 index 0000000..5f66fb3 --- /dev/null +++ b/Cosette.Tuner/Cosette.Tuner.csproj @@ -0,0 +1,27 @@ + + + + Exe + net5.0 + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/Cosette.Tuner/Engine/BestMoveData.cs b/Cosette.Tuner/Engine/BestMoveData.cs new file mode 100644 index 0000000..faf8236 --- /dev/null +++ b/Cosette.Tuner/Engine/BestMoveData.cs @@ -0,0 +1,8 @@ +namespace Cosette.Tuner.Engine +{ + public class BestMoveData + { + public string BestMove { get; set; } + public InfoData LastInfoData { get; set; } + } +} diff --git a/Cosette.Tuner/Engine/EngineOperator.cs b/Cosette.Tuner/Engine/EngineOperator.cs new file mode 100644 index 0000000..f9cd0ec --- /dev/null +++ b/Cosette.Tuner/Engine/EngineOperator.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Cosette.Tuner.Settings; + +namespace Cosette.Tuner.Engine +{ + public class EngineOperator + { + private string _enginePath; + private string _engineArguments; + private Process _engineProcess; + private Dictionary _options; + + public EngineOperator(string path, string arguments) + { + _enginePath = path; + _engineArguments = arguments; + _options = new Dictionary(); + } + + public void Init() + { + _engineProcess = Process.Start(new ProcessStartInfo + { + FileName = _enginePath, + Arguments = _engineArguments, + CreateNoWindow = true, + RedirectStandardInput = true, + RedirectStandardOutput = true + }); + + Write("uci"); + WaitForMessage("uciok"); + + Write("isready"); + WaitForMessage("readyok"); + } + + public void Restart() + { + if (!_engineProcess.HasExited) + { + _engineProcess.Close(); + } + + Init(); + ApplyOptions(); + } + + public void InitNewGame() + { + Write("ucinewgame"); + Write("isready"); + WaitForMessage("readyok"); + } + + public void SetOption(string name, string value) + { + _options[name] = value; + } + + public void ApplyOptions() + { + SettingsLoader.Data.Options.ForEach(Write); + foreach (var option in _options) + { + Write($"setoption name {option.Key} value {option.Value}"); + } + + Write("isready"); + + if (!WaitForMessage("readyok")) + { + throw new Exception("Invalid option passed to the engine"); + } + } + + public BestMoveData Go(List moves, int whiteClock, int blackClock) + { + var bestMoveData = new BestMoveData(); + var movesJoined = string.Join(' ', moves); + + if (moves.Count > 0) + { + Write($"position startpos moves {movesJoined}"); + } + + Write($"go wtime {whiteClock} btime {blackClock} winc {SettingsLoader.Data.IncTime} binc {SettingsLoader.Data.IncTime}"); + + while (true) + { + var response = Read(); + if (response.StartsWith("info depth")) + { + bestMoveData.LastInfoData = InfoData.FromString(response); + } + else if (response.StartsWith("bestmove")) + { + bestMoveData.BestMove = response.Split(' ')[1]; + break; + } + else if (response.StartsWith("error")) + { + return null; + } + } + + return bestMoveData; + } + + public void Write(string message) + { + _engineProcess.StandardInput.WriteLine(message); + } + + public string Read() + { + return _engineProcess.StandardOutput.ReadLine(); + } + + public bool WaitForMessage(string message) + { + while (true) + { + var response = Read(); + if (response.StartsWith("error")) + { + return false; + } + + if (response == message) + { + return true; + } + } + } + } +} diff --git a/Cosette.Tuner/Engine/InfoData.cs b/Cosette.Tuner/Engine/InfoData.cs new file mode 100644 index 0000000..82fb449 --- /dev/null +++ b/Cosette.Tuner/Engine/InfoData.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace Cosette.Tuner.Engine +{ + public class InfoData + { + public int Depth { get; set; } + public int SelDepth { get; set; } + public int Time { get; set; } + public int ScoreCp { get; set; } + public int ScoreMate { get; set; } + public ulong Nodes { get; set; } + public ulong Nps { get; set; } + + public static InfoData FromString(string data) + { + var splitData = data.Split(' '); + return new InfoData + { + Depth = GetValue("depth", splitData), + SelDepth = GetValue("seldepth", splitData), + Time = GetValue("time", splitData), + ScoreCp = GetValue("cp", splitData), + ScoreMate = GetValue("mate", splitData), + Nodes = GetValue("nodes", splitData), + Nps = GetValue("nps", splitData), + }; + } + + private static T GetValue(string name, string[] splitData) + { + for (var i = 0; i < splitData.Length; i++) + { + if (splitData[i] == name) + { + return (T)Convert.ChangeType(splitData[i + 1], typeof(T)); + } + } + + return default; + } + } +} diff --git a/Cosette.Tuner/Genetics/EvaluationChromosome.cs b/Cosette.Tuner/Genetics/EvaluationChromosome.cs new file mode 100644 index 0000000..5189d19 --- /dev/null +++ b/Cosette.Tuner/Genetics/EvaluationChromosome.cs @@ -0,0 +1,27 @@ +using Cosette.Tuner.Settings; +using GeneticSharp.Domain.Chromosomes; +using GeneticSharp.Domain.Randomizations; + +namespace Cosette.Tuner.Genetics +{ + public class EvaluationChromosome : ChromosomeBase + { + public EvaluationChromosome() : base(SettingsLoader.Data.Genes.Count) + { + CreateGenes(); + } + + public override Gene GenerateGene(int geneIndex) + { + var gene = SettingsLoader.Data.Genes[geneIndex]; + var value = RandomizationProvider.Current.GetInt(gene.MinValue, gene.MaxValue); + + return new Gene(value); + } + + public override IChromosome CreateNew() + { + return new EvaluationChromosome(); + } + } +} diff --git a/Cosette.Tuner/Genetics/EvaluationFitness.cs b/Cosette.Tuner/Genetics/EvaluationFitness.cs new file mode 100644 index 0000000..d2d523f --- /dev/null +++ b/Cosette.Tuner/Genetics/EvaluationFitness.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Cosette.Polyglot; +using Cosette.Tuner.Common.Requests; +using Cosette.Tuner.Engine; +using Cosette.Tuner.Genetics.Game; +using Cosette.Tuner.Settings; +using Cosette.Tuner.Web; +using GeneticSharp.Domain.Chromosomes; +using GeneticSharp.Domain.Fitnesses; + +namespace Cosette.Tuner.Genetics +{ + public class EvaluationFitness : IFitness + { + private int _testId; + private WebService _webService; + private EngineOperator _referenceEngineOperator; + private EngineOperator _experimentalEngineOperator; + private PolyglotBook _polyglotBook; + + public EvaluationFitness(int testId, WebService webService) + { + _testId = testId; + _webService = webService; + _referenceEngineOperator = new EngineOperator(SettingsLoader.Data.EnginePath, SettingsLoader.Data.EngineArguments); + _experimentalEngineOperator = new EngineOperator(SettingsLoader.Data.EnginePath, SettingsLoader.Data.EngineArguments); + _polyglotBook = new PolyglotBook(SettingsLoader.Data.PolyglotOpeningBook); + + _referenceEngineOperator.Init(); + _experimentalEngineOperator.Init(); + } + + public double Evaluate(IChromosome chromosome) + { + var referenceParticipant = new EvaluationParticipant(_referenceEngineOperator); + var experimentalParticipant = new EvaluationParticipant(_experimentalEngineOperator); + + for (var geneIndex = 0; geneIndex < SettingsLoader.Data.Genes.Count; geneIndex++) + { + var geneName = SettingsLoader.Data.Genes[geneIndex].Name; + var geneValue = chromosome.GetGene(geneIndex).ToString(); + + experimentalParticipant.EngineOperator.SetOption(geneName, geneValue); + } + + referenceParticipant.EngineOperator.ApplyOptions(); + experimentalParticipant.EngineOperator.ApplyOptions(); + + var stopwatch = Stopwatch.StartNew(); + var (whitePlayer, blackPlayer) = (referenceParticipant, experimentalParticipant); + + for (var gameIndex = 0; gameIndex < SettingsLoader.Data.GamesPerFitnessTest;) + { + var openingBookMoves = _polyglotBook.GetRandomOpening(SettingsLoader.Data.PolyglotMaxMoves); + + for (var i = 0; i < 2; i++) + { + try + { + var gameData = new GameData(openingBookMoves); + var (playerToMove, opponent) = (whitePlayer, blackPlayer); + + playerToMove.EngineOperator.InitNewGame(); + opponent.EngineOperator.InitNewGame(); + + while (true) + { + var bestMoveData = playerToMove.EngineOperator.Go(gameData.HalfMovesDone, gameData.WhiteClock, gameData.BlackClock); + if (bestMoveData == null || bestMoveData.LastInfoData == null) + { + playerToMove.History.Add(new ArchivedGame(gameData, GameResult.Draw)); + opponent.History.Add(new ArchivedGame(gameData, GameResult.Draw)); + break; + } + + playerToMove.Logs.Add(bestMoveData.LastInfoData); + gameData.MakeMove(bestMoveData); + + if (gameData.GameIsDone) + { + if (gameData.WhiteClock <= 0 || gameData.BlackClock <= 0) + { + playerToMove.History.Add(new ArchivedGame(gameData, GameResult.Loss)); + opponent.History.Add(new ArchivedGame(gameData, GameResult.Win)); + } + else if (gameData.Winner == Color.None) + { + playerToMove.History.Add(new ArchivedGame(gameData, GameResult.Draw)); + opponent.History.Add(new ArchivedGame(gameData, GameResult.Draw)); + } + else + { + playerToMove.History.Add(new ArchivedGame(gameData, GameResult.Win)); + opponent.History.Add(new ArchivedGame(gameData, GameResult.Loss)); + } + + break; + } + + (playerToMove, opponent) = (opponent, playerToMove); + } + + (whitePlayer, blackPlayer) = (blackPlayer, whitePlayer); + gameIndex++; + } + catch + { + referenceParticipant.EngineOperator.Restart(); + experimentalParticipant.EngineOperator.Restart(); + gameIndex--; + } + } + } + + var elapsedTime = (double)stopwatch.ElapsedMilliseconds / 1000; + var fitness = experimentalParticipant.Wins - referenceParticipant.Wins + referenceParticipant.Draws / 2; + + var chromosomeRequest = RequestsFactory.CreateChromosomeRequest(_testId, fitness, elapsedTime, chromosome, referenceParticipant, experimentalParticipant); + _webService.SendChromosomeData(chromosomeRequest).GetAwaiter().GetResult(); + + Console.WriteLine($"[{DateTime.Now}] Run done! Fitness: {fitness}"); + return fitness; + } + } +} diff --git a/Cosette.Tuner/Genetics/Game/ArchivedGame.cs b/Cosette.Tuner/Genetics/Game/ArchivedGame.cs new file mode 100644 index 0000000..c0bdd17 --- /dev/null +++ b/Cosette.Tuner/Genetics/Game/ArchivedGame.cs @@ -0,0 +1,14 @@ +namespace Cosette.Tuner.Genetics.Game +{ + public class ArchivedGame + { + public GameData GameData { get; } + public GameResult Result { get; } + + public ArchivedGame(GameData gameData, GameResult result) + { + GameData = gameData; + Result = result; + } + } +} diff --git a/Cosette.Tuner/Genetics/Game/Color.cs b/Cosette.Tuner/Genetics/Game/Color.cs new file mode 100644 index 0000000..843b51f --- /dev/null +++ b/Cosette.Tuner/Genetics/Game/Color.cs @@ -0,0 +1,9 @@ +namespace Cosette.Tuner.Genetics.Game +{ + public enum Color + { + None = 0, + White = 1, + Black = -1 + } +} diff --git a/Cosette.Tuner/Genetics/Game/EvaluationParticipant.cs b/Cosette.Tuner/Genetics/Game/EvaluationParticipant.cs new file mode 100644 index 0000000..4ddfb51 --- /dev/null +++ b/Cosette.Tuner/Genetics/Game/EvaluationParticipant.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using Cosette.Tuner.Engine; + +namespace Cosette.Tuner.Genetics.Game +{ + public class EvaluationParticipant + { + public EngineOperator EngineOperator { get; } + public List History { get; } + public List Logs { get; } + + public int Wins => History.Count(p => p.Result == GameResult.Win); + public int Losses => History.Count(p => p.Result == GameResult.Loss); + public int Draws => History.Count(p => p.Result == GameResult.Draw); + public double AverageDepth => Logs.Count == 0 ? 0 : Logs.Average(p => p.Depth); + public int AverageNodesCount => Logs.Count == 0 ? 0 : (int)Logs.Average(p => (int)p.Nodes); + public int AverageNps => Logs.Count == 0 ? 0 : (int)Logs.Average(p => (int)p.Nps); + + public EvaluationParticipant(EngineOperator engineOperator) + { + EngineOperator = engineOperator; + History = new List(); + Logs = new List(); + } + } +} diff --git a/Cosette.Tuner/Genetics/Game/GameData.cs b/Cosette.Tuner/Genetics/Game/GameData.cs new file mode 100644 index 0000000..161e6f9 --- /dev/null +++ b/Cosette.Tuner/Genetics/Game/GameData.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Drawing; +using Cosette.Tuner.Engine; +using Cosette.Tuner.Settings; + +namespace Cosette.Tuner.Genetics.Game +{ + public class GameData + { + public List HalfMovesDone { get; set; } + public bool GameIsDone { get; private set; } + public Color Winner { get; private set; } + + public int WhiteClock { get; private set; } + public int BlackClock { get; private set; } + + private BestMoveData _lastBestMove; + private Color _colorToMove; + + public GameData(List opening) + { + HalfMovesDone = new List(opening); + WhiteClock = SettingsLoader.Data.BaseTime; + BlackClock = SettingsLoader.Data.BaseTime; + + _colorToMove = Color.White; + } + + public void MakeMove(BestMoveData bestMoveData) + { + HalfMovesDone.Add(bestMoveData.BestMove); + _lastBestMove = bestMoveData; + + var clock = _colorToMove == Color.White ? WhiteClock : BlackClock; + clock -= bestMoveData.LastInfoData.Time; + + if (clock <= 0) + { + GameIsDone = true; + WhiteClock = _colorToMove == Color.White ? clock : WhiteClock; + BlackClock = _colorToMove == Color.Black ? clock : BlackClock; + + Winner = _colorToMove == Color.White ? Color.Black : Color.White; + return; + } + + clock += SettingsLoader.Data.IncTime; + + WhiteClock = _colorToMove == Color.White ? clock : WhiteClock; + BlackClock = _colorToMove == Color.Black ? clock : BlackClock; + + if (IsCheckmate() || bestMoveData.LastInfoData.ScoreCp >= 2000) + { + GameIsDone = true; + Winner = _colorToMove; + return; + } + + if (IsDraw()) + { + GameIsDone = true; + Winner = Color.None; + return; + } + + _colorToMove = _colorToMove == Color.White ? Color.Black : Color.White; + } + + public bool IsCheckmate() + { + return _lastBestMove != null && _lastBestMove.LastInfoData.ScoreMate == 1; + } + + public bool IsDraw() + { + if (HalfMovesDone.Count > SettingsLoader.Data.MaxMovesCount * 2) + { + return true; + } + + if (HalfMovesDone.Count > 8) + { + return HalfMovesDone[^1] == HalfMovesDone[^5] && HalfMovesDone[^5] == HalfMovesDone[^9]; + } + + return false; + } + } +} diff --git a/Cosette.Tuner/Genetics/Game/GameResult.cs b/Cosette.Tuner/Genetics/Game/GameResult.cs new file mode 100644 index 0000000..0c2337c --- /dev/null +++ b/Cosette.Tuner/Genetics/Game/GameResult.cs @@ -0,0 +1,9 @@ +namespace Cosette.Tuner.Genetics.Game +{ + public enum GameResult + { + Draw, + Win, + Loss + } +} diff --git a/Cosette.Tuner/Program.cs b/Cosette.Tuner/Program.cs new file mode 100644 index 0000000..5d96af7 --- /dev/null +++ b/Cosette.Tuner/Program.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Cosette.Tuner.Common.Requests; +using Cosette.Tuner.Genetics; +using Cosette.Tuner.Settings; +using Cosette.Tuner.Web; +using GeneticSharp.Domain; +using GeneticSharp.Domain.Crossovers; +using GeneticSharp.Domain.Mutations; +using GeneticSharp.Domain.Populations; +using GeneticSharp.Domain.Selections; +using GeneticSharp.Domain.Terminations; + +namespace Cosette.Tuner +{ + public class Program + { + private static int _testId; + private static WebService _webService; + private static Stopwatch _generationStopwatch; + + public static async Task Main(string[] args) + { + Console.WriteLine($"[{DateTime.Now}] Tuner start"); + SettingsLoader.Init("settings.json"); + + _webService = new WebService(); + _generationStopwatch = new Stopwatch(); + + await _webService.EnableIfAvailable(); + _testId = await _webService.RegisterTest(); + + var selection = new EliteSelection(); + var crossover = new UniformCrossover(0.5f); + var mutation = new UniformMutation(true); + var fitness = new EvaluationFitness(_testId, _webService); + var chromosome = new EvaluationChromosome(); + var population = new Population(SettingsLoader.Data.MinPopulation, SettingsLoader.Data.MaxPopulation, chromosome); + + var geneticAlgorithm = new GeneticAlgorithm(population, fitness, selection, crossover, mutation); + geneticAlgorithm.Termination = new GenerationNumberTermination(SettingsLoader.Data.GenerationsCount); + geneticAlgorithm.GenerationRan += GeneticAlgorithm_GenerationRan; + + _generationStopwatch.Start(); + geneticAlgorithm.Start(); + + Console.WriteLine("Best solution found has {0} fitness.", geneticAlgorithm.BestChromosome.Fitness); + Console.ReadLine(); + } + + private static void GeneticAlgorithm_GenerationRan(object sender, EventArgs e) + { + var geneticAlgorithm = (GeneticAlgorithm)sender; + var genesList = new List(); + + for (var geneIndex = 0; geneIndex < SettingsLoader.Data.Genes.Count; geneIndex++) + { + var name = SettingsLoader.Data.Genes[geneIndex].Name; + var value = geneticAlgorithm.BestChromosome.GetGene(geneIndex).ToString(); + + genesList.Add($"{name}={value}"); + } + + var generationDataRequest = RequestsFactory.CreateGenerationRequest(_testId, _generationStopwatch.Elapsed.TotalSeconds, geneticAlgorithm.BestChromosome); + _webService.SendGenerationData(generationDataRequest).GetAwaiter().GetResult(); + + Console.WriteLine("======================================"); + Console.WriteLine($"[{DateTime.Now}] Generation done!"); + Console.WriteLine($" - best chromosome: {string.Join(", ", genesList)}"); + Console.WriteLine($" - best fitness: {geneticAlgorithm.BestChromosome.Fitness}"); + Console.WriteLine("======================================"); + + _generationStopwatch.Restart(); + } + } +} diff --git a/Cosette.Tuner/Settings/GeneInfo.cs b/Cosette.Tuner/Settings/GeneInfo.cs new file mode 100644 index 0000000..57b4713 --- /dev/null +++ b/Cosette.Tuner/Settings/GeneInfo.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Cosette.Tuner.Settings +{ + public class GeneInfo + { + public string Name { get; set; } + + [JsonProperty("min_value")] + public int MinValue { get; set; } + + [JsonProperty("max_value")] + public int MaxValue { get; set; } + } +} diff --git a/Cosette.Tuner/Settings/SettingsLoader.cs b/Cosette.Tuner/Settings/SettingsLoader.cs new file mode 100644 index 0000000..a987562 --- /dev/null +++ b/Cosette.Tuner/Settings/SettingsLoader.cs @@ -0,0 +1,19 @@ +using System.IO; +using Newtonsoft.Json; + +namespace Cosette.Tuner.Settings +{ + public static class SettingsLoader + { + public static SettingsModel Data { get; set; } + + public static void Init(string settingsPath) + { + using (var streamReader = new StreamReader(settingsPath)) + { + var content = streamReader.ReadToEnd(); + Data = JsonConvert.DeserializeObject(content); + } + } + } +} diff --git a/Cosette.Tuner/Settings/SettingsModel.cs b/Cosette.Tuner/Settings/SettingsModel.cs new file mode 100644 index 0000000..c16022f --- /dev/null +++ b/Cosette.Tuner/Settings/SettingsModel.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Cosette.Tuner.Settings +{ + public class SettingsModel + { + public List Options { get; set; } + public List Genes { get; set; } + + [JsonProperty("engine_path")] + public string EnginePath { get; set; } + + [JsonProperty("engine_arguments")] + public string EngineArguments { get; set; } + + [JsonProperty("base_time")] + public int BaseTime { get; set; } + + [JsonProperty("inc_time")] + public int IncTime { get; set; } + + [JsonProperty("polyglot_opening_book")] + public string PolyglotOpeningBook { get; set; } + + [JsonProperty("polyglot_max_moves")] + public int PolyglotMaxMoves { get; set; } + + [JsonProperty("max_moves_count")] + public int MaxMovesCount { get; set; } + + [JsonProperty("min_population")] + public int MinPopulation { get; set; } + + [JsonProperty("max_population")] + public int MaxPopulation { get; set; } + + [JsonProperty("generations_count")] + public int GenerationsCount { get; set; } + + [JsonProperty("games_per_fitness_test")] + public int GamesPerFitnessTest { get; set; } + } +} diff --git a/Cosette.Tuner/Web/RequestsFactory.cs b/Cosette.Tuner/Web/RequestsFactory.cs new file mode 100644 index 0000000..4b01972 --- /dev/null +++ b/Cosette.Tuner/Web/RequestsFactory.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using Cosette.Tuner.Common.Requests; +using Cosette.Tuner.Genetics.Game; +using Cosette.Tuner.Settings; +using GeneticSharp.Domain.Chromosomes; + +namespace Cosette.Tuner.Web +{ + public static class RequestsFactory + { + public static ChromosomeDataRequest CreateChromosomeRequest(int testId, int fitness, double elapsedTime, IChromosome chromosome, EvaluationParticipant referenceParticipant, EvaluationParticipant experimentalParticipant) + { + return new ChromosomeDataRequest + { + TestId = testId, + ElapsedTime = elapsedTime, + Fitness = fitness, + ReferenceEngineWins = referenceParticipant.Wins, + ExperimentalEngineWins = experimentalParticipant.Wins, + Draws = referenceParticipant.Draws, + + ReferenceEngineStatistics = new EngineStatisticsDataRequest + { + AverageTimePerGame = elapsedTime / SettingsLoader.Data.GamesPerFitnessTest, + AverageDepth = referenceParticipant.AverageDepth, + AverageNodesCount = referenceParticipant.AverageNodesCount, + AverageNodesPerSecond = referenceParticipant.AverageNps + }, + + ExperimentalEngineStatistics = new EngineStatisticsDataRequest + { + AverageTimePerGame = elapsedTime / SettingsLoader.Data.GamesPerFitnessTest, + AverageDepth = experimentalParticipant.AverageDepth, + AverageNodesCount = experimentalParticipant.AverageNodesCount, + AverageNodesPerSecond = experimentalParticipant.AverageNps + }, + + Genes = CreateGenesRequest(chromosome) + }; + } + + public static GenerationDataRequest CreateGenerationRequest(int testId, double elapsedTime, IChromosome chromosome) + { + return new GenerationDataRequest + { + TestId = testId, + BestFitness = (int)chromosome.Fitness, + ElapsedTime = elapsedTime, + BestChromosomeGenes = CreateGenesRequest(chromosome) + }; + } + + public static List CreateGenesRequest(IChromosome chromosome) + { + var genes = new List(); + for (var geneIndex = 0; geneIndex < SettingsLoader.Data.Genes.Count; geneIndex++) + { + genes.Add(new GeneDataRequest + { + Name = SettingsLoader.Data.Genes[geneIndex].Name, + Value = (int)chromosome.GetGene(geneIndex).Value + }); + } + + return genes; + } + } +} diff --git a/Cosette.Tuner/Web/WebService.cs b/Cosette.Tuner/Web/WebService.cs new file mode 100644 index 0000000..0a8e8b8 --- /dev/null +++ b/Cosette.Tuner/Web/WebService.cs @@ -0,0 +1,120 @@ +using System; +using System.Net.Http; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Cosette.Tuner.Common.Requests; +using Cosette.Tuner.Common.Responses; +using Newtonsoft.Json; + +namespace Cosette.Tuner.Web +{ + public class WebService + { + private HttpClient _httpClient; + private bool _enabled; + + public WebService() + { + _httpClient = new HttpClient(); + _httpClient.BaseAddress = new Uri("http://localhost:42000/api/"); + _httpClient.Timeout = new TimeSpan(0, 0, 0, 5); + } + + public async Task EnableIfAvailable() + { + try + { + var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "ping")); + if (response.IsSuccessStatusCode) + { + Console.WriteLine($"[{DateTime.Now}] Web client enabled!"); + _enabled = true; + } + else + { + throw new HttpRequestException(); + } + } + catch + { + Console.WriteLine($"[{DateTime.Now}] Web client disabled"); + } + } + + public async Task RegisterTest() + { + if (!_enabled) + { + return -1; + } + + try + { + var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, "test/register")); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(responseContent).Id; + } + else + { + throw new HttpRequestException(); + } + } + catch + { + Console.WriteLine($"[{DateTime.Now}] Can't register new test"); + } + + return -1; + } + + public async Task SendGenerationData(GenerationDataRequest requestData) + { + if (!_enabled) + { + return; + } + + try + { + var httpContent = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync("generation", httpContent); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException(); + } + } + catch + { + Console.WriteLine($"[{DateTime.Now}] Request containing generation data failed!"); + } + } + + public async Task SendChromosomeData(ChromosomeDataRequest requestData) + { + if (!_enabled) + { + return; + } + + try + { + var httpContent = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync("chromosome", httpContent); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException(); + } + } + catch + { + Console.WriteLine($"[{DateTime.Now}] Request containing chromosome data failed!"); + } + } + } +} diff --git a/Cosette.sln b/Cosette.sln index 8c97576..b2e59a7 100644 --- a/Cosette.sln +++ b/Cosette.sln @@ -15,9 +15,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cosette.Arbiter", "Cosette.Arbiter\Cosette.Arbiter.csproj", "{DAB140A1-17EB-4688-8107-8C2AEA737AD6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cosette.Arbiter", "Cosette.Arbiter\Cosette.Arbiter.csproj", "{DAB140A1-17EB-4688-8107-8C2AEA737AD6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cosette.Arbiter.Tests", "Cosette.Arbiter.Tests\Cosette.Arbiter.Tests.csproj", "{446DB14C-F30C-4A0C-86AB-8CCF3CF7CB53}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cosette.Polyglot.Tests", "Cosette.Polyglot.Tests\Cosette.Polyglot.Tests.csproj", "{446DB14C-F30C-4A0C-86AB-8CCF3CF7CB53}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cosette.Polyglot", "Cosette.Polyglot\Cosette.Polyglot.csproj", "{AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cosette.Tuner", "Cosette.Tuner\Cosette.Tuner.csproj", "{0EEBE02C-E239-4727-8B8B-6ADD603B73B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cosette.Tuner.Web", "Cosette.Tuner.Web\Cosette.Tuner.Web.csproj", "{5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cosette.Tuner.Common", "Cosette.Tuner.Common\Cosette.Tuner.Common.csproj", "{97466830-B71A-4DC4-AD43-D92CC6F867DE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -91,6 +99,70 @@ Global {446DB14C-F30C-4A0C-86AB-8CCF3CF7CB53}.Release|Any CPU.Build.0 = Release|Any CPU {446DB14C-F30C-4A0C-86AB-8CCF3CF7CB53}.Release|x64.ActiveCfg = Release|Any CPU {446DB14C-F30C-4A0C-86AB-8CCF3CF7CB53}.Release|x64.Build.0 = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.BMI|Any CPU.ActiveCfg = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.BMI|Any CPU.Build.0 = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.BMI|x64.ActiveCfg = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.BMI|x64.Build.0 = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.CCRL|Any CPU.ActiveCfg = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.CCRL|Any CPU.Build.0 = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.CCRL|x64.ActiveCfg = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.CCRL|x64.Build.0 = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.Debug|x64.Build.0 = Debug|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.Release|Any CPU.Build.0 = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.Release|x64.ActiveCfg = Release|Any CPU + {AB6B0059-A70B-4A8F-9F55-22DB3A92E83F}.Release|x64.Build.0 = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.BMI|Any CPU.ActiveCfg = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.BMI|Any CPU.Build.0 = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.BMI|x64.ActiveCfg = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.BMI|x64.Build.0 = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.CCRL|Any CPU.ActiveCfg = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.CCRL|Any CPU.Build.0 = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.CCRL|x64.ActiveCfg = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.CCRL|x64.Build.0 = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.Debug|x64.ActiveCfg = Debug|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.Debug|x64.Build.0 = Debug|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.Release|Any CPU.Build.0 = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.Release|x64.ActiveCfg = Release|Any CPU + {0EEBE02C-E239-4727-8B8B-6ADD603B73B7}.Release|x64.Build.0 = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.BMI|Any CPU.ActiveCfg = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.BMI|Any CPU.Build.0 = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.BMI|x64.ActiveCfg = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.BMI|x64.Build.0 = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.CCRL|Any CPU.ActiveCfg = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.CCRL|Any CPU.Build.0 = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.CCRL|x64.ActiveCfg = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.CCRL|x64.Build.0 = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.Debug|x64.ActiveCfg = Debug|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.Debug|x64.Build.0 = Debug|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.Release|Any CPU.Build.0 = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.Release|x64.ActiveCfg = Release|Any CPU + {5AD2DF22-EEB4-4696-B4D0-FA97C1DE871E}.Release|x64.Build.0 = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.BMI|Any CPU.ActiveCfg = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.BMI|Any CPU.Build.0 = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.BMI|x64.ActiveCfg = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.BMI|x64.Build.0 = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.CCRL|Any CPU.ActiveCfg = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.CCRL|Any CPU.Build.0 = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.CCRL|x64.ActiveCfg = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.CCRL|x64.Build.0 = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.Debug|x64.Build.0 = Debug|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.Release|Any CPU.Build.0 = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.Release|x64.ActiveCfg = Release|Any CPU + {97466830-B71A-4DC4-AD43-D92CC6F867DE}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Cosette/Cosette.csproj b/Cosette/Cosette.csproj index 5f6f977..ad2e6b4 100644 --- a/Cosette/Cosette.csproj +++ b/Cosette/Cosette.csproj @@ -2,11 +2,11 @@ Exe - netcoreapp3.1 + net5.0 Debug;Release;BMI;CCRL AnyCPU;x64 - 2.0.0 - 2.0.0.0 + 3.0.0 + 3.0.0.0 @@ -24,7 +24,7 @@ true x64 - TRACE;UCI_DEBUG_OUTPUT;LOGGER + TRACE diff --git a/Cosette/Engine/Ai/Ordering/HistoryHeuristic.cs b/Cosette/Engine/Ai/Ordering/HistoryHeuristic.cs index 0a57e37..94ec9c5 100644 --- a/Cosette/Engine/Ai/Ordering/HistoryHeuristic.cs +++ b/Cosette/Engine/Ai/Ordering/HistoryHeuristic.cs @@ -33,7 +33,7 @@ public static void AddHistoryMove(int color, int from, int to, int depth) public static short GetHistoryMoveValue(int color, int from, int to) { - return (short)(_historyMoves[color][from][to] / (_max / MoveOrderingConstants.HistoryHeuristicMaxScore)); + return (short)(_historyMoves[color][from][to] / ((float)_max / MoveOrderingConstants.HistoryHeuristicMaxScore)); } public static void Clear() diff --git a/Cosette/Engine/Ai/Ordering/MoveOrdering.cs b/Cosette/Engine/Ai/Ordering/MoveOrdering.cs index f0b0292..1562e55 100644 --- a/Cosette/Engine/Ai/Ordering/MoveOrdering.cs +++ b/Cosette/Engine/Ai/Ordering/MoveOrdering.cs @@ -8,7 +8,7 @@ namespace Cosette.Engine.Ai.Ordering { public static class MoveOrdering { - public static void AssignValues(BoardState board, Span moves, Span moveValues, int movesCount, int depth, Move hashOrPvMove) + public static void AssignLoudValues(BoardState board, Span moves, Span moveValues, int movesCount, int depth, Move hashOrPvMove) { for (var moveIndex = 0; moveIndex < movesCount; moveIndex++) { @@ -16,27 +16,15 @@ public static void AssignValues(BoardState board, Span moves, Span { moveValues[moveIndex] = MoveOrderingConstants.HashMove; } - else if (moves[moveIndex].IsQuiet()) - { - var pieceType = board.PieceTable[moves[moveIndex].From]; - if (pieceType == Piece.Pawn && IsPawnNearPromotion(board.ColorToMove, moves[moveIndex].To)) - { - moveValues[moveIndex] = MoveOrderingConstants.PawnNearPromotion; - } - else if (KillerHeuristic.KillerMoveExists(moves[moveIndex], board.ColorToMove, depth)) - { - moveValues[moveIndex] = MoveOrderingConstants.KillerMove; - } - else - { - moveValues[moveIndex] = HistoryHeuristic.GetHistoryMoveValue(board.ColorToMove, moves[moveIndex].From, moves[moveIndex].To); - } - } else if (moves[moveIndex].Flags == MoveFlags.EnPassant) { moveValues[moveIndex] = MoveOrderingConstants.EnPassant; } - else if (((byte)moves[moveIndex].Flags & MoveFlagFields.Capture) != 0) + else if (((byte)moves[moveIndex].Flags & MoveFlagFields.Promotion) != 0) + { + moveValues[moveIndex] = (short)(MoveOrderingConstants.Promotion + (int)moves[moveIndex].Flags); + } + else if (moves[moveIndex].IsCapture()) { var enemyColor = ColorOperations.Invert(board.ColorToMove); @@ -53,9 +41,24 @@ public static void AssignValues(BoardState board, Span moves, Span { moveValues[moveIndex] = MoveOrderingConstants.Castling; } - else if (((byte)moves[moveIndex].Flags & MoveFlagFields.Promotion) != 0) + else { - moveValues[moveIndex] = (short)(MoveOrderingConstants.Promotion + (int)moves[moveIndex].Flags); + moveValues[moveIndex] = MoveOrderingConstants.PawnNearPromotion; + } + } + } + + public static void AssignQuietValues(BoardState board, Span moves, Span moveValues, int startIndex, int movesCount, int depth) + { + for (var moveIndex = startIndex; moveIndex < movesCount; moveIndex++) + { + if (KillerHeuristic.KillerMoveExists(moves[moveIndex], board.ColorToMove, depth)) + { + moveValues[moveIndex] = MoveOrderingConstants.KillerMove; + } + else + { + moveValues[moveIndex] = HistoryHeuristic.GetHistoryMoveValue(board.ColorToMove, moves[moveIndex].From, moves[moveIndex].To); } } } @@ -85,6 +88,11 @@ public static void AssignQValues(BoardState board, Span moves, Span public static void SortNextBestMove(Span moves, Span moveValues, int movesCount, int currentIndex) { + if (movesCount <= 1) + { + return; + } + var max = short.MinValue; var maxIndex = -1; @@ -100,16 +108,5 @@ public static void SortNextBestMove(Span moves, Span moveValues, in (moves[maxIndex], moves[currentIndex]) = (moves[currentIndex], moves[maxIndex]); (moveValues[maxIndex], moveValues[currentIndex]) = (moveValues[currentIndex], moveValues[maxIndex]); } - - - private static bool IsPawnNearPromotion(int color, byte to) - { - if (color == Color.White && to >= 40 || color == Color.Black && to <= 23) - { - return true; - } - - return false; - } } } diff --git a/Cosette/Engine/Ai/Ordering/MoveOrderingConstants.cs b/Cosette/Engine/Ai/Ordering/MoveOrderingConstants.cs index e3428c1..6b92aae 100644 --- a/Cosette/Engine/Ai/Ordering/MoveOrderingConstants.cs +++ b/Cosette/Engine/Ai/Ordering/MoveOrderingConstants.cs @@ -2,15 +2,15 @@ { public static class MoveOrderingConstants { - public const int HashMove = 10000; - public const int Promotion = 5000; - public const int Castling = 1000; - public const int PawnNearPromotion = 150; - public const int Capture = 100; - public const int EnPassant = 100; - public const int KillerMove = 90; - public const int HistoryHeuristicMaxScore = 80; + public static short HashMove = 10000; + public static short Promotion = 5000; + public static short Castling = 1000; + public static short PawnNearPromotion = 300; + public static short Capture = 200; + public static short EnPassant = 100; + public static short KillerMove = 90; - public const int KillerSlots = 3; + public static uint HistoryHeuristicMaxScore = 80; + public static int KillerSlots = 3; } } diff --git a/Cosette/Engine/Ai/Ordering/SeePiece.cs b/Cosette/Engine/Ai/Ordering/SeePiece.cs new file mode 100644 index 0000000..915fbad --- /dev/null +++ b/Cosette/Engine/Ai/Ordering/SeePiece.cs @@ -0,0 +1,14 @@ +namespace Cosette.Engine.Ai.Ordering +{ + public static class SeePiece + { + public const int Pawn = 0; + public const int Knight1 = 1; + public const int Knight2 = 2; + public const int Bishop = 3; + public const int Rook1 = 4; + public const int Rook2 = 5; + public const int Queen = 6; + public const int King = 7; + } +} diff --git a/Cosette/Engine/Ai/Ordering/StaticExchangeEvaluation.cs b/Cosette/Engine/Ai/Ordering/StaticExchangeEvaluation.cs index 4725608..4b723c9 100644 --- a/Cosette/Engine/Ai/Ordering/StaticExchangeEvaluation.cs +++ b/Cosette/Engine/Ai/Ordering/StaticExchangeEvaluation.cs @@ -6,7 +6,7 @@ namespace Cosette.Engine.Ai.Ordering { public static class StaticExchangeEvaluation { - private static short[][][][] _table; + private static short[][][] _table; public static void Init() { @@ -16,22 +16,18 @@ public static void Init() public static short Evaluate(int attackingPiece, int capturedPiece, int attacker, int defender) { - return _table[attackingPiece][capturedPiece][attacker][defender]; + return (short)(EvaluationConstants.Pieces[capturedPiece] + _table[attackingPiece][attacker][defender]); } private static void InitTable() { - _table = new short[6][][][]; + _table = new short[6][][]; for (var attackingPiece = 0; attackingPiece < 6; attackingPiece++) { - _table[attackingPiece] = new short[6][][]; - for (var capturedPiece = 0; capturedPiece < 6; capturedPiece++) + _table[attackingPiece] = new short[256][]; + for (var attackerIndex = 0; attackerIndex < 256; attackerIndex++) { - _table[attackingPiece][capturedPiece] = new short[64][]; - for (var attackerIndex = 0; attackerIndex < 64; attackerIndex++) - { - _table[attackingPiece][capturedPiece][attackerIndex] = new short[64]; - } + _table[attackingPiece][attackerIndex] = new short[256]; } } } @@ -41,78 +37,111 @@ private static void PopulateTable() var gainList = new List(); for (var attackingPiece = 0; attackingPiece < 6; attackingPiece++) { - for (var capturedPiece = 0; capturedPiece < 6; capturedPiece++) + for (ulong attackerIndex = 0; attackerIndex < 256; attackerIndex++) { - for (ulong attackerIndex = 0; attackerIndex < 64; attackerIndex++) + for (ulong defenderIndex = 0; defenderIndex < 256; defenderIndex++) { - for (ulong defenderIndex = 0; defenderIndex < 64; defenderIndex++) + var attackingPieceSeeIndex = GetSeeIndexByPiece(attackingPiece); + var attackers = attackerIndex & ~(1ul << attackingPieceSeeIndex); + var defenders = defenderIndex; + + var currentPieceOnField = attackingPiece; + var result = 0; + + gainList.Add(result); + + if (defenders != 0) { - var attackers = attackerIndex & ~(1ul << attackingPiece); - var defenders = defenderIndex; + var leastValuableDefenderPiece = GetLeastValuablePiece(defenders); + defenders = BitOperations.PopLsb(defenders); - var currentPieceOnField = attackingPiece; - var result = EvaluationConstants.Pieces[capturedPiece]; + result -= EvaluationConstants.Pieces[currentPieceOnField]; + currentPieceOnField = leastValuableDefenderPiece; gainList.Add(result); - if (defenders != 0) + while (attackers != 0) { - var leastValuableDefenderField = BitOperations.GetLsb(defenders); - var leastValuableDefenderPiece = BitOperations.BitScan(leastValuableDefenderField); - defenders = BitOperations.PopLsb(defenders); + var leastValuableAttackerPiece = GetLeastValuablePiece(attackers); + attackers = BitOperations.PopLsb(attackers); - result -= EvaluationConstants.Pieces[currentPieceOnField]; - currentPieceOnField = leastValuableDefenderPiece; + result += EvaluationConstants.Pieces[currentPieceOnField]; + currentPieceOnField = leastValuableAttackerPiece; gainList.Add(result); - while (attackers != 0) + if (gainList[^1] > gainList[^3]) { - var leastValuableAttackerField = BitOperations.GetLsb(attackers); - var leastValuableAttackerPiece = BitOperations.BitScan(leastValuableAttackerField); - attackers = BitOperations.PopLsb(attackers); + result = gainList[^3]; + break; + } + + if (defenders != 0) + { + leastValuableDefenderPiece = GetLeastValuablePiece(defenders); + defenders = BitOperations.PopLsb(defenders); - result += EvaluationConstants.Pieces[currentPieceOnField]; - currentPieceOnField = leastValuableAttackerPiece; + result -= EvaluationConstants.Pieces[currentPieceOnField]; + currentPieceOnField = leastValuableDefenderPiece; gainList.Add(result); - if (gainList[^1] > gainList[^3]) + if (gainList[^1] < gainList[^3]) { result = gainList[^3]; break; } - - if (defenders != 0) - { - leastValuableDefenderField = BitOperations.GetLsb(defenders); - leastValuableDefenderPiece = BitOperations.BitScan(leastValuableDefenderField); - defenders = BitOperations.PopLsb(defenders); - - result -= EvaluationConstants.Pieces[currentPieceOnField]; - currentPieceOnField = leastValuableDefenderPiece; - - gainList.Add(result); - - if (gainList[^1] < gainList[^3]) - { - result = gainList[^3]; - break; - } - } - else - { - break; - } + } + else + { + break; } } - - _table[attackingPiece][capturedPiece][attackerIndex][defenderIndex] = (short)result; - gainList.Clear(); } + + _table[attackingPiece][attackerIndex][defenderIndex] = (short)result; + gainList.Clear(); } } } } + + private static int GetPieceBySeeIndex(int index) + { + switch (index) + { + case SeePiece.Pawn: return Piece.Pawn; + case SeePiece.Knight1: case SeePiece.Knight2: return Piece.Knight; + case SeePiece.Bishop: return Piece.Bishop; + case SeePiece.Rook1: case SeePiece.Rook2: return Piece.Rook; + case SeePiece.Queen: return Piece.Queen; + case SeePiece.King: return Piece.King; + } + + return -1; + } + + private static int GetSeeIndexByPiece(int piece) + { + switch (piece) + { + case Piece.Pawn: return SeePiece.Pawn; + case Piece.Knight: return SeePiece.Knight1; + case Piece.Bishop: return SeePiece.Bishop; + case Piece.Rook: return SeePiece.Rook1; + case Piece.Queen: return SeePiece.Queen; + case Piece.King: return SeePiece.King; + } + + return -1; + } + + private static int GetLeastValuablePiece(ulong data) + { + var leastValuableDefenderField = BitOperations.GetLsb(data); + var leastValuableDefenderPiece = BitOperations.BitScan(leastValuableDefenderField); + + return GetPieceBySeeIndex(leastValuableDefenderPiece); + } } } diff --git a/Cosette/Engine/Ai/Score/Evaluation.cs b/Cosette/Engine/Ai/Score/Evaluation.cs index 7c3e029..e42994d 100644 --- a/Cosette/Engine/Ai/Score/Evaluation.cs +++ b/Cosette/Engine/Ai/Score/Evaluation.cs @@ -1,4 +1,5 @@ using Cosette.Engine.Ai.Score.Evaluators; +using Cosette.Engine.Ai.Search; using Cosette.Engine.Board; using Cosette.Engine.Common; @@ -9,15 +10,23 @@ public class Evaluation public static int Evaluate(BoardState board, EvaluationStatistics statistics) { var openingPhase = board.GetPhaseRatio(); - var endingPhase = 1f - openingPhase; + var endingPhase = BoardConstants.PhaseResolution - openingPhase; var result = MaterialEvaluator.Evaluate(board); - result += CastlingEvaluator.Evaluate(board, openingPhase, endingPhase); result += PositionEvaluator.Evaluate(board, openingPhase, endingPhase); result += PawnStructureEvaluator.Evaluate(board, statistics, openingPhase, endingPhase); - result += MobilityEvaluator.Evaluate(board, openingPhase, endingPhase); - result += KingSafetyEvaluator.Evaluate(board, openingPhase, endingPhase); - result += PiecesEvaluator.Evaluate(board, openingPhase, endingPhase); + + if (endingPhase != BoardConstants.PhaseResolution) + { + var fieldsAttackedByWhite = 0ul; + var fieldsAttackedByBlack = 0ul; + + result += MobilityEvaluator.Evaluate(board, openingPhase, endingPhase, ref fieldsAttackedByWhite, ref fieldsAttackedByBlack); + result += KingSafetyEvaluator.Evaluate(board, openingPhase, endingPhase, fieldsAttackedByWhite, fieldsAttackedByBlack); + result += CastlingEvaluator.Evaluate(board, openingPhase, endingPhase); + result += FianchettoEvaluator.Evaluate(board, openingPhase, endingPhase); + result += PiecesEvaluator.Evaluate(board, openingPhase, endingPhase); + } return board.ColorToMove == Color.White ? result : -result; } diff --git a/Cosette/Engine/Ai/Score/EvaluationConstants.cs b/Cosette/Engine/Ai/Score/EvaluationConstants.cs index ed70a97..8eae08f 100644 --- a/Cosette/Engine/Ai/Score/EvaluationConstants.cs +++ b/Cosette/Engine/Ai/Score/EvaluationConstants.cs @@ -6,20 +6,35 @@ public static class EvaluationConstants public const int Checkmate = 32000; public const int ThreefoldRepetition = 0; + public const int InsufficientMaterial = 0; + + public static int CastlingDone = 50; + public static int CastlingFailed = -50; - public static int[] CastlingDone = { 50, 0 }; - public static int[] CastlingFailed = { -50, 0 }; public static int[] DoubledPawns = { -20, -10 }; public static int[] IsolatedPawns = { -20, -10 }; public static int[] ChainedPawns = { 5, 5 }; public static int[] PassingPawns = { 10, 50 }; - public static int[] Mobility = { 4, 1 }; - public static int[] KingInDanger = { -20, -5 }; - public static int[] PawnShield = { 20, 0 }; - public static int[] DoubledRooks = { 40, 10 }; - public static int[] RookOnOpenFile = { 20, 0 }; - public static int[] PairOfBishops = { 30, 20 }; + + public static int Mobility = 3; + public static int CenterMobilityModifier = 3; + public static int ExtendedCenterMobilityModifier = 2; + public static int OutsideMobilityModifier = 1; + + public static int KingInDanger = -10; + public static int PawnShield = 15; + + public static int DoubledRooks = 40; + public static int RookOnOpenFile = 20; + public static int PairOfBishops = 30; + + public static int Fianchetto = 20; + public static int FianchettoWithoutBishop = -20; public const int OpeningEndgameEdge = 20500; + + public const ulong Center = 0x1818000000; + public const ulong ExtendedCenter = 0x3c24243c0000; + public const ulong Outside = 0xffffc3c3c3c3ffff; } } diff --git a/Cosette/Engine/Ai/Score/Evaluators/CastlingEvaluator.cs b/Cosette/Engine/Ai/Score/Evaluators/CastlingEvaluator.cs index 50a59c4..8b36353 100644 --- a/Cosette/Engine/Ai/Score/Evaluators/CastlingEvaluator.cs +++ b/Cosette/Engine/Ai/Score/Evaluators/CastlingEvaluator.cs @@ -5,25 +5,25 @@ namespace Cosette.Engine.Ai.Score.Evaluators { public static class CastlingEvaluator { - public static int Evaluate(BoardState board, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, int openingPhase, int endingPhase) { return Evaluate(board, Color.White, openingPhase, endingPhase) - Evaluate(board, Color.Black, openingPhase, endingPhase); } - public static int Evaluate(BoardState board, int color, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, int color, int openingPhase, int endingPhase) { if (board.CastlingDone[color]) { - return (int)(EvaluationConstants.CastlingDone[GamePhase.Opening] * openingPhase + - EvaluationConstants.CastlingDone[GamePhase.Ending] * endingPhase); + var openingScore = EvaluationConstants.CastlingDone; + return TaperedEvaluation.AdjustToPhase(openingScore, 0, openingPhase, endingPhase); } if (color == Color.White && (board.Castling & Castling.WhiteCastling) == 0 || color == Color.Black && (board.Castling & Castling.BlackCastling) == 0) { - return (int)(EvaluationConstants.CastlingFailed[GamePhase.Opening] * openingPhase + - EvaluationConstants.CastlingFailed[GamePhase.Ending] * endingPhase); + var openingScore = EvaluationConstants.CastlingFailed; + return TaperedEvaluation.AdjustToPhase(openingScore, 0, openingPhase, endingPhase); } return 0; diff --git a/Cosette/Engine/Ai/Score/Evaluators/FianchettoEvaluator.cs b/Cosette/Engine/Ai/Score/Evaluators/FianchettoEvaluator.cs new file mode 100644 index 0000000..f08fbde --- /dev/null +++ b/Cosette/Engine/Ai/Score/Evaluators/FianchettoEvaluator.cs @@ -0,0 +1,61 @@ +using Cosette.Engine.Board; +using Cosette.Engine.Common; +using Cosette.Engine.Moves.Patterns; + +namespace Cosette.Engine.Ai.Score.Evaluators +{ + public static class FianchettoEvaluator + { + private const ulong KingSideWhitePawnsPattern = 132352; + private const ulong KingSideWhiteBishopPattern = 512; + private const ulong QueenSideWhitePawnsPattern = 4235264; + private const ulong QueenSideWhiteBishopPattern = 16384; + + private const ulong KingSideBlackPawnsPattern = 1409573906808832; + private const ulong KingSideBlackBishopPattern = 562949953421312; + private const ulong QueenSideBlackPawnsPattern = 45106365017882624; + private const ulong QueenSideBlackBishopPattern = 18014398509481984; + + public static int Evaluate(BoardState board, int openingPhase, int endingPhase) + { + return Evaluate(board, Color.White, openingPhase, endingPhase) - + Evaluate(board, Color.Black, openingPhase, endingPhase); + } + + public static int Evaluate(BoardState board, int color, int openingPhase, int endingPhase) + { + var kingSidePawnsPattern = color == Color.White ? KingSideWhitePawnsPattern : KingSideBlackPawnsPattern; + var kingSideBishopPattern = color == Color.White ? KingSideWhiteBishopPattern : KingSideBlackBishopPattern; + var queenSidePawnsPattern = color == Color.White ? QueenSideWhitePawnsPattern : QueenSideBlackPawnsPattern; + var queenSideBishopPattern = color == Color.White ? QueenSideWhiteBishopPattern : QueenSideBlackBishopPattern; + + var pawns = board.Pieces[color][Piece.Pawn]; + var bishops = board.Pieces[color][Piece.Bishop]; + + var fianchettoOpeningScore = EvaluationConstants.Fianchetto; + var fianchettoPenaltyOpeningScore = EvaluationConstants.FianchettoWithoutBishop; + + if ((pawns & kingSidePawnsPattern) == kingSidePawnsPattern) + { + if ((bishops & kingSideBishopPattern) == kingSideBishopPattern) + { + return TaperedEvaluation.AdjustToPhase(fianchettoOpeningScore, 0, openingPhase, endingPhase); + } + + return TaperedEvaluation.AdjustToPhase(fianchettoPenaltyOpeningScore, 0, openingPhase, endingPhase); + } + + if ((pawns & queenSidePawnsPattern) == queenSidePawnsPattern) + { + if ((bishops & queenSideBishopPattern) == queenSideBishopPattern) + { + return TaperedEvaluation.AdjustToPhase(fianchettoOpeningScore, 0, openingPhase, endingPhase); + } + + return TaperedEvaluation.AdjustToPhase(fianchettoPenaltyOpeningScore, 0, openingPhase, endingPhase); + } + + return 0; + } + } +} diff --git a/Cosette/Engine/Ai/Score/Evaluators/KingSafetyEvaluator.cs b/Cosette/Engine/Ai/Score/Evaluators/KingSafetyEvaluator.cs index df4f602..a3feab4 100644 --- a/Cosette/Engine/Ai/Score/Evaluators/KingSafetyEvaluator.cs +++ b/Cosette/Engine/Ai/Score/Evaluators/KingSafetyEvaluator.cs @@ -6,45 +6,58 @@ namespace Cosette.Engine.Ai.Score.Evaluators { public static class KingSafetyEvaluator { - public static int Evaluate(BoardState board, float openingPhase, float endingPhase) - { - return Evaluate(board, Color.White, openingPhase, endingPhase) - - Evaluate(board, Color.Black, openingPhase, endingPhase); - } + private static ulong[][] _kingSafetyMask; - public static int Evaluate(BoardState board, int color, float openingPhase, float endingPhase) + static KingSafetyEvaluator() { - var king = board.Pieces[color][Piece.King]; - var kingField = BitOperations.BitScan(king); - var fieldsAroundKing = BoxPatternGenerator.GetPattern(kingField); + _kingSafetyMask = new ulong[2][]; - var attackersCount = 0; - var pawnShield = 0; - - var attackedFieldsToCheck = fieldsAroundKing; - while (attackedFieldsToCheck != 0) + for (var color = 0; color < 2; color++) { - var lsb = BitOperations.GetLsb(attackedFieldsToCheck); - var field = BitOperations.BitScan(lsb); - attackedFieldsToCheck = BitOperations.PopLsb(attackedFieldsToCheck); + var offset = color == Color.White ? 8 : -8; + _kingSafetyMask[color] = new ulong[64]; - var attackingPieces = board.IsFieldAttacked(color, (byte)field); - if (attackingPieces) + for (var fieldIndex = 0; fieldIndex < 64; fieldIndex++) { - attackersCount++; + var mask = BoxPatternGenerator.GetPattern(fieldIndex); + var fieldIndexWithOffset = fieldIndex + offset; + + if (fieldIndexWithOffset >= 0 && fieldIndexWithOffset < 64) + { + mask |= BoxPatternGenerator.GetPattern(fieldIndexWithOffset); + mask &= ~(1ul << fieldIndex); + } + + _kingSafetyMask[color][fieldIndex] = mask; } } + } + + public static int Evaluate(BoardState board, int openingPhase, int endingPhase, ulong fieldsAttackedByWhite, ulong fieldsAttackedByBlack) + { + return Evaluate(board, Color.White, openingPhase, endingPhase, fieldsAttackedByBlack) - + Evaluate(board, Color.Black, openingPhase, endingPhase, fieldsAttackedByWhite); + } + + public static int Evaluate(BoardState board, int color, int openingPhase, int endingPhase, ulong fieldsAttackedByEnemy) + { + var king = board.Pieces[color][Piece.King]; + var kingField = BitOperations.BitScan(king); + var fieldsAroundKing = _kingSafetyMask[color][kingField]; + + var attackedFieldsAroundKing = fieldsAroundKing & fieldsAttackedByEnemy; + var attackersCount = (int)BitOperations.Count(attackedFieldsAroundKing); var pawnsNearKing = fieldsAroundKing & board.Pieces[color][Piece.Pawn]; - pawnShield = (int)BitOperations.Count(pawnsNearKing); + var pawnShield = (int)BitOperations.Count(pawnsNearKing); - var attackersCountScore = attackersCount * EvaluationConstants.KingInDanger[GamePhase.Opening] * openingPhase + - attackersCount * EvaluationConstants.KingInDanger[GamePhase.Ending] * endingPhase; + var attackersCountOpeningScore = attackersCount * EvaluationConstants.KingInDanger; + var attackersCountAdjusted = TaperedEvaluation.AdjustToPhase(attackersCountOpeningScore, 0, openingPhase, endingPhase); - var pawnShieldScore = pawnShield * EvaluationConstants.PawnShield[GamePhase.Opening] * openingPhase + - pawnShield * EvaluationConstants.PawnShield[GamePhase.Ending] * endingPhase; + var pawnShieldOpeningScore = pawnShield * EvaluationConstants.PawnShield; + var pawnShieldAdjusted = TaperedEvaluation.AdjustToPhase(pawnShieldOpeningScore, 0, openingPhase, endingPhase); - return (int)(attackersCountScore + pawnShieldScore); + return attackersCountAdjusted + pawnShieldAdjusted; } } } diff --git a/Cosette/Engine/Ai/Score/Evaluators/MaterialEvaluator.cs b/Cosette/Engine/Ai/Score/Evaluators/MaterialEvaluator.cs index 9c78c5f..35a68bf 100644 --- a/Cosette/Engine/Ai/Score/Evaluators/MaterialEvaluator.cs +++ b/Cosette/Engine/Ai/Score/Evaluators/MaterialEvaluator.cs @@ -7,8 +7,7 @@ public static class MaterialEvaluator { public static int Evaluate(BoardState board) { - return board.Material[Color.White] - - board.Material[Color.Black]; + return board.Material[Color.White] - board.Material[Color.Black]; } } } diff --git a/Cosette/Engine/Ai/Score/Evaluators/MobilityEvaluator.cs b/Cosette/Engine/Ai/Score/Evaluators/MobilityEvaluator.cs index efab005..75b7082 100644 --- a/Cosette/Engine/Ai/Score/Evaluators/MobilityEvaluator.cs +++ b/Cosette/Engine/Ai/Score/Evaluators/MobilityEvaluator.cs @@ -6,21 +6,21 @@ namespace Cosette.Engine.Ai.Score.Evaluators { public static class MobilityEvaluator { - public static int Evaluate(BoardState board, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, int openingPhase, int endingPhase, ref ulong fieldsAttackedByWhite, ref ulong fieldsAttackedByBlack) { - return Evaluate(board, Color.White, openingPhase, endingPhase) - - Evaluate(board, Color.Black, openingPhase, endingPhase); + return Evaluate(board, Color.White, openingPhase, endingPhase, ref fieldsAttackedByWhite) - + Evaluate(board, Color.Black, openingPhase, endingPhase, ref fieldsAttackedByBlack); } - public static int Evaluate(BoardState board, int color, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, int color, int openingPhase, int endingPhase, ref ulong fieldsAttackedByColor) { - var mobility = KnightOperator.GetMobility(board, color) + - BishopOperator.GetMobility(board, color) + - RookOperator.GetMobility(board, color) + - QueenOperator.GetMobility(board, color); - - return (int)(mobility * EvaluationConstants.Mobility[GamePhase.Opening] * openingPhase + - mobility * EvaluationConstants.Mobility[GamePhase.Ending] * endingPhase); + var mobility = KnightOperator.GetMobility(board, color, ref fieldsAttackedByColor) + + BishopOperator.GetMobility(board, color, ref fieldsAttackedByColor) + + RookOperator.GetMobility(board, color, ref fieldsAttackedByColor) + + QueenOperator.GetMobility(board, color, ref fieldsAttackedByColor); + + var mobilityOpeningScore = mobility * EvaluationConstants.Mobility; + return TaperedEvaluation.AdjustToPhase(mobilityOpeningScore, 0, openingPhase, endingPhase); } } } diff --git a/Cosette/Engine/Ai/Score/Evaluators/PawnStructureEvaluator.cs b/Cosette/Engine/Ai/Score/Evaluators/PawnStructureEvaluator.cs index 20e9744..4b99c8d 100644 --- a/Cosette/Engine/Ai/Score/Evaluators/PawnStructureEvaluator.cs +++ b/Cosette/Engine/Ai/Score/Evaluators/PawnStructureEvaluator.cs @@ -38,7 +38,7 @@ static PawnStructureEvaluator() } } - public static int Evaluate(BoardState board, EvaluationStatistics statistics, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, EvaluationStatistics statistics, int openingPhase, int endingPhase) { var entry = PawnHashTable.Get(board.PawnHash); if (entry.IsKeyValid(board.PawnHash)) @@ -71,7 +71,7 @@ public static int Evaluate(BoardState board, EvaluationStatistics statistics, fl return result; } - public static int Evaluate(BoardState board, int color, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, int color, int openingPhase, int endingPhase) { var doubledPawns = 0; var isolatedPawns = 0; @@ -120,19 +120,23 @@ public static int Evaluate(BoardState board, int color, float openingPhase, floa } } - var doubledPawnsScore = doubledPawns * EvaluationConstants.DoubledPawns[GamePhase.Opening] * openingPhase + - doubledPawns * EvaluationConstants.DoubledPawns[GamePhase.Ending] * endingPhase; + var doubledPawnsOpeningScore = doubledPawns * EvaluationConstants.DoubledPawns[GamePhase.Opening]; + var doubledPawnsEndingScore = doubledPawns * EvaluationConstants.DoubledPawns[GamePhase.Ending]; + var doubledPawnsAdjusted = TaperedEvaluation.AdjustToPhase(doubledPawnsOpeningScore, doubledPawnsEndingScore, openingPhase, endingPhase); - var isolatedPawnsScore = isolatedPawns * EvaluationConstants.IsolatedPawns[GamePhase.Opening] * openingPhase + - isolatedPawns * EvaluationConstants.IsolatedPawns[GamePhase.Ending] * endingPhase; + var isolatedPawnsOpeningScore = isolatedPawns * EvaluationConstants.IsolatedPawns[GamePhase.Opening]; + var isolatedPawnsEndingScore = isolatedPawns * EvaluationConstants.IsolatedPawns[GamePhase.Ending]; + var isolatedPawnsAdjusted = TaperedEvaluation.AdjustToPhase(isolatedPawnsOpeningScore, isolatedPawnsEndingScore, openingPhase, endingPhase); - var chainedPawnsScore = chainedPawns * EvaluationConstants.ChainedPawns[GamePhase.Opening] * openingPhase + - chainedPawns * EvaluationConstants.ChainedPawns[GamePhase.Ending] * endingPhase; + var chainedPawnsOpeningScore = chainedPawns * EvaluationConstants.ChainedPawns[GamePhase.Opening]; + var chainedPawnsEndingScore = chainedPawns * EvaluationConstants.ChainedPawns[GamePhase.Ending]; + var chainedPawnsAdjusted = TaperedEvaluation.AdjustToPhase(chainedPawnsOpeningScore, chainedPawnsEndingScore, openingPhase, endingPhase); - var passingPawnsScore = passingPawns * EvaluationConstants.PassingPawns[GamePhase.Opening] * openingPhase + - passingPawns * EvaluationConstants.PassingPawns[GamePhase.Ending] * endingPhase; + var passingPawnsOpeningScore = passingPawns * EvaluationConstants.PassingPawns[GamePhase.Opening]; + var passingPawnsEndingScore = passingPawns * EvaluationConstants.PassingPawns[GamePhase.Ending]; + var passingPawnsAdjusted = TaperedEvaluation.AdjustToPhase(passingPawnsOpeningScore, passingPawnsEndingScore, openingPhase, endingPhase); - return (int)(doubledPawnsScore + isolatedPawnsScore + chainedPawnsScore + passingPawnsScore); + return doubledPawnsAdjusted + isolatedPawnsAdjusted + chainedPawnsAdjusted + passingPawnsAdjusted; } } } diff --git a/Cosette/Engine/Ai/Score/Evaluators/PiecesEvaluator.cs b/Cosette/Engine/Ai/Score/Evaluators/PiecesEvaluator.cs index e2d1620..e970786 100644 --- a/Cosette/Engine/Ai/Score/Evaluators/PiecesEvaluator.cs +++ b/Cosette/Engine/Ai/Score/Evaluators/PiecesEvaluator.cs @@ -6,13 +6,13 @@ namespace Cosette.Engine.Ai.Score.Evaluators { public static class PiecesEvaluator { - public static int Evaluate(BoardState board, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, int openingPhase, int endingPhase) { return Evaluate(board, Color.White, openingPhase, endingPhase) - Evaluate(board, Color.Black, openingPhase, endingPhase); } - public static int Evaluate(BoardState board, int color, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, int color, int openingPhase, int endingPhase) { var doubledRooks = 0; var rooksOnOpenFile = 0; @@ -49,16 +49,16 @@ public static int Evaluate(BoardState board, int color, float openingPhase, floa pairOfBishops = 1; } - var doubledRooksScore = doubledRooks * EvaluationConstants.DoubledRooks[GamePhase.Opening] * openingPhase + - doubledRooks * EvaluationConstants.DoubledRooks[GamePhase.Ending] * endingPhase; + var doubledRooksOpeningScore = doubledRooks * EvaluationConstants.DoubledRooks; + var doubledRooksAdjusted = TaperedEvaluation.AdjustToPhase(doubledRooksOpeningScore, 0, openingPhase, endingPhase); - var rooksOnOpenFileScore = rooksOnOpenFile * EvaluationConstants.RookOnOpenFile[GamePhase.Opening] * openingPhase + - rooksOnOpenFile * EvaluationConstants.RookOnOpenFile[GamePhase.Ending] * endingPhase; + var rooksOnOpenFileOpeningScore = rooksOnOpenFile * EvaluationConstants.RookOnOpenFile; + var rooksOnOpenFileAdjusted = TaperedEvaluation.AdjustToPhase(rooksOnOpenFileOpeningScore, 0, openingPhase, endingPhase); - var pairOfBishopsScore = pairOfBishops * EvaluationConstants.PairOfBishops[GamePhase.Opening] * openingPhase + - pairOfBishops * EvaluationConstants.PairOfBishops[GamePhase.Ending] * endingPhase; + var pairOfBishopsOpeningScore = pairOfBishops * EvaluationConstants.PairOfBishops; + var pairOfBishopsAdjusted = TaperedEvaluation.AdjustToPhase(pairOfBishopsOpeningScore, 0, openingPhase, endingPhase); - return (int)(doubledRooksScore + rooksOnOpenFileScore + pairOfBishopsScore); + return doubledRooksAdjusted + rooksOnOpenFileAdjusted + pairOfBishopsAdjusted; } } } \ No newline at end of file diff --git a/Cosette/Engine/Ai/Score/Evaluators/PositionEvaluator.cs b/Cosette/Engine/Ai/Score/Evaluators/PositionEvaluator.cs index f496302..3c32b7f 100644 --- a/Cosette/Engine/Ai/Score/Evaluators/PositionEvaluator.cs +++ b/Cosette/Engine/Ai/Score/Evaluators/PositionEvaluator.cs @@ -5,16 +5,17 @@ namespace Cosette.Engine.Ai.Score.Evaluators { public static class PositionEvaluator { - public static int Evaluate(BoardState board, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, int openingPhase, int endingPhase) { return Evaluate(board, Color.White, openingPhase, endingPhase) - Evaluate(board, Color.Black, openingPhase, endingPhase); } - public static int Evaluate(BoardState board, int color, float openingPhase, float endingPhase) + public static int Evaluate(BoardState board, int color, int openingPhase, int endingPhase) { - return (int)(board.Position[color][GamePhase.Opening] * openingPhase + - board.Position[color][GamePhase.Ending] * endingPhase); + var positionOpeningScore = board.Position[color][GamePhase.Opening]; + var positionEndingScore = board.Position[color][GamePhase.Ending]; + return TaperedEvaluation.AdjustToPhase(positionOpeningScore, positionEndingScore, openingPhase, endingPhase); } } } diff --git a/Cosette/Engine/Ai/Score/PieceSquareTables/PawnTables.cs b/Cosette/Engine/Ai/Score/PieceSquareTables/PawnTables.cs index 95e5704..f7ec568 100644 --- a/Cosette/Engine/Ai/Score/PieceSquareTables/PawnTables.cs +++ b/Cosette/Engine/Ai/Score/PieceSquareTables/PawnTables.cs @@ -17,11 +17,11 @@ public static class PawnTables 0, 0, 0, 0, 0, 0, 0, 0 }, // Ending - new[] { 25, 25, 25, 25, 25, 25, 25, 25, - 20, 20, 20, 20, 20, 20, 20, 20, - 15, 15, 15, 15, 15, 15, 15, 15, + new[] { 90, 90, 90, 90, 90, 90, 90, 90, + 70, 70, 70, 70, 70, 70, 70, 70, + 50, 50, 50, 50, 50, 50, 50, 50, + 30, 30, 30, 30, 30, 30, 30, 30, 10, 10, 10, 10, 10, 10, 10, 10, - 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } diff --git a/Cosette/Engine/Ai/Score/TaperedEvaluation.cs b/Cosette/Engine/Ai/Score/TaperedEvaluation.cs new file mode 100644 index 0000000..8503e9f --- /dev/null +++ b/Cosette/Engine/Ai/Score/TaperedEvaluation.cs @@ -0,0 +1,12 @@ +using Cosette.Engine.Board; + +namespace Cosette.Engine.Ai.Score +{ + public static class TaperedEvaluation + { + public static int AdjustToPhase(int openingScore, int endingScore, int openingPhase, int endingPhase) + { + return (openingScore * openingPhase + endingScore * endingPhase) / BoardConstants.PhaseResolution; + } + } +} diff --git a/Cosette/Engine/Ai/Search/IterativeDeepening.cs b/Cosette/Engine/Ai/Search/IterativeDeepening.cs index ce2474e..110e599 100644 --- a/Cosette/Engine/Ai/Search/IterativeDeepening.cs +++ b/Cosette/Engine/Ai/Search/IterativeDeepening.cs @@ -21,17 +21,17 @@ public static Move FindBestMove(SearchContext context) var expectedExecutionTime = 0; var alpha = SearchConstants.MinValue; var beta = SearchConstants.MaxValue; - var lastSearchTime = 10ul; + var lastSearchTime = 0ul; var bestMove = Move.Empty; var stopwatch = Stopwatch.StartNew(); + context.Statistics = new SearchStatistics(); + for (var depth = 1; ShouldContinueDeepening(context, depth, expectedExecutionTime); depth++) { - context.Statistics = new SearchStatistics(); - context.Statistics.Board = context.BoardState; context.Statistics.Depth = depth; - context.Statistics.Score = NegaMax.FindBestMove(context, depth, 0, alpha, beta, true); + context.Statistics.Score = NegaMax.FindBestMove(context, depth, 0, alpha, beta); context.Statistics.SearchTime = (ulong)stopwatch.ElapsedMilliseconds; if (context.AbortSearch) @@ -44,8 +44,12 @@ public static Move FindBestMove(SearchContext context) OnSearchUpdate?.Invoke(null, context.Statistics); - var ratio = (float)context.Statistics.SearchTime / lastSearchTime; - expectedExecutionTime = (int)(context.Statistics.SearchTime * ratio); + if (lastSearchTime != 0) + { + var ratio = (float)context.Statistics.SearchTime / lastSearchTime; + expectedExecutionTime = (int)(context.Statistics.SearchTime * ratio); + } + lastSearchTime = context.Statistics.SearchTime; } @@ -55,13 +59,14 @@ public static Move FindBestMove(SearchContext context) } context.AbortSearch = false; + context.HelperTasksCancellationTokenSource?.Cancel(); + return bestMove; } public static bool ShouldContinueDeepening(SearchContext context, int depth, int expectedExecutionTime) { - return depth < context.MaxDepth && - expectedExecutionTime <= context.MaxTime; + return depth < context.MaxDepth && expectedExecutionTime <= context.MaxTime; } public static bool IsScoreNearCheckmate(int score) @@ -81,6 +86,11 @@ private static int GetPrincipalVariation(BoardState board, Move[] moves, int mov var entry = TranspositionTable.Get(board.Hash); if (entry.Flags == TranspositionTableEntryFlags.ExactScore && entry.IsKeyValid(board.Hash) && movesCount < SearchConstants.MaxDepth) { + if (!board.IsMoveLegal(entry.BestMove)) + { + return movesCount; + } + moves[movesCount] = entry.BestMove; board.MakeMove(entry.BestMove); diff --git a/Cosette/Engine/Ai/Search/NegaMax.cs b/Cosette/Engine/Ai/Search/NegaMax.cs index 7b89386..47c6965 100644 --- a/Cosette/Engine/Ai/Search/NegaMax.cs +++ b/Cosette/Engine/Ai/Search/NegaMax.cs @@ -1,7 +1,6 @@ using System; using Cosette.Engine.Ai.Ordering; using Cosette.Engine.Ai.Score; -using Cosette.Engine.Ai.Score.Evaluators; using Cosette.Engine.Ai.Transposition; using Cosette.Engine.Board; using Cosette.Engine.Common; @@ -11,7 +10,13 @@ namespace Cosette.Engine.Ai.Search { public static class NegaMax { - public static int FindBestMove(SearchContext context, int depth, int ply, int alpha, int beta, bool allowNullMove) + public static int FindBestMove(SearchContext context, int depth, int ply, int alpha, int beta) + { + var friendlyKingInCheck = context.BoardState.IsKingChecked(context.BoardState.ColorToMove); + return FindBestMove(context, depth, ply, alpha, beta, true, friendlyKingInCheck); + } + + public static int FindBestMove(SearchContext context, int depth, int ply, int alpha, int beta, bool allowNullMove, bool friendlyKingInCheck) { if (context.Statistics.Nodes >= context.MaxNodesCount) { @@ -38,6 +43,15 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al return EvaluationConstants.ThreefoldRepetition; } + if (context.BoardState.IsInsufficientMaterial()) + { + if (!friendlyKingInCheck && !context.BoardState.IsKingChecked(ColorOperations.Invert(context.BoardState.ColorToMove))) + { + context.Statistics.Leafs++; + return EvaluationConstants.InsufficientMaterial; + } + } + if (context.BoardState.IsFiftyMoveRuleDraw()) { context.Statistics.Leafs++; @@ -57,18 +71,32 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al } var originalAlpha = alpha; - var bestMove = Move.Empty; var pvNode = beta - alpha > 1; var entry = TranspositionTable.Get(context.BoardState.Hash); - if (entry.IsKeyValid(context.BoardState.Hash)) + var hashMove = Move.Empty; + + if (entry.Flags != TranspositionTableEntryFlags.Invalid && entry.IsKeyValid(context.BoardState.Hash)) { #if DEBUG context.Statistics.TTHits++; #endif if (entry.Flags != TranspositionTableEntryFlags.AlphaScore) { - bestMove = entry.BestMove; + var isMoveLegal = context.BoardState.IsMoveLegal(entry.BestMove); + if (isMoveLegal) + { + hashMove = entry.BestMove; +#if DEBUG + context.Statistics.TTValidMoves++; +#endif + } +#if DEBUG + else + { + context.Statistics.TTInvalidMoves++; + } +#endif } if (entry.Depth >= depth) @@ -87,7 +115,7 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al case TranspositionTableEntryFlags.ExactScore: { - if (!pvNode || entry.Age == context.TranspositionTableEntryAge) + if (!pvNode) { entry.Score = (short)TranspositionTable.TTToRegularScore(entry.Score, ply); return entry.Score; @@ -120,11 +148,11 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al context.Statistics.TTNonHits++; } #endif - - if (NullWindowCanBeApplied(context.BoardState, depth, allowNullMove, pvNode)) + + if (NullWindowCanBeApplied(context.BoardState, depth, allowNullMove, pvNode, friendlyKingInCheck)) { context.BoardState.MakeNullMove(); - var score = -FindBestMove(context, depth - 1 - SearchConstants.NullWindowDepthReduction, ply + 1, -beta, -beta + 1, false); + var score = -FindBestMove(context, depth - 1 - SearchConstants.NullWindowDepthReduction, ply + 1, -beta, -beta + 1, false, false); context.BoardState.UndoNullMove(); if (score >= beta) @@ -134,20 +162,50 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al } } + if (IIDCanBeApplied(depth, entry.Flags, hashMove)) + { + FindBestMove(context, depth - 1 - SearchConstants.IIDDepthReduction, ply, alpha, beta, allowNullMove, friendlyKingInCheck); + + var iidEntry = TranspositionTable.Get(context.BoardState.Hash); + if (iidEntry.IsKeyValid(context.BoardState.Hash)) + { + hashMove = iidEntry.BestMove; + +#if DEBUG + context.Statistics.IIDHits++; +#endif + } + } + Span moves = stackalloc Move[SearchConstants.MaxMovesCount]; Span moveValues = stackalloc short[SearchConstants.MaxMovesCount]; + var bestMove = Move.Empty; var movesCount = 0; - var movesGenerated = false; - if (bestMove == Move.Empty) + var loudMovesGenerated = false; + var quietMovesGenerated = false; + + if (hashMove == Move.Empty) { - movesCount = context.BoardState.GetAvailableMoves(moves); - MoveOrdering.AssignValues(context.BoardState, moves, moveValues, movesCount, depth, bestMove); - movesGenerated = true; + movesCount = context.BoardState.GetLoudMoves(moves, 0); + MoveOrdering.AssignLoudValues(context.BoardState, moves, moveValues, movesCount, depth, bestMove); + loudMovesGenerated = true; + + context.Statistics.LoudMovesGenerated++; + + if (movesCount == 0) + { + movesCount = context.BoardState.GetQuietMoves(moves, 0); + MoveOrdering.AssignQuietValues(context.BoardState, moves, moveValues, 0, movesCount, depth); + quietMovesGenerated = true; + + context.Statistics.QuietMovesGenerated++; + } } else { - moves[0] = bestMove; + moves[0] = hashMove; + moveValues[0] = MoveOrderingConstants.HashMove; movesCount = 1; } @@ -156,6 +214,28 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al { MoveOrdering.SortNextBestMove(moves, moveValues, movesCount, moveIndex); + if (loudMovesGenerated && moves[moveIndex] == hashMove) + { + goto postLoopOperations; + } + + if (loudMovesGenerated && !quietMovesGenerated && moveValues[moveIndex] < 100) + { + var loudMovesCount = movesCount; + + movesCount = context.BoardState.GetQuietMoves(moves, movesCount); + MoveOrdering.AssignQuietValues(context.BoardState, moves, moveValues, loudMovesCount, movesCount, depth); + MoveOrdering.SortNextBestMove(moves, moveValues, movesCount, moveIndex); + quietMovesGenerated = true; + + context.Statistics.QuietMovesGenerated++; + + if (moves[moveIndex] == hashMove) + { + goto postLoopOperations; + } + } + if (context.MoveRestrictions != null && ply == 0) { if (!context.MoveRestrictions.Contains(moves[moveIndex])) @@ -165,27 +245,28 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al } context.BoardState.MakeMove(moves[moveIndex]); - + var score = 0; + var enemyKingInCheck = context.BoardState.IsKingChecked(context.BoardState.ColorToMove); + if (pvs) { - score = -FindBestMove(context, depth - 1, ply + 1, -beta, -alpha, allowNullMove); + score = -FindBestMove(context, depth - 1, ply + 1, -beta, -alpha, allowNullMove, enemyKingInCheck); pvs = false; } else { var reducedDepth = depth; - var kingCheckedAfterMove = context.BoardState.IsKingChecked(context.BoardState.ColorToMove); - if (LMRCanBeApplied(depth, kingCheckedAfterMove, moveIndex, moves)) + if (LMRCanBeApplied(depth, friendlyKingInCheck, enemyKingInCheck, moveIndex, moves)) { reducedDepth = LMRGetReducedDepth(depth, pvNode); } - score = -FindBestMove(context, reducedDepth - 1, ply + 1, -alpha - 1, -alpha, allowNullMove); + score = -FindBestMove(context, reducedDepth - 1, ply + 1, -alpha - 1, -alpha, allowNullMove, enemyKingInCheck); if (score > alpha) { - score = -FindBestMove(context, depth - 1, ply + 1, -beta, -alpha, allowNullMove); + score = -FindBestMove(context, depth - 1, ply + 1, -beta, -alpha, allowNullMove, enemyKingInCheck); } } @@ -220,12 +301,35 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al } } - if (!movesGenerated) + postLoopOperations: + if (!loudMovesGenerated) + { + movesCount = context.BoardState.GetLoudMoves(moves, 0); + MoveOrdering.AssignLoudValues(context.BoardState, moves, moveValues, movesCount, depth, bestMove); + moveIndex = -1; + loudMovesGenerated = true; + + context.Statistics.LoudMovesGenerated++; + + if (movesCount == 0) + { + movesCount = context.BoardState.GetQuietMoves(moves, 0); + MoveOrdering.AssignQuietValues(context.BoardState, moves, moveValues, 0, movesCount, depth); + quietMovesGenerated = true; + + context.Statistics.QuietMovesGenerated++; + } + } + + if (!quietMovesGenerated && moveIndex == movesCount - 1) { - movesCount = context.BoardState.GetAvailableMoves(moves); - MoveOrdering.AssignValues(context.BoardState, moves, moveValues, movesCount, depth, bestMove); - MoveOrdering.SortNextBestMove(moves, moveValues, movesCount, 0); - movesGenerated = true; + var loudMovesCount = movesCount; + + movesCount = context.BoardState.GetQuietMoves(moves, movesCount); + MoveOrdering.AssignQuietValues(context.BoardState, moves, moveValues, loudMovesCount, movesCount, depth); + quietMovesGenerated = true; + + context.Statistics.QuietMovesGenerated++; } } @@ -241,16 +345,18 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al return alpha; } - // Set alpha to zero if the stalemate has been detected + // Return draw score or checkmate score as leafs if (alpha == -EvaluationConstants.Checkmate + ply + 2) { - if (!context.BoardState.IsKingChecked(context.BoardState.ColorToMove)) + if (context.BoardState.IsKingChecked(context.BoardState.ColorToMove)) { - alpha = 0; + return alpha; } + + return 0; } - if (entry.Age < context.TranspositionTableEntryAge || entry.Depth < depth) + if (entry.Age != context.TranspositionTableEntryAge || entry.Depth <= depth) { var valueToSave = alpha; var entryType = alpha <= originalAlpha ? TranspositionTableEntryFlags.AlphaScore : @@ -262,8 +368,12 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al valueToSave = TranspositionTable.RegularToTTScore(alpha, ply); } - TranspositionTable.Add(context.BoardState.Hash, (byte)depth, (short)valueToSave, - (byte)context.TranspositionTableEntryAge, bestMove, entryType); + TranspositionTable.Add(context.BoardState.Hash, new TranspositionTableEntry( + context.BoardState.Hash, + (short)valueToSave, bestMove, + (byte)depth, entryType, + (byte)context.TranspositionTableEntryAge) + ); #if DEBUG if (entry.Flags != TranspositionTableEntryFlags.Invalid) @@ -278,17 +388,21 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al return alpha; } - private static bool NullWindowCanBeApplied(BoardState board, int depth, bool allowNullMove, bool pvNode) + private static bool NullWindowCanBeApplied(BoardState board, int depth, bool allowNullMove, bool pvNode, bool friendlyKingInCheck) { return !pvNode && allowNullMove && depth >= SearchConstants.NullWindowMinimalDepth && - board.GetGamePhase() == GamePhase.Opening && - !board.IsKingChecked(board.ColorToMove); + board.GetGamePhase() == GamePhase.Opening && !friendlyKingInCheck; + } + + private static bool IIDCanBeApplied(int depth, TranspositionTableEntryFlags ttEntryType, Move bestMove) + { + return ttEntryType == TranspositionTableEntryFlags.Invalid && depth >= SearchConstants.IIDMinimalDepth && bestMove == Move.Empty; } - private static bool LMRCanBeApplied(int depth, bool kingChecked, int moveIndex, Span moves) + private static bool LMRCanBeApplied(int depth, bool friendlyKingInCheck, bool enemyKingInCheck, int moveIndex, Span moves) { - return depth >= SearchConstants.LMRMinimalDepth && moveIndex > SearchConstants.LMRMovesWithoutReduction && - moves[moveIndex].IsQuiet() && !kingChecked; + return depth >= SearchConstants.LMRMinimalDepth && moveIndex >= SearchConstants.LMRMovesWithoutReduction && + moves[moveIndex].IsQuiet() && !friendlyKingInCheck && !enemyKingInCheck; } private static int LMRGetReducedDepth(int depth, bool pvNode) diff --git a/Cosette/Engine/Ai/Search/QuiescenceSearch.cs b/Cosette/Engine/Ai/Search/QuiescenceSearch.cs index b72ad8e..af4676d 100644 --- a/Cosette/Engine/Ai/Search/QuiescenceSearch.cs +++ b/Cosette/Engine/Ai/Search/QuiescenceSearch.cs @@ -65,13 +65,18 @@ public static int FindBestMove(SearchContext context, int depth, int ply, int al Span moves = stackalloc Move[SearchConstants.MaxMovesCount]; Span moveValues = stackalloc short[SearchConstants.MaxMovesCount]; - var movesCount = context.BoardState.GetAvailableQMoves(moves); + var movesCount = context.BoardState.GetAvailableCaptureMoves(moves); MoveOrdering.AssignQValues(context.BoardState, moves, moveValues, movesCount); for (var moveIndex = 0; moveIndex < movesCount; moveIndex++) { MoveOrdering.SortNextBestMove(moves, moveValues, movesCount, moveIndex); + if (moveValues[moveIndex] < -50) + { + break; + } + context.BoardState.MakeMove(moves[moveIndex]); var score = -FindBestMove(context, depth - 1, ply + 1, -beta, -alpha); context.BoardState.UndoMove(moves[moveIndex]); diff --git a/Cosette/Engine/Ai/Search/SearchConstants.cs b/Cosette/Engine/Ai/Search/SearchConstants.cs index 5a44763..4afafba 100644 --- a/Cosette/Engine/Ai/Search/SearchConstants.cs +++ b/Cosette/Engine/Ai/Search/SearchConstants.cs @@ -5,14 +5,18 @@ public static class SearchConstants public const int MinValue = short.MinValue; public const int MaxValue = short.MaxValue; public const int MaxDepth = 32; - public const int MaxMovesCount = 128; + public const int MaxMovesCount = 218; + public const float DeadlineFactor = 1.5f; - public const int NullWindowMinimalDepth = 5; - public const int NullWindowDepthReduction = 3; + public static int IIDMinimalDepth = 4; + public static int IIDDepthReduction = 2; - public const int LMRMinimalDepth = 2; - public const int LMRMovesWithoutReduction = 2; - public const int LMRPvNodeDepthReduction = 1; - public const int LMRNonPvNodeDepthDivisor = 3; + public static int NullWindowMinimalDepth = 5; + public static int NullWindowDepthReduction = 3; + + public static int LMRMinimalDepth = 2; + public static int LMRMovesWithoutReduction = 3; + public static int LMRPvNodeDepthReduction = 1; + public static int LMRNonPvNodeDepthDivisor = 3; } } diff --git a/Cosette/Engine/Ai/Search/SearchContext.cs b/Cosette/Engine/Ai/Search/SearchContext.cs index 69891d1..6cc7f6c 100644 --- a/Cosette/Engine/Ai/Search/SearchContext.cs +++ b/Cosette/Engine/Ai/Search/SearchContext.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using Cosette.Engine.Board; using Cosette.Engine.Moves; @@ -8,6 +9,7 @@ public class SearchContext { public BoardState BoardState { get; set; } public SearchStatistics Statistics { get; set; } + public CancellationTokenSource HelperTasksCancellationTokenSource { get; set; } public int MaxDepth { get; set; } public int MaxTime { get; set; } diff --git a/Cosette/Engine/Ai/Search/SearchStatistics.cs b/Cosette/Engine/Ai/Search/SearchStatistics.cs index d984280..6ca808d 100644 --- a/Cosette/Engine/Ai/Search/SearchStatistics.cs +++ b/Cosette/Engine/Ai/Search/SearchStatistics.cs @@ -38,6 +38,8 @@ public class SearchStatistics public ulong TTReplacements { get; set; } public ulong TTHits { get; set; } public ulong TTNonHits { get; set; } + public ulong TTInvalidMoves { get; set; } + public ulong TTValidMoves { get; set; } public float TTHitsPercent => (float) TTHits * 100 / (TTHits + TTNonHits); public float TTReplacesPercent => (float) TTReplacements * 100 / TTAddedEntries; @@ -52,6 +54,10 @@ public class SearchStatistics public float BetaCutoffsAtFirstMovePercent => (float) BetaCutoffsAtFirstMove * 100 / (BetaCutoffsAtFirstMove + BetaCutoffsNotAtFirstMove); public float QBetaCutoffsAtFirstMovePercent => (float) QBetaCutoffsAtFirstMove * 100 / (QBetaCutoffsAtFirstMove + QBetaCutoffsNotAtFirstMove); + public int IIDHits { get; set; } + public int LoudMovesGenerated { get; set; } + public int QuietMovesGenerated { get; set; } + public Move[] PrincipalVariation { get; set; } public int PrincipalVariationMovesCount { get; set; } diff --git a/Cosette/Engine/Ai/Time/TimeScheduler.cs b/Cosette/Engine/Ai/Time/TimeScheduler.cs index ce7eb86..f3a29f3 100644 --- a/Cosette/Engine/Ai/Time/TimeScheduler.cs +++ b/Cosette/Engine/Ai/Time/TimeScheduler.cs @@ -6,7 +6,7 @@ public static class TimeScheduler { public static int CalculateTimeForMove(int remainingTime, int incTime, int moveNumber) { - return remainingTime / Math.Max(20, 40 - moveNumber) + (int)(incTime * 0.5); + return remainingTime / Math.Max(20, 40 - moveNumber) + (int)(incTime * 1.25f); } } } diff --git a/Cosette/Engine/Ai/Transposition/TranspositionTable.cs b/Cosette/Engine/Ai/Transposition/TranspositionTable.cs index f5df9b5..8f99224 100644 --- a/Cosette/Engine/Ai/Transposition/TranspositionTable.cs +++ b/Cosette/Engine/Ai/Transposition/TranspositionTable.cs @@ -17,9 +17,9 @@ public static unsafe void Init(int sizeMegabytes) _table = new TranspositionTableEntry[_size]; } - public static void Add(ulong hash, byte depth, short score, byte age, Move bestMove, TranspositionTableEntryFlags flags) + public static void Add(ulong hash, TranspositionTableEntry entry) { - _table[hash % _size] = new TranspositionTableEntry(hash, depth, score, age, bestMove, flags); + _table[hash % _size] = entry; } public static TranspositionTableEntry Get(ulong hash) diff --git a/Cosette/Engine/Ai/Transposition/TranspositionTableEntry.cs b/Cosette/Engine/Ai/Transposition/TranspositionTableEntry.cs index b4f076b..66c5740 100644 --- a/Cosette/Engine/Ai/Transposition/TranspositionTableEntry.cs +++ b/Cosette/Engine/Ai/Transposition/TranspositionTableEntry.cs @@ -5,26 +5,30 @@ namespace Cosette.Engine.Ai.Transposition { public struct TranspositionTableEntry { - public uint Key { get; set; } - public byte Depth { get; set; } + public ushort Key { get; set; } public short Score { get; set; } - public byte Age { get; set; } - public TranspositionTableEntryFlags Flags { get; set; } public Move BestMove { get; set; } + public byte Depth { get; set; } + + public byte Age => (byte)(_ageFlagsField >> 3); + public TranspositionTableEntryFlags Flags => (TranspositionTableEntryFlags)(_ageFlagsField & 7); + + private byte _ageFlagsField; - public TranspositionTableEntry(ulong hash, byte depth, short score, byte age, Move bestMove, TranspositionTableEntryFlags flags) + public TranspositionTableEntry(ulong hash, short score, Move bestMove, byte depth, TranspositionTableEntryFlags flags, byte age) { - Key = (uint)(hash >> 32); - Depth = depth; + Key = (ushort)(hash >> 48); Score = score; - Age = age; BestMove = bestMove; - Flags = flags; + Depth = depth; + + _ageFlagsField = (byte)flags; + _ageFlagsField |= (byte)(age << 3); } public bool IsKeyValid(ulong hash) { - return Key == (uint)(hash >> 32); + return Key == (ushort)(hash >> 48); } } } diff --git a/Cosette/Engine/Board/BoardConstants.cs b/Cosette/Engine/Board/BoardConstants.cs index 73c9783..80ca3c7 100644 --- a/Cosette/Engine/Board/BoardConstants.cs +++ b/Cosette/Engine/Board/BoardConstants.cs @@ -27,5 +27,10 @@ public static class BoardConstants public const ulong BoardWithoutEdges = Full & ~Edges; public const ulong RightLeftEdge = AFile | HFile; public const ulong TopBottomEdge = ARank | HRank; + + public const ulong NearPromotionAreaWhite = 0xffffff00000000; + public const ulong NearPromotionAreaBlack = 0xffffff00; + + public const int PhaseResolution = 1024; } } diff --git a/Cosette/Engine/Board/BoardState.cs b/Cosette/Engine/Board/BoardState.cs index b014752..0cfe2a3 100644 --- a/Cosette/Engine/Board/BoardState.cs +++ b/Cosette/Engine/Board/BoardState.cs @@ -1,4 +1,5 @@ using System; +using Cosette.Engine.Ai.Ordering; using Cosette.Engine.Ai.Score; using Cosette.Engine.Ai.Score.PieceSquareTables; using Cosette.Engine.Board.Operators; @@ -25,6 +26,7 @@ public class BoardState public int[][] Position { get; set; } public int[] PieceTable { get; set; } + public int MaterialAtOpening; public ulong Hash { get; set; } public ulong PawnHash { get; set; } @@ -37,7 +39,6 @@ public class BoardState private readonly FastStack _pawnHashes; private readonly FastStack _irreversibleMovesCounts; - private readonly int _materialAtOpening; public BoardState() { @@ -62,14 +63,6 @@ public BoardState() _hashes = new FastStack(512); _pawnHashes = new FastStack(512); _irreversibleMovesCounts = new FastStack(512); - - _materialAtOpening = - EvaluationConstants.Pieces[Piece.King] + - EvaluationConstants.Pieces[Piece.Queen] + - EvaluationConstants.Pieces[Piece.Rook] * 2 + - EvaluationConstants.Pieces[Piece.Bishop] * 2 + - EvaluationConstants.Pieces[Piece.Knight] * 2 + - EvaluationConstants.Pieces[Piece.Pawn] * 8; } public void SetDefaultState() @@ -112,45 +105,13 @@ public void SetDefaultState() Array.Fill(PieceTable, -1); - PieceTable[0] = Piece.Rook; - PieceTable[1] = Piece.Knight; - PieceTable[2] = Piece.Bishop; - PieceTable[3] = Piece.King; - PieceTable[4] = Piece.Queen; - PieceTable[5] = Piece.Bishop; - PieceTable[6] = Piece.Knight; - PieceTable[7] = Piece.Rook; - - PieceTable[8] = Piece.Pawn; - PieceTable[9] = Piece.Pawn; - PieceTable[10] = Piece.Pawn; - PieceTable[11] = Piece.Pawn; - PieceTable[12] = Piece.Pawn; - PieceTable[13] = Piece.Pawn; - PieceTable[14] = Piece.Pawn; - PieceTable[15] = Piece.Pawn; - - PieceTable[48] = Piece.Pawn; - PieceTable[49] = Piece.Pawn; - PieceTable[50] = Piece.Pawn; - PieceTable[51] = Piece.Pawn; - PieceTable[52] = Piece.Pawn; - PieceTable[53] = Piece.Pawn; - PieceTable[54] = Piece.Pawn; - PieceTable[55] = Piece.Pawn; - - PieceTable[56] = Piece.Rook; - PieceTable[57] = Piece.Knight; - PieceTable[58] = Piece.Bishop; - PieceTable[59] = Piece.King; - PieceTable[60] = Piece.Queen; - PieceTable[61] = Piece.Bishop; - PieceTable[62] = Piece.Knight; - PieceTable[63] = Piece.Rook; + CalculatePieceTable(PieceTable); Hash = ZobristHashing.CalculateHash(this); PawnHash = ZobristHashing.CalculatePawnHash(this); + MaterialAtOpening = CalculateMaterialAtOpening(); + _killedPieces.Clear(); _enPassants.Clear(); _castlings.Clear(); @@ -160,30 +121,69 @@ public void SetDefaultState() _irreversibleMovesCounts.Clear(); } - public int GetAvailableMoves(Span moves) + public int GetLoudMoves(Span moves, int offset) { - var movesCount = PawnOperator.GetAvailableMoves(this, ColorToMove, moves, 0); - movesCount = KnightOperator.GetAvailableMoves(this, ColorToMove, moves, movesCount); - movesCount = BishopOperator.GetAvailableMoves(this, ColorToMove, moves, movesCount); - movesCount = RookOperator.GetAvailableMoves(this, ColorToMove, moves, movesCount); - movesCount = QueenOperator.GetAvailableMoves(this, ColorToMove, moves, movesCount); - movesCount = KingOperator.GetAvailableMoves(this, ColorToMove, moves, movesCount); + var movesCount = PawnOperator.GetLoudMoves(this, moves, offset); + movesCount = KnightOperator.GetLoudMoves(this, moves, movesCount); + movesCount = BishopOperator.GetLoudMoves(this, moves, movesCount); + movesCount = RookOperator.GetLoudMoves(this, moves, movesCount); + movesCount = QueenOperator.GetLoudMoves(this, moves, movesCount); + movesCount = KingOperator.GetLoudMoves(this, moves, movesCount); return movesCount; } - public int GetAvailableQMoves(Span moves) + public int GetQuietMoves(Span moves, int offset) { - var movesCount = PawnOperator.GetAvailableQMoves(this, ColorToMove, moves, 0); - movesCount = KnightOperator.GetAvailableQMoves(this, ColorToMove, moves, movesCount); - movesCount = BishopOperator.GetAvailableQMoves(this, ColorToMove, moves, movesCount); - movesCount = RookOperator.GetAvailableQMoves(this, ColorToMove, moves, movesCount); - movesCount = QueenOperator.GetAvailableQMoves(this, ColorToMove, moves, movesCount); - movesCount = KingOperator.GetAvailableQMoves(this, ColorToMove, moves, movesCount); + var movesCount = PawnOperator.GetQuietMoves(this, moves, offset); + movesCount = KnightOperator.GetQuietMoves(this, moves, movesCount); + movesCount = BishopOperator.GetQuietMoves(this, moves, movesCount); + movesCount = RookOperator.GetQuietMoves(this, moves, movesCount); + movesCount = QueenOperator.GetQuietMoves(this, moves, movesCount); + movesCount = KingOperator.GetQuietMoves(this, moves, movesCount); return movesCount; } + public int GetAllMoves(Span moves) + { + var movesCount = GetLoudMoves(moves, 0); + return GetQuietMoves(moves, movesCount); + } + + public int GetAvailableCaptureMoves(Span moves) + { + var movesCount = PawnOperator.GetAvailableCaptureMoves(this, moves, 0); + movesCount = KnightOperator.GetAvailableCaptureMoves(this, moves, movesCount); + movesCount = BishopOperator.GetAvailableCaptureMoves(this, moves, movesCount); + movesCount = RookOperator.GetAvailableCaptureMoves(this, moves, movesCount); + movesCount = QueenOperator.GetAvailableCaptureMoves(this, moves, movesCount); + movesCount = KingOperator.GetAvailableCaptureMoves(this, moves, movesCount); + + return movesCount; + } + + public bool IsMoveLegal(Move move) + { + if (((1ul << move.From) & Occupancy[ColorToMove]) == 0) + { + return false; + } + + var fromPiece = PieceTable[move.From]; + switch (fromPiece) + { + case Piece.Pawn: return PawnOperator.IsMoveLegal(this, move); + case Piece.Knight: return KnightOperator.IsMoveLegal(this, move); + case Piece.Bishop: return BishopOperator.IsMoveLegal(this, move); + case Piece.Rook: return RookOperator.IsMoveLegal(this, move); + case Piece.Queen: return QueenOperator.IsMoveLegal(this, move); + case Piece.King: return KingOperator.IsMoveLegal(this, move); + } + + return false; + } + public void MakeMove(Move move) { var pieceType = PieceTable[move.From]; @@ -200,7 +200,7 @@ public void MakeMove(Move move) _enPassants.Push(EnPassant); _irreversibleMovesCounts.Push(IrreversibleMovesCount); - if (pieceType == Piece.Pawn || ((byte)move.Flags & MoveFlagFields.Capture) != 0) + if (pieceType == Piece.Pawn || move.IsCapture()) { IrreversibleMovesCount = 0; } @@ -253,7 +253,7 @@ public void MakeMove(Move move) _killedPieces.Push(killedPiece); } - else if (((byte)move.Flags & MoveFlagFields.Capture) != 0) + else if (move.IsCapture()) { var killedPiece = PieceTable[move.To]; @@ -269,29 +269,29 @@ public void MakeMove(Move move) switch (move.To) { case 0: - { - Hash = ZobristHashing.RemoveCastlingFlag(Hash, Castling, Castling.WhiteShort); - Castling &= ~Castling.WhiteShort; - break; - } + { + Hash = ZobristHashing.RemoveCastlingFlag(Hash, Castling, Castling.WhiteShort); + Castling &= ~Castling.WhiteShort; + break; + } case 7: - { - Hash = ZobristHashing.RemoveCastlingFlag(Hash, Castling, Castling.WhiteLong); - Castling &= ~Castling.WhiteLong; - break; - } + { + Hash = ZobristHashing.RemoveCastlingFlag(Hash, Castling, Castling.WhiteLong); + Castling &= ~Castling.WhiteLong; + break; + } case 56: - { - Hash = ZobristHashing.RemoveCastlingFlag(Hash, Castling, Castling.BlackShort); - Castling &= ~Castling.BlackShort; - break; - } + { + Hash = ZobristHashing.RemoveCastlingFlag(Hash, Castling, Castling.BlackShort); + Castling &= ~Castling.BlackShort; + break; + } case 63: - { - Hash = ZobristHashing.RemoveCastlingFlag(Hash, Castling, Castling.BlackLong); - Castling &= ~Castling.BlackLong; - break; - } + { + Hash = ZobristHashing.RemoveCastlingFlag(Hash, Castling, Castling.BlackLong); + Castling &= ~Castling.BlackLong; + break; + } } } @@ -455,7 +455,7 @@ public void UndoMove(Move move) MovePiece(ColorToMove, Piece.Pawn, move.To, move.From); AddPiece(enemyColor, killedPiece, enemyPieceField); } - else if (((byte)move.Flags & MoveFlagFields.Capture) != 0) + else if (move.IsCapture()) { var enemyColor = ColorOperations.Invert(ColorToMove); var killedPiece = _killedPieces.Pop(); @@ -612,38 +612,41 @@ public byte GetAttackingPiecesWithColor(int color, int fieldIndex) { byte result = 0; - var fileRankAttacks = RookMovesGenerator.GetMoves(OccupancySummary, fieldIndex) & Occupancy[color]; - var attackingRooks = fileRankAttacks & Pieces[color][Piece.Rook]; - if (attackingRooks != 0) + var jumpAttacks = KnightMovesGenerator.GetMoves(fieldIndex); + var attackingKnights = jumpAttacks & Pieces[color][Piece.Knight]; + var attackingKnightsCount = BitOperations.Count(attackingKnights); + if (attackingKnightsCount != 0) { - result |= 1 << Piece.Rook; + result |= (byte)((attackingKnightsCount == 1 ? 1 : 3) << SeePiece.Knight1); } var diagonalAttacks = BishopMovesGenerator.GetMoves(OccupancySummary, fieldIndex) & Occupancy[color]; var attackingBishops = diagonalAttacks & Pieces[color][Piece.Bishop]; if (attackingBishops != 0) { - result |= 1 << Piece.Bishop; + result |= 1 << SeePiece.Bishop; } - var attackingQueens = (fileRankAttacks | diagonalAttacks) & Pieces[color][Piece.Queen]; - if (attackingQueens != 0) + var occupancyWithoutFileRantPieces = OccupancySummary & ~Pieces[color][Piece.Rook] & ~Pieces[color][Piece.Queen]; + var fileRankAttacks = RookMovesGenerator.GetMoves(occupancyWithoutFileRantPieces, fieldIndex) & Occupancy[color]; + var attackingRooks = fileRankAttacks & Pieces[color][Piece.Rook]; + var attackingRooksCount = BitOperations.Count(attackingRooks); + if (attackingRooksCount != 0) { - result |= 1 << Piece.Queen; + result |= (byte)((attackingRooksCount == 1 ? 1 : 3) << SeePiece.Rook1); } - var jumpAttacks = KnightMovesGenerator.GetMoves(fieldIndex); - var attackingKnights = jumpAttacks & Pieces[color][Piece.Knight]; - if (attackingKnights != 0) + var attackingQueens = (fileRankAttacks | diagonalAttacks) & Pieces[color][Piece.Queen]; + if (attackingQueens != 0) { - result |= 1 << Piece.Knight; + result |= 1 << SeePiece.Queen; } var boxAttacks = KingMovesGenerator.GetMoves(fieldIndex); var attackingKings = boxAttacks & Pieces[color][Piece.King]; if (attackingKings != 0) { - result |= 1 << Piece.King; + result |= 1 << SeePiece.King; } var field = 1ul << fieldIndex; @@ -654,7 +657,7 @@ public byte GetAttackingPiecesWithColor(int color, int fieldIndex) if (attackingPawns != 0) { - result |= 1 << Piece.Pawn; + result |= 1 << SeePiece.Pawn; } return result; @@ -679,7 +682,7 @@ public void MovePiece(int color, int piece, int from, int to) Pieces[color][piece] ^= move; Occupancy[color] ^= move; OccupancySummary ^= move; - + Position[color][GamePhase.Opening] -= PieceSquareTablesData.Values[piece][color][GamePhase.Opening][from]; Position[color][GamePhase.Opening] += PieceSquareTablesData.Values[piece][color][GamePhase.Opening][to]; @@ -754,15 +757,14 @@ public int CalculatePosition(int color, int phase) return result; } - public float GetPhaseRatio() + public int GetPhaseRatio() { var materialOfWeakerSide = Math.Min(Material[Color.White], Material[Color.Black]); - var openingDelta = _materialAtOpening - EvaluationConstants.OpeningEndgameEdge; + var openingDelta = MaterialAtOpening - EvaluationConstants.OpeningEndgameEdge; var boardDelta = materialOfWeakerSide - EvaluationConstants.OpeningEndgameEdge; - var ratio = (float) boardDelta / openingDelta; - return Math.Max(0, ratio); + return Math.Max(boardDelta, 0) * BoardConstants.PhaseResolution / openingDelta; } public int GetGamePhase() @@ -797,6 +799,48 @@ public bool IsFiftyMoveRuleDraw() return false; } + public bool IsInsufficientMaterial() + { + var drawEdge = EvaluationConstants.Pieces[Piece.King] + 4 * EvaluationConstants.Pieces[Piece.Pawn]; + if (Material[Color.White] < drawEdge && Material[Color.Black] < drawEdge) + { + if (Pieces[Color.White][Piece.Pawn] == 0 && Pieces[Color.Black][Piece.Pawn] == 0) + { + return true; + } + } + + return false; + } + + public void CalculatePieceTable(int[] pieceTable) + { + for (var fieldIndex = 0; fieldIndex < 64; fieldIndex++) + { + for (var pieceIndex = 0; pieceIndex < 6; pieceIndex++) + { + var bitboard = Pieces[Color.White][pieceIndex] | Pieces[Color.Black][pieceIndex]; + pieceTable[fieldIndex] = -1; + + if ((bitboard & (1ul << fieldIndex)) != 0) + { + pieceTable[fieldIndex] = pieceIndex; + break; + } + } + } + } + + public int CalculateMaterialAtOpening() + { + return EvaluationConstants.Pieces[Piece.King] + + EvaluationConstants.Pieces[Piece.Queen] + + EvaluationConstants.Pieces[Piece.Rook] * 2 + + EvaluationConstants.Pieces[Piece.Bishop] * 2 + + EvaluationConstants.Pieces[Piece.Knight] * 2 + + EvaluationConstants.Pieces[Piece.Pawn] * 8; + } + public override string ToString() { return BoardToFen.Parse(this); diff --git a/Cosette/Engine/Board/Operators/BishopOperator.cs b/Cosette/Engine/Board/Operators/BishopOperator.cs index 5b449f5..420b4e1 100644 --- a/Cosette/Engine/Board/Operators/BishopOperator.cs +++ b/Cosette/Engine/Board/Operators/BishopOperator.cs @@ -1,4 +1,5 @@ using System; +using Cosette.Engine.Ai.Score; using Cosette.Engine.Common; using Cosette.Engine.Moves; @@ -6,8 +7,9 @@ namespace Cosette.Engine.Board.Operators { public static class BishopOperator { - public static int GetAvailableMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetLoudMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var bishops = boardState.Pieces[color][Piece.Bishop]; @@ -17,7 +19,34 @@ public static int GetAvailableMoves(BoardState boardState, int color, Span bishops = BitOperations.PopLsb(bishops); var from = BitOperations.BitScan(piece); - var availableMoves = BishopMovesGenerator.GetMoves(boardState.OccupancySummary, from) & ~boardState.Occupancy[color]; + var availableMoves = BishopMovesGenerator.GetMoves(boardState.OccupancySummary, from) & boardState.Occupancy[enemyColor]; + + while (availableMoves != 0) + { + var field = BitOperations.GetLsb(availableMoves); + var fieldIndex = BitOperations.BitScan(field); + availableMoves = BitOperations.PopLsb(availableMoves); + + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Capture); + } + } + + return offset; + } + + public static int GetQuietMoves(BoardState boardState, Span moves, int offset) + { + var color = boardState.ColorToMove; + var enemyColor = ColorOperations.Invert(color); + var bishops = boardState.Pieces[color][Piece.Bishop]; + + while (bishops != 0) + { + var piece = BitOperations.GetLsb(bishops); + bishops = BitOperations.PopLsb(bishops); + + var from = BitOperations.BitScan(piece); + var availableMoves = BishopMovesGenerator.GetMoves(boardState.OccupancySummary, from) & ~boardState.OccupancySummary; while (availableMoves != 0) { @@ -25,16 +54,16 @@ public static int GetAvailableMoves(BoardState boardState, int color, Span var fieldIndex = BitOperations.BitScan(field); availableMoves = BitOperations.PopLsb(availableMoves); - var flags = (field & boardState.Occupancy[enemyColor]) != 0 ? MoveFlags.Capture : MoveFlags.Quiet; - moves[offset++] = new Move(from, fieldIndex, flags); + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Quiet); } } return offset; } - public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetAvailableCaptureMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var bishops = boardState.Pieces[color][Piece.Bishop]; @@ -59,9 +88,12 @@ public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetLoudMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var piece = boardState.Pieces[color][Piece.King]; + if (piece == 0) + { + return offset; + } + var from = BitOperations.BitScan(piece); - var availableMoves = KingMovesGenerator.GetMoves(from) & ~boardState.Occupancy[color]; + var availableMoves = KingMovesGenerator.GetMoves(from) & boardState.Occupancy[enemyColor]; while (availableMoves != 0) { @@ -20,55 +26,74 @@ public static int GetAvailableMoves(BoardState boardState, int color, Span var fieldIndex = BitOperations.BitScan(field); availableMoves = BitOperations.PopLsb(availableMoves); - var flags = (field & boardState.Occupancy[enemyColor]) != 0 ? MoveFlags.Capture : MoveFlags.Quiet; - moves[offset++] = new Move(from, fieldIndex, flags); + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Capture); } if (color == Color.White) { - if ((boardState.Castling & Castling.WhiteShort) != 0 && (boardState.OccupancySummary & 6) == 0) + if (IsWhiteKingCastlingAvailable(boardState, color)) { - if (!boardState.IsFieldAttacked(color, 1) && !boardState.IsFieldAttacked(color, 2) && !boardState.IsFieldAttacked(color, 3)) - { - moves[offset++] = new Move(3, 1, MoveFlags.KingCastle); - } + moves[offset++] = new Move(3, 1, MoveFlags.KingCastle); } - - if ((boardState.Castling & Castling.WhiteLong) != 0 && (boardState.OccupancySummary & 112) == 0) + + if (IsWhiteQueenCastlingAvailable(boardState, color)) { - if (!boardState.IsFieldAttacked(color, 3) && !boardState.IsFieldAttacked(color, 4) && !boardState.IsFieldAttacked(color, 5)) - { - moves[offset++] = new Move(3, 5, MoveFlags.QueenCastle); - } + moves[offset++] = new Move(3, 5, MoveFlags.QueenCastle); } } else { - if ((boardState.Castling & Castling.BlackShort) != 0 && (boardState.OccupancySummary & 432345564227567616) == 0) + if (IsBlackKingCastlingAvailable(boardState, color)) { - if (!boardState.IsFieldAttacked(color, 57) && !boardState.IsFieldAttacked(color, 58) && !boardState.IsFieldAttacked(color, 59)) - { - moves[offset++] = new Move(59, 57, MoveFlags.KingCastle); - } + moves[offset++] = new Move(59, 57, MoveFlags.KingCastle); } - - if ((boardState.Castling & Castling.BlackLong) != 0 && (boardState.OccupancySummary & 8070450532247928832) == 0) + + if (IsBlackQueenCastlingAvailable(boardState, color)) { - if (!boardState.IsFieldAttacked(color, 59) && !boardState.IsFieldAttacked(color, 60) && !boardState.IsFieldAttacked(color, 61)) - { - moves[offset++] = new Move(59, 61, MoveFlags.QueenCastle); - } + moves[offset++] = new Move(59, 61, MoveFlags.QueenCastle); } } return offset; } - public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetQuietMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var piece = boardState.Pieces[color][Piece.King]; + if (piece == 0) + { + return offset; + } + + var from = BitOperations.BitScan(piece); + var availableMoves = KingMovesGenerator.GetMoves(from) & ~boardState.OccupancySummary; + + while (availableMoves != 0) + { + var field = BitOperations.GetLsb(availableMoves); + var fieldIndex = BitOperations.BitScan(field); + availableMoves = BitOperations.PopLsb(availableMoves); + + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Quiet); + } + + return offset; + } + + public static int GetAvailableCaptureMoves(BoardState boardState, Span moves, int offset) + { + var color = boardState.ColorToMove; + var enemyColor = ColorOperations.Invert(color); + var piece = boardState.Pieces[color][Piece.King]; + + if (piece == 0) + { + return offset; + } + var from = BitOperations.BitScan(piece); var availableMoves = KingMovesGenerator.GetMoves(from) & boardState.Occupancy[enemyColor]; @@ -83,5 +108,100 @@ public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetLoudMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var knights = boardState.Pieces[color][Piece.Knight]; @@ -17,7 +19,34 @@ public static int GetAvailableMoves(BoardState boardState, int color, Span knights = BitOperations.PopLsb(knights); var from = BitOperations.BitScan(piece); - var availableMoves = KnightMovesGenerator.GetMoves(from) & ~boardState.Occupancy[color]; + var availableMoves = KnightMovesGenerator.GetMoves(from) & boardState.Occupancy[enemyColor]; + + while (availableMoves != 0) + { + var field = BitOperations.GetLsb(availableMoves); + var fieldIndex = BitOperations.BitScan(field); + availableMoves = BitOperations.PopLsb(availableMoves); + + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Capture); + } + } + + return offset; + } + + public static int GetQuietMoves(BoardState boardState, Span moves, int offset) + { + var color = boardState.ColorToMove; + var enemyColor = ColorOperations.Invert(color); + var knights = boardState.Pieces[color][Piece.Knight]; + + while (knights != 0) + { + var piece = BitOperations.GetLsb(knights); + knights = BitOperations.PopLsb(knights); + + var from = BitOperations.BitScan(piece); + var availableMoves = KnightMovesGenerator.GetMoves(from) & ~boardState.OccupancySummary; while (availableMoves != 0) { @@ -25,16 +54,16 @@ public static int GetAvailableMoves(BoardState boardState, int color, Span var fieldIndex = BitOperations.BitScan(field); availableMoves = BitOperations.PopLsb(availableMoves); - var flags = (field & boardState.Occupancy[enemyColor]) != 0 ? MoveFlags.Capture : MoveFlags.Quiet; - moves[offset++] = new Move(from, fieldIndex, flags); + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Quiet); } } return offset; } - public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetAvailableCaptureMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var knights = boardState.Pieces[color][Piece.Knight]; @@ -59,9 +88,12 @@ public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetLoudMoves(BoardState boardState, Span moves, int offset) { - offset = GetSinglePush(boardState, color, moves, offset); - offset = GetDoublePush(boardState, color, moves, offset); - offset = GetDiagonalAttacks(boardState, color, color == Color.White ? 9 : 7, BoardConstants.AFile, moves, offset); - offset = GetDiagonalAttacks(boardState, color, color == Color.White ? 7 : 9, BoardConstants.HFile, moves, offset); + var color = boardState.ColorToMove; + + offset = GetSinglePush(boardState, moves, offset, true); + offset = GetDiagonalAttacks(boardState, color == Color.White ? 9 : 7, BoardConstants.AFile, moves, offset); + offset = GetDiagonalAttacks(boardState, color == Color.White ? 7 : 9, BoardConstants.HFile, moves, offset); return offset; } - public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetQuietMoves(BoardState boardState, Span moves, int offset) { - offset = GetDiagonalAttacks(boardState, color, color == Color.White ? 9 : 7, BoardConstants.AFile, moves, offset); - offset = GetDiagonalAttacks(boardState, color, color == Color.White ? 7 : 9, BoardConstants.HFile, moves, offset); + offset = GetSinglePush(boardState, moves, offset, false); + offset = GetDoublePush(boardState, moves, offset); + + return offset; + } + + public static int GetAvailableCaptureMoves(BoardState boardState, Span moves, int offset) + { + var color = boardState.ColorToMove; + + offset = GetDiagonalAttacks(boardState, color == Color.White ? 9 : 7, BoardConstants.AFile, moves, offset); + offset = GetDiagonalAttacks(boardState, color == Color.White ? 7 : 9, BoardConstants.HFile, moves, offset); return offset; } - private static int GetSinglePush(BoardState boardState, int color, Span moves, int offset) + public static bool IsMoveLegal(BoardState boardState, Move move) + { + var enemyColor = ColorOperations.Invert(boardState.ColorToMove); + var toField = 1ul << move.To; + + if (!move.IsCapture()) + { + if (move.Flags == MoveFlags.Quiet || ((int)move.Flags & MoveFlagFields.Promotion) != 0) + { + if ((boardState.OccupancySummary & toField) == 0) + { + return true; + } + } + else if (move.Flags == MoveFlags.DoublePush) + { + var middleField = 1ul << ((move.From + move.To) / 2); + if ((boardState.OccupancySummary & middleField) == 0 && (boardState.OccupancySummary & toField) == 0) + { + return true; + } + } + } + else + { + if (move.Flags == MoveFlags.EnPassant) + { + if ((boardState.EnPassant & toField) != 0) + { + return true; + } + } + else + { + var difference = move.To - move.From; + var colorDifference = -(boardState.ColorToMove * 2 - 1) * difference; + + if ((boardState.Occupancy[enemyColor] & toField) != 0 && (colorDifference == 7 || colorDifference == 9)) + { + return true; + } + } + } + + return false; + } + + private static int GetSinglePush(BoardState boardState, Span moves, int offset, bool promotionsMode) { int shift; ulong promotionRank, pawns; + var color = boardState.ColorToMove; if (color == Color.White) { shift = 8; promotionRank = BoardConstants.HRank; pawns = boardState.Pieces[Color.White][Piece.Pawn]; + + if (promotionsMode) + { + pawns &= BoardConstants.NearPromotionAreaWhite; + } + else + { + pawns &= ~BoardConstants.NearPromotionAreaWhite; + } + pawns = (pawns << 8) & ~boardState.OccupancySummary; } else @@ -41,6 +110,16 @@ private static int GetSinglePush(BoardState boardState, int color, Span mo shift = -8; promotionRank = BoardConstants.ARank; pawns = boardState.Pieces[Color.Black][Piece.Pawn]; + + if (promotionsMode) + { + pawns &= BoardConstants.NearPromotionAreaBlack; + } + else + { + pawns &= ~BoardConstants.NearPromotionAreaBlack; + } + pawns = (pawns >> 8) & ~boardState.OccupancySummary; } @@ -52,12 +131,13 @@ private static int GetSinglePush(BoardState boardState, int color, Span mo var from = BitOperations.BitScan(piece) - shift; var to = BitOperations.BitScan(piece); - if ((piece & promotionRank) != 0) + if (promotionsMode && (piece & promotionRank) != 0) { moves[offset++] = new Move(from, to, MoveFlags.QueenPromotion); moves[offset++] = new Move(from, to, MoveFlags.RookPromotion); moves[offset++] = new Move(from, to, MoveFlags.KnightPromotion); moves[offset++] = new Move(from, to, MoveFlags.BishopPromotion); + } else { @@ -68,10 +148,11 @@ private static int GetSinglePush(BoardState boardState, int color, Span mo return offset; } - private static int GetDoublePush(BoardState boardState, int color, Span moves, int offset) + private static int GetDoublePush(BoardState boardState, Span moves, int offset) { int shift; ulong startRank, pawns; + var color = boardState.ColorToMove; if (color == Color.White) { @@ -104,10 +185,11 @@ private static int GetDoublePush(BoardState boardState, int color, Span mo return offset; } - private static int GetDiagonalAttacks(BoardState boardState, int color, int dir, ulong prohibitedFile, Span moves, int offset) + private static int GetDiagonalAttacks(BoardState boardState, int dir, ulong prohibitedFile, Span moves, int offset) { int shift; ulong promotionRank, enemyOccupancy, pawns; + var color = boardState.ColorToMove; if (color == Color.White) { diff --git a/Cosette/Engine/Board/Operators/QueenOperator.cs b/Cosette/Engine/Board/Operators/QueenOperator.cs index 08f8bd8..64e41cf 100644 --- a/Cosette/Engine/Board/Operators/QueenOperator.cs +++ b/Cosette/Engine/Board/Operators/QueenOperator.cs @@ -1,4 +1,5 @@ using System; +using Cosette.Engine.Ai.Score; using Cosette.Engine.Common; using Cosette.Engine.Moves; @@ -6,8 +7,9 @@ namespace Cosette.Engine.Board.Operators { public static class QueenOperator { - public static int GetAvailableMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetLoudMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var queens = boardState.Pieces[color][Piece.Queen]; @@ -17,7 +19,34 @@ public static int GetAvailableMoves(BoardState boardState, int color, Span queens = BitOperations.PopLsb(queens); var from = BitOperations.BitScan(piece); - var availableMoves = QueenMovesGenerator.GetMoves(boardState.OccupancySummary, from) & ~boardState.Occupancy[color]; + var availableMoves = QueenMovesGenerator.GetMoves(boardState.OccupancySummary, from) & boardState.Occupancy[enemyColor]; + + while (availableMoves != 0) + { + var field = BitOperations.GetLsb(availableMoves); + var fieldIndex = BitOperations.BitScan(field); + availableMoves = BitOperations.PopLsb(availableMoves); + + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Capture); + } + } + + return offset; + } + + public static int GetQuietMoves(BoardState boardState, Span moves, int offset) + { + var color = boardState.ColorToMove; + var enemyColor = ColorOperations.Invert(color); + var queens = boardState.Pieces[color][Piece.Queen]; + + while (queens != 0) + { + var piece = BitOperations.GetLsb(queens); + queens = BitOperations.PopLsb(queens); + + var from = BitOperations.BitScan(piece); + var availableMoves = QueenMovesGenerator.GetMoves(boardState.OccupancySummary, from) & ~boardState.OccupancySummary; while (availableMoves != 0) { @@ -25,16 +54,16 @@ public static int GetAvailableMoves(BoardState boardState, int color, Span var fieldIndex = BitOperations.BitScan(field); availableMoves = BitOperations.PopLsb(availableMoves); - var flags = (field & boardState.Occupancy[enemyColor]) != 0 ? MoveFlags.Capture : MoveFlags.Quiet; - moves[offset++] = new Move(from, fieldIndex, flags); + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Quiet); } } return offset; } - public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetAvailableCaptureMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var queens = boardState.Pieces[color][Piece.Queen]; @@ -59,9 +88,12 @@ public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetLoudMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var rooks = boardState.Pieces[color][Piece.Rook]; @@ -17,7 +19,34 @@ public static int GetAvailableMoves(BoardState boardState, int color, Span rooks = BitOperations.PopLsb(rooks); var from = BitOperations.BitScan(piece); - var availableMoves = RookMovesGenerator.GetMoves(boardState.OccupancySummary, from) & ~boardState.Occupancy[color]; + var availableMoves = RookMovesGenerator.GetMoves(boardState.OccupancySummary, from) & boardState.Occupancy[enemyColor]; + + while (availableMoves != 0) + { + var field = BitOperations.GetLsb(availableMoves); + var fieldIndex = BitOperations.BitScan(field); + availableMoves = BitOperations.PopLsb(availableMoves); + + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Capture); + } + } + + return offset; + } + + public static int GetQuietMoves(BoardState boardState, Span moves, int offset) + { + var color = boardState.ColorToMove; + var enemyColor = ColorOperations.Invert(color); + var rooks = boardState.Pieces[color][Piece.Rook]; + + while (rooks != 0) + { + var piece = BitOperations.GetLsb(rooks); + rooks = BitOperations.PopLsb(rooks); + + var from = BitOperations.BitScan(piece); + var availableMoves = RookMovesGenerator.GetMoves(boardState.OccupancySummary, from) & ~boardState.OccupancySummary; while (availableMoves != 0) { @@ -25,16 +54,16 @@ public static int GetAvailableMoves(BoardState boardState, int color, Span var fieldIndex = BitOperations.BitScan(field); availableMoves = BitOperations.PopLsb(availableMoves); - var flags = (field & boardState.Occupancy[enemyColor]) != 0 ? MoveFlags.Capture : MoveFlags.Quiet; - moves[offset++] = new Move(from, fieldIndex, flags); + moves[offset++] = new Move(from, fieldIndex, MoveFlags.Quiet); } } return offset; } - public static int GetAvailableQMoves(BoardState boardState, int color, Span moves, int offset) + public static int GetAvailableCaptureMoves(BoardState boardState, Span moves, int offset) { + var color = boardState.ColorToMove; var enemyColor = ColorOperations.Invert(color); var rooks = boardState.Pieces[color][Piece.Rook]; @@ -59,9 +88,12 @@ public static int GetAvailableQMoves(BoardState boardState, int color, Span 4 ? int.Parse(split[4]) : 0; + var movesCount = split.Length > 5 ? int.Parse(split[5]) : 0; var result = new BoardState(); var currentColor = ParseCurrentColor(colorState); @@ -31,6 +31,9 @@ public static BoardState Parse(string fen) result.Position[Color.Black][GamePhase.Opening] = result.CalculatePosition(Color.Black, GamePhase.Opening); result.Position[Color.Black][GamePhase.Ending] = result.CalculatePosition(Color.Black, GamePhase.Ending); + result.CalculatePieceTable(result.PieceTable); + result.MaterialAtOpening = result.CalculateMaterialAtOpening(); + result.MovesCount = movesCount; result.IrreversibleMovesCount = halfmoveClock; result.ColorToMove = currentColor; diff --git a/Cosette/Engine/Moves/Move.cs b/Cosette/Engine/Moves/Move.cs index 387a99d..923b6f9 100644 --- a/Cosette/Engine/Moves/Move.cs +++ b/Cosette/Engine/Moves/Move.cs @@ -58,7 +58,7 @@ public static Move FromTextNotation(BoardState board, string textNotation) var flags = textNotation.Length == 5 ? GetMoveFlags(textNotation[4]) : MoveFlags.Quiet; Span moves = stackalloc Move[SearchConstants.MaxMovesCount]; - var movesCount = board.GetAvailableMoves(moves); + var movesCount = board.GetAllMoves(moves); for (var i = 0; i < movesCount; i++) { @@ -79,6 +79,11 @@ public bool IsQuiet() return Flags == MoveFlags.Quiet || Flags == MoveFlags.DoublePush; } + public bool IsCapture() + { + return ((int)Flags & MoveFlagFields.Capture) != 0; + } + public override string ToString() { var baseMove = $"{Position.FromFieldIndex(From)}{Position.FromFieldIndex(To)}"; diff --git a/Cosette/Engine/Perft/AdvancedPerft.cs b/Cosette/Engine/Perft/AdvancedPerft.cs index f0f3728..4702ab2 100644 --- a/Cosette/Engine/Perft/AdvancedPerft.cs +++ b/Cosette/Engine/Perft/AdvancedPerft.cs @@ -23,7 +23,7 @@ public static AdvancedPerftResult Run(BoardState boardState, int depth) private static void Perft(BoardState boardState, int depth, AdvancedPerftResult result) { Span moves = stackalloc Move[SearchConstants.MaxMovesCount]; - var movesCount = boardState.GetAvailableMoves(moves); + var movesCount = boardState.GetAllMoves(moves); if (depth <= 0) { @@ -57,7 +57,7 @@ private static void UpdateResult(BoardState boardState, Span moves, int mo if (!boardState.IsKingChecked(ColorOperations.Invert(boardState.ColorToMove))) { - if (((byte)moves[i].Flags & MoveFlagFields.Capture) != 0) + if (moves[i].IsCapture()) { result.Captures++; } diff --git a/Cosette/Engine/Perft/DividedPerft.cs b/Cosette/Engine/Perft/DividedPerft.cs index cc93682..a0f9152 100644 --- a/Cosette/Engine/Perft/DividedPerft.cs +++ b/Cosette/Engine/Perft/DividedPerft.cs @@ -12,7 +12,7 @@ public static class DividedPerft public static DividedPerftResult Run(BoardState boardState, int depth) { Span moves = stackalloc Move[SearchConstants.MaxMovesCount]; - var movesCount = boardState.GetAvailableMoves(moves); + var movesCount = boardState.GetAllMoves(moves); var result = new DividedPerftResult(); if (depth <= 0) @@ -44,7 +44,7 @@ private static ulong Perft(BoardState boardState, int depth) } Span moves = stackalloc Move[SearchConstants.MaxMovesCount]; - var movesCount = boardState.GetAvailableMoves(moves); + var movesCount = boardState.GetAllMoves(moves); ulong nodes = 0; for (var i = 0; i < movesCount; i++) diff --git a/Cosette/Engine/Perft/SimplePerft.cs b/Cosette/Engine/Perft/SimplePerft.cs index 84e97d1..a10eb07 100644 --- a/Cosette/Engine/Perft/SimplePerft.cs +++ b/Cosette/Engine/Perft/SimplePerft.cs @@ -34,7 +34,7 @@ private static ulong Perft(BoardState boardState, int depth) } Span moves = stackalloc Move[SearchConstants.MaxMovesCount]; - var movesCount = boardState.GetAvailableMoves(moves); + var movesCount = boardState.GetAllMoves(moves); ulong nodes = 0; for (var i = 0; i < movesCount; i++) diff --git a/Cosette/Engine/Perft/VerificationPerft.cs b/Cosette/Engine/Perft/VerificationPerft.cs index f9fbadf..ad85ce4 100644 --- a/Cosette/Engine/Perft/VerificationPerft.cs +++ b/Cosette/Engine/Perft/VerificationPerft.cs @@ -26,6 +26,7 @@ private static ulong Perft(BoardState boardState, int depth, ref bool verificati if (!VerifyBoard(boardState)) { verificationSuccess = false; + return 0; } if (depth <= 0) @@ -34,7 +35,7 @@ private static ulong Perft(BoardState boardState, int depth, ref bool verificati } Span moves = stackalloc Move[SearchConstants.MaxMovesCount]; - var movesCount = boardState.GetAvailableMoves(moves); + var movesCount = boardState.GetAllMoves(moves); ulong nodes = 0; for (var i = 0; i < movesCount; i++) @@ -79,6 +80,17 @@ private static bool VerifyBoard(BoardState board) return false; } + var pieceTable = new int[64]; + board.CalculatePieceTable(pieceTable); + + for (var fieldIndex = 0; fieldIndex < 64; fieldIndex++) + { + if (board.PieceTable[fieldIndex] != pieceTable[fieldIndex]) + { + return false; + } + } + return true; } } diff --git a/Cosette/Interactive/Commands/BenchmarkCommand.cs b/Cosette/Interactive/Commands/BenchmarkCommand.cs index a3d6d8a..81883ed 100644 --- a/Cosette/Interactive/Commands/BenchmarkCommand.cs +++ b/Cosette/Interactive/Commands/BenchmarkCommand.cs @@ -120,6 +120,10 @@ private void IterativeDeepening_OnSearchUpdate(object sender, SearchStatistics s $"Hits: {statistics.EvaluationStatistics.EHTHits} ({statistics.EvaluationStatistics.EHTHitsPercent:F} %), " + $"Missed: {statistics.EvaluationStatistics.EHTNonHits}, " + $"Filled: {EvaluationHashTable.GetFillLevel():F} %"); + + _interactiveConsole.WriteLine($" Valid TT moves: {statistics.TTValidMoves}, Invalid TT moves: {statistics.TTInvalidMoves}, " + + $"IID hits: {statistics.IIDHits}, Loud generations: {statistics.LoudMovesGenerated}, " + + $"Quiet generations: {statistics.QuietMovesGenerated}"); #endif diff --git a/Cosette/Interactive/Commands/EvaluateCommand.cs b/Cosette/Interactive/Commands/EvaluateCommand.cs index 38b2277..6584cec 100644 --- a/Cosette/Interactive/Commands/EvaluateCommand.cs +++ b/Cosette/Interactive/Commands/EvaluateCommand.cs @@ -1,6 +1,7 @@ using System; using Cosette.Engine.Ai.Score; using Cosette.Engine.Ai.Score.Evaluators; +using Cosette.Engine.Board; using Cosette.Engine.Fen; namespace Cosette.Interactive.Commands @@ -23,20 +24,24 @@ public void Run(params string[] parameters) var evaluationStatistics = new EvaluationStatistics(); var openingPhase = boardState.GetPhaseRatio(); - var endingPhase = 1 - openingPhase; + var endingPhase = BoardConstants.PhaseResolution - openingPhase; + + var fieldsAttackedByWhite = 0ul; + var fieldsAttackedByBlack = 0ul; var materialEvaluation = MaterialEvaluator.Evaluate(boardState); var castlingEvaluation = CastlingEvaluator.Evaluate(boardState, openingPhase, endingPhase); var positionEvaluation = PositionEvaluator.Evaluate(boardState, openingPhase, endingPhase); var pawnStructureEvaluation = PawnStructureEvaluator.Evaluate(boardState, evaluationStatistics, openingPhase, endingPhase); - var mobility = MobilityEvaluator.Evaluate(boardState, openingPhase, endingPhase); - var kingSafety = KingSafetyEvaluator.Evaluate(boardState, openingPhase, endingPhase); + var mobility = MobilityEvaluator.Evaluate(boardState, openingPhase, endingPhase, ref fieldsAttackedByWhite, ref fieldsAttackedByBlack); + var kingSafety = KingSafetyEvaluator.Evaluate(boardState, openingPhase, endingPhase, fieldsAttackedByWhite, fieldsAttackedByBlack); var pieces = PiecesEvaluator.Evaluate(boardState, openingPhase, endingPhase); + var fianchetto = FianchettoEvaluator.Evaluate(boardState, openingPhase, endingPhase); var total = materialEvaluation + castlingEvaluation + positionEvaluation + pawnStructureEvaluation + - mobility + kingSafety + pieces; + mobility + kingSafety + pieces + fianchetto; - _interactiveConsole.WriteLine($"Evaluation for board with hash {boardState.Hash} (phase {openingPhase:F}, " + + _interactiveConsole.WriteLine($"Evaluation for board with hash {boardState.Hash} (phase {openingPhase}, " + $"{boardState.IrreversibleMovesCount} irreversible moves)"); _interactiveConsole.WriteLine($" = Material: {materialEvaluation}"); @@ -46,6 +51,7 @@ public void Run(params string[] parameters) _interactiveConsole.WriteLine($" = Mobility: {mobility}"); _interactiveConsole.WriteLine($" = King safety: {kingSafety}"); _interactiveConsole.WriteLine($" = Pieces evaluation: {pieces}"); + _interactiveConsole.WriteLine($" = Fianchetto evaluation: {fianchetto}"); _interactiveConsole.WriteLine(); _interactiveConsole.WriteLine($" = Total: {total}"); } diff --git a/Cosette/Interactive/InteractiveConsole.cs b/Cosette/Interactive/InteractiveConsole.cs index 8ddadac..8dab1d6 100644 --- a/Cosette/Interactive/InteractiveConsole.cs +++ b/Cosette/Interactive/InteractiveConsole.cs @@ -1,7 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; using System.Text.RegularExpressions; using Cosette.Interactive.Commands; @@ -111,13 +116,45 @@ public void WriteLine(string message) private void DisplayIntro() { var runtimeVersion = $"{Environment.Version.Major}.{Environment.Version.Minor}.{Environment.Version.Build}"; + var executableHash = GetExecutableHash(); - Console.WriteLine($"Cosette v2.0 (Darkness), 19.10.2020 @ {Environment.OSVersion} (.NET Core {runtimeVersion})"); + Console.WriteLine($"Cosette v3.0 (Luna), 12.12.2020 @ {Environment.OSVersion} (.NET Core {runtimeVersion})"); Console.WriteLine("Distributed under AGPL license, homepage and source code: https://github.com/Tearth/Cosette"); + Console.WriteLine($"Executable hash: {executableHash}"); Console.WriteLine(); - Console.WriteLine("\"The blunders are all there on the board, waiting to be made.\" ~ Savielly Tartakower"); + Console.WriteLine("\"When you see a good move, look for a better one.\" ~ Emanuel Lasker"); Console.WriteLine(); Console.WriteLine("Type \"help\" to display all available commands"); } + + private string GetExecutableHash() + { + try + { + var md5 = new MD5CryptoServiceProvider(); + var path = Assembly.GetExecutingAssembly().Location; + + path = string.IsNullOrEmpty(path) ? + Process.GetCurrentProcess().MainModule.FileName : + Path.Combine(AppContext.BaseDirectory, path); + + using (var streamReader = new StreamReader(path)) + { + md5.ComputeHash(streamReader.BaseStream); + } + + var hashBuilder = new StringBuilder(); + foreach (var b in md5.Hash) + { + hashBuilder.Append(b.ToString("x2")); + } + + return hashBuilder.ToString(); + } + catch + { + return "GET_EXECUTABLE_HASH_ERROR"; + } + } } } diff --git a/Cosette/Program.cs b/Cosette/Program.cs index 89fb36f..cd2ad37 100644 --- a/Cosette/Program.cs +++ b/Cosette/Program.cs @@ -1,10 +1,13 @@ using System; +using System.Runtime.CompilerServices; using Cosette.Engine.Ai.Ordering; using Cosette.Engine.Ai.Transposition; using Cosette.Engine.Moves.Magic; using Cosette.Interactive; using Cosette.Logs; +[module: SkipLocalsInit] + namespace Cosette { public class Program diff --git a/Cosette/Uci/Commands/GoCommand.cs b/Cosette/Uci/Commands/GoCommand.cs index 9c82848..30df364 100644 --- a/Cosette/Uci/Commands/GoCommand.cs +++ b/Cosette/Uci/Commands/GoCommand.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Cosette.Engine.Ai.Search; using Cosette.Engine.Ai.Time; @@ -40,39 +41,54 @@ public void Run(params string[] parameters) _uciClient.SearchContext = new SearchContext(_uciClient.BoardState) { + HelperTasksCancellationTokenSource = new CancellationTokenSource(), MaxDepth = depth + 1, MaxNodesCount = nodesCount, MoveRestrictions = searchMoves }; - if (moveTime != 0) + if (moveTime == 0) + { + RunAbortTask((int)(maxColorTime * SearchConstants.DeadlineFactor), _uciClient.SearchContext.HelperTasksCancellationTokenSource.Token); + } + else { - maxColorTime = int.MaxValue; _uciClient.SearchContext.WaitForStopCommand = true; - Task.Run(() => - { - var stopwatch = Stopwatch.StartNew(); - while (stopwatch.ElapsedMilliseconds < moveTime) - { - Task.Delay(1).GetAwaiter().GetResult(); - } - - _uciClient.SearchContext.AbortSearch = true; - _uciClient.SearchContext.WaitForStopCommand = false; - }); + maxColorTime = int.MaxValue; + RunAbortTask(moveTime, _uciClient.SearchContext.HelperTasksCancellationTokenSource.Token); } if (infiniteFlag) { - maxColorTime = int.MaxValue; _uciClient.SearchContext.WaitForStopCommand = true; + maxColorTime = int.MaxValue; } _uciClient.SearchContext.MaxTime = maxColorTime; Task.Run(SearchEntryPoint); } + private void RunAbortTask(int deadline, CancellationToken cancellationToken) + { + Task.Run(() => + { + var stopwatch = Stopwatch.StartNew(); + while (stopwatch.ElapsedMilliseconds < deadline || _uciClient.SearchContext.Statistics.Depth <= 1) + { + Task.Delay(1).GetAwaiter().GetResult(); + + if (cancellationToken.IsCancellationRequested) + { + return; + } + } + + _uciClient.SearchContext.AbortSearch = true; + _uciClient.SearchContext.WaitForStopCommand = false; + }); + } + private void SearchEntryPoint() { try diff --git a/Cosette/Uci/Commands/PositionCommand.cs b/Cosette/Uci/Commands/PositionCommand.cs index 887cb9f..4842fde 100644 --- a/Cosette/Uci/Commands/PositionCommand.cs +++ b/Cosette/Uci/Commands/PositionCommand.cs @@ -60,7 +60,8 @@ private void ParseMoves(List moves) var parsedMove = Move.FromTextNotation(_uciClient.BoardState, move); if (parsedMove == Move.Empty) { - throw new InvalidOperationException(); + _uciClient.SendError("invalidmove"); + return; } _uciClient.BoardState.MakeMove(parsedMove); diff --git a/Cosette/Uci/Commands/SetOptionCommand.cs b/Cosette/Uci/Commands/SetOptionCommand.cs index 5ab1dc3..cbfd2bc 100644 --- a/Cosette/Uci/Commands/SetOptionCommand.cs +++ b/Cosette/Uci/Commands/SetOptionCommand.cs @@ -1,29 +1,95 @@ -using Cosette.Engine.Ai.Search; +using System; +using System.Collections.Generic; +using Cosette.Engine.Ai.Ordering; +using Cosette.Engine.Ai.Score; +using Cosette.Engine.Ai.Search; using Cosette.Engine.Ai.Transposition; +using Cosette.Engine.Common; namespace Cosette.Uci.Commands { public class SetOptionCommand : IUciCommand { private readonly UciClient _uciClient; + private readonly Dictionary> _optionExecutors; public SetOptionCommand(UciClient uciClient) { _uciClient = uciClient; + _optionExecutors = new Dictionary> + { + { "Hash", p => HashTableAllocator.Allocate(int.Parse(p)) }, + + { "PawnValue", p => EvaluationConstants.Pieces[Piece.Pawn] = int.Parse(p) }, + { "KnightValue", p => EvaluationConstants.Pieces[Piece.Knight] = int.Parse(p) }, + { "BishopValue", p => EvaluationConstants.Pieces[Piece.Bishop] = int.Parse(p) }, + { "RookValue", p => EvaluationConstants.Pieces[Piece.Rook] = int.Parse(p) }, + { "QueenValue", p => EvaluationConstants.Pieces[Piece.Queen] = int.Parse(p) }, + { "KingValue", p => EvaluationConstants.Pieces[Piece.King] = int.Parse(p) }, + + { "DoubledPawnsOpening", p => EvaluationConstants.DoubledPawns[GamePhase.Opening] = int.Parse(p) }, + { "DoubledPawnsEnding", p => EvaluationConstants.DoubledPawns[GamePhase.Ending] = int.Parse(p) }, + { "IsolatedPawnsOpening", p => EvaluationConstants.IsolatedPawns[GamePhase.Opening] = int.Parse(p) }, + { "IsolatedPawnsEnding", p => EvaluationConstants.IsolatedPawns[GamePhase.Ending] = int.Parse(p) }, + { "ChainedPawnsOpening", p => EvaluationConstants.ChainedPawns[GamePhase.Opening] = int.Parse(p) }, + { "ChainedPawnsEnding", p => EvaluationConstants.ChainedPawns[GamePhase.Ending] = int.Parse(p) }, + { "PassingPawnsOpening", p => EvaluationConstants.PassingPawns[GamePhase.Opening] = int.Parse(p) }, + { "PassingPawnsEnding", p => EvaluationConstants.PassingPawns[GamePhase.Ending] = int.Parse(p) }, + + { "CastlingDone", p => EvaluationConstants.CastlingDone = int.Parse(p) }, + { "CastlingFailed", p => EvaluationConstants.CastlingFailed = int.Parse(p) }, + { "Mobility", p => EvaluationConstants.Mobility = int.Parse(p) }, + { "CenterMobilityModifier", p => EvaluationConstants.CenterMobilityModifier = int.Parse(p) }, + { "ExtendedCenterMobilityModifier", p => EvaluationConstants.ExtendedCenterMobilityModifier = int.Parse(p) }, + { "OutsideMobilityModifier", p => EvaluationConstants.OutsideMobilityModifier = int.Parse(p) }, + { "KingInDanger", p => EvaluationConstants.KingInDanger = int.Parse(p) }, + { "PawnShield", p => EvaluationConstants.PawnShield = int.Parse(p) }, + { "DoubledRooks", p => EvaluationConstants.DoubledRooks = int.Parse(p) }, + { "RookOnOpenFile", p => EvaluationConstants.RookOnOpenFile = int.Parse(p) }, + { "PairOfBishops", p => EvaluationConstants.PairOfBishops = int.Parse(p) }, + { "Fianchetto", p => EvaluationConstants.Fianchetto = int.Parse(p) }, + { "FianchettoWithoutBishop", p => EvaluationConstants.FianchettoWithoutBishop = int.Parse(p) }, + + { "HashMove", p => MoveOrderingConstants.HashMove = short.Parse(p) }, + { "Promotion", p => MoveOrderingConstants.Promotion = short.Parse(p) }, + { "Castling", p => MoveOrderingConstants.Castling = short.Parse(p) }, + { "PawnNearPromotion", p => MoveOrderingConstants.PawnNearPromotion = short.Parse(p) }, + { "Capture", p => MoveOrderingConstants.Capture = short.Parse(p) }, + { "EnPassant", p => MoveOrderingConstants.EnPassant = short.Parse(p) }, + { "KillerMove", p => MoveOrderingConstants.KillerMove = short.Parse(p) }, + { "HistoryHeuristicMaxScore", p => MoveOrderingConstants.HistoryHeuristicMaxScore = uint.Parse(p) }, + { "KillerSlots", p => MoveOrderingConstants.KillerSlots = int.Parse(p) }, + + { "IIDMinimalDepth", p => SearchConstants.IIDMinimalDepth = int.Parse(p) }, + { "IIDDepthReduction", p => SearchConstants.IIDDepthReduction = int.Parse(p) }, + { "NullWindowMinimalDepth", p => SearchConstants.NullWindowMinimalDepth = int.Parse(p) }, + { "NullWindowDepthReduction", p => SearchConstants.NullWindowDepthReduction = int.Parse(p) }, + { "LMRMinimalDepth", p => SearchConstants.LMRMinimalDepth = int.Parse(p) }, + { "LMRMovesWithoutReduction", p => SearchConstants.LMRMovesWithoutReduction = int.Parse(p) }, + { "LMRPvNodeDepthReduction", p => SearchConstants.LMRPvNodeDepthReduction = int.Parse(p) }, + { "LMRNonPvNodeDepthDivisor", p => SearchConstants.LMRNonPvNodeDepthDivisor = int.Parse(p) }, + }; } public void Run(params string[] parameters) { - switch (parameters[1]) + var key = parameters[1]; + var value = parameters[3]; + + if (_optionExecutors.ContainsKey(key)) { - case "Hash": - { - var hashTablesSize = int.Parse(parameters[3]); - HashTableAllocator.Allocate(hashTablesSize); + _optionExecutors[key](value); - break; + // Value of material has changed, SEE table needs to be recalculated + if (key.EndsWith("Value")) + { + StaticExchangeEvaluation.Init(); } } + else + { + _uciClient.SendError("badoption"); + } } } } \ No newline at end of file diff --git a/Cosette/Uci/UciClient.cs b/Cosette/Uci/UciClient.cs index 96b6de3..dbe42cd 100644 --- a/Cosette/Uci/UciClient.cs +++ b/Cosette/Uci/UciClient.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Cosette.Engine.Ai.Ordering; using Cosette.Engine.Ai.Score; using Cosette.Engine.Ai.Score.Evaluators; using Cosette.Engine.Ai.Search; @@ -63,6 +64,11 @@ public void Send(string command) LogManager.LogInfo("[SEND] " + command); } + public void SendError(string errorMessage) + { + Send($"error {errorMessage}"); + } + public (string Command, string[] parameters) Receive() { while (true) @@ -99,7 +105,57 @@ private void SendAuthor() private void SendOptions() { - Send($"option name Hash type spin default {HashTableConstants.DefaultHashTablesSize} min 3 max 65536"); + Send($"option name Hash type spin default {HashTableConstants.DefaultHashTablesSize} min 3 max 65535"); + + Send($"option name PawnValue type spin default {EvaluationConstants.Pieces[Piece.Pawn]} min 0 max 65535"); + Send($"option name KnightValue type spin default {EvaluationConstants.Pieces[Piece.Knight]} min 0 max 65535"); + Send($"option name BishopValue type spin default {EvaluationConstants.Pieces[Piece.Bishop]} min 0 max 65535"); + Send($"option name RookValue type spin default {EvaluationConstants.Pieces[Piece.Rook]} min 0 max 65535"); + Send($"option name QueenValue type spin default {EvaluationConstants.Pieces[Piece.Queen]} min 0 max 65535"); + Send($"option name KingValue type spin default {EvaluationConstants.Pieces[Piece.King]} min 0 max 65535"); + + Send($"option name DoubledPawnsOpening type spin default {EvaluationConstants.DoubledPawns[GamePhase.Opening]} min -100 max 100"); + Send($"option name DoubledPawnsEnding type spin default {EvaluationConstants.DoubledPawns[GamePhase.Ending]} min -100 max 100"); + Send($"option name IsolatedPawnsOpening type spin default {EvaluationConstants.IsolatedPawns[GamePhase.Opening]} min -100 max 100"); + Send($"option name IsolatedPawnsEnding type spin default {EvaluationConstants.IsolatedPawns[GamePhase.Ending]} min -100 max 100"); + Send($"option name ChainedPawnsOpening type spin default {EvaluationConstants.ChainedPawns[GamePhase.Opening]} min -100 max 100"); + Send($"option name ChainedPawnsEnding type spin default {EvaluationConstants.ChainedPawns[GamePhase.Ending]} min -100 max 100"); + Send($"option name PassingPawnsOpening type spin default {EvaluationConstants.PassingPawns[GamePhase.Opening]} min -100 max 100"); + Send($"option name PassingPawnsEnding type spin default {EvaluationConstants.PassingPawns[GamePhase.Ending]} min -100 max 100"); + + Send($"option name CastlingDone type spin default {EvaluationConstants.CastlingDone} min -100 max 100"); + Send($"option name CastlingFailed type spin default {EvaluationConstants.CastlingFailed} min -100 max 100"); + Send($"option name Mobility type spin default {EvaluationConstants.Mobility} min -100 max 100"); + Send($"option name CenterMobilityModifier type spin default {EvaluationConstants.CenterMobilityModifier} min -100 max 100"); + Send($"option name ExtendedCenterMobilityModifier type spin default {EvaluationConstants.ExtendedCenterMobilityModifier} min -100 max 100"); + Send($"option name OutsideMobilityModifier type spin default {EvaluationConstants.OutsideMobilityModifier} min -100 max 100"); + Send($"option name KingInDanger type spin default {EvaluationConstants.KingInDanger} min -100 max 100"); + Send($"option name PawnShield type spin default {EvaluationConstants.PawnShield} min -100 max 100"); + Send($"option name DoubledRooks type spin default {EvaluationConstants.DoubledRooks} min -100 max 100"); + Send($"option name RookOnOpenFile type spin default {EvaluationConstants.RookOnOpenFile} min -100 max 100"); + Send($"option name PairOfBishops type spin default {EvaluationConstants.PairOfBishops} min -100 max 100"); + Send($"option name Fianchetto type spin default {EvaluationConstants.Fianchetto} min -100 max 100"); + Send($"option name FianchettoWithoutBishop type spin default {EvaluationConstants.FianchettoWithoutBishop} min -100 max 100"); + + Send($"option name HashMove type spin default {MoveOrderingConstants.HashMove} min -10000 max 10000"); + Send($"option name Promotion type spin default {MoveOrderingConstants.Promotion} min -10000 max 10000"); + Send($"option name Castling type spin default {MoveOrderingConstants.Castling} min -10000 max 10000"); + Send($"option name PawnNearPromotion type spin default {MoveOrderingConstants.PawnNearPromotion} min -10000 max 10000"); + Send($"option name Capture type spin default {MoveOrderingConstants.Capture} min -10000 max 10000"); + Send($"option name EnPassant type spin default {MoveOrderingConstants.EnPassant} min -10000 max 10000"); + Send($"option name KillerMove type spin default {MoveOrderingConstants.KillerMove} min -10000 max 10000"); + Send($"option name HistoryHeuristicMaxScore type spin default {MoveOrderingConstants.HistoryHeuristicMaxScore} min -10000 max 10000"); + Send($"option name KillerSlots type spin default {MoveOrderingConstants.KillerSlots} min -10000 max 10000"); + + Send($"option name IIDMinimalDepth type spin default {SearchConstants.IIDMinimalDepth} min 0 max 32"); + Send($"option name IIDDepthReduction type spin default {SearchConstants.IIDDepthReduction} min 0 max 32"); + Send($"option name NullWindowMinimalDepth type spin default {SearchConstants.NullWindowMinimalDepth} min 0 max 32"); + Send($"option name NullWindowDepthReduction type spin default {SearchConstants.NullWindowDepthReduction} min 0 max 32"); + Send($"option name LMRMinimalDepth type spin default {SearchConstants.LMRMinimalDepth} min 0 max 32"); + Send($"option name LMRMovesWithoutReduction type spin default {SearchConstants.LMRMovesWithoutReduction} min 0 max 32"); + Send($"option name LMRPvNodeDepthReduction type spin default {SearchConstants.LMRPvNodeDepthReduction} min 0 max 32"); + Send($"option name LMRNonPvNodeDepthDivisor type spin default {SearchConstants.LMRNonPvNodeDepthDivisor} min 0 max 32"); + Send("uciok"); } @@ -127,22 +183,26 @@ private void OnSearchUpdate(object sender, SearchStatistics stats) { var evaluationStatistics = new EvaluationStatistics(); var openingPhase = stats.Board.GetPhaseRatio(); - var endingPhase = 1 - openingPhase; + var endingPhase = BoardConstants.PhaseResolution - openingPhase; + + var fieldsAttackedByWhite = 0ul; + var fieldsAttackedByBlack = 0ul; var materialEvaluation = MaterialEvaluator.Evaluate(stats.Board); var castlingEvaluation = CastlingEvaluator.Evaluate(stats.Board, openingPhase, endingPhase); var positionEvaluation = PositionEvaluator.Evaluate(stats.Board, openingPhase, endingPhase); var pawnStructureEvaluation = PawnStructureEvaluator.Evaluate(stats.Board, evaluationStatistics, openingPhase, endingPhase); - var mobility = MobilityEvaluator.Evaluate(stats.Board, openingPhase, endingPhase); - var kingSafety = KingSafetyEvaluator.Evaluate(stats.Board, openingPhase, endingPhase); + var mobility = MobilityEvaluator.Evaluate(stats.Board, openingPhase, endingPhase, ref fieldsAttackedByWhite, ref fieldsAttackedByBlack); + var kingSafety = KingSafetyEvaluator.Evaluate(stats.Board, openingPhase, endingPhase, fieldsAttackedByWhite, fieldsAttackedByBlack); var pieces = PiecesEvaluator.Evaluate(stats.Board, openingPhase, endingPhase); + var fianchetto = FianchettoEvaluator.Evaluate(stats.Board, openingPhase, endingPhase); var total = materialEvaluation + castlingEvaluation + positionEvaluation + pawnStructureEvaluation + mobility + kingSafety + pieces; - Send($"info string evaluation {total} phase {openingPhase:F} material {materialEvaluation} castling {castlingEvaluation} " + + Send($"info string evaluation {total} phase {openingPhase} material {materialEvaluation} castling {castlingEvaluation} " + $"position {positionEvaluation} pawns {pawnStructureEvaluation} mobility {mobility} ksafety {kingSafety} " + - $"pieces {pieces} irrmoves {stats.Board.IrreversibleMovesCount}"); + $"pieces {pieces} fianchetto {fianchetto} irrmoves {stats.Board.IrreversibleMovesCount}"); } } diff --git a/README.md b/README.md index e77d48f..1218752 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ # Cosette -**Current version: v2.0 (Darkness), 19.10.2020** +**Current version: v3.0 (Luna), 12.12.2020** -A brand new UCI-compliant chess engine written in C# for .NET Core platform. The project is still in the early stage of development and lacks a few major performance improvements like more advanced pruning, extensions, or better evaluation. The current strength is estimated at 1950 ELO and hopefully will significantly improve in the future. +An UCI-compliant chess engine written in C# for .NET Core platform, with **[an official profile on CCRL](http://ccrl.chessdom.com/ccrl/404/cgi/compare_engines.cgi?family=Cosette&print=Rating+list&print=Results+table&print=LOS+table&print=Ponder+hit+table&print=Eval+difference+table&print=Comopp+gamenum+table&print=Overlap+table&print=Score+with+common+opponents)** (Computer Chess Rating Lists) where you can check the best strength estimation. Feel free to visit **[a dedicated forum thread](http://kirill-kryukov.com/chess/discussion-board/viewtopic.php?f=7&t=12402)** for Cosette releases and discussions! -The engine has **[an official profile on CCRL](http://ccrl.chessdom.com/ccrl/404/cgi/compare_engines.cgi?family=Cosette&print=Rating+list&print=Results+table&print=LOS+table&print=Ponder+hit+table&print=Eval+difference+table&print=Comopp+gamenum+table&print=Overlap+table&print=Score+with+common+opponents)** (Computer Chess Rating Lists) where you can check the best strength estimation. Also, feel free to visit **[a dedicated forum thread](http://kirill-kryukov.com/chess/discussion-board/viewtopic.php?f=7&t=12402)** for Cosette releases and discussions! - -![Cosette interactive console example](https://i.imgur.com/hIcaAmz.png) +![Cosette interactive console example](https://i.imgur.com/2cSfVBR.png) ## How to play? The simplest way is to download the newest version from the **[Releases page](https://github.com/Tearth/Cosette/releases)** and use it with a graphical interface like Arena or WinBoard. The engine has been tested extensively on the first one, but should work with every UCI-compliant GUI. @@ -31,22 +29,46 @@ Cosette has an official account on **[lichess.org](https://lichess.org/)** platf - doubled rooks - rooks on open files - bishop pair + - fianchetto + - tapering **Search:** - negamax - alpha-beta pruning - transposition table - quiescence search + - quiescence SEE pruning - iterative deepening + - internal iterative deepening + - staged move generating - null-move pruning - principal variation search - late move reduction **Move ordering:** + - staged move ordering - static exchange evaluation - killer heuristic - history heuristic +## Additional tools + +### Arbiter + +The simple console application which allows to do tests chess engines using super-fast time control (like 2s+20"). It has been designed to support Cosette development, but should be also usable with other UCI-compliant engines. + +![Cosette Arbiter](https://i.imgur.com/m7rYtuf.png) + +### Tuner + +The console application + web interface, which allows adjusting engine parameters using standard UCI command setoption. Results are displayed on the local website using a set of tables and charts. + +![Cosette Tuner](https://i.imgur.com/uxXeYW9.png) + +### Polyglot + +The small library which allows using opening books saved in the Polyglot format. Feel free to use it if you need this in your own project. + ## Why Cosette? https://www.youtube.com/watch?v=XuYF-EnpLpc \ No newline at end of file