diff --git a/src/Lynx/Model/Game.cs b/src/Lynx/Model/Game.cs
index 0785d31ea..004d239b1 100644
--- a/src/Lynx/Model/Game.cs
+++ b/src/Lynx/Model/Game.cs
@@ -19,7 +19,8 @@ public sealed class Game : IDisposable
///
/// Indexed by ply
///
- private readonly Move[] _moveStack;
+ private readonly PlyStackEntry[] _gameStack;
+
private bool _disposedValue;
public int HalfMovesWithoutCaptureOrPawnMove { get; set; }
@@ -36,7 +37,7 @@ public Game(ReadOnlySpan fen, ReadOnlySpan rawMoves, Span ran
{
Debug.Assert(Constants.MaxNumberMovesInAGame <= 1024, "Need to customized ArrayPool due to desired array size requirements");
_positionHashHistory = ArrayPool.Shared.Rent(Constants.MaxNumberMovesInAGame);
- _moveStack = ArrayPool.Shared.Rent(Constants.MaxNumberMovesInAGame);
+ _gameStack = ArrayPool.Shared.Rent(Constants.MaxNumberMovesInAGame);
var parsedFen = FENParser.ParseFEN(fen);
CurrentPosition = new Position(parsedFen);
@@ -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;
@@ -251,7 +261,7 @@ public void UpdateInitialPosition()
public void FreeResources()
{
- ArrayPool.Shared.Return(_moveStack);
+ ArrayPool.Shared.Return(_gameStack);
ArrayPool.Shared.Return(_positionHashHistory);
CurrentPosition.FreeResources();
diff --git a/src/Lynx/Model/PlyStackEntry.cs b/src/Lynx/Model/PlyStackEntry.cs
new file mode 100644
index 000000000..5465cbd77
--- /dev/null
+++ b/src/Lynx/Model/PlyStackEntry.cs
@@ -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;
+ }
+}
diff --git a/src/Lynx/Search/MoveOrdering.cs b/src/Lynx/Search/MoveOrdering.cs
index c561179ca..16878d71e 100644
--- a/src/Lynx/Search/MoveOrdering.cs
+++ b/src/Lynx/Search/MoveOrdering.cs
@@ -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();
@@ -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();
diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs
index b4186d866..4fdb48d60 100644
--- a/src/Lynx/Search/NegaMax.cs
+++ b/src/Lynx/Search/NegaMax.cs
@@ -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;
@@ -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;
@@ -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,
@@ -231,7 +247,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()
@@ -269,7 +285,7 @@ void RevertMove()
// 🔍 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 * depth / (improving ? 1 : 2))) // Based on formula suggested by Antares
{
RevertMove();
break;
@@ -486,6 +502,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)
{
@@ -553,7 +571,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);