summaryrefslogtreecommitdiff
path: root/src/searchsup.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/searchsup.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/searchsup.c')
-rw-r--r--src/searchsup.c131
1 files changed, 114 insertions, 17 deletions
diff --git a/src/searchsup.c b/src/searchsup.c
index decce3b..66851a5 100644
--- a/src/searchsup.c
+++ b/src/searchsup.c
@@ -383,6 +383,30 @@ Return value:
{
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];
+#ifdef DEBUG
+ ASSERT(GET_COLOR(pTrapped) == FLIP(uOpponent));
+#endif
+ return (SCORE)(PIECE_VALUE(pTrapped) / 2);
+ }
return 0;
}
@@ -428,6 +452,57 @@ GapCpBucket(SCORE iGapCp)
return MINU(u, GAP_CP_BUCKETS - 1);
}
+// See chess.h's IMPROVE_BUCKETS comment for the bucket shape. Unlike
+// HangCpBucket/GapCpBucket, there is no single "safe/below-threshold"
+// catchall here -- iImprovement is genuinely signed and two-sided (a
+// node legitimately being worse than two plies ago is an expected,
+// common outcome, not a degenerate corner case), so this buckets both
+// tails symmetrically around a center "didn't really move" bucket
+// instead of collapsing one whole sign onto bucket 0.
+ULONG
+ImproveBucket(SCORE iImprovement)
+{
+ // Center bucket (index 5 of 11): "didn't really move" either way.
+ if ((iImprovement > -IMPROVE_CP_CENTER_WIDTH) &&
+ (iImprovement < IMPROVE_CP_CENTER_WIDTH))
+ {
+ return 5;
+ }
+ if (iImprovement > 0)
+ {
+ // 6..10: getting better, 100cp-wide bins, 10 is the 500cp+
+ // overflow.
+ ULONG u = 6 + (((ULONG)iImprovement - IMPROVE_CP_CENTER_WIDTH) /
+ IMPROVE_BUCKET_WIDTH);
+ return MINU(u, IMPROVE_BUCKETS - 1);
+ }
+ // 0..4: getting worse, 100cp-wide bins, 0 is the 500cp+ overflow
+ // (most negative == smallest index, mirroring 6..10's direction).
+ {
+ ULONG uMag = (ULONG)(-iImprovement) - IMPROVE_CP_CENTER_WIDTH;
+ ULONG u = 4 - (uMag / IMPROVE_BUCKET_WIDTH);
+
+ if (uMag / IMPROVE_BUCKET_WIDTH >= 5)
+ {
+ return 0;
+ }
+ return u;
+ }
+}
+
+// See chess.h's GAP_BETA_BUCKETS comment. Unlike ImproveBucket, this
+// is called only where the caller has already gated iEval > iBeta
+// (the static-NM/beta-table's own eligibility), so iGapBeta is always
+// >= 0 here -- no symmetric/center bucket needed, just plain
+// 100cp-wide bins from 0, wide enough (2000cp) to find where FH%
+// actually saturates.
+ULONG
+GapBetaBucket(SCORE iGapBeta)
+{
+ ULONG u = (ULONG)iGapBeta / GAP_BETA_BUCKET_WIDTH;
+ return MINU(u, GAP_BETA_BUCKETS - 1);
+}
+
// See chess.h's MOVE_NUM_BUCKETS comment. uMoveNum is 1-based (the
// caller passes uLegalMoves, right after MakeMove succeeded) -- index
// 0 is never produced.
@@ -498,7 +573,8 @@ USHORT
_JointTablePredictedFHThousandths(IN ULONG uRawDepthBucket,
IN ULONG uRawDumbBucket,
IN ULONG uRawGapBucket,
- IN ULONG uRawLateBucket)
+ IN ULONG uRawLateBucket,
+ IN ULONG uRawImproveBucket)
{
if (1 == uRawDumbBucket)
{
@@ -507,7 +583,41 @@ _JointTablePredictedFHThousandths(IN ULONG uRawDepthBucket,
return g_uJointFHThousandths[_ShipDepthBucket(uRawDepthBucket)]
[uRawDumbBucket]
[uRawGapBucket]
- [_ShipLateBucket(uRawLateBucket)];
+ [_ShipLateBucket(uRawLateBucket)]
+ [uRawImproveBucket];
+}
+
+// Looks up this node's predicted FH% (thousandths of a percent, i.e.
+// 100000 == 100.0%) from g_uBetaFHThousandths, given the raw depth
+// bucket (shipped via _ShipDepthBucket same as the alpha lookup
+// above) and the gap/own-improve/enemy-improve buckets. Caller is
+// responsible for eligibility (iEval > iBeta, etc.) -- this function
+// does no gating of its own and returns JOINT_TABLE_SENTINEL (0xFFFF)
+// for any below-floor cell regardless.
+//
+// g_uBetaFHThousandths itself stores the GAP TO 100% (thousandths of
+// a percentage point), not FH% directly -- a USHORT can't hold
+// FH%*1000 up to 100000, so the direct encoding would saturate every
+// cell above FH%=65.534% into one bucket, right where this table is
+// actually read. Storing the complement instead keeps full
+// resolution across the near-100% range that matters. This function
+// re-inverts back to a true FH%*1000 before returning, so callers get
+// ordinary "bigger == more confident" semantics.
+ULONG
+_BetaTablePredictedFHThousandths(IN ULONG uRawDepthBucket,
+ IN ULONG uGapBetaBucket,
+ IN ULONG uOwnImproveBucket,
+ IN ULONG uEnemyImproveBucket)
+{
+ USHORT uRawGap = g_uBetaFHThousandths[_ShipDepthBucket(uRawDepthBucket)]
+ [uGapBetaBucket]
+ [uOwnImproveBucket]
+ [uEnemyImproveBucket];
+ if (0xFFFF == uRawGap)
+ {
+ return 0xFFFF;
+ }
+ return (ULONG)(100000 - uRawGap);
}
// 2026-10-02: reverted from the DELIBERATE EXTREME EXPERIMENT values
@@ -1349,8 +1459,8 @@ Return value:
if (pos->uNonPawnMaterial[FLIP(pos->uToMove)] >
(VALUE_KING + VALUE_BISHOP))
{
- *piExtend += THREE_QUARTERS_PLY;
- INC(ctx->sCounters.extension.uCheckByQuarterPly[THREE_QUARTERS_PLY / QUARTER_PLY]);
+ *piExtend += ONE_PLY;
+ INC(ctx->sCounters.extension.uCheckByQuarterPly[ONE_PLY / QUARTER_PLY]);
}
else
{
@@ -1437,19 +1547,6 @@ Return value:
if ((iRoughEval >= (g_iRootScore[uColor] - _window[ctx->uPly] * 2)) &&
(iRoughEval <= (g_iRootScore[uColor] + _window[ctx->uPly] * 2)))
{
- // Entering the endgame extension
- if ((ctx->sPlyInfo[1].uTotalNonPawns > 2) &&
- ((TotalNonPawnCount(pos, BLACK) +
- TotalNonPawnCount(pos, WHITE)) == 2))
- {
- if ((mv.pCaptured && !IS_PAWN(mv.pCaptured)) ||
- (mvLast.pCaptured && !IS_PAWN(mvLast.pCaptured)))
- {
- *piExtend += HALF_PLY;
- INC(ctx->sCounters.extension.uEndgame);
- }
- }
-
// Recapture extension. Own tighter (1x) window restored --
// measured on ECM.721 (ringers): sharing the outer 2x window
// grew this position's recapture-fire count 6.8x vs HEAD