Skip to content

Commit

Permalink
🔍 If there's a single legal move, make it immediately (#401)
Browse files Browse the repository at this point in the history
  • Loading branch information
eduherminio authored Sep 13, 2023
1 parent cc4a785 commit 61434c7
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 25 deletions.
5 changes: 5 additions & 0 deletions src/Lynx/EvaluationConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,4 +310,9 @@ static EvaluationConstants()
/// Outside of the evaluation ranges (higher than any sensible evaluation, lower than <see cref="PositiveCheckmateDetectionLimit"/>)
/// </summary>
public const int NoHashEntry = 25_000;

/// <summary>
/// Evaluation to be returned when there's one single legal move
/// </summary>
public const int SingleMoveEvaluation = 200;
}
102 changes: 77 additions & 25 deletions src/Lynx/Search/IDDFS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,39 +50,91 @@ public sealed partial class Engine
bool isCancelled = false;
bool isMateDetected = false;

if (Game.MoveHistory.Count >= 2
&& _previousSearchResult?.Moves.Count > 2
&& _previousSearchResult.BestMove != default
&& Game.MoveHistory[^2] == _previousSearchResult.Moves[0]
&& Game.MoveHistory[^1] == _previousSearchResult.Moves[1])
try
{
_logger.Debug("Ponder hit");

lastSearchResult = new SearchResult(_previousSearchResult);
_stopWatch.Start();

Array.Copy(_previousSearchResult.Moves.ToArray(), 2, _pVTable, 0, _previousSearchResult.Moves.Count - 2);
bool onlyOneLegalMove = false;
Move firstLegalMove = default;
foreach (var move in MoveGenerator.GenerateAllMoves(Game.CurrentPosition))
{
var gameState = Game.CurrentPosition.MakeMove(move);
bool isPositionValid = Game.CurrentPosition.IsValid();
Game.CurrentPosition.UnmakeMove(move, gameState);

await _engineWriter.WriteAsync(InfoCommand.SearchResultInfo(lastSearchResult));
if (isPositionValid)
{
// We save the first legal move and check if there's at least another one
if (firstLegalMove == default)
{
firstLegalMove = move;
onlyOneLegalMove = true;
}
// If there's a second legal move, we exit and let the search continue
else
{
onlyOneLegalMove = false;
break;
}
}
}

for (int d = 1; d < Configuration.EngineSettings.MaxDepth - 2; ++d)
// Detect if there was only one legal move
if (onlyOneLegalMove)
{
_killerMoves[0, d] = _previousKillerMoves[0, d + 2];
_killerMoves[1, d] = _previousKillerMoves[1, d + 2];
_logger.Debug("One single move found");
var elapsedTime = _stopWatch.ElapsedMilliseconds;

// We don't have or need any eval, and we don't want to return 0 or a negative eval that
// could make the GUI resign or take a draw from this position.
// Since this only happens in root, we don't really care about being more precise for raising
// alphas or betas of parent moves, so let's just return +-2 pawns depending on the side to move
var eval = Game.CurrentPosition.Side == Side.White
? +EvaluationConstants.SingleMoveEvaluation
: -EvaluationConstants.SingleMoveEvaluation;

var result = new SearchResult(firstLegalMove, eval, 0, [firstLegalMove], alpha, beta)
{
DepthReached = 0,
Nodes = 0,
Time = elapsedTime,
NodesPerSecond = 0
};

await _engineWriter.WriteAsync(InfoCommand.SearchResultInfo(result));

return result;
}

depth = lastSearchResult.Depth - 1;
alpha = lastSearchResult.Alpha;
beta = lastSearchResult.Beta;
}
else
{
Array.Clear(_killerMoves);
Array.Clear(_historyMoves);
}
if (Game.MoveHistory.Count >= 2
&& _previousSearchResult?.Moves.Count > 2
&& _previousSearchResult.BestMove != default
&& Game.MoveHistory[^2] == _previousSearchResult.Moves[0]
&& Game.MoveHistory[^1] == _previousSearchResult.Moves[1])
{
_logger.Debug("Ponder hit");

try
{
_stopWatch.Start();
lastSearchResult = new SearchResult(_previousSearchResult);

Array.Copy(_previousSearchResult.Moves.ToArray(), 2, _pVTable, 0, _previousSearchResult.Moves.Count - 2);

await _engineWriter.WriteAsync(InfoCommand.SearchResultInfo(lastSearchResult));

for (int d = 1; d < Configuration.EngineSettings.MaxDepth - 2; ++d)
{
_killerMoves[0, d] = _previousKillerMoves[0, d + 2];
_killerMoves[1, d] = _previousKillerMoves[1, d + 2];
}

depth = lastSearchResult.Depth - 1;
alpha = lastSearchResult.Alpha;
beta = lastSearchResult.Beta;
}
else
{
Array.Clear(_killerMoves);
Array.Clear(_historyMoves);
}

do
{
Expand Down
69 changes: 69 additions & 0 deletions tests/Lynx.Test/BestMove/SingleLegalMoveTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Lynx.Model;
using NUnit.Framework;

namespace Lynx.Test.BestMove;

/// <summary>
/// "If there's a single move, just do it"
/// </summary>
public class SingleLegalMoveTest : BaseTest
{
// https://lichess.org/MThBKius/black#313
[TestCase("8/PPPPP1Pk/1PPPPP2/5PPP/2R5/R2RKR2/1R4R1/7R b - - 58 154")]
[TestCase("Q5k1/1PPPP1P1/1PPPPP2/5PPP/2R5/R2RKR2/1R4R1/7R b - - 0 155")]
[TestCase("6Q1/1PPPP1Pk/1PPPPP2/5PPP/2R5/R2RKR2/1R4R1/7R b - - 2 156")]
[TestCase("6k1/1PPPP1P1/1PPPPP2/5PPP/2R5/R2RKRR1/1R6/7R b - - 1 157")]

// https://lichess.org/BmNRWSXV/black#363
[TestCase("1k6/3PPPPP/PPPPPPPP/PPPRK3/1RR3RR/R4R2/8/8 b - - 8 181")]
[TestCase("k7/3PPPPP/PPPPPPPP/PPPRK3/1RR3RR/R7/5R2/8 b - - 10 182")]

[TestCase("8/8/8/8/8/k7/1R6/K1q5 w - - 0 1")]
[TestCase("8/8/8/8/8/k7/2B5/K1q5 w - - 0 1")]
[TestCase("8/8/8/8/8/k1N5/8/K1q5 w - - 0 1")]
[TestCase("1Q6/8/8/8/8/k7/8/K1q5 w - - 0 1")]

[TestCase("8/8/8/8/8/K7/1r6/k1Q5 b - - 0 1")]
[TestCase("8/8/8/8/8/K7/2b5/k1Q5 b - - 0 1")]
[TestCase("8/8/8/8/8/K1n5/8/k1Q5 b - - 0 1")]
[TestCase("1q6/8/8/8/8/K7/8/k1Q5 b - - 0 1")]
public async Task SingleMove(string fen)
{
// Arrange
const int depth = 61;
Move? singleMove = null;
var pos = new Position(fen);
foreach (var move in MoveGenerator.GenerateAllMoves(pos))
{
var state = pos.MakeMove(move);
if (pos.IsValid())
{
Assert.IsNull(singleMove);
singleMove = move;
}

pos.UnmakeMove(move, state);
}

Assert.LessOrEqual(depth, Configuration.EngineSettings.MaxDepth);

// Act
var result = await SearchBestMove(fen, depth);

Assert.AreEqual(singleMove, result.BestMove);
Assert.AreEqual(singleMove, result.Moves.Single());

if (pos.Side == Side.White)
{
Assert.AreEqual(EvaluationConstants.SingleMoveEvaluation, result.Evaluation);
}
else
{
Assert.AreEqual(-EvaluationConstants.SingleMoveEvaluation, result.Evaluation);
}

Assert.AreEqual(0, result.Depth);
Assert.AreEqual(0, result.DepthReached);
Assert.AreEqual(0, result.NodesPerSecond);
}
}
12 changes: 12 additions & 0 deletions tests/Lynx.Test/EvaluationConstantsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,16 @@ public void SecondKillerMoveValueConstant()

Assert.Greater(SecondKillerMoveValue, default);
}

/// <summary>
/// Avoids drawish evals that can lead the GUI to declare a draw
/// or negative ones that can lead it to resign
/// </summary>
[Test]
public void SingleMoveEvaluation()
{
Assert.NotZero(EvaluationConstants.SingleMoveEvaluation);
Assert.Greater(EvaluationConstants.SingleMoveEvaluation, 100);
Assert.Less(EvaluationConstants.SingleMoveEvaluation, 400);
}
}

0 comments on commit 61434c7

Please sign in to comment.