Skip to content

Commit

Permalink
Use standard depth concept in NegaMax method (#377)
Browse files Browse the repository at this point in the history
* Replace custom-made fixed `targetDepth` and increasing `ply` schema, where `ply == targetDepth` was used to trigger QSearch, with the standard, decreasing `depth` by one + reductions, and increasing `ply` by one no-matter-what.
* Fix LMR condition, using depth instead of ply
* Fix NMP, which was usign ply instead of depth, and get rid of always true condition
  • Loading branch information
eduherminio authored Aug 26, 2023
1 parent bb2f8ee commit 01247a5
Show file tree
Hide file tree
Showing 11 changed files with 64 additions and 64 deletions.
16 changes: 8 additions & 8 deletions src/Lynx.Dev/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1051,20 +1051,20 @@ static void TesSize(int size)
//transpositionTable.RecordHash(position, depth: 3, maxDepth: 5, move: 1234, eval: +5, nodeType: NodeType.Alpha);
//var entry = transpositionTable.ProbeHash(position, maxDepth: 5, depth: 3, alpha: 1, beta: 2);

transpositionTable.RecordHash(mask, position, targetDepth: 5, ply: 3, eval: +19, nodeType: NodeType.Alpha, move: 1234);
var entry = transpositionTable.ProbeHash(mask, position, targetDepth: 5, ply: 3, alpha: 20, beta: 30);
transpositionTable.RecordHash(mask, position, depth: 5, ply: 3, eval: +19, nodeType: NodeType.Alpha, move: 1234);
var entry = transpositionTable.ProbeHash(mask, position, depth: 5, ply: 3, alpha: 20, beta: 30);
Console.WriteLine(entry); // Expected 20

transpositionTable.RecordHash(mask, position, targetDepth: 5, ply: 3, eval: +21, nodeType: NodeType.Alpha, move: 1234);
entry = transpositionTable.ProbeHash(mask, position, targetDepth: 5, ply: 3, alpha: 20, beta: 30);
transpositionTable.RecordHash(mask, position, depth: 5, ply: 3, eval: +21, nodeType: NodeType.Alpha, move: 1234);
entry = transpositionTable.ProbeHash(mask, position, depth: 5, ply: 3, alpha: 20, beta: 30);
Console.WriteLine(entry); // Expected 12_345_678

transpositionTable.RecordHash(mask, position, targetDepth: 5, ply: 3, eval: +29, nodeType: NodeType.Beta, move: 1234);
entry = transpositionTable.ProbeHash(mask, position, targetDepth: 5, ply: 3, alpha: 20, beta: 30);
transpositionTable.RecordHash(mask, position, depth: 5, ply: 3, eval: +29, nodeType: NodeType.Beta, move: 1234);
entry = transpositionTable.ProbeHash(mask, position, depth: 5, ply: 3, alpha: 20, beta: 30);
Console.WriteLine(entry); // Expected 12_345_678

transpositionTable.RecordHash(mask, position, targetDepth: 5, ply: 3, eval: +31, nodeType: NodeType.Beta, move: 1234);
entry = transpositionTable.ProbeHash(mask, position, targetDepth: 5, ply: 3, alpha: 20, beta: 30);
transpositionTable.RecordHash(mask, position, depth: 5, ply: 3, eval: +31, nodeType: NodeType.Beta, move: 1234);
entry = transpositionTable.ProbeHash(mask, position, depth: 5, ply: 3, alpha: 20, beta: 30);
Console.WriteLine(entry); // Expected 30
}

Expand Down
6 changes: 3 additions & 3 deletions src/Lynx/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public async Task<SearchResult> BestMove(GoCommand? goCommand)
Game.MakeMove(resultToReturn.BestMove);
}

AverageDepth += (resultToReturn.TargetDepth - AverageDepth) / Math.Ceiling(0.5 * Game.MoveHistory.Count);
AverageDepth += (resultToReturn.Depth - AverageDepth) / Math.Ceiling(0.5 * Game.MoveHistory.Count);

return resultToReturn;
}
Expand Down Expand Up @@ -173,7 +173,7 @@ private async Task<SearchResult> SearchBestMove(int minDepth, int? maxDepth, int
if (searchResult is not null)
{
_logger.Info("Search evaluation result - eval: {0}, mate: {1}, depth: {2}, pv: {3}",
searchResult.Evaluation, searchResult.Mate, searchResult.TargetDepth, string.Join(", ", searchResult.Moves.Select(m => m.ToMoveString())));
searchResult.Evaluation, searchResult.Mate, searchResult.Depth, string.Join(", ", searchResult.Moves.Select(m => m.ToMoveString())));
}

if (tbResult is not null)
Expand All @@ -184,7 +184,7 @@ private async Task<SearchResult> SearchBestMove(int minDepth, int? maxDepth, int
if (searchResult?.Mate > 0 && searchResult.Mate <= tbResult.Mate && searchResult.Mate + currentHalfMovesWithoutCaptureOrPawnMove < 96)
{
_logger.Info("Relying on search result mate line due to dtm match and low enough dtz");
++searchResult.TargetDepth;
++searchResult.Depth;
tbResult = null;
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/Lynx/Model/Position.cs
Original file line number Diff line number Diff line change
Expand Up @@ -708,17 +708,17 @@ public int StaticEvaluation(int movesWithoutCaptureOrPawnMove, CancellationToken
/// this method determines if a position is a result of either a loss by Checkmate or a draw by stalemate.
/// NegaMax style
/// </summary>
/// <param name="depth">Modulates the output, favouring positions with lower depth left (i.e. Checkmate in less moves)</param>
/// <param name="ply">Modulates the output, favouring positions with lower ply (i.e. Checkmate in less moves)</param>
/// <param name="isInCheck"></param>
/// <returns>At least <see cref="CheckMateEvaluation"/> if Position.Side lost (more extreme values when <paramref name="depth"/> increases)
/// <returns>At least <see cref="CheckMateEvaluation"/> if Position.Side lost (more extreme values when <paramref name="ply"/> increases)
/// or 0 if Position.Side was stalemated</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int EvaluateFinalPosition(int depth, bool isInCheck)
public static int EvaluateFinalPosition(int ply, bool isInCheck)
{
if (isInCheck)
{
// Checkmate evaluation, but not as bad/shallow as it looks like since we're already searching at a certain depth
return -EvaluationConstants.CheckMateBaseEvaluation + (EvaluationConstants.CheckmateDepthFactor * depth);
return -EvaluationConstants.CheckMateBaseEvaluation + (EvaluationConstants.CheckmateDepthFactor * ply);
}
else
{
Expand Down
16 changes: 8 additions & 8 deletions src/Lynx/Model/TranspositionTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public static void ClearTranspositionTable(this TranspositionTable transposition
/// <param name="beta"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static (int Evaluation, Move BestMove) ProbeHash(this TranspositionTable tt, int ttMask, Position position, int targetDepth, int ply, int alpha, int beta)
public static (int Evaluation, Move BestMove) ProbeHash(this TranspositionTable tt, int ttMask, Position position, int depth, int ply, int alpha, int beta)
{
if (!Configuration.EngineSettings.TranspositionTableEnabled)
{
Expand All @@ -120,7 +120,7 @@ public static (int Evaluation, Move BestMove) ProbeHash(this TranspositionTable

var eval = EvaluationConstants.NoHashEntry;

if (entry.Depth >= (targetDepth - ply))
if (entry.Depth >= depth)
{
// We want to translate the checkmate position relative to the saved node to our root position from which we're searching
// If the recorded score is a checkmate in 3 and we are at depth 5, we want to read checkmate in 8
Expand Down Expand Up @@ -149,7 +149,7 @@ public static (int Evaluation, Move BestMove) ProbeHash(this TranspositionTable
/// <param name="nodeType"></param>
/// <param name="move"></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RecordHash(this TranspositionTable tt, int ttMask, Position position, int targetDepth, int ply, int eval, NodeType nodeType, Move? move = 0)
public static void RecordHash(this TranspositionTable tt, int ttMask, Position position, int depth, int ply, int eval, NodeType nodeType, Move? move = 0)
{
if (!Configuration.EngineSettings.TranspositionTableEnabled)
{
Expand All @@ -169,7 +169,7 @@ public static void RecordHash(this TranspositionTable tt, int ttMask, Position p

entry.Key = position.UniqueIdentifier;
entry.Score = score;
entry.Depth = targetDepth - ply;
entry.Depth = depth;
entry.Move = move ?? 0;
entry.Type = nodeType;
}
Expand All @@ -180,14 +180,14 @@ public static void RecordHash(this TranspositionTable tt, int ttMask, Position p
/// Logic for when to pass +depth or -depth for the desired effect in https://www.talkchess.com/forum3/viewtopic.php?f=7&t=74411 and https://talkchess.com/forum3/viewtopic.php?p=861852#p861852
/// </summary>
/// <param name="score"></param>
/// <param name="depth"></param>
/// <param name="ply"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int RecalculateMateScores(int score, int depth) => score +
internal static int RecalculateMateScores(int score, int ply) => score +
score switch
{
> EvaluationConstants.PositiveCheckmateDetectionLimit => -EvaluationConstants.CheckmateDepthFactor * depth,
< EvaluationConstants.NegativeCheckmateDetectionLimit => EvaluationConstants.CheckmateDepthFactor * depth,
> EvaluationConstants.PositiveCheckmateDetectionLimit => -EvaluationConstants.CheckmateDepthFactor * ply,
< EvaluationConstants.NegativeCheckmateDetectionLimit => EvaluationConstants.CheckmateDepthFactor * ply,
_ => 0
};

Expand Down
6 changes: 3 additions & 3 deletions src/Lynx/Search/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class SearchResult
{
public Move BestMove { get; init; }
public double Evaluation { get; init; }
public int TargetDepth { get; set; }
public int Depth { get; set; }
public List<Move> Moves { get; init; }
public int Alpha { get; init; }
public int Beta { get; init; }
Expand All @@ -31,7 +31,7 @@ public SearchResult(Move bestMove, double evaluation, int targetDepth, List<Move
{
BestMove = bestMove;
Evaluation = evaluation;
TargetDepth = targetDepth;
Depth = targetDepth;
Moves = moves;
Alpha = alpha;
Beta = beta;
Expand All @@ -42,7 +42,7 @@ public SearchResult(SearchResult previousSearchResult)
{
BestMove = previousSearchResult.Moves.ElementAtOrDefault(2);
Evaluation = previousSearchResult.Evaluation;
TargetDepth = previousSearchResult.TargetDepth - 2;
Depth = previousSearchResult.Depth - 2;
DepthReached = previousSearchResult.DepthReached - 2;
Moves = previousSearchResult.Moves.Skip(2).ToList();
Alpha = previousSearchResult.Alpha;
Expand Down
4 changes: 2 additions & 2 deletions src/Lynx/Search/IDDFS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public sealed partial class Engine
_killerMoves[1, d] = _previousKillerMoves[1, d + 2];
}

depth = lastSearchResult.TargetDepth + 1;
depth = lastSearchResult.Depth - 1;
alpha = lastSearchResult.Alpha;
beta = lastSearchResult.Beta;
}
Expand All @@ -97,7 +97,7 @@ public sealed partial class Engine
AspirationWindows_SearchAgain:

_isFollowingPV = true;
bestEvaluation = NegaMax(minDepth, targetDepth: depth, ply: 0, alpha, beta, isVerifyingNullMoveCutOff: true);
bestEvaluation = NegaMax(depth: depth, ply: 0, alpha, beta, isVerifyingNullMoveCutOff: true); ;

var bestEvaluationAbs = Math.Abs(bestEvaluation);
isMateDetected = bestEvaluationAbs > EvaluationConstants.PositiveCheckmateDetectionLimit;
Expand Down
52 changes: 26 additions & 26 deletions src/Lynx/Search/NegaMax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ public sealed partial class Engine
/// <summary>
/// NegaMax algorithm implementation using alpha-beta pruning, quiescence search and Iterative Deepeting Depth-First Search (IDDFS)
/// </summary>
/// <param name="minDepth">Minimum number of depth (plies), regardless of time constrains</param>
/// <param name="targetDepth"></param>
/// <param name="ply">Current depth or number of half moves</param>
/// <param name="depth"></param>
/// <param name="ply"></param>
/// <param name="alpha">
/// Best score the Side to move can achieve, assuming best play by the opponent.
/// Defaults to the worse possible score for Side to move, Int.MinValue.
Expand All @@ -21,11 +20,11 @@ public sealed partial class Engine
/// <param name="isVerifyingNullMoveCutOff">Indicates if the search is verifying an ancestors null-move that failed high, or the root node</param>
/// <param name="ancestorWasNullMove">Indicates whether the immediate ancestor node was a null move</param>
/// <returns></returns>
private int NegaMax(int minDepth, int targetDepth, int ply, int alpha, int beta, bool isVerifyingNullMoveCutOff, bool ancestorWasNullMove = false)
private int NegaMax(int depth, int ply, int alpha, int beta, bool isVerifyingNullMoveCutOff, bool ancestorWasNullMove = false)
{
var position = Game.CurrentPosition;

// Prevents runtime failure, in case targetDepth is increased due to check extension
// Prevents runtime failure in case depth is increased due to check extension, since we're using ply when calculating pvTable index,
if (ply >= Configuration.EngineSettings.MaxDepth)
{
_logger.Info("Max depth {0} reached", Configuration.EngineSettings.MaxDepth);
Expand All @@ -39,12 +38,13 @@ private int NegaMax(int minDepth, int targetDepth, int ply, int alpha, int beta,
var nextPvIndex = PVTable.Indexes[ply + 1];
_pVTable[pvIndex] = _defaultMove; // Nulling the first value before any returns

bool isRoot = ply == 0;
bool pvNode = beta - alpha > 1;
Move ttBestMove = default;

if (ply > 0)
if (!isRoot)
{
var ttProbeResult = _tt.ProbeHash(_ttMask, position, targetDepth, ply, alpha, beta);
var ttProbeResult = _tt.ProbeHash(_ttMask, position, depth, ply, alpha, beta);
if (ttProbeResult.Evaluation != EvaluationConstants.NoHashEntry)
{
return ttProbeResult.Evaluation;
Expand All @@ -59,39 +59,39 @@ private int NegaMax(int minDepth, int targetDepth, int ply, int alpha, int beta,

if (isInCheck)
{
++targetDepth;
++depth;
}
if (ply >= targetDepth)
if (depth <= 0)
{
if (MoveGenerator.CanGenerateAtLeastAValidMove(position))
{
return QuiescenceSearch(ply, alpha, beta);
}

var finalPositionEvaluation = Position.EvaluateFinalPosition(ply, isInCheck);
_tt.RecordHash(_ttMask, position, targetDepth, ply, finalPositionEvaluation, NodeType.Exact);
_tt.RecordHash(_ttMask, position, depth, ply, finalPositionEvaluation, NodeType.Exact);
return finalPositionEvaluation;
}

// 🔍 Null-move pruning
// 🔍 Verified Null-move pruning (NMP) - https://www.researchgate.net/publication/297377298_Verified_Null-Move_Pruning
bool isFailHigh = false; // In order to detect zugzwangs
if (ply > Configuration.EngineSettings.NullMovePruning_R
if (depth > Configuration.EngineSettings.NullMovePruning_R
&& !isInCheck
&& !ancestorWasNullMove
&& (!isVerifyingNullMoveCutOff || ply < targetDepth - 1)) // verify == true and ply == targetDepth -1 -> No null pruning, since verification will not be possible)
// following pv?
/*&& (!isVerifyingNullMoveCutOff || depth > 1)*/) // verify == true and ply == targetDepth -1 -> No null pruning, since verification will not be possible)
// following pv?
{
var gameState = position.MakeNullMove();

var evaluation = -NegaMax(minDepth, targetDepth, ply + 1 + Configuration.EngineSettings.NullMovePruning_R, -beta, -beta + 1, isVerifyingNullMoveCutOff, ancestorWasNullMove: true);
var evaluation = -NegaMax(depth - 1 - Configuration.EngineSettings.NullMovePruning_R, ply + 1, -beta, -beta + 1, isVerifyingNullMoveCutOff, ancestorWasNullMove: true);

position.UnMakeNullMove(gameState);

if (evaluation >= beta) // Fail high
{
if (isVerifyingNullMoveCutOff)
{
++ply;
--depth;
isVerifyingNullMoveCutOff = false;
isFailHigh = true;
}
Expand Down Expand Up @@ -145,21 +145,21 @@ private int NegaMax(int minDepth, int targetDepth, int ply, int alpha, int beta,
}
else if (movesSearched == 0)
{
evaluation = -NegaMax(minDepth, targetDepth, ply + 1, -beta, -alpha, isVerifyingNullMoveCutOff);
evaluation = -NegaMax(depth - 1, ply + 1, -beta, -alpha, isVerifyingNullMoveCutOff);
}
else
{
// 🔍 Late Move Reduction (LMR)
if (movesSearched >= Configuration.EngineSettings.LMR_FullDepthMoves
&& ply >= Configuration.EngineSettings.LMR_ReductionLimit
&& depth >= Configuration.EngineSettings.LMR_ReductionLimit
&& !pvNode
&& !isInCheck
//&& !newPosition.IsInCheck()
&& !move.IsCapture()
&& move.PromotedPiece() == default)
{
// Search with reduced depth
evaluation = -NegaMax(minDepth, targetDepth, ply + 1 + Configuration.EngineSettings.LMR_DepthReduction, -alpha - 1, -alpha, isVerifyingNullMoveCutOff);
evaluation = -NegaMax(depth - 1 - Configuration.EngineSettings.LMR_DepthReduction, ply + 1, -alpha - 1, -alpha, isVerifyingNullMoveCutOff);
}
else
{
Expand All @@ -177,17 +177,17 @@ private int NegaMax(int minDepth, int targetDepth, int ply, int alpha, int beta,
// https://web.archive.org/web/20071030220825/http://www.brucemo.com/compchess/programming/pvs.htm

// Search with full depth but narrowed score bandwidth
evaluation = -NegaMax(minDepth, targetDepth, ply + 1, -alpha - 1, -alpha, isVerifyingNullMoveCutOff);
evaluation = -NegaMax(depth - 1, ply + 1, -alpha - 1, -alpha, isVerifyingNullMoveCutOff);

if (evaluation > alpha && evaluation < beta)
{
// Hipothesis invalidated -> search with full depth and full score bandwidth
evaluation = -NegaMax(minDepth, targetDepth, ply + 1, -beta, -alpha, isVerifyingNullMoveCutOff);
evaluation = -NegaMax(depth - 1, ply + 1, -beta, -alpha, isVerifyingNullMoveCutOff);
}
}
else
{
evaluation = -NegaMax(minDepth, targetDepth, ply + 1, -beta, -alpha, isVerifyingNullMoveCutOff);
evaluation = -NegaMax(depth - 1, ply + 1, -beta, -alpha, isVerifyingNullMoveCutOff);
}
}
}
Expand All @@ -214,7 +214,7 @@ private int NegaMax(int minDepth, int targetDepth, int ply, int alpha, int beta,
_killerMoves[0, ply] = move;
}

_tt.RecordHash(_ttMask, position, targetDepth, ply, beta, NodeType.Beta, bestMove);
_tt.RecordHash(_ttMask, position, depth, ply, beta, NodeType.Beta, bestMove);

return beta; // TODO return evaluation?
}
Expand Down Expand Up @@ -242,7 +242,7 @@ private int NegaMax(int minDepth, int targetDepth, int ply, int alpha, int beta,
// [Null-move pruning] If there is a fail-high report, but no cutoff was found, the position is a zugzwang and has to be re-searched with the original depth
if (isFailHigh && alpha < beta)
{
--ply;
++depth;
isFailHigh = false;
isVerifyingNullMoveCutOff = true;
goto VerifiedNullMovePruning_SearchAgain;
Expand All @@ -252,11 +252,11 @@ private int NegaMax(int minDepth, int targetDepth, int ply, int alpha, int beta,
{
var eval = Position.EvaluateFinalPosition(ply, isInCheck);

_tt.RecordHash(_ttMask, position, targetDepth, ply, eval, NodeType.Exact);
_tt.RecordHash(_ttMask, position, depth, ply, eval, NodeType.Exact);
return eval;
}

_tt.RecordHash(_ttMask, position, targetDepth, ply, alpha, nodeType, bestMove);
_tt.RecordHash(_ttMask, position, depth, ply, alpha, nodeType, bestMove);

// Node fails low
return alpha;
Expand Down
Loading

0 comments on commit 01247a5

Please sign in to comment.