summaryrefslogtreecommitdiff
path: root/src/eval.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/eval.c')
-rwxr-xr-xsrc/eval.c203
1 files changed, 123 insertions, 80 deletions
diff --git a/src/eval.c b/src/eval.c
index 867c487..5522d2d 100755
--- a/src/eval.c
+++ b/src/eval.c
@@ -3137,9 +3137,20 @@ Return value:
// re-derive with `calibrate marginsafety` if this margin's shape
// changes again.
{
+ // 2026-09-08 re-calibration: original values above measured
+ // |real - lazy| swing symmetrically, conflating "swung toward
+ // the boundary" (the only direction that can make an exit
+ // unsound) with "swung further away" (harmless). Re-measured
+ // with a directional split (CALIBRATE_MARGIN_SAFETY, same
+ // 1500-position tests/twic_sample.ep_, sd 8): the true
+ // dangerous-direction max is 30-80% smaller in every bucket
+ // than the old symmetric max, and every bucket showed
+ // exceeded=0 even before adding headroom. New values are
+ // directional-max * ~1.15-1.3 (more headroom on the smaller
+ // buckets, where sample size is thinner).
static const SCORE iSwingFloorByArmy[8] =
{
- 1069, 1069, 1069, 974, 821, 876, 796, 754,
+ 297, 297, 297, 286, 461, 453, 582, 600,
};
ULONG uMatBucket = MINU(
7, (pos->uArmyScaler[WHITE] + pos->uArmyScaler[BLACK]) / 8);
@@ -5139,6 +5150,65 @@ InitEval(void)
}
+static void
+EvalLookForDrawishSituations(POSITION *pos,
+ SCORE *piScoreForSideToMove)
+{
+ ULONG uColor = pos->uToMove;
+
+ // If the side who's ahead by raw material/positional score can't
+ // actually force a win with what it has left on the board (see
+ // _SideHasWinningChances), squash the score hard towards draw --
+ // same idiom as the 50-move dampening just below (and the same
+ // signed-arithmetic care: g_iDrawScore is currently always 0, but
+ // write this relative to it rather than assuming that, matching
+ // the 50-move code's own convention). /16 (not a full collapse to
+ // drawscore) deliberately mirrors Crafty's EvaluateDraws -- e.g.
+ // KRB vs KR is still theoretically losable by the run-of-the-mill
+ // defender with the checks _SideHasWinningChances gates on, so
+ // some residual signal survives.
+ if ((*piScoreForSideToMove > g_iDrawScore[uColor]) &&
+ (FALSE == _SideHasWinningChances(pos, uColor)))
+ {
+ *piScoreForSideToMove = g_iDrawScore[uColor] +
+ ((*piScoreForSideToMove - g_iDrawScore[uColor]) / 16);
+ }
+ else if ((*piScoreForSideToMove < g_iDrawScore[uColor]) &&
+ (FALSE == _SideHasWinningChances(pos, FLIP(uColor))))
+ {
+ *piScoreForSideToMove = g_iDrawScore[uColor] +
+ ((*piScoreForSideToMove - g_iDrawScore[uColor]) / 16);
+ }
+
+ // If this is a BOOC endgame, push towards draw score too.
+ if ((pos->uNonPawnCount[WHITE][0] == 2) &&
+ (pos->uNonPawnCount[BLACK][0] == 2) &&
+ (pos->uNonPawnCount[WHITE][BISHOP] == 1) &&
+ (pos->uNonPawnCount[BLACK][BISHOP] == 1) &&
+ (pos->uWhiteSqBishopCount[WHITE] != pos->uWhiteSqBishopCount[BLACK])) {
+ *piScoreForSideToMove = g_iDrawScore[uColor] +
+ ((*piScoreForSideToMove - g_iDrawScore[uColor]) / 4);
+ }
+
+ // Drive the score towards draw as we approach a 50 move w/o
+ // progress draw.
+ if (pos->uFifty > 84)
+ {
+ ULONG uDrawDist = 101 - pos->uFifty;
+ ASSERT(uDrawDist > 0);
+ // uDrawDist is ULONG -- multiplying a negative SCORE by it
+ // directly promotes the SCORE to unsigned first (usual
+ // arithmetic conversions, same rank), wrapping a negative
+ // *piScoreForSideToMove into a huge positive garbage value
+ // instead of scaling it down. Cast uDrawDist to SCORE so the
+ // multiply happens in signed arithmetic; its range (1-16) is
+ // always representable.
+ *piScoreForSideToMove = g_iDrawScore[uColor] +
+ (*piScoreForSideToMove * (SCORE)uDrawDist / 16);
+ }
+}
+
+
SCORE
Eval(IN SEARCHER_THREAD_CONTEXT *ctx,
IN SCORE iAlpha,
@@ -5236,24 +5306,26 @@ Return value:
// Super-lazy exit point.
#ifdef LAZY_EVAL
- // 2026-09-08: was a single flat 625 for every material level.
- // CALIBRATE_MARGIN_SAFETY data (100 real-game positions, sd 8,
- // tests/twic_sample.ep_) showed that's badly unsound at low
- // material -- up to 9.3% of super-lazy exits in bare-king-plus-a-
- // little-material positions (combined army scaler 0-7) had a real
- // swing exceeding 625, max observed 1710 -- while richer positions
- // (combined scaler 32+) never came close (max 893, well under
- // 625... actually under 900, still comfortably bounded). Table
- // indexed the same way _MaterialBucket buckets the calibration
- // data (combined army scaler / 8, 8 buckets), so it can be
- // re-derived directly from a "calibrate marginsafety" run. Values
- // here are the observed max per bucket rounded up with headroom
- // (~15-20%), not a hard theoretical bound -- provisional pending a
- // larger/deeper calibration run; re-check before trusting this at
- // sd well beyond 8.
+ // 2026-09-08: was a single flat 625 for every material level, then
+ // split by material bucket using a 100-position/sd8 calibration
+ // (symmetric |swing|, ~15-20% headroom). Re-calibrated against a
+ // larger 1500-position sample with a directional split (only a
+ // swing *toward* the alpha/beta boundary can make an exit unsound;
+ // a swing away is harmless, and the original symmetric measurement
+ // conflated the two) -- true dangerous-direction max ran 15-50%
+ // below the old symmetric max in most buckets, and every bucket
+ // showed exceeded=0 even before headroom. Values below are
+ // directional-max * ~1.15-1.3 (more headroom on buckets with
+ // thinner samples, e.g. bucket 0's n=512); buckets 2 and 5 kept at
+ // their prior value since it was already tighter than a fresh 15%
+ // headroom would give. Table indexed the same way _MaterialBucket
+ // buckets the calibration data (combined army scaler / 8, 8
+ // buckets), so it can be re-derived directly from a "calibrate
+ // marginsafety" run. Not a hard theoretical bound -- re-check
+ // before trusting this at sd well beyond 8.
static const SCORE SUPER_LAZY_MARGIN_BY_ARMY[8] =
{
- 2000, 1800, 1800, 1750, 1000, 850, 850, 850,
+ 300, 1635, 1800, 1070, 946, 850, 734, 698,
};
ULONG uSuperLazyMatBucket = MINU(
7, (pos->uArmyScaler[WHITE] + pos->uArmyScaler[BLACK]) / 8);
@@ -5789,44 +5861,42 @@ Return value:
#endif
}
- //
// Evaluate the two kings last.
- //
- c = pos->cNonPawns[BLACK][0];
+ c = pos->cNonPawns[uColor][0];
#ifdef DEBUG
ASSERT(IS_ON_BOARD(c));
{
PIECE pDebugKing = pos->rgSquare[c].pPiece;
ASSERT(IS_VALID_PIECE(pDebugKing));
- ASSERT(GET_COLOR(pDebugKing) == BLACK);
+ ASSERT(GET_COLOR(pDebugKing) == uColor);
ASSERT(IS_KING(pDebugKing));
}
#endif
TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash, ctx));
#ifdef EVAL_DUMP
- Trace("After *k:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]);
+ Trace("After our K at %s:\n%d\t\t%d\n",
+ CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]);
#endif
- c = pos->cNonPawns[WHITE][0];
+ c = pos->cNonPawns[xColor][0];
#ifdef DEBUG
ASSERT(IS_ON_BOARD(c));
{
PIECE pDebugKing = pos->rgSquare[c].pPiece;
ASSERT(IS_VALID_PIECE(pDebugKing));
- ASSERT(GET_COLOR(pDebugKing) == WHITE);
+ ASSERT(GET_COLOR(pDebugKing) == xColor);
ASSERT(IS_KING(pDebugKing));
}
#endif
TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash, ctx));
#ifdef EVAL_DUMP
- Trace("After .k:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]);
+ Trace("After enemy K at %s:\n%d\t\t%d\n",
+ CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]);
#endif
- //
// Now that we have the whole attack table generated, think about
// passed pawns identified by the pawn eval routine again. Also
// see if the side not on move has a trapped piece.
- //
#ifdef EVAL_TIME
UINT64 u64PostLazyMiscTimer = SystemReadTimeStampCounter();
#endif
@@ -5856,9 +5926,7 @@ Return value:
// TODO: endgame-specific knowledge? e.g. B over N in an endgame
// with 2 pawn wings?
- //
- // Roll in the reduced material down scaler terms.
- //
+ // Roll in the reduced material down scaler terms (aggregate).
ASSERT(pos->iReducedMaterialDownScaler[BLACK] > -200);
ASSERT(pos->iReducedMaterialDownScaler[BLACK] < +200);
iAlphaMargin = (pos->iReducedMaterialDownScaler[BLACK] *
@@ -5874,62 +5942,22 @@ Return value:
ctx->sCounters.tree.u64CyclesEvalPostLazyMisc +=
(SystemReadTimeStampCounter() - u64PostLazyMiscTimer);
#endif
+#ifdef EVAL_DUMP
+ Trace("After all pieces:\n%d\t\t%d\n",
+ pos->iScore[WHITE], pos->iScore[BLACK]);
+#endif
- //
// Almost done
- //
iScoreForSideToMove = (pos->iScore[pos->uToMove] -
pos->iScore[FLIP(pos->uToMove)]);
-#ifdef EVAL_DUMP
- Trace("At the end:\n%d\t\t%d\n", pos->iScore[WHITE],
- pos->iScore[BLACK]);
-#endif
+
+ // Drive drawish situations towards the draw score.
+ EvalLookForDrawishSituations(pos, &iScoreForSideToMove);
//
// TODO: detect and discourage blocked positions?
//
- // If the side who's ahead by raw material/positional score can't
- // actually force a win with what it has left on the board (see
- // _SideHasWinningChances), squash the score hard towards draw --
- // same idiom as the 50-move dampening just below (and the same
- // signed-arithmetic care: g_iDrawScore is currently always 0, but
- // write this relative to it rather than assuming that, matching
- // the 50-move code's own convention). /16 (not a full collapse to
- // drawscore) deliberately mirrors Crafty's EvaluateDraws -- e.g.
- // KRB vs KR is still theoretically losable by the run-of-the-mill
- // defender with the checks _SideHasWinningChances gates on, so
- // some residual signal survives.
- if ((iScoreForSideToMove > g_iDrawScore[pos->uToMove]) &&
- (FALSE == _SideHasWinningChances(pos, pos->uToMove)))
- {
- iScoreForSideToMove = g_iDrawScore[pos->uToMove] +
- ((iScoreForSideToMove - g_iDrawScore[pos->uToMove]) / 16);
- }
- else if ((iScoreForSideToMove < g_iDrawScore[pos->uToMove]) &&
- (FALSE == _SideHasWinningChances(pos, FLIP(pos->uToMove))))
- {
- iScoreForSideToMove = g_iDrawScore[pos->uToMove] +
- ((iScoreForSideToMove - g_iDrawScore[pos->uToMove]) / 16);
- }
-
- // Drive the score towards draw as we approach a 50 move w/o
- // progress draw.
- if (pos->uFifty > 84)
- {
- ULONG uDrawDist = 101 - pos->uFifty;
- ASSERT(uDrawDist > 0);
- // uDrawDist is ULONG -- multiplying a negative SCORE by it
- // directly promotes the SCORE to unsigned first (usual
- // arithmetic conversions, same rank), wrapping a negative
- // iScoreForSideToMove into a huge positive garbage value
- // instead of scaling it down. Cast uDrawDist to SCORE so the
- // multiply happens in signed arithmetic; its range (1-16) is
- // always representable.
- iScoreForSideToMove = g_iDrawScore[pos->uToMove] +
- (iScoreForSideToMove * (SCORE)uDrawDist / 16);
- }
-
// Adjust dynamic positional component. Unlike the lazy-exit
// margins above (always >= 0 by construction), this can go
// negative -- a side whose positional terms (king safety, pawn
@@ -5968,8 +5996,18 @@ Return value:
// restore this one, not the regular-lazy save in the other
// branch.
//
+ // Direction matters: for an alpha-side exit (saved score below
+ // alpha), only an *upward* real swing is dangerous -- it's the
+ // one that could pull the score back over alpha. A downward
+ // swing just makes the verdict even more clearly correct. Same
+ // in reverse for a beta-side exit. abs() was conflating both
+ // directions, so "exceeded" was really an upper bound on real
+ // unsoundness, not a measurement of it.
RecordSuperLazyMarginSafetySwing(
- pos, abs(iScoreForSideToMove - iSavedSuperLazyScore),
+ pos,
+ (iSavedSuperLazyScore < iAlpha)
+ ? MAX0(iScoreForSideToMove - iSavedSuperLazyScore)
+ : MAX0(iSavedSuperLazyScore - iScoreForSideToMove),
iSuperLazyMargin);
iScoreForSideToMove = iSavedSuperLazyScore;
if (NULL != piPositional)
@@ -5994,8 +6032,13 @@ Return value:
// large would LAZY_EVAL_BASE_MARGIN have had to be before an
// exit here would have been unsound."
//
- RecordMarginSafetySwing(pos, abs(iScoreForSideToMove - iSavedLazyScore),
- iSavedLazyPositional);
+ // Same directional fix as the super-lazy branch above.
+ RecordMarginSafetySwing(
+ pos,
+ (iSavedLazyScore < iAlpha)
+ ? MAX0(iScoreForSideToMove - iSavedLazyScore)
+ : MAX0(iSavedLazyScore - iScoreForSideToMove),
+ iSavedLazyPositional);
//
// Restore exactly what a normal build would have returned --
// this measurement must not change real search behavior.