Fix bug in repetition detection
This commit is contained in:
parent
4438d69e7e
commit
a6c102ba30
4 changed files with 313 additions and 20 deletions
|
@ -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,10 +76,12 @@ 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++;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -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.
|
||||
|
|
|
@ -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
|
|||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
58
Chess-Challenge/src/Framework/Chess/Board/RepetitionTable.cs
Normal file
58
Chess-Challenge/src/Framework/Chess/Board/RepetitionTable.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue