Gitea/workflows #1
@@ -40,6 +40,10 @@ public class GameState
|
|||||||
// Position keys (FEN placement/side/castling/en-passant) for threefold-repetition detection.
|
// Position keys (FEN placement/side/castling/en-passant) for threefold-repetition detection.
|
||||||
public List<string> PositionHistory { get; set; }
|
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).
|
// A list of all pieces to quickly reference them (optional but convenient).
|
||||||
// Alternatively, you can iterate the Board array.
|
// Alternatively, you can iterate the Board array.
|
||||||
public List<ChessPiece> Pieces { get; set; }
|
public List<ChessPiece> Pieces { get; set; }
|
||||||
|
|||||||
@@ -85,11 +85,34 @@ public static class ChessEngineHelpers
|
|||||||
|
|
||||||
/* 5-6) half-move clock + full-move number */
|
/* 5-6) half-move clock + full-move number */
|
||||||
int fullMoves = gs.MoveHistory.Count / 2 + 1;
|
int fullMoves = gs.MoveHistory.Count / 2 + 1;
|
||||||
sb.Append(" 0 ").Append(fullMoves);
|
sb.Append(' ').Append(gs.HalfmoveClock).Append(' ').Append(fullMoves);
|
||||||
|
|
||||||
return sb.ToString();
|
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 ---------- */
|
/* ---------- helpers ---------- */
|
||||||
|
|
||||||
private static string GetCastlingFlags(GameState gs)
|
private static string GetCastlingFlags(GameState gs)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public class ChessService : IChessService
|
|||||||
gameState.BlackCanCastleKingside = true;
|
gameState.BlackCanCastleKingside = true;
|
||||||
gameState.BlackCanCastleQueenside = true;
|
gameState.BlackCanCastleQueenside = true;
|
||||||
gameState.EnPassantTarget = null;
|
gameState.EnPassantTarget = null;
|
||||||
|
gameState.HalfmoveClock = 0;
|
||||||
|
|
||||||
SetupBlackPieces(gameState);
|
SetupBlackPieces(gameState);
|
||||||
SetupWhitePieces(gameState);
|
SetupWhitePieces(gameState);
|
||||||
@@ -178,6 +179,7 @@ public class ChessService : IChessService
|
|||||||
{
|
{
|
||||||
var oldPos = piece.Position;
|
var oldPos = piece.Position;
|
||||||
var captured = gs.Board[targetPos.Row, targetPos.Col];
|
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);
|
HandleEnPassantIfNeeded(gs, piece, targetPos, ref captured);
|
||||||
|
|
||||||
@@ -199,6 +201,9 @@ public class ChessService : IChessService
|
|||||||
|
|
||||||
UpdateCastlingRights(gs, piece, oldPos);
|
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
|
gs.CurrentPlayer = gs.CurrentPlayer == PieceColor.White
|
||||||
? PieceColor.Black
|
? PieceColor.Black
|
||||||
: PieceColor.White;
|
: PieceColor.White;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public sealed class ComputerMoveOrchestrator(
|
|||||||
{
|
{
|
||||||
public async Task<(MoveDto move, MoveResultDto result)> PlayAsync(GameState state, IChessEngine engine)
|
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(
|
var move = uci.ToMoveDto(
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ public sealed partial class CustomChessEngine : IChessEngine
|
|||||||
_handle = new EngineSafeHandle(handle);
|
_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
|
const int bufferLength = 16; // longest UCI move is 5 chars ("e7e8q") + NUL
|
||||||
byte* buffer = stackalloc byte[bufferLength];
|
byte* buffer = stackalloc byte[bufferLength];
|
||||||
@@ -40,7 +41,7 @@ public sealed partial class CustomChessEngine : IChessEngine
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_handle.DangerousAddRef(ref added);
|
_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)
|
if (code != 0)
|
||||||
throw new InvalidOperationException($"Native chess engine failed to produce a move for FEN '{fen}' (engine_best_move returned {code}).");
|
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)]
|
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
|
||||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
[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)]
|
[LibraryImport(LibName)]
|
||||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||||
|
|||||||
@@ -74,8 +74,9 @@ public sealed class Stockfish : IChessEngine
|
|||||||
WaitFor("readyok").GetAwaiter().GetResult();
|
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($"position fen {fen}");
|
||||||
Send($"go depth {_skill}");
|
Send($"go depth {_skill}");
|
||||||
string? best = null;
|
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").
|
/// Returns the engine's chosen move in UCI long-algebraic form (e.g. "e2e4", "e7e8q").
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fen">The current position as a FEN string.</param>
|
/// <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>
|
/// <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