From 0bb20a94f4c0d8a027373cf6b128298056924dcf 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] Wrap state-related position internals in PositionState --- src/Lynx.Benchmark/ParseGame_Benchmark.cs | 6 +- .../TryParseFromUCIString_Benchmark.cs | 3 +- src/Lynx/Evaluation.cs | 48 +-- src/Lynx/Model/Game.cs | 2 +- src/Lynx/Model/GameState.cs | 62 ---- src/Lynx/Model/Position.cs | 292 ++++++++---------- src/Lynx/Model/PositionState.cs | 67 ++++ 7 files changed, 233 insertions(+), 247 deletions(-) delete mode 100644 src/Lynx/Model/GameState.cs create mode 100644 src/Lynx/Model/PositionState.cs diff --git a/src/Lynx.Benchmark/ParseGame_Benchmark.cs b/src/Lynx.Benchmark/ParseGame_Benchmark.cs index 729b10581..a63d4cfb7 100644 --- a/src/Lynx.Benchmark/ParseGame_Benchmark.cs +++ b/src/Lynx.Benchmark/ParseGame_Benchmark.cs @@ -525,7 +525,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); @@ -608,7 +608,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); @@ -695,7 +695,7 @@ public ImprovedGame2(ReadOnlySpan fen, ReadOnlySpan rawMoves, Span fen, ReadOnlySpan _gameInitialPosition = new Position(CurrentPosition); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move moveToPlay) + public PositionState MakeMove(Move moveToPlay) { var gameState = CurrentPosition.MakeMove(moveToPlay); diff --git a/src/Lynx/Evaluation.cs b/src/Lynx/Evaluation.cs index 88bc7b241..e9320ec6d 100644 --- a/src/Lynx/Evaluation.cs +++ b/src/Lynx/Evaluation.cs @@ -91,16 +91,16 @@ public partial class Position int blackPawnKingRingAttacks = (blackPawnAttacks & KingRing[whiteKing]).CountBits(); evaluationContext.IncreaseKingRingAttacks((int)Side.Black, blackPawnKingRingAttacks); - if (IsIncrementalEval) + if (_state.IsIncrementalEval) { - packedScore = IncrementalEvalAccumulator; - gamePhase = IncrementalPhaseAccumulator; + packedScore = _state.IncrementalEvalAccumulator; + gamePhase = _state.IncrementalPhaseAccumulator; - var kingPawnIndex = _kingPawnUniqueIdentifier & Constants.KingPawnHashMask; + var kingPawnIndex = _state.KingPawnUniqueIdentifier & Constants.KingPawnHashMask; ref var entry = ref pawnEvalTable[kingPawnIndex]; // pawnEvalTable hit: We can reuse cached eval for pawn additional evaluation + PieceProtectedByPawnBonus + KingShieldBonus - if (entry.Key == _kingPawnUniqueIdentifier) + if (entry.Key == _state.KingPawnUniqueIdentifier) { packedScore += entry.PackedScore; } @@ -152,7 +152,7 @@ public partial class Position // Pawn islands pawnScore += PawnIslands(whitePawns, blackPawns); - entry.Update(_kingPawnUniqueIdentifier, pawnScore); + entry.Update(_state.KingPawnUniqueIdentifier, pawnScore); packedScore += pawnScore; } @@ -191,14 +191,14 @@ public partial class Position } else { - IncrementalEvalAccumulator = 0; - IncrementalPhaseAccumulator = 0; + _state.IncrementalEvalAccumulator = 0; + _state.IncrementalPhaseAccumulator = 0; - var kingPawnIndex = _kingPawnUniqueIdentifier & Constants.KingPawnHashMask; + var kingPawnIndex = _state.KingPawnUniqueIdentifier & Constants.KingPawnHashMask; ref var entry = ref pawnEvalTable[kingPawnIndex]; // pawnTable hit: We can reuse cached eval for pawn additional evaluation + PieceProtectedByPawnBonus + KingShieldBonus - if (entry.Key == _kingPawnUniqueIdentifier) + if (entry.Key == _state.KingPawnUniqueIdentifier) { packedScore += entry.PackedScore; @@ -211,7 +211,7 @@ public partial class Position { whitePawnsCopy = whitePawnsCopy.WithoutLS1B(out var pieceSquareIndex); - IncrementalEvalAccumulator += PSQT(whiteBucket, blackBucket, (int)Piece.P, pieceSquareIndex); + _state.IncrementalEvalAccumulator += PSQT(whiteBucket, blackBucket, (int)Piece.P, pieceSquareIndex); // No incremental eval - included in pawn table | packedScore += AdditionalPieceEvaluation(...); } @@ -225,7 +225,7 @@ public partial class Position { blackPawnsCopy = blackPawnsCopy.WithoutLS1B(out var pieceSquareIndex); - IncrementalEvalAccumulator += PSQT(blackBucket, whiteBucket, (int)Piece.p, pieceSquareIndex); + _state.IncrementalEvalAccumulator += PSQT(blackBucket, whiteBucket, (int)Piece.p, pieceSquareIndex); // No incremental eval - included in pawn table | packedScore -= AdditionalPieceEvaluation(...); } @@ -249,7 +249,7 @@ public partial class Position { whitePawnsCopy = whitePawnsCopy.WithoutLS1B(out var pieceSquareIndex); - IncrementalEvalAccumulator += PSQT(whiteBucket, blackBucket, (int)Piece.P, pieceSquareIndex); + _state.IncrementalEvalAccumulator += PSQT(whiteBucket, blackBucket, (int)Piece.P, pieceSquareIndex); pawnScore += PawnAdditionalEvaluation(ref evaluationContext, whiteBucket, blackBucket, pieceSquareIndex, (int)Piece.P, whiteKing, blackKing); } @@ -268,7 +268,7 @@ public partial class Position { blackPawnsCopy = blackPawnsCopy.WithoutLS1B(out var pieceSquareIndex); - IncrementalEvalAccumulator += PSQT(blackBucket, whiteBucket, (int)Piece.p, pieceSquareIndex); + _state.IncrementalEvalAccumulator += PSQT(blackBucket, whiteBucket, (int)Piece.p, pieceSquareIndex); pawnScore -= PawnAdditionalEvaluation(ref evaluationContext, blackBucket, whiteBucket, pieceSquareIndex, (int)Piece.p, blackKing, whiteKing); } @@ -276,7 +276,7 @@ public partial class Position // Pawn islands pawnScore += PawnIslands(whitePawns, blackPawns); - entry.Update(_kingPawnUniqueIdentifier, pawnScore); + entry.Update(_state.KingPawnUniqueIdentifier, pawnScore); packedScore += pawnScore; } @@ -292,9 +292,9 @@ public partial class Position { bitboard = bitboard.WithoutLS1B(out var pieceSquareIndex); - IncrementalEvalAccumulator += PSQT(whiteBucket, blackBucket, pieceIndex, pieceSquareIndex); + _state.IncrementalEvalAccumulator += PSQT(whiteBucket, blackBucket, pieceIndex, pieceSquareIndex); - IncrementalPhaseAccumulator += GamePhaseByPiece[pieceIndex]; + _state.IncrementalPhaseAccumulator += GamePhaseByPiece[pieceIndex]; packedScore += AdditionalPieceEvaluation(ref evaluationContext, pieceSquareIndex, whiteBucket, blackBucket, pieceIndex, (int)Side.White, blackPawnAttacks, blackKing); } @@ -313,17 +313,17 @@ public partial class Position { bitboard = bitboard.WithoutLS1B(out var pieceSquareIndex); - IncrementalEvalAccumulator += PSQT(blackBucket, whiteBucket, pieceIndex, pieceSquareIndex); + _state.IncrementalEvalAccumulator += PSQT(blackBucket, whiteBucket, pieceIndex, pieceSquareIndex); - IncrementalPhaseAccumulator += GamePhaseByPiece[pieceIndex]; + _state.IncrementalPhaseAccumulator += GamePhaseByPiece[pieceIndex]; packedScore -= AdditionalPieceEvaluation(ref evaluationContext, pieceSquareIndex, blackBucket, whiteBucket, pieceIndex, (int)Side.Black, whitePawnAttacks, whiteKing); } } - packedScore += IncrementalEvalAccumulator; - gamePhase += IncrementalPhaseAccumulator; - IsIncrementalEval = true; + packedScore += _state.IncrementalEvalAccumulator; + gamePhase += _state.IncrementalPhaseAccumulator; + _state.IsIncrementalEval = true; } // Kings - they can't be incremental due to the king buckets @@ -519,8 +519,8 @@ public partial class Position [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Phase() { - var gamePhase = IsIncrementalEval - ? IncrementalPhaseAccumulator + var gamePhase = _state.IsIncrementalEval + ? _state.IncrementalPhaseAccumulator : PhaseFromScratch(); return (gamePhase > MaxPhase) // Early promotions diff --git a/src/Lynx/Model/Game.cs b/src/Lynx/Model/Game.cs index c3380d138..3c2874373 100644 --- a/src/Lynx/Model/Game.cs +++ b/src/Lynx/Model/Game.cs @@ -266,7 +266,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/GameState.cs b/src/Lynx/Model/GameState.cs deleted file mode 100644 index e7c779e2e..000000000 --- a/src/Lynx/Model/GameState.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace Lynx.Model; - -#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.IncrementalEvalAccumulator; - IncrementalPhaseAccumulator = position.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; - } -} - -#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 2906f80d1..8d3fe93ef 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 ulong[] _pieceBitBoards; private ulong[] _occupancyBitBoards; private 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 @@ -115,19 +102,18 @@ public int InitialKingSquare(int side) => private Position() { - // Allocate all required backing arrays (independent copy, avoids sharing state) _pieceBitBoards = ArrayPool.Shared.Rent(12); _occupancyBitBoards = ArrayPool.Shared.Rent(3); _board = ArrayPool.Shared.Rent(64); - _nonPawnHash = ArrayPool.Shared.Rent(2); _castlingRightsUpdateConstants = ArrayPool.Shared.Rent(64); - // Always allocate length 2 so ResetTo can index safely regardless of castling rights KingsideCastlingFreeSquares = ArrayPool.Shared.Rent(2); KingsideCastlingNonAttackedSquares = ArrayPool.Shared.Rent(2); QueensideCastlingFreeSquares = ArrayPool.Shared.Rent(2); QueensideCastlingNonAttackedSquares = ArrayPool.Shared.Rent(2); + _state = new(); + #if DEBUG _initialKingSquares = ArrayPool.Shared.Rent(2); _initialKingsideRookSquares = ArrayPool.Shared.Rent(2); @@ -161,23 +147,23 @@ public void PopulateFrom(ParseFENResult parsedFEN) _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[(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; Array.Fill(_castlingRightsUpdateConstants, Constants.NoUpdateCastlingRight, 0, 64); @@ -282,25 +268,20 @@ public void PopulateFrom(ParseFENResult parsedFEN) public void ResetTo(Position position) { - _uniqueIdentifier = position._uniqueIdentifier; - _kingPawnUniqueIdentifier = position._kingPawnUniqueIdentifier; - _minorHash = position._minorHash; - _majorHash = position._majorHash; - - _nonPawnHash[(int)Side.White] = position._nonPawnHash[(int)Side.White]; - _nonPawnHash[(int)Side.Black] = position._nonPawnHash[(int)Side.Black]; + _state.Reset(position._state); + _pieceBitBoards = ArrayPool.Shared.Rent(12); Array.Copy(position._pieceBitBoards, _pieceBitBoards, 12); Array.Copy(position._occupancyBitBoards, _occupancyBitBoards, 3); 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; Array.Copy(position._castlingRightsUpdateConstants, _castlingRightsUpdateConstants, 64); @@ -338,15 +319,15 @@ public void ResetTo(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); @@ -377,62 +358,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(); @@ -446,10 +427,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()) @@ -465,30 +446,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; @@ -499,8 +480,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; } @@ -517,10 +498,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; @@ -544,24 +525,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; } } } @@ -574,8 +555,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; } @@ -609,9 +590,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 @@ -629,9 +610,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; } @@ -665,9 +646,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 @@ -685,9 +666,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; } @@ -704,8 +685,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; } @@ -716,16 +697,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 @@ -735,7 +716,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); @@ -894,19 +875,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(); } @@ -916,12 +897,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(); @@ -932,8 +913,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(); } @@ -1215,19 +1196,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'); } @@ -1235,22 +1216,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); @@ -1264,7 +1245,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); @@ -1274,7 +1255,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) @@ -1319,33 +1300,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()]); } @@ -1571,22 +1552,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}"); } } @@ -1627,7 +1608,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..57da22b49 --- /dev/null +++ b/src/Lynx/Model/PositionState.cs @@ -0,0 +1,67 @@ +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) + : this() + { + Reset(original); + } + + public void Reset(PositionState original) + { + UniqueIdentifier = original.UniqueIdentifier; + KingPawnUniqueIdentifier = original.KingPawnUniqueIdentifier; + NonPawnHash[(int)Side.White] = original.NonPawnHash[(int)Side.White]; + NonPawnHash[(int)Side.Black] = original.NonPawnHash[(int)Side.Black]; + 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