summaryrefslogtreecommitdiff
path: root/src/searchsup.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-23 13:19:17 -0700
committerScott Gasch <[email protected]>2026-10-23 13:19:17 -0700
commit69525eb2f42ea9cca8255f6cc3b85e6b1312fd05 (patch)
treed78420b6c45d584a3f09b4dcbaf4f1edb36e8d5d /src/searchsup.c
parent6b883e83935f5e8110788565cfcb77cd6fbf8950 (diff)
NNUE search integration: FH% tables, NODE_TYPE refinement, TREEDUMP rewrite
- data.c/chess.h: widen the joint FH% table's late axis 16->17, dedicating the new bucket 0 to a whole-node ("will something here fail high at all") sample rather than sharing it with the first-leftover-move curve. Add an NNUE-eval counterpart table (g_uJointFHThousandths_NNUE) and an NNUE counterpart of the beta-side table, selected at runtime based on whether NNUE eval is active. - search.c/searchsup.c: add _RefineNodeTypeGuess, which uses the FH% tables (via g_NodeTypeGuessTable) to refine a node's NODE_TYPE guess instead of the old heuristic; thread uParentMoveFHEstimate through PLY_INFO so a node can see its parent's prediction for the move that led to it. - generate.c: initialize sKillers[2]/[3].uBonus unconditionally (an uninitialized-stack read caught by valgrind under LTO; harmless today but a real bug). - dumptree.c/chess.h/GNUmakefile: replace the long-dead DUMP_TREE XML tree dump (written for a GUI this project no longer has) with TREEDUMP, a flat, path-keyed move/result trace meant to diff against an identically-instrumented Stockfish build -- see docs/TREEDUMP_FORMAT.md. - docs/DEEPER_PRUNING.md: notes on closing the search-efficiency gap vs Stockfish. - crafty_egtb/stockfish_xboard/typhoon_nnue: wrapper scripts so match_play.py's Engine class can drive Crafty, Stockfish (via PolyGlot), and an NNUE-enabled typhoon the same way it drives HCE typhoon. - tests/lop: rename lop.txt -> lop.ep_; update compare_lop.cpp. - annotate_pgns: add aggregate_marginsafety.py, dedup_pgn.py, fit_lazy_probe.py; update annotate.py/run_nnue_pipeline.py for the NNUE training pipeline. - .gitignore: ignore compile_commands.json (clangd build DB) and eval.nnue (a symlink into the untracked head_reference/ comparison baseline), neither of which belongs in the repo. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/searchsup.c')
-rw-r--r--src/searchsup.c291
1 files changed, 227 insertions, 64 deletions
diff --git a/src/searchsup.c b/src/searchsup.c
index eed9846..d43d387 100644
--- a/src/searchsup.c
+++ b/src/searchsup.c
@@ -329,8 +329,10 @@ Return value:
// A king can't "hang material" in the sense this function measures
// -- it's never capturable, so PIECE_VALUE(KING) (== INFINITY_SCORE)
- // has no business feeding into a cp-loss estimate at all.
- if (IS_KING(mv.pMoved))
+ // has no business feeding into a cp-loss estimate at all. Likewise
+ // we don't care about "hung pawns", they are not worth enough and
+ // we can't tell if it's actually hung or not easily.
+ if (IS_KING(mv.pMoved) || IS_PAWN(mv.pMoved))
{
return 0;
}
@@ -338,8 +340,7 @@ Return value:
// Any non-pawn moved onto a square an enemy pawn attacks --
// always unconditionally bad, a pawn is the cheapest possible
// attacker there is.
- if ((!IS_PAWN(mv.pMoved)) &&
- (0 != (g_PawnAttackOriginBB[uOpponent][mv.cTo] & pos->bbPawns[uOpponent])))
+ if (0 != (g_PawnAttackOriginBB[uOpponent][mv.cTo] & pos->bbPawns[uOpponent]))
{
return (SCORE)(PIECE_VALUE(mv.pMoved) - VALUE_PAWN);
}
@@ -350,15 +351,12 @@ Return value:
// "attacking" our destination square means nothing by itself --
// only flag this when OUR side has zero defenders of the square.
// Pawns excluded, same reasoning as the pawn-attack check above.
- bbTo = COOR_TO_BB(mv.cTo);
- if ((!IS_PAWN(mv.pMoved)) &&
- (0 != (g_KingAttacksBB[mv.cTo] & COOR_TO_BB(KingPosition(pos, uOpponent)))))
+ if (0 != (g_KingAttacksBB[mv.cTo] & COOR_TO_BB(KingPosition(pos, uOpponent))))
{
ULONG uMover = FLIP(uOpponent);
BITBOARD bbOurDefenders =
_WhoAttacksSquareBB(pos, mv.cTo, uMover, pos->bbOccupied) |
(g_PawnAttackOriginBB[uMover][mv.cTo] & pos->bbPawns[uMover]);
-
if (0 == bbOurDefenders)
{
return (SCORE)PIECE_VALUE(mv.pMoved);
@@ -367,47 +365,32 @@ Return value:
// to the minor/rook checks below.
}
- // Anything worth more than a minor, moved onto a square an enemy
- // minor attacks. Snapshotted locally by the caller -- NOT read
- // from pos->bbMinorAttacks directly, which gets clobbered by every
- // recursive Eval() call made while searching an earlier sibling
- // move and is never restored by UnmakeMove.
- if ((PIECE_VALUE(mv.pMoved) > VALUE_KNIGHT) &&
- (0 != (bbMinorAttacksSnapshot[uOpponent] & bbTo)))
- {
- return (SCORE)(PIECE_VALUE(mv.pMoved) - VALUE_KNIGHT);
- }
-
- // Anything worth more than a rook, moved onto a square an enemy
- // rook attacks. Same snapshot reasoning as the minor check above.
- if ((PIECE_VALUE(mv.pMoved) > VALUE_ROOK) &&
- (0 != (bbRookAttacksSnapshot[uOpponent] & bbTo)))
+ // If we're using NNUE eval, cTrapped, bbMinorAttacks, etc... will
+ // be unset.
+ if (FALSE == g_fUseNnueEval)
{
- return (SCORE)(PIECE_VALUE(mv.pMoved) - VALUE_ROOK);
- }
-
- // 2026-10-10: mv itself looks perfectly clean, but this node already
- // had one of the mover's OWN pieces flagged trapped by
- // _EvalTrappedPieces at node entry (ctx->cTrapped[uPly], uPly being
- // the PARENT ply here -- MakeMove already advanced ctx->uPly to the
- // child, so the parent's own slot is uPly-1; see RecordTrappedPiece's
- // ASSERT that the recorded piece's color always equals the mover's,
- // never the opponent's). Half credit, not full: mv doesn't move that
- // piece, so it isn't the thing hanging it, but it's also not doing
- // anything about it either, and a position with a trapped piece
- // sitting there is a real, if lesser, source of risk than this
- // move's own clean bill of health suggests. Only reached for a quiet
- // move (every capture/promotion already returned above) and only
- // overrides the "nothing hung" verdict -- never stacks with an
- // already-nonzero estimate from this same move.
- COOR cOwnTrapped = ctx->cTrapped[ctx->uPly - 1];
- if (IS_ON_BOARD(cOwnTrapped) && (mv.cFrom != cOwnTrapped))
- {
- PIECE pTrapped = ctx->pTrapped[ctx->uPly - 1];
+ // But if we're using the HCE, consult the attack bitboards
+ // and trapped piece records too.
+ bbTo = COOR_TO_BB(mv.cTo);
+ if ((PIECE_VALUE(mv.pMoved) > VALUE_KNIGHT) &&
+ (0 != (bbMinorAttacksSnapshot[uOpponent] & bbTo)))
+ {
+ return (SCORE)(PIECE_VALUE(mv.pMoved) - VALUE_KNIGHT);
+ }
+ if ((PIECE_VALUE(mv.pMoved) > VALUE_ROOK) &&
+ (0 != (bbRookAttacksSnapshot[uOpponent] & bbTo)))
+ {
+ return (SCORE)(PIECE_VALUE(mv.pMoved) - VALUE_ROOK);
+ }
+ COOR cOwnTrapped = ctx->cTrapped[ctx->uPly - 1];
+ if (IS_ON_BOARD(cOwnTrapped) && (mv.cFrom != cOwnTrapped))
+ {
+ PIECE pTrapped = ctx->pTrapped[ctx->uPly - 1];
#ifdef DEBUG
- ASSERT(GET_COLOR(pTrapped) == FLIP(uOpponent));
+ ASSERT(GET_COLOR(pTrapped) == FLIP(uOpponent));
#endif
- return (SCORE)(PIECE_VALUE(pTrapped) / 2);
+ return (SCORE)(PIECE_VALUE(pTrapped) / 2);
+ }
}
return 0;
}
@@ -546,18 +529,24 @@ _ShipDepthBucket(ULONG uRawDepthBucket)
static ULONG
_ShipLateBucket(ULONG uRawLateBucket)
{
- // Raw indices 0 and 1 are never produced: uMoveNum is 1-based (so 0
- // is out), and the collection/live eligibility gate itself
- // (fPruneReduceEligible, search.c, requires uLegalMoves > 1) means
- // the raw value handed to MoveNumBucket is never less than 2
- // either. Shift the real range (2..) down by two so the shipped
- // array wastes no slot -- confirmed against the 2026-09-30
- // collection run, which showed ship_late bucket 0 permanently empty
- // under the old (-1) shift.
- if ((uRawLateBucket >= 2) && (uRawLateBucket <= 13)) return uRawLateBucket - 2;
- if (uRawLateBucket <= 21) return 12;
- if (uRawLateBucket <= 31) return 13;
- return 14;
+ // 2026-10-21: rewritten to match dual_table_run{,_nnue}/dashboard.py's
+ // real, data-derived ship_late_17() exactly -- this function had
+ // drifted out of sync with the actual shipped shape baked into
+ // data.c (it still had the old 2026-09-30, pre-derive_late_buckets.py
+ // 15-bucket boundaries; the live table has been 16/17-bucket-shaped
+ // for a while). MUST stay in exact lockstep with ship_late_17() --
+ // see that function's own comment for the full raw-bucket story
+ // (why raw==0 gets its own dedicated slot now, why raw==1 is
+ // permanently unreachable and harmlessly lumped with raw==2).
+ if (0 == uRawLateBucket) return 0;
+ if (uRawLateBucket <= 1) return 1;
+ if (uRawLateBucket <= 11) return uRawLateBucket - 1;
+ if (uRawLateBucket <= 13) return 11;
+ if (uRawLateBucket <= 15) return 12;
+ if (uRawLateBucket <= 18) return 13;
+ if (uRawLateBucket <= 21) return 14;
+ if (uRawLateBucket <= 31) return 15;
+ return 16;
}
#define JOINT_TABLE_SENTINEL (0xFFFF)
@@ -583,6 +572,21 @@ _JointTablePredictedFHThousandths(IN ULONG uRawDepthBucket,
{
return JOINT_TABLE_SENTINEL;
}
+ // 2026-10-21: two tables compiled in (HCE-collected and NNUE-
+ // collected, see chess.h's g_uJointFHThousandths_NNUE comment) --
+ // runtime-selected, not a build-time #ifdef, since NNUE eval
+ // itself is already a runtime switch in this binary. Both flags
+ // checked (not just g_fUseNnueEval) for the same reason eval.c's
+ // own NNUE dispatch does: g_fUseNnueEval alone doesn't guarantee
+ // real weights are behind it.
+ if (g_fUseNnueEval && g_fNnueWeightsLoaded)
+ {
+ return g_uJointFHThousandths_NNUE[_ShipDepthBucket(uRawDepthBucket)]
+ [uRawDumbBucket]
+ [uRawGapBucket]
+ [_ShipLateBucket(uRawLateBucket)]
+ [uRawImproveBucket];
+ }
return g_uJointFHThousandths[_ShipDepthBucket(uRawDepthBucket)]
[uRawDumbBucket]
[uRawGapBucket]
@@ -612,10 +616,24 @@ _BetaTablePredictedFHThousandths(IN ULONG uRawDepthBucket,
IN ULONG uOwnImproveBucket,
IN ULONG uEnemyImproveBucket)
{
- USHORT uRawGap = g_uBetaFHThousandths[_ShipDepthBucket(uRawDepthBucket)]
- [uGapBetaBucket]
- [uOwnImproveBucket]
- [uEnemyImproveBucket];
+ USHORT uRawGap;
+
+ // Same runtime HCE/NNUE table selection as
+ // _JointTablePredictedFHThousandths above -- see its comment.
+ if (g_fUseNnueEval && g_fNnueWeightsLoaded)
+ {
+ uRawGap = g_uBetaFHThousandths_NNUE[_ShipDepthBucket(uRawDepthBucket)]
+ [uGapBetaBucket]
+ [uOwnImproveBucket]
+ [uEnemyImproveBucket];
+ }
+ else
+ {
+ uRawGap = g_uBetaFHThousandths[_ShipDepthBucket(uRawDepthBucket)]
+ [uGapBetaBucket]
+ [uOwnImproveBucket]
+ [uEnemyImproveBucket];
+ }
if (JOINT_TABLE_SENTINEL == uRawGap)
{
return JOINT_TABLE_SENTINEL;
@@ -623,6 +641,93 @@ _BetaTablePredictedFHThousandths(IN ULONG uRawDepthBucket,
return (ULONG)(100000 - uRawGap);
}
+// Buckets a predicted-FH%-thousandths value (from either table above,
+// or one of the two PLY_INFO.uParentMoveFHEstimate sentinels) into one
+// of NODE_TYPE_FH_BANDS coarse bands for g_NodeTypeGuessTable's (data.c)
+// lookup. MOVE_FH_ESTIMATE_NO_DATA and JOINT_TABLE_SENTINEL are the
+// same numeric value (0xFFFF) on purpose -- see both macros' comments.
+ULONG
+FHThousandthsToBand(IN ULONG uFHThousandths)
+{
+ if (MOVE_FH_ESTIMATE_NONLEFTOVER == uFHThousandths) return 1;
+ if (MOVE_FH_ESTIMATE_NO_DATA == uFHThousandths) return 0;
+ if (uFHThousandths < 50000) return 2;
+ if (uFHThousandths < 80000) return 3;
+ if (uFHThousandths < 96000) return 4;
+ if (uFHThousandths < 99900) return 5;
+ return 6;
+}
+
+// Refines CommonSearchInit's ancestor-only NODE_TYPE guess (above,
+// this file) using two more signals that aren't available yet at
+// CommonSearchInit time (before iEval is computed): this node's own
+// predicted FH% (from whichever of the alpha/beta tables applies --
+// the same two call sites Search()'s nullmove block already consults
+// them from) and the PARENT's predicted FH% for the move that led
+// here (PLY_INFO.uParentMoveFHEstimate, chess.h -- written by this
+// file's SearchOneMove before it recursed into this node). See
+// data.c's g_NodeTypeGuessTable for how the three signals combine and
+// the measurement behind it (90.7% vs today's 72.3%, on the
+// scope-limited population described there). Deliberately a no-op on
+// PV nodes (not a CUT/ALL question) and not applied in QSearch (whose
+// own eval/stand-pat timing differs enough to warrant its own look,
+// if ever extended there).
+//
+// Returns the local (this-node, non-PV-only) predicted FH thousandths
+// it just computed -- MOVE_FH_ESTIMATE_NO_DATA if neither the beta nor
+// alpha branch applied (including the PV early-return). Search()'s own
+// static-nullmove/LMR-skip blocks need the exact same table lookup,
+// keyed off the exact same (iEval>iBeta) / (iEval<=iAlpha) branch and
+// the exact same bucket inputs -- returning it here lets the caller
+// reuse it instead of redoing the same data.c table consult a second
+// time per node. This is the hottest function in the engine by
+// profiler; don't reintroduce the duplicate lookup.
+ULONG
+_RefineNodeTypeGuess(SEARCHER_THREAD_CONTEXT *ctx,
+ SCORE iEval,
+ SCORE iAlpha,
+ SCORE iBeta,
+ ULONG uDepth,
+ SCORE iEnemyImprovement,
+ ULONG uRawImproveB)
+{
+ PLY_INFO *pi = &ctx->sPlyInfo[ctx->uPly];
+ ULONG uAncestorGuess;
+ ULONG uLocalFH = MOVE_FH_ESTIMATE_NO_DATA;
+ ULONG uDepthInPlies;
+
+ if (NODE_PV == pi->eNodeType)
+ {
+ return(uLocalFH);
+ }
+ uAncestorGuess = (NODE_CUT == pi->eNodeType) ? 0 : 1;
+ uDepthInPlies = uDepth / ONE_PLY;
+ if (iEval > iBeta)
+ {
+ uLocalFH = _BetaTablePredictedFHThousandths(
+ DepthBucket(uDepthInPlies),
+ GapBetaBucket((SCORE)(iEval - iBeta)),
+ uRawImproveB,
+ ImproveBucket(iEnemyImprovement));
+ }
+ else if (iEval <= iAlpha)
+ {
+ uLocalFH = _JointTablePredictedFHThousandths(
+ DepthBucket(uDepthInPlies),
+ 0, // hang bucket: assume nothing hung
+ GapCpBucket((SCORE)(iAlpha - iEval)),
+ 0, // late bucket: real whole-node sample
+ uRawImproveB);
+ }
+
+ pi->eNodeType = g_NodeTypeGuessTable
+ [uAncestorGuess]
+ [FHThousandthsToBand(uLocalFH)]
+ [FHThousandthsToBand(pi->uParentMoveFHEstimate)];
+
+ return(uLocalFH);
+}
+
// 2026-10-02: reverted from the DELIBERATE EXTREME EXPERIMENT values
// (9000/8300/1000) back to the original p75/p25/p5-derived calibration
// (72/5/1) -- that experiment was measured while the dynamic gate
@@ -1734,6 +1839,20 @@ Return value:
FLAG fThisMoveLMPPruned;
FLAG fMoveIsPruneReduceEligible = FALSE;
SCORE iScore;
+ // This move's own predicted FH% (thousandths), to be stashed onto
+ // the CHILD ply's PLY_INFO before we recurse into it -- see
+ // chess.h's PLY_INFO.uParentMoveFHEstimate comment. A real table
+ // lookup only ever happens for a leftover move that also clears
+ // CanWeReduceOrPruneMove's eligibility gate (just below), so:
+ // non-leftover (hash move, good capture, killer, etc) defaults to
+ // MOVE_FH_ESTIMATE_NONLEFTOVER -- deliberately a high-confidence
+ // band, not "unknown" (see that macro's comment) -- while a
+ // leftover move defaults to MOVE_FH_ESTIMATE_NO_DATA (real
+ // "unknown"), overwritten with an actual value below if the table
+ // has one.
+ ULONG uThisMoveFHEstimate = (TRUE == fIsLeftoverMove) ?
+ MOVE_FH_ESTIMATE_NO_DATA :
+ MOVE_FH_ESTIMATE_NONLEFTOVER;
#ifdef PERF_COUNTERS
ULONG uThisMoveJointExtraTier = 0;
FLAG fThisMoveJointExtraEligible = FALSE;
@@ -1837,6 +1956,9 @@ Return value:
uRawGapB,
uRawLateB,
uRawImproveB);
+ // JOINT_TABLE_SENTINEL and MOVE_FH_ESTIMATE_NO_DATA are the
+ // same numeric value on purpose -- see both macros' comments.
+ uThisMoveFHEstimate = uPredictedFH;
// Late move pruning; skip searching under this move?
#ifdef FH_TABLE_COLLECTION
@@ -1896,6 +2018,9 @@ Return value:
#ifdef PERF_COUNTERS
INC(ctx->sCounters.tree.u64LMPPruned);
#endif
+ TDMove(ctx, uDepth, iAlpha, iBeta,
+ IS_CAPTURE_OR_PROMOTION(mv) ? "CAP" : "QUIET",
+ "LMP", 0.0, 0.0, 0, TRUE);
UnmakeMove(ctx, mv);
ASSERT(PositionsAreEquivalent(&ctx->sPosition, &pi->sPosition));
#ifdef PERF_COUNTERS
@@ -1988,6 +2113,13 @@ Return value:
pi->iExtensionAmount = iExtend;
pf->iCumulativeExtend += iExtend;
+ // Stash this move's predicted FH% on the CHILD ply (MakeMove above
+ // already incremented ctx->uPly) -- see chess.h's PLY_INFO.
+ // uParentMoveFHEstimate comment. Must happen before the recursive
+ // Search() calls below, and after any point where mv could still
+ // change (it can't past here).
+ ctx->sPlyInfo[ctx->uPly].uParentMoveFHEstimate = uThisMoveFHEstimate;
+
// Compute the next search depth for this move/subtree.
uNextDepth = uDepth - ONE_PLY + iExtend;
if (uNextDepth >= MAX_DEPTH_PER_SEARCH) {
@@ -2191,6 +2323,12 @@ Return value:
}
}
#endif
+ TDMove(ctx, uDepth, iAlpha, iBeta,
+ fFullWindow ? "FIRST" : IS_CAPTURE_OR_PROMOTION(mv) ? "CAP" : "QUIET",
+ (iExtend < 0) ? "LMR" : (iExtend > 0) ? "EXT" : "FULL",
+ (iExtend < 0) ? -(double)iExtend / ONE_PLY : 0.0,
+ (iExtend > 0) ? (double)iExtend / ONE_PLY : 0.0,
+ iScore, FALSE);
UnmakeMove(ctx, mv);
pf->iCumulativeExtend -= iExtend;
ASSERT(PositionsAreEquivalent(&ctx->sPosition, &pi->sPosition));
@@ -2302,6 +2440,15 @@ Return value:
ctx->sMoveStack.mvf[x].mv = mv; // <-- update the move's flags, WouldGiveCheck is slow
if (TRUE == MakeMove(ctx, mv))
{
+ // This loop recurses into Search() directly, not through
+ // SearchOneMove (which stashes this on every OTHER
+ // recursion -- see chess.h's PLY_INFO.uParentMoveFHEstimate
+ // comment). An IID rescore pass has no real leftover-move
+ // table lookup to report here -- MOVE_FH_ESTIMATE_NO_DATA
+ // ("unknown"), not MOVE_FH_ESTIMATE_NONLEFTOVER (which
+ // would falsely claim high confidence).
+ ctx->sPlyInfo[ctx->uPly].uParentMoveFHEstimate =
+ MOVE_FH_ESTIMATE_NO_DATA;
if (-INFINITY_SCORE == iBestScore)
{
iScore = -Search(ctx, -iBeta, -iAlpha, uDepth);
@@ -2670,7 +2817,6 @@ Return value:
// describe structurally unreachable from this site, not just
// defended against.
UpdatePV(ctx, DRAWMOVE);
- DTTRACE("<i bold=\"draw\"/>");
goto end;
}
@@ -2730,7 +2876,6 @@ Return value:
// mechanism and the move.c:881 assert it produces).
pi->mvBest.uMove = 0;
pi->PV[ctx->uPly].uMove = 0;
- DTTRACE("<toodeep/>");
goto end;
}
@@ -2993,12 +3138,28 @@ TryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
ASSERT(pos->uToMove != pi->sPosition.uToMove);
INC(ctx->sCounters.tree.u64NullMoves);
+ // This function recurses into Search() directly, not through this
+ // file's own SearchOneMove (which stashes this on every OTHER
+ // recursion -- see chess.h's PLY_INFO.uParentMoveFHEstimate
+ // comment). A null move has no per-move FH% table lookup to
+ // report -- MOVE_FH_ESTIMATE_NO_DATA ("unknown"), not
+ // MOVE_FH_ESTIMATE_NONLEFTOVER (which would falsely claim high
+ // confidence). Without this, the child ply's slot reads whatever
+ // was last left there -- caught by valgrind as a real
+ // uninitialised-value read inside search.c's _RefineNodeTypeGuess.
+ ctx->sPlyInfo[ctx->uPly].uParentMoveFHEstimate = MOVE_FH_ESTIMATE_NO_DATA;
+
// (Reduced depth) search under the nullmove; don't allow two
// nullmoves in a row.
ASSERT(pf->fAvoidNullmove == FALSE);
pf->fAvoidNullmove = TRUE;
*piNullScore = -Search(ctx, -iBeta, -iBeta + 1, uNullDepth);
pf->fAvoidNullmove = FALSE;
+ // depth=uNullDepth here (the already-reduced depth passed to the
+ // probe), not the pre-reduction parent depth, which this function
+ // never receives -- r is left 0.0 rather than faked.
+ TDMove(ctx, uNullDepth, iAlpha, iBeta, "NULL", "NMP", 0.0, 0.0,
+ *piNullScore, FALSE);
// Check the results
if (*piNullScore >= iBeta)
@@ -3108,6 +3269,8 @@ TryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
if ((mv.cFrom == (pi-1)->mv.cTo) && ((pi-1)->iExtensionAmount < 0))
{
ASSERT(GET_COLOR(mv.pMoved) == GET_COLOR((pi-1)->mv.pMoved));
+ TDMove(ctx, uNullDepth, iAlpha, iBeta, "NULL", "NULLBAILOUT",
+ 0.0, 0.0, iAlpha - 1, FALSE);
UnmakeMove(ctx, NULLMOVE);
ASSERT(PositionsAreEquivalent(pos, &pi->sPosition));
*piNullScore = iAlpha - 1;