Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion include/ironphoenix/search.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@ namespace ironphoenix {
std::array<std::int64_t, COLOR_NB> 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<HistoryContext&>(*this) = context;
staticEval = 0;
improvementRate = 0;
improving = false;
return *this;
}
};

class SearchEngine {
public:
static constexpr int MAX_MULTI_PV = 32;
Expand Down Expand Up @@ -79,7 +102,7 @@ namespace ironphoenix {

std::array<std::array<Move, MAX_PLY>, MAX_PLY> pv_{};
std::array<int, MAX_PLY> pvLength_{};
std::array<HistoryContext, MAX_PLY> searchStack_{};
std::array<SearchStackEntry, MAX_PLY> searchStack_{};
mutable std::mutex outputMutex_;

void run(Position position, std::ostream& out);
Expand Down
2 changes: 1 addition & 1 deletion src/multipv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
33 changes: 29 additions & 4 deletions src/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Color>(static_cast<unsigned>(c) ^ 2u);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down