From e46ad7f46c1af8d66c8f3936d032e836a0b023e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 21 Apr 2025 00:43:40 +0200 Subject: [PATCH 1/6] Implement Position.PieceHash using an array --- src/Lynx/Model/GameState.cs | 22 ++++++++++-- src/Lynx/Model/Position.cs | 68 +++++++++++++++++++++++++++++-------- src/Lynx/Search/NegaMax.cs | 4 +-- src/Lynx/ZobristTable.cs | 50 +++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 19 deletions(-) diff --git a/src/Lynx/Model/GameState.cs b/src/Lynx/Model/GameState.cs index 39c9e47ff..ea218f59a 100644 --- a/src/Lynx/Model/GameState.cs +++ b/src/Lynx/Model/GameState.cs @@ -1,8 +1,10 @@ -namespace Lynx.Model; +using System.Buffers; + +namespace Lynx.Model; #pragma warning disable CA1051 // Do not declare visible instance fields -public readonly struct GameState +public readonly struct GameState : IDisposable { public readonly ulong ZobristKey; @@ -12,6 +14,12 @@ public readonly struct GameState public readonly ulong NonPawnBlackKey; + #region PieceKeys + + public readonly ulong[] PieceKey; + + #endregion + public readonly int IncremetalEvalAccumulator; public readonly int IncrementalPhaseAccumulator; @@ -22,19 +30,27 @@ public readonly struct GameState public readonly bool IsIncrementalEval; - public GameState(ulong zobristKey, ulong kingPawnKey, ulong nonPawnWhiteKey, ulong nonPawnBlackKey, + public GameState(ulong zobristKey, ulong kingPawnKey, ulong nonPawnWhiteKey, ulong nonPawnBlackKey, ulong[] pieceKey, int incrementalEvalAccumulator, int incrementalPhaseAccumulator, BoardSquare enpassant, byte castle, bool isIncrementalEval) { ZobristKey = zobristKey; KingPawnKey = kingPawnKey; NonPawnWhiteKey = nonPawnWhiteKey; NonPawnBlackKey = nonPawnBlackKey; + PieceKey = ArrayPool.Shared.Rent(12); + Array.Copy(pieceKey, PieceKey, 12); + IncremetalEvalAccumulator = incrementalEvalAccumulator; IncrementalPhaseAccumulator = incrementalPhaseAccumulator; EnPassant = enpassant; Castle = castle; IsIncrementalEval = isIncrementalEval; } + + public void Dispose() + { + ArrayPool.Shared.Return(PieceKey, clearArray: true); + } } #pragma warning restore CA1051 // Do not declare visible instance fields diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 97fc75f3d..265d46afe 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -23,6 +23,8 @@ public class Position : IDisposable public ulong[] NonPawnHash { get; private set; } + public ulong[] PieceUniqueIdentifiers { get; private set; } + /// /// Use as index /// @@ -81,7 +83,8 @@ public Position((BitBoard[] PieceBitBoards, BitBoard[] OccupancyBitBoards, int[] KingPawnUniqueIdentifier = ZobristTable.KingPawnHash(this); UniqueIdentifier = ZobristTable.PositionHash(this, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black]); - + PieceUniqueIdentifiers = ArrayPool.Shared.Rent(12); + ZobristTable.PieceUniqueIdentifiers(this, PieceUniqueIdentifiers); Debug.Assert(UniqueIdentifier == ZobristTable.PositionHash(this)); Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.White) == NonPawnHash[(int)Side.White]); Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.Black) == NonPawnHash[(int)Side.Black]); @@ -103,6 +106,9 @@ public Position(Position position) NonPawnHash[(int)Side.White] = position.NonPawnHash[(int)Side.White]; NonPawnHash[(int)Side.Black] = position.NonPawnHash[(int)Side.Black]; + PieceUniqueIdentifiers = ArrayPool.Shared.Rent(12); + Array.Copy(position.PieceUniqueIdentifiers, PieceUniqueIdentifiers, position.PieceUniqueIdentifiers.Length); + PieceBitBoards = ArrayPool.Shared.Rent(12); Array.Copy(position.PieceBitBoards, PieceBitBoards, position.PieceBitBoards.Length); @@ -130,16 +136,24 @@ public GameState MakeMove(Move move) Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.White) == NonPawnHash[(int)Side.White]); Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.Black) == NonPawnHash[(int)Side.Black]); - byte castleCopy = Castle; - BoardSquare enpassantCopy = EnPassant; - ulong uniqueIdentifierCopy = UniqueIdentifier; - ulong kingPawnKeyUniqueIdentifierCopy = KingPawnUniqueIdentifier; - ulong nonPawnWhiteHashCopy = NonPawnHash[(int)Side.White]; - ulong nonPawnBlackHashCopy = NonPawnHash[(int)Side.Black]; - int incrementalEvalAccumulatorCopy = _incrementalEvalAccumulator; - int incrementalPhaseAccumulatorCopy = _incrementalPhaseAccumulator; - // We also save a copy of _isIncrementalEval, so that current move doesn't affect 'sibling' moves exploration - bool isIncrementalEvalCopy = _isIncrementalEval; +#if DEBUG + Span arr = stackalloc ulong[12]; + ZobristTable.PieceUniqueIdentifiers(this, arr); + + Debug.Assert(arr[(int)Piece.N] == PieceUniqueIdentifiers[(int)Piece.N]); + Debug.Assert(arr[(int)Piece.B] == PieceUniqueIdentifiers[(int)Piece.B]); + Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); + Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); + Debug.Assert(arr[(int)Piece.K] == PieceUniqueIdentifiers[(int)Piece.K]); + Debug.Assert(arr[(int)Piece.n] == PieceUniqueIdentifiers[(int)Piece.n]); + Debug.Assert(arr[(int)Piece.b] == PieceUniqueIdentifiers[(int)Piece.b]); + Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); + Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); + Debug.Assert(arr[(int)Piece.k] == PieceUniqueIdentifiers[(int)Piece.k]); +#endif + // No need to make copies of value type, and reference ones are copied inside of the constructor + var gameState = new GameState(UniqueIdentifier, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black], PieceUniqueIdentifiers, + _incrementalEvalAccumulator, _incrementalPhaseAccumulator, EnPassant, Castle, _isIncrementalEval); var oldSide = (int)Side; var offset = Utils.PieceOffset(oldSide); @@ -170,13 +184,15 @@ public GameState MakeMove(Move move) var targetPieceHash = ZobristTable.PieceHash(targetSquare, newPiece); var fullPieceMovementHash = sourcePieceHash ^ targetPieceHash; - UniqueIdentifier ^= ZobristTable.SideHash() ^ fullPieceMovementHash ^ ZobristTable.EnPassantHash((int)EnPassant) // We clear the existing enpassant square, if any ^ ZobristTable.CastleHash(Castle); // We clear the existing castle rights + PieceUniqueIdentifiers[piece] ^= sourcePieceHash; + PieceUniqueIdentifiers[newPiece] ^= targetPieceHash; + if (piece == (int)Piece.P || piece == (int)Piece.p) { KingPawnUniqueIdentifier ^= sourcePieceHash; // We remove pawn from start square @@ -245,6 +261,7 @@ public GameState MakeMove(Move move) var capturedPieceHash = ZobristTable.PieceHash(capturedSquare, capturedPiece); UniqueIdentifier ^= capturedPieceHash; + PieceUniqueIdentifiers[capturedPiece] ^= capturedPieceHash; // Kings can't be captured if (capturedPiece == (int)Piece.P || capturedPiece == (int)Piece.p) @@ -294,6 +311,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; + PieceUniqueIdentifiers[rookIndex] ^= hashChange; _incrementalEvalAccumulator -= PSQT(0, sameSideBucket, rookIndex, rookSourceSquare); _incrementalEvalAccumulator -= PSQT(1, opposideSideBucket, rookIndex, rookSourceSquare); @@ -322,6 +340,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; + PieceUniqueIdentifiers[rookIndex] ^= hashChange; _incrementalEvalAccumulator -= PSQT(0, sameSideBucket, rookIndex, rookSourceSquare); _incrementalEvalAccumulator -= PSQT(1, opposideSideBucket, rookIndex, rookSourceSquare); @@ -372,6 +391,7 @@ public GameState MakeMove(Move move) ulong capturedPieceHash = ZobristTable.PieceHash(capturedSquare, capturedPiece); UniqueIdentifier ^= capturedPieceHash; + PieceUniqueIdentifiers[capturedPiece] ^= capturedPieceHash; // Kings can't be captured if (capturedPiece == (int)Piece.P || capturedPiece == (int)Piece.p) @@ -416,6 +436,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; + PieceUniqueIdentifiers[rookIndex] ^= hashChange; break; } @@ -438,6 +459,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; + PieceUniqueIdentifiers[rookIndex] ^= hashChange; break; } @@ -475,10 +497,26 @@ public GameState MakeMove(Move move) Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.White) == NonPawnHash[(int)Side.White]); Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.Black) == NonPawnHash[(int)Side.Black]); +#if DEBUG + arr = stackalloc ulong[12]; + ZobristTable.PieceUniqueIdentifiers(this, arr); + + Debug.Assert(arr[(int)Piece.N] == PieceUniqueIdentifiers[(int)Piece.N]); + Debug.Assert(arr[(int)Piece.B] == PieceUniqueIdentifiers[(int)Piece.B]); + Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); + Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); + Debug.Assert(arr[(int)Piece.K] == PieceUniqueIdentifiers[(int)Piece.K]); + Debug.Assert(arr[(int)Piece.n] == PieceUniqueIdentifiers[(int)Piece.n]); + Debug.Assert(arr[(int)Piece.b] == PieceUniqueIdentifiers[(int)Piece.b]); + Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); + Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); + Debug.Assert(arr[(int)Piece.k] == PieceUniqueIdentifiers[(int)Piece.k]); +#endif + // KingPawn hash assert won't work due to PassedPawnBonusNoEnemiesAheadBonus //Debug.Assert(ZobristTable.PawnKingHash(this) != _kingPawnUniqueIdentifier && WasProduceByAValidMove()); - return new GameState(uniqueIdentifierCopy, kingPawnKeyUniqueIdentifierCopy, nonPawnWhiteHashCopy, nonPawnBlackHashCopy, incrementalEvalAccumulatorCopy, incrementalPhaseAccumulatorCopy, enpassantCopy, castleCopy, isIncrementalEvalCopy); + return gameState; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -582,6 +620,7 @@ public void UnmakeMove(Move move, GameState gameState) KingPawnUniqueIdentifier = gameState.KingPawnKey; NonPawnHash[(int)Side.White] = gameState.NonPawnWhiteKey; NonPawnHash[(int)Side.Black] = gameState.NonPawnBlackKey; + Array.Copy(gameState.PieceKey, PieceUniqueIdentifiers, 12); _incrementalEvalAccumulator = gameState.IncremetalEvalAccumulator; _incrementalPhaseAccumulator = gameState.IncrementalPhaseAccumulator; _isIncrementalEval = gameState.IsIncrementalEval; @@ -599,7 +638,7 @@ public GameState MakeNullMove() ZobristTable.SideHash() ^ ZobristTable.EnPassantHash((int)oldEnPassant); - return new GameState(oldUniqueIdentifier, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black], + return new GameState(oldUniqueIdentifier, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black], PieceUniqueIdentifiers, _incrementalEvalAccumulator, _incrementalPhaseAccumulator, oldEnPassant, byte.MaxValue, _isIncrementalEval); } @@ -1812,6 +1851,7 @@ public void FreeResources() ArrayPool.Shared.Return(PieceBitBoards, clearArray: true); ArrayPool.Shared.Return(OccupancyBitBoards, clearArray: true); ArrayPool.Shared.Return(NonPawnHash, clearArray: true); + ArrayPool.Shared.Return(PieceUniqueIdentifiers, clearArray: true); // No need to clear, since we always have to initialize it to Piece.None after renting it anyway #pragma warning disable S3254 // Default parameter values should not be passed as arguments ArrayPool.Shared.Return(Board, clearArray: false); diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index 29c7f62d8..cec783cfd 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -265,7 +265,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool cutnode, Cance // depth, // 3 + (depth / 3) + Math.Min((staticEval - beta) / 200, 3)); - var gameState = position.MakeNullMove(); + using var gameState = position.MakeNullMove(); var nmpScore = -NegaMax(depth - 1 - nmpReduction, ply + 1, -beta, -beta + 1, !cutnode, cancellationToken, parentWasNullMove: true); position.UnMakeNullMove(gameState); @@ -796,7 +796,7 @@ public int QuiescenceSearch(int ply, int alpha, int beta, bool pvNode, Cancellat continue; } - var gameState = position.MakeMove(move); + using var gameState = position.MakeMove(move); if (!position.WasProduceByAValidMove()) { position.UnmakeMove(move, gameState); diff --git a/src/Lynx/ZobristTable.cs b/src/Lynx/ZobristTable.cs index 3c2f3e81f..36e99dfa0 100644 --- a/src/Lynx/ZobristTable.cs +++ b/src/Lynx/ZobristTable.cs @@ -193,6 +193,56 @@ public static ulong NonPawnSideHash(Position position, int side) return nonPawnSideHash; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MinorHash(Position position) + { + ulong minorHash = 0; + + for (int pieceIndex = (int)Piece.N; pieceIndex <= (int)Piece.B; ++pieceIndex) + { + var whiteBitboard = position.PieceBitBoards[pieceIndex]; + while (whiteBitboard != default) + { + whiteBitboard = whiteBitboard.WithoutLS1B(out var pieceSquareIndex); + + minorHash ^= PieceHash(pieceSquareIndex, pieceIndex); + } + + var blackBitboard = position.PieceBitBoards[pieceIndex + 6]; + while (blackBitboard != default) + { + blackBitboard = blackBitboard.WithoutLS1B(out var pieceSquareIndex); + + minorHash ^= PieceHash(pieceSquareIndex, pieceIndex + 6); + } + } + + return minorHash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void PieceUniqueIdentifiers(Position position, Span pieceIdentifiers) + { + for (int pieceIndex = (int)Piece.N; pieceIndex <= (int)Piece.K; ++pieceIndex) + { + var whiteBitboard = position.PieceBitBoards[pieceIndex]; + while (whiteBitboard != default) + { + whiteBitboard = whiteBitboard.WithoutLS1B(out var pieceSquareIndex); + + pieceIdentifiers[pieceIndex] ^= PieceHash(pieceSquareIndex, pieceIndex); + } + + var blackBitboard = position.PieceBitBoards[pieceIndex + 6]; + while (blackBitboard != default) + { + blackBitboard = blackBitboard.WithoutLS1B(out var pieceSquareIndex); + + pieceIdentifiers[pieceIndex + 6] ^= PieceHash(pieceSquareIndex, pieceIndex + 6); + } + } + } + /// /// Initializes Zobrist table (long[64][12]) /// From e30d14f8ad8dfaa25f0a916863b694344e29f1ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 21 Apr 2025 01:00:01 +0200 Subject: [PATCH 2/6] Add minor corrhist implementation, dividing by `4 * scale` --- src/Lynx/Constants.cs | 3 +++ src/Lynx/Engine.cs | 1 + src/Lynx/Model/Position.cs | 8 ++++++++ src/Lynx/Search/Helpers.cs | 32 ++++++++++++++++++++++++++++---- src/Lynx/Search/IDDFS.cs | 6 ++++++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Lynx/Constants.cs b/src/Lynx/Constants.cs index e6b85e9a6..0f20d161b 100644 --- a/src/Lynx/Constants.cs +++ b/src/Lynx/Constants.cs @@ -536,6 +536,9 @@ public static class Constants public const int NonPawnCorrHistorySize = 16_384; public const int NonPawnCorrHistoryMask = NonPawnCorrHistorySize - 1; + public const int MinorCorrHistorySize = 16_384; + public const int MinorCorrHistoryMask = MinorCorrHistorySize - 1; + public const int CorrectionHistoryScale = 256; public const string NumberWithSignFormat = "+#;-#;0"; diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index 00c3823af..3ee82f12f 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -82,6 +82,7 @@ private void ResetEngine() Array.Clear(_pawnCorrHistory); Array.Clear(_nonPawnCorrHistory); + Array.Clear(_minorCorrHistory); // No need to clear killer move or pv table because they're cleared on every search (IDDFS) } diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 265d46afe..d3e469873 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -127,6 +127,12 @@ public Position(Position position) _incrementalPhaseAccumulator = position._incrementalPhaseAccumulator; } + public ulong MinorHash => + PieceUniqueIdentifiers[(int)Piece.N] + ^ PieceUniqueIdentifiers[(int)Piece.B] + ^ PieceUniqueIdentifiers[(int)Piece.n] + ^ PieceUniqueIdentifiers[(int)Piece.b]; + #region Move making [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -150,6 +156,8 @@ public GameState MakeMove(Move move) Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); Debug.Assert(arr[(int)Piece.k] == PieceUniqueIdentifiers[(int)Piece.k]); + + Debug.Assert(ZobristTable.MinorHash(this) == MinorHash); #endif // No need to make copies of value type, and reference ones are copied inside of the constructor var gameState = new GameState(UniqueIdentifier, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black], PieceUniqueIdentifiers, diff --git a/src/Lynx/Search/Helpers.cs b/src/Lynx/Search/Helpers.cs index 4480c1738..49eee526f 100644 --- a/src/Lynx/Search/Helpers.cs +++ b/src/Lynx/Search/Helpers.cs @@ -114,6 +114,7 @@ private void UpdateCorrectionHistory(Position position, int evaluationDelta, int var scaledBonus = evaluationDelta * Constants.CorrectionHistoryScale; var weight = 2 * Math.Min(16, depth + 1); + // Pawn correction history var pawnHash = position.KingPawnUniqueIdentifier ^ ZobristTable.PieceHash(position.WhiteKingSquare, (int)Piece.K) ^ ZobristTable.PieceHash(position.BlackKingSquare, (int)Piece.k); @@ -124,9 +125,9 @@ private void UpdateCorrectionHistory(Position position, int evaluationDelta, int Debug.Assert(pawnCorrHistIndex < (ulong)_pawnCorrHistory.Length); ref var pawnCorrHistEntry = ref _pawnCorrHistory[pawnCorrHistIndex]; - pawnCorrHistEntry = UpdateCorrectionHistory(pawnCorrHistEntry, scaledBonus, weight); + // Non-pawn correction history - side to move var nonPawnSTMIndex = position.NonPawnHash[side] & Constants.NonPawnCorrHistoryMask; var nonPawnCorrHistSTMIndex = @@ -137,9 +138,9 @@ private void UpdateCorrectionHistory(Position position, int evaluationDelta, int Debug.Assert(nonPawnCorrHistSTMIndex < (ulong)_nonPawnCorrHistory.Length); ref var nonPawnSTMCorrHistEntry = ref _nonPawnCorrHistory[nonPawnCorrHistSTMIndex]; - nonPawnSTMCorrHistEntry = UpdateCorrectionHistory(nonPawnSTMCorrHistEntry, scaledBonus, weight); + // Non-pawn correction history - not side to move var nonPawnNoSTMIndex = position.NonPawnHash[oppositeSide] & Constants.NonPawnCorrHistoryMask; var nonPawnNoSTMCorrHistIndex = (nonPawnNoSTMIndex * 2 * 2) @@ -152,6 +153,17 @@ private void UpdateCorrectionHistory(Position position, int evaluationDelta, int nonPawnNoSTMCorrHistEntry = UpdateCorrectionHistory(nonPawnNoSTMCorrHistEntry, scaledBonus, weight); + // Minor correction history + var minorHash = position.MinorHash; + var minorIndex = minorHash & Constants.MinorCorrHistoryMask; + + var minorCorrHistIndex = (2 * minorIndex) + side; + Debug.Assert(minorCorrHistIndex < (ulong)_minorCorrHistory.Length); + + ref var minorCorrHistEntry = ref _minorCorrHistory[minorCorrHistIndex]; + minorCorrHistEntry = UpdateCorrectionHistory(minorCorrHistEntry, scaledBonus, weight); + + // Common update logic [MethodImpl(MethodImplOptions.AggressiveInlining)] static int UpdateCorrectionHistory(int previousCorrectedScore, int scaledBonus, int weight) { @@ -176,6 +188,7 @@ private int CorrectStaticEvaluation(Position position, int staticEvaluation) var side = (ulong)position.Side; var oppositeSide = Utils.OppositeSide((int)side); + // Pawn correction history var pawnHash = position.KingPawnUniqueIdentifier ^ ZobristTable.PieceHash(position.WhiteKingSquare, (int)Piece.K) ^ ZobristTable.PieceHash(position.BlackKingSquare, (int)Piece.k); @@ -187,6 +200,7 @@ private int CorrectStaticEvaluation(Position position, int staticEvaluation) var pawnCorrHist = _pawnCorrHistory[pawnCorrHistIndex]; + // Non-pawn correction history - side to move var nonPawnSTMoveIndex = position.NonPawnHash[side] & Constants.NonPawnCorrHistoryMask; var nonPawnSTMoveCorrHistIndex = (nonPawnSTMoveIndex * 2 * 2) @@ -197,6 +211,7 @@ private int CorrectStaticEvaluation(Position position, int staticEvaluation) var nonPawnSTMCorrHist = _nonPawnCorrHistory[nonPawnSTMoveCorrHistIndex]; + // Non-pawn correction history - not side to move var nonPawnNoSTMIndex = position.NonPawnHash[oppositeSide] & Constants.NonPawnCorrHistoryMask; var nonPawnNoSTMCorrHistIndex = (nonPawnNoSTMIndex * 2 * 2) @@ -207,8 +222,17 @@ private int CorrectStaticEvaluation(Position position, int staticEvaluation) var nonPawnNoSTMCorrHist = _nonPawnCorrHistory[nonPawnNoSTMCorrHistIndex]; - var correction = pawnCorrHist + nonPawnSTMCorrHist + nonPawnNoSTMCorrHist; - var correctStaticEval = staticEvaluation + (correction / (Constants.CorrectionHistoryScale * 3)); + // Minor correction history + var minorHash = position.MinorHash; + var minorIndex = minorHash & Constants.MinorCorrHistoryMask; + + var minorCorrHistIndex = (2 * minorIndex) + side; + Debug.Assert(minorCorrHistIndex < (ulong)_minorCorrHistory.Length); + + var minorCorrHist = _minorCorrHistory[minorCorrHistIndex]; + + var correction = pawnCorrHist + nonPawnSTMCorrHist + nonPawnNoSTMCorrHist + minorCorrHist; + var correctStaticEval = staticEvaluation + (correction / (Constants.CorrectionHistoryScale * 4)); return Math.Clamp(correctStaticEval, EvaluationConstants.MinStaticEval, EvaluationConstants.MaxStaticEval); } diff --git a/src/Lynx/Search/IDDFS.cs b/src/Lynx/Search/IDDFS.cs index b4404acd7..a993fa6ef 100644 --- a/src/Lynx/Search/IDDFS.cs +++ b/src/Lynx/Search/IDDFS.cs @@ -53,6 +53,12 @@ public sealed partial class Engine /// private readonly int[] _nonPawnCorrHistory = GC.AllocateArray(Constants.NonPawnCorrHistorySize * 2 * 2, pinned: true); + /// + /// x 2 + /// Pawn hash x side to move + /// + private readonly int[] _minorCorrHistory = GC.AllocateArray(Constants.PawnCorrHistorySize * 2, pinned: true); + /// /// 12 x 64 /// piece x target square From 7a9a4fac994a5f66586bc7cda20ef96b2f1ffad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 21 Apr 2025 02:03:22 +0200 Subject: [PATCH 3/6] Avoid the array in GameState --- src/Lynx/Model/GameState.cs | 27 ++++++++++++----------- src/Lynx/Model/Position.cs | 43 ++++++++++++++++++++++++------------- src/Lynx/Search/NegaMax.cs | 4 ++-- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/Lynx/Model/GameState.cs b/src/Lynx/Model/GameState.cs index ea218f59a..5df6569ab 100644 --- a/src/Lynx/Model/GameState.cs +++ b/src/Lynx/Model/GameState.cs @@ -1,10 +1,8 @@ -using System.Buffers; - -namespace Lynx.Model; +namespace Lynx.Model; #pragma warning disable CA1051 // Do not declare visible instance fields -public readonly struct GameState : IDisposable +public readonly struct GameState { public readonly ulong ZobristKey; @@ -16,7 +14,13 @@ namespace Lynx.Model; #region PieceKeys - public readonly ulong[] PieceKey; + public readonly ulong KnightWhiteKey; + + public readonly ulong KnightBlackKey; + + public readonly ulong BishopWhiteKey; + + public readonly ulong BishopBlackKey; #endregion @@ -30,15 +34,17 @@ namespace Lynx.Model; public readonly bool IsIncrementalEval; - public GameState(ulong zobristKey, ulong kingPawnKey, ulong nonPawnWhiteKey, ulong nonPawnBlackKey, ulong[] pieceKey, + public GameState(ulong zobristKey, ulong kingPawnKey, ulong nonPawnWhiteKey, ulong nonPawnBlackKey, ulong knightWhiteKey, ulong knightBlackKey, ulong bishopWhiteKey, ulong bishopBlackKey, int incrementalEvalAccumulator, int incrementalPhaseAccumulator, BoardSquare enpassant, byte castle, bool isIncrementalEval) { ZobristKey = zobristKey; KingPawnKey = kingPawnKey; NonPawnWhiteKey = nonPawnWhiteKey; NonPawnBlackKey = nonPawnBlackKey; - PieceKey = ArrayPool.Shared.Rent(12); - Array.Copy(pieceKey, PieceKey, 12); + KnightWhiteKey = knightWhiteKey; + KnightBlackKey = knightBlackKey; + BishopWhiteKey = bishopWhiteKey; + BishopBlackKey = bishopBlackKey; IncremetalEvalAccumulator = incrementalEvalAccumulator; IncrementalPhaseAccumulator = incrementalPhaseAccumulator; @@ -46,11 +52,6 @@ public GameState(ulong zobristKey, ulong kingPawnKey, ulong nonPawnWhiteKey, ulo Castle = castle; IsIncrementalEval = isIncrementalEval; } - - public void Dispose() - { - ArrayPool.Shared.Return(PieceKey, clearArray: true); - } } #pragma warning restore CA1051 // Do not declare visible instance fields diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index d3e469873..7f84546b4 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -148,19 +148,20 @@ public GameState MakeMove(Move move) Debug.Assert(arr[(int)Piece.N] == PieceUniqueIdentifiers[(int)Piece.N]); Debug.Assert(arr[(int)Piece.B] == PieceUniqueIdentifiers[(int)Piece.B]); - Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); - Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); - Debug.Assert(arr[(int)Piece.K] == PieceUniqueIdentifiers[(int)Piece.K]); + //Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); + //Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); + //Debug.Assert(arr[(int)Piece.K] == PieceUniqueIdentifiers[(int)Piece.K]); Debug.Assert(arr[(int)Piece.n] == PieceUniqueIdentifiers[(int)Piece.n]); Debug.Assert(arr[(int)Piece.b] == PieceUniqueIdentifiers[(int)Piece.b]); - Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); - Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); - Debug.Assert(arr[(int)Piece.k] == PieceUniqueIdentifiers[(int)Piece.k]); + //Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); + //Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); + //Debug.Assert(arr[(int)Piece.k] == PieceUniqueIdentifiers[(int)Piece.k]); Debug.Assert(ZobristTable.MinorHash(this) == MinorHash); #endif // No need to make copies of value type, and reference ones are copied inside of the constructor - var gameState = new GameState(UniqueIdentifier, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black], PieceUniqueIdentifiers, + var gameState = new GameState(UniqueIdentifier, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black], + PieceUniqueIdentifiers[(int)Piece.N], PieceUniqueIdentifiers[(int)Piece.n], PieceUniqueIdentifiers[(int)Piece.B], PieceUniqueIdentifiers[(int)Piece.b], _incrementalEvalAccumulator, _incrementalPhaseAccumulator, EnPassant, Castle, _isIncrementalEval); var oldSide = (int)Side; @@ -511,14 +512,14 @@ public GameState MakeMove(Move move) Debug.Assert(arr[(int)Piece.N] == PieceUniqueIdentifiers[(int)Piece.N]); Debug.Assert(arr[(int)Piece.B] == PieceUniqueIdentifiers[(int)Piece.B]); - Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); - Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); - Debug.Assert(arr[(int)Piece.K] == PieceUniqueIdentifiers[(int)Piece.K]); + //Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); + //Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); + //Debug.Assert(arr[(int)Piece.K] == PieceUniqueIdentifiers[(int)Piece.K]); Debug.Assert(arr[(int)Piece.n] == PieceUniqueIdentifiers[(int)Piece.n]); Debug.Assert(arr[(int)Piece.b] == PieceUniqueIdentifiers[(int)Piece.b]); - Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); - Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); - Debug.Assert(arr[(int)Piece.k] == PieceUniqueIdentifiers[(int)Piece.k]); + //Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); + //Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); + //Debug.Assert(arr[(int)Piece.k] == PieceUniqueIdentifiers[(int)Piece.k]); #endif // KingPawn hash assert won't work due to PassedPawnBonusNoEnemiesAheadBonus @@ -624,11 +625,16 @@ public void UnmakeMove(Move move, GameState gameState) // Updating saved values Castle = gameState.Castle; EnPassant = gameState.EnPassant; + UniqueIdentifier = gameState.ZobristKey; KingPawnUniqueIdentifier = gameState.KingPawnKey; NonPawnHash[(int)Side.White] = gameState.NonPawnWhiteKey; NonPawnHash[(int)Side.Black] = gameState.NonPawnBlackKey; - Array.Copy(gameState.PieceKey, PieceUniqueIdentifiers, 12); + PieceUniqueIdentifiers[(int)Piece.N] = gameState.KnightWhiteKey; + PieceUniqueIdentifiers[(int)Piece.n] = gameState.KnightBlackKey; + PieceUniqueIdentifiers[(int)Piece.B] = gameState.BishopWhiteKey; + PieceUniqueIdentifiers[(int)Piece.b] = gameState.BishopBlackKey; + _incrementalEvalAccumulator = gameState.IncremetalEvalAccumulator; _incrementalPhaseAccumulator = gameState.IncrementalPhaseAccumulator; _isIncrementalEval = gameState.IsIncrementalEval; @@ -646,7 +652,8 @@ public GameState MakeNullMove() ZobristTable.SideHash() ^ ZobristTable.EnPassantHash((int)oldEnPassant); - return new GameState(oldUniqueIdentifier, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black], PieceUniqueIdentifiers, + return new GameState(oldUniqueIdentifier, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black], + PieceUniqueIdentifiers[(int)Piece.N], PieceUniqueIdentifiers[(int)Piece.n], PieceUniqueIdentifiers[(int)Piece.B], PieceUniqueIdentifiers[(int)Piece.b], _incrementalEvalAccumulator, _incrementalPhaseAccumulator, oldEnPassant, byte.MaxValue, _isIncrementalEval); } @@ -655,10 +662,16 @@ public void UnMakeNullMove(GameState gameState) { Side = (Side)Utils.OppositeSide(Side); EnPassant = gameState.EnPassant; + UniqueIdentifier = gameState.ZobristKey; KingPawnUniqueIdentifier = gameState.KingPawnKey; NonPawnHash[(int)Side.White] = gameState.NonPawnWhiteKey; NonPawnHash[(int)Side.Black] = gameState.NonPawnBlackKey; + PieceUniqueIdentifiers[(int)Piece.N] = gameState.KnightWhiteKey; + PieceUniqueIdentifiers[(int)Piece.n] = gameState.KnightBlackKey; + PieceUniqueIdentifiers[(int)Piece.B] = gameState.BishopWhiteKey; + PieceUniqueIdentifiers[(int)Piece.b] = gameState.BishopBlackKey; + _incrementalEvalAccumulator = gameState.IncremetalEvalAccumulator; _incrementalPhaseAccumulator = gameState.IncrementalPhaseAccumulator; _isIncrementalEval = gameState.IsIncrementalEval; diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index cec783cfd..29c7f62d8 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -265,7 +265,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool cutnode, Cance // depth, // 3 + (depth / 3) + Math.Min((staticEval - beta) / 200, 3)); - using var gameState = position.MakeNullMove(); + var gameState = position.MakeNullMove(); var nmpScore = -NegaMax(depth - 1 - nmpReduction, ply + 1, -beta, -beta + 1, !cutnode, cancellationToken, parentWasNullMove: true); position.UnMakeNullMove(gameState); @@ -796,7 +796,7 @@ public int QuiescenceSearch(int ply, int alpha, int beta, bool pvNode, Cancellat continue; } - using var gameState = position.MakeMove(move); + var gameState = position.MakeMove(move); if (!position.WasProduceByAValidMove()) { position.UnmakeMove(move, gameState); From 6f08a829e57697291bb23a41f9918328b8f4c5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 21 Apr 2025 02:26:46 +0200 Subject: [PATCH 4/6] No need to update rook hashes for now, so minor speedup --- src/Lynx/Model/Position.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 7f84546b4..c6c01dabe 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -320,7 +320,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; - PieceUniqueIdentifiers[rookIndex] ^= hashChange; + //PieceUniqueIdentifiers[rookIndex] ^= hashChange; _incrementalEvalAccumulator -= PSQT(0, sameSideBucket, rookIndex, rookSourceSquare); _incrementalEvalAccumulator -= PSQT(1, opposideSideBucket, rookIndex, rookSourceSquare); @@ -349,7 +349,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; - PieceUniqueIdentifiers[rookIndex] ^= hashChange; + //PieceUniqueIdentifiers[rookIndex] ^= hashChange; _incrementalEvalAccumulator -= PSQT(0, sameSideBucket, rookIndex, rookSourceSquare); _incrementalEvalAccumulator -= PSQT(1, opposideSideBucket, rookIndex, rookSourceSquare); @@ -445,7 +445,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; - PieceUniqueIdentifiers[rookIndex] ^= hashChange; + //PieceUniqueIdentifiers[rookIndex] ^= hashChange; break; } @@ -468,7 +468,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; - PieceUniqueIdentifiers[rookIndex] ^= hashChange; + //PieceUniqueIdentifiers[rookIndex] ^= hashChange; break; } From a3a9e952560a8aed859ef3601b7966dc5eaf3cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 21 Apr 2025 03:50:23 +0200 Subject: [PATCH 5/6] `* 4` -> `* 3` --- src/Lynx/Search/Helpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lynx/Search/Helpers.cs b/src/Lynx/Search/Helpers.cs index 49eee526f..ad90e25ae 100644 --- a/src/Lynx/Search/Helpers.cs +++ b/src/Lynx/Search/Helpers.cs @@ -232,7 +232,7 @@ private int CorrectStaticEvaluation(Position position, int staticEvaluation) var minorCorrHist = _minorCorrHistory[minorCorrHistIndex]; var correction = pawnCorrHist + nonPawnSTMCorrHist + nonPawnNoSTMCorrHist + minorCorrHist; - var correctStaticEval = staticEvaluation + (correction / (Constants.CorrectionHistoryScale * 4)); + var correctStaticEval = staticEvaluation + (correction / (Constants.CorrectionHistoryScale * 3)); return Math.Clamp(correctStaticEval, EvaluationConstants.MinStaticEval, EvaluationConstants.MaxStaticEval); } From 7ca866180751b00b64019a40b1fb854239b572a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Tue, 22 Apr 2025 01:36:35 +0200 Subject: [PATCH 6/6] Add major corrhist on top of minor --- src/Lynx/Constants.cs | 3 +++ src/Lynx/Engine.cs | 1 + src/Lynx/Model/GameState.cs | 16 ++++++++++++++- src/Lynx/Model/Position.cs | 41 +++++++++++++++++++++++++------------ src/Lynx/Search/Helpers.cs | 22 +++++++++++++++++++- src/Lynx/Search/IDDFS.cs | 16 ++++++++++----- src/Lynx/ZobristTable.cs | 27 ++++++++++++++++++++++++ 7 files changed, 106 insertions(+), 20 deletions(-) diff --git a/src/Lynx/Constants.cs b/src/Lynx/Constants.cs index 0f20d161b..9985daf6f 100644 --- a/src/Lynx/Constants.cs +++ b/src/Lynx/Constants.cs @@ -539,6 +539,9 @@ public static class Constants public const int MinorCorrHistorySize = 16_384; public const int MinorCorrHistoryMask = MinorCorrHistorySize - 1; + public const int MajorCorrHistorySize = 16_384; + public const int MajorCorrHistoryMask = MajorCorrHistorySize - 1; + public const int CorrectionHistoryScale = 256; public const string NumberWithSignFormat = "+#;-#;0"; diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index 3ee82f12f..2b8e2e345 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -83,6 +83,7 @@ private void ResetEngine() Array.Clear(_pawnCorrHistory); Array.Clear(_nonPawnCorrHistory); Array.Clear(_minorCorrHistory); + Array.Clear(_majorCorrHistory); // No need to clear killer move or pv table because they're cleared on every search (IDDFS) } diff --git a/src/Lynx/Model/GameState.cs b/src/Lynx/Model/GameState.cs index ac347981f..ed1166363 100644 --- a/src/Lynx/Model/GameState.cs +++ b/src/Lynx/Model/GameState.cs @@ -22,6 +22,14 @@ public readonly struct GameState public readonly ulong BishopBlackKey; + public readonly ulong RookWhiteKey; + + public readonly ulong RookBlackKey; + + public readonly ulong QueenWhiteKey; + + public readonly ulong QueenBlackKey; + #endregion public readonly int IncremetalEvalAccumulator; @@ -35,7 +43,9 @@ public readonly struct GameState public readonly bool IsIncrementalEval; public GameState(ulong zobristKey, - ulong kingPawnKey, ulong nonPawnWhiteKey, ulong nonPawnBlackKey, ulong knightWhiteKey, ulong knightBlackKey, ulong bishopWhiteKey, ulong bishopBlackKey, + ulong kingPawnKey, ulong nonPawnWhiteKey, + ulong nonPawnBlackKey, ulong knightWhiteKey, ulong knightBlackKey, ulong bishopWhiteKey, ulong bishopBlackKey, + ulong rookWhiteKey, ulong rookBlackKey, ulong queenWhiteKey, ulong queenBlackKey, int incrementalEvalAccumulator, int incrementalPhaseAccumulator, BoardSquare enpassant, byte castle, bool isIncrementalEval) : this(zobristKey, incrementalEvalAccumulator, incrementalPhaseAccumulator, enpassant, castle, isIncrementalEval) { @@ -46,6 +56,10 @@ public GameState(ulong zobristKey, KnightBlackKey = knightBlackKey; BishopWhiteKey = bishopWhiteKey; BishopBlackKey = bishopBlackKey; + RookWhiteKey = rookWhiteKey; + RookBlackKey = rookBlackKey; + QueenWhiteKey = queenWhiteKey; + QueenBlackKey = queenBlackKey; } /// diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 0324f9a84..36a4d3ea2 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -133,6 +133,12 @@ public Position(Position position) ^ PieceUniqueIdentifiers[(int)Piece.n] ^ PieceUniqueIdentifiers[(int)Piece.b]; + public ulong MajorHash => + PieceUniqueIdentifiers[(int)Piece.R] + ^ PieceUniqueIdentifiers[(int)Piece.r] + ^ PieceUniqueIdentifiers[(int)Piece.Q] + ^ PieceUniqueIdentifiers[(int)Piece.q]; + #region Move making [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -141,6 +147,9 @@ public GameState MakeMove(Move move) Debug.Assert(ZobristTable.PositionHash(this) == UniqueIdentifier); Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.White) == NonPawnHash[(int)Side.White]); Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.Black) == NonPawnHash[(int)Side.Black]); + Debug.Assert(ZobristTable.MinorHash(this) == MinorHash); + Debug.Assert(ZobristTable.MajorHash(this) == MajorHash); + #if DEBUG Span arr = stackalloc ulong[12]; @@ -148,20 +157,20 @@ public GameState MakeMove(Move move) Debug.Assert(arr[(int)Piece.N] == PieceUniqueIdentifiers[(int)Piece.N]); Debug.Assert(arr[(int)Piece.B] == PieceUniqueIdentifiers[(int)Piece.B]); - //Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); - //Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); + Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); + Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); //Debug.Assert(arr[(int)Piece.K] == PieceUniqueIdentifiers[(int)Piece.K]); Debug.Assert(arr[(int)Piece.n] == PieceUniqueIdentifiers[(int)Piece.n]); Debug.Assert(arr[(int)Piece.b] == PieceUniqueIdentifiers[(int)Piece.b]); - //Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); - //Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); + Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); + Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); //Debug.Assert(arr[(int)Piece.k] == PieceUniqueIdentifiers[(int)Piece.k]); - Debug.Assert(ZobristTable.MinorHash(this) == MinorHash); #endif // No need to make copies of value type, and reference ones are copied inside of the constructor var gameState = new GameState(UniqueIdentifier, KingPawnUniqueIdentifier, NonPawnHash[(int)Side.White], NonPawnHash[(int)Side.Black], PieceUniqueIdentifiers[(int)Piece.N], PieceUniqueIdentifiers[(int)Piece.n], PieceUniqueIdentifiers[(int)Piece.B], PieceUniqueIdentifiers[(int)Piece.b], + PieceUniqueIdentifiers[(int)Piece.R], PieceUniqueIdentifiers[(int)Piece.r], PieceUniqueIdentifiers[(int)Piece.Q], PieceUniqueIdentifiers[(int)Piece.q], _incrementalEvalAccumulator, _incrementalPhaseAccumulator, EnPassant, Castle, _isIncrementalEval); var oldSide = (int)Side; @@ -320,7 +329,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; - //PieceUniqueIdentifiers[rookIndex] ^= hashChange; + PieceUniqueIdentifiers[rookIndex] ^= hashChange; _incrementalEvalAccumulator -= PSQT(0, sameSideBucket, rookIndex, rookSourceSquare); _incrementalEvalAccumulator -= PSQT(1, opposideSideBucket, rookIndex, rookSourceSquare); @@ -349,7 +358,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; - //PieceUniqueIdentifiers[rookIndex] ^= hashChange; + PieceUniqueIdentifiers[rookIndex] ^= hashChange; _incrementalEvalAccumulator -= PSQT(0, sameSideBucket, rookIndex, rookSourceSquare); _incrementalEvalAccumulator -= PSQT(1, opposideSideBucket, rookIndex, rookSourceSquare); @@ -445,7 +454,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; - //PieceUniqueIdentifiers[rookIndex] ^= hashChange; + PieceUniqueIdentifiers[rookIndex] ^= hashChange; break; } @@ -468,7 +477,7 @@ public GameState MakeMove(Move move) UniqueIdentifier ^= hashChange; NonPawnHash[oldSide] ^= hashChange; - //PieceUniqueIdentifiers[rookIndex] ^= hashChange; + PieceUniqueIdentifiers[rookIndex] ^= hashChange; break; } @@ -505,6 +514,8 @@ public GameState MakeMove(Move move) Debug.Assert(ZobristTable.PositionHash(this) == UniqueIdentifier); Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.White) == NonPawnHash[(int)Side.White]); Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.Black) == NonPawnHash[(int)Side.Black]); + Debug.Assert(ZobristTable.MinorHash(this) == MinorHash); + Debug.Assert(ZobristTable.MajorHash(this) == MajorHash); #if DEBUG arr = stackalloc ulong[12]; @@ -512,13 +523,13 @@ public GameState MakeMove(Move move) Debug.Assert(arr[(int)Piece.N] == PieceUniqueIdentifiers[(int)Piece.N]); Debug.Assert(arr[(int)Piece.B] == PieceUniqueIdentifiers[(int)Piece.B]); - //Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); - //Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); + Debug.Assert(arr[(int)Piece.R] == PieceUniqueIdentifiers[(int)Piece.R]); + Debug.Assert(arr[(int)Piece.Q] == PieceUniqueIdentifiers[(int)Piece.Q]); //Debug.Assert(arr[(int)Piece.K] == PieceUniqueIdentifiers[(int)Piece.K]); Debug.Assert(arr[(int)Piece.n] == PieceUniqueIdentifiers[(int)Piece.n]); Debug.Assert(arr[(int)Piece.b] == PieceUniqueIdentifiers[(int)Piece.b]); - //Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); - //Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); + Debug.Assert(arr[(int)Piece.r] == PieceUniqueIdentifiers[(int)Piece.r]); + Debug.Assert(arr[(int)Piece.q] == PieceUniqueIdentifiers[(int)Piece.q]); //Debug.Assert(arr[(int)Piece.k] == PieceUniqueIdentifiers[(int)Piece.k]); #endif @@ -634,6 +645,10 @@ public void UnmakeMove(Move move, GameState gameState) PieceUniqueIdentifiers[(int)Piece.n] = gameState.KnightBlackKey; PieceUniqueIdentifiers[(int)Piece.B] = gameState.BishopWhiteKey; PieceUniqueIdentifiers[(int)Piece.b] = gameState.BishopBlackKey; + PieceUniqueIdentifiers[(int)Piece.R] = gameState.RookWhiteKey; + PieceUniqueIdentifiers[(int)Piece.r] = gameState.RookBlackKey; + PieceUniqueIdentifiers[(int)Piece.Q] = gameState.QueenWhiteKey; + PieceUniqueIdentifiers[(int)Piece.q] = gameState.QueenBlackKey; _incrementalEvalAccumulator = gameState.IncremetalEvalAccumulator; _incrementalPhaseAccumulator = gameState.IncrementalPhaseAccumulator; diff --git a/src/Lynx/Search/Helpers.cs b/src/Lynx/Search/Helpers.cs index ad90e25ae..142515722 100644 --- a/src/Lynx/Search/Helpers.cs +++ b/src/Lynx/Search/Helpers.cs @@ -163,6 +163,16 @@ private void UpdateCorrectionHistory(Position position, int evaluationDelta, int ref var minorCorrHistEntry = ref _minorCorrHistory[minorCorrHistIndex]; minorCorrHistEntry = UpdateCorrectionHistory(minorCorrHistEntry, scaledBonus, weight); + // Minor correction history + var majorHash = position.MajorHash; + var majorIndex = majorHash & Constants.MajorCorrHistoryMask; + + var majorCorrHistIndex = (2 * majorIndex) + side; + Debug.Assert(majorCorrHistIndex < (ulong)_majorCorrHistory.Length); + + ref var majorCorrHistEntry = ref _majorCorrHistory[majorCorrHistIndex]; + majorCorrHistEntry = UpdateCorrectionHistory(majorCorrHistEntry, scaledBonus, weight); + // Common update logic [MethodImpl(MethodImplOptions.AggressiveInlining)] static int UpdateCorrectionHistory(int previousCorrectedScore, int scaledBonus, int weight) @@ -231,7 +241,17 @@ private int CorrectStaticEvaluation(Position position, int staticEvaluation) var minorCorrHist = _minorCorrHistory[minorCorrHistIndex]; - var correction = pawnCorrHist + nonPawnSTMCorrHist + nonPawnNoSTMCorrHist + minorCorrHist; + // Major correction history + var majorHash = position.MajorHash; + var majorIndex = majorHash & Constants.MajorCorrHistoryMask; + + var majorCorrHistIndex = (2 * majorIndex) + side; + Debug.Assert(majorCorrHistIndex < (ulong)_majorCorrHistory.Length); + + var majorCorrHist = _majorCorrHistory[majorCorrHistIndex]; + + // Aggregated correction + var correction = pawnCorrHist + nonPawnSTMCorrHist + nonPawnNoSTMCorrHist + minorCorrHist + majorCorrHist; var correctStaticEval = staticEvaluation + (correction / (Constants.CorrectionHistoryScale * 3)); return Math.Clamp(correctStaticEval, EvaluationConstants.MinStaticEval, EvaluationConstants.MaxStaticEval); diff --git a/src/Lynx/Search/IDDFS.cs b/src/Lynx/Search/IDDFS.cs index a993fa6ef..70f41c2ba 100644 --- a/src/Lynx/Search/IDDFS.cs +++ b/src/Lynx/Search/IDDFS.cs @@ -48,16 +48,22 @@ public sealed partial class Engine private readonly int[] _pawnCorrHistory = GC.AllocateArray(Constants.PawnCorrHistorySize * 2, pinned: true); /// - /// x 2 x 2 - /// Side hash x side to move x piece hash side + /// x 2 x 2 + /// Side non-pawn hash x side to move x piece hash side /// private readonly int[] _nonPawnCorrHistory = GC.AllocateArray(Constants.NonPawnCorrHistorySize * 2 * 2, pinned: true); /// - /// x 2 - /// Pawn hash x side to move + /// x 2 + /// Minor hash x side to move + /// + private readonly int[] _minorCorrHistory = GC.AllocateArray(Constants.MinorCorrHistorySize * 2, pinned: true); + + /// + /// x 2 + /// Major hash x side to move /// - private readonly int[] _minorCorrHistory = GC.AllocateArray(Constants.PawnCorrHistorySize * 2, pinned: true); + private readonly int[] _majorCorrHistory = GC.AllocateArray(Constants.MajorCorrHistorySize * 2, pinned: true); /// /// 12 x 64 diff --git a/src/Lynx/ZobristTable.cs b/src/Lynx/ZobristTable.cs index 36e99dfa0..693a00244 100644 --- a/src/Lynx/ZobristTable.cs +++ b/src/Lynx/ZobristTable.cs @@ -220,6 +220,33 @@ public static ulong MinorHash(Position position) return minorHash; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MajorHash(Position position) + { + ulong majorHash = 0; + + for (int pieceIndex = (int)Piece.R; pieceIndex <= (int)Piece.Q; ++pieceIndex) + { + var whiteBitboard = position.PieceBitBoards[pieceIndex]; + while (whiteBitboard != default) + { + whiteBitboard = whiteBitboard.WithoutLS1B(out var pieceSquareIndex); + + majorHash ^= PieceHash(pieceSquareIndex, pieceIndex); + } + + var blackBitboard = position.PieceBitBoards[pieceIndex + 6]; + while (blackBitboard != default) + { + blackBitboard = blackBitboard.WithoutLS1B(out var pieceSquareIndex); + + majorHash ^= PieceHash(pieceSquareIndex, pieceIndex + 6); + } + } + + return majorHash; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void PieceUniqueIdentifiers(Position position, Span pieceIdentifiers) {