diff --git a/Chess-Challenge/src/API/Board.cs b/Chess-Challenge/src/API/Board.cs index bf4eb94..f1b9a3d 100644 --- a/Chess-Challenge/src/API/Board.cs +++ b/Chess-Challenge/src/API/Board.cs @@ -10,16 +10,17 @@ namespace ChessChallenge.API { readonly Chess.Board board; readonly APIMoveGen moveGen; + readonly RepetitionTable repetitionTable; - readonly HashSet repetitionHistory; readonly PieceList[] allPieceLists; readonly PieceList[] validPieceLists; + readonly Move[] movesDest; Move[] cachedLegalMoves; bool hasCachedMoves; Move[] cachedLegalCaptureMoves; bool hasCachedCaptureMoves; - readonly Move[] movesDest; + int depth; /// /// 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(board.RepetitionPositionHistory); - GameRepetitionHistory = repetitionHistory.ToArray(); - repetitionHistory.Remove(board.ZobristKey); + GameRepetitionHistory = board.RepetitionPositionHistory.ToArray(); + repetitionTable.Init(board); } /// @@ -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++; + + } } /// @@ -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. /// - public bool IsRepeatedPosition() => repetitionHistory.Contains(board.ZobristKey); + public bool IsRepeatedPosition() => depth > 0 && repetitionTable.Contains(board.ZobristKey); /// /// Test if there are sufficient pieces remaining on the board to potentially deliver checkmate. diff --git a/Chess-Challenge/src/Framework/Application/Helpers/Tester.cs b/Chess-Challenge/src/Framework/Application/Helpers/Tester.cs index b9010b4..bfc6eec 100644 --- a/Chess-Challenge/src/Framework/Application/Helpers/Tester.cs +++ b/Chess-Challenge/src/Framework/Application/Helpers/Tester.cs @@ -18,16 +18,20 @@ namespace ChessChallenge.Application { anyFailed = false; + new SearchTest2().Run(); + + RepetitionTest(); + + DrawTest(); MoveGenTest(); PieceListTest(); - DrawTest(); CheckTest(); MiscTest(); TestBitboards(); TestMoveCreate(); new SearchTest().Run(false); new SearchTest().Run(true); - + if (runPerft) { @@ -42,7 +46,7 @@ namespace ChessChallenge.Application else { WriteWithCol("ALL TESTS PASSED", ConsoleColor.Green); - } + } } public static void RunPerft(bool useStackalloc = true) @@ -148,7 +152,7 @@ namespace ChessChallenge.Application Assert(boardAPI.SquareIsAttackedByOpponent(new Square("c3")), "Square attacked wrong"); Assert(boardAPI.SquareIsAttackedByOpponent(new Square("h6")), "Square attacked wrong"); Assert(!boardAPI.SquareIsAttackedByOpponent(new Square("h4")), "Square attacked wrong"); - + boardAPI.ForceSkipTurn(); Assert(boardAPI.SquareIsAttackedByOpponent(new Square("f7")), "Square attacked wrong"); @@ -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"); @@ -418,7 +534,7 @@ namespace ChessChallenge.Application Assert(RecreateOpponentAttackMap() == 18446743649919696896ul, "Wrong attack map"); Assert(boardAPI.GetLegalMoves().Length == 43, "Wrong move count"); Assert(boardAPI.GetLegalMoves(true).Length == 3, "Wrong capture count"); - + boardAPI.MakeMove(m1); Assert(RecreateOpponentAttackMap() == 68361585683595006ul, "Wrong attack map"); Assert(boardAPI.GetLegalMoves().Length == 31, "Wrong move count"); @@ -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 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; @@ -560,8 +787,8 @@ namespace ChessChallenge.Application void Search(int plyRemaining) { - - + + numCalls++; var square = new Square(numCalls % 64); miscSumTest += (int)boardAPI.GetPiece(square).PieceType; @@ -593,8 +820,8 @@ namespace ChessChallenge.Application boardAPI.UndoSkipTurn(); } } - - + + API.Move[] moves; if (useStackalloc) { @@ -620,4 +847,3 @@ namespace ChessChallenge.Application } } - \ No newline at end of file diff --git a/Chess-Challenge/src/Framework/Chess/Board/Board.cs b/Chess-Challenge/src/Framework/Chess/Board/Board.cs index 528d604..9fd4c1f 100644 --- a/Chess-Challenge/src/Framework/Chess/Board/Board.cs +++ b/Chess-Challenge/src/Framework/Chess/Board/Board.cs @@ -54,6 +54,7 @@ namespace ChessChallenge.Chess // List of (hashed) positions since last pawn move or capture (for detecting 3-fold repetition) public Stack RepetitionPositionHistory; + public Stack RepetitionPositionHistoryFen; Stack 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(capacity: 64); + RepetitionPositionHistoryFen = new Stack(capacity: 64); gameStateHistory = new Stack(capacity: 64); currentGameState = new GameState(); diff --git a/Chess-Challenge/src/Framework/Chess/Board/RepetitionTable.cs b/Chess-Challenge/src/Framework/Chess/Board/RepetitionTable.cs new file mode 100644 index 0000000..df5861e --- /dev/null +++ b/Chess-Challenge/src/Framework/Chess/Board/RepetitionTable.cs @@ -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; + } + } +} \ No newline at end of file