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 729b10581..f29dc6789 100644 --- a/src/Lynx.Benchmark/ParseGame_Benchmark.cs +++ b/src/Lynx.Benchmark/ParseGame_Benchmark.cs @@ -525,9 +525,9 @@ internal OriginalGame(string fen, string[] movesUCIString) : this(fen) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move moveToPlay) + public void MakeMove(Move moveToPlay) { - var gameState = CurrentPosition.MakeMove(moveToPlay); + CurrentPosition.MakeMove(moveToPlay); if (CurrentPosition.WasProduceByAValidMove()) { @@ -538,13 +538,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; } } @@ -608,9 +606,9 @@ internal ImprovedGame(string fen, ReadOnlySpan rawMoves, Span range } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move moveToPlay) + public void MakeMove(Move moveToPlay) { - var gameState = CurrentPosition.MakeMove(moveToPlay); + CurrentPosition.MakeMove(moveToPlay); if (CurrentPosition.WasProduceByAValidMove()) { @@ -621,13 +619,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; } } @@ -695,9 +691,9 @@ public ImprovedGame2(ReadOnlySpan fen, ReadOnlySpan rawMoves, Span 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()) { @@ -246,13 +247,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/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index 38e5ee70d..097442826 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -130,6 +130,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 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..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" @@ -266,9 +265,9 @@ public static bool IsThreefoldRepetition(ReadOnlySpan positionHashHistory public static bool Is50MovesRepetition(int halfMovesWithoutCaptureOrPawnMove) => halfMovesWithoutCaptureOrPawnMove >= 100; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move moveToPlay) + public void MakeMove(Move moveToPlay) { - var gameState = CurrentPosition.MakeMove(moveToPlay); + CurrentPosition.MakeMove(moveToPlay); if (CurrentPosition.WasProduceByAValidMove()) { @@ -280,17 +279,15 @@ public GameState 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; } /// /// 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() { 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/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 2906f80d1..37f4dca4b 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -12,24 +12,15 @@ 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 int _stackCounter; + private readonly State[] _stateStack; - private ulong _uniqueIdentifier; - private ulong _kingPawnUniqueIdentifier; - private readonly ulong[] _nonPawnHash; - private ulong _minorHash; - private ulong _majorHash; + private State _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 +42,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 @@ -114,20 +104,31 @@ public int InitialKingSquare(int side) => : BlackShortCastle.SourceSquare(); private Position() + : this(Constants.MaxNumberMovesInAGame) + { + } + + private Position(int stateStackLength) { - // 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); + _stateStack = new State[stateStackLength]; + for (int i = 0; i < _stateStack.Length; ++i) + { + _stateStack[i] = new(); + } + + _state = _stateStack[0]; + _stackCounter = 0; + #if DEBUG _initialKingSquares = ArrayPool.Shared.Rent(2); _initialKingsideRookSquares = ArrayPool.Shared.Rent(2); @@ -154,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; @@ -161,23 +171,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 +292,22 @@ public void PopulateFrom(ParseFENResult parsedFEN) public void ResetTo(Position position) { - _uniqueIdentifier = position._uniqueIdentifier; - _kingPawnUniqueIdentifier = position._kingPawnUniqueIdentifier; - _minorHash = position._minorHash; - _majorHash = position._majorHash; + Debug.Assert(position._state != null); - _nonPawnHash[(int)Side.White] = position._nonPawnHash[(int)Side.White]; - _nonPawnHash[(int)Side.Black] = position._nonPawnHash[(int)Side.Black]; + // TODO see if we can avoid + for (int i = 0; i < _stateStack.Length; ++i) + { + _stateStack[i].Reset(); + } + _stateStack[0].SetupFromPrevious(position._state); + _state = _stateStack[0]; + _stackCounter = 0; 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; - - IsIncrementalEval = position.IsIncrementalEval; - IncrementalEvalAccumulator = position.IncrementalEvalAccumulator; - IncrementalPhaseAccumulator = position.IncrementalPhaseAccumulator; Array.Copy(position._castlingRightsUpdateConstants, _castlingRightsUpdateConstants, 64); @@ -338,15 +345,20 @@ public void ResetTo(Position position) #region Move making [MethodImpl(MethodImplOptions.AggressiveInlining)] - public GameState MakeMove(Move move) + public void 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 oldState = _state; + + ++_stackCounter; + _state = _stateStack[_stackCounter]; + _state.SetupFromPrevious(oldState); + _state.EnPassant = BoardSquare.noSquare; var oldSide = (int)_side; var offset = Utils.PieceOffset(oldSide); @@ -377,62 +389,60 @@ 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)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) { - _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; - // _incrementalEvalAccumulator updates - if (IsIncrementalEval) + if (_state.IsIncrementalEval) { var whiteKing = _pieceBitBoards[(int)Piece.K].GetLS1BIndex(); var blackKing = _pieceBitBoards[(int)Piece.k].GetLS1BIndex(); @@ -446,10 +456,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 +475,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 +509,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 +527,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 +554,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 +584,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 +619,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 +639,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 +675,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 +695,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 +714,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,26 +726,24 @@ 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 //Debug.Assert(ZobristTable.PawnKingHash(this) != _kingPawnUniqueIdentifier && WasProduceByAValidMove()); - - return gameState; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UnmakeMove(Move move, GameState gameState) + public void UnmakeMove(Move move) { var oppositeSide = (int)_side; var side = Utils.OppositeSide(oppositeSide); @@ -893,20 +901,8 @@ public void UnmakeMove(Move move, GameState gameState) _occupancyBitBoards[2] = _occupancyBitBoards[1] | _occupancyBitBoards[0]; - // Updating saved values - _castle = gameState.Castle; - _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; - - IncrementalEvalAccumulator = gameState.IncrementalEvalAccumulator; - IncrementalPhaseAccumulator = gameState.IncrementalPhaseAccumulator; - IsIncrementalEval = gameState.IsIncrementalEval; + --_stackCounter; + _state = _stateStack[_stackCounter]; Validate(); } @@ -916,12 +912,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 +928,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 +1211,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 +1231,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 +1260,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 +1270,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 +1315,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()]); } @@ -1529,12 +1525,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"); @@ -1542,7 +1538,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"); @@ -1552,7 +1548,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"); @@ -1560,7 +1556,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"); @@ -1571,22 +1567,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 +1623,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..e9e1fcafe --- /dev/null +++ b/src/Lynx/Model/PositionState.cs @@ -0,0 +1,161 @@ +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 + { + [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 + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } +#pragma warning restore S3887 // Mutable, non-private fields should not be "readonly" + + public ulong MinorHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } + + public ulong MajorHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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 + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set; + } + + 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) + { + 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; + + EnPassant = previous.EnPassant; + 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/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/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 73ee45b14..df07939e1 100644 --- a/src/Lynx/Search/IDDFS.cs +++ b/src/Lynx/Search/IDDFS.cs @@ -449,9 +449,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) { @@ -578,7 +578,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) @@ -618,10 +618,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 5d2ebff87..3709aba4f 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; @@ -926,10 +926,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; } @@ -945,7 +945,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); 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; })