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/searchsup.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/searchsup.c')
| -rw-r--r-- | src/searchsup.c | 131 |
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 |
