From 1ee77d5cfcbece81dee373e44b6a80d7b7cab2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Sat, 1 Jun 2024 18:06:14 +0200 Subject: [PATCH 1/7] Add special movegen and make/unmake methods optimized to handle quiet moves --- src/Lynx/Model/Position.cs | 106 +++++++++++++++++++++++ src/Lynx/MoveGenerator.cs | 167 +++++++++++++++++++++++++++++-------- 2 files changed, 237 insertions(+), 36 deletions(-) diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index ede746443..235a07dff 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; @@ -363,6 +364,11 @@ public GameState MakeMove(Move move) //} } + /// + /// Special version of for those cases when we know the capture piece isn't already included in the move + /// + /// Move that doesn't have the capture piece encoded + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public GameState MakeMoveCalculatingCapturedPiece(ref Move move) { @@ -507,6 +513,69 @@ public GameState MakeMoveCalculatingCapturedPiece(ref Move move) //} } + /// + /// Special version of optimized for quiet moves + /// + /// Not a capture, en-passant, promotion or castling move + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GameState MakeQuietMove(Move move) + { + Debug.Assert(!move.IsCapture(), "Quiet move expected"); + Debug.Assert(!move.IsEnPassant(), "Quiet move expected"); + Debug.Assert(!move.IsCastle(), "Quiet move expected"); + Debug.Assert(!move.IsPromotion(), "Quiet move expected"); + Debug.Assert(move.SpecialMoveFlag() == SpecialMoveType.None || move.SpecialMoveFlag() == SpecialMoveType.DoublePawnPush, "Quiet move expected"); + Debug.Assert(move.CapturedPiece() == (int)Piece.None || move.CapturedPiece() == 0, "Quiet move expected"); + + byte castleCopy = Castle; + BoardSquare enpassantCopy = EnPassant; + long uniqueIdentifierCopy = UniqueIdentifier; + + var oldSide = (int)Side; + var oppositeSide = Utils.OppositeSide(oldSide); + + int sourceSquare = move.SourceSquare(); + int targetSquare = move.TargetSquare(); + int piece = move.Piece(); + + PieceBitBoards[piece].PopBit(sourceSquare); + OccupancyBitBoards[oldSide].PopBit(sourceSquare); + + PieceBitBoards[piece].SetBit(targetSquare); + OccupancyBitBoards[oldSide].SetBit(targetSquare); + + UniqueIdentifier ^= + ZobristTable.SideHash() + ^ ZobristTable.PieceHash(sourceSquare, piece) + ^ ZobristTable.PieceHash(targetSquare, piece) + ^ ZobristTable.EnPassantHash((int)EnPassant) // We clear the existing enpassant square, if any + ^ ZobristTable.CastleHash(Castle); // We clear the existing castle rights + + EnPassant = BoardSquare.noSquare; + + if (move.SpecialMoveFlag() == SpecialMoveType.DoublePawnPush) + { + var pawnPush = +8 - (oldSide * 16); + 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); + } + + Side = (Side)oppositeSide; + OccupancyBitBoards[2] = OccupancyBitBoards[1] | OccupancyBitBoards[0]; + + // Updating castling rights + Castle &= Constants.CastlingRightsUpdateConstants[sourceSquare]; + Castle &= Constants.CastlingRightsUpdateConstants[targetSquare]; + + UniqueIdentifier ^= ZobristTable.CastleHash(Castle); + + return new GameState(uniqueIdentifierCopy, enpassantCopy, castleCopy); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void UnmakeMove(Move move, GameState gameState) { @@ -598,6 +667,43 @@ public void UnmakeMove(Move move, GameState gameState) UniqueIdentifier = gameState.ZobristKey; } + /// + /// Special case of optimized quiet moves + /// + /// Not a capture, en-passant, promotion or castling move + /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UnmakeQuietMove(Move move, GameState gameState) + { + Debug.Assert(!move.IsCapture(), "Quiet move expected"); + Debug.Assert(!move.IsEnPassant(), "Quiet move expected"); + Debug.Assert(!move.IsCastle(), "Quiet move expected"); + Debug.Assert(!move.IsPromotion(), "Quiet move expected"); + Debug.Assert(move.SpecialMoveFlag() == SpecialMoveType.None || move.SpecialMoveFlag() == SpecialMoveType.DoublePawnPush, "Quiet move expected"); + Debug.Assert(move.CapturedPiece() == (int)Piece.None || move.CapturedPiece() == 0, "Quiet move expected"); + + var oppositeSide = (int)Side; + var side = Utils.OppositeSide(oppositeSide); + Side = (Side)side; + + int sourceSquare = move.SourceSquare(); + int targetSquare = move.TargetSquare(); + int piece = move.Piece(); + + PieceBitBoards[piece].PopBit(targetSquare); + OccupancyBitBoards[side].PopBit(targetSquare); + + PieceBitBoards[piece].SetBit(sourceSquare); + OccupancyBitBoards[side].SetBit(sourceSquare); + + OccupancyBitBoards[2] = OccupancyBitBoards[1] | OccupancyBitBoards[0]; + + // Updating saved values + Castle = gameState.Castle; + EnPassant = gameState.EnPassant; + UniqueIdentifier = gameState.ZobristKey; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public GameState MakeNullMove() { diff --git a/src/Lynx/MoveGenerator.cs b/src/Lynx/MoveGenerator.cs index 3dd301727..f8f7ea4e2 100644 --- a/src/Lynx/MoveGenerator.cs +++ b/src/Lynx/MoveGenerator.cs @@ -65,12 +65,7 @@ internal static Move[] GenerateAllMoves(Position position, bool capturesOnly = f [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span GenerateAllMoves(Position position, Span movePool) { -#if DEBUG - if (position.Side == Side.Both) - { - return []; - } -#endif + Debug.Assert(position.Side != Side.Both); int localIndex = 0; @@ -96,12 +91,7 @@ public static Span GenerateAllMoves(Position position, Span movePool [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Move[] GenerateAllCaptures(Position position, Move[] movePool) { -#if DEBUG - if (position.Side == Side.Both) - { - return []; - } -#endif + Debug.Assert(position.Side != Side.Both); int localIndex = 0; @@ -127,12 +117,7 @@ public static Move[] GenerateAllCaptures(Position position, Move[] movePool) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Span GenerateAllCaptures(Position position, Span movePool) { -#if DEBUG - if (position.Side == Side.Both) - { - return []; - } -#endif + Debug.Assert(position.Side != Side.Both); int localIndex = 0; @@ -467,12 +452,7 @@ internal static void GeneratePieceCaptures(ref int localIndex, Span movePo [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CanGenerateAtLeastAValidMove(Position position) { -#if DEBUG - if (position.Side == Side.Both) - { - return false; - } -#endif + Debug.Assert(position.Side != Side.Both); var offset = Utils.PieceOffset(position.Side); @@ -497,6 +477,38 @@ public static bool CanGenerateAtLeastAValidMove(Position position) #endif } + /// + /// Generates all psuedo-legal moves from , ordered by + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanGenerateAtLeastAValidQuietMove(Position position) + { + Debug.Assert(position.Side != Side.Both); + + var offset = Utils.PieceOffset(position.Side); + +#if DEBUG + try + { +#endif + return IsAnyPawnQuietMoveValid(position, offset) + || IsAnyPieceQuietMoveValid((int)Piece.K + offset, position) + || IsAnyPieceQuietMoveValid((int)Piece.Q + offset, position) + || IsAnyPieceQuietMoveValid((int)Piece.B + offset, position) + || IsAnyPieceQuietMoveValid((int)Piece.N + offset, position) + || IsAnyPieceQuietMoveValid((int)Piece.R + offset, position); +#if DEBUG + } + catch (Exception e) + { + Debug.Fail($"Error in {nameof(CanGenerateAtLeastAValidQuietMove)}", e.StackTrace); + return false; + } +#endif + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsAnyPawnMoveValid(Position position, int offset) { @@ -592,12 +604,55 @@ private static bool IsAnyPawnMoveValid(Position position, int offset) return false; } - /// - /// Obvious moves that put the king in check have been discarded, but the rest still need to be discarded - /// see FEN position "8/8/8/2bbb3/2bKb3/2bbb3/8/8 w - - 0 1", where 4 legal moves (corners) are found - /// - /// - /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAnyPawnQuietMoveValid(Position position, int offset) + { + int sourceSquare; + + var piece = (int)Piece.P + offset; + var pawnPush = +8 - ((int)position.Side * 16); // position.Side == Side.White ? -8 : +8 + var bitboard = position.PieceBitBoards[piece]; + + while (bitboard != default) + { + sourceSquare = bitboard.GetLS1BIndex(); + bitboard.ResetLS1B(); + + var sourceRank = (sourceSquare >> 3) + 1; + +#if DEBUG + if (sourceRank == 1 || sourceRank == 8) + { + _logger.Warn("There's a non-promoted {0} pawn in rank {1}", position.Side, sourceRank); + continue; + } +#endif + // Pawn pushes + var singlePushSquare = sourceSquare + pawnPush; + if (!position.OccupancyBitBoards[2].GetBit(singlePushSquare)) + { + // Single pawn push + if (IsValidQuietMove(position, MoveExtensions.Encode(sourceSquare, singlePushSquare, piece))) + { + return true; + } + + // Double pawn push + // Inside of the if because singlePush square cannot be occupied either + + var doublePushSquare = sourceSquare + (2 * pawnPush); + if (!position.OccupancyBitBoards[2].GetBit(doublePushSquare) + && ((sourceRank == 2 && position.Side == Side.Black) || (sourceRank == 7 && position.Side == Side.White)) + && IsValidQuietMove(position, MoveExtensions.EncodeDoublePawnPush(sourceSquare, doublePushSquare, piece))) + { + return true; + } + } + } + + return false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsAnyCastlingMoveValid(Position position) { @@ -662,12 +717,6 @@ private static bool IsAnyCastlingMoveValid(Position position) return false; } - /// - /// Generate Knight, Bishop, Rook and Queen moves - /// - /// - /// - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsAnyPieceMoveValid(int piece, Position position) { @@ -702,6 +751,35 @@ private static bool IsAnyPieceMoveValid(int piece, Position position) return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAnyPieceQuietMoveValid(int piece, Position position) + { + var bitboard = position.PieceBitBoards[piece]; + int sourceSquare, targetSquare; + + while (bitboard != default) + { + sourceSquare = bitboard.GetLS1BIndex(); + bitboard.ResetLS1B(); + + var attacks = _pieceAttacks[piece](sourceSquare, position.OccupancyBitBoards[(int)Side.Both]) + & (~position.OccupancyBitBoards[(int)Side.Both]); + + while (attacks != default) + { + targetSquare = attacks.GetLS1BIndex(); + attacks.ResetLS1B(); + + if (IsValidQuietMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece))) + { + return true; + } + } + } + + return false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsValidMove(Position position, Move move) { @@ -727,6 +805,23 @@ private static bool IsValidMove(Position position, Move move) return result; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidQuietMove(Position position, Move move) + { + Debug.Assert(!move.IsCapture(), "Quiet move expected"); + Debug.Assert(!move.IsEnPassant(), "Quiet move expected"); + Debug.Assert(!move.IsCastle(), "Quiet move expected"); + Debug.Assert(!move.IsPromotion(), "Quiet move expected"); + Debug.Assert(move.SpecialMoveFlag() == SpecialMoveType.None || move.SpecialMoveFlag() == SpecialMoveType.DoublePawnPush, "Quiet move expected"); + Debug.Assert(move.CapturedPiece() == (int)Piece.None || move.CapturedPiece() == 0, "Quiet move expected"); + + var gameState = position.MakeQuietMove(move); + bool result = position.WasProduceByAValidMove(); + position.UnmakeQuietMove(move, gameState); + + return result; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FindCapturedPiece(Position position, int offset, int targetSquare) { From 194b989fcf82644132d4314d66aae8694fc3f5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 3 Jun 2024 22:55:46 +0200 Subject: [PATCH 2/7] Add MakeMoveFast and UnmakeMoveFast, which make use of the MakeQuietMove/UnmakeQuietMove where possible --- src/Lynx/Model/Game.cs | 4 ++-- src/Lynx/Model/GameState.cs | 11 +++++++++++ src/Lynx/Model/Position.cs | 31 +++++++++++++++++++++++++++++-- src/Lynx/MoveGenerator.cs | 4 ++-- src/Lynx/Perft.cs | 8 ++++---- src/Lynx/Search/IDDFS.cs | 4 ++-- src/Lynx/Search/NegaMax.cs | 12 ++++++------ 7 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/Lynx/Model/Game.cs b/src/Lynx/Model/Game.cs index 324d3edda..fc3297a95 100644 --- a/src/Lynx/Model/Game.cs +++ b/src/Lynx/Model/Game.cs @@ -184,7 +184,7 @@ public static bool IsThreefoldRepetition(List positionHashHistory, Positio [MethodImpl(MethodImplOptions.AggressiveInlining)] public GameState MakeMove(Move moveToPlay) { - var gameState = CurrentPosition.MakeMove(moveToPlay); + var gameState = CurrentPosition.MakeMoveFast(moveToPlay); if (CurrentPosition.WasProduceByAValidMove()) { @@ -195,7 +195,7 @@ public GameState MakeMove(Move moveToPlay) else { _logger.Warn("Error trying to play {0}", moveToPlay.UCIString()); - CurrentPosition.UnmakeMove(moveToPlay, gameState); + CurrentPosition.UnmakeMoveFast(moveToPlay, gameState); } PositionHashHistory.Add(CurrentPosition.UniqueIdentifier); diff --git a/src/Lynx/Model/GameState.cs b/src/Lynx/Model/GameState.cs index b68fe6333..d8461326b 100644 --- a/src/Lynx/Model/GameState.cs +++ b/src/Lynx/Model/GameState.cs @@ -5,6 +5,8 @@ public readonly struct GameState public readonly BoardSquare EnPassant; + public readonly bool Quiet; + public readonly byte Castle; public GameState(long zobristKey, BoardSquare enpassant, byte castle) @@ -12,5 +14,14 @@ public GameState(long zobristKey, BoardSquare enpassant, byte castle) ZobristKey = zobristKey; EnPassant = enpassant; Castle = castle; + Quiet = false; + } + + public GameState(long zobristKey, BoardSquare enpassant, bool isQuiet, byte castle) + { + ZobristKey = zobristKey; + EnPassant = enpassant; + Castle = castle; + Quiet = isQuiet; } } diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 235a07dff..8c33f70e4 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -230,6 +230,33 @@ public Position(Position position, Move move) : this(position) UniqueIdentifier ^= ZobristTable.CastleHash(Castle); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GameState MakeMoveFast(Move move) + { + if (move.IsCapture() + || move.IsCastle() + || move.IsPromotion() + || move.IsEnPassant()) + { + return MakeMove(move); + } + + return MakeQuietMove(move); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void UnmakeMoveFast(Move move, GameState gameState) + { + if (gameState.Quiet) + { + UnmakeQuietMove(move, gameState); + } + else + { + UnmakeMove(move, gameState); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public GameState MakeMove(Move move) { @@ -573,7 +600,7 @@ public GameState MakeQuietMove(Move move) UniqueIdentifier ^= ZobristTable.CastleHash(Castle); - return new GameState(uniqueIdentifierCopy, enpassantCopy, castleCopy); + return new GameState(uniqueIdentifierCopy, enpassantCopy, isQuiet: true, castleCopy); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -720,7 +747,7 @@ public GameState MakeNullMove() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UnMakeNullMove(GameState gameState) + public void UnmakeNullMove(GameState gameState) { Side = (Side)Utils.OppositeSide(Side); EnPassant = gameState.EnPassant; diff --git a/src/Lynx/MoveGenerator.cs b/src/Lynx/MoveGenerator.cs index f8f7ea4e2..c7d4128e5 100644 --- a/src/Lynx/MoveGenerator.cs +++ b/src/Lynx/MoveGenerator.cs @@ -549,7 +549,7 @@ private static bool IsAnyPawnMoveValid(Position position, int offset) return true; } } - else if (IsValidMove(position, MoveExtensions.Encode(sourceSquare, singlePushSquare, piece))) + else if (IsValidQuietMove(position, MoveExtensions.Encode(sourceSquare, singlePushSquare, piece))) { return true; } @@ -741,7 +741,7 @@ private static bool IsAnyPieceMoveValid(int piece, Position position) { return true; } - else if (IsValidMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece))) + else if (IsValidQuietMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece))) { return true; } diff --git a/src/Lynx/Perft.cs b/src/Lynx/Perft.cs index de20d5eef..fe1bef202 100644 --- a/src/Lynx/Perft.cs +++ b/src/Lynx/Perft.cs @@ -43,13 +43,13 @@ internal static long ResultsImpl(Position position, int depth, long nodes) Span moves = stackalloc Move[Constants.MaxNumberOfPossibleMovesInAPosition]; foreach (var move in MoveGenerator.GenerateAllMoves(position, moves)) { - var state = position.MakeMove(move); + var state = position.MakeMoveFast(move); if (position.WasProduceByAValidMove()) { nodes = ResultsImpl(position, depth - 1, nodes); } - position.UnmakeMove(move, state); + position.UnmakeMoveFast(move, state); } return nodes; @@ -65,7 +65,7 @@ private static long DivideImpl(Position position, int depth, long nodes, Action< Span moves = stackalloc Move[Constants.MaxNumberOfPossibleMovesInAPosition]; foreach (var move in MoveGenerator.GenerateAllMoves(position, moves)) { - var state = position.MakeMove(move); + var state = position.MakeMoveFast(move); if (position.WasProduceByAValidMove()) { @@ -75,7 +75,7 @@ private static long DivideImpl(Position position, int depth, long nodes, Action< write($"{move.UCIString()}\t\t{nodes - accumulatedNodes}"); } - position.UnmakeMove(move, state); + position.UnmakeMoveFast(move, state); } write(string.Empty); diff --git a/src/Lynx/Search/IDDFS.cs b/src/Lynx/Search/IDDFS.cs index a7cbb8c47..2e206c2dd 100644 --- a/src/Lynx/Search/IDDFS.cs +++ b/src/Lynx/Search/IDDFS.cs @@ -225,9 +225,9 @@ private bool OnlyOneLegalMove(ref Move firstLegalMove, [NotNullWhen(true)] out S Span moves = stackalloc Move[Constants.MaxNumberOfPossibleMovesInAPosition]; foreach (var move in MoveGenerator.GenerateAllMoves(Game.CurrentPosition, moves)) { - var gameState = Game.CurrentPosition.MakeMove(move); + var gameState = Game.CurrentPosition.MakeMoveFast(move); bool isPositionValid = Game.CurrentPosition.WasProduceByAValidMove(); - Game.CurrentPosition.UnmakeMove(move, gameState); + Game.CurrentPosition.UnmakeMoveFast(move, gameState); if (isPositionValid) { diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index 03259e715..ea944696f 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -156,7 +156,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM var gameState = position.MakeNullMove(); var evaluation = -NegaMax(depth - 1 - nmpReduction, ply + 1, -beta, -beta + 1, parentWasNullMove: true); - position.UnMakeNullMove(gameState); + position.UnmakeNullMove(gameState); if (evaluation >= beta) { @@ -213,11 +213,11 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM var move = pseudoLegalMoves[moveIndex]; - var gameState = position.MakeMove(move); + var gameState = position.MakeMoveFast(move); if (!position.WasProduceByAValidMove()) { - position.UnmakeMove(move, gameState); + position.UnmakeMoveFast(move, gameState); continue; } @@ -262,7 +262,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM // After making a move Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; Game.PositionHashHistory.RemoveAt(Game.PositionHashHistory.Count - 1); - position.UnmakeMove(move, gameState); + position.UnmakeMoveFast(move, gameState); break; } @@ -278,7 +278,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM // After making a move Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; Game.PositionHashHistory.RemoveAt(Game.PositionHashHistory.Count - 1); - position.UnmakeMove(move, gameState); + position.UnmakeMoveFast(move, gameState); break; } @@ -353,7 +353,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM // Game.PositionHashHistory is update above Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; Game.PositionHashHistory.RemoveAt(Game.PositionHashHistory.Count - 1); - position.UnmakeMove(move, gameState); + position.UnmakeMoveFast(move, gameState); PrintMove(ply, move, evaluation); From 4021682480e5270fd11b9adf425d9a9a07cefe40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Sat, 6 Jul 2024 02:51:52 +0200 Subject: [PATCH 3/7] Add `GeneratePawnQuietMoves` and `GeneratePieceQuietMoves` --- src/Lynx/MoveGenerator.cs | 80 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/src/Lynx/MoveGenerator.cs b/src/Lynx/MoveGenerator.cs index 0b2e0a9dd..deb947463 100644 --- a/src/Lynx/MoveGenerator.cs +++ b/src/Lynx/MoveGenerator.cs @@ -293,6 +293,53 @@ internal static void GeneratePawnCapturesAndPromotions(ref int localIndex, Span< } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void GeneratePawnQuietMoves(ref int localIndex, Span movePool, Position position, int offset) + { + int sourceSquare; + + var piece = (int)Piece.P + offset; + var pawnPush = +8 - ((int)position.Side * 16); // position.Side == Side.White ? -8 : +8 + var bitboard = position.PieceBitBoards[piece]; + + while (bitboard != default) + { + sourceSquare = bitboard.GetLS1BIndex(); + bitboard.ResetLS1B(); + + var sourceRank = (sourceSquare >> 3) + 1; + +#if DEBUG + if (sourceRank == 1 || sourceRank == 8) + { + _logger.Warn("There's a non-promoted {0} pawn in rank {1}", position.Side, sourceRank); + continue; + } +#endif + + // Pawn pushes + var singlePushSquare = sourceSquare + pawnPush; + if (!position.OccupancyBitBoards[2].GetBit(singlePushSquare)) + { + // Single pawn push + var targetRank = (singlePushSquare >> 3) + 1; + if (targetRank != 1 && targetRank != 8) // No promotion + { + movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, singlePushSquare, piece); + } + + // Double pawn push + // Inside of the if because singlePush square cannot be occupied either + var doublePushSquare = sourceSquare + (2 * pawnPush); + if (!position.OccupancyBitBoards[2].GetBit(doublePushSquare) + && ((sourceRank == 2 && position.Side == Side.Black) || (sourceRank == 7 && position.Side == Side.White))) + { + movePool[localIndex++] = MoveExtensions.EncodeDoublePawnPush(sourceSquare, doublePushSquare, piece); + } + } + } + } + /// /// Obvious moves that put the king in check have been discarded, but the rest still need to be discarded /// see FEN position "8/8/8/2bbb3/2bKb3/2bbb3/8/8 w - - 0 1", where 4 legal moves (corners) are found @@ -442,6 +489,37 @@ internal static void GeneratePieceCaptures(ref int localIndex, Span movePo } } + /// + /// Generate Knight, Bishop, Rook and Queen moves + /// + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void GeneratePieceQuietMoves(ref int localIndex, Span movePool, int piece, Position position, int offset) + { + var bitboard = position.PieceBitBoards[piece]; + int sourceSquare, targetSquare; + + while (bitboard != default) + { + sourceSquare = bitboard.GetLS1BIndex(); + bitboard.ResetLS1B(); + + var attacks = _pieceAttacks[piece](sourceSquare, position.OccupancyBitBoards[(int)Side.Both]) + & ~position.OccupancyBitBoards[(int)Side.Both]; + + while (attacks != default) + { + targetSquare = attacks.GetLS1BIndex(); + attacks.ResetLS1B(); + + movePool[localIndex++] = MoveExtensions.Encode(sourceSquare, targetSquare, piece); + } + } + } + /// /// Generates all psuedo-legal moves from , ordered by /// @@ -736,7 +814,7 @@ private static bool IsAnyPieceMoveValid(int piece, Position position) if (position.OccupancyBitBoards[(int)Side.Both].GetBit(targetSquare)) { - if(IsValidMove(position, MoveExtensions.EncodeCapture(sourceSquare, targetSquare, piece))) + if (IsValidMove(position, MoveExtensions.EncodeCapture(sourceSquare, targetSquare, piece))) { return true; } From 3fec8f73216586e81271d5862b2a8ff5aafc0cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Sat, 6 Jul 2024 02:57:24 +0200 Subject: [PATCH 4/7] Initial implementation, where an `IEnumerable` that marks the end of the current movegen stage is used and an array is passed (beware, allocs!) --- src/Lynx/MoveGenerator.cs | 36 +++ src/Lynx/Search/NegaMax.cs | 441 +++++++++++++++++++------------------ 2 files changed, 262 insertions(+), 215 deletions(-) diff --git a/src/Lynx/MoveGenerator.cs b/src/Lynx/MoveGenerator.cs index deb947463..053b6965c 100644 --- a/src/Lynx/MoveGenerator.cs +++ b/src/Lynx/MoveGenerator.cs @@ -82,6 +82,42 @@ public static Span GenerateAllMoves(Position position, Span movePool return movePool[..localIndex]; } + /// + /// Generates all psuedo-legal moves from , ordered by + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable GenerateAllMovesStaged(Position position, Move[] movePool) + { + //https://antao-almada.medium.com/how-to-use-span-t-and-memory-t-c0b126aae652 + Debug.Assert(position.Side != Side.Both); + + int localIndex = 0; + + var offset = Utils.PieceOffset(position.Side); + + GeneratePawnCapturesAndPromotions(ref localIndex, movePool, position, offset); + GenerateCastlingMoves(ref localIndex, movePool, position); + GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.K + offset, position, offset); + GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.N + offset, position, offset); + GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.B + offset, position, offset); + GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.R + offset, position, offset); + GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.Q + offset, position, offset); + + yield return localIndex; + + GeneratePawnQuietMoves(ref localIndex, movePool, position, offset); + GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.K + offset, position, offset); + GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.N + offset, position, offset); + GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.B + offset, position, offset); + GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.R + offset, position, offset); + GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.Q + offset, position, offset); + + yield return localIndex; + } + /// /// Generates all psuedo-legal captures from , ordered by /// diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index c106dcfaf..4f30d3834 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -165,289 +165,300 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM } } - Span moves = stackalloc Move[Constants.MaxNumberOfPossibleMovesInAPosition]; - var pseudoLegalMoves = MoveGenerator.GenerateAllMoves(position, moves); - Span scores = stackalloc int[pseudoLegalMoves.Length]; - if (_isFollowingPV) + Move[] moves = new Move[Constants.MaxNumberOfPossibleMovesInAPosition]; + var pseudoLegalMovesStaged = MoveGenerator.GenerateAllMovesStaged(position, moves); + var enumerator = pseudoLegalMovesStaged.GetEnumerator(); + + var nodeType = NodeType.Alpha; + Move? bestMove = null; + bool isAnyMoveValid = false; + + bool enumeratorHasNext = true; + var lowerLimit = 0; + while (enumeratorHasNext) { - _isFollowingPV = false; - for (int i = 0; i < pseudoLegalMoves.Length; ++i) + enumeratorHasNext = enumerator.MoveNext(); + var pseudoLegalMoves = moves[lowerLimit..enumerator.Current]; + lowerLimit = enumerator.Current; + Span scores = stackalloc int[pseudoLegalMoves.Length]; + if (_isFollowingPV) { - scores[i] = ScoreMove(pseudoLegalMoves[i], ply, isNotQSearch: true, ttBestMove); - - if (pseudoLegalMoves[i] == _pVTable[depth]) + _isFollowingPV = false; + for (int i = 0; i < pseudoLegalMoves.Length; ++i) { - _isFollowingPV = true; - _isScoringPV = true; + scores[i] = ScoreMove(pseudoLegalMoves[i], ply, isNotQSearch: true, ttBestMove); + + if (pseudoLegalMoves[i] == _pVTable[depth]) + { + _isFollowingPV = true; + _isScoringPV = true; + } } } - } - else - { - for (int i = 0; i < pseudoLegalMoves.Length; ++i) + else { - scores[i] = ScoreMove(pseudoLegalMoves[i], ply, isNotQSearch: true, ttBestMove); + for (int i = 0; i < pseudoLegalMoves.Length; ++i) + { + scores[i] = ScoreMove(pseudoLegalMoves[i], ply, isNotQSearch: true, ttBestMove); + } } - } - var nodeType = NodeType.Alpha; - Move? bestMove = null; - bool isAnyMoveValid = false; - Span visitedMoves = stackalloc Move[pseudoLegalMoves.Length]; - int visitedMovesCounter = 0; + Span visitedMoves = stackalloc Move[pseudoLegalMoves.Length]; + int visitedMovesCounter = 0; - for (int moveIndex = 0; moveIndex < pseudoLegalMoves.Length; ++moveIndex) - { - // Incremental move sorting, inspired by https://github.com/jw1912/Chess-Challenge and suggested by toanth - // There's no need to sort all the moves since most of them don't get checked anyway - // So just find the first unsearched one with the best score and try it - for (int j = moveIndex + 1; j < pseudoLegalMoves.Length; j++) + for (int moveIndex = 0; moveIndex < pseudoLegalMoves.Length; ++moveIndex) { - if (scores[j] > scores[moveIndex]) + // Incremental move sorting, inspired by https://github.com/jw1912/Chess-Challenge and suggested by toanth + // There's no need to sort all the moves since most of them don't get checked anyway + // So just find the first unsearched one with the best score and try it + for (int j = moveIndex + 1; j < pseudoLegalMoves.Length; j++) { - (scores[moveIndex], scores[j], pseudoLegalMoves[moveIndex], pseudoLegalMoves[j]) = (scores[j], scores[moveIndex], pseudoLegalMoves[j], pseudoLegalMoves[moveIndex]); + if (scores[j] > scores[moveIndex]) + { + (scores[moveIndex], scores[j], pseudoLegalMoves[moveIndex], pseudoLegalMoves[j]) = (scores[j], scores[moveIndex], pseudoLegalMoves[j], pseudoLegalMoves[moveIndex]); + } } - } - var move = pseudoLegalMoves[moveIndex]; + var move = pseudoLegalMoves[moveIndex]; - var gameState = position.MakeMoveFast(move); + var gameState = position.MakeMoveFast(move); - if (!position.WasProduceByAValidMove()) - { - position.UnmakeMoveFast(move, gameState); - continue; - } + if (!position.WasProduceByAValidMove()) + { + position.UnmakeMoveFast(move, gameState); + continue; + } - visitedMoves[visitedMovesCounter] = move; + visitedMoves[visitedMovesCounter] = move; - ++_nodes; - isAnyMoveValid = true; - var isCapture = move.IsCapture(); + ++_nodes; + isAnyMoveValid = true; + var isCapture = move.IsCapture(); - PrintPreMove(position, ply, move); + PrintPreMove(position, ply, move); - // Before making a move - var oldHalfMovesWithoutCaptureOrPawnMove = Game.HalfMovesWithoutCaptureOrPawnMove; - var canBeRepetition = Game.Update50movesRule(move, isCapture); - Game.PositionHashHistory.Add(position.UniqueIdentifier); + // Before making a move + var oldHalfMovesWithoutCaptureOrPawnMove = Game.HalfMovesWithoutCaptureOrPawnMove; + var canBeRepetition = Game.Update50movesRule(move, isCapture); + Game.PositionHashHistory.Add(position.UniqueIdentifier); - int evaluation; - if (canBeRepetition && (Game.IsThreefoldRepetition() || Game.Is50MovesRepetition())) - { - evaluation = 0; + int evaluation; + if (canBeRepetition && (Game.IsThreefoldRepetition() || Game.Is50MovesRepetition())) + { + evaluation = 0; - // We don't need to evaluate further down to know it's a draw. - // Since we won't be evaluating further down, we need to clear the PV table because those moves there - // don't belong to this line and if this move were to beat alpha, they'd incorrectly copied to pv line. - Array.Clear(_pVTable, nextPvIndex, _pVTable.Length - nextPvIndex); - } - else if (pvNode && visitedMovesCounter == 0) - { - PrefetchTTEntry(); - evaluation = -NegaMax(depth - 1, ply + 1, -beta, -alpha); - } - else - { - if (!pvNode && !isInCheck - && scores[moveIndex] < EvaluationConstants.PromotionMoveScoreValue) // Quiet move + // We don't need to evaluate further down to know it's a draw. + // Since we won't be evaluating further down, we need to clear the PV table because those moves there + // don't belong to this line and if this move were to beat alpha, they'd incorrectly copied to pv line. + Array.Clear(_pVTable, nextPvIndex, _pVTable.Length - nextPvIndex); + } + else if (pvNode && visitedMovesCounter == 0) + { + PrefetchTTEntry(); + evaluation = -NegaMax(depth - 1, ply + 1, -beta, -alpha); + } + else { - // Late Move Pruning (LMP) - all quiet moves can be pruned - // after searching the first few given by the move ordering algorithm - if (depth <= Configuration.EngineSettings.LMP_MaxDepth - && moveIndex >= Configuration.EngineSettings.LMP_BaseMovesToTry + (Configuration.EngineSettings.LMP_MovesDepthMultiplier * depth)) // Based on formula suggested by Antares + if (!pvNode && !isInCheck + && scores[moveIndex] < EvaluationConstants.PromotionMoveScoreValue) // Quiet move { - // After making a move - Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; - Game.PositionHashHistory.RemoveAt(Game.PositionHashHistory.Count - 1); - position.UnmakeMoveFast(move, gameState); + // Late Move Pruning (LMP) - all quiet moves can be pruned + // after searching the first few given by the move ordering algorithm + if (depth <= Configuration.EngineSettings.LMP_MaxDepth + && moveIndex >= Configuration.EngineSettings.LMP_BaseMovesToTry + (Configuration.EngineSettings.LMP_MovesDepthMultiplier * depth)) // Based on formula suggested by Antares + { + // After making a move + Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; + Game.PositionHashHistory.RemoveAt(Game.PositionHashHistory.Count - 1); + position.UnmakeMoveFast(move, gameState); + + break; + } + + // Futility Pruning (FP) - all quiet moves can be pruned + // once it's considered that they don't have potential to raise alpha + if (visitedMovesCounter > 0 + //&& alpha < EvaluationConstants.PositiveCheckmateDetectionLimit + //&& beta > EvaluationConstants.NegativeCheckmateDetectionLimit + && depth <= Configuration.EngineSettings.FP_MaxDepth + && staticEval + Configuration.EngineSettings.FP_Margin + (Configuration.EngineSettings.FP_DepthScalingFactor * depth) <= alpha) + { + // After making a move + Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; + Game.PositionHashHistory.RemoveAt(Game.PositionHashHistory.Count - 1); + position.UnmakeMoveFast(move, gameState); - break; + break; + } } - // Futility Pruning (FP) - all quiet moves can be pruned - // once it's considered that they don't have potential to raise alpha - if (visitedMovesCounter > 0 - //&& alpha < EvaluationConstants.PositiveCheckmateDetectionLimit - //&& beta > EvaluationConstants.NegativeCheckmateDetectionLimit - && depth <= Configuration.EngineSettings.FP_MaxDepth - && staticEval + Configuration.EngineSettings.FP_Margin + (Configuration.EngineSettings.FP_DepthScalingFactor * depth) <= alpha) + PrefetchTTEntry(); + + int reduction = 0; + + // 🔍 Late Move Reduction (LMR) - search with reduced depth + // Impl. based on Ciekce (Stormphrax) and Martin (Motor) advice, and Stormphrax & Akimbo implementations + if (visitedMovesCounter >= (pvNode ? Configuration.EngineSettings.LMR_MinFullDepthSearchedMoves : Configuration.EngineSettings.LMR_MinFullDepthSearchedMoves - 1) + && depth >= Configuration.EngineSettings.LMR_MinDepth + && !isCapture) { - // After making a move - Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; - Game.PositionHashHistory.RemoveAt(Game.PositionHashHistory.Count - 1); - position.UnmakeMoveFast(move, gameState); + reduction = EvaluationConstants.LMRReductions[depth][visitedMovesCounter]; - break; - } - } + if (pvNode) + { + --reduction; + } + if (position.IsInCheck()) // i.e. move gives check + { + --reduction; + } - PrefetchTTEntry(); + if (ttBestMove != default && isCapture) + { + ++reduction; + } - int reduction = 0; + // -= history/(maxHistory/2) + reduction -= 2 * _quietHistory[move.Piece()][move.TargetSquare()] / Configuration.EngineSettings.History_MaxMoveValue; - // 🔍 Late Move Reduction (LMR) - search with reduced depth - // Impl. based on Ciekce (Stormphrax) and Martin (Motor) advice, and Stormphrax & Akimbo implementations - if (visitedMovesCounter >= (pvNode ? Configuration.EngineSettings.LMR_MinFullDepthSearchedMoves : Configuration.EngineSettings.LMR_MinFullDepthSearchedMoves - 1) - && depth >= Configuration.EngineSettings.LMR_MinDepth - && !isCapture) - { - reduction = EvaluationConstants.LMRReductions[depth][visitedMovesCounter]; + // Don't allow LMR to drop into qsearch or increase the depth + // depth - 1 - depth +2 = 1, min depth we want + reduction = Math.Clamp(reduction, 0, depth - 2); + } - if (pvNode) + // 🔍 Static Exchange Evaluation (SEE) reduction + // Bad captures are reduced more + if (!isInCheck + && scores[moveIndex] < EvaluationConstants.PromotionMoveScoreValue + && scores[moveIndex] >= EvaluationConstants.BadCaptureMoveBaseScoreValue) { - --reduction; + reduction += Configuration.EngineSettings.SEE_BadCaptureReduction; + reduction = Math.Clamp(reduction, 0, depth - 1); } - if (position.IsInCheck()) // i.e. move gives check + + // Search with reduced depth + evaluation = -NegaMax(depth - 1 - reduction, ply + 1, -alpha - 1, -alpha); + + // 🔍 Principal Variation Search (PVS) + if (evaluation > alpha && reduction > 0) { - --reduction; + // Optimistic search, validating that the rest of the moves are worse than bestmove. + // It should produce more cutoffs and therefore be faster. + // https://web.archive.org/web/20071030220825/http://www.brucemo.com/compchess/programming/pvs.htm + + // Search with full depth but narrowed score bandwidth + evaluation = -NegaMax(depth - 1, ply + 1, -alpha - 1, -alpha); } - if (ttBestMove != default && isCapture) + if (evaluation > alpha && evaluation < beta) { - ++reduction; + // PVS Hypothesis invalidated -> search with full depth and full score bandwidth + evaluation = -NegaMax(depth - 1, ply + 1, -beta, -alpha); } - - // -= history/(maxHistory/2) - reduction -= 2 * _quietHistory[move.Piece()][move.TargetSquare()] / Configuration.EngineSettings.History_MaxMoveValue; - - // Don't allow LMR to drop into qsearch or increase the depth - // depth - 1 - depth +2 = 1, min depth we want - reduction = Math.Clamp(reduction, 0, depth - 2); } - // 🔍 Static Exchange Evaluation (SEE) reduction - // Bad captures are reduced more - if (!isInCheck - && scores[moveIndex] < EvaluationConstants.PromotionMoveScoreValue - && scores[moveIndex] >= EvaluationConstants.BadCaptureMoveBaseScoreValue) - { - reduction += Configuration.EngineSettings.SEE_BadCaptureReduction; - reduction = Math.Clamp(reduction, 0, depth - 1); - } - - // Search with reduced depth - evaluation = -NegaMax(depth - 1 - reduction, ply + 1, -alpha - 1, -alpha); - - // 🔍 Principal Variation Search (PVS) - if (evaluation > alpha && reduction > 0) - { - // Optimistic search, validating that the rest of the moves are worse than bestmove. - // It should produce more cutoffs and therefore be faster. - // https://web.archive.org/web/20071030220825/http://www.brucemo.com/compchess/programming/pvs.htm + // After making a move + // Game.PositionHashHistory is update above + Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; + Game.PositionHashHistory.RemoveAt(Game.PositionHashHistory.Count - 1); + position.UnmakeMoveFast(move, gameState); - // Search with full depth but narrowed score bandwidth - evaluation = -NegaMax(depth - 1, ply + 1, -alpha - 1, -alpha); - } + PrintMove(position, ply, move, evaluation); - if (evaluation > alpha && evaluation < beta) + // Fail-hard beta-cutoff - refutation found, no need to keep searching this line + if (evaluation >= beta) { - // PVS Hypothesis invalidated -> search with full depth and full score bandwidth - evaluation = -NegaMax(depth - 1, ply + 1, -beta, -alpha); - } - } + PrintMessage($"Pruning: {move} is enough"); - // After making a move - // Game.PositionHashHistory is update above - Game.HalfMovesWithoutCaptureOrPawnMove = oldHalfMovesWithoutCaptureOrPawnMove; - Game.PositionHashHistory.RemoveAt(Game.PositionHashHistory.Count - 1); - position.UnmakeMoveFast(move, gameState); - - PrintMove(position, ply, move, evaluation); + if (isCapture) + { + var piece = move.Piece(); + var targetSquare = move.TargetSquare(); + var capturedPiece = move.CapturedPiece(); - // Fail-hard beta-cutoff - refutation found, no need to keep searching this line - if (evaluation >= beta) - { - PrintMessage($"Pruning: {move} is enough"); + _captureHistory[piece][targetSquare][capturedPiece] = ScoreHistoryMove( + _captureHistory[piece][targetSquare][capturedPiece], + EvaluationConstants.HistoryBonus[depth]); - if (isCapture) - { - var piece = move.Piece(); - var targetSquare = move.TargetSquare(); - var capturedPiece = move.CapturedPiece(); + // 🔍 Capture history penalty/malus + // When a capture fails high, penalize previous visited captures + for (int i = 0; i < visitedMovesCounter; ++i) + { + var visitedMove = visitedMoves[i]; - _captureHistory[piece][targetSquare][capturedPiece] = ScoreHistoryMove( - _captureHistory[piece][targetSquare][capturedPiece], - EvaluationConstants.HistoryBonus[depth]); + if (visitedMove.IsCapture()) + { + var visitedMovePiece = visitedMove.Piece(); + var visitedMoveTargetSquare = visitedMove.TargetSquare(); + var visitedMoveCapturedPiece = visitedMove.CapturedPiece(); - // 🔍 Capture history penalty/malus - // When a capture fails high, penalize previous visited captures - for (int i = 0; i < visitedMovesCounter; ++i) + _captureHistory[visitedMovePiece][visitedMoveTargetSquare][visitedMoveCapturedPiece] = ScoreHistoryMove( + _captureHistory[visitedMovePiece][visitedMoveTargetSquare][visitedMoveCapturedPiece], + -EvaluationConstants.HistoryBonus[depth]); + } + } + } + else { - var visitedMove = visitedMoves[i]; - - if (visitedMove.IsCapture()) + // 🔍 Quiet history moves + // Doing this only in beta cutoffs (instead of when eval > alpha) was suggested by Sirius author + var piece = move.Piece(); + var targetSquare = move.TargetSquare(); + + _quietHistory[piece][targetSquare] = ScoreHistoryMove( + _quietHistory[piece][targetSquare], + EvaluationConstants.HistoryBonus[depth]); + + // 🔍 Quiet history penalty/malus + // When a quiet move fails high, penalize previous visited quiet moves + for (int i = 0; i < visitedMovesCounter; ++i) { - var visitedMovePiece = visitedMove.Piece(); - var visitedMoveTargetSquare = visitedMove.TargetSquare(); - var visitedMoveCapturedPiece = visitedMove.CapturedPiece(); + var visitedMove = visitedMoves[i]; + + if (!visitedMove.IsCapture()) + { + var visitedMovePiece = visitedMove.Piece(); + var visitedMoveTargetSquare = visitedMove.TargetSquare(); - _captureHistory[visitedMovePiece][visitedMoveTargetSquare][visitedMoveCapturedPiece] = ScoreHistoryMove( - _captureHistory[visitedMovePiece][visitedMoveTargetSquare][visitedMoveCapturedPiece], - -EvaluationConstants.HistoryBonus[depth]); + _quietHistory[visitedMovePiece][visitedMoveTargetSquare] = ScoreHistoryMove( + _quietHistory[visitedMovePiece][visitedMoveTargetSquare], + -EvaluationConstants.HistoryBonus[depth]); + } } - } - } - else - { - // 🔍 Quiet history moves - // Doing this only in beta cutoffs (instead of when eval > alpha) was suggested by Sirius author - var piece = move.Piece(); - var targetSquare = move.TargetSquare(); - - _quietHistory[piece][targetSquare] = ScoreHistoryMove( - _quietHistory[piece][targetSquare], - EvaluationConstants.HistoryBonus[depth]); - - // 🔍 Quiet history penalty/malus - // When a quiet move fails high, penalize previous visited quiet moves - for (int i = 0; i < visitedMovesCounter; ++i) - { - var visitedMove = visitedMoves[i]; - if (!visitedMove.IsCapture()) + // 🔍 Killer moves + if (move.PromotedPiece() == default && move != _killerMoves[0][ply]) { - var visitedMovePiece = visitedMove.Piece(); - var visitedMoveTargetSquare = visitedMove.TargetSquare(); + if (move != _killerMoves[1][ply]) + { + _killerMoves[2][ply] = _killerMoves[1][ply]; + } - _quietHistory[visitedMovePiece][visitedMoveTargetSquare] = ScoreHistoryMove( - _quietHistory[visitedMovePiece][visitedMoveTargetSquare], - -EvaluationConstants.HistoryBonus[depth]); + _killerMoves[1][ply] = _killerMoves[0][ply]; + _killerMoves[0][ply] = move; } } - // 🔍 Killer moves - if (move.PromotedPiece() == default && move != _killerMoves[0][ply]) - { - if (move != _killerMoves[1][ply]) - { - _killerMoves[2][ply] = _killerMoves[1][ply]; - } + _tt.RecordHash(_ttMask, position, depth, ply, beta, NodeType.Beta, bestMove); - _killerMoves[1][ply] = _killerMoves[0][ply]; - _killerMoves[0][ply] = move; - } + return beta; // TODO return evaluation? } - _tt.RecordHash(_ttMask, position, depth, ply, beta, NodeType.Beta, bestMove); - - return beta; // TODO return evaluation? - } + if (evaluation > alpha) + { + alpha = evaluation; + bestMove = move; - if (evaluation > alpha) - { - alpha = evaluation; - bestMove = move; + _pVTable[pvIndex] = move; + CopyPVTableMoves(pvIndex + 1, nextPvIndex, Configuration.EngineSettings.MaxDepth - ply - 1); - _pVTable[pvIndex] = move; - CopyPVTableMoves(pvIndex + 1, nextPvIndex, Configuration.EngineSettings.MaxDepth - ply - 1); + nodeType = NodeType.Exact; + } - nodeType = NodeType.Exact; + ++visitedMovesCounter; } - - ++visitedMovesCounter; } if (bestMove is null && !isAnyMoveValid) From 6fabd6eb63d7d0dd4a28182485da80fa679bb0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Sat, 6 Jul 2024 13:24:44 +0200 Subject: [PATCH 5/7] Move stuff around to keep same high-level logic in move loop, optimize MakeMoveFast method --- src/Lynx/Model/Position.cs | 11 +++++++++++ src/Lynx/Perft.cs | 24 ++++++++++++++++++------ src/Lynx/Search/NegaMax.cs | 14 +++++++------- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index 6e6b7ebdb..059b3e7e6 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -244,6 +244,17 @@ public GameState MakeMoveFast(Move move) return MakeQuietMove(move); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public GameState MakeMoveFast(Move move, bool isNoisy) + { + if (isNoisy) + { + return MakeMove(move); + } + + return MakeQuietMove(move); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void UnmakeMoveFast(Move move, GameState gameState) { diff --git a/src/Lynx/Perft.cs b/src/Lynx/Perft.cs index fe1bef202..efecd7c91 100644 --- a/src/Lynx/Perft.cs +++ b/src/Lynx/Perft.cs @@ -40,16 +40,28 @@ internal static long ResultsImpl(Position position, int depth, long nodes) { if (depth != 0) { - Span moves = stackalloc Move[Constants.MaxNumberOfPossibleMovesInAPosition]; - foreach (var move in MoveGenerator.GenerateAllMoves(position, moves)) + Move[] moves = new Move[Constants.MaxNumberOfPossibleMovesInAPosition]; + var pseudoLegalMovesStaged = MoveGenerator.GenerateAllMovesStaged(position, moves); + var enumerator = pseudoLegalMovesStaged.GetEnumerator(); + + bool enumeratorHasNext = true; + var lowerLimit = 0; + while (enumeratorHasNext) { - var state = position.MakeMoveFast(move); + enumeratorHasNext = enumerator.MoveNext(); + var pseudoLegalMoves = moves[lowerLimit..enumerator.Current]; + lowerLimit = enumerator.Current; - if (position.WasProduceByAValidMove()) + foreach (var move in pseudoLegalMoves) { - nodes = ResultsImpl(position, depth - 1, nodes); + var state = position.MakeMoveFast(move, enumeratorHasNext); + + if (position.WasProduceByAValidMove()) + { + nodes = ResultsImpl(position, depth - 1, nodes); + } + position.UnmakeMoveFast(move, state); } - position.UnmakeMoveFast(move, state); } return nodes; diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index 4f30d3834..71a08b83e 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -174,13 +174,15 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM Move? bestMove = null; bool isAnyMoveValid = false; + Span visitedMoves = stackalloc Move[moves.Length]; + int visitedMovesCounter = 0; + bool enumeratorHasNext = true; var lowerLimit = 0; while (enumeratorHasNext) { enumeratorHasNext = enumerator.MoveNext(); - var pseudoLegalMoves = moves[lowerLimit..enumerator.Current]; - lowerLimit = enumerator.Current; + var pseudoLegalMoves = moves[lowerLimit..enumerator.Current]; // Copy can be allowed using lower and upper limits Span scores = stackalloc int[pseudoLegalMoves.Length]; if (_isFollowingPV) { @@ -204,10 +206,6 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM } } - - Span visitedMoves = stackalloc Move[pseudoLegalMoves.Length]; - int visitedMovesCounter = 0; - for (int moveIndex = 0; moveIndex < pseudoLegalMoves.Length; ++moveIndex) { // Incremental move sorting, inspired by https://github.com/jw1912/Chess-Challenge and suggested by toanth @@ -223,7 +221,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM var move = pseudoLegalMoves[moveIndex]; - var gameState = position.MakeMoveFast(move); + var gameState = position.MakeMoveFast(move, enumeratorHasNext); if (!position.WasProduceByAValidMove()) { @@ -459,6 +457,8 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM ++visitedMovesCounter; } + + lowerLimit = enumerator.Current; } if (bestMove is null && !isAnyMoveValid) From fd5df14a9f6fc5a77a827381761d76a8ab10968b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Sat, 6 Jul 2024 13:33:51 +0200 Subject: [PATCH 6/7] Generate captires before castling moves --- src/Lynx/MoveGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lynx/MoveGenerator.cs b/src/Lynx/MoveGenerator.cs index 053b6965c..07dba8405 100644 --- a/src/Lynx/MoveGenerator.cs +++ b/src/Lynx/MoveGenerator.cs @@ -99,12 +99,12 @@ public static IEnumerable GenerateAllMovesStaged(Position position, Move[] var offset = Utils.PieceOffset(position.Side); GeneratePawnCapturesAndPromotions(ref localIndex, movePool, position, offset); - GenerateCastlingMoves(ref localIndex, movePool, position); GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.K + offset, position, offset); GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.N + offset, position, offset); GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.B + offset, position, offset); GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.R + offset, position, offset); GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.Q + offset, position, offset); + GenerateCastlingMoves(ref localIndex, movePool, position); yield return localIndex; From 9297eef57c51d21446a09c80fb1a5e820429990c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Sat, 6 Jul 2024 15:44:03 +0200 Subject: [PATCH 7/7] Generate all the moves at the same time but return them in stages --- src/Lynx/MoveGenerator.cs | 155 ++++++++++++++++++++++++++++++++----- src/Lynx/Perft.cs | 14 ++-- src/Lynx/Search/NegaMax.cs | 12 ++- 3 files changed, 147 insertions(+), 34 deletions(-) diff --git a/src/Lynx/MoveGenerator.cs b/src/Lynx/MoveGenerator.cs index 07dba8405..4484fa0fb 100644 --- a/src/Lynx/MoveGenerator.cs +++ b/src/Lynx/MoveGenerator.cs @@ -89,33 +89,27 @@ public static Span GenerateAllMoves(Position position, Span movePool /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEnumerable GenerateAllMovesStaged(Position position, Move[] movePool) + public static IEnumerable GenerateAllMovesStaged(Position position, Move[] movePool) { //https://antao-almada.medium.com/how-to-use-span-t-and-memory-t-c0b126aae652 Debug.Assert(position.Side != Side.Both); - int localIndex = 0; + int captureIndex = 0; + int quietIndex = movePool.Length - 1; var offset = Utils.PieceOffset(position.Side); - GeneratePawnCapturesAndPromotions(ref localIndex, movePool, position, offset); - GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.K + offset, position, offset); - GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.N + offset, position, offset); - GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.B + offset, position, offset); - GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.R + offset, position, offset); - GeneratePieceCaptures(ref localIndex, movePool, (int)Piece.Q + offset, position, offset); - GenerateCastlingMoves(ref localIndex, movePool, position); + GenerateAllPawnMoves(ref captureIndex, ref quietIndex, movePool, position, offset); + GenerateCastlingMoves(ref captureIndex, movePool, position); + GenerateAllPieceMoves(ref captureIndex, ref quietIndex, movePool, (int)Piece.K + offset, position, offset); + GenerateAllPieceMoves(ref captureIndex, ref quietIndex, movePool, (int)Piece.N + offset, position, offset); + GenerateAllPieceMoves(ref captureIndex, ref quietIndex, movePool, (int)Piece.B + offset, position, offset); + GenerateAllPieceMoves(ref captureIndex, ref quietIndex, movePool, (int)Piece.R + offset, position, offset); + GenerateAllPieceMoves(ref captureIndex, ref quietIndex, movePool, (int)Piece.Q + offset, position, offset); - yield return localIndex; + yield return ..captureIndex; - GeneratePawnQuietMoves(ref localIndex, movePool, position, offset); - GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.K + offset, position, offset); - GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.N + offset, position, offset); - GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.B + offset, position, offset); - GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.R + offset, position, offset); - GeneratePieceQuietMoves(ref localIndex, movePool, (int)Piece.Q + offset, position, offset); - - yield return localIndex; + yield return (quietIndex + 1)..; } /// @@ -256,6 +250,92 @@ internal static void GenerateAllPawnMoves(ref int localIndex, Span movePoo } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void GenerateAllPawnMoves(ref int captureIndex, ref int quietIndex, Span movePool, Position position, int offset) + { + int sourceSquare, targetSquare; + + var piece = (int)Piece.P + offset; + var pawnPush = +8 - ((int)position.Side * 16); // position.Side == Side.White ? -8 : +8 + int oppositeSide = Utils.OppositeSide(position.Side); // position.Side == Side.White ? (int)Side.Black : (int)Side.White + var bitboard = position.PieceBitBoards[piece]; + + while (bitboard != default) + { + sourceSquare = bitboard.GetLS1BIndex(); + bitboard.ResetLS1B(); + + var sourceRank = (sourceSquare >> 3) + 1; + +#if DEBUG + if (sourceRank == 1 || sourceRank == 8) + { + _logger.Warn("There's a non-promoted {0} pawn in rank {1}", position.Side, sourceRank); + continue; + } +#endif + + // Pawn pushes + var singlePushSquare = sourceSquare + pawnPush; + if (!position.OccupancyBitBoards[2].GetBit(singlePushSquare)) + { + // Single pawn push + var targetRank = (singlePushSquare >> 3) + 1; + if (targetRank == 1 || targetRank == 8) // Promotion + { + movePool[captureIndex++] = MoveExtensions.EncodePromotion(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.Q + offset); + movePool[captureIndex++] = MoveExtensions.EncodePromotion(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.R + offset); + movePool[captureIndex++] = MoveExtensions.EncodePromotion(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.N + offset); + movePool[captureIndex++] = MoveExtensions.EncodePromotion(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.B + offset); + } + else + { + movePool[quietIndex--] = MoveExtensions.Encode(sourceSquare, singlePushSquare, piece); + } + + // Double pawn push + // Inside of the if because singlePush square cannot be occupied either + var doublePushSquare = sourceSquare + (2 * pawnPush); + if (!position.OccupancyBitBoards[2].GetBit(doublePushSquare) + && ((sourceRank == 2 && position.Side == Side.Black) || (sourceRank == 7 && position.Side == Side.White))) + { + movePool[quietIndex--] = MoveExtensions.EncodeDoublePawnPush(sourceSquare, doublePushSquare, piece); + } + } + + var attacks = Attacks.PawnAttacks[(int)position.Side][sourceSquare]; + + // En passant + if (position.EnPassant != BoardSquare.noSquare && attacks.GetBit(position.EnPassant)) + // We assume that position.OccupancyBitBoards[oppositeOccupancy].GetBit(targetSquare + singlePush) == true + { + movePool[captureIndex++] = MoveExtensions.EncodeEnPassant(sourceSquare, (int)position.EnPassant, piece, capturedPiece: (int)Piece.p - offset); + } + + // Captures + var attackedSquares = attacks & position.OccupancyBitBoards[oppositeSide]; + while (attackedSquares != default) + { + targetSquare = attackedSquares.GetLS1BIndex(); + attackedSquares.ResetLS1B(); + var capturedPiece = FindCapturedPiece(position, offset, targetSquare); + + var targetRank = (targetSquare >> 3) + 1; + if (targetRank == 1 || targetRank == 8) // Capture with promotion + { + movePool[captureIndex++] = MoveExtensions.EncodePromotion(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.Q + offset, capturedPiece: capturedPiece); + movePool[captureIndex++] = MoveExtensions.EncodePromotion(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.R + offset, capturedPiece: capturedPiece); + movePool[captureIndex++] = MoveExtensions.EncodePromotion(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.N + offset, capturedPiece: capturedPiece); + movePool[captureIndex++] = MoveExtensions.EncodePromotion(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.B + offset, capturedPiece: capturedPiece); + } + else + { + movePool[captureIndex++] = MoveExtensions.EncodeCapture(sourceSquare, targetSquare, piece, capturedPiece: capturedPiece); + } + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void GeneratePawnCapturesAndPromotions(ref int localIndex, Span movePool, Position position, int offset) { @@ -492,6 +572,45 @@ internal static void GenerateAllPieceMoves(ref int localIndex, Span movePo } } + /// + /// Generate Knight, Bishop, Rook and Queen moves + /// + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void GenerateAllPieceMoves(ref int captureIndex, ref int quietIndex, Span movePool, int piece, Position position, int offset) + { + var bitboard = position.PieceBitBoards[piece]; + int sourceSquare, targetSquare; + + while (bitboard != default) + { + sourceSquare = bitboard.GetLS1BIndex(); + bitboard.ResetLS1B(); + + var attacks = _pieceAttacks[piece](sourceSquare, position.OccupancyBitBoards[(int)Side.Both]) + & ~position.OccupancyBitBoards[(int)position.Side]; + + while (attacks != default) + { + targetSquare = attacks.GetLS1BIndex(); + attacks.ResetLS1B(); + + if (position.OccupancyBitBoards[(int)Side.Both].GetBit(targetSquare)) + { + var capturedPiece = FindCapturedPiece(position, offset, targetSquare); + movePool[captureIndex++] = MoveExtensions.EncodeCapture(sourceSquare, targetSquare, piece, capturedPiece: capturedPiece); + } + else + { + movePool[quietIndex--] = MoveExtensions.Encode(sourceSquare, targetSquare, piece); + } + } + } + } + /// /// Generate Knight, Bishop, Rook and Queen capture moves /// diff --git a/src/Lynx/Perft.cs b/src/Lynx/Perft.cs index efecd7c91..ca4478763 100644 --- a/src/Lynx/Perft.cs +++ b/src/Lynx/Perft.cs @@ -44,17 +44,12 @@ internal static long ResultsImpl(Position position, int depth, long nodes) var pseudoLegalMovesStaged = MoveGenerator.GenerateAllMovesStaged(position, moves); var enumerator = pseudoLegalMovesStaged.GetEnumerator(); - bool enumeratorHasNext = true; - var lowerLimit = 0; - while (enumeratorHasNext) + bool isCaptureStage = true; + while (enumerator.MoveNext()) { - enumeratorHasNext = enumerator.MoveNext(); - var pseudoLegalMoves = moves[lowerLimit..enumerator.Current]; - lowerLimit = enumerator.Current; - - foreach (var move in pseudoLegalMoves) + foreach (var move in moves[enumerator.Current]) { - var state = position.MakeMoveFast(move, enumeratorHasNext); + var state = position.MakeMoveFast(move, isCaptureStage); if (position.WasProduceByAValidMove()) { @@ -62,6 +57,7 @@ internal static long ResultsImpl(Position position, int depth, long nodes) } position.UnmakeMoveFast(move, state); } + isCaptureStage = false; } return nodes; diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index 71a08b83e..c2c497a5a 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -177,12 +177,10 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM Span visitedMoves = stackalloc Move[moves.Length]; int visitedMovesCounter = 0; - bool enumeratorHasNext = true; - var lowerLimit = 0; - while (enumeratorHasNext) + bool isCaptureStage = true; + while (enumerator.MoveNext()) { - enumeratorHasNext = enumerator.MoveNext(); - var pseudoLegalMoves = moves[lowerLimit..enumerator.Current]; // Copy can be allowed using lower and upper limits + var pseudoLegalMoves = moves[enumerator.Current]; // Copy can be allowed using lower and upper limits Span scores = stackalloc int[pseudoLegalMoves.Length]; if (_isFollowingPV) { @@ -221,7 +219,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM var move = pseudoLegalMoves[moveIndex]; - var gameState = position.MakeMoveFast(move, enumeratorHasNext); + var gameState = position.MakeMoveFast(move, isCaptureStage); if (!position.WasProduceByAValidMove()) { @@ -458,7 +456,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM ++visitedMovesCounter; } - lowerLimit = enumerator.Current; + isCaptureStage = false; } if (bestMove is null && !isAnyMoveValid)