diff options
Diffstat (limited to 'src/eval.c')
| -rwxr-xr-x | src/eval.c | 203 |
1 files changed, 123 insertions, 80 deletions
@@ -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. |
