Removed memory allocations from IsInCheckmate and IsInStalemate

This commit is contained in:
Sebastian Lague 2023-07-30 22:01:17 +02:00
parent a6c102ba30
commit 30644066b1
3 changed files with 154 additions and 38 deletions

View file

@ -20,6 +20,8 @@ namespace ChessChallenge.API
bool hasCachedMoves;
Move[] cachedLegalCaptureMoves;
bool hasCachedCaptureMoves;
bool hasCachedMoveCount;
int cachedMoveCount;
int depth;
/// <summary>
@ -154,6 +156,8 @@ namespace ChessChallenge.API
moveGen.GenerateMoves(ref moveSpan, board, includeQuietMoves: true);
cachedLegalMoves = moveSpan.ToArray();
hasCachedMoves = true;
hasCachedMoveCount = true;
cachedMoveCount = moveSpan.Length;
}
return cachedLegalMoves;
@ -169,6 +173,8 @@ namespace ChessChallenge.API
{
bool includeQuietMoves = !capturesOnly;
moveGen.GenerateMoves(ref moveList, board, includeQuietMoves);
hasCachedMoveCount = true;
cachedMoveCount = moveList.Length;
}
@ -187,12 +193,12 @@ namespace ChessChallenge.API
/// <summary>
/// Test if the player to move is in check in the current position.
/// </summary>
public bool IsInCheck() => board.IsInCheck();
public bool IsInCheck() => moveGen.IsInitialized ? moveGen.InCheck() : board.IsInCheck();
/// <summary>
/// Test if the current position is checkmate
/// </summary>
public bool IsInCheckmate() => IsInCheck() && GetLegalMoves().Length == 0;
public bool IsInCheckmate() => IsInCheck() && HasZeroLegalMoves();
/// <summary>
/// Test if the current position is a draw due stalemate, repetition, insufficient material, or 50-move rule.
@ -202,11 +208,18 @@ namespace ChessChallenge.API
public bool IsDraw()
{
return IsFiftyMoveDraw() || IsInsufficientMaterial() || IsInStalemate() || IsRepeatedPosition();
bool IsInStalemate() => !IsInCheck() && GetLegalMoves().Length == 0;
bool IsFiftyMoveDraw() => board.currentGameState.fiftyMoveCounter >= 100;
}
/// <summary>
/// Test if the current position is a draw due to stalemate
/// </summary>
public bool IsInStalemate() => !IsInCheck() && HasZeroLegalMoves();
/// <summary>
/// Test if the current position is a draw due to the fifty move rule
/// </summary>
public bool IsFiftyMoveDraw() => board.currentGameState.fiftyMoveCounter >= 100;
/// <summary>
/// Test if the current position has occurred at least once before on the board.
/// This includes both positions in the actual game, and positions reached by
@ -362,6 +375,16 @@ namespace ChessChallenge.API
moveGen.NotifyPositionChanged();
hasCachedMoves = false;
hasCachedCaptureMoves = false;
hasCachedMoveCount = false;
}
bool HasZeroLegalMoves()
{
if (hasCachedMoveCount)
{
return cachedMoveCount == 0;
}
return moveGen.NoLegalMovesInPosition(board);
}
}

View file

@ -52,6 +52,8 @@ namespace ChessChallenge.Application.APIHelpers
board = new Board();
}
public bool IsInitialized => hasInitializedCurrentPosition;
// Movegen needs to know when position has changed to allow for some caching optims in api
public void NotifyPositionChanged()
{
@ -64,6 +66,27 @@ namespace ChessChallenge.Application.APIHelpers
return opponentAttackMap;
}
public bool NoLegalMovesInPosition(Board board)
{
Span<API.Move> moves = stackalloc API.Move[128];
generateNonCapture = true;
Init(board);
GenerateKingMoves(moves);
if (currMoveIndex > 0) { return false; }
if (!inDoubleCheck)
{
GenerateKnightMoves(moves);
if (currMoveIndex > 0) { return false; }
GeneratePawnMoves(moves);
if (currMoveIndex > 0) { return false; }
GenerateSlidingMoves(moves, true);
if (currMoveIndex > 0) { return false; }
}
return true;
}
// Generates list of legal moves in current position.
// Quiet moves (non captures) can optionally be excluded. This is used in quiescence search.
public void GenerateMoves(ref Span<API.Move> moves, Board board, bool includeQuietMoves = true)
@ -187,7 +210,7 @@ namespace ChessChallenge.Application.APIHelpers
}
}
void GenerateSlidingMoves(Span<API.Move> moves)
void GenerateSlidingMoves(Span<API.Move> moves, bool exitEarly = false)
{
// Limit movement to empty or enemy squares, and must block check if king is in check.
ulong moveMask = emptyOrEnemySquares & checkRayBitmask & moveTypeMask;
@ -218,6 +241,10 @@ namespace ChessChallenge.Application.APIHelpers
{
int targetSquare = BitBoardUtility.PopLSB(ref moveSquares);
moves[currMoveIndex++] = CreateAPIMove(startSquare, targetSquare, 0);
if (exitEarly)
{
return;
}
}
}
@ -237,6 +264,10 @@ namespace ChessChallenge.Application.APIHelpers
{
int targetSquare = BitBoardUtility.PopLSB(ref moveSquares);
moves[currMoveIndex++] = CreateAPIMove(startSquare, targetSquare, 0);
if (exitEarly)
{
return;
}
}
}
}

View file

@ -18,10 +18,12 @@ namespace ChessChallenge.Application
{
anyFailed = false;
new SearchTest().Run(false);
new SearchTest().Run(true);
new SearchTest2().Run();
new SearchTest3().Run();
RepetitionTest();
DrawTest();
MoveGenTest();
PieceListTest();
@ -29,9 +31,6 @@ namespace ChessChallenge.Application
MiscTest();
TestBitboards();
TestMoveCreate();
new SearchTest().Run(false);
new SearchTest().Run(true);
if (runPerft)
{
@ -654,6 +653,69 @@ namespace ChessChallenge.Application
Console.ResetColor();
}
public class SearchTest3
{
API.Board board;
int numCaptures;
int numChecks;
int numMates;
int nodes;
public void Run()
{
Console.WriteLine("Running misc search test");
Chess.Board b = new();
b.LoadPosition("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - ");
board = new API.Board(b);
var sw = System.Diagnostics.Stopwatch.StartNew();
Search(4, API.Move.NullMove);
sw.Stop();
Console.WriteLine("Test3 time: " + sw.ElapsedMilliseconds + " ms");
bool passed = nodes == 4085603 && numCaptures == 757163 && numChecks == 25523 && numMates == 43;
Assert(passed, "Test3 failed");
}
void Search(int depth, API.Move prevMove)
{
Span<API.Move> moveSpan = stackalloc API.Move[256];
board.GetLegalMovesNonAlloc(ref moveSpan);
if (depth == 0)
{
if (prevMove.IsCapture)
{
numCaptures++;
}
if (board.IsInCheck())
{
numChecks++;
}
if (board.IsInCheckmate())
{
numMates++;
}
nodes += 1;
return;
}
foreach (var move in moveSpan)
{
board.MakeMove(move);
Search(depth - 1, move);
board.UndoMove(move);
}
}
}
public class SearchTest2
{
API.Board board;
@ -778,7 +840,7 @@ namespace ChessChallenge.Application
this.useStackalloc = useStackalloc;
Console.WriteLine("Running misc search test | stackalloc = " + useStackalloc);
Chess.Board b = new();
b.LoadPosition("1r4k1/2P1r1pp/3p4/4n1Q1/1p6/2PB3P/P3pPP1/2B3K1 w - - 7 16");
b.LoadPosition("2rqk2r/5p1p/p2p1n2/1pPPn3/8/3B1QP1/PR1K1P1p/2B1R3 w k b6 0 28");
board = new API.Board(b);
Search(4);
@ -791,19 +853,19 @@ namespace ChessChallenge.Application
numCalls++;
var square = new Square(numCalls % 64);
miscSumTest += (int)boardAPI.GetPiece(square).PieceType;
miscSumTest += boardAPI.GetAllPieceLists()[numCalls % 12].Count;
miscSumTest += (long)(boardAPI.ZobristKey % 100);
miscSumTest += boardAPI.IsInCheckmate() ? 1 : 0;
miscSumTest += (int)board.GetPiece(square).PieceType;
miscSumTest += board.GetAllPieceLists()[numCalls % 12].Count;
miscSumTest += (long)(board.ZobristKey % 100);
miscSumTest += board.IsInCheckmate() ? 1 : 0;
if (numCalls % 6 == 0)
{
miscSumTest += boardAPI.IsInCheck() ? 1 : 0;
miscSumTest += board.IsInCheck() ? 1 : 0;
}
if (numCalls % 18 == 0)
{
miscSumTest += boardAPI.SquareIsAttackedByOpponent(square) ? 1 : 0;
miscSumTest += board.SquareIsAttackedByOpponent(square) ? 1 : 0;
}
if (plyRemaining == 0)
@ -814,10 +876,10 @@ namespace ChessChallenge.Application
if (numCalls % 3 == 0 && plyRemaining > 2)
{
if (boardAPI.TrySkipTurn())
if (board.TrySkipTurn())
{
Search(plyRemaining - 2);
boardAPI.UndoSkipTurn();
board.UndoSkipTurn();
}
}
@ -826,19 +888,19 @@ namespace ChessChallenge.Application
if (useStackalloc)
{
Span<API.Move> moveSpan = stackalloc API.Move[256];
boardAPI.GetLegalMovesNonAlloc(ref moveSpan);
board.GetLegalMovesNonAlloc(ref moveSpan);
moves = moveSpan.ToArray(); // (don't actually care about allocations here, just testing the func)
}
else
{
moves = boardAPI.GetLegalMoves();
moves = board.GetLegalMoves();
}
foreach (var move in moves)
{
boardAPI.MakeMove(move);
board.MakeMove(move);
Search(plyRemaining - 1);
boardAPI.UndoMove(move);
board.UndoMove(move);
}