summaryrefslogtreecommitdiff
path: root/src/eval.c
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-08-26 11:50:25 -0700
committerScott Gasch <[email protected]>2026-08-26 11:50:25 -0700
commit7857096f39e16619a42ee85b4aa593abd846b74a (patch)
treece697561b7e6c0978f53323256c0ab9038662e3e /src/eval.c
parent01f5b71fd484801f7b06df3bfb0156f5bb69fef7 (diff)
Replace ctx->uPositional with a data-calibrated Eval() return value.
uPositional was a per-thread EWMA of abs(material - true score) used to size lazy-eval and futility margins. It was history-derived (reflecting whatever recent, unrelated positions looked like) rather than derived from the position actually being margined, and its update/consumption was tangled with EVAL_HASH (now disabled). Eval() now takes an optional SCORE *piPositional out-param and fills it in on every return path: exact (abs(material-delta)) on a full eval, or an estimate from a new EstimatePositionalScore() on a lazy exit. EstimatePositionalScore()'s two terms (king-safety-defect-bucketed, and a flat residual for mobility/passers/everything else) are calibrated from ~1.6M measured full-eval samples (p90 of the actual swing), not guessed -- an initial guessed version measurably regressed ECM solve rate (630 vs a 650 baseline at sn=4M); the recalibrated version is back at parity (649/879). search.c's qsearch futility now reads the value Eval() just computed instead of the stale/shared ctx field. Also removes QSearchInDangerNoStandPat and SideCanStandPat, dead since the danger-hash check that fed them was already commented out (e08387a) -- they depended on the same enprise/ trapped-piece data this conversation is about to move off of g_PositionHash entirely. Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/eval.c')
-rwxr-xr-xsrc/eval.c169
1 files changed, 67 insertions, 102 deletions
diff --git a/src/eval.c b/src/eval.c
index 92d1edc..876e882 100755
--- a/src/eval.c
+++ b/src/eval.c
@@ -2227,20 +2227,47 @@ Return value:
static void
-_QuicklyEstimateKingSafetyTerm(IN POSITION *pos,
- IN OUT SCORE *piAlphaMargin,
- IN OUT SCORE *piBetaMargin)
+EstimatePositionalScore(IN POSITION *pos,
+ IN UNUSED PAWN_HASH_ENTRY *pHash,
+ IN OUT SCORE *piAlphaMargin,
+ IN OUT SCORE *piBetaMargin)
/**
Routine description:
- Before doing an early lazy eval, look at the position and try to
- quickly see if there is a large king safety positional component
- to the score.
+ Before doing an early lazy eval, look at the position and widen
+ the alpha/beta margins to account for the positional terms that
+ have not been computed yet at this point in Eval() -- king safety
+ plus a flat residual covering mobility, passers, and everything
+ else that genuinely requires attack-generation to know exactly.
+
+ Both terms below are calibrated from measured data (CALIBRATE_
+ POSITIONAL instrumentation, ~1.6M full-eval samples over an ECM
+ slice), not guessed: for each value, p90 of the *actual* score
+ swing between this point in Eval() and full-eval completion, so
+ the resulting margin is wrong (too narrow) on at most ~10% of
+ calls, in either bucket. See conversation history for the
+ percentile tables -- if re-tuning, regenerate them, don't hand-
+ edit these numbers.
+
+ Note: king safety and "everything else" turned out to be close to
+ independent of piece count and of each other, so summing their
+ two p90s (rather than deriving one joint p90) is a deliberately
+ conservative (wider than strictly necessary) combination.
+
+ O(1) -- no attack bitboard generation -- so it's safe to call
+ whenever the cheap material-only lazy check (the caller's
+ first-pass margin) wasn't enough to resolve the cutoff on its own.
Parameters:
POSITION *pos
+ PAWN_HASH_ENTRY *pHash : unused now (kept for call-site symmetry);
+ pHash->iScore is already folded into pos->iScore by this point,
+ and the passer-specific estimate this used to compute turned
+ out to be negligible next to the residual term below.
+ SCORE *piAlphaMargin, *piBetaMargin : widened in place, identically
+ (no measured basis for an asymmetric alpha/beta split)
Return value:
@@ -2248,98 +2275,21 @@ Return value:
**/
{
- //
- // IDEA: make this stuff dynamic like ctx->uPositional
- //
- static const SCORE iScoreByDefectCount[15] = {
- 33, 50, 88, 146, 215, 245, 280, 430, 550, 630, 646, 646, 646, 646, 646
+ // p90 of |true king-safety swing|, indexed by combined defect
+ // count (CountKingSafetyDefects(stm) + CountKingSafetyDefects(xsm)),
+ // clamped above index 10 (sparse data beyond that).
+ static const SCORE iKingSwingP90[11] = {
+ 47, 62, 87, 119, 169, 157, 181, 282, 342, 342, 385
};
- SCORE iPenaltyEst[2];
- SCORE iRet;
-
- ASSERT(CountKingSafetyDefects(pos, pos->uToMove) < 15);
- iPenaltyEst[WHITE] =
- iScoreByDefectCount[CountKingSafetyDefects(pos, WHITE)];
- ASSERT(iPenaltyEst[WHITE] > 0);
-
- ASSERT(CountKingSafetyDefects(pos, BLACK) < 15);
- iPenaltyEst[BLACK] =
- iScoreByDefectCount[CountKingSafetyDefects(pos, BLACK)];
- ASSERT(iPenaltyEst[BLACK] > 0);
-
- //
- // For the beta margin, assume our king safety penalty will stand
- // and our opponents will be less severe than we guess... net bonus
- // to them / our bonus not as high.
- //
- iRet = iPenaltyEst[pos->uToMove] - iPenaltyEst[FLIP(pos->uToMove)] / 2;
- iRet = MAX0(iRet);
- ASSERT(iRet >= 0);
- *piBetaMargin += iRet;
+ // p90 of |mobility + passers + everything else combined|, measured
+ // directly (no useful correlation found with piece count).
+ static const SCORE iResidualP90 = 154;
+ ULONG uDefects = (CountKingSafetyDefects(pos, WHITE) +
+ CountKingSafetyDefects(pos, BLACK));
+ SCORE iKingTerm = iKingSwingP90[MINU(10, uDefects)];
- //
- // For the alpha margin, assume our king safety penalty will not
- // be too bad and our opponents will be as bad as we guess... net
- // bonus to us / their offsetting bonus not as high.
- //
- iRet = iPenaltyEst[FLIP(pos->uToMove)] - iPenaltyEst[pos->uToMove] / 2;
- iRet = MAX0(iRet);
- ASSERT(iRet >= 0);
- *piAlphaMargin += iRet;
-}
-
-
-static void
-_QuicklyEstimatePasserBonuses(POSITION *pos,
- PAWN_HASH_ENTRY *pHash,
- SCORE *piAlphaMargin,
- SCORE *piBetaMargin)
-{
- SCORE iBonusEst[2] = {0, 0};
- SCORE iRet;
- ULONG u;
-
- //
- // Guess about the passer bonuses
- //
- if (pHash->bbPasserLocations[WHITE])
- {
- u = pos->uNonPawnMaterial[BLACK] - VALUE_KING;
- u /= VALUE_PAWN;
- u = MINU(31, u);
- ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0);
- iBonusEst[WHITE] =
- (CountBits(pHash->bbPasserLocations[WHITE]) *
- PASSER_BONUS_AS_MATERIAL_COMES_OFF[u]);
- }
- if (pHash->bbPasserLocations[BLACK])
- {
- u = pos->uNonPawnMaterial[WHITE] - VALUE_KING;
- u /= VALUE_PAWN;
- u = MINU(31, u);
- ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0);
- iBonusEst[BLACK] =
- (CountBits(pHash->bbPasserLocations[BLACK]) *
- PASSER_BONUS_AS_MATERIAL_COMES_OFF[u]);
- }
-
- //
- // For the beta margin, assume our passer bonus gets reduced and
- // our opponent's is enhanced... net bonus to them / our bonus
- // not as high.
- //
- iRet = iBonusEst[pos->uToMove] / 2 - iBonusEst[FLIP(pos->uToMove)];
- iRet = MAX0(iRet);
- *piBetaMargin += iRet;
-
- //
- // For the alpha bonus, assume our passer bonus gets doubled and
- // our opponent's is reduced. Net bonus to us / their bonus not
- // as high.
- //
- iRet = iBonusEst[pos->uToMove] - iBonusEst[FLIP(pos->uToMove)] / 2;
- iRet = MAX0(iRet);
- *piAlphaMargin += iRet;
+ *piAlphaMargin += iKingTerm + iResidualP90;
+ *piBetaMargin += iKingTerm + iResidualP90;
}
@@ -5533,7 +5483,8 @@ Return value:
SCORE
Eval(IN SEARCHER_THREAD_CONTEXT *ctx,
IN SCORE iAlpha,
- IN SCORE iBeta)
+ IN SCORE iBeta,
+ OUT SCORE *piPositional)
/**
Routine description:
@@ -5543,6 +5494,11 @@ Parameters:
SEARCHER_THREAD_CONTEXT *ctx,
SCORE iAlpha,
SCORE iBeta,
+ SCORE *piPositional : if non-NULL, filled in with a magnitude
+ (always >= 0) estimating the non-material component of the
+ score -- exact if a full eval ran, a cheap estimate otherwise.
+ Callers must treat it as an estimate either way; it's only
+ ever used to size a pruning margin, never as a hard fact.
Return value:
@@ -5693,7 +5649,7 @@ Return value:
// We build two "margins" to account for these components of the
// score.
//
- iAlphaMargin = iBetaMargin = (50 + (SCORE)ctx->uPositional);
+ iAlphaMargin = iBetaMargin = LAZY_EVAL_BASE_MARGIN;
//
// If (score + alpha_margin) is already > alpha -OR-
@@ -5712,13 +5668,15 @@ Return value:
// widen the margin further for king safety issues and passed
// pawns.
//
- _QuicklyEstimateKingSafetyTerm(pos, &iAlphaMargin, &iBetaMargin);
- _QuicklyEstimatePasserBonuses(pos, pHash, &iAlphaMargin, &iBetaMargin);
+ EstimatePositionalScore(pos, pHash, &iAlphaMargin, &iBetaMargin);
if (iScoreForSideToMove + iAlphaMargin <= iAlpha)
{
- ctx->uPositional = MINU(200, ctx->uPositional);
INC(ctx->sCounters.tree.u64LazyEvals);
+ if (NULL != piPositional)
+ {
+ *piPositional = iAlphaMargin;
+ }
#ifdef EVAL_HASH
//
// The true eval can't reach iAlpha; that's a fact about
@@ -5733,8 +5691,11 @@ Return value:
}
else if (iScoreForSideToMove - iBetaMargin >= iBeta)
{
- ctx->uPositional = MINU(200, ctx->uPositional);
INC(ctx->sCounters.tree.u64LazyEvals);
+ if (NULL != piPositional)
+ {
+ *piPositional = iBetaMargin;
+ }
#ifdef EVAL_HASH
StoreEvalHash(ctx, iScoreForSideToMove,
(SCORE)(iScoreForSideToMove - iBetaMargin),
@@ -6101,6 +6062,10 @@ Return value:
// Adjust dynamic positional component.
//
iAlphaMargin = abs(pos->iMaterialBalance[pos->uToMove] - iScoreForSideToMove);
+ if (NULL != piPositional)
+ {
+ *piPositional = iAlphaMargin;
+ }
if ((ULONG)iAlphaMargin > ctx->uPositional)
{
ctx->uPositional = (ULONG)iAlphaMargin;