From 979cb5b47f504164ac78361f1f186d4530b3f7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Wed, 19 Nov 2025 20:33:23 +0100 Subject: [PATCH 1/8] Wrap state-related position internals in PositionState --- .../Model => Lynx.Benchmark}/GameState.cs | 23 +- src/Lynx.Benchmark/ParseGame_Benchmark.cs | 6 +- src/Lynx/Evaluation.cs | 48 +-- src/Lynx/Model/Game.cs | 2 +- src/Lynx/Model/Position.cs | 298 +++++++++--------- src/Lynx/Model/PositionState.cs | 60 ++++ 6 files changed, 237 insertions(+), 200 deletions(-) rename src/{Lynx/Model => Lynx.Benchmark}/GameState.cs (70%) create mode 100644 src/Lynx/Model/PositionState.cs diff --git a/src/Lynx/Model/GameState.cs b/src/Lynx.Benchmark/GameState.cs similarity index 70% rename from src/Lynx/Model/GameState.cs rename to src/Lynx.Benchmark/GameState.cs index e7c779e2e..af6baf0d7 100644 --- a/src/Lynx/Model/GameState.cs +++ b/src/Lynx.Benchmark/GameState.cs @@ -1,4 +1,6 @@ -namespace Lynx.Model; +using Lynx.Model; + +namespace Lynx.Benchmark; #pragma warning disable CA1051 // Do not declare visible instance fields @@ -38,24 +40,11 @@ public GameState(Position position) EnPassant = position.EnPassant; Castle = position.Castle; - IncrementalEvalAccumulator = position.IncrementalEvalAccumulator; - IncrementalPhaseAccumulator = position.IncrementalPhaseAccumulator; + IncrementalEvalAccumulator = position._state.IncrementalEvalAccumulator; + IncrementalPhaseAccumulator = position._state.IncrementalPhaseAccumulator; // We also save a copy of _isIncrementalEval, so that current move doesn't affect 'sibling' moves exploration - IsIncrementalEval = position.IsIncrementalEval; - } -} - -public readonly struct NullMoveGameState -{ - public readonly ulong ZobristKey; - - public readonly BoardSquare EnPassant; - - public NullMoveGameState(Position position) - { - ZobristKey = position.UniqueIdentifier; - EnPassant = position.EnPassant; + IsIncrementalEval = position._state.IsIncrementalEval; } } diff --git a/src/Lynx.Benchmark/ParseGame_Benchmark.cs b/src/Lynx.Benchmark/ParseGame_Benchmark.cs index a685791a1..93aaac308 100644 --- a/src/Lynx.Benchmark/ParseGame_Benchmark.cs +++ b/src/Lynx.Benchmark/ParseGame_Benchmark.cs @@ -524,7 +524,7 @@ internal OriginalGame(string fen, string[] movesUCIString) : this(fen) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move moveToPlay) + public PositionState MakeMove(Move moveToPlay) { var gameState = CurrentPosition.MakeMove(moveToPlay); @@ -605,7 +605,7 @@ internal ImprovedGame(string fen, ReadOnlySpan rawMoves, Span range } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move moveToPlay) + public PositionState MakeMove(Move moveToPlay) { var gameState = CurrentPosition.MakeMove(moveToPlay); @@ -690,7 +690,7 @@ public ImprovedGame2(ReadOnlySpan fen, ReadOnlySpan rawMoves, Span MaxPhase) // Early promotions diff --git a/src/Lynx/Model/Game.cs b/src/Lynx/Model/Game.cs index 716e41ebc..467231e63 100644 --- a/src/Lynx/Model/Game.cs +++ b/src/Lynx/Model/Game.cs @@ -193,7 +193,7 @@ public static bool IsThreefoldRepetition(ReadOnlySpan positionHashHistory public static bool Is50MovesRepetition(int halfMovesWithoutCaptureOrPawnMove) => halfMovesWithoutCaptureOrPawnMove >= 100; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move moveToPlay) + public PositionState MakeMove(Move moveToPlay) { var gameState = CurrentPosition.MakeMove(moveToPlay); diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index d9c9a68b1..39d53f8d2 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -12,24 +12,12 @@ public partial class Position : IDisposable { private bool _disposedValue; -#pragma warning disable IDE1006 // Naming Styles - internal int IncrementalEvalAccumulator; - internal int IncrementalPhaseAccumulator; - internal bool IsIncrementalEval; -#pragma warning restore IDE1006 // Naming Styles - - private ulong _uniqueIdentifier; - private ulong _kingPawnUniqueIdentifier; - private readonly ulong[] _nonPawnHash; - private ulong _minorHash; - private ulong _majorHash; + internal PositionState _state; private readonly ulong[] _pieceBitBoards; private readonly ulong[] _occupancyBitBoards; private readonly int[] _board; - private byte _castle; - #pragma warning disable S3887, CA1051 private readonly byte[] _castlingRightsUpdateConstants; public readonly ulong[] KingsideCastlingFreeSquares; @@ -51,27 +39,26 @@ public partial class Position : IDisposable private readonly int[] _initialKingSquares; #endif - private BoardSquare _enPassant; private Side _side; #pragma warning disable RCS1085 // Use auto-implemented property - public ulong UniqueIdentifier => _uniqueIdentifier; - public ulong KingPawnUniqueIdentifier => _kingPawnUniqueIdentifier; - public ulong[] NonPawnHash => _nonPawnHash; - public ulong MinorHash => _minorHash; - public ulong MajorHash => _majorHash; + public ulong UniqueIdentifier => _state.UniqueIdentifier; + public ulong KingPawnUniqueIdentifier => _state.KingPawnUniqueIdentifier; + public ulong[] NonPawnHash => _state.NonPawnHash; + public ulong MinorHash => _state.MinorHash; + public ulong MajorHash => _state.MajorHash; public BitBoard[] PieceBitBoards => _pieceBitBoards; public BitBoard[] OccupancyBitBoards => _occupancyBitBoards; public int[] Board => _board; public Side Side => _side; - public BoardSquare EnPassant => _enPassant; + public BoardSquare EnPassant => _state.EnPassant; /// /// See /// - public byte Castle { get => _castle; private set => _castle = value; } + public byte Castle { get => _state.Castle; private set => _state.Castle = value; } #pragma warning restore RCS1085 // Use auto-implemented property @@ -123,35 +110,36 @@ public Position(string fen) : this(FENParser.ParseFEN(fen)) public Position(ParseFENResult parsedFEN) { + _state = new(); + _pieceBitBoards = parsedFEN.PieceBitBoards; _occupancyBitBoards = parsedFEN.OccupancyBitBoards; _board = parsedFEN.Board; _side = parsedFEN.Side; - _castle = parsedFEN.Castle; - _enPassant = parsedFEN.EnPassant; + _state.Castle = parsedFEN.Castle; + _state.EnPassant = parsedFEN.EnPassant; #pragma warning disable S3366 // "this" should not be exposed from constructors - _nonPawnHash = ArrayPool.Shared.Rent(2); - _nonPawnHash[(int)Side.White] = ZobristTable.NonPawnSideHash(this, (int)Side.White); - _nonPawnHash[(int)Side.Black] = ZobristTable.NonPawnSideHash(this, (int)Side.Black); + _state.NonPawnHash[(int)Side.White] = ZobristTable.NonPawnSideHash(this, (int)Side.White); + _state.NonPawnHash[(int)Side.Black] = ZobristTable.NonPawnSideHash(this, (int)Side.Black); - _minorHash = ZobristTable.MinorHash(this); - _majorHash = ZobristTable.MajorHash(this); - _kingPawnUniqueIdentifier = ZobristTable.KingPawnHash(this); + _state.MinorHash = ZobristTable.MinorHash(this); + _state.MajorHash = ZobristTable.MajorHash(this); + _state.KingPawnUniqueIdentifier = ZobristTable.KingPawnHash(this); - _uniqueIdentifier = ZobristTable.PositionHash(this, _kingPawnUniqueIdentifier, _nonPawnHash[(int)Side.White], _nonPawnHash[(int)Side.Black]); + _state.UniqueIdentifier = ZobristTable.PositionHash(this, _state.KingPawnUniqueIdentifier, _state.NonPawnHash[(int)Side.White], _state.NonPawnHash[(int)Side.Black]); - Debug.Assert(_uniqueIdentifier == ZobristTable.PositionHash(this)); + Debug.Assert(_state.UniqueIdentifier == ZobristTable.PositionHash(this)); #pragma warning restore S3366 // "this" should not be exposed from constructors - IsIncrementalEval = false; + _state.IsIncrementalEval = false; _castlingRightsUpdateConstants = ArrayPool.Shared.Rent(64); Array.Fill(_castlingRightsUpdateConstants, Constants.NoUpdateCastlingRight, 0, 64); // It won't be possible to add castling rights to a position created from a FEN without them - if (_castle == (int)CastlingRights.None) + if (_state.Castle == (int)CastlingRights.None) { KingsideCastlingFreeSquares = []; QueensideCastlingFreeSquares = []; @@ -280,14 +268,15 @@ public Position(ParseFENResult parsedFEN) /// public Position(Position position) { - _uniqueIdentifier = position._uniqueIdentifier; - _kingPawnUniqueIdentifier = position._kingPawnUniqueIdentifier; - _minorHash = position._minorHash; - _majorHash = position._majorHash; + _state = new(position._state); + + _state.UniqueIdentifier = position._state.UniqueIdentifier; + _state.KingPawnUniqueIdentifier = position._state.KingPawnUniqueIdentifier; + _state.MinorHash = position._state.MinorHash; + _state.MajorHash = position._state.MajorHash; - _nonPawnHash = ArrayPool.Shared.Rent(2); - _nonPawnHash[(int)Side.White] = position._nonPawnHash[(int)Side.White]; - _nonPawnHash[(int)Side.Black] = position._nonPawnHash[(int)Side.Black]; + _state.NonPawnHash[(int)Side.White] = position._state.NonPawnHash[(int)Side.White]; + _state.NonPawnHash[(int)Side.Black] = position._state.NonPawnHash[(int)Side.Black]; _pieceBitBoards = ArrayPool.Shared.Rent(12); Array.Copy(position._pieceBitBoards, _pieceBitBoards, 12); @@ -299,12 +288,12 @@ public Position(Position position) Array.Copy(position._board, _board, 64); _side = position._side; - _castle = position._castle; - _enPassant = position._enPassant; + _state.Castle = position._state.Castle; + _state.EnPassant = position._state.EnPassant; - IsIncrementalEval = position.IsIncrementalEval; - IncrementalEvalAccumulator = position.IncrementalEvalAccumulator; - IncrementalPhaseAccumulator = position.IncrementalPhaseAccumulator; + _state.IsIncrementalEval = position._state.IsIncrementalEval; + _state.IncrementalEvalAccumulator = position._state.IncrementalEvalAccumulator; + _state.IncrementalPhaseAccumulator = position._state.IncrementalPhaseAccumulator; _castlingRightsUpdateConstants = ArrayPool.Shared.Rent(64); Array.Copy(position._castlingRightsUpdateConstants, _castlingRightsUpdateConstants, 64); @@ -367,15 +356,15 @@ public Position(Position position) #region Move making [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move move) + public PositionState 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); + Debug.Assert(ZobristTable.PositionHash(this) == _state.UniqueIdentifier); + Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.White) == _state.NonPawnHash[(int)Side.White]); + Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.Black) == _state.NonPawnHash[(int)Side.Black]); + Debug.Assert(ZobristTable.MinorHash(this) == _state.MinorHash); + Debug.Assert(ZobristTable.MajorHash(this) == _state.MajorHash); - var gameState = new GameState(this); + var gameState = new PositionState(_state); var oldSide = (int)_side; var offset = Utils.PieceOffset(oldSide); @@ -406,62 +395,62 @@ public GameState MakeMove(Move move) var targetPieceHash = ZobristTable.PieceHash(targetSquare, newPiece); var fullPieceMovementHash = sourcePieceHash ^ targetPieceHash; - _uniqueIdentifier ^= + _state.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 + ^ ZobristTable.EnPassantHash((int)_state.EnPassant) // We clear the existing enpassant square, if any + ^ ZobristTable.CastleHash(_state.Castle); // We clear the existing castle rights if (piece == (int)Piece.P || piece == (int)Piece.p) { - _kingPawnUniqueIdentifier ^= sourcePieceHash; // We remove pawn from start square + _state.KingPawnUniqueIdentifier ^= sourcePieceHash; // We remove pawn from start square if (promotedPiece == default) { - _kingPawnUniqueIdentifier ^= targetPieceHash; // We add pawn again to end square + _state.KingPawnUniqueIdentifier ^= targetPieceHash; // We add pawn again to end square } else { // In case of promotion, the promoted piece won't be a pawn or a king, so no need to update the KingPawn hash with it, just to remove the pawn (done right above) // We do need to update the NonPawn hash - _nonPawnHash[oldSide] ^= targetPieceHash; // We add piece piece to the end square + _state.NonPawnHash[oldSide] ^= targetPieceHash; // We add piece piece to the end square if (Utils.IsMinorPiece(newPiece)) { - _minorHash ^= targetPieceHash; + _state.MinorHash ^= targetPieceHash; } else if (Utils.IsMajorPiece(newPiece)) { - _majorHash ^= targetPieceHash; + _state.MajorHash ^= targetPieceHash; } } } else { - _nonPawnHash[oldSide] ^= fullPieceMovementHash; + _state.NonPawnHash[oldSide] ^= fullPieceMovementHash; if (piece == (int)Piece.K || piece == (int)Piece.k) { // King (and castling) moves require calculating king buckets twice and recalculating all related parameters, so skipping incremental eval for those cases for now // No need to check for move.IsCastle(), see CastlingMovesAreKingMoves test - IsIncrementalEval = false; + _state.IsIncrementalEval = false; - _kingPawnUniqueIdentifier ^= fullPieceMovementHash; + _state.KingPawnUniqueIdentifier ^= fullPieceMovementHash; } else if (Utils.IsMinorPiece(piece)) { - _minorHash ^= fullPieceMovementHash; + _state.MinorHash ^= fullPieceMovementHash; } else if (Utils.IsMajorPiece(piece)) { - _majorHash ^= fullPieceMovementHash; + _state.MajorHash ^= fullPieceMovementHash; } } - _enPassant = BoardSquare.noSquare; + _state.EnPassant = BoardSquare.noSquare; // _incrementalEvalAccumulator updates - if (IsIncrementalEval) + if (_state.IsIncrementalEval) { var whiteKing = _pieceBitBoards[(int)Piece.K].GetLS1BIndex(); var blackKing = _pieceBitBoards[(int)Piece.k].GetLS1BIndex(); @@ -475,10 +464,10 @@ public GameState MakeMove(Move move) (sameSideBucket, oppositeSideBucket) = (oppositeSideBucket, sameSideBucket); } - IncrementalEvalAccumulator -= PSQT(sameSideBucket, oppositeSideBucket, piece, sourceSquare); - IncrementalEvalAccumulator += PSQT(sameSideBucket, oppositeSideBucket, newPiece, targetSquare); + _state.IncrementalEvalAccumulator -= PSQT(sameSideBucket, oppositeSideBucket, piece, sourceSquare); + _state.IncrementalEvalAccumulator += PSQT(sameSideBucket, oppositeSideBucket, newPiece, targetSquare); - IncrementalPhaseAccumulator += extraPhaseIfIncremental; + _state.IncrementalPhaseAccumulator += extraPhaseIfIncremental; // No need to check for castling if it's incremental eval switch (move.SpecialMoveFlag()) @@ -494,30 +483,30 @@ public GameState MakeMove(Move move) _occupancyBitBoards[oppositeSide].PopBit(capturedSquare); var capturedPieceHash = ZobristTable.PieceHash(capturedSquare, capturedPiece); - _uniqueIdentifier ^= capturedPieceHash; + _state.UniqueIdentifier ^= capturedPieceHash; // Kings can't be captured if (capturedPiece == (int)Piece.P || capturedPiece == (int)Piece.p) { - _kingPawnUniqueIdentifier ^= capturedPieceHash; + _state.KingPawnUniqueIdentifier ^= capturedPieceHash; } else { - _nonPawnHash[oppositeSide] ^= capturedPieceHash; + _state.NonPawnHash[oppositeSide] ^= capturedPieceHash; if (Utils.IsMinorPiece(capturedPiece)) { - _minorHash ^= capturedPieceHash; + _state.MinorHash ^= capturedPieceHash; } else if (Utils.IsMajorPiece(capturedPiece)) { - _majorHash ^= capturedPieceHash; + _state.MajorHash ^= capturedPieceHash; } } - IncrementalEvalAccumulator -= PSQT(oppositeSideBucket, sameSideBucket, capturedPiece, capturedSquare); + _state.IncrementalEvalAccumulator -= PSQT(oppositeSideBucket, sameSideBucket, capturedPiece, capturedSquare); - IncrementalPhaseAccumulator -= GamePhaseByPiece[capturedPiece]; + _state.IncrementalPhaseAccumulator -= GamePhaseByPiece[capturedPiece]; } break; @@ -528,8 +517,8 @@ public GameState MakeMove(Move move) var enPassantSquare = sourceSquare + pawnPush; Utils.Assert(Constants.EnPassantCaptureSquares.Length > enPassantSquare && Constants.EnPassantCaptureSquares[enPassantSquare] != 0, $"Unexpected en passant square : {(BoardSquare)enPassantSquare}"); - _enPassant = (BoardSquare)enPassantSquare; - _uniqueIdentifier ^= ZobristTable.EnPassantHash(enPassantSquare); + _state.EnPassant = (BoardSquare)enPassantSquare; + _state.UniqueIdentifier ^= ZobristTable.EnPassantHash(enPassantSquare); break; } @@ -546,10 +535,10 @@ public GameState MakeMove(Move move) _board[capturedSquare] = (int)Piece.None; var capturedPawnHash = ZobristTable.PieceHash(capturedSquare, capturedPiece); - _uniqueIdentifier ^= capturedPawnHash; - _kingPawnUniqueIdentifier ^= capturedPawnHash; + _state.UniqueIdentifier ^= capturedPawnHash; + _state.KingPawnUniqueIdentifier ^= capturedPawnHash; - IncrementalEvalAccumulator -= PSQT(oppositeSideBucket, sameSideBucket, capturedPiece, capturedSquare); + _state.IncrementalEvalAccumulator -= PSQT(oppositeSideBucket, sameSideBucket, capturedPiece, capturedSquare); //_incrementalPhaseAccumulator -= GamePhaseByPiece[capturedPiece]; break; @@ -573,24 +562,24 @@ public GameState MakeMove(Move move) _occupancyBitBoards[oppositeSide].PopBit(capturedSquare); ulong capturedPieceHash = ZobristTable.PieceHash(capturedSquare, capturedPiece); - _uniqueIdentifier ^= capturedPieceHash; + _state.UniqueIdentifier ^= capturedPieceHash; // Kings can't be captured if (capturedPiece == (int)Piece.P || capturedPiece == (int)Piece.p) { - _kingPawnUniqueIdentifier ^= capturedPieceHash; + _state.KingPawnUniqueIdentifier ^= capturedPieceHash; } else { - _nonPawnHash[oppositeSide] ^= capturedPieceHash; + _state.NonPawnHash[oppositeSide] ^= capturedPieceHash; if (Utils.IsMinorPiece(capturedPiece)) { - _minorHash ^= capturedPieceHash; + _state.MinorHash ^= capturedPieceHash; } else if (Utils.IsMajorPiece(capturedPiece)) { - _majorHash ^= capturedPieceHash; + _state.MajorHash ^= capturedPieceHash; } } } @@ -603,8 +592,8 @@ public GameState MakeMove(Move move) var enPassantSquare = sourceSquare + pawnPush; Utils.Assert(Constants.EnPassantCaptureSquares.Length > enPassantSquare && Constants.EnPassantCaptureSquares[enPassantSquare] != 0, $"Unexpected en passant square : {(BoardSquare)enPassantSquare}"); - _enPassant = (BoardSquare)enPassantSquare; - _uniqueIdentifier ^= ZobristTable.EnPassantHash(enPassantSquare); + _state.EnPassant = (BoardSquare)enPassantSquare; + _state.UniqueIdentifier ^= ZobristTable.EnPassantHash(enPassantSquare); break; } @@ -638,9 +627,9 @@ public GameState MakeMove(Move move) var hashFix = hashToRevert ^ hashToApply; - _uniqueIdentifier ^= hashFix; - _nonPawnHash[oldSide] ^= hashFix; - _kingPawnUniqueIdentifier ^= hashFix; + _state.UniqueIdentifier ^= hashFix; + _state.NonPawnHash[oldSide] ^= hashFix; + _state.KingPawnUniqueIdentifier ^= hashFix; } // In DFRC the square where the rook was could be occupied by the king after castling @@ -658,9 +647,9 @@ public GameState MakeMove(Move move) var hashChange = ZobristTable.PieceHash(rookSourceSquare, rookIndex) ^ ZobristTable.PieceHash(rookTargetSquare, rookIndex); - _uniqueIdentifier ^= hashChange; - _nonPawnHash[oldSide] ^= hashChange; - _majorHash ^= hashChange; + _state.UniqueIdentifier ^= hashChange; + _state.NonPawnHash[oldSide] ^= hashChange; + _state.MajorHash ^= hashChange; break; } @@ -694,9 +683,9 @@ public GameState MakeMove(Move move) var hashFix = hashToRevert ^ hashToApply; - _uniqueIdentifier ^= hashFix; - _nonPawnHash[oldSide] ^= hashFix; - _kingPawnUniqueIdentifier ^= hashFix; + _state.UniqueIdentifier ^= hashFix; + _state.NonPawnHash[oldSide] ^= hashFix; + _state.KingPawnUniqueIdentifier ^= hashFix; } // In DFRC the square where the rook was could be occupied by the king after castling @@ -714,9 +703,9 @@ public GameState MakeMove(Move move) var hashChange = ZobristTable.PieceHash(rookSourceSquare, rookIndex) ^ ZobristTable.PieceHash(rookTargetSquare, rookIndex); - _uniqueIdentifier ^= hashChange; - _nonPawnHash[oldSide] ^= hashChange; - _majorHash ^= hashChange; + _state.UniqueIdentifier ^= hashChange; + _state.NonPawnHash[oldSide] ^= hashChange; + _state.MajorHash ^= hashChange; break; } @@ -733,8 +722,8 @@ public GameState MakeMove(Move move) _board[capturedSquare] = (int)Piece.None; ulong capturedPawnHash = ZobristTable.PieceHash(capturedSquare, capturedPiece); - _uniqueIdentifier ^= capturedPawnHash; - _kingPawnUniqueIdentifier ^= capturedPawnHash; + _state.UniqueIdentifier ^= capturedPawnHash; + _state.KingPawnUniqueIdentifier ^= capturedPawnHash; break; } @@ -745,16 +734,16 @@ public GameState MakeMove(Move move) _occupancyBitBoards[2] = _occupancyBitBoards[1] | _occupancyBitBoards[0]; // Updating castling rights - _castle &= _castlingRightsUpdateConstants[sourceSquare]; - _castle &= _castlingRightsUpdateConstants[targetSquare]; + _state.Castle &= _castlingRightsUpdateConstants[sourceSquare]; + _state.Castle &= _castlingRightsUpdateConstants[targetSquare]; - _uniqueIdentifier ^= ZobristTable.CastleHash(_castle); + _state.UniqueIdentifier ^= ZobristTable.CastleHash(_state.Castle); - 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); + Debug.Assert(ZobristTable.PositionHash(this) == _state.UniqueIdentifier); + Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.White) == _state.NonPawnHash[(int)Side.White]); + Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.Black) == _state.NonPawnHash[(int)Side.Black]); + Debug.Assert(ZobristTable.MinorHash(this) == _state.MinorHash); + Debug.Assert(ZobristTable.MajorHash(this) == _state.MajorHash); Debug.Assert(Math.Min(MaxPhase, PhaseFromScratch()) == Phase()); // KingPawn hash assert won't work due to PassedPawnBonusNoEnemiesAheadBonus @@ -764,7 +753,7 @@ public GameState MakeMove(Move move) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UnmakeMove(Move move, GameState gameState) + public void UnmakeMove(Move move, PositionState gameState) { var oppositeSide = (int)_side; var side = Utils.OppositeSide(oppositeSide); @@ -923,19 +912,19 @@ public void UnmakeMove(Move move, GameState gameState) _occupancyBitBoards[2] = _occupancyBitBoards[1] | _occupancyBitBoards[0]; // Updating saved values - _castle = gameState.Castle; - _enPassant = gameState.EnPassant; + _state.Castle = gameState.Castle; + _state.EnPassant = gameState.EnPassant; - _uniqueIdentifier = gameState.ZobristKey; - _kingPawnUniqueIdentifier = gameState.KingPawnKey; - _minorHash = gameState.MinorKey; - _majorHash = gameState.MajorKey; - _nonPawnHash[(int)Side.White] = gameState.NonPawnWhiteKey; - _nonPawnHash[(int)Side.Black] = gameState.NonPawnBlackKey; + _state.UniqueIdentifier = gameState.UniqueIdentifier; + _state.KingPawnUniqueIdentifier = gameState.KingPawnUniqueIdentifier; + _state.MinorHash = gameState.MinorHash; + _state.MajorHash = gameState.MajorHash; + _state.NonPawnHash[(int)Side.White] = gameState.NonPawnHash[(int)Side.White]; + _state.NonPawnHash[(int)Side.Black] = gameState.NonPawnHash[(int)Side.Black]; - IncrementalEvalAccumulator = gameState.IncrementalEvalAccumulator; - IncrementalPhaseAccumulator = gameState.IncrementalPhaseAccumulator; - IsIncrementalEval = gameState.IsIncrementalEval; + _state.IncrementalEvalAccumulator = gameState.IncrementalEvalAccumulator; + _state.IncrementalPhaseAccumulator = gameState.IncrementalPhaseAccumulator; + _state.IsIncrementalEval = gameState.IsIncrementalEval; Validate(); } @@ -945,12 +934,12 @@ public NullMoveGameState MakeNullMove() { var gameState = new NullMoveGameState(this); - _uniqueIdentifier ^= + _state.UniqueIdentifier ^= ZobristTable.SideHash() - ^ ZobristTable.EnPassantHash((int)_enPassant); + ^ ZobristTable.EnPassantHash((int)_state.EnPassant); _side = (Side)Utils.OppositeSide((int)_side); - _enPassant = BoardSquare.noSquare; + _state.EnPassant = BoardSquare.noSquare; Validate(); @@ -961,8 +950,8 @@ public NullMoveGameState MakeNullMove() public void UnMakeNullMove(NullMoveGameState gameState) { _side = (Side)Utils.OppositeSide((int)_side); - _enPassant = gameState.EnPassant; - _uniqueIdentifier = gameState.ZobristKey; + _state.EnPassant = gameState.EnPassant; + _state.UniqueIdentifier = gameState.ZobristKey; Validate(); } @@ -1244,19 +1233,19 @@ public string FEN(int halfMovesWithoutCaptureOrPawnMove = 0, int fullMoveClock = if (!Configuration.EngineSettings.IsChess960) { - if ((_castle & (int)CastlingRights.WK) != default) + if ((_state.Castle & (int)CastlingRights.WK) != default) { sb.Append('K'); } - if ((_castle & (int)CastlingRights.WQ) != default) + if ((_state.Castle & (int)CastlingRights.WQ) != default) { sb.Append('Q'); } - if ((_castle & (int)CastlingRights.BK) != default) + if ((_state.Castle & (int)CastlingRights.BK) != default) { sb.Append('k'); } - if ((_castle & (int)CastlingRights.BQ) != default) + if ((_state.Castle & (int)CastlingRights.BQ) != default) { sb.Append('q'); } @@ -1264,22 +1253,22 @@ public string FEN(int halfMovesWithoutCaptureOrPawnMove = 0, int fullMoveClock = else { // Shredder-FEN style (always showing columns), no support for X-FEN style yet (showing KQkq when not-ambiguous) - if ((_castle & (int)CastlingRights.WK) != default) + if ((_state.Castle & (int)CastlingRights.WK) != default) { char file = (char)('A' + Constants.File[WhiteShortCastle.TargetSquare()]); sb.Append(file); } - if ((_castle & (int)CastlingRights.WQ) != default) + if ((_state.Castle & (int)CastlingRights.WQ) != default) { char file = (char)('A' + Constants.File[WhiteLongCastle.TargetSquare()]); sb.Append(file); } - if ((_castle & (int)CastlingRights.BK) != default) + if ((_state.Castle & (int)CastlingRights.BK) != default) { char file = (char)('a' + Constants.File[BlackShortCastle.TargetSquare()]); sb.Append(file); } - if ((_castle & (int)CastlingRights.BQ) != default) + if ((_state.Castle & (int)CastlingRights.BQ) != default) { char file = (char)('a' + Constants.File[BlackLongCastle.TargetSquare()]); sb.Append(file); @@ -1293,7 +1282,7 @@ public string FEN(int halfMovesWithoutCaptureOrPawnMove = 0, int fullMoveClock = sb.Append(' '); - sb.Append(_enPassant == BoardSquare.noSquare ? "-" : Constants.Coordinates[(int)_enPassant]); + sb.Append(_state.EnPassant == BoardSquare.noSquare ? "-" : Constants.Coordinates[(int)_state.EnPassant]); sb.Append(' ').Append(halfMovesWithoutCaptureOrPawnMove).Append(' ').Append(fullMoveClock); @@ -1303,7 +1292,7 @@ public string FEN(int halfMovesWithoutCaptureOrPawnMove = 0, int fullMoveClock = #pragma warning disable S106, S2228 // Standard outputs should not be used directly to log anything /// - /// Combines , , and + /// Combines , , and /// into a human-friendly representation /// public void Print(int halfMovesWithoutCaptureOrPawnMove = -1) @@ -1348,33 +1337,33 @@ public void Print(int halfMovesWithoutCaptureOrPawnMove = -1) #pragma warning disable RCS1214 // Unnecessary interpolated string. Console.WriteLine(); Console.WriteLine($" Side:\t{_side}"); - Console.WriteLine($" Enpassant:\t{(_enPassant == BoardSquare.noSquare ? "no" : Constants.Coordinates[(int)_enPassant])}"); + Console.WriteLine($" Enpassant:\t{(_state.EnPassant == BoardSquare.noSquare ? "no" : Constants.Coordinates[(int)_state.EnPassant])}"); if (!Configuration.EngineSettings.IsChess960) { Console.WriteLine($" Castling:\t" + - $"{((_castle & (int)CastlingRights.WK) != default ? 'K' : '-')}" + - $"{((_castle & (int)CastlingRights.WQ) != default ? 'Q' : '-')} | " + - $"{((_castle & (int)CastlingRights.BK) != default ? 'k' : '-')}" + - $"{((_castle & (int)CastlingRights.BQ) != default ? 'q' : '-')}"); + $"{((_state.Castle & (int)CastlingRights.WK) != default ? 'K' : '-')}" + + $"{((_state.Castle & (int)CastlingRights.WQ) != default ? 'Q' : '-')} | " + + $"{((_state.Castle & (int)CastlingRights.BK) != default ? 'k' : '-')}" + + $"{((_state.Castle & (int)CastlingRights.BQ) != default ? 'q' : '-')}"); } else { char whiteKingSide = '-', whiteQueenside = '-', blackKingside = '-', blackQueenside = '-'; - if ((_castle & (int)CastlingRights.WK) != default) + if ((_state.Castle & (int)CastlingRights.WK) != default) { whiteKingSide = (char)('A' + Constants.File[WhiteShortCastle.TargetSquare()]); } - if ((_castle & (int)CastlingRights.WQ) != default) + if ((_state.Castle & (int)CastlingRights.WQ) != default) { whiteQueenside = (char)('A' + Constants.File[WhiteLongCastle.TargetSquare()]); } - if ((_castle & (int)CastlingRights.BK) != default) + if ((_state.Castle & (int)CastlingRights.BK) != default) { blackKingside = (char)('a' + Constants.File[BlackShortCastle.TargetSquare()]); } - if ((_castle & (int)CastlingRights.BQ) != default) + if ((_state.Castle & (int)CastlingRights.BQ) != default) { blackQueenside = (char)('a' + Constants.File[BlackLongCastle.TargetSquare()]); } @@ -1600,22 +1589,22 @@ public void Validate() #endif // En-passant and pawn to be captured position - if (_enPassant != BoardSquare.noSquare) + if (_state.EnPassant != BoardSquare.noSquare) { - Debug.Assert(!_occupancyBitBoards[(int)Side.Both].GetBit((int)_enPassant), failureMessage, $"Non-empty en passant square {_enPassant}"); + Debug.Assert(!_occupancyBitBoards[(int)Side.Both].GetBit((int)_state.EnPassant), failureMessage, $"Non-empty en passant square {_state.EnPassant}"); - var rank = Constants.Rank[(int)_enPassant]; - Debug.Assert(rank == 2 || rank == 5, failureMessage, $"Wrong en-passant rank for {_enPassant}"); + var rank = Constants.Rank[(int)_state.EnPassant]; + Debug.Assert(rank == 2 || rank == 5, failureMessage, $"Wrong en-passant rank for {_state.EnPassant}"); - var pawnToCaptureSquare = Constants.EnPassantCaptureSquares[(int)_enPassant]; + var pawnToCaptureSquare = Constants.EnPassantCaptureSquares[(int)_state.EnPassant]; if (Side == Side.White) { - Debug.Assert(blackPawns.GetBit(pawnToCaptureSquare), failureMessage, $"No black pawn on en-passant capture square for {_enPassant}"); + Debug.Assert(blackPawns.GetBit(pawnToCaptureSquare), failureMessage, $"No black pawn on en-passant capture square for {_state.EnPassant}"); } else { - Debug.Assert(whitePawns.GetBit(pawnToCaptureSquare), failureMessage, $"No white pawn on en-passant capture square for {_enPassant}"); + Debug.Assert(whitePawns.GetBit(pawnToCaptureSquare), failureMessage, $"No white pawn on en-passant capture square for {_state.EnPassant}"); } } @@ -1656,7 +1645,6 @@ protected virtual void Dispose(bool disposing) ArrayPool.Shared.Return(_pieceBitBoards); ArrayPool.Shared.Return(_occupancyBitBoards); - ArrayPool.Shared.Return(_nonPawnHash); ArrayPool.Shared.Return(KingsideCastlingFreeSquares); ArrayPool.Shared.Return(QueensideCastlingFreeSquares); ArrayPool.Shared.Return(KingsideCastlingNonAttackedSquares); diff --git a/src/Lynx/Model/PositionState.cs b/src/Lynx/Model/PositionState.cs new file mode 100644 index 000000000..d69a96405 --- /dev/null +++ b/src/Lynx/Model/PositionState.cs @@ -0,0 +1,60 @@ +namespace Lynx.Model; + +#pragma warning disable CA1051 // Do not declare visible instance fields + +public struct PositionState +{ + public ulong UniqueIdentifier; + public ulong KingPawnUniqueIdentifier; +#pragma warning disable S3887 // Mutable, non-private fields should not be "readonly" + public readonly ulong[] NonPawnHash; +#pragma warning restore S3887 // Mutable, non-private fields should not be "readonly" + public ulong MinorHash; + public ulong MajorHash; + + public int IncrementalEvalAccumulator; + public int IncrementalPhaseAccumulator; + + public BoardSquare EnPassant; + + public byte Castle; + + /// + /// We save it so that current move doesn't affect 'sibling' moves exploration + /// + public bool IsIncrementalEval; + + public PositionState() + { + NonPawnHash = new ulong[2]; + } + + public PositionState(PositionState original) + { + UniqueIdentifier = original.UniqueIdentifier; + KingPawnUniqueIdentifier = original.KingPawnUniqueIdentifier; + NonPawnHash = [original.NonPawnHash[0], original.NonPawnHash[1]]; + MinorHash = original.MinorHash; + MajorHash = original.MajorHash; + IncrementalEvalAccumulator = original.IncrementalEvalAccumulator; + IncrementalPhaseAccumulator = original.IncrementalPhaseAccumulator; + EnPassant = original.EnPassant; + Castle = original.Castle; + IsIncrementalEval = original.IsIncrementalEval; + } +} + +public readonly struct NullMoveGameState +{ + public readonly ulong ZobristKey; + + public readonly BoardSquare EnPassant; + + public NullMoveGameState(Position position) + { + ZobristKey = position.UniqueIdentifier; + EnPassant = position.EnPassant; + } +} + +#pragma warning restore CA1051 // Do not declare visible instance fields From aadf4189cd4fb6f3e56e45478e240f710c9f6b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Wed, 19 Nov 2025 22:32:17 +0100 Subject: [PATCH 2/8] Make the state internal and handle it with an array of states --- src/Lynx/Model/Game.cs | 8 ++- src/Lynx/Model/Move.cs | 4 +- src/Lynx/Model/Position.cs | 77 ++++++++++++++--------------- src/Lynx/Model/PositionState.cs | 60 ----------------------- src/Lynx/Model/State.cs | 86 +++++++++++++++++++++++++++++++++ src/Lynx/MoveGenerator.cs | 18 +++---- src/Lynx/Perft.cs | 8 +-- src/Lynx/Search/IDDFS.cs | 8 +-- src/Lynx/Search/NegaMax.cs | 16 +++--- 9 files changed, 152 insertions(+), 133 deletions(-) delete mode 100644 src/Lynx/Model/PositionState.cs create mode 100644 src/Lynx/Model/State.cs diff --git a/src/Lynx/Model/Game.cs b/src/Lynx/Model/Game.cs index 467231e63..3258d57bf 100644 --- a/src/Lynx/Model/Game.cs +++ b/src/Lynx/Model/Game.cs @@ -193,9 +193,9 @@ public static bool IsThreefoldRepetition(ReadOnlySpan positionHashHistory public static bool Is50MovesRepetition(int halfMovesWithoutCaptureOrPawnMove) => halfMovesWithoutCaptureOrPawnMove >= 100; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PositionState MakeMove(Move moveToPlay) + public void MakeMove(Move moveToPlay) { - var gameState = CurrentPosition.MakeMove(moveToPlay); + CurrentPosition.MakeMove(moveToPlay); if (CurrentPosition.WasProduceByAValidMove()) { @@ -207,11 +207,9 @@ public PositionState MakeMove(Move moveToPlay) } else { - CurrentPosition.UnmakeMove(moveToPlay, gameState); + CurrentPosition.UnmakeMove(moveToPlay); _logger.Warn("Error trying to play move {0} in {1}", moveToPlay.UCIString(), CurrentPosition.FEN(HalfMovesWithoutCaptureOrPawnMove)); } - - return gameState; } /// diff --git a/src/Lynx/Model/Move.cs b/src/Lynx/Model/Move.cs index 10e46bfdf..17ababd62 100644 --- a/src/Lynx/Model/Move.cs +++ b/src/Lynx/Model/Move.cs @@ -378,9 +378,9 @@ private static string DisambiguateMove(Move move, Position position) .Where(m => { // If any illegal moves exist with the same simple representation there's no need to disambiguate - var gameState = position.MakeMove(m); + position.MakeMove(m); var isLegal = position.WasProduceByAValidMove(); - position.UnmakeMove(m, gameState); + position.UnmakeMove(m); return isLegal; }) diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 39d53f8d2..bc4d9156e 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -12,7 +12,10 @@ public partial class Position : IDisposable { private bool _disposedValue; - internal PositionState _state; + private int _stackCounter; + private readonly State[] _stateStack; + + private State _state; private readonly ulong[] _pieceBitBoards; private readonly ulong[] _occupancyBitBoards; @@ -110,7 +113,15 @@ public Position(string fen) : this(FENParser.ParseFEN(fen)) public Position(ParseFENResult parsedFEN) { - _state = new(); + _stackCounter = 0; + + _stateStack = new State[Constants.MaxNumberMovesInAGame + Constants.ArrayDepthMargin]; + for (int i = 0; i < _stateStack.Length; ++i) + { + _stateStack[i] = new(); + } + + _state = _stateStack[_stackCounter]; _pieceBitBoards = parsedFEN.PieceBitBoards; _occupancyBitBoards = parsedFEN.OccupancyBitBoards; @@ -268,15 +279,15 @@ public Position(ParseFENResult parsedFEN) /// public Position(Position position) { - _state = new(position._state); + _stackCounter = position._stackCounter; - _state.UniqueIdentifier = position._state.UniqueIdentifier; - _state.KingPawnUniqueIdentifier = position._state.KingPawnUniqueIdentifier; - _state.MinorHash = position._state.MinorHash; - _state.MajorHash = position._state.MajorHash; + _stateStack = new State[Constants.MaxNumberMovesInAGame + Constants.ArrayDepthMargin]; + for (int i = 0; i < _stateStack.Length; ++i) + { + _stateStack[i] = new(position._stateStack[i]); + } - _state.NonPawnHash[(int)Side.White] = position._state.NonPawnHash[(int)Side.White]; - _state.NonPawnHash[(int)Side.Black] = position._state.NonPawnHash[(int)Side.Black]; + _state = _stateStack[_stackCounter]; _pieceBitBoards = ArrayPool.Shared.Rent(12); Array.Copy(position._pieceBitBoards, _pieceBitBoards, 12); @@ -288,12 +299,6 @@ public Position(Position position) Array.Copy(position._board, _board, 64); _side = position._side; - _state.Castle = position._state.Castle; - _state.EnPassant = position._state.EnPassant; - - _state.IsIncrementalEval = position._state.IsIncrementalEval; - _state.IncrementalEvalAccumulator = position._state.IncrementalEvalAccumulator; - _state.IncrementalPhaseAccumulator = position._state.IncrementalPhaseAccumulator; _castlingRightsUpdateConstants = ArrayPool.Shared.Rent(64); Array.Copy(position._castlingRightsUpdateConstants, _castlingRightsUpdateConstants, 64); @@ -356,7 +361,7 @@ public Position(Position position) #region Move making [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PositionState MakeMove(Move move) + public void MakeMove(Move move) { Debug.Assert(ZobristTable.PositionHash(this) == _state.UniqueIdentifier); Debug.Assert(ZobristTable.NonPawnSideHash(this, (int)Side.White) == _state.NonPawnHash[(int)Side.White]); @@ -364,7 +369,11 @@ public PositionState MakeMove(Move move) Debug.Assert(ZobristTable.MinorHash(this) == _state.MinorHash); Debug.Assert(ZobristTable.MajorHash(this) == _state.MajorHash); - var gameState = new PositionState(_state); + var oldState = _state; + + ++_stackCounter; + _state = _stateStack[_stackCounter]; + _state.SetupFromPrevious(oldState); var oldSide = (int)_side; var offset = Utils.PieceOffset(oldSide); @@ -398,8 +407,8 @@ public PositionState MakeMove(Move move) _state.UniqueIdentifier ^= ZobristTable.SideHash() ^ fullPieceMovementHash - ^ ZobristTable.EnPassantHash((int)_state.EnPassant) // We clear the existing enpassant square, if any - ^ ZobristTable.CastleHash(_state.Castle); // We clear the existing castle rights + ^ ZobristTable.EnPassantHash((int)oldState.EnPassant) // We clear the existing enpassant square, if any + ^ ZobristTable.CastleHash(oldState.Castle); // We clear the existing castle rights if (piece == (int)Piece.P || piece == (int)Piece.p) { @@ -748,12 +757,10 @@ public PositionState MakeMove(Move move) // KingPawn hash assert won't work due to PassedPawnBonusNoEnemiesAheadBonus //Debug.Assert(ZobristTable.PawnKingHash(this) != _kingPawnUniqueIdentifier && WasProduceByAValidMove()); - - return gameState; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UnmakeMove(Move move, PositionState gameState) + public void UnmakeMove(Move move) { var oppositeSide = (int)_side; var side = Utils.OppositeSide(oppositeSide); @@ -911,20 +918,8 @@ public void UnmakeMove(Move move, PositionState gameState) _occupancyBitBoards[2] = _occupancyBitBoards[1] | _occupancyBitBoards[0]; - // Updating saved values - _state.Castle = gameState.Castle; - _state.EnPassant = gameState.EnPassant; - - _state.UniqueIdentifier = gameState.UniqueIdentifier; - _state.KingPawnUniqueIdentifier = gameState.KingPawnUniqueIdentifier; - _state.MinorHash = gameState.MinorHash; - _state.MajorHash = gameState.MajorHash; - _state.NonPawnHash[(int)Side.White] = gameState.NonPawnHash[(int)Side.White]; - _state.NonPawnHash[(int)Side.Black] = gameState.NonPawnHash[(int)Side.Black]; - - _state.IncrementalEvalAccumulator = gameState.IncrementalEvalAccumulator; - _state.IncrementalPhaseAccumulator = gameState.IncrementalPhaseAccumulator; - _state.IsIncrementalEval = gameState.IsIncrementalEval; + --_stackCounter; + _state = _stateStack[_stackCounter]; Validate(); } @@ -1547,12 +1542,12 @@ public void Validate() Debug.Assert(blackKings.CountBits() == 1, failureMessage, $"More than one black king, or none: {blackKings}"); #if DEBUG - if (_castle != 0) + if (_state.Castle != 0) { var whiteKingSourceSquare = _initialKingSquares[(int)Side.White]; // Castling rights and king/rook positions - if ((_castle & (int)CastlingRights.WK) != 0) + if ((_state.Castle & (int)CastlingRights.WK) != 0) { Debug.Assert(whiteKings.GetBit(whiteKingSourceSquare), failureMessage, "No white king on e1 when short castling rights"); @@ -1560,7 +1555,7 @@ public void Validate() Debug.Assert(whiteRooks.GetBit(_initialKingsideRookSquares[(int)Side.White]), failureMessage, $"No white rook on {(BoardSquare)_initialKingsideRookSquares[(int)Side.White]} when short castling rights"); } - if ((_castle & (int)CastlingRights.WQ) != 0) + if ((_state.Castle & (int)CastlingRights.WQ) != 0) { Debug.Assert(whiteKings.GetBit(whiteKingSourceSquare), failureMessage, "No white king on e1 when long castling rights"); @@ -1570,7 +1565,7 @@ public void Validate() var blackKingSourceSquare = _initialKingSquares[(int)Side.Black]; - if ((_castle & (int)CastlingRights.BK) != 0) + if ((_state.Castle & (int)CastlingRights.BK) != 0) { Debug.Assert(blackKings.GetBit(blackKingSourceSquare), failureMessage, "No black king on e8 when short castling rights"); @@ -1578,7 +1573,7 @@ public void Validate() Debug.Assert(blackRooks.GetBit(_initialKingsideRookSquares[(int)Side.Black]), failureMessage, $"No black rook on {(BoardSquare)_initialKingsideRookSquares[(int)Side.Black]} when short castling rights"); } - if ((_castle & (int)CastlingRights.BQ) != 0) + if ((_state.Castle & (int)CastlingRights.BQ) != 0) { Debug.Assert(blackKings.GetBit(blackKingSourceSquare), failureMessage, "No black king on e8 when long castling rights"); diff --git a/src/Lynx/Model/PositionState.cs b/src/Lynx/Model/PositionState.cs deleted file mode 100644 index d69a96405..000000000 --- a/src/Lynx/Model/PositionState.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace Lynx.Model; - -#pragma warning disable CA1051 // Do not declare visible instance fields - -public struct PositionState -{ - public ulong UniqueIdentifier; - public ulong KingPawnUniqueIdentifier; -#pragma warning disable S3887 // Mutable, non-private fields should not be "readonly" - public readonly ulong[] NonPawnHash; -#pragma warning restore S3887 // Mutable, non-private fields should not be "readonly" - public ulong MinorHash; - public ulong MajorHash; - - public int IncrementalEvalAccumulator; - public int IncrementalPhaseAccumulator; - - public BoardSquare EnPassant; - - public byte Castle; - - /// - /// We save it so that current move doesn't affect 'sibling' moves exploration - /// - public bool IsIncrementalEval; - - public PositionState() - { - NonPawnHash = new ulong[2]; - } - - public PositionState(PositionState original) - { - UniqueIdentifier = original.UniqueIdentifier; - KingPawnUniqueIdentifier = original.KingPawnUniqueIdentifier; - NonPawnHash = [original.NonPawnHash[0], original.NonPawnHash[1]]; - MinorHash = original.MinorHash; - MajorHash = original.MajorHash; - IncrementalEvalAccumulator = original.IncrementalEvalAccumulator; - IncrementalPhaseAccumulator = original.IncrementalPhaseAccumulator; - EnPassant = original.EnPassant; - Castle = original.Castle; - IsIncrementalEval = original.IsIncrementalEval; - } -} - -public readonly struct NullMoveGameState -{ - public readonly ulong ZobristKey; - - public readonly BoardSquare EnPassant; - - public NullMoveGameState(Position position) - { - ZobristKey = position.UniqueIdentifier; - EnPassant = position.EnPassant; - } -} - -#pragma warning restore CA1051 // Do not declare visible instance fields diff --git a/src/Lynx/Model/State.cs b/src/Lynx/Model/State.cs new file mode 100644 index 000000000..d02b9849b --- /dev/null +++ b/src/Lynx/Model/State.cs @@ -0,0 +1,86 @@ +using System.Runtime.CompilerServices; + +namespace Lynx.Model; + +#pragma warning disable CA1051 // Do not declare visible instance fields + +partial class Position +{ + private sealed class State + { + public ulong UniqueIdentifier { get; set; } + public ulong KingPawnUniqueIdentifier { get; set; } +#pragma warning disable S3887 // Mutable, non-private fields should not be "readonly" + public ulong[] NonPawnHash { get; set; } +#pragma warning restore S3887 // Mutable, non-private fields should not be "readonly" + public ulong MinorHash { get; set; } + public ulong MajorHash { get; set; } + + public int IncrementalEvalAccumulator { get; set; } + public int IncrementalPhaseAccumulator { get; set; } + + public BoardSquare EnPassant { get; set; } = BoardSquare.noSquare; + + public byte Castle { get; set; } + + /// + /// We save it so that current move doesn't affect 'sibling' moves exploration + /// + public bool IsIncrementalEval; + + public State() + { + NonPawnHash = new ulong[2]; + } + + public State(State original) + { + UniqueIdentifier = original.UniqueIdentifier; + KingPawnUniqueIdentifier = original.KingPawnUniqueIdentifier; + NonPawnHash = [original.NonPawnHash[0], original.NonPawnHash[1]]; + MinorHash = original.MinorHash; + MajorHash = original.MajorHash; + + IncrementalEvalAccumulator = original.IncrementalEvalAccumulator; + IncrementalPhaseAccumulator = original.IncrementalPhaseAccumulator; + + EnPassant = original.EnPassant; + Castle = original.Castle; + + IsIncrementalEval = original.IsIncrementalEval; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetupFromPrevious(State previous) + { + UniqueIdentifier = previous.UniqueIdentifier; + KingPawnUniqueIdentifier = previous.KingPawnUniqueIdentifier; + NonPawnHash[(int)Side.White] = previous.NonPawnHash[(int)Side.White]; + NonPawnHash[(int)Side.Black] = previous.NonPawnHash[(int)Side.Black]; + MinorHash = previous.MinorHash; + MajorHash = previous.MajorHash; + + IncrementalEvalAccumulator = previous.IncrementalEvalAccumulator; + IncrementalPhaseAccumulator = previous.IncrementalPhaseAccumulator; + + Castle = previous.Castle; + + IsIncrementalEval = previous.IsIncrementalEval; + } + } +} + +public readonly struct NullMoveGameState +{ + public readonly ulong ZobristKey; + + public readonly BoardSquare EnPassant; + + public NullMoveGameState(Position position) + { + ZobristKey = position.UniqueIdentifier; + EnPassant = position.EnPassant; + } +} + +#pragma warning restore CA1051 // Do not declare visible instance fields diff --git a/src/Lynx/MoveGenerator.cs b/src/Lynx/MoveGenerator.cs index 27aaecde7..29628dbde 100644 --- a/src/Lynx/MoveGenerator.cs +++ b/src/Lynx/MoveGenerator.cs @@ -467,13 +467,13 @@ public static bool CanGenerateAtLeastAValidMove(Position position, ref Evaluatio try { #endif - return IsAnyPawnMoveValid(position, offset) - || IsAnyKingMoveValid((int)Piece.K + offset, position, ref evaluationContext) // in? - || IsAnyPieceMoveValid((int)Piece.Q + offset, position) - || IsAnyPieceMoveValid((int)Piece.B + offset, position) - || IsAnyPieceMoveValid((int)Piece.N + offset, position) - || IsAnyPieceMoveValid((int)Piece.R + offset, position) - || IsAnyCastlingMoveValid(position, ref evaluationContext); + return IsAnyPawnMoveValid(position, offset) + || IsAnyKingMoveValid((int)Piece.K + offset, position, ref evaluationContext) // in? + || IsAnyPieceMoveValid((int)Piece.Q + offset, position) + || IsAnyPieceMoveValid((int)Piece.B + offset, position) + || IsAnyPieceMoveValid((int)Piece.N + offset, position) + || IsAnyPieceMoveValid((int)Piece.R + offset, position) + || IsAnyCastlingMoveValid(position, ref evaluationContext); #if DEBUG } catch (Exception e) @@ -699,10 +699,10 @@ private static bool IsAnyKingMoveValid(int piece, Position position, ref Evaluat [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsValidMove(Position position, Move move) { - var gameState = position.MakeMove(move); + position.MakeMove(move); bool result = position.WasProduceByAValidMove(); - position.UnmakeMove(move, gameState); + position.UnmakeMove(move); return result; } diff --git a/src/Lynx/Perft.cs b/src/Lynx/Perft.cs index 81d5c0dda..82cf0f875 100644 --- a/src/Lynx/Perft.cs +++ b/src/Lynx/Perft.cs @@ -46,13 +46,13 @@ internal static long PerftRecursiveImpl(Position position, int depth, long nodes foreach (var move in MoveGenerator.GenerateAllMoves(position, ref evaluationContext, moves)) { - var state = position.MakeMove(move); + position.MakeMove(move); if (position.WasProduceByAValidMove()) { nodes = PerftRecursiveImpl(position, depth - 1, nodes); } - position.UnmakeMove(move, state); + position.UnmakeMove(move); } return nodes; @@ -74,7 +74,7 @@ private static long DivideImpl(Position position, int depth, long nodes, Action< foreach (var move in MoveGenerator.GenerateAllMoves(position, ref evaluationContext, moves)) { - var state = position.MakeMove(move); + position.MakeMove(move); if (position.WasProduceByAValidMove()) { @@ -84,7 +84,7 @@ private static long DivideImpl(Position position, int depth, long nodes, Action< write($"{move.UCIString()}\t\t{nodes - accumulatedNodes}"); } - position.UnmakeMove(move, state); + position.UnmakeMove(move); } write(string.Empty); diff --git a/src/Lynx/Search/IDDFS.cs b/src/Lynx/Search/IDDFS.cs index f43d167b9..f9edcd6d0 100644 --- a/src/Lynx/Search/IDDFS.cs +++ b/src/Lynx/Search/IDDFS.cs @@ -437,9 +437,9 @@ private bool OnlyOneLegalMove(ref Move firstLegalMove, [NotNullWhen(true)] out S foreach (var move in MoveGenerator.GenerateAllMoves(Game.CurrentPosition, ref evaluationContext, moves)) { - var gameState = Game.CurrentPosition.MakeMove(move); + Game.CurrentPosition.MakeMove(move); bool isPositionValid = Game.CurrentPosition.WasProduceByAValidMove(); - Game.CurrentPosition.UnmakeMove(move, gameState); + Game.CurrentPosition.UnmakeMove(move); if (isPositionValid) { @@ -606,10 +606,10 @@ private SearchResult BestMoveRoot(Move firstLegalMove) var move = pseudoLegalMoves[i]; - var gameState = position.MakeMove(move); + position.MakeMove(move); if (!position.WasProduceByAValidMove()) { - position.UnmakeMove(move, gameState); + position.UnmakeMove(move); continue; } diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index 5b4f31128..05bdab20f 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -434,11 +434,11 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool cutnode, Cance } } - var gameState = position.MakeMove(move); + position.MakeMove(move); if (!position.WasProduceByAValidMove()) { - position.UnmakeMove(move, gameState); + position.UnmakeMove(move); continue; } @@ -457,7 +457,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool cutnode, Cance && ttEntry.NodeType != NodeType.Alpha && ply < 3 * depth) // Preventing search explosions { - position.UnmakeMove(move, gameState); + position.UnmakeMove(move); var verificationDepth = (depth - 1) / 2; // TODO tune? var singularBeta = ttEntry.Score - (depth * Configuration.EngineSettings.SE_DepthMultiplier); @@ -496,7 +496,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool cutnode, Cance --singularDepthExtensions; } - gameState = position.MakeMove(move); + position.MakeMove(move); } var previousNodes = _nodes; @@ -518,7 +518,7 @@ void RevertMove() { Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; Game.RemoveFromPositionHashHistory(); - position.UnmakeMove(move, gameState); + position.UnmakeMove(move); } int score = 0; @@ -933,10 +933,10 @@ public int QuiescenceSearch(int ply, int alpha, int beta, bool pvNode, Cancellat continue; } - var gameState = position.MakeMove(move); + position.MakeMove(move); if (!position.WasProduceByAValidMove()) { - position.UnmakeMove(move, gameState); + position.UnmakeMove(move); continue; } @@ -952,7 +952,7 @@ public int QuiescenceSearch(int ply, int alpha, int beta, bool pvNode, Cancellat #pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters int score = -QuiescenceSearch(ply + 1, -beta, -alpha, pvNode, cancellationToken); #pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters - position.UnmakeMove(move, gameState); + position.UnmakeMove(move); PrintMove(position, ply, move, score, isQuiescence: true); From c42a87d5cf467aa93bef543fc71331bd7975558c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Wed, 19 Nov 2025 22:42:52 +0100 Subject: [PATCH 3/8] Fix dev projects --- src/Lynx.Benchmark/GameState.cs | 51 ------------------- .../MoveGenerator_SpanUnsafeAdd_Benchmark.cs | 8 +-- src/Lynx.Benchmark/ParseGame_Benchmark.cs | 24 ++++----- .../TryParseFromUCIString_Benchmark.cs | 13 +++-- src/Lynx.Dev/Program.cs | 12 ++--- .../Lynx.Test/BestMove/SingleLegalMoveTest.cs | 4 +- tests/Lynx.Test/Model/MoveToEPDStringTest.cs | 5 +- 7 files changed, 30 insertions(+), 87 deletions(-) delete mode 100644 src/Lynx.Benchmark/GameState.cs diff --git a/src/Lynx.Benchmark/GameState.cs b/src/Lynx.Benchmark/GameState.cs deleted file mode 100644 index af6baf0d7..000000000 --- a/src/Lynx.Benchmark/GameState.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Lynx.Model; - -namespace Lynx.Benchmark; - -#pragma warning disable CA1051 // Do not declare visible instance fields - -public readonly struct GameState -{ - public readonly ulong ZobristKey; - - public readonly ulong KingPawnKey; - - public readonly ulong NonPawnWhiteKey; - - public readonly ulong NonPawnBlackKey; - - public readonly ulong MinorKey; - - public readonly ulong MajorKey; - - public readonly int IncrementalEvalAccumulator; - - public readonly int IncrementalPhaseAccumulator; - - public readonly BoardSquare EnPassant; - - public readonly byte Castle; - - public readonly bool IsIncrementalEval; - - public GameState(Position position) - { - ZobristKey = position.UniqueIdentifier; - - KingPawnKey = position.KingPawnUniqueIdentifier; - NonPawnWhiteKey = position.NonPawnHash[(int)Side.White]; - NonPawnBlackKey = position.NonPawnHash[(int)Side.Black]; - MinorKey = position.MinorHash; - MajorKey = position.MajorHash; - - EnPassant = position.EnPassant; - Castle = position.Castle; - IncrementalEvalAccumulator = position._state.IncrementalEvalAccumulator; - IncrementalPhaseAccumulator = position._state.IncrementalPhaseAccumulator; - - // We also save a copy of _isIncrementalEval, so that current move doesn't affect 'sibling' moves exploration - IsIncrementalEval = position._state.IsIncrementalEval; - } -} - -#pragma warning restore CA1051 // Do not declare visible instance fields diff --git a/src/Lynx.Benchmark/MoveGenerator_SpanUnsafeAdd_Benchmark.cs b/src/Lynx.Benchmark/MoveGenerator_SpanUnsafeAdd_Benchmark.cs index 0406c8a9f..494e04bf4 100644 --- a/src/Lynx.Benchmark/MoveGenerator_SpanUnsafeAdd_Benchmark.cs +++ b/src/Lynx.Benchmark/MoveGenerator_SpanUnsafeAdd_Benchmark.cs @@ -822,10 +822,10 @@ private static bool IsAnyKingMoveValid(int piece, Position position, ref Evaluat [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsValidMove(Position position, Move move) { - var gameState = position.MakeMove(move); + position.MakeMove(move); bool result = position.WasProduceByAValidMove(); - position.UnmakeMove(move, gameState); + position.UnmakeMove(move); return result; } @@ -1499,10 +1499,10 @@ private static bool IsAnyKingMoveValid(int piece, Position position, ref Evaluat [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsValidMove(Position position, Move move) { - var gameState = position.MakeMove(move); + position.MakeMove(move); bool result = position.WasProduceByAValidMove(); - position.UnmakeMove(move, gameState); + position.UnmakeMove(move); return result; } diff --git a/src/Lynx.Benchmark/ParseGame_Benchmark.cs b/src/Lynx.Benchmark/ParseGame_Benchmark.cs index 93aaac308..1d2ea2f11 100644 --- a/src/Lynx.Benchmark/ParseGame_Benchmark.cs +++ b/src/Lynx.Benchmark/ParseGame_Benchmark.cs @@ -524,9 +524,9 @@ internal OriginalGame(string fen, string[] movesUCIString) : this(fen) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PositionState MakeMove(Move moveToPlay) + public void MakeMove(Move moveToPlay) { - var gameState = CurrentPosition.MakeMove(moveToPlay); + CurrentPosition.MakeMove(moveToPlay); if (CurrentPosition.WasProduceByAValidMove()) { @@ -537,13 +537,11 @@ public PositionState MakeMove(Move moveToPlay) else { _logger.Warn("Error trying to play {0}", moveToPlay.UCIString()); - CurrentPosition.UnmakeMove(moveToPlay, gameState); + CurrentPosition.UnmakeMove(moveToPlay); } PositionHashHistory.Add(CurrentPosition.UniqueIdentifier); HalfMovesWithoutCaptureOrPawnMove = Utils.Update50movesRule(moveToPlay, HalfMovesWithoutCaptureOrPawnMove); - - return gameState; } } @@ -605,9 +603,9 @@ internal ImprovedGame(string fen, ReadOnlySpan rawMoves, Span range } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PositionState MakeMove(Move moveToPlay) + public void MakeMove(Move moveToPlay) { - var gameState = CurrentPosition.MakeMove(moveToPlay); + CurrentPosition.MakeMove(moveToPlay); if (CurrentPosition.WasProduceByAValidMove()) { @@ -618,13 +616,11 @@ public PositionState MakeMove(Move moveToPlay) else { _logger.Warn("Error trying to play {0}", moveToPlay.UCIString()); - CurrentPosition.UnmakeMove(moveToPlay, gameState); + CurrentPosition.UnmakeMove(moveToPlay); } PositionHashHistory.Add(CurrentPosition.UniqueIdentifier); HalfMovesWithoutCaptureOrPawnMove = Utils.Update50movesRule(moveToPlay, HalfMovesWithoutCaptureOrPawnMove); - - return gameState; } } @@ -690,9 +686,9 @@ public ImprovedGame2(ReadOnlySpan fen, ReadOnlySpan rawMoves, Span catch (Exception) { #pragma warning disable S112 // General or reserved exceptions should never be thrown - throw new($"Error parsing position command '{positionCommandSpan.ToString()}'"); + throw new($"Error parsing position command '{positionCommandSpan}'"); #pragma warning restore S112 // General or reserved exceptions should never be thrown } } @@ -191,7 +191,7 @@ public TryParseFromUCIString_Benchmark_Game(ReadOnlySpan fen) CurrentPosition = new Position(parsedFen); if (!CurrentPosition.IsValid()) { - _logger.Warn($"Invalid position detected: {fen.ToString()}"); + _logger.Warn($"Invalid position detected: {fen}"); } PositionHashHistory = new(1024) { CurrentPosition.UniqueIdentifier }; @@ -229,10 +229,11 @@ public TryParseFromUCIString_Benchmark_Game(ReadOnlySpan fen, ReadOnlySpan _gameInitialPosition = new Position(CurrentPosition); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move moveToPlay) + public void MakeMove(Move moveToPlay) { - var gameState = CurrentPosition.MakeMove(moveToPlay); + CurrentPosition.MakeMove(moveToPlay); if (CurrentPosition.WasProduceByAValidMove()) { @@ -243,13 +244,11 @@ public GameState MakeMove(Move moveToPlay) else { _logger.Warn("Error trying to play {0}", moveToPlay.UCIString()); - CurrentPosition.UnmakeMove(moveToPlay, gameState); + CurrentPosition.UnmakeMove(moveToPlay); } PositionHashHistory.Add(CurrentPosition.UniqueIdentifier); HalfMovesWithoutCaptureOrPawnMove = Utils.Update50movesRule(moveToPlay, HalfMovesWithoutCaptureOrPawnMove); - - return gameState; } } } diff --git a/src/Lynx.Dev/Program.cs b/src/Lynx.Dev/Program.cs index 89cc35bfc..2b4b5604d 100644 --- a/src/Lynx.Dev/Program.cs +++ b/src/Lynx.Dev/Program.cs @@ -529,7 +529,7 @@ static void GeneralMoveTest(Game game) game.CurrentPosition.Print(); Console.WriteLine(move.ToEPDString(game.CurrentPosition)); - var gameState = game.MakeMove(move); + game.MakeMove(move); game.CurrentPosition.Print(); Console.WriteLine("White occupancy:"); @@ -538,7 +538,7 @@ static void GeneralMoveTest(Game game) Console.WriteLine("Black occupancy:"); game.CurrentPosition.OccupancyBitBoards[(int)Side.Black].Print(); - game.CurrentPosition.UnmakeMove(move, gameState); + game.CurrentPosition.UnmakeMove(move); } } @@ -552,9 +552,9 @@ static void CastlingRightsTest(Game game) game.CurrentPosition.Print(); Console.WriteLine(move.ToEPDString(game.CurrentPosition)); - var gameState = game.MakeMove(move); + game.MakeMove(move); game.CurrentPosition.Print(); - game.CurrentPosition.UnmakeMove(move, gameState); + game.CurrentPosition.UnmakeMove(move); } } } @@ -1118,14 +1118,14 @@ static void TestMoveGen(string fen) var newPosition = new Position(position); newPosition.MakeMove(move); - var savedState = position.MakeMove(move); + position.MakeMove(move); Console.WriteLine($"Position\t{newPosition.FEN()}, Zobrist key {newPosition.UniqueIdentifier}"); Console.WriteLine($"Position\t{position.FEN()}, Zobrist key {position.UniqueIdentifier}"); Console.WriteLine($"Unmaking {epdMoveString} in\t{position.FEN()}"); - //position.UnmakeMove(move, savedState); + //position.UnmakeMove(move); Console.WriteLine($"Position\t{position.FEN()}, Zobrist key {position.UniqueIdentifier}"); diff --git a/tests/Lynx.Test/BestMove/SingleLegalMoveTest.cs b/tests/Lynx.Test/BestMove/SingleLegalMoveTest.cs index 69b65a4c7..a45e73e20 100644 --- a/tests/Lynx.Test/BestMove/SingleLegalMoveTest.cs +++ b/tests/Lynx.Test/BestMove/SingleLegalMoveTest.cs @@ -35,14 +35,14 @@ public void SingleMove(string fen) var pos = new Position(fen); foreach (var move in MoveGenerator.GenerateAllMoves(pos)) { - var state = pos.MakeMove(move); + pos.MakeMove(move); if (pos.IsValid()) { Assert.IsNull(singleMove); singleMove = move; } - pos.UnmakeMove(move, state); + pos.UnmakeMove(move); } Assert.LessOrEqual(depth, Configuration.EngineSettings.MaxDepth); diff --git a/tests/Lynx.Test/Model/MoveToEPDStringTest.cs b/tests/Lynx.Test/Model/MoveToEPDStringTest.cs index ed700ffab..9e1fb319f 100644 --- a/tests/Lynx.Test/Model/MoveToEPDStringTest.cs +++ b/tests/Lynx.Test/Model/MoveToEPDStringTest.cs @@ -2,6 +2,7 @@ using NUnit.Framework; namespace Lynx.Test.Model; + public class MoveToEPDStringTest { [TestCase("d5", (int)BoardSquare.d4, (int)BoardSquare.d5, (int)Piece.P, default, 0)] @@ -104,9 +105,9 @@ public void ToStrictEPDString(string fen, Piece piece, BoardSquare targetSquare, .Where(m => m.Piece() == (int)piece && m.TargetSquare() == (int)targetSquare) .Where(m => { - var gameState = position.MakeMove(m); + position.MakeMove(m); var isLegal = position.WasProduceByAValidMove(); - position.UnmakeMove(m, gameState); + position.UnmakeMove(m); return isLegal; }) From a429494e0b8db3895797f0216a297824d71fab1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 24 Nov 2025 11:46:45 +0100 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9A=A1=20Inline=20State=20(#2261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Lynx/Model/{State.cs => PositionState.cs} | 92 +++++++++++++++++-- 1 file changed, 82 insertions(+), 10 deletions(-) rename src/Lynx/Model/{State.cs => PositionState.cs} (52%) diff --git a/src/Lynx/Model/State.cs b/src/Lynx/Model/PositionState.cs similarity index 52% rename from src/Lynx/Model/State.cs rename to src/Lynx/Model/PositionState.cs index d02b9849b..b705f989c 100644 --- a/src/Lynx/Model/State.cs +++ b/src/Lynx/Model/PositionState.cs @@ -8,25 +8,97 @@ partial class Position { private sealed class State { - public ulong UniqueIdentifier { get; set; } - public ulong KingPawnUniqueIdentifier { get; set; } + public ulong UniqueIdentifier + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } + + public ulong KingPawnUniqueIdentifier + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } + #pragma warning disable S3887 // Mutable, non-private fields should not be "readonly" - public ulong[] NonPawnHash { get; set; } + public ulong[] NonPawnHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } #pragma warning restore S3887 // Mutable, non-private fields should not be "readonly" - public ulong MinorHash { get; set; } - public ulong MajorHash { get; set; } - public int IncrementalEvalAccumulator { get; set; } - public int IncrementalPhaseAccumulator { get; set; } + public ulong MinorHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } + + public ulong MajorHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; - public BoardSquare EnPassant { get; set; } = BoardSquare.noSquare; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } - public byte Castle { get; set; } + public int IncrementalEvalAccumulator + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } + + public int IncrementalPhaseAccumulator + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } + + public BoardSquare EnPassant + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } = BoardSquare.noSquare; + + public byte Castle + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } /// /// We save it so that current move doesn't affect 'sibling' moves exploration /// - public bool IsIncrementalEval; + public bool IsIncrementalEval + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } public State() { From 1548837223aead1aa605ff1ac52038af5e68ac5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 24 Nov 2025 12:26:08 +0100 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9A=A1=20Avoid=20unnecessary=20big=20arr?= =?UTF-8?q?ay=20allocations=20in=20Gamestate=20(#2262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Lynx/Engine.cs | 3 ++- src/Lynx/Model/Game.cs | 6 +++--- src/Lynx/Model/Position.cs | 23 +++++++++++++---------- src/Lynx/OnlineTablebaseProber.cs | 8 ++++---- src/Lynx/Search/IDDFS.cs | 2 +- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index 5386e5d44..b17b02a68 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -133,6 +133,7 @@ public SearchResult BestMove(in SearchConstraints searchConstrains, bool isPonde SearchResult resultToReturn = IDDFS(isPondering, jointCts.Token); //SearchResult resultToReturn = await SearchBestMove(maxDepth, decisionTime); + // This is done to allow sending consecutive search commands Game.ResetCurrentPositionToBeforeSearchState(); if (!isPondering && resultToReturn.BestMove != default @@ -240,7 +241,7 @@ public void Dispose() // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); - #pragma warning disable S3234, IDISP024 // "GC.SuppressFinalize" should not be invoked for types without destructors - https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose +#pragma warning disable S3234, IDISP024 // "GC.SuppressFinalize" should not be invoked for types without destructors - https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose GC.SuppressFinalize(this); #pragma warning restore S3234, IDISP024 // "GC.SuppressFinalize" should not be invoked for types without destructors } diff --git a/src/Lynx/Model/Game.cs b/src/Lynx/Model/Game.cs index 3258d57bf..eb30646a2 100644 --- a/src/Lynx/Model/Game.cs +++ b/src/Lynx/Model/Game.cs @@ -80,7 +80,7 @@ public Game(ReadOnlySpan fen, ReadOnlySpan rawMoves, Span ran MakeMove(parsedMove.Value); } - PositionBeforeLastSearch = new Position(CurrentPosition); + PositionBeforeLastSearch = new Position(CurrentPosition, 1); //_positionHashHistoryPointerBeforeLastSearch = _positionHashHistoryPointer; } @@ -215,7 +215,7 @@ public void MakeMove(Move moveToPlay) /// /// Cleans value, since in case of search cancellation /// (either by the engine time management logic or by external stop command) - /// currentPosition won't be the initial one + /// currentPosition won't be the initial one. /// public void ResetCurrentPositionToBeforeSearchState() { @@ -227,7 +227,7 @@ public void ResetCurrentPositionToBeforeSearchState() public void UpdateInitialPosition() { PositionBeforeLastSearch.Dispose(); - PositionBeforeLastSearch = new(CurrentPosition); + PositionBeforeLastSearch = new(CurrentPosition, 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index bc4d9156e..ece2214f9 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -113,15 +113,14 @@ public Position(string fen) : this(FENParser.ParseFEN(fen)) public Position(ParseFENResult parsedFEN) { - _stackCounter = 0; - - _stateStack = new State[Constants.MaxNumberMovesInAGame + Constants.ArrayDepthMargin]; + _stateStack = new State[Constants.MaxNumberMovesInAGame]; for (int i = 0; i < _stateStack.Length; ++i) { _stateStack[i] = new(); } - _state = _stateStack[_stackCounter]; + _state = _stateStack[0]; + _stackCounter = 0; _pieceBitBoards = parsedFEN.PieceBitBoards; _occupancyBitBoards = parsedFEN.OccupancyBitBoards; @@ -274,20 +273,24 @@ public Position(ParseFENResult parsedFEN) Validate(); } + public Position(Position position) + : this(position, Configuration.EngineSettings.MaxDepth + Constants.ArrayDepthMargin) + { } + /// /// Clone constructor /// - public Position(Position position) + public Position(Position position, int stateStackLength) { - _stackCounter = position._stackCounter; + Debug.Assert(position._state != null); - _stateStack = new State[Constants.MaxNumberMovesInAGame + Constants.ArrayDepthMargin]; + _stateStack = new State[stateStackLength]; for (int i = 0; i < _stateStack.Length; ++i) { - _stateStack[i] = new(position._stateStack[i]); + _stateStack[i] = new(); } - - _state = _stateStack[_stackCounter]; + _state = _stateStack[0] = new(position._state); + _stackCounter = 0; _pieceBitBoards = ArrayPool.Shared.Rent(12); Array.Copy(position._pieceBitBoards, _pieceBitBoards, 12); diff --git a/src/Lynx/OnlineTablebaseProber.cs b/src/Lynx/OnlineTablebaseProber.cs index 40fc42271..8eaa70c59 100644 --- a/src/Lynx/OnlineTablebaseProber.cs +++ b/src/Lynx/OnlineTablebaseProber.cs @@ -130,7 +130,7 @@ public static class OnlineTablebaseProber throw new LynxException($"{move!.Uci} should be parsable from position {fen}"); } - using var newPosition = new Position(position); + using var newPosition = new Position(position, 2); newPosition.MakeMove(moveCandidate.Value); var oldValue = halfMovesWithoutCaptureOrPawnMove; @@ -191,7 +191,7 @@ public static class OnlineTablebaseProber throw new LynxException($"{move!.Uci} should be parsable from position {fen}"); } - using var newPosition = new Position(position); + using var newPosition = new Position(position, 2); newPosition.MakeMove(moveCandidate.Value); var oldValue = halfMovesWithoutCaptureOrPawnMove; @@ -254,7 +254,7 @@ public static class OnlineTablebaseProber throw new LynxException($"{move!.Uci} should be parsable from position {fen}"); } - using var newPosition = new Position(position); + using var newPosition = new Position(position, 2); newPosition.MakeMove(moveCandidate.Value); var oldValue = halfMovesWithoutCaptureOrPawnMove; @@ -314,7 +314,7 @@ public static class OnlineTablebaseProber throw new LynxException($"{move!.Uci} should be parsable from position {fen}"); } - using var newPosition = new Position(position); + using var newPosition = new Position(position, 2); newPosition.MakeMove(moveCandidate.Value); var oldValue = halfMovesWithoutCaptureOrPawnMove; diff --git a/src/Lynx/Search/IDDFS.cs b/src/Lynx/Search/IDDFS.cs index f9edcd6d0..1cdedc854 100644 --- a/src/Lynx/Search/IDDFS.cs +++ b/src/Lynx/Search/IDDFS.cs @@ -566,7 +566,7 @@ private SearchResult BestMoveRoot(Move firstLegalMove) var score = 0; ShortMove ttBestMove = default; - using var position = new Position(Game.PositionBeforeLastSearch); + using var position = new Position(Game.PositionBeforeLastSearch, 2); var ttHit = _tt.ProbeHash(position, Game.HalfMovesWithoutCaptureOrPawnMove, ply: 0, out var ttEntry); if (ttHit) From 782046f0456c5ce13b57e62bc097e0475c25a491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Thu, 27 Nov 2025 13:50:47 +0100 Subject: [PATCH 6/8] Fix and integrate merge --- src/Lynx/Model/Position.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 1179ab293..741897c20 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -104,6 +104,11 @@ public int InitialKingSquare(int side) => : BlackShortCastle.SourceSquare(); private Position() + : this(Constants.MaxNumberMovesInAGame) + { + } + + private Position(int stateStackLength) { _pieceBitBoards = ArrayPool.Shared.Rent(12); _occupancyBitBoards = ArrayPool.Shared.Rent(3); @@ -115,7 +120,7 @@ private Position() QueensideCastlingFreeSquares = ArrayPool.Shared.Rent(2); QueensideCastlingNonAttackedSquares = ArrayPool.Shared.Rent(2); - _stateStack = new State[Constants.MaxNumberMovesInAGame]; + _stateStack = new State[stateStackLength]; for (int i = 0; i < _stateStack.Length; ++i) { _stateStack[i] = new(); @@ -150,6 +155,15 @@ public Position(Position position) ResetTo(position); } + /// + /// Clone constructor + /// + public Position(Position position, int stateStackLength) + : this(stateStackLength) + { + ResetTo(position); + } + public void PopulateFrom(ParseFENResult parsedFEN) { _pieceBitBoards = parsedFEN.PieceBitBoards; From f4da203db05a164e3b4280123e34428c6ed30eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Thu, 27 Nov 2025 13:59:12 +0100 Subject: [PATCH 7/8] Simplify PositionState --- src/Lynx/Model/Game.cs | 1 - src/Lynx/Model/Position.cs | 6 +++--- src/Lynx/Model/PositionState.cs | 18 +----------------- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/Lynx/Model/Game.cs b/src/Lynx/Model/Game.cs index 00842ff3d..35d056813 100644 --- a/src/Lynx/Model/Game.cs +++ b/src/Lynx/Model/Game.cs @@ -67,7 +67,6 @@ public void ParsePositionCommand(ReadOnlySpan positionCommandSpan) { try { - // We divide the position command in these two sections: // "position startpos ||" // "position startpos || moves e2e4 e7e5" diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 741897c20..48c25e479 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -299,7 +299,8 @@ public void ResetTo(Position position) { _stateStack[i] = new(); } - _state = _stateStack[0] = new(position._state); + _stateStack[0].SetupFromPrevious(position._state); + _state = _stateStack[0]; _stackCounter = 0; Array.Copy(position._pieceBitBoards, _pieceBitBoards, 12); @@ -357,6 +358,7 @@ public void MakeMove(Move move) ++_stackCounter; _state = _stateStack[_stackCounter]; _state.SetupFromPrevious(oldState); + _state.EnPassant = BoardSquare.noSquare; var oldSide = (int)_side; var offset = Utils.PieceOffset(oldSide); @@ -439,8 +441,6 @@ public void MakeMove(Move move) } } - _state.EnPassant = BoardSquare.noSquare; - // _incrementalEvalAccumulator updates if (_state.IsIncrementalEval) { diff --git a/src/Lynx/Model/PositionState.cs b/src/Lynx/Model/PositionState.cs index b705f989c..f7e9549d9 100644 --- a/src/Lynx/Model/PositionState.cs +++ b/src/Lynx/Model/PositionState.cs @@ -105,23 +105,6 @@ public State() NonPawnHash = new ulong[2]; } - public State(State original) - { - UniqueIdentifier = original.UniqueIdentifier; - KingPawnUniqueIdentifier = original.KingPawnUniqueIdentifier; - NonPawnHash = [original.NonPawnHash[0], original.NonPawnHash[1]]; - MinorHash = original.MinorHash; - MajorHash = original.MajorHash; - - IncrementalEvalAccumulator = original.IncrementalEvalAccumulator; - IncrementalPhaseAccumulator = original.IncrementalPhaseAccumulator; - - EnPassant = original.EnPassant; - Castle = original.Castle; - - IsIncrementalEval = original.IsIncrementalEval; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetupFromPrevious(State previous) { @@ -135,6 +118,7 @@ public void SetupFromPrevious(State previous) IncrementalEvalAccumulator = previous.IncrementalEvalAccumulator; IncrementalPhaseAccumulator = previous.IncrementalPhaseAccumulator; + EnPassant = previous.EnPassant; Castle = previous.Castle; IsIncrementalEval = previous.IsIncrementalEval; From d63911a5e9a1158be3b526c04903dfe3aebc72a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Thu, 27 Nov 2025 14:44:56 +0100 Subject: [PATCH 8/8] Implement reset method --- src/Lynx/Model/Position.cs | 2 +- src/Lynx/Model/PositionState.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 48c25e479..37f4dca4b 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -297,7 +297,7 @@ public void ResetTo(Position position) // TODO see if we can avoid for (int i = 0; i < _stateStack.Length; ++i) { - _stateStack[i] = new(); + _stateStack[i].Reset(); } _stateStack[0].SetupFromPrevious(position._state); _state = _stateStack[0]; diff --git a/src/Lynx/Model/PositionState.cs b/src/Lynx/Model/PositionState.cs index f7e9549d9..e9e1fcafe 100644 --- a/src/Lynx/Model/PositionState.cs +++ b/src/Lynx/Model/PositionState.cs @@ -105,6 +105,25 @@ public State() NonPawnHash = new ulong[2]; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Reset() + { + UniqueIdentifier = default; + KingPawnUniqueIdentifier = default; + NonPawnHash[(int)Side.White] = default; + NonPawnHash[(int)Side.Black] = default; + MinorHash = default; + MajorHash = default; + + IncrementalEvalAccumulator = default; + IncrementalPhaseAccumulator = default; + + EnPassant = BoardSquare.noSquare; + Castle = default; + + IsIncrementalEval = default; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetupFromPrevious(State previous) {