diff options
| author | Scott Gasch <[email protected]> | 2026-10-13 11:12:35 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-13 11:12:35 -0700 |
| commit | 7400001fc2d43a507b54281f9860c60177860fe5 (patch) | |
| tree | 0ab8e3d1bd5fe54cc7f2883e38da67c3bfac36d8 /src/search.c | |
| parent | 78881b8e6422b7980e5f71780ae4bed862b93e72 (diff) | |
Add 5D FH% joint table (improve axis) + beta-side static-null-move
table, wire up static null-move pruning; misc searchsup.c tuning;
--cpus=4 fix; multi-pool match_play.py openings.
data.c: g_uJointFHThousandths grows a 5th "improve" axis
([12][11][11][15] -> [12][11][11][16][11]); new
g_uBetaFHThousandths[12][21][11][11] table added. chess.h/searchsup.c/
search.c/split.c: minimal consumer wiring for the improve axis
(ImproveBucket(), extended _JointTablePredictedFHThousandths
signature/callers).
search.c: new static (score-only, no verification search) null-move
cutoff ahead of the existing real null-move check -- when iEval > iBeta
by enough that the new beta-side table predicts a real null-move
search would fail high with >95% confidence, take the cutoff on the
table's say-so instead of paying for the verification search.
searchsup.c: EstimateHangingMaterialCp now gives half credit when the
mover has an unrelated piece already flagged trapped at node entry;
bumped the SEE>=0 check extension from 3/4 ply to a full ply when the
opponent has significant material left; removed the "entering the
endgame extension" bonus (recapture extension and its window check
untouched).
main.c: accept "--opt=value" in addition to "--opt value" on the
command line (a bare "--cpus=4" used to silently fail to match and
leave uNumProcessors at its default of 1); also fixes a latent
off-by-one that could read one past the end of argv for a value-taking
flag given as the last command-line argument.
unix.c: brace-scoping cleanup around signal handler setup, no behavior
change.
eval_tune/match_play.py, test_vs_head.sh: sample opening positions from
multiple PGN pools (twic_filtered.pgn + lichess*.pgn) instead of one,
for more opening diversity in match-play testing.
Brought out of a larger stashed changeset one piece at a time, each
verified independently against a --cpus 1 / --cpus 4 ratio benchmark
to rule out the MP scaling regression that changeset originally
triggered (see SCOTT_IS_A_MORON.md) -- no regression reproduced in any
of the pieces here. Playing-strength impact measured as neutral via
self-play (170 games, llr=-0.04, score=0.500) despite ~1 ply deeper
average search. The stashed changeset's EFP margin rewrite, LMP
threshold rewrite, and a disabled null-move-skip experiment are
deliberately excluded -- those need their own careful, separate
commits and testing. The stash's eval.c retuning is abandoned (measured
~even/worse over a ~700-game overnight self-play run).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/search.c')
| -rw-r--r-- | src/search.c | 50 |
1 files changed, 49 insertions, 1 deletions
diff --git a/src/search.c b/src/search.c index 89db55c..ae6ba01 100644 --- a/src/search.c +++ b/src/search.c @@ -730,6 +730,17 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, { iImprovement = (iEval - ctx->sPlyInfo[ctx->uPly - 2].iEval); } + // 2026-10-10: opponent's own eval-trend, for the beta-side static- + // NM table below (g_uBetaFHThousandths) -- needs the move before + // the mover's own, hence uPly - 1/uPly - 3 rather than iImprovement's + // uPly - 2, and its own, deeper eligibility guard (uPly - 3 must be + // a valid index). + SCORE iEnemyImprovement = 0; + if (ctx->uPly > 2) + { + iEnemyImprovement = (ctx->sPlyInfo[ctx->uPly - 1].iEval - + ctx->sPlyInfo[ctx->uPly - 3].iEval); + } // If the Eval call above was a full eval (i.e. not lazy or super // lazy), there are usable attack bitboards. We'll consult them @@ -828,6 +839,42 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, } #endif + // 2026-10-10: static (score-only, no verification search) null- + // move cutoff -- if the eval already clears beta by a wide enough + // margin that the beta-side FH% table (g_uBetaFHThousandths) + // predicts a real null-move search would fail high here with + // >95% confidence, skip the verification search entirely and + // take the cutoff on the table's say-so. Same eligibility + // (not in check, non-PV, not already avoiding null, no fThreat) + // as the real null-move check just below, plus iEval > iBeta + // (the table's own domain) and iBeta < +NMATE (don't short- + // circuit a mate score on a magnitude-based prediction). + if ((FALSE == fSkipNull) && + (FALSE == fThreat) && + (FALSE == pi->fInCheck) && + (NODE_PV != pi->eNodeType) && + (iBeta < +NMATE) && + (iEval > iBeta)) + { + ULONG uStaticNMDepthB = DepthBucket(uDepth / ONE_PLY); + ULONG uRawImproveB = ImproveBucket(iImprovement); + ULONG uRawEnemyImproveB = ImproveBucket(iEnemyImprovement); + + INC(ctx->sCounters.tree.u64QuickNullMoves); + ULONG uBetaPredictedFHThousandths = _BetaTablePredictedFHThousandths( + uStaticNMDepthB, + GapBetaBucket((SCORE)(iEval - iBeta)), + uRawImproveB, + uRawEnemyImproveB); + if ((uBetaPredictedFHThousandths != 0xFFFF) && + (uBetaPredictedFHThousandths > 95000)) + { + INC(ctx->sCounters.tree.u64QuickNullMoveSuccess); + iBestScore = iBeta; + goto end; + } + } + if (!fSkipNull && !fThreat && WeShouldTryNullmovePruning(ctx, @@ -1291,8 +1338,9 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, // not enough data (never, anymore) or when the // buckets are noisy (e.g. "dumb" bucket 1 is crazy, // no signal). + ULONG uRawImproveB = ImproveBucket(iImprovement); ULONG uPredictedFH = _JointTablePredictedFHThousandths( - uRawDepthB, uRawDumbB, uRawGapB, uRawLateB); + uRawDepthB, uRawDumbB, uRawGapB, uRawLateB, uRawImproveB); // Late move pruning; skip searching under this move? // Because this is a prune, we only do it when we are |
