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: 6 additions & 1 deletion include/ironphoenix/tt.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

namespace ironphoenix {

inline constexpr int TT_EVAL_NONE = -32768;

enum class TTBound : std::uint8_t {
None = 0,
Upper = 1,
Expand All @@ -19,6 +21,7 @@ struct TTProbe {
bool hit = false;
Move move{};
int score = 0;
int eval = TT_EVAL_NONE;
int depth = -1;
TTBound bound = TTBound::None;
bool pvNode = false;
Expand All @@ -36,6 +39,7 @@ class TranspositionTable {
void store(Key key,
int depth,
int score,
int staticEval,
TTBound bound,
Move bestMove,
bool pvNode,
Expand All @@ -46,9 +50,10 @@ class TranspositionTable {

private:
struct Entry {
Key key = 0;
std::uint32_t key = 0;
std::uint32_t move = 0;
std::int16_t score = 0;
std::int16_t eval = TT_EVAL_NONE;
std::int8_t depth = -1;
std::uint8_t flags = 0;
};
Expand Down
115 changes: 83 additions & 32 deletions src/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ namespace ironphoenix {

constexpr int NULL_MOVE_MIN_DEPTH = 2;
constexpr int NULL_MOVE_DIVISOR = 190;
constexpr int FUTILITY_MAX_DEPTH = 2;
constexpr int FUTILITY_MARGIN_PER_DEPTH = 200;
constexpr int TT_MATE_THRESHOLD = 29'000;

IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept {
const Color partner = static_cast<Color>(static_cast<unsigned>(c) ^ 2u);
Expand All @@ -34,6 +37,29 @@ namespace ironphoenix {
return static_cast<bool>(pieces);
}

IRONPHOENIX_FORCE_INLINE int futilityMargin(int depth) noexcept {
return FUTILITY_MARGIN_PER_DEPTH * depth;
}

IRONPHOENIX_FORCE_INLINE int ttImprovedEval(int staticEval, bool ttHit, const TTProbe& ttProbe) noexcept {
if (staticEval == TT_EVAL_NONE || !ttHit)
return staticEval;

// Search scores in the mate range are distance-dependent and should
// never replace a static evaluation used by pruning heuristics.
if (ttProbe.score <= -TT_MATE_THRESHOLD || ttProbe.score >= TT_MATE_THRESHOLD)
return staticEval;

if (ttProbe.bound == TTBound::Exact)
return ttProbe.score;
if (ttProbe.bound == TTBound::Lower && ttProbe.score > staticEval)
return ttProbe.score;
if (ttProbe.bound == TTBound::Upper && ttProbe.score < staticEval)
return ttProbe.score;

return staticEval;
}

}

SearchEngine::SearchEngine() = default;
Expand Down Expand Up @@ -291,14 +317,18 @@ namespace ironphoenix {
return ttProbe.score;
}

int staticEval = TT_EVAL_NONE;
int bestScore = -INF;
Move bestMove{};

if (!inCheckNode) {
bestScore = evaluate(pos);
staticEval = ttHit && ttProbe.eval != TT_EVAL_NONE
? ttProbe.eval
: evaluate(pos);
bestScore = staticEval;

if (bestScore >= beta) {
tt_.store(pos.key(), 0, bestScore, TTBound::Lower, Move{}, pvNode, ply);
tt_.store(pos.key(), 0, bestScore, staticEval, TTBound::Lower, Move{}, pvNode, ply);
return bestScore;
}

Expand Down Expand Up @@ -375,7 +405,7 @@ namespace ironphoenix {
pvLength_[ply] = std::max(ply + 1, pvLength_[ply + 1]);

if (alpha >= beta) {
tt_.store(pos.key(), 0, bestScore, TTBound::Lower, bestMove, pvNode, ply);
tt_.store(pos.key(), 0, bestScore, staticEval, TTBound::Lower, bestMove, pvNode, ply);
return bestScore;
}
}
Expand All @@ -386,20 +416,22 @@ namespace ironphoenix {

if (inCheckNode && legalMoves == 0) {
const int mateScore = -MATE_SCORE + ply;
tt_.store(pos.key(), 0, mateScore, TTBound::Exact, Move{}, pvNode, ply);
tt_.store(pos.key(), 0, mateScore, TT_EVAL_NONE, TTBound::Exact, Move{}, pvNode, ply);
return mateScore;
}

if (bestScore == -INF)
bestScore = evaluate(pos);
if (bestScore == -INF) {
staticEval = evaluate(pos);
bestScore = staticEval;
}

TTBound bound = TTBound::Exact;
if (bestScore <= originalAlpha)
bound = TTBound::Upper;
else if (bestScore >= beta)
bound = TTBound::Lower;

tt_.store(pos.key(), 0, bestScore, bound, bestMove, pvNode, ply);
tt_.store(pos.key(), 0, bestScore, staticEval, bound, bestMove, pvNode, ply);
return bestScore;
}

Expand Down Expand Up @@ -429,11 +461,11 @@ namespace ironphoenix {
const bool ttHit = tt_.probe(pos.key(), ply, ttProbe);
const Move ttMove = ttHit ? ttProbe.move : Move{};

// Transposition Table (TT) Cutoffs: if the current position has been
// previously searched at a depth equal to or greater than the current
// depth, and the stored score is sufficient to cause a cutoff, return
// the stored score. This is only done at non-PV nodes, since PV nodes
// require a full search to find the best move.
// Transposition Table (TT) Cutoffs: if the current position has been
// previously searched at a depth equal to or greater than the current
// depth, and the stored score is sufficient to cause a cutoff, return
// the stored score. This is only done at non-PV nodes, since PV nodes
// require a full search to find the best move.
if (!pvNode && ply > 0 && ttHit && ttProbe.depth >= depth) {
if (ttProbe.bound == TTBound::Exact)
return ttProbe.score;
Expand All @@ -442,8 +474,11 @@ namespace ironphoenix {
if (ttProbe.bound == TTBound::Upper && ttProbe.score <= alpha)
return ttProbe.score;
}

const int staticEval = evaluate(pos);

int staticEval = ttHit && ttProbe.eval != TT_EVAL_NONE
? ttProbe.eval
: evaluate(pos);
const int eval = ttImprovedEval(staticEval, ttHit, ttProbe);

const bool inCheckNode = pos.inCheck();
const auto& tuning = Tuning::search();
Expand All @@ -452,19 +487,18 @@ namespace ironphoenix {
static_cast<Color>(static_cast<unsigned>(us) ^ 2u);

const bool partnerInCheck = pos.inCheck(partner);

const bool mateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD);

// Null Move Pruning (NMP): if the current player can effectively pass
// and the opposing team still cannot push the score below beta, normal
// moves are very likely to cause the same fail-high. Keep this away
// from PV nodes, checks, mate windows, pawn-only zugzwang positions,
// and consecutive null moves.
// partner-check positions, and consecutive null moves.
if (allowNull
&& !pvNode
&& ply > 0
&& depth >= NULL_MOVE_MIN_DEPTH
&& staticEval >= beta
&& eval >= beta
&& !inCheckNode
&& !partnerInCheck
&& !mateWindow
Expand All @@ -473,10 +507,11 @@ namespace ironphoenix {
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);

// Dynamic null-move reduction: reduce the depth more aggressively
// at deeper nodes and for higher pruning evaluations.
const int reduction = 4 + depth / 5
+ std::min(3, (eval - beta) / NULL_MOVE_DIVISOR);
const int nullScore = -negamax(
pos,
depth - reduction,
Expand All @@ -486,12 +521,12 @@ namespace ironphoenix {
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;
Expand Down Expand Up @@ -523,7 +558,7 @@ namespace ironphoenix {
bool legal = true;

const bool quiet = !move.isCapture() && !move.isPromotion();
const int mateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD)
const bool moveMateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD)
&& !terminalKingCapture;

// Late Move Pruning (LMP): at non-PV nodes, stop spending search
Expand All @@ -536,15 +571,31 @@ namespace ironphoenix {
&& quiet
&& move != ttMove
&& !givesCheckMove
&& !mateWindow
&& !moveMateWindow
&& lmrHistoryScore < LMP_HISTORY_THRESHOLD) {
continue;
}

// Static Exchange Evaluation (SEE) Pruning: at non-PV nodes, if a quiet
// capture move is likely to lose material, skip it. This is only done at
// shallow depths, since SEE is not always accurate in complex positions.
// SEE pruning is not applied to TT moves, promotions, checks, or mate-window searches.
// Futility Pruning: at shallow non-PV nodes, skip quiet moves when
// even the TT-improved node evaluation plus a depth-scaled margin
// cannot raise alpha. Keep the first legal move searchable so the
// node cannot be turned into a false mate or stalemate.
if (!pvNode
&& !inCheckNode
&& depth <= FUTILITY_MAX_DEPTH
&& legalMoves > 0
&& quiet
&& move != ttMove
&& !givesCheckMove
&& !moveMateWindow
&& eval + futilityMargin(depth) <= alpha) {
continue;
}

// Static Exchange Evaluation (SEE) Pruning: at non-PV nodes, if a
// capture move is likely to lose material, skip it. This is only done
// at shallow depths, since SEE is not always accurate in complex positions.
// SEE pruning is not applied to TT moves, promotions, checks, or mate-window searches.
if (!pvNode
&& !inCheckNode
&& depth <= tuning.seePruneMaxDepth
Expand All @@ -553,7 +604,7 @@ namespace ironphoenix {
&& !move.isPromotion()
&& move != ttMove
&& !givesCheckMove
&& !mateWindow) {
&& !moveMateWindow) {
const int seeThreshold = -tuning.seePruneMarginPerDepth * depth;
if (!seeGE(pos, move, seeThreshold, SeeMode::Legal))
continue;
Expand Down Expand Up @@ -668,7 +719,7 @@ namespace ironphoenix {
else if (bestScore >= beta)
bound = TTBound::Lower;

tt_.store(pos.key(), depth, bestScore, bound, bestMove, pvNode, ply);
tt_.store(pos.key(), depth, bestScore, staticEval, bound, bestMove, pvNode, ply);
return bestScore;
}

Expand Down
27 changes: 21 additions & 6 deletions src/tt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ constexpr unsigned GENERATION_SHIFT = 3u;
constexpr std::uint8_t GENERATION_MASK = 0xF8u;
constexpr std::uint8_t GENERATION_MOD = 32u;

IRONPHOENIX_FORCE_INLINE std::uint32_t keySignature(Key key) noexcept {
return static_cast<std::uint32_t>(key >> 32u);
}

}

TranspositionTable::TranspositionTable(std::size_t megabytes) {
Expand Down Expand Up @@ -87,12 +91,14 @@ bool TranspositionTable::probe(Key key, int ply, TTProbe& out) const noexcept {
if (table_.empty())
return false;

const std::uint32_t signature = keySignature(key);
const Cluster& cluster = table_[static_cast<std::size_t>(key) & mask_];
for (const Entry& entry : cluster.entry) {
if (entry.key == key && entry.key != 0) {
if (entry.depth >= 0 && entry.key == signature) {
out.hit = true;
out.move = Move(entry.move);
out.score = scoreFromTT(entry.score, ply);
out.eval = entry.eval;
out.depth = entry.depth;
out.bound = boundOf(entry);
out.pvNode = pvOf(entry);
Expand All @@ -105,22 +111,24 @@ bool TranspositionTable::probe(Key key, int ply, TTProbe& out) const noexcept {
void TranspositionTable::store(Key key,
int depth,
int score,
int staticEval,
TTBound bound,
Move bestMove,
bool pvNode,
int ply) noexcept {
if (table_.empty())
return;

const std::uint32_t signature = keySignature(key);
Cluster& cluster = table_[static_cast<std::size_t>(key) & mask_];
Entry* replace = &cluster.entry[0];

for (Entry& entry : cluster.entry) {
if (entry.key == key) {
if (entry.depth >= 0 && entry.key == signature) {
replace = &entry;
break;
}
if (entry.key == 0) {
if (entry.depth < 0) {
replace = &entry;
break;
}
Expand All @@ -135,13 +143,20 @@ void TranspositionTable::store(Key key,
replace = &entry;
}

if (!bestMove && replace->key == key)
const bool sameKey = replace->depth >= 0 && replace->key == signature;

if (!bestMove && sameKey)
bestMove = Move(replace->move);

if (staticEval == TT_EVAL_NONE && sameKey)
staticEval = replace->eval;

const int ttScore = scoreToTT(score, ply);
replace->key = key;
replace->key = signature;
replace->move = bestMove.v;
replace->score = static_cast<std::int16_t>(std::clamp(ttScore, -32767, 32767));
replace->eval = static_cast<std::int16_t>(
staticEval == TT_EVAL_NONE ? TT_EVAL_NONE : std::clamp(staticEval, -32767, 32767));
replace->depth = static_cast<std::int8_t>(std::clamp(depth, -1, 127));
replace->flags = static_cast<std::uint8_t>(static_cast<std::uint8_t>(bound)
| (pvNode ? PV_MASK : 0u)
Expand All @@ -157,7 +172,7 @@ int TranspositionTable::hashfull() const noexcept {
std::size_t total = samples * 4;
for (std::size_t i = 0; i < samples; ++i) {
for (const Entry& entry : table_[i].entry) {
if (entry.key != 0 && generationOf(entry) == generation_)
if (entry.depth >= 0 && generationOf(entry) == generation_)
++used;
}
}
Expand Down
Loading