Skip to content

Commit

Permalink
Revert "🐛 Detect threefold repetition on pvNode (#796)" (#818)
Browse files Browse the repository at this point in the history
This reverts commit 49b926f, which caused Lynx to draw by threefold repetition completely won endgames.
  • Loading branch information
eduherminio authored Jun 20, 2024
1 parent 649deda commit f79faa3
Show file tree
Hide file tree
Showing 5 changed files with 24 additions and 106 deletions.
31 changes: 4 additions & 27 deletions src/Lynx/Model/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,43 +118,20 @@ public bool Update50movesRule(Move moveToPlay, bool isCapture)

/// <summary>
/// Basic algorithm described in https://web.archive.org/web/20201107002606/https://marcelk.net/2013-04-06/paper/upcoming-rep-v2.pdf
/// Appart from that, tests for three fold repetition if <paramref name="requiresThreefold"/> is true, two otherwise
/// </summary>
/// <param name="requiresThreefold">Whether real threefold repetition is required, 'two-fold' will be checked otherwise. Usual strategy is to do threefold only for pv nodes</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsThreefoldRepetition(bool requiresThreefold)
public bool IsThreefoldRepetition()
{
var currentHash = CurrentPosition.UniqueIdentifier;

// [Count - 1] would be the last one, we want to start searching 2 ealier and finish HalfMovesWithoutCaptureOrPawnMove earlier
var limit = Math.Max(0, PositionHashHistory.Count - 1 - HalfMovesWithoutCaptureOrPawnMove);

if (!requiresThreefold)
for (int i = PositionHashHistory.Count - 3; i >= limit; i -= 2)
{
for (int i = PositionHashHistory.Count - 3; i >= limit; i -= 2)
if (currentHash == PositionHashHistory[i])
{
if (currentHash == PositionHashHistory[i])
{
return true;
}
}
}
else
{
for (int i = PositionHashHistory.Count - 3; i >= limit; i -= 2)
{
if (currentHash == PositionHashHistory[i])
{
if (requiresThreefold)
{
requiresThreefold = false;
}
else
{
return true;
}
}
return true;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Lynx/Search/NegaMax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM
Game.PositionHashHistory.Add(position.UniqueIdentifier);

int evaluation;
if (canBeRepetition && (Game.IsThreefoldRepetition(pvNode) || Game.Is50MovesRepetition()))
if (canBeRepetition && (Game.IsThreefoldRepetition() || Game.Is50MovesRepetition()))
{
evaluation = 0;

Expand Down
36 changes: 2 additions & 34 deletions tests/Lynx.Test/BestMove/RegressionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ public void InvalidPV(string positionCommand)
engine.AdjustPosition(positionCommand);

var bestMove = engine.BestMove(new GoCommand($"go depth {5}"));
Assert.Zero(bestMove.Evaluation);
Assert.AreEqual(1, bestMove.Moves.Count);
Assert.AreEqual("b8c7", bestMove.BestMove.UCIString());
}

Expand Down Expand Up @@ -392,38 +394,4 @@ public void PawnlessEndgames(string fen, string[]? allowedUCIMoveString, string[
{
TestBestMove(fen, allowedUCIMoveString, excludedUCIMoveString);
}

[Test]
public void FalseThreefoldRepetitionDetected()
{
const string positionCommand =
"position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK1NR w KQq - 0 1 " +
"moves b1c3 c7c5 d2d4 c5d4 d1d4 b8c6 d4a4 d7d5 g1f3 g8f6 c1g5 c8d7 g5f6 e7f6 e1c1 c6b4 a4b3 d7e6 d1d4 " +
"d8b6 a2a3 b4c6 b3b6 a7b6 d4d2 f8c5 e2e3 c6e7 h1d1 c5d6 c3b5 e8d7 b5d6 d7d6 f3d4 h8c8 e3e4 c8c5 b2b4 c5c4 " +
"e4d5 e7d5 c1b2 g7g6 d4b5 d6c6 b5d4 c6d7 d4b5 d7c6 b5d4 c6d6 d4b5 d6e5 d2e2 e5f4 e2d2 f4e5 d2e2 e5f4 e2d2 " +
"d5e7 b5d6 c4c7 d1e1 f4g4 c2c3 g4g5 d6e4 g5h6 e4f6 h6g7 f6e4 h7h6 g2g3 e7d5 d2d4 d5f6 e4d6 c7d7 e1e2 a8d8 " +
"d6b5 e6d5 h2h4 d8c8 e2e1 c8d8 e1e3 d8c8";

var engine = GetEngine();

engine.AdjustPosition(positionCommand);

var bestMove = engine.BestMove(new GoCommand("go depth 1"));
Assert.Less(bestMove.Evaluation, -150);

engine.NewGame();
engine.AdjustPosition(positionCommand);
bestMove = engine.BestMove(new GoCommand("go depth 5"));
Assert.Less(bestMove.Evaluation, -150);

engine.NewGame();
engine.AdjustPosition(positionCommand);
bestMove = engine.BestMove(new GoCommand("go depth 2"));
Assert.Less(bestMove.Evaluation, -150);

engine.NewGame();
engine.AdjustPosition(positionCommand);
bestMove = engine.BestMove(new GoCommand("go depth 10"));
Assert.Less(bestMove.Evaluation, -150);
}
}
57 changes: 15 additions & 42 deletions tests/Lynx.Test/GameTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ public void IsThreefoldRepetition_1()
Assert.DoesNotThrow(() => game.MakeMove(repeatedMoves[2]));

game.MakeMove(repeatedMoves[3]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

game.MakeMove(repeatedMoves[4]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

game.MakeMove(repeatedMoves[5]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

game.MakeMove(repeatedMoves[6]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

game.MakeMove(repeatedMoves[7]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());
}

[Test]
Expand All @@ -66,46 +66,19 @@ public void IsThreefoldRepetition_2()
Assert.DoesNotThrow(() => game.MakeMove(repeatedMoves[2]));

game.MakeMove(repeatedMoves[3]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.False(game.IsThreefoldRepetition(true));
Assert.True(game.IsThreefoldRepetition());

game.MakeMove(repeatedMoves[4]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

game.MakeMove(repeatedMoves[5]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

game.MakeMove(repeatedMoves[6]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

game.MakeMove(repeatedMoves[7]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition(true));
}

[Test]
public void EvaluateFinalPosition_Threefold_PVNode()
{
const string winningPosition = "7k/8/5KR1/8/8/8/5R2/8 w - - 0 1";

var game = new Game(winningPosition);
var repeatedMoves = new List<Move>
{
MoveExtensions.Encode((int)BoardSquare.f2, (int)BoardSquare.e2, (int)Piece.R),
MoveExtensions.Encode((int)BoardSquare.h8, (int)BoardSquare.h7, (int)Piece.k),
MoveExtensions.Encode((int)BoardSquare.e2, (int)BoardSquare.f2, (int)Piece.R),
MoveExtensions.Encode((int)BoardSquare.h7, (int)BoardSquare.h8, (int)Piece.k),
MoveExtensions.Encode((int)BoardSquare.f2, (int)BoardSquare.e2, (int)Piece.R),
MoveExtensions.Encode((int)BoardSquare.h8, (int)BoardSquare.h7, (int)Piece.k),
MoveExtensions.Encode((int)BoardSquare.e2, (int)BoardSquare.f2, (int)Piece.R),
MoveExtensions.Encode((int)BoardSquare.h7, (int)BoardSquare.h8, (int)Piece.k), // Triple position repetition
};

repeatedMoves.ForEach(move => Assert.DoesNotThrow(() => game.MakeMove(move)));

Assert.AreEqual(repeatedMoves.Count + 1, game.PositionHashHistory.Count);
Assert.True(game.IsThreefoldRepetition(requiresThreefold: true));
Assert.True(game.IsThreefoldRepetition(requiresThreefold: false));
Assert.True(game.IsThreefoldRepetition());
}

[Test]
Expand Down Expand Up @@ -134,18 +107,18 @@ public void IsThreefoldRepetition_CastleRightsRemoval()
Assert.DoesNotThrow(() => game.MakeMove(repeatedMoves[2]));

game.MakeMove(repeatedMoves[3]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

Assert.DoesNotThrow(() => game.MakeMove(repeatedMoves[4]));
Assert.DoesNotThrow(() => game.MakeMove(repeatedMoves[5]));

game.MakeMove(repeatedMoves[6]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

Assert.DoesNotThrow(() => game.MakeMove(repeatedMoves[6]));

game.MakeMove(repeatedMoves[7]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

// Position with castling rights, lost in move Ke1d1
winningPosition = new Position("1n2k2r/8/8/8/8/8/4PPPP/1N2K2R w Kk - 0 1");
Expand All @@ -170,7 +143,7 @@ public void IsThreefoldRepetition_CastleRightsRemoval()
}

game.MakeMove(repeatedMoves[^1]);
Assert.False(game.IsThreefoldRepetition(false)); // Same position, but white not can't castle
Assert.False(game.IsThreefoldRepetition()); // Same position, but white not can't castle

#if DEBUG
Assert.AreEqual(repeatedMoves.Count, game.MoveHistory.Count);
Expand All @@ -183,7 +156,7 @@ public void IsThreefoldRepetition_CastleRightsRemoval()
Assert.DoesNotThrow(() => game.MakeMove(repeatedMoves[5]));

game.MakeMove(repeatedMoves[6]);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());
}

[Test]
Expand Down
4 changes: 2 additions & 2 deletions tests/Lynx.Test/OnlineTablebaseProberTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ public async Task RootSearch_ForceThreefoldRepetitionWhenLosing()
Assert.AreEqual("h8g7", result.BestMove.UCIString());

game.MakeMove(result.BestMove);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

// Using local method due to async Span limitation
static Game ParseGame()
Expand All @@ -413,7 +413,7 @@ public async Task RootSearch_ForceThreefoldRepetitionWhenBlessedLosing()
Assert.AreEqual("h8g7", result.BestMove.UCIString());

game.MakeMove(result.BestMove);
Assert.True(game.IsThreefoldRepetition(false));
Assert.True(game.IsThreefoldRepetition());

// Using local method due to async Span limitation
static Game ParseGame()
Expand Down

0 comments on commit f79faa3

Please sign in to comment.