Skip to content
Draft
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
7 changes: 5 additions & 2 deletions include/ironphoenix/search.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -132,4 +135,4 @@ namespace ironphoenix {
void emitBestMove(std::ostream& out, Move move);
};

}
}
150 changes: 112 additions & 38 deletions src/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
Expand All @@ -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;
Expand Down Expand Up @@ -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<Color>(static_cast<unsigned>(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
Expand All @@ -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<Color>(static_cast<unsigned>(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;
Expand All @@ -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)
Expand Down Expand Up @@ -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 {

Expand All @@ -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);
Expand Down
Loading