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);