From f1f9e5a7ba1b59b166a6c0fe6c88b3495a6388ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Sat, 19 Aug 2023 01:35:25 +0200 Subject: [PATCH] Add `MoveGenerator.CanGenerateAtLeastAValidMove` to check move validity on the fly instead of generating all moves first We use this in NegaMax to see if we can trigger QSearch and to validate the outcome in case no best move is found during QSearch --- src/Lynx/Model/Position.cs | 3 + src/Lynx/MoveGenerator.cs | 246 +++++++++++++++++++++++++++++++++++++ src/Lynx/Search/NegaMax.cs | 35 ++---- 3 files changed, 256 insertions(+), 28 deletions(-) diff --git a/src/Lynx/Model/Position.cs b/src/Lynx/Model/Position.cs index c09019798..d07211208 100644 --- a/src/Lynx/Model/Position.cs +++ b/src/Lynx/Model/Position.cs @@ -593,6 +593,9 @@ private string CalculateFEN() [MethodImpl(MethodImplOptions.AggressiveInlining)] public IEnumerable AllCapturesMoves(Move[]? movePool = null) => MoveGenerator.GenerateAllMoves(this, movePool, capturesOnly: true); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasValidMoves() => MoveGenerator.CanGenerateAtLeastAValidMove(this); + public int CountPieces() => PieceBitBoards.Sum(b => b.CountBits()); /// diff --git a/src/Lynx/MoveGenerator.cs b/src/Lynx/MoveGenerator.cs index 99f63de92..d6384aed2 100644 --- a/src/Lynx/MoveGenerator.cs +++ b/src/Lynx/MoveGenerator.cs @@ -260,6 +260,252 @@ internal static void GeneratePieceMoves(ref int localIndex, Move[] movePool, int } } + /// + /// Generates all psuedo-legal moves from , ordered by + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanGenerateAtLeastAValidMove(Position position) + { +#if DEBUG + if (position.Side == Side.Both) + { + return false; + } +#endif + + var offset = Utils.PieceOffset(position.Side); + + return IsAnyPawnMoveValid(position, offset) + || IsAnyPieceMoveValid((int)Piece.K + offset, position) + || 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, offset); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAnyPawnMoveValid(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 + { + if (IsValidMove(position, MoveExtensions.Encode(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.Q + offset)) + || IsValidMove(position, MoveExtensions.Encode(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.R + offset)) + || IsValidMove(position, MoveExtensions.Encode(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.N + offset)) + || IsValidMove(position, MoveExtensions.Encode(sourceSquare, singlePushSquare, piece, promotedPiece: (int)Piece.B + offset))) + { + return true; + } + } + else if (IsValidMove(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)) + && IsValidMove(position, MoveExtensions.Encode(sourceSquare, doublePushSquare, piece, isDoublePawnPush: TRUE))) + { + return true; + } + } + + 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 + && IsValidMove(position, MoveExtensions.Encode(sourceSquare, (int)position.EnPassant, piece, isCapture: TRUE, isEnPassant: TRUE))) + { + return true; + } + + // Captures + var attackedSquares = attacks & position.OccupancyBitBoards[oppositeSide]; + while (attackedSquares != default) + { + targetSquare = attackedSquares.GetLS1BIndex(); + attackedSquares.ResetLS1B(); + + var targetRank = (targetSquare >> 3) + 1; + if (targetRank == 1 || targetRank == 8) // Capture with promotion + { + if (IsValidMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.Q + offset, isCapture: TRUE)) + || IsValidMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.R + offset, isCapture: TRUE)) + || IsValidMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.N + offset, isCapture: TRUE)) + || IsValidMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece, promotedPiece: (int)Piece.B + offset, isCapture: TRUE))) + { + return true; + } + } + else if (IsValidMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece, isCapture: TRUE))) + { + return true; + } + } + } + + 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 IsAnyCastlingMoveValid(Position position, int offset) + { + var piece = (int)Piece.K + offset; + var oppositeSide = (Side)Utils.OppositeSide(position.Side); + + int sourceSquare = position.PieceBitBoards[piece].GetLS1BIndex(); // There's for sure only one + + // Castles + if (position.Castle != default) + { + if (position.Side == Side.White) + { + bool ise1Attacked = Attacks.IsSquaredAttackedBySide((int)BoardSquare.e1, position, oppositeSide); + if (((position.Castle & (int)CastlingRights.WK) != default) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.f1) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.g1) + && !ise1Attacked + && !Attacks.IsSquaredAttackedBySide((int)BoardSquare.f1, position, oppositeSide) + && !Attacks.IsSquaredAttackedBySide((int)BoardSquare.g1, position, oppositeSide) + && IsValidMove(position, MoveExtensions.Encode(sourceSquare, Constants.WhiteShortCastleKingSquare, piece, isShortCastle: TRUE))) + { + return true; + } + + if (((position.Castle & (int)CastlingRights.WQ) != default) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.d1) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.c1) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.b1) + && !ise1Attacked + && !Attacks.IsSquaredAttackedBySide((int)BoardSquare.d1, position, oppositeSide) + && !Attacks.IsSquaredAttackedBySide((int)BoardSquare.c1, position, oppositeSide) + && IsValidMove(position, MoveExtensions.Encode(sourceSquare, Constants.WhiteLongCastleKingSquare, piece, isLongCastle: TRUE))) + { + return true; + } + } + else + { + bool ise8Attacked = Attacks.IsSquaredAttackedBySide((int)BoardSquare.e8, position, oppositeSide); + if (((position.Castle & (int)CastlingRights.BK) != default) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.f8) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.g8) + && !ise8Attacked + && !Attacks.IsSquaredAttackedBySide((int)BoardSquare.f8, position, oppositeSide) + && !Attacks.IsSquaredAttackedBySide((int)BoardSquare.g8, position, oppositeSide) + && IsValidMove(position, MoveExtensions.Encode(sourceSquare, Constants.BlackShortCastleKingSquare, piece, isShortCastle: TRUE))) + { + return true; + } + + if (((position.Castle & (int)CastlingRights.BQ) != default) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.d8) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.c8) + && !position.OccupancyBitBoards[(int)Side.Both].GetBit(BoardSquare.b8) + && !ise8Attacked + && !Attacks.IsSquaredAttackedBySide((int)BoardSquare.d8, position, oppositeSide) + && !Attacks.IsSquaredAttackedBySide((int)BoardSquare.c8, position, oppositeSide) + && IsValidMove(position, MoveExtensions.Encode(sourceSquare, Constants.BlackLongCastleKingSquare, piece, isLongCastle: TRUE))) + { + return true; + } + } + } + + return false; + } + + /// + /// Generate Knight, Bishop, Rook and Queen moves + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAnyPieceMoveValid(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)position.Side]; + + while (attacks != default) + { + targetSquare = attacks.GetLS1BIndex(); + attacks.ResetLS1B(); + + if (position.OccupancyBitBoards[(int)Side.Both].GetBit(targetSquare) + && IsValidMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece, isCapture: TRUE))) + { + return true; + } + else if (IsValidMove(position, MoveExtensions.Encode(sourceSquare, targetSquare, piece))) + { + return true; + } + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidMove(Position position, Move move) + { + var gameState = position.MakeMove(move); + bool result = position.WasProduceByAValidMove(); + position.UnmakeMove(move, gameState); + + return result; + } + #region Only for reference, but unused /// diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index feeefcc6c..e207ec0ac 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -63,16 +63,9 @@ private int NegaMax(int minDepth, int targetDepth, int ply, int alpha, int beta, } if (ply >= targetDepth) { - foreach (var candidateMove in position.AllPossibleMoves(Game.MovePool)) + if (position.HasValidMoves()) { - var gameState = position.MakeMove(candidateMove); - bool isValid = position.WasProduceByAValidMove(); - position.UnmakeMove(candidateMove, gameState); - - if (isValid) - { - return QuiescenceSearch(ply, alpha, beta); - } + return QuiescenceSearch(ply, alpha, beta); } var finalPositionEvaluation = Position.EvaluateFinalPosition(ply, isInCheck); @@ -319,7 +312,8 @@ public int QuiescenceSearch(int ply, int alpha, int beta) var generatedMoves = position.AllCapturesMoves(Game.MovePool); if (!generatedMoves.Any()) { - return staticEvaluation; // TODO check if in check or drawn position + // Checking if final position first: https://github.com/lynx-chess/Lynx/pull/358 + return staticEvaluation; } var movesToEvaluate = generatedMoves.OrderByDescending(move => ScoreMove(move, ply, false)); @@ -390,24 +384,9 @@ public int QuiescenceSearch(int ply, int alpha, int beta) if (bestMove is null) { - if (isAnyMoveValid) - { - return alpha; - } - - foreach (var move in position.AllPossibleMoves(Game.MovePool)) - { - var gameState = position.MakeMove(move); - bool isValid = position.WasProduceByAValidMove(); - position.UnmakeMove(move, gameState); - - if (isValid) - { - return alpha; - } - } - - return Position.EvaluateFinalPosition(ply, position.IsInCheck()); + return isAnyMoveValid || position.HasValidMoves() + ? alpha + : Position.EvaluateFinalPosition(ply, position.IsInCheck()); } // Node fails low