diff --git a/include/ironphoenix/search.hpp b/include/ironphoenix/search.hpp index 2e61f85..d753ea8 100644 --- a/include/ironphoenix/search.hpp +++ b/include/ironphoenix/search.hpp @@ -87,7 +87,10 @@ namespace ironphoenix { void run(Position position, std::ostream& out); void runMultiPV(Position position, int multiPV, std::ostream& out); - int negamax(Position& pos, int depth, int alpha, int beta, int ply, bool pvNode, bool allowNull = true); + // cutNode marks a non-PV node expected to fail high. allNode is derived + // inside negamax as the complementary non-PV node type. + int negamax(Position& pos, int depth, int alpha, int beta, int ply, + bool pvNode, bool allowNull = true, bool cutNode = false); int qsearch(Position& pos, int alpha, int beta, int ply, bool pvNode); int evaluate(const Position& pos) const noexcept; int moveScore(const Position& pos, Move move, Move ttMove, int ply, bool givesCheck) const noexcept; @@ -132,4 +135,4 @@ namespace ironphoenix { void emitBestMove(std::ostream& out, Move move); }; -} +} \ No newline at end of file diff --git a/src/search.cpp b/src/search.cpp index b9766ff..470f855 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -24,6 +24,8 @@ namespace ironphoenix { constexpr int HISTORY_MOVE_PRUNE_MAX_DEPTH = 4; constexpr int HISTORY_MOVE_PRUNE_MARGIN_PER_DEPTH = 1'500; + constexpr int IIR_MIN_DEPTH = 6; + constexpr int NULL_MOVE_MIN_DEPTH = 2; constexpr int NULL_MOVE_DIVISOR = 190; constexpr std::size_t CHECK_BIT_WORDS = (MAX_MOVES + 63u) / 64u; @@ -473,7 +475,8 @@ namespace ironphoenix { return bestScore; } - int SearchEngine::negamax(Position& pos, int depth, int alpha, int beta, int ply, bool pvNode, bool allowNull) { + int SearchEngine::negamax(Position& pos, int depth, int alpha, int beta, int ply, + bool pvNode, bool allowNull, bool cutNode) { if (ply >= MAX_PLY - 1) return evaluate(pos); if (shouldStop()) @@ -482,6 +485,11 @@ namespace ironphoenix { if (depth <= 0) return qsearch(pos, alpha, beta, ply, pvNode); + // Node-type classification. PV nodes are never cut nodes. A non-PV node + // that is not expected to fail high is an all node. + cutNode = !pvNode && cutNode; + const bool allNode = !pvNode && !cutNode; + ++nodes_; selDepth_ = std::max(selDepth_, ply); pvLength_[ply] = ply; @@ -513,9 +521,16 @@ namespace ironphoenix { return ttProbe.score; } + const int staticEval = evaluate(pos); + 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 partnerChecked = pos.inCheck(partner); + const bool mateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD); // Null Move Pruning (NMP): keep expensive partner-check detection and @@ -524,42 +539,50 @@ namespace ironphoenix { && !pvNode && ply > 0 && depth >= NULL_MOVE_MIN_DEPTH + && staticEval >= beta && !inCheckNode + && !partnerChecked && !mateWindow && hasNonPawnMaterial(pos, us)) { - const Color partner = static_cast(static_cast(us) ^ 2u); - if (!pos.inCheck(partner)) { - const int staticEval = evaluate(pos); - - if (staticEval >= beta) { - StateInfo nullState; - pos.makeNullMove(nullState); - searchStack_[ply + 1] = HistoryContext{}; - - // Dynamic null-move reduction: reduce the depth more aggressively - // at deeper nodes and for higher static evaluations. - const int reduction = 4 + depth / 5 + std::min(3, (staticEval - beta) / NULL_MOVE_DIVISOR); - const int nullScore = -negamax( - pos, - depth - reduction, - -beta, - -beta + 1, - ply + 1, - false, - false - ); - - pos.undoNullMove(nullState); - - if (shouldStop()) - return 0; + StateInfo nullState; + pos.makeNullMove(nullState); + searchStack_[ply + 1] = HistoryContext{}; + + // Dynamic null-move reduction: reduce the depth more aggressively + // at deeper nodes and for higher static evaluations. + const int reduction = 4 + depth / 5 + std::min(3, (staticEval - beta) / NULL_MOVE_DIVISOR); + const int nullScore = -negamax( + pos, + depth - reduction, + -beta, + -beta + 1, + ply + 1, + false, + false, + false + ); + + pos.undoNullMove(nullState); + + if (shouldStop()) + return 0; + + // Never manufacture a mate score from a synthetic pass. + if (nullScore >= beta && nullScore < MATE_THRESHOLD) + return nullScore; + } - // Never manufacture a mate score from a synthetic pass. - if (nullScore >= beta && nullScore < MATE_THRESHOLD) - return nullScore; - } - } + // Internal Iterative Reduction (IIR): keep this deliberately conservative + // in 4PC. Only reduce deep PV nodes when move ordering has no TT move to + // guide the search. Non-PV nodes retain their full depth. + if (pvNode + && ply > 0 + && depth >= IIR_MIN_DEPTH + && !inCheckNode + && !partnerChecked + && !ttMove) { + depth -= 1; } MoveList moves; @@ -580,7 +603,7 @@ namespace ironphoenix { givesCheckBits.data()); } else { - // At cut nodes avoid sorting the entire move list and avoid eager + // At non-PV nodes avoid sorting the entire move list and avoid eager // givesCheck() calls. Score cheaply, then select the best few moves // only as they are actually requested by the search loop. for (std::size_t i = 0; i < moves.size; ++i) @@ -696,7 +719,19 @@ namespace ironphoenix { ++legalMoves; if (legalMoves == 1) { - score = -negamax(pos, depth - 1, -beta, -alpha, ply + 1, pvNode); + // PV nodes keep a PV child. For non-PV nodes, an all node + // expects its first child to be a cut node, while a cut node + // expects its first child to be an all node. + score = -negamax( + pos, + depth - 1, + -beta, + -alpha, + ply + 1, + pvNode, + true, + allNode + ); } else { @@ -712,19 +747,58 @@ namespace ironphoenix { reduction = std::clamp(reduction, 0, depth - 2); } + // PVS scout searches are cut nodes: from the child point of + // view we expect a fail-high against the null window when the + // move is not good enough to raise the parent's alpha. if (reduction > 0) { const int reducedDepth = depth - 1 - reduction; - score = -negamax(pos, reducedDepth, -alpha - 1, -alpha, ply + 1, false); + score = -negamax( + pos, + reducedDepth, + -alpha - 1, + -alpha, + ply + 1, + false, + true, + true + ); if (!shouldStop() && score > alpha) - score = -negamax(pos, depth - 1, -alpha - 1, -alpha, ply + 1, false); + score = -negamax( + pos, + depth - 1, + -alpha - 1, + -alpha, + ply + 1, + false, + true, + true + ); } else { - score = -negamax(pos, depth - 1, -alpha - 1, -alpha, ply + 1, false); + score = -negamax( + pos, + depth - 1, + -alpha - 1, + -alpha, + ply + 1, + false, + true, + true + ); } if (!shouldStop() && score > alpha && score < beta) - score = -negamax(pos, depth - 1, -beta, -alpha, ply + 1, pvNode); + score = -negamax( + pos, + depth - 1, + -beta, + -alpha, + ply + 1, + pvNode, + true, + false + ); } } pos.undoMove(move, st);