From f09ca60315f5f8632385a67bb1fd13dd7f5bc435 Mon Sep 17 00:00:00 2001 From: Josh-Heaps Date: Fri, 12 Jun 2026 18:02:09 -0600 Subject: [PATCH] foo bar --- .gitignore | 3 + JoshHeaps.Net/Controllers/ChessController.cs | 198 +++++++++- JoshHeaps.Net/Models/GameState.cs | 18 +- .../Models/LearnedWeightsSnapshot.cs | 9 + JoshHeaps.Net/Pages/Watch.cshtml | 36 +- JoshHeaps.Net/Pages/Weights.cshtml | 44 +++ JoshHeaps.Net/Pages/Weights.cshtml.cs | 11 + JoshHeaps.Net/Program.cs | 1 + JoshHeaps.Net/Resources/chess_engine.dll | Bin 192512 -> 249344 bytes .../Implementations/ChessEngineFactory.cs | 16 +- .../ComputerMoveOrchestrator.cs | 51 ++- .../Implementations/CustomChessEngine.cs | 49 ++- .../Implementations/LearnedWeightsStore.cs | 64 ++++ .../Interfaces/IChessEngineFactory.cs | 15 +- .../Interfaces/IComputerMoveOrchestrator.cs | 18 +- .../Interfaces/ILearnedWeightsStore.cs | 33 ++ JoshHeaps.Net/wwwroot/css/chess/spectate.css | 20 +- JoshHeaps.Net/wwwroot/css/chess/weights.css | 187 ++++++++++ .../wwwroot/js/ChessScripts/Spectate.js | 25 +- .../wwwroot/js/ChessScripts/Weights.js | 158 ++++++++ native/chess_engine/include/chess_engine.h | 29 ++ native/chess_engine/src/chess_engine.cpp | 346 +++++++++++++++++- native/chess_engine/src/position.h | 16 + 23 files changed, 1286 insertions(+), 61 deletions(-) create mode 100644 JoshHeaps.Net/Models/LearnedWeightsSnapshot.cs create mode 100644 JoshHeaps.Net/Pages/Weights.cshtml create mode 100644 JoshHeaps.Net/Pages/Weights.cshtml.cs create mode 100644 JoshHeaps.Net/Services/Implementations/LearnedWeightsStore.cs create mode 100644 JoshHeaps.Net/Services/Interfaces/ILearnedWeightsStore.cs create mode 100644 JoshHeaps.Net/wwwroot/css/chess/weights.css create mode 100644 JoshHeaps.Net/wwwroot/js/ChessScripts/Weights.js diff --git a/.gitignore b/.gitignore index e8379fe..6a4cfa1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# Learned chess engine weights (runtime training output, not source) +chess-data/ + # User-specific files *.rsuser *.suo diff --git a/JoshHeaps.Net/Controllers/ChessController.cs b/JoshHeaps.Net/Controllers/ChessController.cs index 46e38e9..350d8f2 100644 --- a/JoshHeaps.Net/Controllers/ChessController.cs +++ b/JoshHeaps.Net/Controllers/ChessController.cs @@ -15,6 +15,7 @@ public class ChessController( IBackgroundTaskQueue queue, IChessEngineFactory engineFactory, IComputerMoveOrchestrator orchestrator, + ILearnedWeightsStore weightsStore, IHubContext chessHub) : ControllerBase { /// @@ -30,6 +31,10 @@ public class ChessController( private static readonly TimeSpan _selfPlayMoveDelay = TimeSpan.FromSeconds(1); private static readonly TimeSpan _selfPlayResultTimeout = TimeSpan.FromSeconds(30); + // Plies of random legal moves at the start of a training game, so self-play and + // engine-vs-engine games explore different lines instead of replaying one game. + private const int _openingRandomPlies = 4; + /// /// Create a new chess game and store it in-memory. /// @@ -52,22 +57,24 @@ public class ChessController( _ => Random.Shared.Next(2) == 0, }; - gameState.Computer = engineFactory.Create(difficulty); + var computer = engineFactory.Create(difficulty); if (isWhite) { gameState.WhitePlayerId = playerId; gameState.BlackPlayerId = computerId; + gameState.BlackComputer = computer; } else { gameState.WhitePlayerId = computerId; gameState.BlackPlayerId = playerId; + gameState.WhiteComputer = computer; queue.Queue(async () => { // Give user's browser time to connect to signalR and such. await Task.Delay(TimeSpan.FromSeconds(1)); - await orchestrator.PlayAsync(gameState, gameState.Computer!); + await orchestrator.PlayAsync(gameState); }); } @@ -82,13 +89,22 @@ public class ChessController( } /// - /// Create a game the computer plays against itself and auto-play it move by move, - /// broadcasting each move so it can be watched on the spectator page. + /// Create a computer-vs-computer game and auto-play it move by move, broadcasting each + /// move so it can be watched on the spectator page. Each side's engine and skill can be + /// chosen independently; when the learned engine plays, the game also trains it. /// [HttpGet("watch/cpu")] [HttpGet("watch/cpu/{difficulty}")] - public ActionResult CreateSelfPlayGame(int difficulty = 4) + public ActionResult CreateSelfPlayGame( + int difficulty = 4, + string whiteEngine = "custom", + string blackEngine = "custom", + int? whiteSkill = null, + int? blackSkill = null) { + var whiteKind = ParseEngineKind(whiteEngine); + var blackKind = ParseEngineKind(blackEngine); + var gameState = chessService.CreateNewGame(); _games[gameState.GameId] = gameState; @@ -98,7 +114,14 @@ public class ChessController( gameState.BlackJoined = true; gameState.WhitePlayerId = Guid.NewGuid(); gameState.BlackPlayerId = Guid.NewGuid(); - gameState.Computer = engineFactory.Create(difficulty); + gameState.WhiteEngineKind = whiteKind; + gameState.BlackEngineKind = blackKind; + gameState.WhiteComputer = engineFactory.Create(whiteSkill ?? difficulty, whiteKind); + gameState.BlackComputer = engineFactory.Create(blackSkill ?? difficulty, blackKind); + + // When the learned engine is playing, attach a trainer so the outcome can train it. + if (whiteKind == ChessEngineKind.CustomLearned || blackKind == ChessEngineKind.CustomLearned) + gameState.Trainer = weightsStore.CreateTrainer(); ScheduleRemoveGame(gameState.GameId, _computerGameTimeout); StartSelfPlay(gameState); @@ -106,6 +129,13 @@ public class ChessController( return Ok(new { gameState.GameId }); } + private static ChessEngineKind ParseEngineKind(string value) => value.ToLowerInvariant() switch + { + "stockfish" => ChessEngineKind.Stockfish, + "customlearned" or "learned" => ChessEngineKind.CustomLearned, + _ => ChessEngineKind.Custom + }; + /// /// Joins the "pool" of chess players. /// Test code expects to receive a GUID for the player @@ -163,6 +193,8 @@ public class ChessController( g.GameId, g.IsVsComputer, g.IsComputerVsComputer, + WhiteEngine = g.WhiteEngineKind.ToString(), + BlackEngine = g.BlackEngineKind.ToString(), CurrentPlayer = g.CurrentPlayer.ToString(), MoveCount = g.MoveHistory.Count, g.IsCheck @@ -172,6 +204,26 @@ public class ChessController( return Ok(activeGames); } + /// + /// The learned engine's piece-square bonus table, for the weights-visualization page: + /// one 64-entry array per piece type (Pawn..King), white-relative (A1=0 .. H8=63). + /// + [HttpGet("weights")] + public ActionResult GetLearnedWeights() + { + var names = new[] { "Pawn", "Knight", "Bishop", "Rook", "Queen", "King" }; + var featureNames = new[] { "Mobility N", "Mobility B", "Mobility R", "Mobility Q", "Passed", "Isolated", "Doubled", "King safety" }; + + var snapshot = weightsStore.Snapshot(); + + return Ok(new + { + mg = snapshot.Mg.Select((squares, i) => new { name = names[i], squares }), + eg = snapshot.Eg.Select((squares, i) => new { name = names[i], squares }), + features = snapshot.Features.Select((value, i) => new { name = featureNames[i], value }) + }); + } + /// /// Get the state of an existing game by ID. /// @@ -232,8 +284,12 @@ public class ChessController( await chessHub.Clients.Group(gameState.GameId.ToString()) .SendAsync("ReceiveMoveUpdate", gameState.GameId.ToString(), moveDto, result, state); - if (!isGameOver && gameState.IsVsComputer && gameState.Computer is not null) - queue.Queue(() => orchestrator.PlayAsync(gameState, gameState.Computer!)); + var sideToMoveEngine = gameState.CurrentPlayer == PieceColor.White + ? gameState.WhiteComputer + : gameState.BlackComputer; + + if (!isGameOver && gameState.IsVsComputer && sideToMoveEngine is not null) + queue.Queue(() => orchestrator.PlayAsync(gameState)); return Ok(new { result, state }); } @@ -314,8 +370,9 @@ public class ChessController( } /// - /// Drives a computer-vs-computer game: keeps asking the engine for the side-to-move's - /// move (which applies and broadcasts it) until the game ends or is removed. + /// Drives a computer-vs-computer game: keeps asking the side-to-move's engine for its + /// move (which applies and broadcasts it) until the game ends or is removed. Training + /// games get a randomized opening and feed their result back into the learned weights. /// private void StartSelfPlay(GameState gameState) { @@ -324,15 +381,19 @@ public class ChessController( // Give spectators a moment to join the SignalR group before the first move. await Task.Delay(TimeSpan.FromSeconds(1)); - while (_games.ContainsKey(gameState.GameId) - && !gameState.IsCheckmate - && !gameState.IsStalemate - && !gameState.IsThreefoldRepetition - && !gameState.IsForfeited) + // Training games open with random moves so they don't replay the same line. + if (gameState.Trainer != nint.Zero) + for (int i = 0; i < _openingRandomPlies && _games.ContainsKey(gameState.GameId) && !IsGameOver(gameState); i++) + { + await orchestrator.PlayRandomMoveAsync(gameState); + await Task.Delay(_selfPlayMoveDelay); + } + + while (_games.ContainsKey(gameState.GameId) && !IsGameOver(gameState)) { try { - await orchestrator.PlayAsync(gameState, gameState.Computer!); + await orchestrator.PlayAsync(gameState); } catch (Exception ex) { @@ -343,12 +404,100 @@ public class ChessController( await Task.Delay(_selfPlayMoveDelay); } + ApplyLearning(gameState); + // Leave the finished game in place briefly so spectators can see the result. if (_games.ContainsKey(gameState.GameId)) ScheduleRemoveGame(gameState.GameId, _selfPlayResultTimeout); }); } + private static bool IsGameOver(GameState gameState) => + gameState.IsCheckmate || gameState.IsStalemate || gameState.IsThreefoldRepetition || gameState.IsForfeited; + + /// + /// Feeds a finished training game's result into the learned weights, then frees the + /// trainer. Both sides teach the table — the winner's squares/features up, the loser's + /// down. A checkmate is a full-strength result; a material-imbalance draw is a half- + /// strength win for the lower-material side (holding a draw while down material is a + /// success; only drawing while up is a failure). A balanced draw, forfeit, or unfinished + /// game teaches nothing (but the trainer is still freed). + /// + private void ApplyLearning(GameState gameState) + { + if (gameState.Trainer == nint.Zero) + return; + + if (TryDetermineOutcome(gameState, out var winner, out var weight)) + weightsStore.ApplyResult(gameState.Trainer, winner, weight); + + weightsStore.DestroyTrainer(gameState.Trainer); + gameState.Trainer = nint.Zero; + } + + /// + /// Determines the trainable outcome of a finished game: the winning color and the reward + /// weight. Returns false when the game teaches nothing (balanced draw, forfeit, unfinished). + /// + private static bool TryDetermineOutcome(GameState gameState, out PieceColor winner, out double weight) + { + winner = PieceColor.White; + weight = 1.0; + + if (gameState.IsCheckmate) + { + // The side to move is the mated one, so the winner is the other color. + winner = gameState.CurrentPlayer == PieceColor.White ? PieceColor.Black : PieceColor.White; + return true; + } + + if (gameState.IsStalemate || gameState.IsThreefoldRepetition) + { + var (white, black) = MaterialCounts(gameState); + + if (white == black) + return false; // a balanced draw carries no signal + + winner = white < black ? PieceColor.White : PieceColor.Black; + weight = 0.5; + return true; + } + + return false; // forfeit / unfinished + } + + /// Total non-king material per side (P=1, N=B=3, R=5, Q=9), for draw adjudication. + private static (int white, int black) MaterialCounts(GameState gameState) + { + int white = 0, black = 0; + + for (int row = 0; row < 8; row++) + for (int col = 0; col < 8; col++) + { + var piece = gameState.Board[row, col]; + + if (piece is null) + continue; + + int value = piece.Type switch + { + PieceType.Pawn => 1, + PieceType.Knight => 3, + PieceType.Bishop => 3, + PieceType.Rook => 5, + PieceType.Queen => 9, + _ => 0 + }; + + if (piece.Color == PieceColor.White) + white += value; + else + black += value; + } + + return (white, black); + } + private static void ScheduleRemoveGame(Guid id, TimeSpan delay) { if (_gameRemovalCancellationTokens.TryRemove(id, out var oldCts)) @@ -366,8 +515,21 @@ public class ChessController( { await Task.Delay(delay, cts.Token); - if (_games.TryGetValue(id, out var game) && game.Computer is not null) - await game.Computer.DisposeAsync(); + if (_games.TryGetValue(id, out var game)) + { + if (game.WhiteComputer is not null) + await game.WhiteComputer.DisposeAsync(); + if (game.BlackComputer is not null) + await game.BlackComputer.DisposeAsync(); + + // Free the trainer if the game never reached ApplyLearning (e.g. timed out). + // The native ABI is shared via CustomChessEngine's import resolver. + if (game.Trainer != nint.Zero) + { + CustomChessEngine.NativeMethods.trainer_destroy(game.Trainer); + game.Trainer = nint.Zero; + } + } _games.Remove(id, out _); } diff --git a/JoshHeaps.Net/Models/GameState.cs b/JoshHeaps.Net/Models/GameState.cs index c3c86da..7ad3e13 100644 --- a/JoshHeaps.Net/Models/GameState.cs +++ b/JoshHeaps.Net/Models/GameState.cs @@ -1,4 +1,5 @@ -using JoshHeaps.Net.Services.Interfaces; +using JoshHeaps.Net.Services.Implementations; +using JoshHeaps.Net.Services.Interfaces; namespace JoshHeaps.Net.Models; @@ -59,7 +60,20 @@ public class GameState public bool IsVsComputer { get; set; } = false; public bool IsComputerVsComputer { get; set; } = false; - public IChessEngine? Computer { get; set; } + // The engine playing each side (null for a human). In a human-vs-computer game only the + // computer's side is set; the orchestrator picks the engine for whoever is to move. + public IChessEngine? WhiteComputer { get; set; } + public IChessEngine? BlackComputer { get; set; } + + // Which engine implementation each side uses — lets game-over handling know which side(s) + // are the learning engine, and lets spectators see who is playing. + public ChessEngineKind WhiteEngineKind { get; set; } + public ChessEngineKind BlackEngineKind { get; set; } + + // Native per-game training accumulator (nint.Zero when this game isn't training the + // learned engine). The engine records each played position into it and applies the + // result on game over. + public nint Trainer { get; set; } // optional: convenience public bool IsOpen => !WhiteJoined || !BlackJoined; diff --git a/JoshHeaps.Net/Models/LearnedWeightsSnapshot.cs b/JoshHeaps.Net/Models/LearnedWeightsSnapshot.cs new file mode 100644 index 0000000..a234c64 --- /dev/null +++ b/JoshHeaps.Net/Models/LearnedWeightsSnapshot.cs @@ -0,0 +1,9 @@ +namespace JoshHeaps.Net.Models; + +/// +/// A copy of the learned engine's weights for display: midgame and endgame piece-square +/// tables (one 64-entry array per piece, in canonical Pawn..King order, white-relative +/// A1=0..H8=63) plus the feature weights (mobility N/B/R/Q, passed, isolated, doubled, +/// king safety). +/// +public sealed record LearnedWeightsSnapshot(int[][] Mg, int[][] Eg, int[] Features); diff --git a/JoshHeaps.Net/Pages/Watch.cshtml b/JoshHeaps.Net/Pages/Watch.cshtml index 80635e6..3e04add 100644 --- a/JoshHeaps.Net/Pages/Watch.cshtml +++ b/JoshHeaps.Net/Pages/Watch.cshtml @@ -9,16 +9,38 @@

Live Chess

Loading games…

- - +
+ White + + +
+
+ Black + + +
← Play a game + View learned weights →
diff --git a/JoshHeaps.Net/Pages/Weights.cshtml b/JoshHeaps.Net/Pages/Weights.cshtml new file mode 100644 index 0000000..c784c51 --- /dev/null +++ b/JoshHeaps.Net/Pages/Weights.cshtml @@ -0,0 +1,44 @@ +@page +@model JoshHeaps.Net.Pages.WeightsModel +@{ + Layout = "_Layout"; + ViewData["Title"] = "Learned Weights"; +} + +
+

Learned Piece-Square Weights

+

Where the learned engine thinks each piece belongs.

+
+
+ low + + high +
+ +
+ ← Watch / train +
+ +
+

Feature weights

+

Learned value of each contextual feature (per normalized unit). Mobility is per piece type; passed pawns are endgame-weighted, king safety midgame-weighted.

+
+
+ +
+

Midgame tables

+
+
+ +
+

Endgame tables

+
+
+ +@section Scripts { + +} + +@section Styles { + +} diff --git a/JoshHeaps.Net/Pages/Weights.cshtml.cs b/JoshHeaps.Net/Pages/Weights.cshtml.cs new file mode 100644 index 0000000..baed090 --- /dev/null +++ b/JoshHeaps.Net/Pages/Weights.cshtml.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace JoshHeaps.Net.Pages +{ + public class WeightsModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/JoshHeaps.Net/Program.cs b/JoshHeaps.Net/Program.cs index 4c0e0c8..0657f4e 100644 --- a/JoshHeaps.Net/Program.cs +++ b/JoshHeaps.Net/Program.cs @@ -23,6 +23,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.Configure(configuration.GetSection(ChessEngineOptions.SectionName)); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/JoshHeaps.Net/Resources/chess_engine.dll b/JoshHeaps.Net/Resources/chess_engine.dll index 190af57982bcd281d2a36570973b1f68c9ce2724..992d57e623f320164f8be16f4543ebcd864faa13 100644 GIT binary patch literal 249344 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¥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 delta 45346 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 WP*OXqHd>olr`&msH^=prUH=DEfxblm diff --git a/JoshHeaps.Net/Services/Implementations/ChessEngineFactory.cs b/JoshHeaps.Net/Services/Implementations/ChessEngineFactory.cs index 34b7c55..2ccd590 100644 --- a/JoshHeaps.Net/Services/Implementations/ChessEngineFactory.cs +++ b/JoshHeaps.Net/Services/Implementations/ChessEngineFactory.cs @@ -7,7 +7,10 @@ namespace JoshHeaps.Net.Services.Implementations; public enum ChessEngineKind { Stockfish, - Custom + Custom, + + ///

The custom engine with the reinforcement-learned piece-square evaluation. + CustomLearned } /// Configuration selecting which to use. @@ -19,14 +22,19 @@ public sealed class ChessEngineOptions } /// Creates the configured per game. -public sealed class ChessEngineFactory(IOptions options) : IChessEngineFactory +public sealed class ChessEngineFactory( + IOptions options, + ILearnedWeightsStore weightsStore) : IChessEngineFactory { private readonly ChessEngineKind _kind = options.Value.Engine; - public IChessEngine Create(int skill) => _kind switch + public IChessEngine Create(int skill) => Create(skill, _kind); + + public IChessEngine Create(int skill, ChessEngineKind kind) => kind switch { ChessEngineKind.Custom => new CustomChessEngine(skill), + ChessEngineKind.CustomLearned => new CustomChessEngine(skill, EngineVariant.Learned, weightsStore.WeightsFilePath), ChessEngineKind.Stockfish => new Stockfish(skill), - _ => throw new InvalidOperationException($"Unknown chess engine '{_kind}'.") + _ => throw new InvalidOperationException($"Unknown chess engine '{kind}'.") }; } diff --git a/JoshHeaps.Net/Services/Implementations/ComputerMoveOrchestrator.cs b/JoshHeaps.Net/Services/Implementations/ComputerMoveOrchestrator.cs index 244b6f6..540979c 100644 --- a/JoshHeaps.Net/Services/Implementations/ComputerMoveOrchestrator.cs +++ b/JoshHeaps.Net/Services/Implementations/ComputerMoveOrchestrator.cs @@ -11,23 +11,60 @@ namespace JoshHeaps.Net.Services.Implementations; ///
public sealed class ComputerMoveOrchestrator( IHubContext chessHub, - IChessService chessService) : IComputerMoveOrchestrator + IChessService chessService, + ILearnedWeightsStore weightsStore) : IComputerMoveOrchestrator { - public async Task<(MoveDto move, MoveResultDto result)> PlayAsync(GameState state, IChessEngine engine) + public async Task<(MoveDto move, MoveResultDto result)> PlayAsync(GameState state) { + var engine = (state.CurrentPlayer == PieceColor.White ? state.WhiteComputer : state.BlackComputer) + ?? throw new InvalidOperationException($"No engine is set for {state.CurrentPlayer} in game {state.GameId}."); + var uci = await engine.GetBestMoveAsync(state.ToFen(), state.RepetitionHistory()); + var move = uci.ToMoveDto(state, CurrentPlayerId(state)); - var move = uci.ToMoveDto( - state, - state.CurrentPlayer == PieceColor.White - ? state.WhitePlayerId - : state.BlackPlayerId); + return await ApplyAndBroadcastAsync(state, move); + } + public async Task PlayRandomMoveAsync(GameState state) + { + var options = chessService.GetAllLegalMoves(state); + + // No legal moves means the game is already over; let the caller's loop detect it. + if (options.Count == 0) + return; + + var (piece, moves) = options[Random.Shared.Next(options.Count)]; + var target = moves[Random.Shared.Next(moves.Count)]; + + var move = new MoveDto + { + GameId = state.GameId, + PlayerId = CurrentPlayerId(state), + PieceId = piece.Id, + SourceRow = piece.Position.Row, + SourceCol = piece.Position.Col, + TargetRow = target.Row, + TargetCol = target.Col, + PromotionChoice = null // a pawn cannot reach the last rank within the opening plies + }; + + await ApplyAndBroadcastAsync(state, move); + } + + private async Task<(MoveDto move, MoveResultDto result)> ApplyAndBroadcastAsync(GameState state, MoveDto move) + { var result = chessService.MakeMove(state, move); + // Record the played position for training (no-op for non-training games). + if (state.Trainer != nint.Zero) + weightsStore.Record(state.Trainer, state.ToFen()); + await chessHub.Clients.Group(state.GameId.ToString()) .SendAsync("ReceiveMoveUpdate", state.GameId.ToString(), move, result, state.ToDto()); return (move, result); } + + private static Guid CurrentPlayerId(GameState state) => + state.CurrentPlayer == PieceColor.White ? state.WhitePlayerId : state.BlackPlayerId; } diff --git a/JoshHeaps.Net/Services/Implementations/CustomChessEngine.cs b/JoshHeaps.Net/Services/Implementations/CustomChessEngine.cs index 5675a69..d7bb8fd 100644 --- a/JoshHeaps.Net/Services/Implementations/CustomChessEngine.cs +++ b/JoshHeaps.Net/Services/Implementations/CustomChessEngine.cs @@ -5,6 +5,16 @@ using System.Runtime.InteropServices; namespace JoshHeaps.Net.Services.Implementations; +/// Which evaluation the native engine uses. +public enum EngineVariant +{ + /// The hand-crafted evaluation. + Classic, + + /// Material plus a learned per-square bonus table loaded from a weights file. + Learned +} + /// /// Middleman wrapper over the native custom chess engine (chess_engine.dll / libchess_engine.so). /// Shares with so the two are swappable. @@ -16,11 +26,17 @@ public sealed partial class CustomChessEngine : IChessEngine public int Skill { get; } - public CustomChessEngine(int skill = 20) + public CustomChessEngine(int skill = 20, EngineVariant variant = EngineVariant.Classic, string? weightsPath = null) { Skill = skill; - var handle = NativeMethods.engine_create($"skill={skill}"); + // weights= must come last: the native side reads the path as the rest of the + // string, which lets it contain ';' and spaces. + var options = variant == EngineVariant.Learned + ? $"skill={skill};variant=learned;weights={weightsPath}" + : $"skill={skill}"; + + var handle = NativeMethods.engine_create(options); if (handle == IntPtr.Zero) throw new InvalidOperationException("Native chess engine failed to initialize (engine_create returned null)."); @@ -79,8 +95,9 @@ public sealed partial class CustomChessEngine : IChessEngine /// /// P/Invoke surface for chess_engine.(dll|so). The resolver maps the logical name /// "chess_engine" to the platform binary in the Resources folder (mirrors Stockfish). + /// Internal so can share the single import resolver. /// - private static partial class NativeMethods + internal static partial class NativeMethods { private const string LibName = "chess_engine"; @@ -115,5 +132,31 @@ public sealed partial class CustomChessEngine : IChessEngine [LibraryImport(LibName)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial void engine_destroy(IntPtr engine); + + // ---- Learned-weights / training ABI (see chess_engine.h) ---- + + [LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial void learned_load(string path); + + [LibraryImport(LibName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe partial int weights_snapshot(int* outBuf, int outLen); + + [LibraryImport(LibName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial IntPtr trainer_create(); + + [LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial void trainer_record(IntPtr trainer, string fen); + + [LibraryImport(LibName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial void trainer_apply(IntPtr trainer, int winner, double weight); + + [LibraryImport(LibName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial void trainer_destroy(IntPtr trainer); } } diff --git a/JoshHeaps.Net/Services/Implementations/LearnedWeightsStore.cs b/JoshHeaps.Net/Services/Implementations/LearnedWeightsStore.cs new file mode 100644 index 0000000..992f387 --- /dev/null +++ b/JoshHeaps.Net/Services/Implementations/LearnedWeightsStore.cs @@ -0,0 +1,64 @@ +using JoshHeaps.Net.Models; +using JoshHeaps.Net.Services.Interfaces; + +namespace JoshHeaps.Net.Services.Implementations; + +/// +/// Managed facade over the native learned-weights model (see ). +/// On construction it points the native engine at the weights file; everything else delegates +/// to the shared native ABI in . +/// +public sealed class LearnedWeightsStore : ILearnedWeightsStore +{ + private const int Pieces = 6; // Pawn..King + private const int Squares = 64; + private const int Features = 8; // mobility N/B/R/Q, passed, isolated, doubled, king safety + + public string WeightsFilePath { get; } + + public LearnedWeightsStore(IHostEnvironment env) + { + WeightsFilePath = Path.Combine(env.ContentRootPath, "chess-data", "learned-weights.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(WeightsFilePath)!); + CustomChessEngine.NativeMethods.learned_load(WeightsFilePath); + } + + public nint CreateTrainer() => CustomChessEngine.NativeMethods.trainer_create(); + + public void Record(nint trainer, string fen) => + CustomChessEngine.NativeMethods.trainer_record(trainer, fen); + + public void ApplyResult(nint trainer, PieceColor winner, double weight) => + CustomChessEngine.NativeMethods.trainer_apply(trainer, winner == PieceColor.White ? 0 : 1, weight); + + public void DestroyTrainer(nint trainer) => + CustomChessEngine.NativeMethods.trainer_destroy(trainer); + + public LearnedWeightsSnapshot Snapshot() + { + const int total = Pieces * Squares * 2 + Features; + var buffer = new int[total]; + + unsafe + { + fixed (int* p = buffer) + CustomChessEngine.NativeMethods.weights_snapshot(p, total); + } + + var mg = new int[Pieces][]; + var eg = new int[Pieces][]; + + for (int piece = 0; piece < Pieces; piece++) + { + mg[piece] = new int[Squares]; + eg[piece] = new int[Squares]; + Array.Copy(buffer, piece * Squares, mg[piece], 0, Squares); + Array.Copy(buffer, Pieces * Squares + piece * Squares, eg[piece], 0, Squares); + } + + var features = new int[Features]; + Array.Copy(buffer, Pieces * Squares * 2, features, 0, Features); + + return new LearnedWeightsSnapshot(mg, eg, features); + } +} diff --git a/JoshHeaps.Net/Services/Interfaces/IChessEngineFactory.cs b/JoshHeaps.Net/Services/Interfaces/IChessEngineFactory.cs index 3f87e24..02b4990 100644 --- a/JoshHeaps.Net/Services/Interfaces/IChessEngineFactory.cs +++ b/JoshHeaps.Net/Services/Interfaces/IChessEngineFactory.cs @@ -1,3 +1,5 @@ +using JoshHeaps.Net.Services.Implementations; + namespace JoshHeaps.Net.Services.Interfaces; /// @@ -6,9 +8,20 @@ namespace JoshHeaps.Net.Services.Interfaces; public interface IChessEngineFactory { /// - /// Creates a new engine instance for a single game. The caller owns and disposes it. + /// Creates a new engine instance for a single game using the configured default engine. + /// The caller owns and disposes it. /// /// The desired playing strength / search depth. /// A new, owned . IChessEngine Create(int skill); + + /// + /// Creates a new engine instance for a single game using an explicitly chosen engine + /// (e.g. for picking a different engine per side in a CPU-vs-CPU game). The caller owns + /// and disposes it. + /// + /// The desired playing strength / search depth. + /// The engine implementation to create. + /// A new, owned . + IChessEngine Create(int skill, ChessEngineKind kind); } diff --git a/JoshHeaps.Net/Services/Interfaces/IComputerMoveOrchestrator.cs b/JoshHeaps.Net/Services/Interfaces/IComputerMoveOrchestrator.cs index 6fcd34d..a4aa7ff 100644 --- a/JoshHeaps.Net/Services/Interfaces/IComputerMoveOrchestrator.cs +++ b/JoshHeaps.Net/Services/Interfaces/IComputerMoveOrchestrator.cs @@ -3,16 +3,24 @@ using JoshHeaps.Net.Models; namespace JoshHeaps.Net.Services.Interfaces; /// -/// Drives a computer move: asks the engine for a move, applies it through the rules -/// service, and broadcasts the result to the game's clients. +/// Drives a computer move: asks the side-to-move's engine for a move, applies it through +/// the rules service, and broadcasts the result to the game's clients. /// public interface IComputerMoveOrchestrator { /// - /// Has the engine pick a move for the current position, applies it, and broadcasts it. + /// Has the side-to-move's engine pick a move for the current position, applies it, and + /// broadcasts it. The engine is taken from the game's per-side computer assignments. /// /// The game to play a move in. - /// The engine that selects the move. /// The applied move and its result. - Task<(MoveDto move, MoveResultDto result)> PlayAsync(GameState state, IChessEngine engine); + Task<(MoveDto move, MoveResultDto result)> PlayAsync(GameState state); + + /// + /// Plays a uniformly-random legal move for the side to move (no engine), applying and + /// broadcasting it. Used to randomize the opening of training games so self-play and + /// engine-vs-engine games don't replay the same line every time. + /// + /// The game to play a random move in. + Task PlayRandomMoveAsync(GameState state); } diff --git a/JoshHeaps.Net/Services/Interfaces/ILearnedWeightsStore.cs b/JoshHeaps.Net/Services/Interfaces/ILearnedWeightsStore.cs new file mode 100644 index 0000000..b0d3491 --- /dev/null +++ b/JoshHeaps.Net/Services/Interfaces/ILearnedWeightsStore.cs @@ -0,0 +1,33 @@ +using JoshHeaps.Net.Models; + +namespace JoshHeaps.Net.Services.Interfaces; + +/// +/// Thin managed facade over the native learned-weights model. The weights, all feature +/// computation, per-game accumulation, the update rule, and persistence live in the native +/// engine; this just points it at the weights file, hands out per-game trainers, and reads +/// the table back for visualization. +/// +public interface ILearnedWeightsStore +{ + /// Absolute path to the weights file the native engine loads and saves. + string WeightsFilePath { get; } + + /// A copy of the current weights (midgame/endgame tables + feature weights). + LearnedWeightsSnapshot Snapshot(); + + /// Creates a per-game training accumulator. The caller owns it (see ). + nint CreateTrainer(); + + /// Records one played position (post-move FEN) into a trainer. + void Record(nint trainer, string fen); + + /// + /// Applies a finished game's outcome to the global weights (and saves): rewards the + /// winner's squares/features, punishes the loser's, scaled by . + /// + void ApplyResult(nint trainer, PieceColor winner, double weight); + + /// Frees a trainer. Safe to call with . + void DestroyTrainer(nint trainer); +} diff --git a/JoshHeaps.Net/wwwroot/css/chess/spectate.css b/JoshHeaps.Net/wwwroot/css/chess/spectate.css index d47db37..297fe93 100644 --- a/JoshHeaps.Net/wwwroot/css/chess/spectate.css +++ b/JoshHeaps.Net/wwwroot/css/chess/spectate.css @@ -21,11 +21,27 @@ html, body { display: flex; align-items: center; justify-content: center; - gap: 0.5rem; + flex-wrap: wrap; + gap: 0.75rem; margin: 1rem 0; } -#cpuDifficulty { +.enginePicker { + display: flex; + align-items: center; + gap: 0.4rem; + border: 1px solid #444; + border-radius: 8px; + padding: 0.3rem 0.6rem 0.5rem; +} + +.enginePicker legend { + color: #9a9a9a; + font-size: 0.8rem; + padding: 0 0.3rem; +} + +#watchControls select { background-color: #1e1e1e; color: #d6d6d6; border: 1px solid #444; diff --git a/JoshHeaps.Net/wwwroot/css/chess/weights.css b/JoshHeaps.Net/wwwroot/css/chess/weights.css new file mode 100644 index 0000000..91e2a90 --- /dev/null +++ b/JoshHeaps.Net/wwwroot/css/chess/weights.css @@ -0,0 +1,187 @@ +body { + margin: 0; + background-color: #141414; + color: #d6d6d6; + font-family: "Segoe UI", system-ui, sans-serif; +} + +#weightsHeader { + text-align: center; + padding: 1.5rem 1rem 0.5rem; +} + +#weightsHeader h1 { + margin: 0 0 0.25rem; + font-size: 1.6rem; +} + +#weightsStatus { + color: #9a9a9a; + margin: 0.25rem 0 1rem; +} + +#weightsControls { + display: flex; + align-items: center; + justify-content: center; + gap: 1.25rem; + margin-bottom: 0.5rem; +} + +.heatLegend { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8rem; + color: #9a9a9a; +} + +.legendBar { + width: 120px; + height: 10px; + border-radius: 5px; + background: linear-gradient(to right, rgba(232, 74, 74, 1), #2a2a2a, rgba(74, 134, 232, 1)); +} + +#refreshWeights { + background-color: #8cd5ed; + color: #262626; + border: 0; + border-radius: 30px; + padding: 0.5rem 1.1rem; + cursor: pointer; +} + +#refreshWeights:hover { + background-color: #a5e0f2; +} + +#backToWatch { + display: inline-block; + margin-top: 0.5rem; + color: #8cd5ed; + text-decoration: none; +} + +.weightsSection { + margin: 0 auto; + max-width: 1200px; + padding: 0.5rem 1rem; +} + +.weightsSection h2 { + text-align: center; + font-size: 1.2rem; + margin: 1rem 0 0.25rem; +} + +.sectionHint { + text-align: center; + color: #9a9a9a; + font-size: 0.8rem; + margin: 0 0 0.75rem; +} + +.weightsGrid { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1.25rem; + padding: 0.5rem 0; +} + +.featurePanel { + max-width: 520px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.featureRow { + display: flex; + align-items: center; + gap: 0.6rem; +} + +.featureLabel { + flex: 0 0 6.5rem; + font-size: 0.8rem; + color: #c8c8c8; + text-align: right; +} + +.featureTrack { + flex: 1 1 auto; + height: 14px; + background-color: #232323; + border-radius: 7px; + overflow: hidden; +} + +.featureBar { + height: 100%; + border-radius: 7px; + min-width: 2px; +} + +.featureValue { + flex: 0 0 3rem; + font-size: 0.8rem; + color: #e8e8e8; + text-align: left; +} + +.weightBoard { + background-color: #1c1c1c; + border: 1px solid #2e2e2e; + border-radius: 10px; + padding: 0.75rem; +} + +.weightBoardHeader { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + font-weight: 600; +} + +.weightBoardIcon { + width: 22px; + height: 22px; +} + +.weightRange { + margin-left: auto; + font-weight: 400; + font-size: 0.75rem; + color: #888; +} + +.miniHeat { + display: grid; + grid-template-columns: 1.1rem repeat(8, 34px); +} + +.heatSquare { + width: 34px; + height: 34px; + box-sizing: border-box; + border: 1px solid #2a2a2a; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.62rem; + color: #f0f0f0; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); +} + +.heatLabel { + display: flex; + align-items: center; + justify-content: center; + padding: 2px 0; + font-size: 0.6rem; + color: #777; +} diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/Spectate.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/Spectate.js index 08f3450..d576798 100644 --- a/JoshHeaps.Net/wwwroot/js/ChessScripts/Spectate.js +++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/Spectate.js @@ -27,10 +27,15 @@ const Spectate = { }, async startCpuGame() { - const difficulty = document.getElementById("cpuDifficulty").value; + const params = new URLSearchParams({ + whiteEngine: document.getElementById("whiteEngine").value, + whiteSkill: document.getElementById("whiteSkill").value, + blackEngine: document.getElementById("blackEngine").value, + blackSkill: document.getElementById("blackSkill").value + }); try { - await fetch(`/api/chess/watch/cpu/${difficulty}`); + await fetch(`/api/chess/watch/cpu?${params}`); await this.refreshGames(); } catch (err) { console.error("❌ Could not start CPU vs CPU game.", err); @@ -68,7 +73,9 @@ const Spectate = { async addGame(game) { this.games.set(game.gameId, { isVsComputer: game.isVsComputer, - isComputerVsComputer: game.isComputerVsComputer + isComputerVsComputer: game.isComputerVsComputer, + whiteEngine: game.whiteEngine, + blackEngine: game.blackEngine }); const card = document.createElement("div"); @@ -183,11 +190,21 @@ const Spectate = { }, gameLabel(stored) { - if (stored.isComputerVsComputer) return "CPU vs CPU"; + if (stored.isComputerVsComputer) + return `${this.engineName(stored.whiteEngine)} (W) vs ${this.engineName(stored.blackEngine)} (B)`; if (stored.isVsComputer) return "Vs CPU"; return "Player vs Player"; }, + engineName(kind) { + switch (kind) { + case "CustomLearned": return "Learned"; + case "Custom": return "Custom"; + case "Stockfish": return "Stockfish"; + default: return kind || "CPU"; + } + }, + headerText(stored, currentPlayer, moveCount, isCheck) { if (stored.result) return `${this.gameLabel(stored)} · move ${moveCount} · final`; diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/Weights.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/Weights.js new file mode 100644 index 0000000..4dff00b --- /dev/null +++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/Weights.js @@ -0,0 +1,158 @@ +const Weights = { + async init() { + await this.refresh(); + }, + + async refresh() { + let data; + + try { + const response = await fetch("/api/chess/weights"); + data = await response.json(); + } catch (err) { + console.error("❌ Could not load weights.", err); + document.getElementById("weightsStatus").textContent = "Could not load weights."; + return; + } + + this.renderBoardSet(data.mg, "weightsGridMg"); + this.renderBoardSet(data.eg, "weightsGridEg"); + this.renderFeatures(data.features); + + const trained = [...data.mg, ...data.eg].some(b => b.squares.some(v => v !== 0)) + || data.features.some(f => f.value !== 0); + + document.getElementById("weightsStatus").textContent = trained + ? "Where the learned engine thinks each piece belongs. Blue = preferred, red = avoided. Midgame vs endgame tables are blended by how much material is left." + : "No training yet — everything is neutral. Run some learned games on the Watch page."; + }, + + // pieces: [{ name, squares[64] }]. Prepends an "Overall" board summing the set. + renderBoardSet(pieces, containerId) { + const overall = new Array(64).fill(0); + for (const piece of pieces) + for (let sq = 0; sq < 64; sq++) + overall[sq] += piece.squares[sq]; + + const boards = [{ name: "Overall", squares: overall }, ...pieces]; + const grid = document.getElementById(containerId); + grid.innerHTML = ""; + boards.forEach(board => grid.appendChild(this.buildBoard(board))); + }, + + buildBoard(board) { + const wrapper = document.createElement("div"); + wrapper.className = "weightBoard"; + + const maxAbs = board.squares.reduce((m, v) => Math.max(m, Math.abs(v)), 0); + + const header = document.createElement("div"); + header.className = "weightBoardHeader"; + if (board.name !== "Overall") { + const icon = document.createElement("img"); + icon.src = `/images/Chess Images/White${board.name}.svg`; + icon.alt = board.name; + icon.className = "weightBoardIcon"; + header.appendChild(icon); + } + const title = document.createElement("span"); + title.textContent = board.name; + header.appendChild(title); + const range = document.createElement("span"); + range.className = "weightRange"; + range.textContent = maxAbs === 0 ? "neutral" : `±${maxAbs}`; + header.appendChild(range); + wrapper.appendChild(header); + + const heat = document.createElement("div"); + heat.className = "miniHeat"; + + for (let row = 0; row < 8; row++) { + const rankLabel = document.createElement("div"); + rankLabel.className = "heatLabel"; + rankLabel.textContent = 8 - row; // rank 8 at top, 1 at bottom + heat.appendChild(rankLabel); + + for (let col = 0; col < 8; col++) { + const rank = 7 - row; // rank index, 0 = rank 1 + const sq = rank * 8 + col; // white-relative square (A1 = 0) + heat.appendChild(this.buildSquare(board.squares[sq], sq, maxAbs)); + } + } + + heat.appendChild(this.cornerSpacer()); + for (let col = 0; col < 8; col++) { + const fileLabel = document.createElement("div"); + fileLabel.className = "heatLabel"; + fileLabel.textContent = String.fromCharCode(97 + col); + heat.appendChild(fileLabel); + } + + wrapper.appendChild(heat); + return wrapper; + }, + + buildSquare(value, sq, maxAbs) { + const cell = document.createElement("div"); + cell.className = "heatSquare"; + + if (value !== 0 && maxAbs > 0) { + const ratio = Math.abs(value) / maxAbs; + const alpha = (0.12 + 0.88 * ratio).toFixed(3); + cell.style.backgroundColor = value > 0 + ? `rgba(74, 134, 232, ${alpha})` // high -> blue + : `rgba(232, 74, 74, ${alpha})`; // low -> red + cell.textContent = value; + } + + const file = String.fromCharCode(97 + (sq & 7)); + const rank = (sq >> 3) + 1; + cell.title = `${file}${rank}: ${value}`; + return cell; + }, + + cornerSpacer() { + const spacer = document.createElement("div"); + spacer.className = "heatLabel"; + return spacer; + }, + + // features: [{ name, value }] + renderFeatures(features) { + const panel = document.getElementById("featureWeights"); + panel.innerHTML = ""; + + const maxAbs = features.reduce((m, f) => Math.max(m, Math.abs(f.value)), 0); + + features.forEach(feature => { + const row = document.createElement("div"); + row.className = "featureRow"; + + const label = document.createElement("span"); + label.className = "featureLabel"; + label.textContent = feature.name; + + const track = document.createElement("div"); + track.className = "featureTrack"; + const bar = document.createElement("div"); + bar.className = "featureBar"; + const ratio = maxAbs === 0 ? 0 : Math.abs(feature.value) / maxAbs; + bar.style.width = `${(ratio * 100).toFixed(1)}%`; + bar.style.backgroundColor = feature.value >= 0 + ? "rgba(74, 134, 232, 0.85)" + : "rgba(232, 74, 74, 0.85)"; + track.appendChild(bar); + + const value = document.createElement("span"); + value.className = "featureValue"; + value.textContent = feature.value; + + row.append(label, track, value); + panel.appendChild(row); + }); + } +}; + +window.addEventListener("load", () => Weights.init()); + +console.log("Weights.js loaded"); diff --git a/native/chess_engine/include/chess_engine.h b/native/chess_engine/include/chess_engine.h index 9caca72..e8af337 100644 --- a/native/chess_engine/include/chess_engine.h +++ b/native/chess_engine/include/chess_engine.h @@ -32,6 +32,9 @@ extern "C" { /* prevent C++ name mangling */ * Internally it points to your engine state object. */ typedef struct ChessEngine* EngineHandle; +/* Opaque per-game training accumulator (see the learned-weights ABI at the bottom). */ +typedef struct Trainer* TrainerHandle; + /* Return codes. 0 == success; negative == error. Keep these values stable. */ enum { CHESS_OK = 0, @@ -79,6 +82,32 @@ CHESS_API int CHESS_CALL engine_version(char* out_buf, int out_len); /* Destroy an instance created by engine_create. Safe to call with NULL. */ CHESS_API void CHESS_CALL engine_destroy(EngineHandle engine); +/* ---- Learned-weights / training ABI ---- + * The learned engine's weights live process-globally here. The host orchestrates games but + * owns no chess logic: it points the engine at the weights file, records each played + * position, and applies the game's result. */ + +/* Load the global learned weights from `path` and remember it for later saves. Idempotent; + * a missing/short file leaves the weights neutral. Call once before learned play/training. */ +CHESS_API void CHESS_CALL learned_load(const char* path); + +/* Copy the global weights out for visualization: 6*64 midgame + 6*64 endgame (PAWN..KING, + * squares 0..63) + feature weights. Returns the count written, or CHESS_ERR_BUFFER if + * out_len is too small (needs >= 776). */ +CHESS_API int CHESS_CALL weights_snapshot(int* out, int out_len); + +/* Create / destroy a per-game training accumulator. Safe to destroy NULL. */ +CHESS_API TrainerHandle CHESS_CALL trainer_create(void); +CHESS_API void CHESS_CALL trainer_destroy(TrainerHandle trainer); + +/* Record one played position (post-move FEN) into the accumulator. */ +CHESS_API void CHESS_CALL trainer_record(TrainerHandle trainer, const char* fen); + +/* Apply a finished game's outcome to the global weights and save: rewards the winner's + * occupied squares / features and punishes the loser's, scaled by `weight` (e.g. 0.5 for a + * material-imbalance draw). winner: 0 = white, 1 = black. */ +CHESS_API void CHESS_CALL trainer_apply(TrainerHandle trainer, int winner, double weight); + #ifdef __cplusplus } #endif diff --git a/native/chess_engine/src/chess_engine.cpp b/native/chess_engine/src/chess_engine.cpp index a674594..c32a252 100644 --- a/native/chess_engine/src/chess_engine.cpp +++ b/native/chess_engine/src/chess_engine.cpp @@ -20,10 +20,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -76,10 +78,63 @@ static chess::Move tt_move (uint64_t d) { return chess::Move(static_cast(static_cast(d >> 48)); } static Bound tt_bound(uint64_t d) { return static_cast(static_cast(d >> 56)); } -/* Internal engine state. One ChessEngine = one game. The table is NOT here: it is the - * shared g_tt above. */ +/* Eval variant for an engine handle. CLASSIC = the hand-crafted evaluate(); LEARNED = + * material + learned phase-split piece-square tables + learned feature weights. */ +enum EvalVariant : int { EVAL_CLASSIC = 0, EVAL_LEARNED = 1 }; + +/* The learned feature knobs (beyond the piece-square tables). Each has one weight learned + * from game outcomes; its activation is computed by compute_features(). Mobility is per + * piece type. Order is fixed — it is the on-disk and snapshot layout after the two tables. */ +enum Feature : int { + FEAT_MOB_N, FEAT_MOB_B, FEAT_MOB_R, FEAT_MOB_Q, /* legal-move counts, per piece type */ + FEAT_PASSED, /* passed pawns, endgame-weighted */ + FEAT_ISOLATED, /* isolated pawns */ + FEAT_DOUBLED, /* doubled pawns */ + FEAT_KING, /* king pawn-shelter, midgame-weighted */ + FEATURE_NB +}; + +/* Per-handle eval configuration, snapshotted from the global learned weights at + * engine_create so the search reads a stable copy. The tables are white-relative: a black + * piece indexes the rank-mirrored square (sq ^ 56). `mg`/`eg` are blended by game phase. + * Indexed by chess::PieceType (PAWN..KING). Only consulted when variant == EVAL_LEARNED. */ +struct EvalParams { + int variant = EVAL_CLASSIC; + int mg[chess::PIECE_TYPE_NB][64] = {}; + int eg[chess::PIECE_TYPE_NB][64] = {}; + int featW[FEATURE_NB] = {}; +}; + +/* Internal engine state. One ChessEngine = one game. The transposition table is NOT + * here: it is the shared g_tt above. */ struct ChessEngine { - int skill = 20; /* 1..20 from the UI; controls search depth */ + int skill = 20; /* 1..20 from the UI; controls search depth */ + EvalParams eval; /* which evaluation the search uses, plus any learned weights */ +}; + +/* The process-global learned weights: the single source of truth, loaded from disk once and + * updated in place by training. Engine handles snapshot it at creation; the visualization + * snapshots it on demand. Guarded by g_weightsMutex for updates/saves (eval reads its own + * per-handle copy, so it never touches this concurrently). */ +struct LearnedWeights { + int mg[chess::PIECE_TYPE_NB][64] = {}; + int eg[chess::PIECE_TYPE_NB][64] = {}; + int featW[FEATURE_NB] = {}; +}; + +static LearnedWeights g_weights; +static std::mutex g_weightsMutex; +static std::string g_weightsPath; + +/* Per-game training accumulator (one per learned CPU-vs-CPU game). Records, per ply, where + * each side's pieces sat (split into midgame/endgame by phase) and each side's feature + * activations; trainer_apply turns the totals into weight nudges. Squares are white-relative + * (black indexes sq ^ 56), so a side's tally lines up with the shared white-relative table. */ +struct Trainer { + double mgOcc[chess::COLOR_NB][chess::PIECE_TYPE_NB][64] = {}; + double egOcc[chess::COLOR_NB][chess::PIECE_TYPE_NB][64] = {}; + double featAcc[chess::COLOR_NB][FEATURE_NB] = {}; + int plies = 0; }; static int copy_out(const char* src, char* out_buf, int out_len) { @@ -108,6 +163,48 @@ static int parse_skill(const char* options, int fallback) { return v < 1 ? 1 : v > 20 ? 20 : v; } +/* "variant=learned" in the options selects the learned eval; anything else is classic. */ +static int parse_variant(const char* options) { + if (!options) return EVAL_CLASSIC; + const char* p = std::strstr(options, "variant="); + if (!p) return EVAL_CLASSIC; + return std::strncmp(p + 8, "learned", 7) == 0 ? EVAL_LEARNED : EVAL_CLASSIC; +} + +/* On-disk format: 6*64 mg ints (PAWN..KING, squares 0..63), then 6*64 eg ints, then + * FEATURE_NB feature ints, whitespace-separated. A missing file or short read leaves the + * rest neutral (0), so an absent weights file just means "train from a blank slate". + * Caller holds g_weightsMutex. */ +static void load_global_weights(const char* path) { + g_weights = LearnedWeights{}; /* reset to neutral before loading */ + + if (!path || !*path) return; + std::ifstream f(path); + if (!f) return; + + for (int pt = chess::PAWN; pt <= chess::KING; ++pt) + for (int sq = 0; sq < 64; ++sq) + if (!(f >> g_weights.mg[pt][sq])) return; + for (int pt = chess::PAWN; pt <= chess::KING; ++pt) + for (int sq = 0; sq < 64; ++sq) + if (!(f >> g_weights.eg[pt][sq])) return; + for (int i = 0; i < FEATURE_NB; ++i) + if (!(f >> g_weights.featW[i])) return; +} + +/* Persist g_weights to g_weightsPath in the format load_global_weights reads. Caller holds the lock. */ +static void save_global_weights() { + if (g_weightsPath.empty()) return; + std::ofstream f(g_weightsPath); + if (!f) return; + + for (int pt = chess::PAWN; pt <= chess::KING; ++pt) + for (int sq = 0; sq < 64; ++sq) f << g_weights.mg[pt][sq] << (sq == 63 ? '\n' : ' '); + for (int pt = chess::PAWN; pt <= chess::KING; ++pt) + for (int sq = 0; sq < 64; ++sq) f << g_weights.eg[pt][sq] << (sq == 63 ? '\n' : ' '); + for (int i = 0; i < FEATURE_NB; ++i) f << g_weights.featW[i] << (i == FEATURE_NB - 1 ? '\n' : ' '); +} + /* Maps the 1..20 difficulty to a search depth. Kept modest: the search has no * quiescence yet, so deep fixed-depth runs get expensive quickly. */ static int depth_for_skill(int skill) { @@ -279,10 +376,136 @@ static int evaluate(const chess::Position& pos) { return score; } +/* ---- Learned (phase-split tables + feature knobs) evaluation --------------------------- + * The model is a linear combination of features whose weights are learned from outcomes: + * eval = Σ pieces [ material + blend(mg, eg, phase) ] + Σ features featW[i]·activation[i] + * compute_features() is the single source of feature activations, used by BOTH the eval here + * and the trainer, so the two can never disagree. Constants below are the only tunables. */ + +/* Per-game-outcome learning rates and clamps. Squares accumulate occupancy (plies on a + * square, summed); features accumulate normalized per-ply activation (averaged, divided by a + * nominal scale so high-magnitude mobility doesn't dwarf the small pawn-structure terms). */ +static constexpr double SQUARE_LR = 0.5; +static constexpr int SQ_CLAMP = 250; +static constexpr double FEAT_LR = 2.0; +static constexpr int FEAT_CLAMP = 500; +static constexpr double FEAT_SCALE[FEATURE_NB] = { 4, 6, 8, 14, 2, 1, 1, 2 }; + +/* Game phase in [0,1] from remaining non-pawn material (PeSTO weights N=B=1, R=2, Q=4; max + * 24 for both full sides): 0 = opening, 1 = bare kings. Drives the mg/eg table blend and + * the phase weighting of the passed-pawn (×phase) and king-safety (×(1−phase)) features. */ +static double game_phase(const chess::Position& pos) { + int npm = chess::popcount(pos.pieces(chess::KNIGHT)) * 1 + + chess::popcount(pos.pieces(chess::BISHOP)) * 1 + + chess::popcount(pos.pieces(chess::ROOK)) * 2 + + chess::popcount(pos.pieces(chess::QUEEN)) * 4; + constexpr int MAX = 24; + if (npm >= MAX) return 0.0; + return double(MAX - npm) / MAX; +} + +/* Blend a midgame and endgame value by phase, rounding per-piece (so training credits a + * square the same way the eval reads it). */ +static int blend(int mg, int eg, double phase) { + return int(std::lround((1.0 - phase) * mg + phase * eg)); +} + +/* Fills `out[FEATURE_NB]` with one color's raw feature activations for a position. The piece- + * square tables handle "where pieces belong"; these capture context a static table can't: + * legal mobility (per piece type, so pins reduce it), passed pawns (endgame-weighted), pawn + * structure, and king shelter (midgame-weighted). Ported nowhere — this is the only copy. */ +static void compute_features(chess::Position& pos, chess::Color c, double phase, double out[FEATURE_NB]) { + for (int i = 0; i < FEATURE_NB; ++i) out[i] = 0.0; + + /* Mobility: legal moves for color c, bucketed by the moving piece's type. */ + chess::MoveList moves; + pos.generate_legal_for(c, moves); + for (int i = 0; i < moves.size(); ++i) { + switch (chess::type_of(pos.piece_on(moves.moves[i].from()))) { + case chess::KNIGHT: out[FEAT_MOB_N] += 1; break; + case chess::BISHOP: out[FEAT_MOB_B] += 1; break; + case chess::ROOK: out[FEAT_MOB_R] += 1; break; + case chess::QUEEN: out[FEAT_MOB_Q] += 1; break; + default: break; + } + } + + /* Pawn structure. */ + chess::Bitboard pawns = pos.pieces(c, chess::PAWN); + chess::Bitboard bb = pawns; + while (bb) { + chess::Square s = chess::pop_lsb(bb); + + if (!(front_span(c, s) & pos.pieces(~c, chess::PAWN))) { /* passed */ + chess::Rank r = chess::rank_of(s); + int toPromotion = (c == chess::WHITE) ? (chess::RANK_8 - r) : (r - chess::RANK_1); + out[FEAT_PASSED] += (6 - toPromotion) * phase; /* 0..5 ranks advanced, late-game */ + } + if (front_span_file_only(c, s) & pawns) /* doubled (friendly pawn ahead) */ + out[FEAT_DOUBLED] += 1; + + chess::File f = chess::file_of(s); + chess::Bitboard adjacent = 0; + if (f > chess::FILE_A) adjacent |= chess::file_bb(chess::File(f - 1)); + if (f < chess::FILE_H) adjacent |= chess::file_bb(chess::File(f + 1)); + if (!(adjacent & pawns)) /* isolated */ + out[FEAT_ISOLATED] += 1; + } + + /* King safety: friendly pawns sheltering the king (its file + adjacent files, the two + * ranks in front), worth more in the midgame. */ + chess::Square k = pos.king_square(c); + chess::File kf = chess::file_of(k); + chess::Rank kr = chess::rank_of(k); + chess::Bitboard kingFiles = chess::file_bb(kf); + if (kf > chess::FILE_A) kingFiles |= chess::file_bb(chess::File(kf - 1)); + if (kf < chess::FILE_H) kingFiles |= chess::file_bb(chess::File(kf + 1)); + chess::Bitboard shelterRanks = 0; + for (int d = 1; d <= 2; ++d) { + int rr = (c == chess::WHITE) ? (kr + d) : (kr - d); + if (rr >= 0 && rr <= 7) shelterRanks |= (0xFFULL << (8 * rr)); + } + out[FEAT_KING] += chess::popcount(kingFiles & shelterRanks & pawns) * (1.0 - phase); +} + +/* Learned eval (white-positive/absolute, like evaluate()): material + phase-blended piece- + * square tables + learned feature weights. Black pieces index the rank-mirrored square + * (s ^ 56) so both colors share one white-relative table. Non-const because mobility + * generates legal moves (which the position's move generator does via do/undo). */ +static int evaluateLearned(chess::Position& pos, const EvalParams& ep) { + double phase = game_phase(pos); + int score = 0; + + chess::Bitboard white = pos.pieces(chess::WHITE); + while (white) { + chess::Square s = chess::pop_lsb(white); + chess::PieceType pt = chess::type_of(pos.piece_on(s)); + score += piece_value(pt) + blend(ep.mg[pt][s], ep.eg[pt][s], phase); + } + + chess::Bitboard black = pos.pieces(chess::BLACK); + while (black) { + chess::Square s = chess::pop_lsb(black); + chess::PieceType pt = chess::type_of(pos.piece_on(s)); + score -= piece_value(pt) + blend(ep.mg[pt][s ^ 56], ep.eg[pt][s ^ 56], phase); + } + + double wFeat[FEATURE_NB], bFeat[FEATURE_NB]; + compute_features(pos, chess::WHITE, phase, wFeat); + compute_features(pos, chess::BLACK, phase, bFeat); + + double feature = 0.0; + for (int i = 0; i < FEATURE_NB; ++i) + feature += ep.featW[i] * (wFeat[i] - bFeat[i]) / FEAT_SCALE[i]; + score += int(std::lround(feature)); + + return score; +} + /* evaluate() is white-positive (absolute). Negamax needs it relative to the side to * move, so flip the sign when black is to move. */ -static int evaluate_stm(const chess::Position& pos, bool whiteToMove) { - int s = evaluate(pos); +static int evaluate_stm(chess::Position& pos, bool whiteToMove, const EvalParams& ep) { + int s = (ep.variant == EVAL_LEARNED) ? evaluateLearned(pos, ep) : evaluate(pos); return whiteToMove ? s : -s; } @@ -358,6 +581,15 @@ CHESS_API EngineHandle CHESS_CALL engine_create(const char* options) { auto* e = new (std::nothrow) ChessEngine(); if (!e) return nullptr; e->skill = parse_skill(options, e->skill); + e->eval.variant = parse_variant(options); + if (e->eval.variant == EVAL_LEARNED) { + /* Snapshot the current global weights so the search reads a stable copy (training + * updates the global between games; the weights path is owned by learned_load). */ + std::lock_guard lock(g_weightsMutex); + std::memcpy(e->eval.mg, g_weights.mg, sizeof e->eval.mg); + std::memcpy(e->eval.eg, g_weights.eg, sizeof e->eval.eg); + std::memcpy(e->eval.featW, g_weights.featW, sizeof e->eval.featW); + } return e; } @@ -376,8 +608,9 @@ CHESS_API int CHESS_CALL engine_set_option(EngineHandle engine, static constexpr int MAX_PLY = 128; /* ply never exceeds maxDepth (<= 20) */ struct SearchContext { - uint64_t nodes = 0; - chess::Move killers[MAX_PLY][2] = {}; /* [ply][slot]; MOVE_NONE until filled */ + uint64_t nodes = 0; + const EvalParams* eval = nullptr; /* eval config for this search; set by engine_best_move */ + chess::Move killers[MAX_PLY][2] = {};/* [ply][slot]; MOVE_NONE until filled */ }; /* Negamax alpha-beta over the shared transposition table. `maxDepth` is the searching @@ -394,7 +627,7 @@ static int negamax(chess::Position& pos, int maxDepth, int depth, int ply, return 0; if (depth <= 0) - return evaluate_stm(pos, whiteToMove); + return evaluate_stm(pos, whiteToMove, *ctx.eval); const uint64_t key = pos.key(); TTEntry& slot = g_tt.entries[key & g_tt.mask]; @@ -509,6 +742,7 @@ CHESS_API int CHESS_CALL engine_best_move(EngineHandle engine, return CHESS_ERR_NO_MOVE; SearchContext ctx; + ctx.eval = &engine->eval; int maxDepth = depth_for_skill(engine->skill); chess::Move bestMove = moves.moves[0]; /* guaranteed-legal fallback */ @@ -554,4 +788,100 @@ CHESS_API void CHESS_CALL engine_destroy(EngineHandle engine) { delete engine; /* delete nullptr is safe */ } +/* ---- Learned-weights / training C ABI -------------------------------------------------- + * The managed side orchestrates games but owns no chess logic: it tells the engine where + * to load/save the global weights, records each played position, and applies the result. */ + +CHESS_API void CHESS_CALL learned_load(const char* path) { + std::lock_guard lock(g_weightsMutex); + g_weightsPath = path ? path : ""; + load_global_weights(path); +} + +CHESS_API int CHESS_CALL weights_snapshot(int* out, int out_len) { + const int need = 6 * 64 * 2 + FEATURE_NB; /* mg + eg (PAWN..KING) + features = 776 */ + if (!out || out_len < need) return CHESS_ERR_BUFFER; + + std::lock_guard lock(g_weightsMutex); + int n = 0; + for (int pt = chess::PAWN; pt <= chess::KING; ++pt) + for (int sq = 0; sq < 64; ++sq) out[n++] = g_weights.mg[pt][sq]; + for (int pt = chess::PAWN; pt <= chess::KING; ++pt) + for (int sq = 0; sq < 64; ++sq) out[n++] = g_weights.eg[pt][sq]; + for (int i = 0; i < FEATURE_NB; ++i) out[n++] = g_weights.featW[i]; + return n; +} + +CHESS_API TrainerHandle CHESS_CALL trainer_create(void) { + return new (std::nothrow) Trainer(); +} + +CHESS_API void CHESS_CALL trainer_record(TrainerHandle t, const char* fen) { + if (!t || !fen || !*fen) return; + ensure_initialized(); + + chess::Position pos = chess::Position::from_fen(fen); + double phase = game_phase(pos); + + /* Per-square occupancy, split into midgame/endgame by phase, white-relative. */ + chess::Bitboard occ = pos.pieces(); + while (occ) { + chess::Square s = chess::pop_lsb(occ); + chess::Piece pc = pos.piece_on(s); + chess::Color c = chess::color_of(pc); + chess::PieceType pt = chess::type_of(pc); + int relSq = (c == chess::WHITE) ? int(s) : (int(s) ^ 56); + t->mgOcc[c][pt][relSq] += (1.0 - phase); + t->egOcc[c][pt][relSq] += phase; + } + + /* Per-side feature activations. */ + double w[FEATURE_NB], b[FEATURE_NB]; + compute_features(pos, chess::WHITE, phase, w); + compute_features(pos, chess::BLACK, phase, b); + for (int i = 0; i < FEATURE_NB; ++i) { + t->featAcc[chess::WHITE][i] += w[i]; + t->featAcc[chess::BLACK][i] += b[i]; + } + + t->plies++; +} + +CHESS_API void CHESS_CALL trainer_apply(TrainerHandle t, int winner, double weight) { + if (!t) return; + + std::lock_guard lock(g_weightsMutex); + + /* pass 0 = winner (reward, +1); pass 1 = loser (punish, -1). */ + for (int pass = 0; pass < 2; ++pass) { + chess::Color side = chess::Color((pass == 0 ? winner : (winner ^ 1)) & 1); + int sign = pass == 0 ? 1 : -1; + + for (int pt = chess::PAWN; pt <= chess::KING; ++pt) + for (int sq = 0; sq < 64; ++sq) { + if (t->mgOcc[side][pt][sq] != 0.0) { + int d = sign * int(std::lround(SQUARE_LR * t->mgOcc[side][pt][sq] * weight)); + g_weights.mg[pt][sq] = std::clamp(g_weights.mg[pt][sq] + d, -SQ_CLAMP, SQ_CLAMP); + } + if (t->egOcc[side][pt][sq] != 0.0) { + int d = sign * int(std::lround(SQUARE_LR * t->egOcc[side][pt][sq] * weight)); + g_weights.eg[pt][sq] = std::clamp(g_weights.eg[pt][sq] + d, -SQ_CLAMP, SQ_CLAMP); + } + } + + if (t->plies > 0) + for (int i = 0; i < FEATURE_NB; ++i) { + double avg = t->featAcc[side][i] / t->plies; /* per-ply average, normalized */ + int d = sign * int(std::lround(FEAT_LR * (avg / FEAT_SCALE[i]) * weight)); + g_weights.featW[i] = std::clamp(g_weights.featW[i] + d, -FEAT_CLAMP, FEAT_CLAMP); + } + } + + save_global_weights(); +} + +CHESS_API void CHESS_CALL trainer_destroy(TrainerHandle t) { + delete t; /* delete nullptr is safe */ +} + } /* extern "C" */ diff --git a/native/chess_engine/src/position.h b/native/chess_engine/src/position.h index 8976394..f615ffb 100644 --- a/native/chess_engine/src/position.h +++ b/native/chess_engine/src/position.h @@ -47,6 +47,22 @@ public: void do_move(Move m); void undo_move(Move m); + // Legal moves for a SPECIFIED color (for mobility eval of either side). When c is not + // the side to move, temporarily flips side-to-move (and clears the en-passant square, + // which belongs to the other side) so generate_legal runs for c, then restores. The + // Zobrist key is untouched and unused by move generation, so this leaves the position + // observably unchanged. + void generate_legal_for(Color c, MoveList& list) { + if (sideToMove == c) { generate_legal(list); return; } + Color savedSide = sideToMove; + Square savedEp = epSquare; + sideToMove = c; + epSquare = SQ_NONE; + generate_legal(list); + sideToMove = savedSide; + epSquare = savedEp; + } + // Seed prior-position keys (oldest first, excluding the current position) so // is_draw() can see game history the FEN doesn't carry. Call once, right after // from_fen and before any do_move.