summaryrefslogtreecommitdiff
path: root/src/searchsup.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-25 08:44:25 -0700
committerScott Gasch <[email protected]>2026-09-25 08:44:25 -0700
commit9b28080f8b3dca4588cb2aaaf748451f45e3c626 (patch)
tree54e0aff5d6ee1520c590c7c04a5ae2f8299e7497 /src/searchsup.c
parent66740dc89c7351c120c25788d0079055034f4942 (diff)
Two ideas:
1. Nullmove R=3 + depth/6 along with some ShouldWeDoNullmoveSearch tweaking and calibrating. 2. Probe EGTB everywhere in the search as long as the table/position is resident in memory.
Diffstat (limited to 'src/searchsup.c')
-rw-r--r--src/searchsup.c128
1 files changed, 106 insertions, 22 deletions
diff --git a/src/searchsup.c b/src/searchsup.c
index 781c702..0201219 100644
--- a/src/searchsup.c
+++ b/src/searchsup.c
@@ -1382,17 +1382,26 @@ ULONG
SelectNullmoveRFactor(SEARCHER_THREAD_CONTEXT *ctx,
INT uDepth)
{
- ULONG u = TWO_PLY;
+ ULONG u;
ULONG uMaxPiecesPerSide = MAXU(TotalNonPawnCount(&ctx->sPosition, WHITE),
TotalNonPawnCount(&ctx->sPosition, BLACK));
ASSERT(uMaxPiecesPerSide <= 16);
- if ((uDepth > 8 * ONE_PLY) ||
- ((uDepth > 6 * ONE_PLY) && (uMaxPiecesPerSide >= 3)))
+ if (uMaxPiecesPerSide < 3)
{
- u = THREE_PLY;
+ // Sparse material on the mover's side: stay with the
+ // conservative, non-growing reduction rather than risk a
+ // zugzwang-prone position getting an aggressive R. (The
+ // TotalNonPawnCount < 4 verification search in
+ // TryNullmovePruning is the other half of this safety net.)
+ u = TWO_PLY;
+ }
+ else
+ {
+ // Crafty-style growth: R = 3 + depth / 6 (in whole plies),
+ // scaled into our fractional-ply depth units.
+ u = THREE_PLY + ((uDepth / (6 * ONE_PLY)) * ONE_PLY);
}
- ASSERT((u == TWO_PLY) || (u == THREE_PLY));
return(u);
}
@@ -1403,14 +1412,82 @@ FLAG WeShouldTryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
SCORE iEval,
SCORE iImprovement,
ULONG uNullDepth)
+/**
+
+Routine description:
+
+ Two-stage try/skip decision, replacing the old flat >2-non-pawn-
+ piece gate plus per-depth eval-margin table. Calibrated from a
+ 30K-position, sd-10, ~9-billion-attempt null-move precision/recall/
+ EV harness (2026-09-24, two revisions -- see
+ nullmove_calib/run_2026-09-24_sd10_30k_v2/, the second and
+ authoritative one). iImprovement and uNullDepth are currently
+ unused: the Stage 1 thresholds below are flat, not depth- or
+ improvement-scaled, and Stage 2 doesn't need its own gate at all
+ (see below). Kept in the signature to avoid touching call sites.
+
+ Stage 1 (safety): is a null-move fail-high here even trustworthy?
+ Gated on the MOVER's own non-pawn piece count -- TotalNonPawnCount
+ (pos, pos->uToMove), NOT MAX(white,black) as an earlier revision of
+ this gate (and of the calibration harness that measured it) used.
+ Zugzwang is a property of the side being given the free "pass", so
+ the opponent's material shouldn't matter -- confirmed empirically:
+ (maxMaterial=6+, moverMaterial=2) measured a 0.30% false-positive
+ rate, indistinguishable from the worst low-material cells overall,
+ despite "6+" sounding safe. A materially-heavier opponent does not
+ protect a mover that is itself thin on material.
+
+ Measured false-positive rate (a null fail-high the real search
+ didn't confirm) by mover's own material x eval-vs-beta deficit:
+
+ deficit<-300 -300..-100 -100..0 0..100 100..300 300+
+ mover 0-1 pcs: 5.57% 1.35% 0.82% 1.02% 0.28% 0.12%
+ mover 2 pcs: 3.73% 0.19% 0.31% 0.32% 0.02% 0.01%
+ mover 3-5 pcs: 0.91% 0.20% 0.09% 0.002% 0.001% 0.0005%
+ mover 6+ pcs: 0.02% 0.002% 0.001% 0.0002% 0.0001% 0.0001%
+
+ 6+ pieces is safe (<=0.02%) at EVERY deficit measured, even the most
+ negative -- no deficit floor needed there at all. 2 pieces is
+ actually salvageable (0.01-0.02%, comparable to the safest cells
+ overall) once deficit >= 100, unlike a flatly-disabled <=2 rule
+ would assume -- but <=1 piece never gets safe (worst case 0.12% at
+ its *best* deficit band), so that tier is disabled outright rather
+ than deficit-gated.
+
+ Stage 2 (profitability/EV): given Stage 1 passes, is trying still
+ worth the reduced-depth probe's node cost? Checked directly against
+ the same run's EV data (EV = successRate*(realLoopCost-nullProbeCost)
+ - (1-successRate)*nullProbeCost, in nodes saved per attempt) across
+ every (depth, material) cell that survives the Stage 1 gate above:
+ EV was strongly positive everywhere measured, from low single digits
+ at shallow depth up to tens of thousands of nodes at deep probes,
+ never negative. Stage 1 is already doing essentially all the
+ discriminating work, so Stage 2 currently just returns TRUE
+ unconditionally once Stage 1 passes -- no separate EV threshold is
+ justified by this data. Revisit if a future, deeper or wider
+ calibration run finds a safe-but-unprofitable pocket this one
+ didn't sample.
+
+Parameters:
+
+ SEARCHER_THREAD_CONTEXT *ctx,
+ SCORE iAlpha,
+ SCORE iBeta,
+ SCORE iEval,
+ SCORE iImprovement,
+ ULONG uNullDepth
+
+Return value:
+
+ FLAG
+
+**/
{
- static SCORE iSkipShallowNullIfThisFarBelowAlpha[] = {
- 700, 950, 1110, 1150, 1190, 1230, 1270, 0
- };
POSITION *pos = &ctx->sPosition;
PLY_INFO *pi = &ctx->sPlyInfo[ctx->uPly];
CUMULATIVE_SEARCH_FLAGS *pf = &ctx->sSearchFlags;
- ULONG u;
+ ULONG uMoverPiecesPerSide;
+ SCORE iDeficit;
ASSERT(IS_VALID_SCORE(iAlpha) && IS_VALID_SCORE(iBeta));
ASSERT(iAlpha < iBeta);
@@ -1418,25 +1495,32 @@ FLAG WeShouldTryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
ASSERT(uNullDepth < MAX_PLY_PER_SEARCH * ONE_PLY);
if ((FALSE == pf->fAvoidNullmove) &&
- (TotalNonPawnCount(pos, pos->uToMove) > 2) &&
(FALSE == pi->fInCheck) &&
(iBeta != +INFINITY_SCORE) &&
(NODE_PV != pi->eNodeType))
{
- if (uNullDepth <= 6 * ONE_PLY)
+ uMoverPiecesPerSide = TotalNonPawnCount(pos, pos->uToMove);
+ iDeficit = iEval - iBeta;
+
+ // ---- Stage 1: safety ----
+ if (uMoverPiecesPerSide <= 1)
{
- u = uNullDepth / ONE_PLY;
- ASSERT(u <= 6);
- SCORE iMargin = iSkipShallowNullIfThisFarBelowAlpha[u];
- iMargin -= iImprovement; // Note: if improving, makes null more likely
- iMargin = MAX(0, iMargin); // if not, less likely.
- if ((iEval + iMargin <= iAlpha) ||
- ((iEval + iMargin / 2 <= iAlpha) &&
- (ValueOfMaterialInTroubleAfterNull(ctx, pos->uToMove))))
- {
- return FALSE;
- }
+ return FALSE; // never measured safe, even at its best deficit
+ }
+ if ((uMoverPiecesPerSide == 2) && (iDeficit < 100))
+ {
+ return FALSE;
+ }
+ if ((uMoverPiecesPerSide <= 5) && (iDeficit < -100))
+ {
+ return FALSE;
}
+ // 6+ pieces: no deficit floor -- safe everywhere measured.
+
+ // ---- Stage 2: efficiency/EV ----
+ // EV is strongly positive at every depth/material cell that
+ // survives Stage 1 (see routine comment) -- nothing further to
+ // gate on here today.
return TRUE;
}
return FALSE;