From 9e995e7c39a83ae9b5ba86f3346e0281744bf773 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 5 Sep 2026 21:52:40 -0700 Subject: King-safety recalibration, lazy-eval material floor, eval hot-path trimming Recalibrate iKingSwingP90 against the bitboard-rewritten CountKingSafetyDefects (~1.28B samples via new CALIBRATE_POSITIONAL/ CALIBRATE_BASE_MARGIN/CALIBRATE_MARGIN_SAFETY diagnostic build flags, board_representation/EVAL.md section 9). Add LAZY_EVAL_MIN_MATERIAL: measured the regular lazy exit's real swing exceeding its own assumed margin 20.6% of the time in near-bare-king endgames (vs <=0.36% elsewhere) -- skip lazy eval entirely below that material floor. Double the stale search.c/searchsup.c CountKingSafetyDefects extension thresholds as a stopgap pending their own recalibration. Eval hot-path trimming (measured via EVAL_TIME, ~1759 -> ~1386 avg cycles/eval on a representative middlegame position): - Pull _GetFileStormDefects out of EstimatePositionalScore's hot path (cost more than the "cheap cached lookup" it was assumed to be, running on ~90% of all Eval() calls). - Add pos->bbOccupiedSide[2], incrementally maintained alongside bbOccupied, so _BuildFriendlySideBB is a field read instead of a 6-term OR. - Switch CoorFromBitBoardRank8ToRank1/Rank1ToRank8 to the existing static-inline FastFirstBit/FastLastBit (same bsf/bsr instruction, no call/ret overhead). - Defer EvalPasserRaces' uRacerDist/fDontCountMeOut past its no-passer early return. - Remove the mailbox-era "max mobility in a row" term from _EvalBishop/_EvalRook (no bitboard-mobility equivalent need for it). - Simplify _EvalBishopPairs and rook file-openness/passer bonuses to flat DNA-tunable constants instead of distance/pawn-count-scaled tables, rook file-openness now a branchless bitboard-indexed lookup. - Remove pos->cPiece (write-only, no reader anywhere). - Collapse WHITE/BLACK mirror-branches (castle-rights block, rook-trapped-in-corner) to color-indexed constants. - Close the PAWN_BIT..KING_BIT gap (bits 7-3 -> bits 4-0), removing the bvPattern >>= 3 before its KING_COUNTER_BY_ATTACK_PATTERN lookup. This also fixes a real bug introduced earlier this session when _WhoControlsSquareFast was converted to read these constants directly: g_SwapTable is only [32][32], but the old bit values (up to 0xF8) indexed far out of bounds on any attacked square -- data.c's InitializeSwapTable was always built assuming the bits 0-4 range this change now actually produces. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01XxmVi2sTMwpPp4i6WYFjan --- src/GNUmakefile | 9 + src/TODO | 2 + src/bitboard.c | 10 +- src/board.c | 11 + src/chess.h | 118 +++- src/command.c | 74 ++ src/eval.c | 2116 +++++++++++++++++++++++++++++++++---------------------- src/fen.c | 2 + src/generate.c | 8 +- src/move.c | 12 + src/root.c | 29 + src/search.c | 17 +- src/searchsup.c | 8 +- src/testsup.c | 4 + 14 files changed, 1552 insertions(+), 868 deletions(-) diff --git a/src/GNUmakefile b/src/GNUmakefile index ab35c7e..f61ae63 100644 --- a/src/GNUmakefile +++ b/src/GNUmakefile @@ -144,6 +144,15 @@ endif ifdef DUMP_TREE PROFILE += -DDUMP_TREE endif +ifdef CALIBRATE_POSITIONAL +PROFILE += -DCALIBRATE_POSITIONAL +endif +ifdef CALIBRATE_BASE_MARGIN +PROFILE += -DCALIBRATE_BASE_MARGIN +endif +ifdef CALIBRATE_MARGIN_SAFETY +PROFILE += -DCALIBRATE_MARGIN_SAFETY +endif endif # EVERYTHING # Short commit hash (plus a -dirty suffix if the working tree has diff --git a/src/TODO b/src/TODO index 516f2c4..ae02475 100644 --- a/src/TODO +++ b/src/TODO @@ -3,3 +3,5 @@ Pawnhash size tunable from cmdline again Think about when to avoid nullmove pruning based on checks in the line Think about futility pruning when a >= +NMATE Stick eval / king score in ply info and use it to trigger extensions + +r2r4/p4pk1/2p3p1/1p1nPR2/5p1Q/2N5/PPPq4/1K4R1 w - - 0 0 -- check extension crazy diff --git a/src/bitboard.c b/src/bitboard.c index ad45ea6..2832fdb 100755 --- a/src/bitboard.c +++ b/src/bitboard.c @@ -344,7 +344,11 @@ Return value: **/ { COOR c = ILLEGAL_COOR; - ULONG uFirstBit = FirstBit(*pbb); + // FastFirstBit (chess.h): same bsf instruction as the extern + // FirstBit, but static inline -- no call/ret/arg-marshal overhead. + // Measured directly: ~7 cycles/call via the extern vs ~1-3 for the + // bare instruction (board_representation/EVAL.md section 9). + ULONG uFirstBit = FastFirstBit(*pbb); ASSERT(uFirstBit == SlowFirstBit(*pbb)); if (0 != uFirstBit) @@ -381,7 +385,9 @@ Return value: **/ { COOR c; - ULONG uLastBit = LastBit(*pbb); + // FastLastBit (chess.h): same bsr instruction as the extern + // LastBit, but static inline -- no call/ret/arg-marshal overhead. + ULONG uLastBit = FastLastBit(*pbb); ASSERT(SlowLastBit(*pbb) == uLastBit); c = ILLEGAL_COOR; diff --git a/src/board.c b/src/board.c index 814b045..9ed2f03 100755 --- a/src/board.c +++ b/src/board.c @@ -171,6 +171,7 @@ Return value: "bbPieces bitboard doesn't match piece list", "bbPawns bitboard doesn't match pawn list", "bbOccupied bitboard doesn't match piece list", + "bbOccupiedSide bitboard doesn't match piece list", }; ULONG u, v; COOR c; @@ -181,6 +182,7 @@ Return value: BITBOARD bbPieces[2][8]; BITBOARD bbPawns[2]; BITBOARD bbOccupied = 0; + BITBOARD bbOccupiedSide[2] = {0, 0}; ULONG uSigmaNonPawnCount[2] = {0, 0}; ULONG uWhiteSqBishopCount[2] = {0, 0}; UINT64 u64Computed; @@ -275,6 +277,7 @@ Return value: uPawnCount[u]++; bbPawns[u] |= COOR_TO_BB(c); bbOccupied |= COOR_TO_BB(c); + bbOccupiedSide[u] |= COOR_TO_BB(c); } } @@ -320,6 +323,7 @@ Return value: bbPieces[u][PIECE_TYPE(p)] |= COOR_TO_BB(c); } bbOccupied |= COOR_TO_BB(c); + bbOccupiedSide[u] |= COOR_TO_BB(c); uNonPawnMaterial[u] += PIECE_VALUE(p); if ((IS_BISHOP(p)) && (IS_WHITE_SQUARE_COOR(c))) @@ -404,6 +408,13 @@ Return value: goto end; } + if ((pos->bbOccupiedSide[WHITE] != bbOccupiedSide[WHITE]) || + (pos->bbOccupiedSide[BLACK] != bbOccupiedSide[BLACK])) + { + uReason = 24; + goto end; + } + // // Now walk the actual board and reduce the material counts we got // by walking the piece lists. If everything is ok then the diff --git a/src/chess.h b/src/chess.h index acab153..8d16837 100755 --- a/src/chess.h +++ b/src/chess.h @@ -560,13 +560,17 @@ typedef union _MOVE // pattern from those same bbXAttacks reads, then index // KING_COUNTER_BY_ATTACK_PATTERN/g_SwapTable with it. Kept exactly as // before; only their old *storage* mechanism (ATTACK_BITV) is gone. -#define PAWN_BIT 0x00000080UL -#define MINOR_BIT 0x00000040UL -#define ROOK_BIT 0x00000020UL -#define QUEEN_BIT 0x00000010UL +// Bits 1-4 (2026-09-06: were 4-7, a leftover from a retired count field +// that used to live in bits 0-2 -- closing that gap removes the +// bvPattern >>= 3 that used to be needed before the +// KING_COUNTER_BY_ATTACK_PATTERN lookup, eval.c). +#define PAWN_BIT 0x00000010UL +#define MINOR_BIT 0x00000008UL +#define ROOK_BIT 0x00000004UL +#define QUEEN_BIT 0x00000002UL // King never x-rays (it can't move through a blocker), so there's no // KING_XRAY_BIT to go with this. -#define KING_BIT 0x00000008UL +#define KING_BIT 0x00000001UL #define INVALID_PIECE_INDEX (17) #define IS_VALID_PIECE_INDEX(x) ((x) < INVALID_PIECE_INDEX) @@ -650,6 +654,14 @@ typedef struct _POSITION // not as something callers should call directly anymore. BITBOARD bbOccupied; + // Per-side occupancy, same incremental-maintenance treatment as + // bbOccupied above -- _BuildFriendlySideBB (generate.c) used to + // rebuild this from scratch (a 6-term OR of bbPieces+bbPawns+king) + // on every call; read far more often per node than a position + // changes per move, so incremental beats on-the-fly OR-ing here + // for the same reason it already does for bbOccupied. + BITBOARD bbOccupiedSide[2]; + // Eval()-scoped attack-bitboard accumulators, one pair per piece // family, populated by Eval()'s piece-by-piece walk (pawns, then // knights/bishops, then rooks, then queens, then king) and read by @@ -718,11 +730,8 @@ typedef struct _POSITION SCORE iScore[2]; SCORE iReducedMaterialDownScaler[2]; SCORE iTempScore; - ULONG uMinMobility[2]; - COOR cPiece; ULONG uMinorsAtHome[2]; BITBOARD bb; - ULONG uPiecesPointingAtKing[2]; } POSITION; @@ -896,6 +905,19 @@ typedef struct _COUNTERS } pawnhash; + // EXPERIMENTAL, prototype-only -- see rgKingSafetyHash and + // _GetFileStormDefects (eval.c). Cycle counters let this be + // measured with a plain PERF_COUNTERS build, no separate + // EVAL_TIME rebuild needed. + struct + { + UINT64 u64Probes; + UINT64 u64Hits; + UINT64 u64CyclesHit; + UINT64 u64CyclesMiss; + } + kingsafetyhash; + struct { UINT64 u64TotalNodeCount; @@ -975,6 +997,21 @@ typedef struct _COUNTERS UINT64 u64CyclesEvalPreLazy; UINT64 u64CyclesEvalPostLazyMisc; UINT64 u64CyclesEvalAttackTablePop; + + // + // 2026-09-06 (board_representation/EVAL.md section 9): finer + // breakdown of what's inside u64CyclesEvalPreLazy, to find out + // exactly what got more expensive there this session. + // u64CyclesEvalLazyDecision times the whole material-gate + + // margin-check + EstimatePositionalScore block; the other two + // further split EstimatePositionalScore's own two calibrated + // sub-calls (each already summed over both colors). Everything + // else in "pre-lazy" (material/passers/bad-trades/bishop-pairs) + // is u64CyclesEvalPreLazy minus u64CyclesEvalLazyDecision. + // + UINT64 u64CyclesEvalLazyDecision; + UINT64 u64CyclesEvalCountKingSafetyDefects; + UINT64 u64CyclesEvalFileStormDefects; } tree; @@ -1098,7 +1135,6 @@ typedef struct _PLY_INFO MOVE PV[MAX_PLY_PER_SEARCH]; SCORE iKingScore[2]; - ULONG uMinMobility[2]; UINT64 u64NonPawnSig; UINT64 u64PawnSig; UINT64 u64Sig; @@ -1123,6 +1159,18 @@ typedef struct _PAWN_HASH_ENTRY } PAWN_HASH_ENTRY; +// EXPERIMENTAL, prototype-only (2026-09-05) -- see +// SEARCHER_THREAD_CONTEXT's rgKingSafetyHash comment. Same table size +// as the pawn hash for a fair, direct hit-rate comparison against the +// already-measured numbers. +#define KING_SAFETY_HASH_TABLE_SIZE PAWN_HASH_TABLE_SIZE +typedef struct _KING_SAFETY_HASH_ENTRY +{ + UINT64 u64Key; + ULONG uFileStormDefects[2]; +} +KING_SAFETY_HASH_ENTRY; + #define NUM_SPLIT_PTRS_IN_CONTEXT (8) // @@ -1159,6 +1207,31 @@ typedef struct _SEARCHER_THREAD_CONTEXT SCORE iRootScore; ULONG uRootDepth; PAWN_HASH_ENTRY rgPawnHash[PAWN_HASH_TABLE_SIZE]; + // EXPERIMENTAL, prototype-only (2026-09-05): real cache for + // _EvalKing's open-file/storming-pawn defect count only (the + // DO_KING_SAFETY_THRESHOLD-gated "safety branch" terms -- measured + // ~96% hit rate in general/middlegame positions). Deliberately + // does NOT cover the u<8-gated "endgame branch" terms (king- + // supporting-own-passer, in/out-of-action) -- those measured a + // much worse ~75-85% hit rate in bare K+P endgames, and are cheap + // enough in that low-piece-count regime that caching them isn't + // expected to pay off; they stay computed directly in _EvalKing, + // uncached, per board_representation/EVAL.md section 9's + // king-safety-hash discussion. Keyed on (pos->u64PawnSig, both + // kings' squares) -- both sides' defect counts are computed and + // stored together on a miss, so whichever king _EvalKing evaluates + // second within the same Eval() call is a guaranteed hit. + KING_SAFETY_HASH_ENTRY rgKingSafetyHash[KING_SAFETY_HASH_TABLE_SIZE]; +#ifdef CALIBRATE_POSITIONAL + // Set/cleared once per Eval() call, around the cheap base-margin + // ("super lazy") gate that decides whether EstimatePositionalScore + // would even run in a normal build -- _RecordPositionalCalibration + // only logs a sample when this is set, so the calibration histogram + // matches the population EstimatePositionalScore actually sees in + // real (uninstrumented) search, not every node Eval() is ever + // called on. + FLAG fCalibrateCandidate; +#endif CHAR szLastPV[SMALL_STRING_LEN_CHAR]; } SEARCHER_THREAD_CONTEXT; @@ -3058,6 +3131,18 @@ PawnHashLookup(SEARCHER_THREAD_CONTEXT *ctx); #define LAZY_EVAL_BASE_MARGIN (75) // cheap material-only lazy exit margin; // widened by EstimatePositionalScore // if this isn't enough on its own +// board_representation/EVAL.md section 9 (2026-09-06): below this +// combined pos->uArmyScaler[WHITE]+pos->uArmyScaler[BLACK] total, both +// stages of lazy eval are skipped entirely (always full eval). Measured +// directly via CALIBRATE_MARGIN_SAFETY: the regular (widened-margin) +// lazy exit's real swing exceeded its own assumed margin 20.6% of the +// time in the 0-7 combined-scaler bucket (n=1229) -- a near-bare-king +// endgame's positional swings (opposition, zugzwang, breakthrough +// timing) are too discontinuous for the margin's "small and bounded" +// assumption to hold. Set at 16 (start of the material bucket that +// measured a clean 0.05% rate) rather than exactly at the 0-7 boundary, +// since the 8-15 bucket's own 0% reading was too thin (n=50) to trust. +#define LAZY_EVAL_MIN_MATERIAL 16 extern const int g_iAhead[2]; extern const int g_iBehind[2]; @@ -3089,6 +3174,21 @@ ULONG CountKingSafetyDefects(POSITION *pos, ULONG uSide); +#ifdef CALIBRATE_POSITIONAL +void +DumpPositionalCalibration(void); +#endif + +#ifdef CALIBRATE_BASE_MARGIN +void +DumpBaseMarginCalibration(void); +#endif + +#ifdef CALIBRATE_MARGIN_SAFETY +void +DumpMarginSafetyCalibration(void); +#endif + // // testeval.c // diff --git a/src/command.c b/src/command.c index a51bb33..cf8f939 100755 --- a/src/command.c +++ b/src/command.c @@ -753,6 +753,74 @@ Return value: } +COMMAND(CalibrateCommand) +/** + +Routine description: + + This function implements the 'calibrate' engine command, only + available in a CALIBRATE_POSITIONAL build (board_representation/ + EVAL.md section 9's king-safety recalibration). + + Usage: + + calibrate dump + + Prints a fresh iKingSwingP90[]-shaped table (p90 of the real + KING_SAFETY_BY_COUNTER magnitude observed at each cheap + defect-index bucket) built from every full-eval sample seen so + far this run. Run a representative workload first (the curated + suites or an ecm.ep_ slice, at real search depth via `sd`/ + `script`) so the histogram reflects positions the search tree + actually evaluates, not just root positions. + +Parameters: + + The COMMAND macro hides four arguments from the input parser: + + CHAR *szInput : the full line of input + ULONG argc : number of argument chunks + CHAR *argv[] : array of ptrs to each argument chunk + POSITION *pos : a POSITION pointer to operate on + +Return value: + + void + +**/ +{ + if (argc >= 2 && !STRCMPI(argv[1], "basemargin")) + { +#ifdef CALIBRATE_BASE_MARGIN + DumpBaseMarginCalibration(); +#else + Trace("This binary was not built with CALIBRATE_BASE_MARGIN.\n"); +#endif + return; + } + if (argc >= 2 && !STRCMPI(argv[1], "marginsafety")) + { +#ifdef CALIBRATE_MARGIN_SAFETY + DumpMarginSafetyCalibration(); +#else + Trace("This binary was not built with CALIBRATE_MARGIN_SAFETY.\n"); +#endif + return; + } +#ifdef CALIBRATE_POSITIONAL + if ((argc < 2) || STRCMPI(argv[1], "dump")) + { + Trace("Usage: calibrate dump | calibrate basemargin | " + "calibrate marginsafety\n"); + return; + } + DumpPositionalCalibration(); +#else + Trace("This binary was not built with CALIBRATE_POSITIONAL.\n"); +#endif +} + + COMMAND(ExitCommand) /** @@ -2352,6 +2420,12 @@ COMMAND_PARSER_ENTRY g_ParserTable[] = FALSE, FALSE, "Offer the engine a draw" }, + { "calibrate", + CalibrateCommand, + FALSE, + FALSE, + FALSE, + "Dump the CALIBRATE_POSITIONAL king-safety histogram (calibrate dump)" }, { "edit", EditCommand, FALSE, diff --git a/src/eval.c b/src/eval.c index 84e001e..eb6e118 100755 --- a/src/eval.c +++ b/src/eval.c @@ -84,79 +84,6 @@ _FastCoorFromBitBoardRank1ToRank8(IN OUT BITBOARD *pbb) #endif // !CROUTINES -// -// Bishop-mobility ray-walk outcome categories -- see BMobCaseTable in -// _EvalBishop. Replaces a table of function pointers (one indirect -// call/ret per square visited) with a table of these tags dispatched -// via switch, inlined directly into the ray-walking loop. -// -typedef enum _BMOB_CASE -{ - BMOB_EMPTY = 0, // empty square - BMOB_INVALID, // should never occur (off-board sentinel) - BMOB_FRIEND_PAWN, // own-color pawn - BMOB_ENEMY_PAWN, // enemy pawn (worth less than a bishop) - BMOB_FRIEND_BLOCK, // own-color knight/rook/king: blocks, no xray - BMOB_ENEMY_SAME, // enemy bishop/knight: captures, blocks - BMOB_FRIEND_XRAY, // own-color bishop/queen: xray through, keep going - BMOB_ENEMY_GREATER, // enemy rook/queen/king: captures, xray bit, keep going -} BMOB_CASE; - -// -// Knight-mobility outcome categories -- see NMobCaseTable in _EvalKnight. -// Knight mobility only ever looks at the single landing square (no ray to -// walk/stop), so there's no xray/stop concept here, just "does landing on -// this square count as mobility." -// -typedef enum _NMOB_CASE -{ - NMOB_INVALID = 0, // should never occur (off-board sentinel) - NMOB_MOBILE_SQUARE, // empty square or enemy pawn: counts unless unsafe for a minor - NMOB_ENEMY_OTHER, // any other enemy piece: always counts - NMOB_FRIEND, // any friendly piece: never counts -} NMOB_CASE; - -// -// Rook-mobility outcome categories -- see RMobCaseTable in _EvalRook. -// RMOB_FRIEND_ROOK carries a real side effect (the "connected rooks" -// bonus), so unlike bishop/knight this isn't purely mobility/xray-bit -// bookkeeping -- kept as a distinct case rather than folded into -// RMOB_FRIEND_QUEEN even though both keep scanning with the xray bit set. -// -typedef enum _RMOB_CASE -{ - RMOB_EMPTY = 0, // empty square - RMOB_INVALID, // should never occur (off-board sentinel) - RMOB_ENEMY_LESS, // enemy pawn/knight/bishop: captures, blocks - RMOB_FRIEND_BLOCK, // own-color knight/bishop/king: blocks, no xray - RMOB_FRIEND_ROOK, // own-color rook: connected-rooks bonus, xray, keep going - RMOB_ENEMY_SAME, // enemy rook: captures, blocks - RMOB_FRIEND_QUEEN, // own-color queen: xray through, keep going - RMOB_ENEMY_GREATER, // enemy queen/king: captures, xray bit, keep going -} RMOB_CASE; - -// -// Queen-mobility outcome categories -- see QMobCaseTable in _EvalQueen. -// A queen's ray set is the union of a bishop's 4 diagonals and a rook's -// 4 orthogonals, so QMOB_FRIEND_BISHOP/QMOB_FRIEND_ROOK only let the -// queen xray through when the *ray it's currently walking* matches that -// piece's own move pattern (diagonal for a bishop, orthogonal for a -// rook) -- computed once per ray below (fOrthogonalRay), not per square, -// since every square on a given ray shares the same rank/file -// relationship to the queen's home square. -// -typedef enum _QMOB_CASE -{ - QMOB_EMPTY = 0, // empty square - QMOB_INVALID, // should never occur (off-board sentinel) - QMOB_ENEMY_LESS, // enemy piece worth less than a queen: captures, blocks - QMOB_FRIEND_BLOCK, // own-color knight/king: blocks, no xray - QMOB_FRIEND_BISHOP, // own-color bishop: xray only on a diagonal ray - QMOB_FRIEND_ROOK, // own-color rook: xray only on an orthogonal ray - QMOB_FRIEND_QUEEN, // own-color queen: xray through, keep going - QMOB_ENEMY_GE, // enemy queen/king: captures, blocks -} QMOB_CASE; - // // To simplify code / maintenance I use the same loop for both colors // in some places. These globals coorespond to "ahead of the piece" @@ -199,7 +126,31 @@ static ULONG REDUCED_MATERIAL_DOWN_SCALER[32] = 8, 8, 8, 8, 8, 8, 8, 8, }; -static ULONG REDUCED_MATERIAL_UP_SCALER[32] = +// +// Same shape as REDUCED_MATERIAL_DOWN_SCALER (0 with little material left, +// ramping to 8 with a full army) but decays to 0 much sooner -- "haven't +// castled yet" / "minors still at home" are opening-specific concerns that +// stop mattering well before the point where real king danger fades, so +// this table's zero region extends much further up the index range than +// REDUCED_MATERIAL_DOWN_SCALER's does. +// +static ULONG CASTLE_AND_DEVELOPMENT_SCALER[32] = +{ +// none na na m na R 2m na + 0, 0, 0, 0, 0, 0, 0, 0, + +// Rm 3m/Q 2R R2m 4m/Qm 2Rm R3m/QR Q2m + 0, 0, 0, 0, 0, 0, 0, 0, + +// 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m +// Q2R Q2Rm + 0, 1, 2, 3, 4, 5, 6, 7, + +// na Q2R2m QR4m na Q2R3m na na full + 7, 8, 8, 8, 8, 8, 8, 8, +}; + +static ULONG REDUCED_MATERIAL_UP_SCALER[32] = { // none na na m na R 2m na 8, 8, 8, 8, 8, 8, 8, 8, @@ -466,13 +417,9 @@ static SCORE BISHOP_OVER_KNIGHT_IN_ENDGAME = +33; // pieces on the board except two minors each." // --Larry Kaufman, IM // "Evaluation of Material Imbalance" -static SCORE BISHOP_PAIR[2][17] = -{ - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, - { 53, 52, 51, 50, 49, 48, 47, 45, 43, 41, 40, 40, 40, 40, 40, 40, 40 } -}; +static SCORE BISHOP_PAIR_BONUS = 40; -static SCORE STATIONARY_PAWN_ON_BISHOP_COLOR[128] = +static SCORE STATIONARY_PAWN_ON_BISHOP_COLOR[128] = { +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0, -4, -6, -7, -7, -7, -7, -6, -4, 0,0,0,0,0,0,0,0, @@ -485,7 +432,7 @@ static SCORE STATIONARY_PAWN_ON_BISHOP_COLOR[128] = +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0 }; -static SCORE TRANSIENT_PAWN_ON_BISHOP_COLOR[128] = +static SCORE TRANSIENT_PAWN_ON_BISHOP_COLOR[128] = { +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0, -1, -1, -2, -2, -2, -2, -1, -1, 0,0,0,0,0,0,0,0, @@ -506,10 +453,6 @@ static SCORE BISHOP_MOBILITY_BY_SQUARES[14] = -22, -14, -10, -5, -1, 0, +1, +3, +4, +5, +6, +7, +8, +9 };// ^ | -static SCORE BISHOP_MAX_MOBILITY_IN_A_ROW_BONUS[8] = -{// 0 1 2 3 4 5 6 7 - -10, -4, +1, +3, +4, +5, +5, +5 -}; static SCORE BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING[9] = {// 0 1 2 3 4 5 6 7 8 @@ -520,7 +463,7 @@ static SCORE BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING[9] = // Knight eval terms // --------------------------------------------------------------------------- // -static SCORE KNIGHT_CENTRALITY_BONUS[128] = +static SCORE KNIGHT_CENTRALITY_BONUS[128] = { -12, -8, -8, -8, -8, -8, -8, -12, 0,0,0,0,0,0,0,0, -8, -2, -2, 0, 0, -2, -2, -8, 0,0,0,0,0,0,0,0, @@ -581,20 +524,20 @@ static SCORE KNIGHT_IN_CLOSED_POSITION[33] = // Rook eval terms // --------------------------------------------------------------------------- // -static SCORE ROOK_ON_FULL_OPEN_BY_DIST_FROM_EKING[8] = -{// 0 1 2 3 4 5 6 7 - +24, +22, +17, +14, +13, +13, +12, +12 -}; - -static SCORE ROOK_ON_HALF_OPEN_WITH_ENEMY_BY_DIST_FROM_EKING[8] = -{// 0 1 2 3 4 5 6 7 - +12, +11, +9, +9, +8, +8, +8, +7 -}; - -static SCORE ROOK_ON_HALF_OPEN_WITH_FRIEND_BY_DIST_FROM_EKING[8] = -{// 0 1 2 3 4 5 6 7 - +13, +12, +11, +11, +9, +9, +9, +8 -}; +// Simplified 2026-09-06 (board_representation/EVAL.md section 9): flat +// average bonus, not scaled by distance from the enemy king -- by direct +// instruction. Each was a [8]-by-distance table; these are that table's +// simple mean, rounded. +static SCORE ROOK_ON_FULL_OPEN = 16; +static SCORE ROOK_ON_HALF_OPEN_WITH_ENEMY = 9; +static SCORE ROOK_ON_HALF_OPEN_WITH_FRIEND = 10; + +// Same treatment for the enemy/friend-passer-on-file bonuses -- was +// PASSER_BY_RANK[xColor][rank]/4 (enemy case) and a rank-scaled +// behind/leads-passer split (friend case, +1..+25 behind, -3..-22 in +// front); flat means of the meaningful (non-padding) entries. +static SCORE ROOK_WITH_ENEMY_PASSER = 17; +static SCORE ROOK_WITH_FRIEND_PASSER = 10; static SCORE ROOK_BEHIND_PASSER_BY_PASSER_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 @@ -637,10 +580,6 @@ static SCORE ROOK_MOBILITY_BY_SQUARES[15] = -28, -24, -20, -14, -7, -2, +0, +4, +8, +12, +15, +17, +19, +21, +22 };// ^ | -static SCORE ROOK_MAX_MOBILITY_IN_A_ROW_BONUS[8] = -{// 0 1 2 3 4 5 6 7 - -15, -6, +0, +4, +8, +8, +8, +8 -}; // // Queen eval terms @@ -666,11 +605,6 @@ static SCORE QUEEN_MOBILITY_BY_SQUARES[28] = +15, +16, +17, +18, +19, +20, +20, +21, +21, +22, +22, +23, +23 }; -static SCORE QUEEN_OUT_EARLY[5] = -{// 0 1 2 3 4 : num unmoved minors - 0, -14, -22, -26, -33 -}; - // // In "Evaluation of Material Imbalance", IM Larry Kaufman makes the // point that queens are worth more then the standard nine "points". @@ -798,6 +732,7 @@ static DNA_BASE_SIZE g_EvalDNA[] = { DNA_MATRIX(TRADE_PIECES), DNA_MATRIX(DONT_TRADE_PAWNS), DNA_ARRAY(REDUCED_MATERIAL_DOWN_SCALER), + DNA_ARRAY(CASTLE_AND_DEVELOPMENT_SCALER), DNA_ARRAY(REDUCED_MATERIAL_UP_SCALER), DNA_ARRAY(PASSER_MATERIAL_UP_SCALER), DNA_ARRAY(PAWN_CENTRALITY_BONUS), @@ -817,11 +752,10 @@ static DNA_BASE_SIZE g_EvalDNA[] = { DNA_VAR(RACER_WINS_RACE), DNA_ARRAY(UNDEVELOPED_MINORS_IN_OPENING), DNA_VAR(BISHOP_OVER_KNIGHT_IN_ENDGAME), - DNA_MATRIX(BISHOP_PAIR), + DNA_VAR(BISHOP_PAIR_BONUS), DNA_ARRAY(STATIONARY_PAWN_ON_BISHOP_COLOR), DNA_ARRAY(TRANSIENT_PAWN_ON_BISHOP_COLOR), DNA_ARRAY(BISHOP_MOBILITY_BY_SQUARES), - DNA_ARRAY(BISHOP_MAX_MOBILITY_IN_A_ROW_BONUS), DNA_ARRAY(BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING), DNA_ARRAY(KNIGHT_CENTRALITY_BONUS), DNA_ARRAY(KNIGHT_KING_TROPISM_BONUS), @@ -830,9 +764,11 @@ static DNA_BASE_SIZE g_EvalDNA[] = { DNA_ARRAY(KNIGHT_MOBILITY_BY_COUNT), DNA_ARRAY(KNIGHT_WITH_N_PAWNS_SUPPORTING), DNA_ARRAY(KNIGHT_IN_CLOSED_POSITION), - DNA_ARRAY(ROOK_ON_FULL_OPEN_BY_DIST_FROM_EKING), - DNA_ARRAY(ROOK_ON_HALF_OPEN_WITH_ENEMY_BY_DIST_FROM_EKING), - DNA_ARRAY(ROOK_ON_HALF_OPEN_WITH_FRIEND_BY_DIST_FROM_EKING), + DNA_VAR(ROOK_ON_FULL_OPEN), + DNA_VAR(ROOK_ON_HALF_OPEN_WITH_ENEMY), + DNA_VAR(ROOK_ON_HALF_OPEN_WITH_FRIEND), + DNA_VAR(ROOK_WITH_ENEMY_PASSER), + DNA_VAR(ROOK_WITH_FRIEND_PASSER), DNA_MATRIX(ROOK_BEHIND_PASSER_BY_PASSER_RANK), DNA_MATRIX(ROOK_LEADS_PASSER_BY_PASSER_RANK), DNA_VAR(KING_TRAPPING_ROOK), @@ -840,9 +776,7 @@ static DNA_BASE_SIZE g_EvalDNA[] = { DNA_VAR(ROOK_CONNECTED_VERT), DNA_VAR(ROOK_CONNECTED_HORIZ), DNA_ARRAY(ROOK_MOBILITY_BY_SQUARES), - DNA_ARRAY(ROOK_MAX_MOBILITY_IN_A_ROW_BONUS), DNA_ARRAY(QUEEN_MOBILITY_BY_SQUARES), - DNA_ARRAY(QUEEN_OUT_EARLY), DNA_ARRAY(QUEEN_KING_TROPISM), DNA_MATRIX(KING_INITIAL_COUNTER_BY_LOCATION), DNA_ARRAY(KING_TO_CENTER), @@ -1172,8 +1106,6 @@ Return value: control it evenly, WHITE if white controls the square, or BLACK if black controls the square. - TODO: fix this to use counts / xrays - **/ { // @@ -1220,10 +1152,6 @@ Return value: ASSERT((uWhite & 0xFFFFFF00) == 0); ASSERT((uBlack & 0xFFFFFF00) == 0); - // TODO: keep these and update the table to use them - uWhite >>= 3; - uBlack >>= 3; - p = pos->rgSquare[c].pPiece; // p -= 2; ch = g_SwapTable[p][uWhite][uBlack]; @@ -2324,10 +2252,10 @@ Return value: ULONG uCounter = 0; ULONG xSide = FLIP(uSide); COOR cKing; + BITBOARD bbKingZone; + BITBOARD bbBlockers; + BITBOARD bb; COOR c; - int i; - PIECE p; - ULONG u; // // Don't count king safety defects if the real eval code in @@ -2345,136 +2273,271 @@ Return value: ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) == uSide); // - // Make sure cKing - 1, cKing and cKing + 1 are on the board - // - cKing += (!IS_ON_BOARD(cKing - 1)); - cKing -= (!IS_ON_BOARD(cKing + 1)); - ASSERT(IS_ON_BOARD(cKing)); - ASSERT(IS_ON_BOARD(cKing + 1)); - ASSERT(IS_ON_BOARD(cKing - 1)); - - // - // Consider all enemy pieces except the king (not including pawns) - // - for (u = 1; - u < pos->uNonPawnCount[xSide][0]; - u++) + // board_representation/EVAL.md section 9 follow-up (2026-09-05): + // real, blocker-aware attack tests against the king's actual + // 8-neighbor zone (the same zone _EvalKing's own KingSafetyDeltas + // loop tests, now that its own bug -- testing 11 squares including + // two bogus same-rank-2-away entries -- is fixed), replacing the + // old CHECK_VECTOR_WITH_INDEX geometry-only lookup. That lookup + // tested three hypothetical king positions (cKing-1, cKing, + // cKing+1 -- always on the same rank, driven by CHECK_VECTOR's + // one-dimensional index arithmetic) via a table with no board- + // occupancy awareness at all -- it couldn't tell a rook on an open + // file from one blocked by its own pawn, and never tested whether + // a piece threatened a square one rank away from the king. This + // version can't reuse pos->bbXAttacks (Eval()-scoped, and this + // function is also called directly from search.c/searchsup.c as + // an extension/reduction gate, independent of whether Eval() ran + // on the current position at all) -- but pos->bbOccupied is a + // real, always-current POSITION field regardless, so a fresh, + // per-piece magic-bitboard lookup here is both cheap (same O(1) + // shape as the old table lookups) and genuinely blocker-aware. + // X-rays deliberately not considered via chain-following (that + // would double the per-slider cost of a function called at every + // lazy-eval check across the whole search tree, for what's already + // a rough magnitude estimate, not an exact score) -- but the + // occupancy fed to the slider lookups below is deliberately just + // xSide's own pieces (_BuildFriendlySideBB(pos, xSide)), not the + // whole board: a piece belonging to uSide (a defending pawn, the + // king itself) is transparent to these rays, only xSide's own + // pieces actually block. This gets latent-threat detection (a + // rook aimed at the king zone but currently shielded by the + // king's own pawn still counts) for free, at zero extra cost -- + // still one magic lookup per slider, just against a different + // occupancy bitboard -- by direct instruction: we don't count our + // own pieces as blocking the enemy king. + // + bbKingZone = g_KingAttacksBB[cKing] | COOR_TO_BB(cKing); + bbBlockers = _BuildFriendlySideBB(pos, xSide); + + bb = pos->bbPieces[xSide][KNIGHT]; + while (bb) { - c = pos->cNonPawns[xSide][u]; - ASSERT(IS_ON_BOARD(c)); - i = (int)c - (int)(cKing + 1); - ASSERT((i >= -128) && (i <= 125)); - p = pos->rgSquare[c].pPiece; - ASSERT(pos->rgSquare[c].uIndex == u); - ASSERT(!IS_KING(p)); - ASSERT(GET_COLOR(p) == xSide); - p = 1 << PIECE_TYPE(p); - - uCounter += (int)(i == 0) | (int)(i == -2) | - (int)((CHECK_VECTOR_WITH_INDEX(i, xSide) & p) != 0) | - (int)((CHECK_VECTOR_WITH_INDEX(i + 1, xSide) & p) != 0) | - (int)((CHECK_VECTOR_WITH_INDEX(i + 2, xSide) & p) != 0); + c = CoorFromBitBoardRank8ToRank1(&bb); + uCounter += (g_KnightAttacksBB[c] & bbKingZone) != 0; + } + bb = pos->bbPieces[xSide][BISHOP]; + while (bb) + { + c = CoorFromBitBoardRank8ToRank1(&bb); + uCounter += (_BishopAttacksBB(c, bbBlockers) & bbKingZone) != 0; + } + bb = pos->bbPieces[xSide][ROOK]; + while (bb) + { + c = CoorFromBitBoardRank8ToRank1(&bb); + uCounter += (_RookAttacksBB(c, bbBlockers) & bbKingZone) != 0; + } + // Queen hits count double -- by direct instruction: a queen near + // the king is a real, distinct danger source _EvalKing's own + // KING_QUEEN_PROXIMITY_DANGER term prices separately from generic + // attack presence, and this counter had no way to reflect that + // (a queen hitting the zone counted exactly the same as a lone + // knight doing so). Raises the bound below accordingly (up to 8 + // queens are legal via promotion, each now worth 2). + bb = pos->bbPieces[xSide][QUEEN]; + while (bb) + { + c = CoorFromBitBoardRank8ToRank1(&bb); + uCounter += 2 * (((_RookAttacksBB(c, bbBlockers) | + _BishopAttacksBB(c, bbBlockers)) & bbKingZone) != 0); } - ASSERT(uCounter < 15); + ASSERT(uCounter < 30); - // Save the number of enemy pieces pointing at this king for later use. - pos->uPiecesPointingAtKing[uSide] = - (uCounter - (KING_INITIAL_COUNTER_BY_LOCATION[uSide][cKing] >> 1)); return uCounter; } -static void -EstimatePositionalScore(IN POSITION *pos, - IN UNUSED PAWN_HASH_ENTRY *pHash, - IN OUT SCORE *piAlphaMargin, - IN OUT SCORE *piBetaMargin) +static ULONG +_ComputeFileStormDefects(IN POSITION *pos, + IN const PAWN_HASH_ENTRY *pHash, + IN ULONG uColor) /** Routine description: - 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. + Real, exact open-file/storming-pawn defect count for uColor's + king -- king's own file plus the two adjacent ones (2026-08-30: + collapsed from three copy-pasted blocks that differed only in + c-1/c/c+1). Extracted unchanged from _EvalKing (2026-09-05, + board_representation/EVAL.md section 9's king-safety-hash + prototype) so it can be called once per side from + _GetFileStormDefects's cache-miss path instead of inline. - 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. +Parameters: - 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. + POSITION *pos + PAWN_HASH_ENTRY *pHash + ULONG uColor - 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. +Return value: + + ULONG + +**/ +{ + static ULONG KingStormingPawnDefects[8] = + { + +0, +4, +3, +1, +0, +0, +0, +0 + }; + static ULONG KingFileDefects[3] = + { + +2, +1, +0 + }; + ULONG ufColor = FLIP(uColor); + COOR c = pos->cNonPawns[uColor][0]; + ULONG uCounter = 0; + COOR cFileSq, cSquare; + ULONG u, v; + BITBOARD bb; + int w; + + for (w = -1; w <= 1; w++) + { + cFileSq = c + w; + if (!IS_ON_BOARD(cFileSq)) + { + continue; + } + u = FILE(cFileSq) + 1; + v = (pHash->uCountPerFile[WHITE][u] > 0) + + (pHash->uCountPerFile[BLACK][u] > 0); + ASSERT((v >= 0) && (v <= 2)); + uCounter += (KingFileDefects[v] + ((v < 2) && ((u == 1) || (u == 8)))); + bb = pos->bbPawns[ufColor] & BBFILE[u - 1]; + if (bb) + { + if (uColor == WHITE) + { + cSquare = CoorFromBitBoardRank1ToRank8(&bb); + } + else + { + cSquare = CoorFromBitBoardRank8ToRank1(&bb); + } + ASSERT(IS_ON_BOARD(cSquare)); + uCounter += KingStormingPawnDefects[DISTANCE(cSquare, c)]; + } + } + return uCounter; +} + + +static ULONG +_GetFileStormDefects(IN SEARCHER_THREAD_CONTEXT *ctx, + IN POSITION *pos, + IN const PAWN_HASH_ENTRY *pHash, + IN ULONG uColor) +/** + +Routine description: + + EXPERIMENTAL, prototype-only (2026-09-05, board_representation/ + EVAL.md section 9's king-safety-hash discussion). Cache front end + for _ComputeFileStormDefects, keyed on (pos->u64PawnSig, both + kings' squares) -- deliberately covers only the DO_KING_SAFETY_ + THRESHOLD-gated "safety branch" term (measured ~96% hit rate in + general/middlegame positions), not the u<8-gated "endgame branch" + terms (king-supporting-own-passer, in/out-of-action), which + measured a much worse ~75-85% hit rate in bare K+P endgames and + are cheap enough there that caching them isn't expected to pay + off -- those stay inline in _EvalKing, uncached. + + On a miss, computes *both* sides' defect counts and stores them + together, so whichever king _EvalKing evaluates second within the + same Eval() call (kings are always evaluated black-then-white) is + a guaranteed hit -- only one real miss per position, not one per + king. Parameters: + SEARCHER_THREAD_CONTEXT *ctx 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) + PAWN_HASH_ENTRY *pHash + ULONG uColor Return value: - void + ULONG **/ { - // 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 - }; - // 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)]; + UINT64 u64Key = pos->u64PawnSig ^ + g_u64SigSeeds[pos->cNonPawns[WHITE][0]][KING][WHITE] ^ + g_u64SigSeeds[pos->cNonPawns[BLACK][0]][KING][BLACK]; + ULONG uSlot = (ULONG)u64Key & (KING_SAFETY_HASH_TABLE_SIZE - 1); + KING_SAFETY_HASH_ENTRY *pEntry = &ctx->rgKingSafetyHash[uSlot]; +#ifdef PERF_COUNTERS + UINT64 u64Start = SystemReadTimeStampCounter(); +#endif - *piAlphaMargin += iKingTerm + iResidualP90; - *piBetaMargin += iKingTerm + iResidualP90; + INC(ctx->sCounters.kingsafetyhash.u64Probes); + if (pEntry->u64Key == u64Key) + { + INC(ctx->sCounters.kingsafetyhash.u64Hits); +#ifdef PERF_COUNTERS + ctx->sCounters.kingsafetyhash.u64CyclesHit += + (SystemReadTimeStampCounter() - u64Start); +#endif + return pEntry->uFileStormDefects[uColor]; + } + pEntry->u64Key = u64Key; + pEntry->uFileStormDefects[WHITE] = _ComputeFileStormDefects(pos, pHash, WHITE); + pEntry->uFileStormDefects[BLACK] = _ComputeFileStormDefects(pos, pHash, BLACK); +#ifdef PERF_COUNTERS + ctx->sCounters.kingsafetyhash.u64CyclesMiss += + (SystemReadTimeStampCounter() - u64Start); +#endif + return pEntry->uFileStormDefects[uColor]; } +#ifdef CALIBRATE_POSITIONAL // -// ====================================================================== +// board_representation/EVAL.md section 9 recalibration (2026-09-05): +// histogram of (cheap defect index -> real KING_SAFETY_BY_COUNTER value), +// one row per possible EstimatePositionalScore bucket (the same +// MINU(10, CountKingSafetyDefects + _GetFileStormDefects) index it looks +// up), so a p90-per-row read after a calibration run over a large sample +// (curated suites or a slice of ecm.ep_, at real search depth so the +// distribution matches what the search tree actually sees, not just root +// positions) gives a fresh iKingSwingP90 table -- coarse cp bins, not raw +// samples, to keep this cheap enough to leave compiled into a whole search +// run rather than needing to buffer millions of individual values. // +#define CALIBRATE_NUM_BUCKETS 11 +#define CALIBRATE_BIN_WIDTH 5 +#define CALIBRATE_NUM_BINS 200 // covers raw KING_SAFETY_BY_COUNTER 0..999 +static UINT64 g_CalibrateHist[CALIBRATE_NUM_BUCKETS][CALIBRATE_NUM_BINS]; +UINT64 g_uCalibrateGateChecked = 0; +UINT64 g_uCalibrateGateTrue = 0; +UINT64 g_uCalibrateEvalCalls = 0; static void -_RecordTrappedCandidate(IN OUT POSITION *pos, - IN ULONG uColor, - IN COOR c) +_RecordPositionalCalibration(IN SEARCHER_THREAD_CONTEXT *ctx, + IN POSITION *pos, + IN const PAWN_HASH_ENTRY *pHash, + IN ULONG uColor, + IN ULONG uRawCounterValue) /** Routine description: - Record a zero-mobility piece as a trapped-piece candidate for - uColor. Used by _EvalBishop/_EvalKnight/_EvalRook/_EvalQueen; the - actual attacked-or-not check happens later, in - _EvalTrappedPieces. Capped at ARRAY_LENGTH(pos->cTrapped[0]) - candidates per side -- extras are just dropped, not a real concern - in practice. + Bucket one real _EvalKing sample (this side's raw, pre-material-scaled + KING_SAFETY_BY_COUNTER lookup) by the same cheap defect index + EstimatePositionalScore would have used to estimate it, so a later + DumpPositionalCalibration can compute a fresh p90-per-bucket table. Parameters: - POSITION *pos, - ULONG uColor, - COOR c + SEARCHER_THREAD_CONTEXT *ctx + POSITION *pos + PAWN_HASH_ENTRY *pHash + ULONG uColor + ULONG uRawCounterValue : KING_SAFETY_BY_COUNTER[uCounter], before the + REDUCED_MATERIAL_DOWN_SCALER scaling _EvalKing applies next -- + EstimatePositionalScore's table is looked up then scaled the same + way, so the table itself should be fit against the unscaled value. Return value: @@ -2482,83 +2545,584 @@ Return value: **/ { - if (pos->uNumTrapped[uColor] < ARRAY_LENGTH(pos->cTrapped[uColor])) + ULONG uIndex; + ULONG uBin; + + // Only sample nodes Eval()'s own base-margin ("super lazy") gate + // would actually pass through to EstimatePositionalScore in a real + // build -- see ctx->fCalibrateCandidate's comment in chess.h. + if (FALSE == ctx->fCalibrateCandidate) { - pos->cTrapped[uColor][pos->uNumTrapped[uColor]++] = c; + return; } + + uIndex = MINU(10, CountKingSafetyDefects(pos, uColor) + + _GetFileStormDefects(ctx, pos, pHash, uColor)); + uBin = MINU(CALIBRATE_NUM_BINS - 1, + uRawCounterValue / CALIBRATE_BIN_WIDTH); + + g_CalibrateHist[uIndex][uBin]++; } -static void -_EvalBishop(IN OUT POSITION *pos, - IN COOR c, - IN PAWN_HASH_ENTRY *pHash) +void +DumpPositionalCalibration(void) /** Routine description: + Print a fresh iKingSwingP90[] table (p90 of the real, unscaled + KING_SAFETY_BY_COUNTER value observed at each cheap defect-index + bucket) from the histogram _RecordPositionalCalibration has been + filling for the duration of this run. Only meaningful in a + CALIBRATE_POSITIONAL build, after running a representative workload + (the curated suites, or an ecm.ep_ slice, at real search depth -- + root-only positions won't exercise the distribution of positions the + search tree actually evaluates). + Parameters: - POSITION *pos, - COOR c, - PAWN_HASH_ENTRY *pHash, + void Return value: - FLAG + void **/ { - static const BITBOARD bbColorSq[2] = { 0x55aa55aa55aa55aaULL, - 0xaa55aa55aa55aa55ULL }; - static const COOR cBishopAtHome[2][2] = + ULONG uIndex; + ULONG uBin; + UINT64 u64Total; + UINT64 u64Running; + SCORE iP90; + + Trace("Positional calibration: Eval() called %" COMPILER_LONGLONG_UNSIGNED_FORMAT " times\n", + g_uCalibrateEvalCalls); + Trace("Positional calibration: gate checked %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " times, true %" COMPILER_LONGLONG_UNSIGNED_FORMAT " times\n", + g_uCalibrateGateChecked, g_uCalibrateGateTrue); + Trace("Positional calibration (p90 per bucket, sample counts):\n"); + for (uIndex = 0; uIndex < CALIBRATE_NUM_BUCKETS; uIndex++) { - { C8, F8 }, // BLACK - { C1, F1 } // WHITE - }; - ULONG uColor; - BITBOARD bb; - BITBOARD bbMask; - BITBOARD bbPc; - COOR cSquare; - SCORE i; - ULONG u; - ULONG uTotalMobility; - ULONG uMaxMobility; - PIECE p; + u64Total = 0; + for (uBin = 0; uBin < CALIBRATE_NUM_BINS; uBin++) + { + u64Total += g_CalibrateHist[uIndex][uBin]; + } + if (0 == u64Total) + { + Trace(" bucket %2u: no samples\n", uIndex); + continue; + } + u64Running = 0; + iP90 = (SCORE)((CALIBRATE_NUM_BINS - 1) * CALIBRATE_BIN_WIDTH); + for (uBin = 0; uBin < CALIBRATE_NUM_BINS; uBin++) + { + u64Running += g_CalibrateHist[uIndex][uBin]; + if (u64Running * 10 >= u64Total * 9) + { + iP90 = (SCORE)(uBin * CALIBRATE_BIN_WIDTH); + break; + } + } + Trace(" bucket %2u: p90=%d (n=%" COMPILER_LONGLONG_UNSIGNED_FORMAT ")\n", + uIndex, iP90, u64Total); + } +} +#endif // CALIBRATE_POSITIONAL - ASSERT(IS_ON_BOARD(c)); - p = pos->rgSquare[c].pPiece; - ASSERT(p && IS_BISHOP(p)); - uColor = GET_COLOR(p); -#ifdef DEBUG - pos->cPiece = c; -#endif +#if defined(CALIBRATE_BASE_MARGIN) || defined(CALIBRATE_MARGIN_SAFETY) +// +// board_representation/EVAL.md section 9: is LAZY_EVAL_BASE_MARGIN (the +// flat, un-widened margin Eval() screens with before ever calling +// EstimatePositionalScore) well-sized? Two distinct questions, tracked +// separately, neither requiring the lazy-exit machinery to be disabled +// (unlike the CALIBRATE_POSITIONAL king-safety work above) -- real +// exits still happen normally here, so this runs at normal speed: +// +// 1. Once the gate passes and EstimatePositionalScore has widened the +// margin, does a real exit actually happen, broken out by the +// worse (max) of the two sides' cheap defect-index buckets? This +// answers "does the 'very dangerous' bucket (10) ever actually +// exit, or does its wide margin effectively rule it out". +// 2. When the gate comes back *false* (we skip straight to full eval, +// no lazy attempt tried at all) -- would a real exit have been +// available anyway, had we bothered to compute the widened margin? +// A "yes" here is a false negative: LAZY_EVAL_BASE_MARGIN was +// tight enough to force full eval on a node that genuinely could +// have taken the cheap path. +// +// 2026-09-06: added a material dimension -- pos->uArmyScaler[WHITE] + +// pos->uArmyScaler[BLACK] (0-62), bucketed into 8 coarse bands, since +// the lazy/full cost ratio measured directly (opening/middlegame vs a +// bare K+P endgame) varies ~7x by material, and LAZY_EVAL_BASE_MARGIN +// is currently one flat constant across all of it. +#define BASE_MARGIN_MAT_BUCKETS 8 +#define BASE_MARGIN_MAT_WIDTH 8 +static UINT64 g_uBaseMarginExitAttempted[BASE_MARGIN_MAT_BUCKETS][11]; +static UINT64 g_uBaseMarginExitTaken[BASE_MARGIN_MAT_BUCKETS][11]; +static UINT64 g_uBaseMarginFalseNegative[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uBaseMarginCorrectlyFull[BASE_MARGIN_MAT_BUCKETS]; - // - // Unmoved piece - // - pos->uMinorsAtHome[uColor] += (c == cBishopAtHome[uColor][0]); - pos->uMinorsAtHome[uColor] += (c == cBishopAtHome[uColor][1]); - ASSERT(pos->uMinorsAtHome[uColor] <= 4); +static ULONG +_MaxKingDangerBucket(IN SEARCHER_THREAD_CONTEXT *ctx, + IN POSITION *pos, + IN PAWN_HASH_ENTRY *pHash) +/** - // - // Good and bad bishops - // - i = 16; - bbMask = bbColorSq[IS_SQUARE_WHITE(c)]; - bb = pHash->bbStationaryPawns[uColor] & bbMask; - ASSERT(CountBits(bb) <= 16); - while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) - { - // - // Consider stationary (rammed or backward) pawns of the same - // color as the bishop. - // - ASSERT(pos->rgSquare[cSquare].pPiece); - ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); - ASSERT(GET_COLOR(pos->rgSquare[cSquare].pPiece) == uColor); +Routine description: + + Cheap helper for CALIBRATE_BASE_MARGIN instrumentation only: the + worse of the two sides' combined cheap defect-index buckets, same + definition EstimatePositionalScore's iKingSwingP90 lookup uses. + Re-reads CountKingSafetyDefects/_GetFileStormDefects rather than + threading extra output params through EstimatePositionalScore -- + both hit the king-safety hash on a repeat call within the same + Eval(), so this is cheap (see rgKingSafetyHash's "guaranteed + intra-call hit" comment). + +Parameters: + + SEARCHER_THREAD_CONTEXT *ctx + POSITION *pos + PAWN_HASH_ENTRY *pHash + +Return value: + + ULONG : 0-10 + +**/ +{ + ULONG uWhite = MINU(10, CountKingSafetyDefects(pos, WHITE) + + _GetFileStormDefects(ctx, pos, pHash, WHITE)); + ULONG uBlack = MINU(10, CountKingSafetyDefects(pos, BLACK) + + _GetFileStormDefects(ctx, pos, pHash, BLACK)); + return MAXU(uWhite, uBlack); +} + + +static ULONG +_MaterialBucket(IN POSITION *pos) +/** + +Routine description: + + CALIBRATE_BASE_MARGIN instrumentation only: coarse combined-material + bucket, 0 (bare kings) to BASE_MARGIN_MAT_BUCKETS-1 (full army). + +Parameters: + + POSITION *pos + +Return value: + + ULONG : 0..BASE_MARGIN_MAT_BUCKETS-1 + +**/ +{ + ULONG uCombined = pos->uArmyScaler[WHITE] + pos->uArmyScaler[BLACK]; + return MINU(BASE_MARGIN_MAT_BUCKETS - 1, uCombined / BASE_MARGIN_MAT_WIDTH); +} + + +void +DumpBaseMarginCalibration(void) +/** + +Routine description: + + Print LAZY_EVAL_BASE_MARGIN calibration stats gathered so far this + run: per-(material bucket, king-danger bucket) exit-attempted/ + exit-taken counts (command.c's "calibrate basemargin"), and the + false-negative rate of the flat pre-EstimatePositionalScore gate, + broken out by material bucket. + +Parameters: + + void + +Return value: + + void + +**/ +{ + ULONG m, u; + UINT64 u64FN, u64CF; + + for (m = 0; m < BASE_MARGIN_MAT_BUCKETS; m++) + { + Trace("Base margin calibration -- material bucket %u " + "(combined army scaler %u-%u):\n", + m, m * BASE_MARGIN_MAT_WIDTH, + (m * BASE_MARGIN_MAT_WIDTH) + BASE_MARGIN_MAT_WIDTH - 1); + for (u = 0; u <= 10; u++) + { + if (0 == g_uBaseMarginExitAttempted[m][u]) + { + continue; + } + Trace(" king-danger bucket %2u: %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " attempted, %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " exited (%.2f%%)\n", + u, g_uBaseMarginExitAttempted[m][u], + g_uBaseMarginExitTaken[m][u], + (100.0 * (double)g_uBaseMarginExitTaken[m][u] / + (double)g_uBaseMarginExitAttempted[m][u])); + } + u64FN = g_uBaseMarginFalseNegative[m]; + u64CF = g_uBaseMarginCorrectlyFull[m]; + Trace(" false negatives: %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " correctly full: %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " rate: %.4f%%\n", + u64FN, u64CF, + (u64FN + u64CF > 0) ? + (100.0 * (double)u64FN / (double)(u64FN + u64CF)) : 0.0); + } +} +#endif // CALIBRATE_BASE_MARGIN + + +#ifdef CALIBRATE_MARGIN_SAFETY +// +// board_representation/EVAL.md section 9: for every node that actually +// took a lazy exit, how far off was the real (full-eval) score from +// the lazy-path score that got returned? The *maximum* observed swing, +// not just its p90, is what answers "how large would LAZY_EVAL_BASE_ +// MARGIN have had to be before some exit here would have been +// unsound" -- unlike iKingSwingP90 (an estimate that's allowed to be +// wrong ~10% of the time by design), a used margin being exceeded even +// once is a genuine soundness question, so max is the right statistic +// here, not a percentile. Bucketed by combined material (same bucketing +// as CALIBRATE_BASE_MARGIN) since the earlier lazy/full cost-ratio +// measurement showed the two regimes are very different. +// +static UINT64 g_uMarginSwingMax[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uMarginSwingCount[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uMarginSwingSum[BASE_MARGIN_MAT_BUCKETS]; +// Genuine soundness check: did the swing ever exceed the *actual* +// (already-widened) margin that was used to justify this specific +// exit? Unlike the raw max above (which just says "how far things +// moved"), this directly answers "was any real exit actually unsound". +static UINT64 g_uMarginExceeded[BASE_MARGIN_MAT_BUCKETS]; + +static void +RecordMarginSafetySwing(IN POSITION *pos, + IN SCORE iSwing, + IN SCORE iActualMarginUsed) +/** + +Routine description: + + Record one (material bucket, |real - lazy| swing) sample for + DumpBaseMarginCalibration's margin-safety report, and check whether + this specific exit's swing exceeded the actual margin that was used + to justify it (a genuine unsoundness event, not just "large"). + +Parameters: + + POSITION *pos + SCORE iSwing : >= 0 + SCORE iActualMarginUsed : the real (post-EstimatePositionalScore) + alpha or beta margin this exit was taken under + +Return value: + + void + +**/ +{ + ULONG uMatBucket = _MaterialBucket(pos); + + if (iSwing > iActualMarginUsed) + { + g_uMarginExceeded[uMatBucket]++; + } + + ASSERT(iSwing >= 0); + g_uMarginSwingCount[uMatBucket]++; + g_uMarginSwingSum[uMatBucket] += (UINT64)iSwing; + if ((UINT64)iSwing > g_uMarginSwingMax[uMatBucket]) + { + g_uMarginSwingMax[uMatBucket] = (UINT64)iSwing; + } +} + + +void +DumpMarginSafetyCalibration(void) +/** + +Routine description: + + Print, per material bucket, the max and average |real - lazy| swing + observed among nodes that actually took a lazy exit this run + (command.c's "calibrate marginsafety"). The max is the number that + tells you how large LAZY_EVAL_BASE_MARGIN would need to be, at that + material level, before an exit could have been unsound. + +Parameters: + + void + +Return value: + + void + +**/ +{ + ULONG m; + + Trace("Margin safety -- max/avg |real - lazy| swing among exits taken, " + "by material bucket:\n"); + for (m = 0; m < BASE_MARGIN_MAT_BUCKETS; m++) + { + if (0 == g_uMarginSwingCount[m]) + { + Trace(" material bucket %u (scaler %u-%u): no exits taken\n", + m, m * BASE_MARGIN_MAT_WIDTH, + (m * BASE_MARGIN_MAT_WIDTH) + BASE_MARGIN_MAT_WIDTH - 1); + continue; + } + Trace(" material bucket %u (scaler %u-%u): max=%" + COMPILER_LONGLONG_UNSIGNED_FORMAT " avg=%.1f (n=%" + COMPILER_LONGLONG_UNSIGNED_FORMAT ") exceeded=%" + COMPILER_LONGLONG_UNSIGNED_FORMAT "\n", + m, m * BASE_MARGIN_MAT_WIDTH, + (m * BASE_MARGIN_MAT_WIDTH) + BASE_MARGIN_MAT_WIDTH - 1, + g_uMarginSwingMax[m], + (double)g_uMarginSwingSum[m] / (double)g_uMarginSwingCount[m], + g_uMarginSwingCount[m], g_uMarginExceeded[m]); + } +} +#endif // CALIBRATE_MARGIN_SAFETY + + +static void +EstimatePositionalScore(IN SEARCHER_THREAD_CONTEXT *ctx, + IN POSITION *pos, + IN 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 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: + + SEARCHER_THREAD_CONTEXT *ctx : unused as of 2026-09-06 + (_GetFileStormDefects, the only user, was pulled out of this + hot path -- see the iKingSwingP90 staleness comment below); + kept in the signature for call-site stability. + POSITION *pos + PAWN_HASH_ENTRY *pHash : unused as of 2026-09-06, same reason as + ctx above. pHash->iScore is already folded into pos->iScore by + this point regardless, 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: + + void + +**/ +{ + // p90 of the real, unscaled |KING_SAFETY_BY_COUNTER| magnitude + // _EvalKing computes, indexed per-side by MINU(10, + // CountKingSafetyDefects(side)). + // + // STALE as of 2026-09-06 (board_representation/EVAL.md section 9): + // fit while this index also included _GetFileStormDefects(side) + // (~1.28B samples, see command.c's "calibrate dump" and git history + // for the fit-time numbers). _GetFileStormDefects was pulled out of + // this hot path the same day -- it cost ~181 of ~1619 avg eval + // cycles (measured via EVAL_TIME), more than the "cheap cached + // lookup" it was assumed to be, running on ~90% of all Eval() calls + // regardless of whether an exit ever resulted. Recalibration against + // the now-narrower (CountKingSafetyDefects-only) index deliberately + // deferred; until then this table is a reasonable but unverified + // approximation. Worst-case error from dropping the storm term: the + // index it fed into is clamped to 10 either way, so the max possible + // per-side swing this can introduce is iKingSwingP90[10] - + // iKingSwingP90[0] = 300, scaled by REDUCED_MATERIAL_DOWN_SCALER/8 + // (0-1.0) -- only reachable when CountKingSafetyDefects alone is 0 + // but a pawn storm alone would have maxed the old combined index. + // _EvalKing's real, exact score is unaffected either way -- this + // only widens/narrows the lazy-eval margin estimate. + // + // search.c's ">2" and searchsup.c's ">1" fixed-threshold extension + // gates still read CountKingSafetyDefects' raw, unscaled return + // value directly and remain equally stale -- not yet recalibrated. + // + static const SCORE iKingSwingP90[11] = { + 10, 20, 35, 60, 90, 120, 135, 165, 195, 225, 310 + }; + // p90 of |mobility + passers + everything else combined|, measured + // directly (no useful correlation found with piece count). + static const SCORE iResidualP90 = 154; + // Per-side lookup-then-scale, deliberately mirroring _EvalKing's + // own shape exactly (look the raw counter up in a table first, + // *then* scale that resulting estimate by the attacker's army, + // rather than scaling the raw counter before the lookup) -- by + // direct instruction, so this stays as symmetric as possible with + // the real term it's estimating. CountKingSafetyDefects itself is + // untouched by this: it keeps returning a small, unscaled count, + // used as-is by its other two callers. + SCORE iWhiteKingSwing; + SCORE iBlackKingSwing; + SCORE iKingTerm; + ULONG uWhiteDefects, uBlackDefects; +#ifdef EVAL_TIME + UINT64 uSubTimer; +#endif + +#ifdef EVAL_TIME + uSubTimer = SystemReadTimeStampCounter(); +#endif + uWhiteDefects = CountKingSafetyDefects(pos, WHITE); + uBlackDefects = CountKingSafetyDefects(pos, BLACK); +#ifdef EVAL_TIME + ctx->sCounters.tree.u64CyclesEvalCountKingSafetyDefects += + (SystemReadTimeStampCounter() - uSubTimer); +#endif + iWhiteKingSwing = iKingSwingP90[MINU(10, uWhiteDefects)]; + iBlackKingSwing = iKingSwingP90[MINU(10, uBlackDefects)]; + + iWhiteKingSwing = (iWhiteKingSwing * + (SCORE)REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[BLACK]]) / 8; + iBlackKingSwing = (iBlackKingSwing * + (SCORE)REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[WHITE]]) / 8; + iKingTerm = iWhiteKingSwing + iBlackKingSwing; + + *piAlphaMargin += iKingTerm + iResidualP90; + *piBetaMargin += iKingTerm + iResidualP90; +} + + +// +// ====================================================================== +// + +static void +_RecordTrappedCandidate(IN OUT POSITION *pos, + IN ULONG uColor, + IN COOR c) +/** + +Routine description: + + Record a zero-mobility piece as a trapped-piece candidate for + uColor. Used by _EvalBishop/_EvalKnight/_EvalRook/_EvalQueen; the + actual attacked-or-not check happens later, in + _EvalTrappedPieces. Capped at ARRAY_LENGTH(pos->cTrapped[0]) + candidates per side -- extras are just dropped, not a real concern + in practice. + +Parameters: + + POSITION *pos, + ULONG uColor, + COOR c + +Return value: + + void + +**/ +{ + if (pos->uNumTrapped[uColor] < ARRAY_LENGTH(pos->cTrapped[uColor])) + { + pos->cTrapped[uColor][pos->uNumTrapped[uColor]++] = c; + } +} + + +static void +_EvalBishop(IN OUT POSITION *pos, + IN COOR c, + IN PAWN_HASH_ENTRY *pHash) +/** + +Routine description: + +Parameters: + + POSITION *pos, + COOR c, + PAWN_HASH_ENTRY *pHash, + +Return value: + + FLAG + +**/ +{ + static const BITBOARD bbColorSq[2] = { 0x55aa55aa55aa55aaULL, + 0xaa55aa55aa55aa55ULL }; + static const COOR cBishopAtHome[2][2] = + { + { C8, F8 }, // BLACK + { C1, F1 } // WHITE + }; + ULONG uColor; + BITBOARD bb; + BITBOARD bbMask; + BITBOARD bbPc; + COOR cSquare; + SCORE i; + ULONG u; + ULONG uTotalMobility; + PIECE p; + + ASSERT(IS_ON_BOARD(c)); + p = pos->rgSquare[c].pPiece; + ASSERT(p && IS_BISHOP(p)); + uColor = GET_COLOR(p); + + // Undeveloped minor piece; maybe penalized later in EvalKing. + pos->uMinorsAtHome[uColor] += (c == cBishopAtHome[uColor][0]); + pos->uMinorsAtHome[uColor] += (c == cBishopAtHome[uColor][1]); + ASSERT(pos->uMinorsAtHome[uColor] <= 4); + + // Good and bad bishops given stationary pawn structure. + i = 16; + bbMask = bbColorSq[IS_SQUARE_WHITE(c)]; + bb = pHash->bbStationaryPawns[uColor] & bbMask; + ASSERT(CountBits(bb) <= 16); + while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) + { + // Consider stationary (rammed or backward) pawns of the same + // color as the bishop. + ASSERT(pos->rgSquare[cSquare].pPiece); + ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cSquare].pPiece) == uColor); i += STATIONARY_PAWN_ON_BISHOP_COLOR[cSquare]; } EVAL_TERM(uColor, @@ -2585,10 +3149,8 @@ Return value: bb = pos->bbPawns[FLIP(uColor)] & bbPc; while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) { - // // N.B. Only count enemy pawns that are supported by another // pawn. - // ASSERT(pos->rgSquare[cSquare].pPiece); ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); ASSERT(GET_COLOR(pos->rgSquare[cSquare].pPiece) == FLIP(uColor)); @@ -2662,7 +3224,6 @@ Return value: BITBOARD bbMobility = 0; BITBOARD bbXrayAccum = 0; BITBOARD bbFirstLayerMask = 0; - ULONG d; pos->bbMinorAttacks[uColor] |= bbAttack; @@ -2715,28 +3276,14 @@ Return value: pos->bbMinorXrayAttacks[uColor] |= bbXrayAccum; uTotalMobility = CountBits(bbMobility); - - uMaxMobility = 0; - for (d = 0; d < 4; d++) - { - ULONG uThisRay = CountBits(bbMobility & g_BishopRayToEdge[d][c]); - uMaxMobility = MAXU(uMaxMobility, uThisRay); - } } ASSERT(uTotalMobility <= 13); - ASSERT(uMaxMobility <= 7); EVAL_TERM(uColor, BISHOP, c, pos->iScore[uColor], BISHOP_MOBILITY_BY_SQUARES[uTotalMobility], "bishop mobility"); - EVAL_TERM(uColor, - BISHOP, - c, - pos->iScore[uColor], - BISHOP_MAX_MOBILITY_IN_A_ROW_BONUS[uMaxMobility], - "consecutive bishop mobility"); // // Look for bishops with no mobility, they are trapped and, later, @@ -2747,15 +3294,6 @@ Return value: _RecordTrappedCandidate(pos, uColor, c); } -#if 0 - // This is never used right now. - uTotalMobility /= 2; - ASSERT((pos->uMinMobility[uColor] & 0x80000000) == 0); - ASSERT((uTotalMobility & 0x80000000) == 0); - pos->uMinMobility[uColor] = MINU(pos->uMinMobility[uColor], - uTotalMobility); -#endif - // // Bonus for a bishop that's securely placed -- safe from ever // being challenged by an enemy pawn, and (checked below) defended @@ -2827,20 +3365,13 @@ Return value: ASSERT(p && IS_KNIGHT(p)); uColor = GET_COLOR(p); ASSERT(IS_VALID_COLOR(uColor)); -#ifdef DEBUG - pos->cPiece = c; -#endif - // - // Unmoved piece - // + // Unmoved piece; potentially penalized in EvalKing. pos->uMinorsAtHome[uColor] += (c == cKnightAtHome[uColor][0]); pos->uMinorsAtHome[uColor] += (c == cKnightAtHome[uColor][1]); ASSERT(pos->uMinorsAtHome[uColor] <= 4); - // // Centrality bonus - // EVAL_TERM(uColor, KNIGHT, c, @@ -2848,9 +3379,7 @@ Return value: KNIGHT_CENTRALITY_BONUS[c], "board centrality"); - // - // Give a bonus to knights on a closed / busy board - // + // Give a bonus to knights on a closed / busy board. ASSERT(pos->uClosedScaler <= 32); EVAL_TERM(uColor, KNIGHT, @@ -2859,20 +3388,17 @@ Return value: KNIGHT_IN_CLOSED_POSITION[pos->uClosedScaler], "in closed/open position"); - // - // See if square c is safe from enemy pawns; if so give bonus for - // outposted knight which increases the closer it is to the enemy - // king and the more pawns it has supporting it. - // + // Outposted knights. See if square c is safe from enemy pawns; + // if so give bonus for outposted knight which increases the + // closer it is to the enemy king and the more pawns it has + // supporting it. uDist = DISTANCE(c, pos->cNonPawns[FLIP(uColor)][0]); ASSERT((uDist > 0) && (uDist <= 8)); bb = pos->bbPawns[FLIP(uColor)] & (~pHash->bbStationaryPawns[FLIP(uColor)]); if (TRUE == _IsSquareSafeFromEnemyPawn(pos, c, bb)) { - // // Count the number of supporting pawns the knight has - // uPawnsSupporting = 0; p = BLACK_PAWN | uColor; cSquare = c + iPawnStart[uColor]; @@ -2883,9 +3409,7 @@ Return value: (pos->rgSquare[cSquare].pPiece == p)); ASSERT(uPawnsSupporting <= 2); - // // Give a bonus based on distance from enemy king - // i = KNIGHT_UNASSAILABLE_BY_DIST_FROM_EKING[uDist] + KNIGHT_WITH_N_PAWNS_SUPPORTING[uPawnsSupporting]; EVAL_TERM(uColor, @@ -2895,10 +3419,8 @@ Return value: i, "[scaled] unassailable/support"); - // // Now, if there's zero or one supporter see if the enemy has a // bishop the color of the knight it could capture with. - // if (uPawnsSupporting != 2) { if (IS_SQUARE_WHITE(c)) @@ -2930,9 +3452,7 @@ Return value: } else // not outposted { - // // Still good to be close to the enemy king. - // i = KNIGHT_KING_TROPISM_BONUS[uDist]; EVAL_TERM(uColor, KNIGHT, @@ -2942,16 +3462,7 @@ Return value: "[scaled] enemy king tropism"); } - // - // Give a bonus for blockading an enemy backward pawn. (2026-08-30: - // the old comment here claimed we also reward pieces for blocking - // enemy passers in the passer code -- checked, no longer true if - // it ever was. _EvalPassers' "enemy controls/occupies sq ahead" - // terms penalize the PASSER'S OWNER for having its stop-square - // blocked; they don't reward the blocking piece directly. This is - // the only place a blocking piece gets a direct bonus, not a - // duplicate of anything.) - // + // Give a bonus for blockading an enemy backward pawn. cSquare = c + 16 * g_iAhead[uColor]; bb = pHash->bbStationaryPawns[FLIP(uColor)]; if (bb & COOR_TO_BB(cSquare)) @@ -3018,14 +3529,6 @@ Return value: { _RecordTrappedCandidate(pos, uColor, c); } - -#if 0 - // This is never used right now. - ASSERT((pos->uMinMobility[uColor] & 0x80000000) == 0); - ASSERT((uMobilitySquares & 0x80000000) == 0); - pos->uMinMobility[uColor] = MINU(pos->uMinMobility[uColor], - uMobilitySquares); -#endif } static void @@ -3047,145 +3550,53 @@ Return value: FLAG **/ -{ - PIECE p; - ULONG uPawnFile = FILE(c) + 1; - ULONG uColor; - ULONG u; - ULONG uMaxMobility; - ULONG uTotalMobility; - COOR cSquare; - BITBOARD bb; - SCORE i; - - ASSERT(IS_ON_BOARD(c)); - p = pos->rgSquare[c].pPiece; - ASSERT(p && IS_ROOK(p)); - uColor = GET_COLOR(p); - ASSERT(IS_VALID_COLOR(uColor)); - pos->cPiece = c; - - // - // Reward being on a half open or full open file. - // - if (0 == pHash->uCountPerFile[uColor][uPawnFile]) - { - u = FILE_DISTANCE(c, pos->cNonPawns[FLIP(uColor)][0]); - if (0 == pHash->uCountPerFile[FLIP(uColor)][uPawnFile]) - { - // - // Full open - // - EVAL_TERM(uColor, - ROOK, - c, - pos->iScore[uColor], - ROOK_ON_FULL_OPEN_BY_DIST_FROM_EKING[u], - "on full open"); - } - else - { - // - // Half open, no friendly, just enemy. - // - EVAL_TERM(uColor, - ROOK, - c, - pos->iScore[uColor], - ROOK_ON_HALF_OPEN_WITH_ENEMY_BY_DIST_FROM_EKING[u], - "on half open"); - - // - // Added bonus if the enemy pawn is a passer. - // - bb = pHash->bbPasserLocations[FLIP(uColor)] & BBFILE[FILE(c)]; - if (bb) - { - cSquare = CoorFromBitBoardRank8ToRank1(&bb); - EVAL_TERM(uColor, - ROOK, - c, - pos->iScore[uColor], - PASSER_BY_RANK[FLIP(uColor)][RANK(cSquare)] / 4, - "hassles enemy passer"); - } - } - } - else if (0 == pHash->uCountPerFile[FLIP(uColor)][uPawnFile]) - { - // - // Half open, no enemy pawn, just a friendly - // - u = FILE_DISTANCE(c, pos->cNonPawns[FLIP(uColor)][0]); - EVAL_TERM(uColor, - ROOK, - c, - pos->iScore[uColor], - ROOK_ON_HALF_OPEN_WITH_FRIEND_BY_DIST_FROM_EKING[u], - "on half open"); +{ + PIECE p; + ULONG uColor; + ULONG uTotalMobility; + COOR cSquare; + BITBOARD bb; + SCORE i; - // - // See if the friend is a passed pawn and if the rook's in - // front of it or behind it. - // - bb = pHash->bbPasserLocations[uColor] & BBFILE[FILE(c)]; - if (bb) + ASSERT(IS_ON_BOARD(c)); + p = pos->rgSquare[c].pPiece; + ASSERT(p && IS_ROOK(p)); + uColor = GET_COLOR(p); + ASSERT(IS_VALID_COLOR(uColor)); + { + ULONG xColor = FLIP(uColor); + BITBOARD bbFile = BBFILE[FILE(c)]; + BITBOARD bbFriendPawns = pos->bbPawns[uColor] & bbFile; + BITBOARD bbEnemyPawns = pos->bbPawns[xColor] & bbFile; + // Not `static` -- ROOK_ON_FULL_OPEN et al are DNA-tunable + // globals (plain mutable SCORE, not compile-time constants), so + // a static initializer would freeze in whatever value happened + // to be compiled in and never see a later `evaldna read`. A + // plain local array is rebuilt from the live values every call + // (cheap: 4 loads, no branches) and stays correct under tuning. + SCORE ROOK_FULL_HALF_OPEN_BONUS[2][2] = { - // - // IDEA: Rook behind candidates, helpers or sentries is good - // because the file may open soon. - // - while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) - { - if (uColor == WHITE) - { - if (cSquare < c) - { - EVAL_TERM(WHITE, - ROOK, - c, - pos->iScore[WHITE], - ROOK_BEHIND_PASSER_BY_PASSER_RANK[WHITE] - [RANK(cSquare)], - "behind own passer"); - } - else - { - EVAL_TERM(WHITE, - ROOK, - c, - pos->iScore[WHITE], - ROOK_LEADS_PASSER_BY_PASSER_RANK[WHITE] - [RANK(cSquare)], - "in the way of passer"); - } - } else { - ASSERT(uColor == BLACK); - if (cSquare > c) - { - EVAL_TERM(BLACK, - ROOK, - c, - pos->iScore[BLACK], - ROOK_BEHIND_PASSER_BY_PASSER_RANK[BLACK] - [RANK(cSquare)], - "behind own passer"); - } - else - { - EVAL_TERM(BLACK, - ROOK, - c, - pos->iScore[BLACK], - ROOK_LEADS_PASSER_BY_PASSER_RANK[BLACK] - [RANK(cSquare)], - "in the way of passer"); - } - } - } - } + { ROOK_ON_FULL_OPEN, ROOK_ON_HALF_OPEN_WITH_ENEMY }, + { ROOK_ON_HALF_OPEN_WITH_FRIEND, 0 }, + }; + // Collapsed 2026-09-06: friend-passer bonus used to distinguish + // rook-behind-passer (good, rank-scaled +1..+25) from rook-in- + // front-of-passer (bad, -3..-22) -- now a single flat bonus + // regardless of which side of the pawn the rook is on, by + // direct instruction. + SCORE bonus = ROOK_FULL_HALF_OPEN_BONUS[bbFriendPawns != 0] + [bbEnemyPawns != 0]; + + bonus += (SCORE)((bbEnemyPawns & pHash->bbPasserLocations[xColor]) != 0) * + ROOK_WITH_ENEMY_PASSER; + bonus += (SCORE)((bbFriendPawns & pHash->bbPasserLocations[uColor]) != 0) * + ROOK_WITH_FRIEND_PASSER; + + EVAL_TERM(uColor, ROOK, c, pos->iScore[uColor], bonus, + "file/passer bonus"); } + // // Removed 2026-08-30 ("rook trapping enemy king"): a rook on the // 7th/8th rank aligned with the enemy king is exactly the @@ -3240,7 +3651,6 @@ Return value: // credit at the blocker itself, matching the old table); a // friendly rook blocker also earns the connected-rook bonus. // - pos->cPiece = c; { BITBOARD bbAttack = _RookAttacksBB(c, pos->bbOccupied); BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, FLIP(uColor)); @@ -3257,7 +3667,6 @@ Return value: BITBOARD bbMobility = 0; BITBOARD bbXrayAccum = 0; BITBOARD bbFirstLayerMask = 0; - ULONG d; pos->bbRookAttacks[uColor] |= bbAttack; @@ -3341,21 +3750,10 @@ Return value: pos->bbRookXrayAttacks[uColor] |= bbXrayAccum; uTotalMobility = CountBits(bbMobility); - - uMaxMobility = 0; - for (d = 0; d < 4; d++) - { - ULONG uThisRay = CountBits(bbMobility & g_RookRayToEdge[d][c]); - uMaxMobility = MAXU(uMaxMobility, uThisRay); - } } ASSERT(uTotalMobility <= 14); - ASSERT(uMaxMobility <= 7); ASSERT(pos->uArmyScaler[FLIP(uColor)] <= 31); - // Tuning can (and has) pushed this out of its original hand-picked - // bound; not a real invariant. - //ASSERT(REDUCED_MATERIAL_UP_SCALER[pos->uArmyScaler[FLIP(uColor)]] <= 8); i = ROOK_MOBILITY_BY_SQUARES[uTotalMobility]; i *= (int)(REDUCED_MATERIAL_UP_SCALER[pos->uArmyScaler[FLIP(uColor)]] + 1); i /= 8; @@ -3365,75 +3763,39 @@ Return value: pos->iScore[uColor], i, "rook mobility"); - EVAL_TERM(uColor, - ROOK, - c, - pos->iScore[uColor], - ROOK_MAX_MOBILITY_IN_A_ROW_BONUS[uMaxMobility], - "consecutive rook mobility"); - -#if 0 - // This is never used right now. - ASSERT((pos->uMinMobility[uColor] & 0x80000000) == 0); - ASSERT((uTotalMobility & 0x80000000) == 0); - pos->uMinMobility[uColor] = MINU(pos->uMinMobility[uColor], - uTotalMobility); -#endif + if (uTotalMobility < 3) { - // // Look for rooks with no mobility who are under attack. These // pieces are trapped! - // if (uTotalMobility == 0) { _RecordTrappedCandidate(pos, uColor, c); } - // - // Rook trapped in the corner by a stupid friendly king? - // + // Rook trapped in the corner by a stupid friendly king? -- + // color-indexed instead of a WHITE/BLACK branch over two + // mirrored blocks (board_representation/EVAL.md section 9). ASSERT(IS_VALID_COLOR(uColor)); - if (uColor == WHITE) - { - ASSERT(IS_ON_BOARD(c)); - if (RANK1(c)) - { - cSquare = pos->cNonPawns[WHITE][0]; - ASSERT(IS_ON_BOARD(cSquare)); - if (RANK1(cSquare)) - { - if (((cSquare > E1) && (c > cSquare)) || - ((cSquare < D1) && (c < cSquare))) - { - EVAL_TERM(WHITE, - ROOK, - c, - pos->iScore[WHITE], - KING_TRAPPING_ROOK, - "king trapping rook"); - } - } - } - } - else { - ASSERT(uColor == BLACK); + static const ULONG BACK_RANK_MASK[2] = { 0x00, 0x70 }; // BLACK, WHITE + static const COOR E_CORNER[2] = { E8, E1 }; + static const COOR D_CORNER[2] = { D8, D1 }; + ASSERT(IS_ON_BOARD(c)); - if (RANK8(c)) + if ((c & 0xF0) == BACK_RANK_MASK[uColor]) { - cSquare = pos->cNonPawns[BLACK][0]; + cSquare = pos->cNonPawns[uColor][0]; ASSERT(IS_ON_BOARD(cSquare)); - - if (RANK8(cSquare)) + if ((cSquare & 0xF0) == BACK_RANK_MASK[uColor]) { - if (((cSquare > E8) && (c > cSquare)) || - ((cSquare < D8) && (c < cSquare))) + if (((cSquare > E_CORNER[uColor]) && (c > cSquare)) || + ((cSquare < D_CORNER[uColor]) && (c < cSquare))) { - EVAL_TERM(BLACK, + EVAL_TERM(uColor, ROOK, c, - pos->iScore[BLACK], + pos->iScore[uColor], KING_TRAPPING_ROOK, "king trapping rook"); } @@ -3472,34 +3834,13 @@ Return value: ASSERT(p && IS_QUEEN(p)); uColor = GET_COLOR(p); ASSERT(IS_VALID_COLOR(uColor)); - pos->cPiece = c; cKing = pos->cNonPawns[FLIP(uColor)][0]; ASSERT(IS_ON_BOARD(cKing)); ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); - if ((FALSE == pos->fCastled[uColor]) && (pos->uMinorsAtHome[uColor] > 1)) - { - // - // Discourage queen being out too early - // - if (!RANK1(c) && !RANK8(c)) - { - // Tuning can (and has) flipped this sign; not a real invariant. - //ASSERT(QUEEN_OUT_EARLY[pos->uMinorsAtHome[uColor]] < 0); - ASSERT(pos->uMinorsAtHome[uColor] <= 4); - EVAL_TERM(uColor, - QUEEN, - c, - pos->iScore[uColor], - QUEEN_OUT_EARLY[pos->uMinorsAtHome[uColor]], - "queen out too early"); - } - } - else + // Encourage enemy king tropism, but not too early. + if ((TRUE == pos->fCastled[uColor]) || (pos->uMinorsAtHome[uColor] <= 1)) { - // - // Encourage enemy king tropism - // u = DISTANCE(c, cKing); ASSERT((u <= 7) && (u > 0)); EVAL_TERM(uColor, @@ -3601,21 +3942,12 @@ Return value: QUEEN_MOBILITY_BY_SQUARES[uTotalMobility], "queen mobility"); - // // Look for queens with no mobility who are under attack. These // pieces are trapped! - // if (uTotalMobility == 0) { _RecordTrappedCandidate(pos, uColor, c); } -#if 0 - // This is never used right now - ASSERT((pos->uMinMobility[uColor] & 0x80000000) == 0); - ASSERT((uTotalMobility & 0x80000000) == 0); - pos->uMinMobility[uColor] = MINU(pos->uMinMobility[uColor], - uTotalMobility); -#endif // // Removed 2026-08-30 ("pointing near enemy K" / @@ -3631,10 +3963,12 @@ Return value: // } + static void _EvalKing(IN OUT POSITION *pos, IN const COOR c, - IN const PAWN_HASH_ENTRY *pHash) + IN const PAWN_HASH_ENTRY *pHash, + IN SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: @@ -3652,11 +3986,9 @@ Return value: **/ { PIECE p; - ULONG uColor, ufColor; + ULONG uColor, xColor; COOR cSquare; - COOR cFileSq; ULONG u, v; - int w; ULONG uCounter; BITV bvAttack; BITV bvXray; @@ -3673,48 +4005,47 @@ Return value: +4, +2, +1, 0, 0, 0, 0, 0, 0 }; - static ULONG KingStormingPawnDefects[8] = - { - +0, +4, +3, +1, +0, +0, +0, +0 - }; - - static ULONG KingFileDefects[3] = - { - +2, +1, +0 - }; - - static INT KingSafetyDeltas[11] = + // Trimmed from 11 entries to the real 8 king-move squares + // (2026-09-05, by direct instruction): the dropped -2/+2 entries + // (two squares away on the same rank) were never a king move -- + // confirmed intentional signal, not a bug, but judged not worth + // keeping once decoupled from the actual bug (KingSafetyDeltas' + // wrong square set leaking into pos->bbKingAttacks' population, + // fixed separately) on two grounds: CountKingSafetyDefects' own + // rewrite doesn't count them either now, and they're "just kinda + // weird" -- not clearly earning their keep as their own thing. + static INT KingSafetyDeltas[9] = { -17, -16, -15, -1, +1, - +15, +16, +17, -2, +2, 0 + +15, +16, +17, 0 }; - static ULONG EmptyAttackedSquare[2][11] = + static ULONG EmptyAttackedSquare[2][8] = { { 1, 1, 1, 1, 1, - 2, 2, 2, 1, 1, 0 + 2, 2, 2 }, { 2, 2, 2, 1, 1, - 1, 1, 1, 1, 1, 0 + 1, 1, 1 }, }; - static ULONG EmptyUnattackedSquare[2][11] = + static ULONG EmptyUnattackedSquare[2][8] = { { 0, 0, 0, 0, 0, - 1, 1, 1, 0, 0, 0 + 1, 1, 1 }, { 1, 1, 1, 0, 0, - 0, 0, 0, 0, 0, 0 + 0, 0, 0 }, }; @@ -3728,10 +4059,11 @@ Return value: p = pos->rgSquare[c].pPiece; ASSERT(p && IS_KING(p)); uColor = GET_COLOR(p); - ufColor = FLIP(uColor); + xColor = FLIP(uColor); ASSERT(IS_VALID_COLOR(uColor)); + pos->bbKingAttacks[uColor] |= g_KingAttacksBB[c]; - u = pos->uNonPawnMaterial[ufColor]; + u = pos->uNonPawnMaterial[xColor]; ASSERT(u >= VALUE_KING); if (u < DO_KING_SAFETY_THRESHOLD) { @@ -3740,7 +4072,6 @@ Return value: // generation) is exactly the old g_iQKDeltas walk's // destination set, IS_ON_BOARD baked in at table-build time -- // one OR instead of an 8-iteration loop. - pos->bbKingAttacks[uColor] |= g_KingAttacksBB[c]; goto skip_safety; } @@ -3751,20 +4082,7 @@ Return value: Trace("%s Initial KS Counter: %u\n", COLOR_NAME(uColor), uCounter); #endif - uCounter += pos->uPiecesPointingAtKing[uColor] / 2; -#ifdef EVAL_DUMP - Trace("%s KS Counter after pieces pointing: %u\n", - COLOR_NAME(uColor), uCounter); -#endif - // board_representation/EVAL.md section 9: written once, before the - // loop below, instead of per-square inside it -- also the bugfix - // agreed on for this conversion. The old per-square write used - // KingSafetyDeltas (11 entries) rather than the real 8-square king - // move pattern, so it spuriously marked two squares 2 files away - // on the same rank (KingSafetyDeltas' -2/+2 entries, present only - // for this loop's own file-distance bookkeeping) as - // "king-attacked" too. g_KingAttacksBB[c] is the real pattern. - pos->bbKingAttacks[uColor] |= g_KingAttacksBB[c]; + BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, xColor); uFlightSquares = 0; u = 0; @@ -3774,54 +4092,53 @@ Return value: cSquare = c + KingSafetyDeltas[u]; if (IS_ON_BOARD(cSquare)) { - p = pos->rgSquare[cSquare].pPiece; - // // board_representation/EVAL.md section 9: bvAttacks/ - // ATTACK_BITV retired entirely -- every bit here now comes - // straight from a bbXAttacks accumulator read, no more - // c|8 shadow-index struct storage or per-square writes. - // No enemy-king contribution in bvAttack, by direct - // instruction (see this function's header comment on the - // black-then-white evaluation-order asymmetry this - // sidesteps). bvDefend's own-king bit is real, load- - // bearing signal (see the bvDefend &= ~8 below, which - // strips it back out when the square is x-rayed or - // multiply attacked -- "a lone king isn't adequate defense - // against that") -- not just self-consistency noise, so - // it keeps its own-color check. + // ATTACK_BITV retired entirely -- every bit here now + // comes straight from a bbXAttacks accumulator read, + // no more c|8 shadow-index struct storage or + // per-square writes. No enemy-king contribution in + // bvAttack, by direct instruction (see this function's + // header comment on the black-then-white evaluation- + // order asymmetry this sidesteps). bvDefend's own-king + // bit is real, load-bearing signal (see the bvDefend + // &= ~8 below, which strips it back out when the + // square is x-rayed or multiply attacked -- "a lone + // king isn't adequate defense against that") -- not + // just self-consistency noise, so it keeps its + // own-color check. // - { - BITBOARD sq = COOR_TO_BB(cSquare); - - bvAttack = ((pos->bbPawnAttacks[ufColor] & sq) ? PAWN_BIT : 0) | - ((pos->bbMinorAttacks[ufColor] & sq) ? MINOR_BIT : 0) | - ((pos->bbRookAttacks[ufColor] & sq) ? ROOK_BIT : 0) | - ((pos->bbQueenAttacks[ufColor] & sq) ? QUEEN_BIT : 0); - bvXray = ((pos->bbMinorXrayAttacks[ufColor] & sq) ? MINOR_BIT : 0) | - ((pos->bbRookXrayAttacks[ufColor] & sq) ? ROOK_BIT : 0) | - ((pos->bbQueenXrayAttacks[ufColor] & sq) ? QUEEN_BIT : 0); - bvDefend = ((pos->bbPawnAttacks[uColor] & sq) ? PAWN_BIT : 0) | - ((pos->bbMinorAttacks[uColor] & sq) ? MINOR_BIT : 0) | - ((pos->bbRookAttacks[uColor] & sq) ? ROOK_BIT : 0) | - ((pos->bbQueenAttacks[uColor] & sq) ? QUEEN_BIT : 0) | - ((pos->bbKingAttacks[uColor] & sq) ? KING_BIT : 0); - } + BITBOARD sq = COOR_TO_BB(cSquare); + + bvAttack = ((pos->bbPawnAttacks[xColor] & sq) ? PAWN_BIT : 0) | + ((pos->bbMinorAttacks[xColor] & sq) ? MINOR_BIT : 0) | + ((pos->bbRookAttacks[xColor] & sq) ? ROOK_BIT : 0) | + ((pos->bbQueenAttacks[xColor] & sq) ? QUEEN_BIT : 0); + bvXray = ((pos->bbMinorXrayAttacks[xColor] & sq) ? MINOR_BIT : 0) | + ((pos->bbRookXrayAttacks[xColor] & sq) ? ROOK_BIT : 0) | + ((pos->bbQueenXrayAttacks[xColor] & sq) ? QUEEN_BIT : 0); + bvDefend = ((pos->bbPawnAttacks[uColor] & sq) ? PAWN_BIT : 0) | + ((pos->bbMinorAttacks[uColor] & sq) ? MINOR_BIT : 0) | + ((pos->bbRookAttacks[uColor] & sq) ? ROOK_BIT : 0) | + ((pos->bbQueenAttacks[uColor] & sq) ? QUEEN_BIT : 0) | + ((pos->bbKingAttacks[uColor] & sq) ? KING_BIT : 0); // - // Count squares near the king the enemy queen specifically - // attacks or x-rays (2026-08-30, replaces the old - // per-queen "pointing near enemy K" bolt-on this loop's - // own bvAttack/bvXray data already made redundant). Reuses - // data already read above -- no extra attack-table work -- - // and, unlike the old term, catches x-ray/latent queen - // threats too, not just direct ray-cast hits. + // Count squares near the king the enemy queen + // specifically attacks or x-rays (2026-08-30, replaces + // the old per-queen "pointing near enemy K" bolt-on + // this loop's own bvAttack/bvXray data already made + // redundant). Reuses data already read above -- no + // extra attack-table work -- and, unlike the old term, + // catches x-ray/latent queen threats too, not just + // direct ray-cast hits. // - // Queen no longer writes small.uQueen/xray.uQueen at all - // (2026-09-05 conversion) -- bvAttack/bvXray above already - // fold its contribution back in via QUEEN_BIT (same byte - // position .small.uQueen used), so just read those instead - // of the now-permanently-zero raw struct fields. + // Queen no longer writes small.uQueen/xray.uQueen at + // all (2026-09-05 conversion) -- bvAttack/bvXray above + // already fold its contribution back in via QUEEN_BIT + // (same byte position .small.uQueen used), so just + // read those instead of the now-permanently-zero raw + // struct fields. // uQueenNearKing += (((bvAttack | bvXray) & QUEEN_BIT) != 0); @@ -3830,27 +4147,30 @@ Return value: bvPattern |= (bvAttack | bvXray); if (bvXray || (bvAttack & (bvAttack - 1))) { - bvDefend &= ~8; + bvDefend &= ~KING_BIT; } - if (IS_EMPTY(p)) + if (!(pos->bbOccupied & sq)) { - ASSERT(u < 11); + ASSERT(u < 8); uCounter += EmptyAttackedSquare[uColor][u]; } else { + // OccupiedAttackedSquare is indexed purely by + // color (is the occupant ufColor's own piece, + // i.e. an enemy from uColor's perspective) -- + // no need for the mailbox piece type at all. uCounter += - OccupiedAttackedSquare[OPPOSITE_COLORS(p, uColor)]; + OccupiedAttackedSquare[(bbEnemyOcc & sq) != 0]; } uCounter += (bvDefend == 0); } else { uCounter += (bvXray != 0); - ASSERT(u < 11); + ASSERT(u < 8); uCounter += EmptyUnattackedSquare[uColor][u]; - ASSERT((IS_EMPTY(p) == 0) || (IS_EMPTY(p) == 1)); - uFlightSquares += ((IS_EMPTY(p)) && (u < 8)); + uFlightSquares += ((pos->bbOccupied & sq) == 0); } } u++; @@ -3861,45 +4181,27 @@ Return value: #endif // - // King's own file plus the two adjacent ones (collapsed 2026-08-30 - // from three copy-pasted blocks that differed only in c-1/c/c+1 -- - // same behavior and iteration order, just not repeated three times - // in the source; confirmed behaviorally neutral by isolated sd10 - // suite testing before landing). + // King's own file plus the two adjacent ones. EXPERIMENTAL, + // prototype-only (2026-09-05, board_representation/EVAL.md + // section 9's king-safety-hash discussion): now routed through + // _GetFileStormDefects's cache instead of computed inline -- + // _ComputeFileStormDefects holds the exact same logic this used + // to be (collapsed 2026-08-30 from three copy-pasted blocks that + // differed only in c-1/c/c+1; confirmed behaviorally neutral by + // isolated sd10 suite testing before that landing). // - for (w = -1; w <= 1; w++) - { - cFileSq = c + w; - if (!IS_ON_BOARD(cFileSq)) - { - continue; - } - u = FILE(cFileSq) + 1; - v = (pHash->uCountPerFile[WHITE][u] > 0) + - (pHash->uCountPerFile[BLACK][u] > 0); - ASSERT((v >= 0) && (v <= 2)); - uCounter += (KingFileDefects[v] + ((v < 2) && ((u == 1) || (u == 8)))); - bb = pos->bbPawns[ufColor] & BBFILE[u - 1]; - if (bb) - { - if (uColor == WHITE) - { - cSquare = CoorFromBitBoardRank1ToRank8(&bb); - } - else - { - cSquare = CoorFromBitBoardRank8ToRank1(&bb); - } - ASSERT(IS_ON_BOARD(cSquare)); - uCounter += KingStormingPawnDefects[DISTANCE(cSquare, c)]; - } - } + uCounter += _GetFileStormDefects(ctx, pos, pHash, uColor); #ifdef EVAL_DUMP Trace("%s KS Counter post-open file/stormers: %u\n", COLOR_NAME(uColor), uCounter); #endif - bvPattern >>= 3; + // No shift needed (2026-09-06): PAWN_BIT/MINOR_BIT/ROOK_BIT/ + // QUEEN_BIT now sit directly in bits 1-4, and bvPattern never gets + // KING_BIT (bit 0) set (bvAttack/bvXray deliberately exclude it -- + // see this function's header comment), so bvPattern already lands + // in [0, 30] here, matching KING_COUNTER_BY_ATTACK_PATTERN's range + // directly. ASSERT(bvPattern >= 0); ASSERT(bvPattern < 32); v = KING_COUNTER_BY_ATTACK_PATTERN[bvPattern]; @@ -3921,7 +4223,14 @@ Return value: // uCounter = MINU(uCounter, 41); i = KING_SAFETY_BY_COUNTER[uCounter]; - i *= REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[ufColor]]; +#ifdef CALIBRATE_POSITIONAL + // KING_SAFETY_BY_COUNTER is <= 0 throughout (a penalty); the + // calibration histogram bins magnitude, matching iKingSwingP90's + // own all-positive-magnitude shape. + ASSERT(i <= 0); + _RecordPositionalCalibration(ctx, pos, pHash, uColor, (ULONG)(-i)); +#endif + i *= REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[xColor]]; i /= 8; EVAL_TERM(uColor, KING, @@ -3944,50 +4253,42 @@ Return value: // if (FALSE == pos->fCastled[uColor]) { - if (pos->uNonPawnCount[uColor][0] > 4) + ULONG uDevelopmentScaler = CASTLE_AND_DEVELOPMENT_SCALER[pos->uArmyScaler[uColor]]; + if (uDevelopmentScaler > 0) { ASSERT(pos->uMinorsAtHome[uColor] <= 4); EVAL_TERM(uColor, - 0, - ILLEGAL_COOR, - pos->iScore[uColor], - UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]], - "development"); - if (uColor == BLACK) - { - EVAL_TERM(BLACK, - KING, - c, - iKingScore, - (KING_MISSING_ONE_CASTLE_OPTION * - ((CASTLE_BLACK_SHORT & pos->bvCastleInfo) == 0)), - "can't castle short"); - EVAL_TERM(BLACK, - KING, - c, - iKingScore, - (KING_MISSING_ONE_CASTLE_OPTION * - ((CASTLE_BLACK_LONG & pos->bvCastleInfo) == 0)), - "can't castle long"); - } - else - { - ASSERT(uColor == WHITE); - EVAL_TERM(WHITE, - KING, - c, - iKingScore, - (KING_MISSING_ONE_CASTLE_OPTION * - ((CASTLE_WHITE_SHORT & pos->bvCastleInfo) == 0)), - "can't castle short"); - EVAL_TERM(WHITE, - KING, - c, - iKingScore, - (KING_MISSING_ONE_CASTLE_OPTION * - ((CASTLE_WHITE_LONG & pos->bvCastleInfo) == 0)), - "can't castle long"); - } + 0, + ILLEGAL_COOR, + pos->iScore[uColor], + (SCORE)((UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] * + (SCORE)uDevelopmentScaler) / 8), + "development"); + + // Color-indexed instead of a WHITE/BLACK branch over + // two copy-pasted blocks -- one fewer per-piece branch + // for the processor to mispredict (board_representation/ + // EVAL.md section 9). + static const BITV CASTLE_SHORT[2] = + { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT }; + static const BITV CASTLE_LONG[2] = + { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG }; + EVAL_TERM(uColor, + KING, + c, + iKingScore, + (SCORE)((KING_MISSING_ONE_CASTLE_OPTION * + ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0) * + (SCORE)uDevelopmentScaler) / 8), + "can't castle short"); + EVAL_TERM(uColor, + KING, + c, + iKingScore, + (SCORE)((KING_MISSING_ONE_CASTLE_OPTION * + ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0) * + (SCORE)uDevelopmentScaler) / 8), + "can't castle long"); } } @@ -4102,11 +4403,9 @@ Return value: COOR cQueen; COOR cKing, c; ULONG uKingDist, uPawnDist, uFriendDist; - ULONG uRacerDist[2] = { 99, 99 }; - FLAG fDontCountMeOut[2] = { ((RANK(pos->cNonPawns[BLACK][0]) <= 3) && - (pos->uPawnCount[BLACK] != 0)), - ((RANK(pos->cNonPawns[WHITE][0]) >= 6) && - (pos->uPawnCount[WHITE] != 0)) }; + ULONG uRacerDist[2]; + FLAG fDontCountMeOut[2]; + bb[BLACK] = pHash->bbPasserLocations[BLACK]; bb[WHITE] = pHash->bbPasserLocations[WHITE]; if (!(bb[BLACK] | bb[WHITE])) @@ -4115,6 +4414,18 @@ Return value: } ASSERT((pos->uNonPawnCount[WHITE][0] == 1) || (pos->uNonPawnCount[BLACK][0] == 1)) + + // Both deferred past the no-passer early return above -- no need to + // pay for this on the common case (most positions have no passer at + // all, and EvalPasserRaces itself is only even called when a side + // is down to a bare king, so this function runs unconditionally in + // the pre-lazy-exit segment whenever that's true). + uRacerDist[BLACK] = 99; + uRacerDist[WHITE] = 99; + fDontCountMeOut[BLACK] = ((RANK(pos->cNonPawns[BLACK][0]) <= 3) && + (pos->uPawnCount[BLACK] != 0)); + fDontCountMeOut[WHITE] = ((RANK(pos->cNonPawns[WHITE][0]) >= 6) && + (pos->uPawnCount[WHITE] != 0)); FOREACH_COLOR(uColor) { if (pos->uNonPawnCount[FLIP(uColor)][0] == 1) @@ -4436,8 +4747,7 @@ Return value: ((pos->uPawnCount[uAhead] != 0) * TRADE_PIECES[uMagnitude][pos->uNonPawnCount[uBehind][0]]), "trade pieces"); - // Tuning can (and has) flipped this sign; not a real invariant. - //ASSERT(TRADE_PIECES[uMagnitude][pos->uNonPawnCount[uBehind][0]] > 0); + ASSERT(TRADE_PIECES[uMagnitude][pos->uNonPawnCount[uBehind][0]] > 0); EVAL_TERM(uAhead, 0, ILLEGAL_COOR, @@ -4597,8 +4907,9 @@ _EvalBishopPairs(IN POSITION *pos) Routine description: - Give a bonus to sides that have a viable bishop pair based; scale - the bonus by the number of pawns remaining on the board. + Give a flat bonus to sides that have a viable bishop pair (one + light-squared, one dark-squared) -- not scaled by pawn count, by + direct instruction (2026-09-06). Parameters: @@ -4610,25 +4921,23 @@ Return value: --*/ { - ULONG uPawnSum = pos->uPawnCount[WHITE] + pos->uPawnCount[BLACK]; - FLAG fPair; ULONG uBishopCount = pos->uNonPawnCount[BLACK][BISHOP]; - ULONG uWhiteSqBishopCount = pos->uWhiteSqBishopCount[BLACK]; - ASSERT(uPawnSum <= 16); ASSERT(uBishopCount <= 10); + ULONG uWhiteSqBishopCount = pos->uWhiteSqBishopCount[BLACK]; ASSERT(uWhiteSqBishopCount <= 10); - fPair = ((uBishopCount > 1) & - (uWhiteSqBishopCount != 0) & - (uWhiteSqBishopCount != uBishopCount)); + FLAG fPair = ((uBishopCount > 1) & + (uWhiteSqBishopCount != 0) & + (uWhiteSqBishopCount != uBishopCount)); EVAL_TERM(BLACK, 0, ILLEGAL_COOR, pos->iScore[BLACK], - BISHOP_PAIR[fPair][uPawnSum], + fPair * BISHOP_PAIR_BONUS, "bishop pair"); + uBishopCount = pos->uNonPawnCount[WHITE][BISHOP]; - uWhiteSqBishopCount = pos->uWhiteSqBishopCount[WHITE]; ASSERT(uBishopCount <= 10); + uWhiteSqBishopCount = pos->uWhiteSqBishopCount[WHITE]; ASSERT(uWhiteSqBishopCount <= 10); fPair = ((uBishopCount > 1) & (uWhiteSqBishopCount != 0) & @@ -4637,7 +4946,7 @@ Return value: 0, ILLEGAL_COOR, pos->iScore[WHITE], - BISHOP_PAIR[fPair][uPawnSum], + fPair * BISHOP_PAIR_BONUS, "bishop pair"); } @@ -4680,18 +4989,25 @@ Return value: FLAG fDeferred; #ifdef EVAL_TIME UINT64 uTimer = SystemReadTimeStampCounter(); +#endif +#ifdef CALIBRATE_POSITIONAL + g_uCalibrateEvalCalls++; +#endif +#ifdef CALIBRATE_MARGIN_SAFETY + // Set when a real exit would have been taken; the rest of Eval() + // still runs (for measurement), but the saved values get restored + // just before returning so real search behavior is bit-for-bit + // unaffected -- see the swing recorded near `end:` below. + FLAG fWouldHaveExited = FALSE; + SCORE iSavedLazyScore = 0; + SCORE iSavedLazyPositional = 0; #endif ASSERT(IS_VALID_SCORE(iAlpha)); ASSERT(IS_VALID_SCORE(iBeta)); ASSERT(iAlpha < iBeta); ASSERT((pos->iMaterialBalance[WHITE] * -1) == pos->iMaterialBalance[BLACK]); - // ASSERT(!InCheck(pos, pos->uToMove)); -#if 0 - // This is never used right now. - pos->uMinMobility[BLACK] = pos->uMinMobility[WHITE] = 100; -#endif pos->uNumTrapped[BLACK] = pos->uNumTrapped[WHITE] = 0; pos->iScore[BLACK] = @@ -4780,94 +5096,187 @@ Return value: #ifdef LAZY_EVAL // - // Compute an estimate of the score for the side to move based on the - // eval terms we have already considered: - // - // 1. Material balance - // 2. Pawn structure bonuses/penalties (incl passers/candidates) - // 3. Passer races are detected already - // 4. "Bad trade" code has already run - // 5. We've already detected unwinnable endgames - // 6. Bishop pairs - // - iScoreForSideToMove = (pos->iScore[pos->uToMove] - - pos->iScore[FLIP(pos->uToMove)]); - ASSERT(IS_VALID_SCORE(iScoreForSideToMove)); - - // - // Eval has not considered several potentially large terms: - // - // 1. Piece positional components (such as mobility, trapped) - // 2. King safety penalties - // 3. Other miscellaneous bonuses/penalties - // - // We build two "margins" to account for these components of the - // score. - // - iAlphaMargin = iBetaMargin = LAZY_EVAL_BASE_MARGIN; - - // - // If (score + alpha_margin) is already > alpha -OR- - // (score - beta_margin) is already < beta + // Below LAZY_EVAL_MIN_MATERIAL combined army scaler, skip lazy eval + // entirely (see its chess.h comment) -- measured directly via + // CALIBRATE_MARGIN_SAFETY, the regular lazy exit's real swing + // exceeded its own assumed margin 20.6% of the time in near-bare- + // king endgames, far above the ~0.01-0.36% rate everywhere else. // - // ...then we can stop thinking about lazy eval; the rest of the - // computation only increases the margin so we know LE will fail - // and can save some work here. - // - if ((iScoreForSideToMove + iAlphaMargin < iAlpha) || - (iScoreForSideToMove - iBetaMargin > iBeta)) + if (pos->uArmyScaler[WHITE] + pos->uArmyScaler[BLACK] >= + LAZY_EVAL_MIN_MATERIAL) { +#ifdef EVAL_TIME + UINT64 uLazyDecisionTimer = SystemReadTimeStampCounter(); +#endif + // + // Compute an estimate of the score for the side to move based on the + // eval terms we have already considered: + // + // 1. Material balance + // 2. Pawn structure bonuses/penalties (incl passers/candidates) + // 3. Passer races are detected already + // 4. "Bad trade" code has already run + // 5. We've already detected unwinnable endgames + // 6. Bishop pairs + // + iScoreForSideToMove = (pos->iScore[pos->uToMove] - + pos->iScore[FLIP(pos->uToMove)]); + ASSERT(IS_VALID_SCORE(iScoreForSideToMove)); + + // + // Eval has not considered several potentially large terms: // - // Ok, we can't say for sure that we won't take a lazy exit - // yet. So do the expensive part of lazy eval estimation and - // widen the margin further for king safety issues and passed - // pawns. + // 1. Piece positional components (such as mobility, trapped) + // 2. King safety penalties + // 3. Other miscellaneous bonuses/penalties // - EstimatePositionalScore(pos, pHash, &iAlphaMargin, &iBetaMargin); + // We build two "margins" to account for these components of the + // score. + // + iAlphaMargin = iBetaMargin = LAZY_EVAL_BASE_MARGIN; - if (iScoreForSideToMove + iAlphaMargin <= iAlpha) + // + // If (score + alpha_margin) is already > alpha -OR- + // (score - beta_margin) is already < beta + // + // ...then we can stop thinking about lazy eval; the rest of the + // computation only increases the margin so we know LE will fail + // and can save some work here. + // +#ifdef CALIBRATE_POSITIONAL + ctx->fCalibrateCandidate = FALSE; + g_uCalibrateGateChecked++; +#endif + if ((iScoreForSideToMove + iAlphaMargin < iAlpha) || + (iScoreForSideToMove - iBetaMargin > iBeta)) { - INC(ctx->sCounters.tree.u64LazyEvals); - if (NULL != piPositional) +#ifdef CALIBRATE_POSITIONAL + g_uCalibrateGateTrue++; +#endif + // + // Ok, we can't say for sure that we won't take a lazy exit + // yet. So do the expensive part of lazy eval estimation and + // widen the margin further for king safety issues and passed + // pawns. + // +#ifdef CALIBRATE_POSITIONAL + // This node is one where a real (uninstrumented) build would + // actually call EstimatePositionalScore and consider a lazy + // exit -- the population _RecordPositionalCalibration should + // be sampling from, per _EvalKing's calls below. + ctx->fCalibrateCandidate = TRUE; +#endif + EstimatePositionalScore(ctx, pos, pHash, &iAlphaMargin, &iBetaMargin); + +#ifdef CALIBRATE_BASE_MARGIN { - *piPositional = iAlphaMargin; + ULONG uBucket = _MaxKingDangerBucket(ctx, pos, pHash); + ULONG uMatBucket = _MaterialBucket(pos); + g_uBaseMarginExitAttempted[uMatBucket][uBucket]++; + if ((iScoreForSideToMove + iAlphaMargin <= iAlpha) || + (iScoreForSideToMove - iBetaMargin >= iBeta)) + { + g_uBaseMarginExitTaken[uMatBucket][uBucket]++; + } } +#endif + // + // CALIBRATE_POSITIONAL builds never take the lazy exit here -- + // the whole point of a calibration run is to compute the cheap + // defect-count estimate *and* let Eval() continue on to + // _EvalKing's real computation on every node, so the two can be + // logged and compared. See _RecordPositionalCalibration below + // (called from _EvalKing) and DumpPositionalCalibration + // (command.c's "calibrate dump"). + // +#ifndef CALIBRATE_POSITIONAL + if (iScoreForSideToMove + iAlphaMargin <= iAlpha) + { + INC(ctx->sCounters.tree.u64LazyEvals); + if (NULL != piPositional) + { + *piPositional = iAlphaMargin; + } #ifdef EVAL_TIME - ctx->sCounters.tree.u64CyclesEvalPreLazy += - (SystemReadTimeStampCounter() - uTimer); + ctx->sCounters.tree.u64CyclesEvalPreLazy += + (SystemReadTimeStampCounter() - uTimer); #endif - goto end; - } - else if (iScoreForSideToMove - iBetaMargin >= iBeta) - { - INC(ctx->sCounters.tree.u64LazyEvals); - if (NULL != piPositional) +#ifdef CALIBRATE_MARGIN_SAFETY + // Don't exit yet -- let the real full eval run below so we + // can measure the true swing, then restore these values + // right before `end:` so the caller sees exactly what a + // normal build would have returned. + fWouldHaveExited = TRUE; + iSavedLazyScore = iScoreForSideToMove; + iSavedLazyPositional = iAlphaMargin; +#else +#ifdef EVAL_TIME + ctx->sCounters.tree.u64CyclesEvalLazyDecision += + (SystemReadTimeStampCounter() - uLazyDecisionTimer); +#endif + goto end; +#endif + } + else if (iScoreForSideToMove - iBetaMargin >= iBeta) { - *piPositional = iBetaMargin; + INC(ctx->sCounters.tree.u64LazyEvals); + if (NULL != piPositional) + { + *piPositional = iBetaMargin; + } +#ifdef EVAL_TIME + ctx->sCounters.tree.u64CyclesEvalPreLazy += + (SystemReadTimeStampCounter() - uTimer); +#endif +#ifdef CALIBRATE_MARGIN_SAFETY + fWouldHaveExited = TRUE; + iSavedLazyScore = iScoreForSideToMove; + iSavedLazyPositional = iBetaMargin; +#else +#ifdef EVAL_TIME + ctx->sCounters.tree.u64CyclesEvalLazyDecision += + (SystemReadTimeStampCounter() - uLazyDecisionTimer); +#endif + goto end; +#endif } +#endif // !CALIBRATE_POSITIONAL #ifdef EVAL_TIME - ctx->sCounters.tree.u64CyclesEvalPreLazy += - (SystemReadTimeStampCounter() - uTimer); + ctx->sCounters.tree.u64CyclesEvalLazyDecision += + (SystemReadTimeStampCounter() - uLazyDecisionTimer); #endif - goto end; } - } - else - { - // - // EstimatePositionalScore (above) is the only place that - // refreshes pos->uPiecesPointingAtKing[] -- _EvalKing reads it - // unconditionally further down. When this branch is skipped - // (position wasn't close enough to the window to risk a lazy - // exit), nothing else sets it for the current position, so - // _EvalKing would silently score king safety off of whatever - // stale value was left over from a prior, unrelated node. - // Refresh it here instead -- once, not twice, since this is - // the innermost eval loop. - // - (void)CountKingSafetyDefects(pos, WHITE); - (void)CountKingSafetyDefects(pos, BLACK); - } +#ifdef CALIBRATE_BASE_MARGIN + else + { + // + // The flat gate said "don't even try" -- measure whether the + // real, widened margin would have allowed an exit anyway had + // we bothered to compute it. A "yes" here is a false negative + // attributable to LAZY_EVAL_BASE_MARGIN being tighter than it + // needs to be. This extra EstimatePositionalScore call is + // measurement-only overhead, real exit behavior is unaffected + // (this is the branch where a real build already skips lazy + // eval entirely). + // + SCORE iMeasuredAlphaMargin = LAZY_EVAL_BASE_MARGIN; + SCORE iMeasuredBetaMargin = LAZY_EVAL_BASE_MARGIN; + ULONG uMatBucket = _MaterialBucket(pos); + + EstimatePositionalScore(ctx, pos, pHash, &iMeasuredAlphaMargin, + &iMeasuredBetaMargin); + if ((iScoreForSideToMove + iMeasuredAlphaMargin <= iAlpha) || + (iScoreForSideToMove - iMeasuredBetaMargin >= iBeta)) + { + g_uBaseMarginFalseNegative[uMatBucket]++; + } + else + { + g_uBaseMarginCorrectlyFull[uMatBucket]++; + } + } +#endif + } // LAZY_EVAL_MIN_MATERIAL #endif #ifdef EVAL_TIME @@ -5067,7 +5476,7 @@ Return value: ASSERT(IS_KING(pDebugKing)); } #endif - TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash)); + TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash, ctx)); ctx->sPlyInfo[ctx->uPly].iKingScore[BLACK] = pos->iTempScore; #ifdef EVAL_DUMP Trace("After *k:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); @@ -5083,7 +5492,7 @@ Return value: ASSERT(IS_KING(pDebugKing)); } #endif - TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash)); + TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash, ctx)); ctx->sPlyInfo[ctx->uPly].iKingScore[WHITE] = pos->iTempScore; #ifdef EVAL_DUMP Trace("After .k:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); @@ -5107,19 +5516,6 @@ Return value: #endif } -#if 0 - // - // Never used right now. - // - // Make one more pass over the piece list for the side on move now - // that the full attack table is computed to detect - // under/unprotected pieces en prise to more valuable enemy pieces - // which we missed when building the attack table incrementally. - // - ctx->sPlyInfo[ctx->uPly].uMinMobility[BLACK] = pos->uMinMobility[BLACK]; - ctx->sPlyInfo[ctx->uPly].uMinMobility[WHITE] = pos->uMinMobility[WHITE]; -#endif - // // _EvalLookForDanger/_EvalTrappedPieces only ever *add* a hint // when they find one; a full eval that finds nothing this time @@ -5249,6 +5645,31 @@ Return value: } g_Options.iLastEvalScore = iScoreForSideToMove; +#ifdef CALIBRATE_MARGIN_SAFETY + if (TRUE == fWouldHaveExited) + { + // + // The real swing: how far the true, full-eval score actually + // ended up from the lazy-exit score this node would have + // returned. RecordMarginSafetySwing buckets this by material; + // its max (not just p90) is the number that answers "how + // large would LAZY_EVAL_BASE_MARGIN have had to be before an + // exit here would have been unsound." + // + RecordMarginSafetySwing(pos, abs(iScoreForSideToMove - iSavedLazyScore), + iSavedLazyPositional); + // + // Restore exactly what a normal build would have returned -- + // this measurement must not change real search behavior. + // + iScoreForSideToMove = iSavedLazyScore; + if (NULL != piPositional) + { + *piPositional = iSavedLazyPositional; + } + } +#endif + #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesInEval += (SystemReadTimeStampCounter() - uTimer); @@ -5262,10 +5683,5 @@ Return value: ASSERT(abs(ctx->sPlyInfo[ctx->uPly].iKingScore[BLACK]) < 700); ASSERT(abs(ctx->sPlyInfo[ctx->uPly].iKingScore[WHITE]) < 700); -#if 0 - // Never used. - ASSERT(ctx->sPlyInfo[ctx->uPly].uMinMobility[BLACK] <= 100); - ASSERT(ctx->sPlyInfo[ctx->uPly].uMinMobility[WHITE] <= 100); -#endif return(iScoreForSideToMove); } diff --git a/src/fen.c b/src/fen.c index 37ce8da..b42ee04 100755 --- a/src/fen.c +++ b/src/fen.c @@ -302,6 +302,7 @@ Return value: pos->cPawns[uColor][uPieceCounters[uPieceIndex]] = cSquare; pos->bbPawns[uColor] |= COOR_TO_BB(cSquare); pos->bbOccupied |= COOR_TO_BB(cSquare); + pos->bbOccupiedSide[uColor] |= COOR_TO_BB(cSquare); pos->rgSquare[cSquare].uIndex = uPieceCounters[uPieceIndex]; pos->rgSquare[cSquare].pPiece = p; pos->uPawnMaterial[uColor] += VALUE_PAWN; @@ -334,6 +335,7 @@ Return value: pos->bbPieces[uColor][PIECE_TYPE(p)] |= COOR_TO_BB(cSquare); } pos->bbOccupied |= COOR_TO_BB(cSquare); + pos->bbOccupiedSide[uColor] |= COOR_TO_BB(cSquare); pos->rgSquare[cSquare].pPiece = p; pos->rgSquare[cSquare].uIndex = uIndex; pos->uNonPawnMaterial[uColor] += PIECE_VALUE(p); diff --git a/src/generate.c b/src/generate.c index faecc2a..6f92382 100755 --- a/src/generate.c +++ b/src/generate.c @@ -867,10 +867,10 @@ Return value: **/ { - return (pos->bbPieces[uSide][KNIGHT] | pos->bbPieces[uSide][BISHOP] | - pos->bbPieces[uSide][ROOK] | pos->bbPieces[uSide][QUEEN] | - pos->bbPawns[uSide] | - COOR_TO_BB(pos->cNonPawns[uSide][0])); + // 2026-09-06: was a 6-term OR of bbPieces+bbPawns+king every call -- + // pos->bbOccupiedSide[uSide] is now incrementally maintained (move.c, + // same treatment as pos->bbOccupied) so this is just a field read. + return pos->bbOccupiedSide[uSide]; } // Non-static (unlike a purely-internal helper would be) so diff --git a/src/move.c b/src/move.c index 27d86df..1da5546 100755 --- a/src/move.c +++ b/src/move.c @@ -66,6 +66,8 @@ Return value: pos->bbPieces[c][PIECE_TYPE(p)] |= COOR_TO_BB(cTo); pos->bbOccupied &= ~COOR_TO_BB(cFrom); pos->bbOccupied |= COOR_TO_BB(cTo); + pos->bbOccupiedSide[c] &= ~COOR_TO_BB(cFrom); + pos->bbOccupiedSide[c] |= COOR_TO_BB(cTo); pos->u64NonPawnSig ^= g_u64SigSeeds[cFrom][PIECE_TYPE(p)][c]; pos->u64NonPawnSig ^= g_u64SigSeeds[cTo][PIECE_TYPE(p)][c]; #ifdef DEBUG @@ -127,6 +129,8 @@ Return value: pos->bbPawns[c] |= COOR_TO_BB(cTo); pos->bbOccupied &= ~COOR_TO_BB(cFrom); pos->bbOccupied |= COOR_TO_BB(cTo); + pos->bbOccupiedSide[c] &= ~COOR_TO_BB(cFrom); + pos->bbOccupiedSide[c] |= COOR_TO_BB(cTo); pos->u64PawnSig ^= g_u64PawnSigSeeds[cFrom][c]; pos->u64PawnSig ^= g_u64PawnSigSeeds[cTo][c]; pos->rgSquare[cTo].pPiece = p; @@ -183,6 +187,8 @@ Return value: pos->bbPieces[c][PIECE_TYPE(p)] |= COOR_TO_BB(cTo); pos->bbOccupied &= ~COOR_TO_BB(cFrom); pos->bbOccupied |= COOR_TO_BB(cTo); + pos->bbOccupiedSide[c] &= ~COOR_TO_BB(cFrom); + pos->bbOccupiedSide[c] |= COOR_TO_BB(cTo); pos->rgSquare[cTo].pPiece = p; pos->rgSquare[cTo].uIndex = uIndex; #ifdef DEBUG @@ -236,6 +242,8 @@ Return value: pos->bbPawns[c] |= COOR_TO_BB(cTo); pos->bbOccupied &= ~COOR_TO_BB(cFrom); pos->bbOccupied |= COOR_TO_BB(cTo); + pos->bbOccupiedSide[c] &= ~COOR_TO_BB(cFrom); + pos->bbOccupiedSide[c] |= COOR_TO_BB(cTo); pos->rgSquare[cTo].pPiece = p; pos->rgSquare[cTo].uIndex = uIndex; #ifdef DEBUG @@ -284,6 +292,7 @@ Return value: ASSERT(IS_VALID_PIECE(pLifted)); ASSERT(!IS_KING(pLifted)); pos->bbOccupied &= ~COOR_TO_BB(cSquare); + pos->bbOccupiedSide[GET_COLOR(pLifted)] &= ~COOR_TO_BB(cSquare); uIndex = pos->rgSquare[cSquare].uIndex; #ifdef DEBUG ASSERT(IS_VALID_PIECE_INDEX(uIndex)); @@ -420,6 +429,7 @@ Return value: ASSERT(IS_VALID_PIECE(pLifted)); ASSERT(!IS_KING(pLifted)); pos->bbOccupied &= ~COOR_TO_BB(cSquare); + pos->bbOccupiedSide[GET_COLOR(pLifted)] &= ~COOR_TO_BB(cSquare); uIndex = pos->rgSquare[cSquare].uIndex; #ifdef DEBUG @@ -544,6 +554,7 @@ Return value: pos->iMaterialBalance[FLIP(color)] -= pv; ASSERT(pos->iMaterialBalance[WHITE] * -1 == pos->iMaterialBalance[BLACK]); pos->bbOccupied |= COOR_TO_BB(cSquare); + pos->bbOccupiedSide[color] |= COOR_TO_BB(cSquare); if (IS_PAWN(pPiece)) { @@ -638,6 +649,7 @@ Return value: pos->iMaterialBalance[FLIP(color)] -= pv; ASSERT(pos->iMaterialBalance[WHITE] * -1 == pos->iMaterialBalance[BLACK]); pos->bbOccupied |= COOR_TO_BB(cSquare); + pos->bbOccupiedSide[color] |= COOR_TO_BB(cSquare); if (IS_PAWN(pPiece)) { diff --git a/src/root.c b/src/root.c index 0ab42f7..d7113c0 100755 --- a/src/root.c +++ b/src/root.c @@ -460,6 +460,19 @@ Return value: ASSERT(d); n = (double)(ctx->sCounters.pawnhash.u64Hits); Trace("Pawn hash hitrate: %5.3f percent.\n", (n/d) * 100.0); + // EXPERIMENTAL, measurement-only -- see eval.c's Eval() / + // rgKingSafetyHashProbe. + d = (double)(ctx->sCounters.kingsafetyhash.u64Probes) + 1; + ASSERT(d); + n = (double)(ctx->sCounters.kingsafetyhash.u64Hits); + Trace("[EXPERIMENTAL] King safety hash: %5.3f%% hitrate, " + "avg %.1f cyc hit / %.1f cyc miss (%" COMPILER_LONGLONG_UNSIGNED_FORMAT + " probes)\n", + (n/d) * 100.0, + (n > 0 ? (double)ctx->sCounters.kingsafetyhash.u64CyclesHit / n : 0.0), + ((d - 1 - n) > 0 ? + (double)ctx->sCounters.kingsafetyhash.u64CyclesMiss / (d - 1 - n) : 0.0), + ctx->sCounters.kingsafetyhash.u64Probes); n = (double)(ctx->sCounters.tree.u64NullMoveSuccess); d = (double)(ctx->sCounters.tree.u64NullMoves) + 1; ASSERT(d); @@ -614,6 +627,22 @@ Return value: Trace(" attack-table pop: %5.1f%% unaccounted: %5.1f%%\n", (u64Total ? (100.0 * (double)u64AttackPop / (double)u64Total) : 0.0), (u64Total ? (100.0 * (double)u64Unaccounted / (double)u64Total) : 0.0)); + { + UINT64 u64LazyDecision = ctx->sCounters.tree.u64CyclesEvalLazyDecision; + UINT64 u64CKSD = ctx->sCounters.tree.u64CyclesEvalCountKingSafetyDefects; + UINT64 u64Storm = ctx->sCounters.tree.u64CyclesEvalFileStormDefects; + UINT64 u64PreLazyRest = (u64PreLazyOther >= u64LazyDecision) ? + (u64PreLazyOther - u64LazyDecision) : 0; + Trace(" -- of which, pre-lazy breakdown --\n"); + Trace(" material/passers/badtrades/bishoppairs: %5.1f%%\n", + (u64Total ? (100.0 * (double)u64PreLazyRest / (double)u64Total) : 0.0)); + Trace(" lazy gate + EstimatePositionalScore: %5.1f%% " + "(of which CountKingSafetyDefects: %5.1f%%, " + "_GetFileStormDefects: %5.1f%%)\n", + (u64Total ? (100.0 * (double)u64LazyDecision / (double)u64Total) : 0.0), + (u64Total ? (100.0 * (double)u64CKSD / (double)u64Total) : 0.0), + (u64Total ? (100.0 * (double)u64Storm / (double)u64Total) : 0.0)); + } { UINT64 u64PHits = ctx->sCounters.tree.u64CountEvalPawnsHit; UINT64 u64PMisses = ctx->sCounters.tree.u64CountEvalPawnsMiss; diff --git a/src/search.c b/src/search.c index 1b83891..25318ed 100755 --- a/src/search.c +++ b/src/search.c @@ -1249,7 +1249,22 @@ QSearchFromCheckNoStandPat(IN SEARCHER_THREAD_CONTEXT *ctx, if ((pf->uQsearchDepth < pf->uQsearchCheckDepth) && (pf->uQsearchDepth < g_uIterateDepth / 4) && (pf->fCouldStandPat[pos->uToMove] == FALSE) && - (CountKingSafetyDefects(pos, pos->uToMove) > 2)) + // + // Threshold doubled 2026-09-06 (board_representation/ + // EVAL.md section 9): was ">2", tuned against the old + // CHECK_VECTOR-based CountKingSafetyDefects. The bitboard + // rewrite (real blocker-aware slider attacks, queen 2x + // weighting) runs systematically hotter for the same + // underlying danger, so the un-rescaled old threshold was + // firing far more liberally than intended -- observed + // directly as a runaway check-extension cascade (85x+ + // branching for a single ply) on a real position. Not yet + // independently recalibrated against real data the way + // iKingSwingP90 was; doubling is a stopgap matching the + // rough inflation this counter picked up, pending a real + // measurement. + // + (CountKingSafetyDefects(pos, pos->uToMove) > 4)) { if ((uMoveCount == 1) || (NUM_KING_MOVES(ctx, ctx->uPly) == 0) || diff --git a/src/searchsup.c b/src/searchsup.c index 4e6a0ed..89ad729 100644 --- a/src/searchsup.c +++ b/src/searchsup.c @@ -479,9 +479,13 @@ Return value: ASSERT(IS_ESCAPING_CHECK(mv)); iMoveScore = iRoughEval + ComputeMoveScore(ctx, mv, uMoveNum); - // One legal move in reply to check... + // One legal move in reply to check... threshold doubled + // 2026-09-06, same stopgap reasoning as search.c's qsearch + // check-extension gate (board_representation/EVAL.md section + // 9) -- was ">1", stale against CountKingSafetyDefects' old + // CHECK_VECTOR-based scale. if (ONE_LEGAL_MOVE(ctx, ctx->uPly - 1) && - CountKingSafetyDefects(&ctx->sPosition, uColor) > 1) + CountKingSafetyDefects(&ctx->sPosition, uColor) > 2) { *piExtend += (QUARTER_PLY + HALF_PLY * diff --git a/src/testsup.c b/src/testsup.c index 852b6d9..0cbc9a4 100644 --- a/src/testsup.c +++ b/src/testsup.c @@ -118,6 +118,7 @@ GenerateRandomLegalPosition(POSITION *pos) pos->uNonPawnCount[WHITE][0] = 1; pos->uNonPawnMaterial[WHITE] = VALUE_KING; pos->bbOccupied |= COOR_TO_BB(c); + pos->bbOccupiedSide[WHITE] |= COOR_TO_BB(c); do { @@ -132,6 +133,7 @@ GenerateRandomLegalPosition(POSITION *pos) pos->uNonPawnCount[BLACK][0] = 1; pos->uNonPawnMaterial[BLACK] = VALUE_KING; pos->bbOccupied |= COOR_TO_BB(c1); + pos->bbOccupiedSide[BLACK] |= COOR_TO_BB(c1); // // Place the rest of the armies @@ -183,6 +185,7 @@ GenerateRandomLegalPosition(POSITION *pos) pos->uPawnMaterial[uColor] += VALUE_PAWN; pos->bbPawns[uColor] |= COOR_TO_BB(c); pos->bbOccupied |= COOR_TO_BB(c); + pos->bbOccupiedSide[uColor] |= COOR_TO_BB(c); break; } else if (!IS_KING(p) && @@ -197,6 +200,7 @@ GenerateRandomLegalPosition(POSITION *pos) pos->uNonPawnMaterial[uColor] += PIECE_VALUE(p); pos->bbPieces[uColor][PIECE_TYPE(p)] |= COOR_TO_BB(c); pos->bbOccupied |= COOR_TO_BB(c); + pos->bbOccupiedSide[uColor] |= COOR_TO_BB(c); if (IS_BISHOP(p)) { if (IS_WHITE_SQUARE_COOR(c)) -- cgit v1.3