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); 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); 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);