summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/GNUmakefile9
-rw-r--r--src/TODO2
-rwxr-xr-xsrc/bitboard.c10
-rwxr-xr-xsrc/board.c11
-rwxr-xr-xsrc/chess.h118
-rwxr-xr-xsrc/command.c74
-rwxr-xr-xsrc/eval.c1900
-rwxr-xr-xsrc/fen.c2
-rwxr-xr-xsrc/generate.c8
-rwxr-xr-xsrc/move.c12
-rwxr-xr-xsrc/root.c29
-rwxr-xr-xsrc/search.c17
-rw-r--r--src/searchsup.c8
-rw-r--r--src/testsup.c4
14 files changed, 1444 insertions, 760 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
@@ -85,79 +85,6 @@ _FastCoorFromBitBoardRank1ToRank8(IN OUT BITBOARD *pbb)
//
-// 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"
// or "behind the piece" for each color.
@@ -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
-};
+// 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;
-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
-};
+// 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,48 +2273,636 @@ Return value:
ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) == uSide);
//
- // Make sure cKing - 1, cKing and cKing + 1 are on the board
+ // 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.
//
- 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));
+ bbKingZone = g_KingAttacksBB[cKing] | COOR_TO_BB(cKing);
+ bbBlockers = _BuildFriendlySideBB(pos, xSide);
- //
- // Consider all enemy pieces except the king (not including pawns)
- //
- for (u = 1;
- u < pos->uNonPawnCount[xSide][0];
- u++)
+ 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 ULONG
+_ComputeFileStormDefects(IN POSITION *pos,
+ IN const PAWN_HASH_ENTRY *pHash,
+ IN ULONG uColor)
+/**
+
+Routine description:
+
+ 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.
+
+Parameters:
+
+ POSITION *pos
+ PAWN_HASH_ENTRY *pHash
+ ULONG uColor
+
+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
+ ULONG uColor
+
+Return value:
+
+ ULONG
+
+**/
+{
+ 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
+
+ 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
-EstimatePositionalScore(IN POSITION *pos,
- IN UNUSED PAWN_HASH_ENTRY *pHash,
+_RecordPositionalCalibration(IN SEARCHER_THREAD_CONTEXT *ctx,
+ IN POSITION *pos,
+ IN const PAWN_HASH_ENTRY *pHash,
+ IN ULONG uColor,
+ IN ULONG uRawCounterValue)
+/**
+
+Routine description:
+
+ 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:
+
+ 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:
+
+ void
+
+**/
+{
+ 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)
+ {
+ 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]++;
+}
+
+
+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:
+
+ void
+
+Return value:
+
+ void
+
+**/
+{
+ 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++)
+ {
+ 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
+
+
+#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];
+
+static ULONG
+_MaxKingDangerBucket(IN SEARCHER_THREAD_CONTEXT *ctx,
+ IN POSITION *pos,
+ IN PAWN_HASH_ENTRY *pHash)
+/**
+
+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)
/**
@@ -2419,11 +2935,16 @@ Routine description:
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 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.
+ 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)
@@ -2433,18 +2954,72 @@ Return value:
**/
{
- // p90 of |true king-safety swing|, indexed by combined defect
- // count (CountKingSafetyDefects(stm) + CountKingSafetyDefects(xsm)),
- // clamped above index 10 (sparse data beyond that).
+ // 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] = {
- 47, 62, 87, 119, 169, 157, 181, 282, 342, 342, 385
+ 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;
- ULONG uDefects = (CountKingSafetyDefects(pos, WHITE) +
- CountKingSafetyDefects(pos, BLACK));
- SCORE iKingTerm = iKingSwingP90[MINU(10, uDefects)];
+ // 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;
@@ -2524,7 +3099,6 @@ Return value:
SCORE i;
ULONG u;
ULONG uTotalMobility;
- ULONG uMaxMobility;
PIECE p;
ASSERT(IS_ON_BOARD(c));
@@ -2532,30 +3106,20 @@ Return value:
ASSERT(p && IS_BISHOP(p));
uColor = GET_COLOR(p);
-#ifdef DEBUG
- pos->cPiece = c;
-#endif
-
- //
- // Unmoved piece
- //
+ // 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
- //
+ // 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);
@@ -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
@@ -3049,10 +3552,7 @@ Return value:
**/
{
PIECE p;
- ULONG uPawnFile = FILE(c) + 1;
ULONG uColor;
- ULONG u;
- ULONG uMaxMobility;
ULONG uTotalMobility;
COOR cSquare;
BITBOARD bb;
@@ -3063,129 +3563,40 @@ Return value:
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])
+ 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] =
{
- //
- // 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");
+ { 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];
- //
- // 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");
+ bonus += (SCORE)((bbEnemyPawns & pHash->bbPasserLocations[xColor]) != 0) *
+ ROOK_WITH_ENEMY_PASSER;
+ bonus += (SCORE)((bbFriendPawns & pHash->bbPasserLocations[uColor]) != 0) *
+ ROOK_WITH_FRIEND_PASSER;
- //
- // 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)
- {
- //
- // 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");
- }
- }
- }
- }
+ 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)
{
+ 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 (RANK1(c))
+ if ((c & 0xF0) == BACK_RANK_MASK[uColor])
{
- cSquare = pos->cNonPawns[WHITE][0];
+ cSquare = pos->cNonPawns[uColor][0];
ASSERT(IS_ON_BOARD(cSquare));
- if (RANK1(cSquare))
+ if ((cSquare & 0xF0) == BACK_RANK_MASK[uColor])
{
- if (((cSquare > E1) && (c > cSquare)) ||
- ((cSquare < D1) && (c < cSquare)))
+ if (((cSquare > E_CORNER[uColor]) && (c > cSquare)) ||
+ ((cSquare < D_CORNER[uColor]) && (c < cSquare)))
{
- EVAL_TERM(WHITE,
- ROOK,
- c,
- pos->iScore[WHITE],
- KING_TRAPPING_ROOK,
- "king trapping rook");
- }
- }
- }
- }
- else
- {
- ASSERT(uColor == BLACK);
- ASSERT(IS_ON_BOARD(c));
- if (RANK8(c))
- {
- cSquare = pos->cNonPawns[BLACK][0];
- ASSERT(IS_ON_BOARD(cSquare));
-
- if (RANK8(cSquare))
- {
- if (((cSquare > E8) && (c > cSquare)) ||
- ((cSquare < D8) && (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);
+ 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);
- }
+ 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");
}
@@ -4681,17 +4990,24 @@ Return value:
#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:
+ // 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.
//
- // 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
- //
- // ...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
//
- // 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.
+ // Compute an estimate of the score for the side to move based on the
+ // eval terms we have already considered:
//
- EstimatePositionalScore(pos, pHash, &iAlphaMargin, &iBetaMargin);
+ // 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 (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);
@@ -615,6 +628,22 @@ Return value:
(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;
UINT64 u64PHitCycles = ctx->sCounters.tree.u64CyclesEvalPawnsHit;
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))