diff options
| author | Scott Gasch <[email protected]> | 2026-09-25 08:44:25 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-25 08:44:25 -0700 |
| commit | 9b28080f8b3dca4588cb2aaaf748451f45e3c626 (patch) | |
| tree | 54e0aff5d6ee1520c590c7c04a5ae2f8299e7497 /src/searchsup.c | |
| parent | 66740dc89c7351c120c25788d0079055034f4942 (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.c | 128 |
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; |
