summaryrefslogtreecommitdiff
path: root/src/search.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-13 11:12:35 -0700
committerScott Gasch <[email protected]>2026-10-13 11:12:35 -0700
commit7400001fc2d43a507b54281f9860c60177860fe5 (patch)
tree0ab8e3d1bd5fe54cc7f2883e38da67c3bfac36d8 /src/search.c
parent78881b8e6422b7980e5f71780ae4bed862b93e72 (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.c50
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