Feature/custom chess engine #2
@@ -40,6 +40,10 @@ public class GameState
|
||||
// Position keys (FEN placement/side/castling/en-passant) for threefold-repetition detection.
|
||||
public List<string> PositionHistory { get; set; }
|
||||
|
||||
// Half-moves since the last capture or pawn move (the FEN 50-move clock). Also tells
|
||||
// us how many trailing PositionHistory entries belong to the current repetition window.
|
||||
public int HalfmoveClock { get; set; }
|
||||
|
||||
// A list of all pieces to quickly reference them (optional but convenient).
|
||||
// Alternatively, you can iterate the Board array.
|
||||
public List<ChessPiece> Pieces { get; set; }
|
||||
|
||||
@@ -85,11 +85,34 @@ public static class ChessEngineHelpers
|
||||
|
||||
/* 5-6) half-move clock + full-move number */
|
||||
int fullMoves = gs.MoveHistory.Count / 2 + 1;
|
||||
sb.Append(" 0 ").Append(fullMoves);
|
||||
sb.Append(' ').Append(gs.HalfmoveClock).Append(' ').Append(fullMoves);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The prior positions since the last irreversible move (capture/pawn move), as
|
||||
/// completed FEN strings, oldest first and excluding the current position. This is
|
||||
/// the repetition window the engine needs to detect threefold/50-move draws that a
|
||||
/// single FEN can't express.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> RepetitionHistory(this GameState gs)
|
||||
{
|
||||
int clock = gs.HalfmoveClock;
|
||||
int count = gs.PositionHistory.Count;
|
||||
int start = count - 1 - clock; // PositionHistory ends with the current position
|
||||
|
||||
if (clock <= 0 || start < 0)
|
||||
return Array.Empty<string>();
|
||||
|
||||
var fens = new List<string>(clock);
|
||||
|
||||
for (int i = start; i < count - 1; i++)
|
||||
fens.Add(gs.PositionHistory[i] + " 0 1"); // complete the 4-field key into a parseable FEN
|
||||
|
||||
return fens;
|
||||
}
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
|
||||
private static string GetCastlingFlags(GameState gs)
|
||||
|
||||
@@ -28,6 +28,7 @@ public class ChessService : IChessService
|
||||
gameState.BlackCanCastleKingside = true;
|
||||
gameState.BlackCanCastleQueenside = true;
|
||||
gameState.EnPassantTarget = null;
|
||||
gameState.HalfmoveClock = 0;
|
||||
|
||||
SetupBlackPieces(gameState);
|
||||
SetupWhitePieces(gameState);
|
||||
@@ -178,6 +179,7 @@ public class ChessService : IChessService
|
||||
{
|
||||
var oldPos = piece.Position;
|
||||
var captured = gs.Board[targetPos.Row, targetPos.Col];
|
||||
var isPawnMove = piece.Type == PieceType.Pawn; // captured before promotion can change Type
|
||||
|
||||
HandleEnPassantIfNeeded(gs, piece, targetPos, ref captured);
|
||||
|
||||
@@ -199,6 +201,9 @@ public class ChessService : IChessService
|
||||
|
||||
UpdateCastlingRights(gs, piece, oldPos);
|
||||
|
||||
// Reset the 50-move clock on captures and pawn moves (irreversible); else advance it.
|
||||
gs.HalfmoveClock = (isPawnMove || captured != null) ? 0 : gs.HalfmoveClock + 1;
|
||||
|
||||
gs.CurrentPlayer = gs.CurrentPlayer == PieceColor.White
|
||||
? PieceColor.Black
|
||||
: PieceColor.White;
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class ComputerMoveOrchestrator(
|
||||
{
|
||||
public async Task<(MoveDto move, MoveResultDto result)> PlayAsync(GameState state, IChessEngine engine)
|
||||
{
|
||||
var uci = await engine.GetBestMoveAsync(state.ToFen());
|
||||
var uci = await engine.GetBestMoveAsync(state.ToFen(), state.RepetitionHistory());
|
||||
|
||||
var move = uci.ToMoveDto(
|
||||
state,
|
||||
|
||||
@@ -28,9 +28,10 @@ public sealed partial class CustomChessEngine : IChessEngine
|
||||
_handle = new EngineSafeHandle(handle);
|
||||
}
|
||||
|
||||
public Task<string> GetBestMoveAsync(string fen) => Task.Run(() => GetBestMove(fen));
|
||||
public Task<string> GetBestMoveAsync(string fen, IReadOnlyList<string> historyFens) =>
|
||||
Task.Run(() => GetBestMove(fen, string.Join('\n', historyFens)));
|
||||
|
||||
private unsafe string GetBestMove(string fen)
|
||||
private unsafe string GetBestMove(string fen, string history)
|
||||
{
|
||||
const int bufferLength = 16; // longest UCI move is 5 chars ("e7e8q") + NUL
|
||||
byte* buffer = stackalloc byte[bufferLength];
|
||||
@@ -40,7 +41,7 @@ public sealed partial class CustomChessEngine : IChessEngine
|
||||
try
|
||||
{
|
||||
_handle.DangerousAddRef(ref added);
|
||||
var code = NativeMethods.engine_best_move(_handle.DangerousGetHandle(), fen, buffer, bufferLength);
|
||||
var code = NativeMethods.engine_best_move(_handle.DangerousGetHandle(), fen, history, buffer, bufferLength);
|
||||
|
||||
if (code != 0)
|
||||
throw new InvalidOperationException($"Native chess engine failed to produce a move for FEN '{fen}' (engine_best_move returned {code}).");
|
||||
@@ -109,7 +110,7 @@ public sealed partial class CustomChessEngine : IChessEngine
|
||||
|
||||
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static unsafe partial int engine_best_move(IntPtr engine, string fen, byte* outBuffer, int outLength);
|
||||
internal static unsafe partial int engine_best_move(IntPtr engine, string fen, string history, byte* outBuffer, int outLength);
|
||||
|
||||
[LibraryImport(LibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
|
||||
@@ -74,8 +74,9 @@ public sealed class Stockfish : IChessEngine
|
||||
WaitFor("readyok").GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<string> GetBestMoveAsync(string fen)
|
||||
public async Task<string> GetBestMoveAsync(string fen, IReadOnlyList<string> historyFens)
|
||||
{
|
||||
// historyFens is unused: Stockfish tracks repetition from the position it's given.
|
||||
Send($"position fen {fen}");
|
||||
Send($"go depth {_skill}");
|
||||
string? best = null;
|
||||
|
||||
@@ -14,6 +14,11 @@ public interface IChessEngine : IAsyncDisposable
|
||||
/// Returns the engine's chosen move in UCI long-algebraic form (e.g. "e2e4", "e7e8q").
|
||||
/// </summary>
|
||||
/// <param name="fen">The current position as a FEN string.</param>
|
||||
/// <param name="historyFens">
|
||||
/// Prior positions since the last irreversible move, oldest first, excluding the
|
||||
/// current one — lets the engine detect threefold/50-move draws the FEN can't carry.
|
||||
/// May be empty.
|
||||
/// </param>
|
||||
/// <returns>The selected move as a UCI string.</returns>
|
||||
Task<string> GetBestMoveAsync(string fen);
|
||||
Task<string> GetBestMoveAsync(string fen, IReadOnlyList<string> historyFens);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user