Fix bug in repetition detection

This commit is contained in:
Sebastian Lague 2023-07-30 16:53:49 +02:00
parent 4438d69e7e
commit a6c102ba30
4 changed files with 313 additions and 20 deletions

View file

@ -10,16 +10,17 @@ namespace ChessChallenge.API
{
readonly Chess.Board board;
readonly APIMoveGen moveGen;
readonly RepetitionTable repetitionTable;
readonly HashSet<ulong> repetitionHistory;
readonly PieceList[] allPieceLists;
readonly PieceList[] validPieceLists;
readonly Move[] movesDest;
Move[] cachedLegalMoves;
bool hasCachedMoves;
Move[] cachedLegalCaptureMoves;
bool hasCachedCaptureMoves;
readonly Move[] movesDest;
int depth;
/// <summary>
/// Create a new board. Note: this should not be used in the challenge,
@ -31,6 +32,7 @@ namespace ChessChallenge.API
board = new Chess.Board();
board.LoadPosition(boardSource.StartPositionInfo);
GameMoveHistory = new Move[boardSource.AllGameMoves.Count];
repetitionTable = new();
for (int i = 0; i < boardSource.AllGameMoves.Count; i ++)
{
@ -61,9 +63,8 @@ namespace ChessChallenge.API
this.validPieceLists = validPieceLists.ToArray();
// Init rep history
repetitionHistory = new HashSet<ulong>(board.RepetitionPositionHistory);
GameRepetitionHistory = repetitionHistory.ToArray();
repetitionHistory.Remove(board.ZobristKey);
GameRepetitionHistory = board.RepetitionPositionHistory.ToArray();
repetitionTable.Init(board);
}
/// <summary>
@ -75,9 +76,11 @@ namespace ChessChallenge.API
{
if (!move.IsNull)
{
repetitionHistory.Add(board.ZobristKey);
OnPositionChanged();
board.MakeMove(new Chess.Move(move.RawValue), inSearch: true);
repetitionTable.Push(ZobristKey, move.IsCapture || move.MovePieceType == PieceType.Pawn);
depth++;
}
}
@ -88,9 +91,9 @@ namespace ChessChallenge.API
{
if (!move.IsNull)
{
repetitionTable.TryPop();
board.UndoMove(new Chess.Move(move.RawValue), inSearch: true);
OnPositionChanged();
repetitionHistory.Remove(board.ZobristKey);
}
}
@ -209,7 +212,7 @@ namespace ChessChallenge.API
/// This includes both positions in the actual game, and positions reached by
/// making moves while the bot is thinking.
/// </summary>
public bool IsRepeatedPosition() => repetitionHistory.Contains(board.ZobristKey);
public bool IsRepeatedPosition() => depth > 0 && repetitionTable.Contains(board.ZobristKey);
/// <summary>
/// Test if there are sufficient pieces remaining on the board to potentially deliver checkmate.

View file

@ -18,9 +18,13 @@ namespace ChessChallenge.Application
{
anyFailed = false;
new SearchTest2().Run();
RepetitionTest();
DrawTest();
MoveGenTest();
PieceListTest();
DrawTest();
CheckTest();
MiscTest();
TestBitboards();
@ -225,6 +229,118 @@ namespace ChessChallenge.Application
}
static void RepetitionTest()
{
Console.WriteLine("Repetition test");
string fen = "3k4/8/3K4/8/8/8/8/4Q3 w - - 0 1";
var board = new Chess.Board();
board.LoadPosition(fen);
boardAPI = new(board);
// -- Simple repeated position in search --
string[] moveStrings = { "d6c6", "d8c8", "c6d6", "c8d8" };
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
Make(moveStrings[0]); // Kc6
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
Make(moveStrings[1]); // ... Kc8
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
Make(moveStrings[2]); // Kd6
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
var move = Make(moveStrings[3]); // ...Kd8 (repeated position)
Assert(boardAPI.IsRepeatedPosition(), "should be repetition");
boardAPI.UndoMove(move); // Undo ...Kd8 (no longer repeated position)
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
// -- Repetition of position in actual game occuring in search --
board.LoadPosition(fen);
board.MakeMove(MoveUtility.GetMoveFromUCIName("e1e2", board), inSearch: false); // Qe2
board.MakeMove(MoveUtility.GetMoveFromUCIName("d8c8", board), inSearch: false); // ...Kc8
board.MakeMove(MoveUtility.GetMoveFromUCIName("d6c6", board), inSearch: false); // Kc6
board.MakeMove(MoveUtility.GetMoveFromUCIName("c8d8", board), inSearch: false); // ...Kd8
boardAPI = new(board);
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
var moveKd6 = Make("c6d6"); // Kd6 (repetition of position in game)
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
boardAPI.UndoMove(moveKd6); // Undo Kd6
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
boardAPI.MakeMove(moveKd6); // Redo Kd6
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
var moveKc8 = Make("d8c8"); // ...Kc8
boardAPI.UndoMove(moveKc8);
boardAPI.UndoMove(moveKd6);
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
boardAPI.MakeMove(moveKd6);
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
// -- Same test but purely in search --
board.LoadPosition(fen);
boardAPI = new(board);
Make("e1e2"); // Qe2
Make("d8c8"); // ...Kc8
Make("d6c6"); // Kc6
Make("c8d8"); // ...Kd8
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
moveKd6 = Make("c6d6"); // Kd6 (repetition of position in game)
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
boardAPI.UndoMove(moveKd6); // Undo Kd6
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
boardAPI.MakeMove(moveKd6); // Redo Kd6
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
moveKc8 = Make("d8c8"); // ...Kc8
boardAPI.UndoMove(moveKc8);
boardAPI.UndoMove(moveKd6);
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
boardAPI.MakeMove(moveKd6);
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
// Another test
board.LoadPosition("k7/1p6/2pp4/1QQ5/1b3N2/8/1qq1PPPP/3q2BK w - - 0 1");
boardAPI = new(board);
Make("b5a5");
Make("b4a5");
Make("c5a5");
Make("a8b8");
Make("a5d8");
Make("b8a7");
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
Make("d8a5");
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
Make("a7b8");
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
Make("a5d8");
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
Make("b8a7");
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
var pawnMove = Make("h2h4");
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
boardAPI.UndoMove(pawnMove);
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
boardAPI.MakeMove(pawnMove);
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
Make("d1c1");
Make("d8a5");
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
Make("a7b8");
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
Make("a5d8");
Assert(!boardAPI.IsRepeatedPosition(), "Should not be repetition");
Make("b8a7");
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
Make("d8a5");
Assert(boardAPI.IsRepeatedPosition(), "Should be repetition");
API.Move Make(string name)
{
var move = new API.Move(name, boardAPI);
boardAPI.MakeMove(move);
return move;
}
}
static void DrawTest()
{
Console.WriteLine("Draw test");
@ -457,7 +573,7 @@ namespace ChessChallenge.Application
ulong RecreateOpponentAttackMap()
{
ulong bb = 0;
for (int i = 0; i < 64; i ++)
for (int i = 0; i < 64; i++)
{
if (boardAPI.SquareIsAttackedByOpponent(new Square(i)))
{
@ -538,6 +654,117 @@ namespace ChessChallenge.Application
Console.ResetColor();
}
public class SearchTest2
{
API.Board board;
int numSkips;
int numCalls;
int numMates;
int numDraws;
int numExtend;
public void Run()
{
Console.WriteLine("Running misc search test");
Chess.Board b = new();
b.LoadPosition("8/2b5/2kp4/2p2K2/7P/1p3RP1/2n3N1/8 w - - 0 1");
board = new API.Board(b);
var sw = System.Diagnostics.Stopwatch.StartNew();
Search(7, -10000, 10000);
sw.Stop();
Console.WriteLine("Time: " + sw.ElapsedMilliseconds + " ms");
long testVal = numCalls + numSkips + numMates + numDraws;
//17092086 skip: 3740 mate: 31 draw: 3803 extend: 172125
//Console.WriteLine(numCalls + " skip: " + numSkips + " mate: " + numMates + " draw: " + numDraws + " extend: " + numExtend);
Console.WriteLine(testVal);
bool passed = testVal == 17092086;
//Assert(passed, "Test failed");
anyFailed &= passed;
}
int Search(int plyRemaining, int alpha, int beta, bool isQ = false)
{
numCalls++;
if (!isQ)
{
if (plyRemaining == 0)
{
return Search(-1, alpha, beta, true);
}
if (board.IsInCheckmate())
{
numMates++;
return -10000;
}
if (board.IsDraw())
{
numDraws++;
return 0;
}
if ((numCalls % 4 == 0 || numCalls % 9 == 0) && plyRemaining > 2)
{
if (board.TrySkipTurn())
{
numSkips++;
Search(plyRemaining - 2, -beta, -alpha);
board.UndoSkipTurn();
}
}
}
API.Move[] moves;
if (numCalls % 3 == 0 || numCalls % 7 == 0)
{
Span<API.Move> moveSpan = stackalloc API.Move[256];
board.GetLegalMovesNonAlloc(ref moveSpan, isQ);
// (don't actually care about allocations here, just testing the func)
moves = moveSpan.ToArray();
}
else
{
moves = board.GetLegalMoves(isQ);
}
if (isQ && moves.Length == 0)
{
int numWhite = BitboardHelper.GetNumberOfSetBits(board.WhitePiecesBitboard);
int numBlack = BitboardHelper.GetNumberOfSetBits(board.BlackPiecesBitboard);
int e = numWhite - numBlack;
return e * (board.IsWhiteToMove ? 1 : -1);
}
int best = int.MinValue;
foreach (var move in moves)
{
board.MakeMove(move);
int extend = !isQ && board.IsInCheck() ? 1 : 0;
numExtend += extend;
int eval = -Search(plyRemaining - 1 + extend, -beta, -alpha, isQ);
best = Math.Max(best, eval);
board.UndoMove(move);
if (eval >= beta)
{
return eval;
}
if (eval > alpha)
{
alpha = eval;
}
}
return best;
}
}
public class SearchTest
{
API.Board board;
@ -620,4 +847,3 @@ namespace ChessChallenge.Application
}
}

View file

@ -54,6 +54,7 @@ namespace ChessChallenge.Chess
// List of (hashed) positions since last pawn move or capture (for detecting 3-fold repetition)
public Stack<ulong> RepetitionPositionHistory;
public Stack<string> RepetitionPositionHistoryFen;
Stack<GameState> gameStateHistory;
public GameState currentGameState;
@ -269,6 +270,7 @@ namespace ChessChallenge.Chess
if (!inSearch)
{
RepetitionPositionHistory.Clear();
RepetitionPositionHistoryFen.Clear();
}
newFiftyMoveCounter = 0;
}
@ -281,6 +283,7 @@ namespace ChessChallenge.Chess
if (!inSearch)
{
RepetitionPositionHistory.Push(newState.zobristKey);
RepetitionPositionHistoryFen.Push(FenUtility.CurrentFen(this));
AllGameMoves.Add(move);
}
}
@ -372,6 +375,7 @@ namespace ChessChallenge.Chess
if (!inSearch && RepetitionPositionHistory.Count > 0)
{
RepetitionPositionHistory.Pop();
RepetitionPositionHistoryFen.Pop();
}
if (!inSearch)
{
@ -522,6 +526,7 @@ namespace ChessChallenge.Chess
RepetitionPositionHistory.Push(zobristKey);
gameStateHistory.Push(currentGameState);
RepetitionPositionHistoryFen.Push(FenUtility.CurrentFen(this));
}
void UpdateSliderBitboards()
@ -546,6 +551,7 @@ namespace ChessChallenge.Chess
KingSquare = new int[2];
RepetitionPositionHistory = new Stack<ulong>(capacity: 64);
RepetitionPositionHistoryFen = new Stack<string>(capacity: 64);
gameStateHistory = new Stack<GameState>(capacity: 64);
currentGameState = new GameState();

View file

@ -0,0 +1,58 @@
using System;
using System.Linq;
namespace ChessChallenge.Chess
{
public class RepetitionTable
{
readonly ulong[] hashes;
readonly int[] startIndices;
int count;
public RepetitionTable()
{
hashes = new ulong[256];
startIndices = new int[hashes.Length];
}
public void Init(Board board)
{
ulong[] initialHashes = board.RepetitionPositionHistory.Reverse().ToArray();
count = initialHashes.Length;
for (int i = 0; i < initialHashes.Length; i++)
{
hashes[i] = initialHashes[i];
startIndices[i] = 0;
}
startIndices[count] = 0;
}
public void Push(ulong hash, bool reset)
{
hashes[count] = hash;
count++;
startIndices[count] = reset ? count - 1 : startIndices[count - 1];
}
public void TryPop()
{
count = Math.Max(0, count - 1);
}
public bool Contains(ulong h)
{
int s = startIndices[count];
// up to count-1 so that curr position is not counted
for (int i = s; i < count - 1; i++)
{
if (hashes[i] == h)
{
return true;
}
}
return false;
}
}
}