From 9b7b00b500edd874e42048c6db4ee2a0f199ea49 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 12:38:15 -0500 Subject: [PATCH 1/7] Add conservative internal iterative reduction --- src/search.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/search.cpp b/src/search.cpp index 393c9b7..2062882 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -22,6 +22,8 @@ namespace ironphoenix { constexpr int SEE_PRUNE_MARGIN_PER_DEPTH = 80; constexpr int QSEARCH_SEE_THRESHOLD = -50; + constexpr int IIR_MIN_DEPTH = 5; + IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { const Color partner = static_cast(static_cast(c) ^ 2u); return pos.occupancy(c) | pos.occupancy(partner); @@ -414,6 +416,18 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); + // Internal Iterative Reduction (IIR): when a sufficiently deep + // non-PV node has no TT move to guide ordering, search it one ply + // shallower. The reduction is deliberately conservative because LMR + // can still reduce later quiet moves independently. + if (!pvNode + && ply > 0 + && depth >= IIR_MIN_DEPTH + && !inCheckNode + && !ttMove) { + --depth; + } + MoveList moves; generatePseudoLegalMoves(pos, moves); orderMoves(pos, moves.begin(), moves.end(), ttMove, ply); From db8aaf4f858902a1805ea858003ac7ebce267efd Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 12:43:17 -0500 Subject: [PATCH 2/7] Apply IIR on PV nodes --- src/search.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 2062882..4ac5913 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -416,11 +416,11 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); - // Internal Iterative Reduction (IIR): when a sufficiently deep - // non-PV node has no TT move to guide ordering, search it one ply - // shallower. The reduction is deliberately conservative because LMR - // can still reduce later quiet moves independently. - if (!pvNode + // Internal Iterative Reduction (IIR): when a sufficiently deep PV + // node has no TT move to guide ordering, search it one ply shallower. + // The reduction is deliberately conservative because LMR can still + // reduce later quiet moves independently. + if (pvNode && ply > 0 && depth >= IIR_MIN_DEPTH && !inCheckNode From 76182225c80d60ff8ce146930b021489eca64b7b Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 19:12:50 -0500 Subject: [PATCH 3/7] Allow non-pv nodes --- src/search.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 19915b6..f5eb5f4 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -415,12 +415,7 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); const auto& tuning = Tuning::search(); - // Internal Iterative Reduction (IIR): when a sufficiently deep PV - // node has no TT move to guide ordering, search it one ply shallower. - // The reduction is deliberately conservative because LMR can still - // reduce later quiet moves independently. - if (pvNode - && ply > 0 + if (ply > 0 && depth >= IIR_MIN_DEPTH && !inCheckNode && !ttMove) { From e84518bde10fbc03cc9ceb95ef0e3eb8abbb8755 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 10 Oct 2026 17:51:28 -0500 Subject: [PATCH 4/7] Tune IIR to PV-only at depth 6 --- src/search.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index f5eb5f4..6f77480 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -18,7 +18,7 @@ namespace ironphoenix { constexpr int LMP_HISTORY_THRESHOLD = 2'000; - constexpr int IIR_MIN_DEPTH = 5; + constexpr int IIR_MIN_DEPTH = 6; IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { const Color partner = static_cast(static_cast(c) ^ 2u); @@ -415,7 +415,11 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); const auto& tuning = Tuning::search(); - if (ply > 0 + // 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 && !ttMove) { From 3045b908d565c0fec31af8f076d8dc25d6e8545b Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 12 Oct 2026 22:57:14 -0500 Subject: [PATCH 5/7] Normalize IIR --- src/search.cpp | 86 ++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index 369dd4f..a1afaa9 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -515,21 +515,16 @@ namespace ironphoenix { return ttProbe.score; } + const int staticEval = evaluate(pos); + const bool inCheckNode = pos.inCheck(); const auto& tuning = Tuning::search(); - // 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 - && !ttMove) { - --depth; - } - 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 @@ -538,42 +533,49 @@ 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 + ); + + 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; From a01dc683850abd7942babc67f332a62118ac4917 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 13 Oct 2026 07:06:58 -0500 Subject: [PATCH 6/7] Add cut/all node search state --- include/ironphoenix/search.hpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 From 0fe2db55e38746742266a3199a5936212832f241 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 13 Oct 2026 07:08:27 -0500 Subject: [PATCH 7/7] Propagate cut and all node types --- src/search.cpp | 72 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index a1afaa9..470f855 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -475,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()) @@ -484,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; @@ -553,6 +559,7 @@ namespace ironphoenix { -beta + 1, ply + 1, false, + false, false ); @@ -596,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) @@ -712,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 { @@ -728,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);