From b05f1ce155dc796bcf0174dc481c18ee026e9a7d Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 13:28:37 -0500 Subject: [PATCH 1/3] Add improving state to search stack --- include/ironphoenix/search.hpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/include/ironphoenix/search.hpp b/include/ironphoenix/search.hpp index 0604b00..e4cdf0d 100644 --- a/include/ironphoenix/search.hpp +++ b/include/ironphoenix/search.hpp @@ -33,6 +33,29 @@ namespace ironphoenix { std::array delayMs{}; }; + // Per-ply search state. HistoryContext remains the base so the existing + // history/countermove interfaces can consume stack entries directly. + // The evaluation fields are used to compare the current player with the + // same player's position one complete 4PC turn cycle (four plies) ago. + struct SearchStackEntry : HistoryContext { + int staticEval = 0; + int improvementRate = 0; + bool improving = false; + + constexpr SearchStackEntry() noexcept = default; + + constexpr SearchStackEntry(const HistoryContext& context) noexcept + : HistoryContext(context) {} + + constexpr SearchStackEntry& operator=(const HistoryContext& context) noexcept { + static_cast(*this) = context; + staticEval = 0; + improvementRate = 0; + improving = false; + return *this; + } + }; + class SearchEngine { public: static constexpr int MAX_MULTI_PV = 32; @@ -79,7 +102,7 @@ namespace ironphoenix { std::array, MAX_PLY> pv_{}; std::array pvLength_{}; - std::array searchStack_{}; + std::array searchStack_{}; mutable std::mutex outputMutex_; void run(Position position, std::ostream& out); From 7f454d7f3d3c71f31c010ec40008ec74da78cfd2 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 13:29:59 -0500 Subject: [PATCH 2/3] Make LMP aware of improving positions --- src/search.cpp | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index ee7ac6c..74e0058 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -23,6 +23,8 @@ namespace ironphoenix { constexpr int QSEARCH_SEE_THRESHOLD = -50; constexpr int LMP_HISTORY_THRESHOLD = 2'000; + constexpr int IMPROVING_MARGIN = 25; + constexpr int WORSENING_MARGIN = -100; IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { const Color partner = static_cast(static_cast(c) ^ 2u); @@ -416,6 +418,27 @@ namespace ironphoenix { const bool inCheckNode = pos.inCheck(); + // In 4PC, compare against four plies ago so the static evaluation is + // measured from the same player's perspective after a complete turn + // cycle. A small positive margin prevents evaluation noise from + // constantly toggling the improving flag. + SearchStackEntry& ss = searchStack_[ply]; + ss.staticEval = evaluate(pos); + ss.improvementRate = 0; + ss.improving = false; + + if (ply >= 4) { + ss.improvementRate = ss.staticEval - searchStack_[ply - 4].staticEval; + ss.improving = ss.improvementRate > IMPROVING_MARGIN; + } + + int lmpMoveLimit = 3 + depth * depth; + if (ss.improving) + lmpMoveLimit += 2 + depth; + else if (ss.improvementRate < WORSENING_MARGIN) + lmpMoveLimit -= 1 + depth / 2; + lmpMoveLimit = std::max(3, lmpMoveLimit); + MoveList moves; generatePseudoLegalMoves(pos, moves); orderMoves(pos, moves.begin(), moves.end(), ttMove, ply); @@ -448,11 +471,13 @@ namespace ironphoenix { // Late Move Pruning (LMP): at non-PV nodes, stop spending search // effort on late quiet moves unless history says the move is - // promising. Checks, TT moves, promotions/captures, and mate-window - // searches are never pruned here. This intentionally has no depth cap. + // promising. Improving positions search more quiet moves before + // pruning; strongly worsening positions prune a little earlier. + // Checks, TT moves, promotions/captures, and mate-window searches + // are never pruned here. This intentionally has no depth cap. if (!pvNode && !inCheckNode - && legalMoves >= 3 + depth * depth + && legalMoves >= lmpMoveLimit && quiet && move != ttMove && !givesCheckMove @@ -659,7 +684,7 @@ namespace ironphoenix { for (auto& row : pv_) row.fill(Move{}); pvLength_.fill(0); - searchStack_.fill(HistoryContext{}); + searchStack_.fill(SearchStackEntry{}); MoveList rootLegal; generateLegalMoves(position, rootLegal); From f2f5b410df778e8a6a9319a43d0a3a8c6402d274 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 13:30:29 -0500 Subject: [PATCH 3/3] Initialize expanded search stack in MultiPV --- src/multipv.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multipv.cpp b/src/multipv.cpp index e8548dc..8ff1405 100644 --- a/src/multipv.cpp +++ b/src/multipv.cpp @@ -108,7 +108,7 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out) for (auto& row : pv_) row.fill(Move{}); pvLength_.fill(0); - searchStack_.fill(HistoryContext{}); + searchStack_.fill(SearchStackEntry{}); MoveList rootLegal; generateLegalMoves(position, rootLegal);