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

View file

@ -51,7 +51,9 @@ namespace ChessChallenge.Application.APIHelpers
{ {
board = new Board(); board = new Board();
} }
public bool IsInitialized => hasInitializedCurrentPosition;
// Movegen needs to know when position has changed to allow for some caching optims in api // Movegen needs to know when position has changed to allow for some caching optims in api
public void NotifyPositionChanged() public void NotifyPositionChanged()
{ {
@ -64,6 +66,27 @@ namespace ChessChallenge.Application.APIHelpers
return opponentAttackMap; 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. // Generates list of legal moves in current position.
// Quiet moves (non captures) can optionally be excluded. This is used in quiescence search. // 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) public void GenerateMoves(ref Span<API.Move> moves, Board board, bool includeQuietMoves = true)
@ -96,17 +119,17 @@ namespace ChessChallenge.Application.APIHelpers
this.board = board; this.board = board;
currMoveIndex = 0; currMoveIndex = 0;
if (hasInitializedCurrentPosition) if (hasInitializedCurrentPosition)
{ {
moveTypeMask = generateNonCapture ? ulong.MaxValue : enemyPieces; moveTypeMask = generateNonCapture ? ulong.MaxValue : enemyPieces;
return; return;
} }
hasInitializedCurrentPosition = true; hasInitializedCurrentPosition = true;
// Reset state // Reset state
inCheck = false; inCheck = false;
inDoubleCheck = false; inDoubleCheck = false;
checkRayBitmask = 0; checkRayBitmask = 0;
@ -131,7 +154,7 @@ namespace ChessChallenge.Application.APIHelpers
CalculateAttackData(); CalculateAttackData();
} }
API.Move CreateAPIMove(int startSquare, int targetSquare, int flag) API.Move CreateAPIMove(int startSquare, int targetSquare, int flag)
@ -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. // Limit movement to empty or enemy squares, and must block check if king is in check.
ulong moveMask = emptyOrEnemySquares & checkRayBitmask & moveTypeMask; ulong moveMask = emptyOrEnemySquares & checkRayBitmask & moveTypeMask;
@ -218,6 +241,10 @@ namespace ChessChallenge.Application.APIHelpers
{ {
int targetSquare = BitBoardUtility.PopLSB(ref moveSquares); int targetSquare = BitBoardUtility.PopLSB(ref moveSquares);
moves[currMoveIndex++] = CreateAPIMove(startSquare, targetSquare, 0); moves[currMoveIndex++] = CreateAPIMove(startSquare, targetSquare, 0);
if (exitEarly)
{
return;
}
} }
} }
@ -237,6 +264,10 @@ namespace ChessChallenge.Application.APIHelpers
{ {
int targetSquare = BitBoardUtility.PopLSB(ref moveSquares); int targetSquare = BitBoardUtility.PopLSB(ref moveSquares);
moves[currMoveIndex++] = CreateAPIMove(startSquare, targetSquare, 0); moves[currMoveIndex++] = CreateAPIMove(startSquare, targetSquare, 0);
if (exitEarly)
{
return;
}
} }
} }
} }

View file

@ -17,11 +17,13 @@ namespace ChessChallenge.Application
public static void Run(bool runPerft) public static void Run(bool runPerft)
{ {
anyFailed = false; anyFailed = false;
new SearchTest2().Run();
RepetitionTest();
new SearchTest().Run(false);
new SearchTest().Run(true);
new SearchTest2().Run();
new SearchTest3().Run();
RepetitionTest();
DrawTest(); DrawTest();
MoveGenTest(); MoveGenTest();
PieceListTest(); PieceListTest();
@ -29,9 +31,6 @@ namespace ChessChallenge.Application
MiscTest(); MiscTest();
TestBitboards(); TestBitboards();
TestMoveCreate(); TestMoveCreate();
new SearchTest().Run(false);
new SearchTest().Run(true);
if (runPerft) if (runPerft)
{ {
@ -654,6 +653,69 @@ namespace ChessChallenge.Application
Console.ResetColor(); 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 public class SearchTest2
{ {
API.Board board; API.Board board;
@ -778,7 +840,7 @@ namespace ChessChallenge.Application
this.useStackalloc = useStackalloc; this.useStackalloc = useStackalloc;
Console.WriteLine("Running misc search test | stackalloc = " + useStackalloc); Console.WriteLine("Running misc search test | stackalloc = " + useStackalloc);
Chess.Board b = new(); 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); board = new API.Board(b);
Search(4); Search(4);
@ -791,19 +853,19 @@ namespace ChessChallenge.Application
numCalls++; numCalls++;
var square = new Square(numCalls % 64); var square = new Square(numCalls % 64);
miscSumTest += (int)boardAPI.GetPiece(square).PieceType; miscSumTest += (int)board.GetPiece(square).PieceType;
miscSumTest += boardAPI.GetAllPieceLists()[numCalls % 12].Count; miscSumTest += board.GetAllPieceLists()[numCalls % 12].Count;
miscSumTest += (long)(boardAPI.ZobristKey % 100); miscSumTest += (long)(board.ZobristKey % 100);
miscSumTest += boardAPI.IsInCheckmate() ? 1 : 0; miscSumTest += board.IsInCheckmate() ? 1 : 0;
if (numCalls % 6 == 0) if (numCalls % 6 == 0)
{ {
miscSumTest += boardAPI.IsInCheck() ? 1 : 0; miscSumTest += board.IsInCheck() ? 1 : 0;
} }
if (numCalls % 18 == 0) if (numCalls % 18 == 0)
{ {
miscSumTest += boardAPI.SquareIsAttackedByOpponent(square) ? 1 : 0; miscSumTest += board.SquareIsAttackedByOpponent(square) ? 1 : 0;
} }
if (plyRemaining == 0) if (plyRemaining == 0)
@ -814,10 +876,10 @@ namespace ChessChallenge.Application
if (numCalls % 3 == 0 && plyRemaining > 2) if (numCalls % 3 == 0 && plyRemaining > 2)
{ {
if (boardAPI.TrySkipTurn()) if (board.TrySkipTurn())
{ {
Search(plyRemaining - 2); Search(plyRemaining - 2);
boardAPI.UndoSkipTurn(); board.UndoSkipTurn();
} }
} }
@ -826,19 +888,19 @@ namespace ChessChallenge.Application
if (useStackalloc) if (useStackalloc)
{ {
Span<API.Move> moveSpan = stackalloc API.Move[256]; 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) moves = moveSpan.ToArray(); // (don't actually care about allocations here, just testing the func)
} }
else else
{ {
moves = boardAPI.GetLegalMoves(); moves = board.GetLegalMoves();
} }
foreach (var move in moves) foreach (var move in moves)
{ {
boardAPI.MakeMove(move); board.MakeMove(move);
Search(plyRemaining - 1); Search(plyRemaining - 1);
boardAPI.UndoMove(move); board.UndoMove(move);
} }