/// <summary> /// Returns the estimated material exchange value of the given move on the /// given position as determined by static analysis. /// </summary> /// <param name="position">The position the move is to be played on.</param> /// <param name="move">The move to evaluate.</param> /// <returns>The estimated material exchange value of the move.</returns> private static Int32 EvaluateStaticExchange(Position position, Int32 move) { Int32 from = Move.From(move); Int32 to = Move.To(move); Int32 piece = Move.Piece(move); Int32 capture = Move.Capture(move); position.Bitboard[piece] ^= 1UL << from; position.OccupiedBitboard ^= 1UL << from; position.Square[to] = piece; Int32 value = 0; if (Move.IsPromotion(move)) { Int32 promotion = Move.Special(move); position.Square[to] = promotion; value += PieceValue[promotion] - PieceValue[Piece.Pawn]; } value += PieceValue[capture] - EvaluateStaticExchange(position, 1 - position.SideToMove, to); position.Bitboard[piece] ^= 1UL << from; position.OccupiedBitboard ^= 1UL << from; position.Square[to] = capture; return(value); }
/// <summary> /// Unmakes the given move on the visual position. /// </summary> /// <param name="move">The move to unmake.</param> public static void Unmake(Int32 move) { Int32 from = Move.From(move); Int32 to = Move.To(move); Point initial = new Point(Position.File(to) * SquareWidth, Position.Rank(to) * SquareWidth); Point final = new Point(Position.File(from) * SquareWidth, Position.Rank(from) * SquareWidth); // Remove captured pieces. lock (PiecesLock) _pieces.Add(new VisualPiece( Move.Capture(move), Position.File(to) * SquareWidth, Position.Rank(to) * SquareWidth)); // Perform special moves. switch (Move.Special(move) & Piece.Mask) { case Piece.King: Point rookInitial = new Point((Position.File(to) / 2 + 2) * SquareWidth, Position.Rank(to) * SquareWidth); Point rookFinal = new Point(7 * (Position.File(to) - 2) / 4 * SquareWidth, Position.Rank(to) * SquareWidth); Animate(move, rookInitial, rookFinal); break; case Piece.Pawn: lock (PiecesLock) _pieces.Add(new VisualPiece( Move.Special(move), Position.File(to) * SquareWidth, Position.Rank(from) * SquareWidth)); break; } Animate(move, initial, final); }
/// <summary> /// Returns a value for the given move that indicates its immediate threat. /// Non-capture moves have a default value of zero, while captures have a /// value that is the ratio of the captured piece to the moving piece. Pawns /// promoting to queen are given an additional increase in value. /// </summary> /// <param name="move">The move to consider.</param> /// <returns>A value for the given move that is useful for move ordering.</returns> private Single MoveOrderingValue(Int32 move) { Single value = PieceValue[Move.Capture(move)] / (Single)PieceValue[Move.Piece(move)]; if (Move.IsQueenPromotion(move)) { value += QueenPromotionMoveValue; } return(value); }
/// <summary> /// Returns the dynamic value of the position as determined by a recursive /// search to the given depth. This implements the main search algorithm. /// </summary> /// <param name="position">The position to search on.</param> /// <param name="depth">The depth to search to.</param> /// <param name="ply">The number of plies from the root position.</param> /// <param name="alpha">The lower bound on the value of the best move.</param> /// <param name="beta">The upper bound on the value of the best move.</param> /// <param name="inCheck">Whether the side to play is in check.</param> /// <param name="allowNull">Whether a null move is permitted.</param> /// <returns>The value of the termination position given optimal play.</returns> private Int32 Search(Position position, Int32 depth, Int32 ply, Int32 alpha, Int32 beta, Boolean inCheck, Boolean allowNull = true) { // Check whether to enter quiescence search and initialize pv length. _pvLength[ply] = 0; if (depth <= 0 && !inCheck) { return(Quiescence(position, ply, alpha, beta)); } // Check for time extension and search termination. This is done once for // every given number of nodes for efficency. if (++_totalNodes > _referenceNodes) { _referenceNodes += NodeResolution; // Apply loss time extension. The value of the best move for the current // root position is compared with the value of the previous root position. // If there is a large loss, a time extension is given. Int32 loss = _finalAlpha - _rootAlpha; if (loss >= TimeControlsLossResolution) { Int32 index = Math.Min(loss / TimeControlsLossResolution, TimeControlsLossExtension.Length - 1); TryTimeExtension(TimeControlsLossThreshold, TimeControlsLossExtension[index]); } if (_stopwatch.ElapsedMilliseconds >= _timeLimit + _timeExtension || _totalNodes >= Restrictions.Nodes) { _abortSearch = true; } } if (_abortSearch) { return(Infinity); } // Perform draw detection. Int32 drawValue = ((ply & 1) == 0) ? DrawValue : -DrawValue; Int32 drawRepetitions = (ply > 2) ? 2 : 3; if (position.FiftyMovesClock >= 100 || position.InsufficientMaterial() || position.HasRepeated(drawRepetitions)) { return(drawValue); } // Perform mate distance pruning. Int32 mateAlpha = Math.Max(alpha, -(CheckmateValue - ply)); Int32 mateBeta = Math.Min(beta, CheckmateValue - (ply + 1)); if (mateAlpha >= mateBeta) { return(mateAlpha); } // Perform hash probe. _hashProbes++; Int32 hashMove = Move.Invalid; if (_table.TryProbe(position.ZobristKey, out HashEntry hashEntry)) { if (hashEntry.Depth >= depth) { Int32 hashType = hashEntry.Type; Int32 hashValue = hashEntry.GetValue(ply); if ((hashType == HashEntry.Beta && hashValue >= beta) || (hashType == HashEntry.Alpha && hashValue <= alpha)) { _hashCutoffs++; return(hashValue); } } hashMove = hashEntry.Move; } Int32 colour = position.SideToMove; // Apply null move heuristic. if (allowNull && !inCheck && position.Bitboard[colour] != (position.Bitboard[colour | Piece.King] | position.Bitboard[colour | Piece.Pawn])) { position.MakeNull(); Int32 reduction = NullMoveReduction + (depth >= NullMoveAggressiveDepth ? depth / NullMoveAggressiveDivisor : 0); Int32 value = -Search(position, depth - 1 - reduction, ply + 1, -beta, -beta + 1, false, false); position.UnmakeNull(); if (value >= beta) { return(value); } } // Generate legal moves and perform basic move ordering. Int32[] moves = _generatedMoves[ply]; Int32 movesCount = position.LegalMoves(moves); if (movesCount == 0) { return(inCheck ? -(CheckmateValue - ply) : drawValue); } for (Int32 i = 0; i < movesCount; i++) { _moveValues[i] = MoveOrderingValue(moves[i]); } // Apply single reply and check extensions. if (movesCount == 1 || inCheck) { depth++; } // Perform killer move ordering. _killerMoveChecks++; bool killerMoveFound = false; for (Int32 slot = 0; slot < KillerMovesAllocation; slot++) { Int32 killerMove = _killerMoves[ply][slot]; for (Int32 i = 0; i < movesCount; i++) { if (moves[i] == killerMove) { _moveValues[i] = KillerMoveValue + slot * KillerMoveSlotValue; if (!killerMoveFound) { _killerMoveMatches++; } killerMoveFound = true; break; } } } // Perform hash move ordering. _hashMoveChecks++; if (hashMove != Move.Invalid) { for (Int32 i = 0; i < movesCount; i++) { if (moves[i] == hashMove) { _moveValues[i] = HashMoveValue; _hashMoveMatches++; break; } } } // Check for futility pruning activation. Boolean futileNode = false; Int32 futilityValue = 0; if (depth < FutilityMargin.Length && !inCheck) { futilityValue = Evaluate(position) + FutilityMargin[depth]; futileNode = futilityValue <= alpha; } // Sort the moves based on their ordering values and initialize variables. Int32 irreducibleMoves = Sort(moves, _moveValues, movesCount); UInt64 preventionBitboard = PassedPawnPreventionBitboard(position); Int32 bestType = HashEntry.Alpha; Int32 bestMove = moves[0]; // Go through the move list. for (Int32 i = 0; i < movesCount; i++) { _movesSearched++; Int32 move = moves[i]; Boolean causesCheck = position.CausesCheck(move); Boolean dangerous = inCheck || causesCheck || alpha < -NearCheckmateValue || IsDangerousPawnAdvance(move, preventionBitboard); Boolean reducible = i + 1 > irreducibleMoves; // Perform futility pruning. if (futileNode && !dangerous && futilityValue + PieceValue[Move.Capture(move)] <= alpha) { _futileMoves++; continue; } // Make the move and initialize its value. position.Make(move); Int32 value = alpha + 1; // Perform late move reductions. if (reducible && !dangerous) { value = -Search(position, depth - 1 - LateMoveReduction, ply + 1, -alpha - 1, -alpha, causesCheck); } // Perform principal variation search. else if (i > 0) { value = -Search(position, depth - 1, ply + 1, -alpha - 1, -alpha, causesCheck); } // Perform a full search. if (value > alpha) { value = -Search(position, depth - 1, ply + 1, -beta, -alpha, causesCheck); } // Unmake the move and check for search termination. position.Unmake(move); if (_abortSearch) { return(Infinity); } // Check for upper bound cutoff. if (value >= beta) { _table.Store(new HashEntry(position, depth, ply, move, value, HashEntry.Beta)); if (reducible) { for (Int32 j = _killerMoves[ply].Length - 2; j >= 0; j--) { _killerMoves[ply][j + 1] = _killerMoves[ply][j]; } _killerMoves[ply][0] = move; } return(value); } // Check for lower bound improvement. if (value > alpha) { alpha = value; bestMove = move; bestType = HashEntry.Exact; // Collect the principal variation. _pvMoves[ply][0] = move; for (Int32 j = 0; j < _pvLength[ply + 1]; j++) { _pvMoves[ply][j + 1] = _pvMoves[ply + 1][j]; } _pvLength[ply] = _pvLength[ply + 1] + 1; } } // Store the results in the hash table and return the lower bound of the // value of the position. _table.Store(new HashEntry(position, depth, ply, bestMove, alpha, bestType)); return(alpha); }
/// <summary> /// Unmakes the given move from the position. /// </summary> /// <param name="move">The move to unmake.</param> public void Unmake(Int32 move) { Int32 from = Move.From(move); Int32 to = Move.To(move); Int32 piece = Move.Piece(move); Int32 capture = Move.Capture(move); Int32 special = Move.Special(move); // Rewind core board state. SideToMove = 1 - SideToMove; Square[from] = piece; Square[to] = capture; Bitboard[piece] ^= (1UL << from) | (1UL << to); Bitboard[SideToMove] ^= (1UL << from) | (1UL << to); OccupiedBitboard ^= (1UL << from) | (1UL << to); // Rewind metainformation. ZobristKey = ZobristKeyHistory[HalfMoves - 1]; EnPassantHistory[HalfMoves] = InvalidSquare; EnPassantSquare = EnPassantHistory[HalfMoves - 1]; FiftyMovesClock = FiftyMovesHistory[HalfMoves - 1]; HalfMoves--; // Rewind capture if applicable. switch (capture & Piece.Mask) { case Piece.Empty: break; case Piece.Rook: if ((SideToMove == Colour.White && to == 0) || (SideToMove == Colour.Black && to == 56)) { CastleQueenside[1 - SideToMove]++; } else if ((SideToMove == Colour.White && to == 7) || (SideToMove == Colour.Black && to == 63)) { CastleKingside[1 - SideToMove]++; } goto default; default: Bitboard[capture] ^= 1UL << to; Bitboard[1 - SideToMove] ^= 1UL << to; OccupiedBitboard |= 1UL << to; Material[1 - SideToMove] += Engine.PieceValue[capture]; break; } switch (special & Piece.Mask) { // Rewind regular move. case Piece.Empty: switch (piece & Piece.Mask) { // For rook move, restore castling on one side if applicable. case Piece.Rook: if ((SideToMove == Colour.White && from == 56) || (SideToMove == Colour.Black && from == 0)) { CastleQueenside[SideToMove]++; } else if ((SideToMove == Colour.White && from == 63) || (SideToMove == Colour.Black && from == 7)) { CastleKingside[SideToMove]++; } break; // For king move, restore castling on both sides if applicable. case Piece.King: CastleQueenside[SideToMove]++; CastleKingside[SideToMove]++; break; } break; // Rewind castling. case Piece.King: CastleQueenside[SideToMove]++; CastleKingside[SideToMove]++; Int32 rookFrom; Int32 rookTo; if (to < from) { rookFrom = Rank(to) * 8; rookTo = 3 + Rank(to) * 8; } else { rookFrom = 7 + Rank(to) * 8; rookTo = 5 + Rank(to) * 8; } Bitboard[SideToMove | Piece.Rook] ^= (1UL << rookFrom) | (1UL << rookTo); Bitboard[SideToMove] ^= (1UL << rookFrom) | (1UL << rookTo); OccupiedBitboard ^= (1UL << rookFrom) | (1UL << rookTo); Square[rookFrom] = SideToMove | Piece.Rook; Square[rookTo] = Piece.Empty; break; // Rewind en passant. case Piece.Pawn: Square[File(to) + Rank(from) * 8] = special; Bitboard[special] ^= 1UL << (File(to) + Rank(from) * 8); Bitboard[1 - SideToMove] ^= 1UL << (File(to) + Rank(from) * 8); OccupiedBitboard ^= 1UL << (File(to) + Rank(from) * 8); Material[1 - SideToMove] += Engine.PieceValue[special]; break; // Rewind pawn promotion. default: Bitboard[piece] ^= 1UL << to; Bitboard[special] ^= 1UL << to; Material[SideToMove] -= Engine.PieceValue[special] - Engine.PieceValue[piece]; break; } }
/// <summary> /// Makes the given move on the position. /// </summary> /// <param name="move">The move to make.</param> public void Make(Int32 move) { Int32 from = Move.From(move); Int32 to = Move.To(move); Int32 piece = Move.Piece(move); Int32 capture = Move.Capture(move); Int32 special = Move.Special(move); // Update core board state. Square[to] = piece; Square[from] = Piece.Empty; Bitboard[piece] ^= (1UL << from) | (1UL << to); Bitboard[SideToMove] ^= (1UL << from) | (1UL << to); OccupiedBitboard ^= (1UL << from) | (1UL << to); // Update metainformation. ZobristKey ^= Zobrist.PiecePosition[piece][from] ^ Zobrist.PiecePosition[piece][to]; ZobristKey ^= Zobrist.Colour; if (EnPassantSquare != InvalidSquare) { ZobristKey ^= Zobrist.EnPassant[EnPassantSquare]; EnPassantSquare = InvalidSquare; } FiftyMovesClock++; HalfMoves++; // Handle capture if applicable. switch (capture & Piece.Mask) { case Piece.Empty: break; case Piece.Rook: if ((SideToMove == Colour.White && to == 0) || (SideToMove == Colour.Black && to == 56)) { if (CastleQueenside[1 - SideToMove]-- > 0) { ZobristKey ^= Zobrist.CastleQueenside[1 - SideToMove]; } } else if ((SideToMove == Colour.White && to == 7) || (SideToMove == Colour.Black && to == 63)) { if (CastleKingside[1 - SideToMove]-- > 0) { ZobristKey ^= Zobrist.CastleKingside[1 - SideToMove]; } } goto default; default: Bitboard[capture] ^= 1UL << to; Bitboard[1 - SideToMove] ^= 1UL << to; OccupiedBitboard |= 1UL << to; ZobristKey ^= Zobrist.PiecePosition[capture][to]; Material[1 - SideToMove] -= Engine.PieceValue[capture]; FiftyMovesClock = 0; break; } switch (special & Piece.Mask) { // Handle regular move (not en passant, castling, or pawn promotion). case Piece.Empty: switch (piece & Piece.Mask) { // For pawn move, update fifty moves clock and en passant state. case Piece.Pawn: FiftyMovesClock = 0; if ((from - to) * (from - to) == 256) { ZobristKey ^= Zobrist.EnPassant[from]; EnPassantHistory[HalfMoves] = EnPassantSquare = (from + to) / 2; } break; // For rook move, disable castling on one side. case Piece.Rook: if ((SideToMove == Colour.White && from == 56) || (SideToMove == Colour.Black && from == 0)) { if (CastleQueenside[SideToMove]-- > 0) { ZobristKey ^= Zobrist.CastleQueenside[SideToMove]; } } else if ((SideToMove == Colour.White && from == 63) || (SideToMove == Colour.Black && from == 7)) { if (CastleKingside[SideToMove]-- > 0) { ZobristKey ^= Zobrist.CastleKingside[SideToMove]; } } break; // For king move, disable castling on both sides. case Piece.King: if (CastleQueenside[SideToMove]-- > 0) { ZobristKey ^= Zobrist.CastleQueenside[SideToMove]; } if (CastleKingside[SideToMove]-- > 0) { ZobristKey ^= Zobrist.CastleKingside[SideToMove]; } break; } break; // Handle castling. case Piece.King: if (CastleQueenside[SideToMove]-- > 0) { ZobristKey ^= Zobrist.CastleQueenside[SideToMove]; } if (CastleKingside[SideToMove]-- > 0) { ZobristKey ^= Zobrist.CastleKingside[SideToMove]; } Int32 rookFrom; Int32 rookTo; if (to < from) { rookFrom = Rank(to) * 8; rookTo = 3 + Rank(to) * 8; } else { rookFrom = 7 + Rank(to) * 8; rookTo = 5 + Rank(to) * 8; } Bitboard[SideToMove | Piece.Rook] ^= (1UL << rookFrom) | (1UL << rookTo); Bitboard[SideToMove] ^= (1UL << rookFrom) | (1UL << rookTo); OccupiedBitboard ^= (1UL << rookFrom) | (1UL << rookTo); ZobristKey ^= Zobrist.PiecePosition[SideToMove | Piece.Rook][rookFrom]; ZobristKey ^= Zobrist.PiecePosition[SideToMove | Piece.Rook][rookTo]; Square[rookFrom] = Piece.Empty; Square[rookTo] = SideToMove | Piece.Rook; break; // Handle en passant. case Piece.Pawn: Square[File(to) + Rank(from) * 8] = Piece.Empty; Bitboard[special] ^= 1UL << (File(to) + Rank(from) * 8); Bitboard[1 - SideToMove] ^= 1UL << (File(to) + Rank(from) * 8); OccupiedBitboard ^= 1UL << (File(to) + Rank(from) * 8); ZobristKey ^= Zobrist.PiecePosition[special][File(to) + Rank(from) * 8]; Material[1 - SideToMove] -= Engine.PieceValue[special]; break; // Handle pawn promotion. default: Bitboard[piece] ^= 1UL << to; Bitboard[special] ^= 1UL << to; ZobristKey ^= Zobrist.PiecePosition[piece][to]; ZobristKey ^= Zobrist.PiecePosition[special][to]; Material[SideToMove] += Engine.PieceValue[special] - Engine.PieceValue[piece]; Square[to] = special; break; } SideToMove = 1 - SideToMove; FiftyMovesHistory[HalfMoves] = FiftyMovesClock; ZobristKeyHistory[HalfMoves] = ZobristKey; }
/// <summary> /// Returns the dynamic value of the position as determined by a recursive /// search that terminates upon reaching a quiescent position. /// </summary> /// <param name="position">The position to search on.</param> /// <param name="ply">The number of plies from the root position.</param> /// <param name="alpha">The lower bound on the value of the best move.</param> /// <param name="beta">The upper bound on the value of the best move.</param> /// <returns>The value of the termination position given optimal play.</returns> private Int32 Quiescence(Position position, Int32 ply, Int32 alpha, Int32 beta) { _totalNodes++; _quiescenceNodes++; // Evaluate the position statically. Check for upper bound cutoff and lower // bound improvement. Int32 value = Evaluate(position); if (value >= beta) { return(value); } if (value > alpha) { alpha = value; } // Initialize variables and generate the pseudo-legal moves to be // considered. Perform basic move ordering and sort the moves. Int32 colour = position.SideToMove; Int32[] moves = _generatedMoves[ply]; Int32 movesCount = position.PseudoQuiescenceMoves(moves); for (Int32 i = 0; i < movesCount; i++) { _moveValues[i] = MoveOrderingValue(moves[i]); } Sort(moves, _moveValues, movesCount); // Go through the move list. for (Int32 i = 0; i < movesCount; i++) { _movesSearched++; Int32 move = moves[i]; // Consider the move only if it doesn't immediately lose material. This // improves efficiency. if ((Move.Piece(move) & Piece.Mask) <= (Move.Capture(move) & Piece.Mask) || EvaluateStaticExchange(position, move) >= 0) { // Make the move. position.Make(move); // Search the move if it is legal. This is equivalent to not leaving the // king in check. if (!position.InCheck(colour)) { value = -Quiescence(position, ply + 1, -beta, -alpha); // Check for upper bound cutoff and lower bound improvement. if (value >= beta) { position.Unmake(move); return(value); } if (value > alpha) { alpha = value; } } // Unmake the move. position.Unmake(move); } } return(alpha); }