diff --git a/src/search.cpp b/src/search.cpp index b9766ff..018e68f 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -24,8 +24,14 @@ namespace ironphoenix { constexpr int HISTORY_MOVE_PRUNE_MAX_DEPTH = 4; constexpr int HISTORY_MOVE_PRUNE_MARGIN_PER_DEPTH = 1'500; + // Post-LMR continuation history: reward a reduced quiet move that proves + // strong enough to beat alpha and require a full-depth re-search. + constexpr int POST_LMR_CONTINUATION_BONUS = 1'334; + constexpr int NULL_MOVE_MIN_DEPTH = 2; constexpr int NULL_MOVE_DIVISOR = 190; + constexpr int REVERSE_FUTILITY_MAX_DEPTH = 4; + constexpr int REVERSE_FUTILITY_MARGIN_PER_DEPTH = 200; constexpr std::size_t CHECK_BIT_WORDS = (MAX_MOVES + 63u) / 64u; // Non-PV nodes normally cut off quickly. Selecting only the strongest @@ -516,10 +522,43 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); const auto& tuning = Tuning::search(); const Color us = pos.sideToMove(); + const Color partner = static_cast(static_cast(us) ^ 2u); const bool mateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD); - // Null Move Pruning (NMP): keep expensive partner-check detection and - // static evaluation completely off nodes where NMP cannot run anyway. + // Static pruning shares one evaluator result between reverse futility + // pruning and null-move pruning. Keep partner-check detection lazy so PV, + // check and mate-window nodes do not pay for it. + int staticEval = 0; + bool staticEvalValid = false; + bool partnerInCheck = false; + bool partnerCheckKnown = false; + + // Reverse Futility Pruning (RFP): at shallow non-PV nodes, if the static + // evaluation already exceeds beta by a depth-scaled safety margin, the + // position is unlikely to fall below beta after a normal move. Avoid RFP + // while either teammate is in check and never return a synthetic mate. + if (!pvNode + && ply > 0 + && depth <= REVERSE_FUTILITY_MAX_DEPTH + && !inCheckNode + && !mateWindow + && hasNonPawnMaterial(pos, us)) { + + partnerInCheck = pos.inCheck(partner); + partnerCheckKnown = true; + + if (!partnerInCheck) { + staticEval = evaluate(pos); + staticEvalValid = true; + + const int margin = REVERSE_FUTILITY_MARGIN_PER_DEPTH * depth; + if (staticEval < MATE_THRESHOLD && staticEval - margin >= beta) + return staticEval; + } + } + + // Null Move Pruning (NMP): reuse the static evaluation and partner-check + // result when RFP already computed them at shallow nodes. if (allowNull && !pvNode && ply > 0 @@ -528,9 +567,16 @@ namespace ironphoenix { && !mateWindow && hasNonPawnMaterial(pos, us)) { - const Color partner = static_cast(static_cast(us) ^ 2u); - if (!pos.inCheck(partner)) { - const int staticEval = evaluate(pos); + if (!partnerCheckKnown) { + partnerInCheck = pos.inCheck(partner); + partnerCheckKnown = true; + } + + if (!partnerInCheck) { + if (!staticEvalValid) { + staticEval = evaluate(pos); + staticEvalValid = true; + } if (staticEval >= beta) { StateInfo nullState; @@ -716,8 +762,32 @@ namespace ironphoenix { const int reducedDepth = depth - 1 - reduction; score = -negamax(pos, reducedDepth, -alpha - 1, -alpha, ply + 1, false); - if (!shouldStop() && score > alpha) + if (!shouldStop() && score > alpha) { + // Post-LMR continuation-history update: the move was + // reduced because it looked unpromising, but the + // reduced search still beat alpha. Reward the + // continuation contexts before the full-depth retry. + if (ply > 0 && searchStack_[ply].valid()) { + history_.updateContinuation( + ContinuationKind::PreviousPly, + searchStack_[ply], + movingPiece, + move.to(), + POST_LMR_CONTINUATION_BONUS); + } + + if (ply >= 4 && searchStack_[ply - 3].valid() + && searchStack_[ply - 3].mover == us) { + history_.updateContinuation( + ContinuationKind::SamePlayer, + searchStack_[ply - 3], + movingPiece, + move.to(), + (POST_LMR_CONTINUATION_BONUS * 3) / 4); + } + score = -negamax(pos, depth - 1, -alpha - 1, -alpha, ply + 1, false); + } } else { score = -negamax(pos, depth - 1, -alpha - 1, -alpha, ply + 1, false);