Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔍 Improving: LMP, multiplying by 2 #1129

Merged
merged 6 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions src/Lynx/Model/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public sealed class Game : IDisposable
/// <summary>
/// Indexed by ply
/// </summary>
private readonly Move[] _moveStack;
private readonly PlyStackEntry[] _gameStack;

private bool _disposedValue;

public int HalfMovesWithoutCaptureOrPawnMove { get; set; }
Expand All @@ -36,7 +37,7 @@ public Game(ReadOnlySpan<char> fen, ReadOnlySpan<char> rawMoves, Span<Range> ran
{
Debug.Assert(Constants.MaxNumberMovesInAGame <= 1024, "Need to customized ArrayPool due to desired array size requirements");
_positionHashHistory = ArrayPool<ulong>.Shared.Rent(Constants.MaxNumberMovesInAGame);
_moveStack = ArrayPool<Move>.Shared.Rent(Constants.MaxNumberMovesInAGame);
_gameStack = ArrayPool<PlyStackEntry>.Shared.Rent(Constants.MaxNumberMovesInAGame);

var parsedFen = FENParser.ParseFEN(fen);
CurrentPosition = new Position(parsedFen);
Expand Down Expand Up @@ -230,10 +231,19 @@ public void UpdateInitialPosition()
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PushToMoveStack(int n, Move move) => _moveStack[n + EvaluationConstants.ContinuationHistoryPlyCount] = move;
public void UpdateMoveinStack(int n, Move move) => _gameStack[n + EvaluationConstants.ContinuationHistoryPlyCount].Move = move;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Move ReadMoveFromStack(int n) => _gameStack[n + EvaluationConstants.ContinuationHistoryPlyCount].Move;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int ReadStaticEvalFromStack(int n) => _gameStack[n].StaticEval;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int UpdateStaticEvalInStack(int n, int value) => _gameStack[n].StaticEval = value;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Move PopFromMoveStack(int n) => _moveStack[n + EvaluationConstants.ContinuationHistoryPlyCount];
public ref PlyStackEntry GameStack(int n) => ref _gameStack[n];

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int PositionHashHistoryLength() => _positionHashHistoryPointer;
Expand All @@ -251,7 +261,7 @@ public void UpdateInitialPosition()

public void FreeResources()
{
ArrayPool<Move>.Shared.Return(_moveStack);
ArrayPool<PlyStackEntry>.Shared.Return(_gameStack);
ArrayPool<ulong>.Shared.Return(_positionHashHistory);

CurrentPosition.FreeResources();
Expand Down
13 changes: 13 additions & 0 deletions src/Lynx/Model/PlyStackEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Lynx.Model;

public struct PlyStackEntry
{
public int StaticEval { get; set; }

public Move Move { get; set; }

public PlyStackEntry()
{
StaticEval = int.MaxValue;
}
}
4 changes: 2 additions & 2 deletions src/Lynx/Search/MoveOrdering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ internal int ScoreMove(Move move, int ply, bool isNotQSearch, ShortMove bestMove

if (ply >= 1)
{
var previousMove = Game.PopFromMoveStack(ply - 1);
var previousMove = Game.ReadMoveFromStack(ply - 1);
Debug.Assert(previousMove != 0);
var previousMovePiece = previousMove.Piece();
var previousMoveTargetSquare = previousMove.TargetSquare();
Expand Down Expand Up @@ -130,7 +130,7 @@ private void UpdateMoveOrderingHeuristicsOnQuietBetaCutoff(int depth, int ply, R
{
// 🔍 Continuation history
// - Counter move history (continuation history, ply - 1)
var previousMove = Game.PopFromMoveStack(ply - 1);
var previousMove = Game.ReadMoveFromStack(ply - 1);
Debug.Assert(previousMove != 0);

previousMovePiece = previousMove.Piece();
Expand Down
26 changes: 23 additions & 3 deletions src/Lynx/Search/NegaMax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM
if (!isRoot)
{
(ttScore, ttBestMove, ttElementType, ttRawScore, ttStaticEval) = _tt.ProbeHash(position, depth, ply, alpha, beta);

// TT cutoffs
if (!pvNode && ttScore != EvaluationConstants.NoHashEntry)
{
return ttScore;
Expand All @@ -67,6 +69,13 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM
// Before any time-consuming operations
_searchCancellationTokenSource.Token.ThrowIfCancellationRequested();

// 🔍 Improving heuristic: the current position has a better static evaluation than
// the previous evaluation from the same side (ply - 2).
// When true, we can:
// - Prune more aggressively when evaluation is too high: current position is even getter
// - Prune less aggressively when evaluation is low low: uncertainty on how bad the position really is
bool improving = false;

bool isInCheck = position.IsInCheck();
int staticEval = int.MaxValue;
int phase = int.MaxValue;
Expand Down Expand Up @@ -100,6 +109,13 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM
phase = position.Phase();
}

Game.UpdateStaticEvalInStack(ply, staticEval);

if (ply >= 2)
{
improving = staticEval > Game.ReadStaticEvalFromStack(ply - 2);
}

// From smol.cs
// ttEvaluation can be used as a better positional evaluation:
// If the score is outside what the current bounds are, but it did match flag and depth,
Expand All @@ -110,6 +126,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM
staticEval = ttRawScore;
}

// Fail-high pruning (moves with high scores) - prune more when improving
if (depth <= Configuration.EngineSettings.RFP_MaxDepth)
{
// 🔍 Reverse Futility Pruning (RFP) - https://www.chessprogramming.org/Reverse_Futility_Pruning
Expand Down Expand Up @@ -231,7 +248,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM
var oldHalfMovesWithoutCaptureOrPawnMove = Game.HalfMovesWithoutCaptureOrPawnMove;
var canBeRepetition = Game.Update50movesRule(move, isCapture);
Game.AddToPositionHashHistory(position.UniqueIdentifier);
Game.PushToMoveStack(ply, move);
Game.UpdateMoveinStack(ply, move);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
void RevertMove()
Expand Down Expand Up @@ -263,13 +280,14 @@ void RevertMove()
// If we prune while getting checmated, we risk not finding any move and having an empty PV
bool isNotGettingCheckmated = bestScore > EvaluationConstants.NegativeCheckmateDetectionLimit;

// Fail-low pruning (moves with low scores) - prune less when improving
if (!pvNode && !isInCheck && isNotGettingCheckmated
&& moveScores[moveIndex] < EvaluationConstants.PromotionMoveScoreValue) // Quiet move
{
// 🔍 Late Move Pruning (LMP) - all quiet moves can be pruned
// after searching the first few given by the move ordering algorithm
if (depth <= Configuration.EngineSettings.LMP_MaxDepth
&& moveIndex >= Configuration.EngineSettings.LMP_BaseMovesToTry + (Configuration.EngineSettings.LMP_MovesDepthMultiplier * depth)) // Based on formula suggested by Antares
&& moveIndex >= Configuration.EngineSettings.LMP_BaseMovesToTry + (Configuration.EngineSettings.LMP_MovesDepthMultiplier * depth * (improving ? 2 : 1))) // Based on formula suggested by Antares
{
RevertMove();
break;
Expand Down Expand Up @@ -486,6 +504,8 @@ public int QuiescenceSearch(int ply, int alpha, int beta)
? ttProbeResult.StaticEval
: position.StaticEvaluation(Game.HalfMovesWithoutCaptureOrPawnMove).Score;

Game.UpdateStaticEvalInStack(ply, staticEval);

// Beta-cutoff (updating alpha after this check)
if (staticEval >= beta)
{
Expand Down Expand Up @@ -553,7 +573,7 @@ public int QuiescenceSearch(int ply, int alpha, int beta)
PrintPreMove(position, ply, move, isQuiescence: true);

// No need to check for threefold or 50 moves repetitions, since we're only searching captures, promotions, and castles
Game.PushToMoveStack(ply, move);
Game.UpdateMoveinStack(ply, move);

#pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters
int score = -QuiescenceSearch(ply + 1, -beta, -alpha);
Expand Down
Loading