summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-05 10:23:28 -0700
committerScott Gasch <[email protected]>2026-09-05 10:23:28 -0700
commitaad154a5a07d2793e52a71415cc5c59276a8f629 (patch)
tree2bcfd67989bb123e13a449080e316ee0d5ecb05e /src
parent1216f4e159af037a32323aa7c942e64fcecafb05 (diff)
King attack-table population: bitboard rewrite, fixes a real bug and a real asymmetry
_EvalKing was the last piece type contributing to the old bvAttacks/ ATTACK_BITV mechanism -- both write sites (the low-enemy-material early-exit path and the main king-safety loop) replaced with a single pos->bbKingAttacks[uColor] |= g_KingAttacksBB[c], plus a new _IsSquareAttackedByKing transitional helper (DEBUG-cross-checked, no x-ray companion needed since a king can't move through a blocker) for _WhoControlsSquareFast and _EvalKing's own bvDefend to read. Two real, intentional behavior changes land with this conversion, both discussed and confirmed before coding rather than assumed: 1. The main king-safety loop's old per-square write used KingSafetyDeltas (11 entries: the 8 real king-move squares plus -2/ +2, two squares away on the same rank, present only for that loop's own file-distance bookkeeping) instead of the real 8-square king move pattern -- a bug, confirmed by direct instruction, not a design choice worth preserving. Fixed by writing the real g_KingAttacksBB[c] pattern once, before the loop, instead of whatever KingSafetyDeltas happened to visit per-square. 2. _EvalKing's own bvAttack computation (does the *enemy* king attack a square near this king?) is now symmetric where it used to be an accidental artifact of evaluation order: kings are evaluated black-then-white, so the old mailbox code let white's computation see black's already-written king bit while black's could never see white's (white hadn't run yet). Rather than preserve or upgrade that asymmetry now that both colors go through an explicit helper either way, neither side sees the enemy king as a threat here, matching the side that already couldn't -- by direct instruction. This is narrow in practice (two kings can never be legally adjacent, so it only ever fires at king-vs-king distance 2) but real. _WhoControlsSquareFast's own king contribution is unaffected by either change and stays fully symmetric: it's only ever called after *both* kings finish evaluating (the passed-pawn re-check and trapped-piece/ danger passes all run after Eval()'s king-eval block), so pos->bbKingAttacks is always fully populated for both colors by the time it runs -- confirmed by checking call-site ordering directly, not assumed from bvAttack's own (different) situation. CountKingSafetyDefects is untouched by this change (confirmed by reading it again): it's pure CHECK_VECTOR geometry over piece locations, computed before any bbXAttacks accumulator exists this Eval() call, and reads none of them. Its correlation with _EvalKing's score is therefore preserved by construction, not something requiring separate re-tuning. Verification: since this bundles three attributable behavior changes (the bitboard rewrite itself, the KingSafetyDeltas bug fix, and the dropped bvAttack asymmetry), used the ringers-suite-plus-bounded-delta bar from bishop's conversion rather than expecting exact node-count equality: precommit_check.sh (self-test + DEBUG smoke test, plus several hand-built king-adjacency/king-proximity positions run directly against a DEBUG binary to exercise the new cross-check) all pass; whole-engine tests/ecm_ringers.ep_ at sd10 vs. the immediately preceding commit holds exact solve parity (10/11 both), with per-position node-count deltas (-50% to +76%) all attributable to the three documented changes above, no unexplained outliers. ATTACK_BITV/bvAttacks itself is intentionally left in place -- nothing writes it any more (king was the last writer), but the struct deletion and the resulting simplification of the transitional _IsSquareAttackedByX/_IsSquareXrayedByX helpers (which collapse to plain bitboard reads once nothing can ever populate the old structure) is staged as a deliberate follow-up commit, not bundled here. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01XxmVi2sTMwpPp4i6WYFjan
Diffstat (limited to 'src')
-rwxr-xr-xsrc/chess.h23
-rwxr-xr-xsrc/eval.c106
2 files changed, 104 insertions, 25 deletions
diff --git a/src/chess.h b/src/chess.h
index 6b09840..aababc0 100755
--- a/src/chess.h
+++ b/src/chess.h
@@ -614,6 +614,10 @@ ATTACK_BITV;
#define ROOK_XRAY_BIT 0x20000000UL
#define QUEEN_BIT 0x00000010UL
#define QUEEN_XRAY_BIT 0x10000000UL
+// King never x-rays (it can't move through a blocker), so there's no
+// KING_XRAY_BIT to go with this -- matches struct _ATTACK_BITV's
+// .small.uKing bit position (byte 0, bit 3).
+#define KING_BIT 0x00000008UL
#define INVALID_PIECE_INDEX (17)
#define IS_VALID_PIECE_INDEX(x) ((x) < INVALID_PIECE_INDEX)
@@ -770,6 +774,25 @@ typedef struct _POSITION
BITBOARD bbQueenAttacks[2];
BITBOARD bbQueenXrayAttacks[2];
+ // King's turn to convert (board_representation/EVAL.md section 9,
+ // 2026-09-05) -- the last piece type contributing to the old
+ // bvAttacks/ATTACK_BITV mechanism. Just g_KingAttacksBB[c]
+ // (generate.c's precomputed table, already used by move
+ // generation), no mobility computation involved and no x-ray
+ // (a king can't move through a blocker). Same Eval()-scoped/
+ // cleared-per-call lifetime as every other bbXAttacks accumulator
+ // above -- this also reproduces, for free, an existing
+ // order-dependent asymmetry _EvalKing's mailbox version already
+ // had: kings are evaluated black-then-white (Eval()'s fixed
+ // order), so white's king-safety computation can see black's
+ // already-written attack bits but not vice versa. Once this
+ // lands, nothing writes bvAttacks/ATTACK_BITV any more -- see
+ // EVAL.md section 9 for the planned follow-up that deletes the
+ // whole mechanism and simplifies the transitional
+ // _IsSquareAttackedByX/_IsSquareXrayedByX helpers into plain
+ // bitboard reads.
+ BITBOARD bbKingAttacks[2];
+
ULONG uWhiteSqBishopCount[2]; // num bishops on white squares
SCORE iMaterialBalance[2]; // material balance
diff --git a/src/eval.c b/src/eval.c
index 12783ba..6adf27b 100755
--- a/src/eval.c
+++ b/src/eval.c
@@ -1526,6 +1526,29 @@ _IsSquareXrayedByQueen(IN POSITION *pos,
return fResult;
}
+//
+// King's turn to convert (board_representation/EVAL.md section 9,
+// 2026-09-05) -- the last piece type contributing to bvAttacks. Same
+// transitional-helper purpose as the others, but simpler: no mobility
+// computation, no x-ray (a king can't move through a blocker), so
+// there's no companion _IsSquareXrayedByKing.
+//
+static FLAG
+_IsSquareAttackedByKing(IN POSITION *pos,
+ IN ULONG uColor,
+ IN COOR c)
+{
+ BITBOARD sq = COOR_TO_BB(c);
+ FLAG fResult = (pos->bbKingAttacks[uColor] & sq) != 0;
+#ifdef DEBUG
+ {
+ BITBOARD bbTrueKingAttacks = g_KingAttacksBB[pos->cNonPawns[uColor][0]];
+ ASSERT(((bbTrueKingAttacks & sq) != 0) == (fResult != 0));
+ }
+#endif
+ return fResult;
+}
+
static ULONG
_WhoControlsSquareFast(IN POSITION *pos,
IN COOR c)
@@ -1559,10 +1582,20 @@ Return value:
//
// .uXray is a standalone byte view (see _IsSquareXrayedByMinor's
// comment on why the byte-scale MINOR_BIT, not MINOR_XRAY_BIT, is
- // the right constant to OR in here) -- bishop's, rook's, and now
- // queen's contributions all moved to pos->bbMinorXrayAttacks/
- // bbRookXrayAttacks/bbQueenXrayAttacks; only king hasn't converted
- // yet, still valid straight off .uXray.
+ // the right constant to OR in here) -- bishop's, rook's, and
+ // queen's xray contributions all moved to pos->bbMinorXrayAttacks/
+ // bbRookXrayAttacks/bbQueenXrayAttacks. King has no x-ray (can't
+ // move through a blocker), and its direct-attack contribution is
+ // added symmetrically for both colors below via
+ // _IsSquareAttackedByKing -- unlike _EvalKing's own internal
+ // bvAttack (which deliberately drops the *enemy* king's
+ // contribution, see that function's comment on the black-then-
+ // white evaluation-order asymmetry this sidesteps), this function
+ // is only ever called after *both* kings have finished evaluating
+ // (the passed-pawn re-check runs after the king-eval block in
+ // Eval()'s own sequencing), so pos->bbKingAttacks is always fully
+ // populated for both colors by the time this runs -- no asymmetry
+ // to worry about here, this is a plain, symmetric fact query.
ULONG uWhite = pos->rgSquare[c|8].bvAttacks[WHITE].uSmall |
pos->rgSquare[c|8].bvAttacks[WHITE].uXray |
((pos->bbPawnAttacks[WHITE] & COOR_TO_BB(c)) ?
@@ -1572,7 +1605,8 @@ Return value:
(_IsSquareAttackedByRook(pos, WHITE, c) ? ROOK_BIT : 0) |
(_IsSquareXrayedByRook(pos, WHITE, c) ? ROOK_BIT : 0) |
(_IsSquareAttackedByQueen(pos, WHITE, c) ? QUEEN_BIT : 0) |
- (_IsSquareXrayedByQueen(pos, WHITE, c) ? QUEEN_BIT : 0);
+ (_IsSquareXrayedByQueen(pos, WHITE, c) ? QUEEN_BIT : 0) |
+ (_IsSquareAttackedByKing(pos, WHITE, c) ? KING_BIT : 0);
ULONG uBlack = pos->rgSquare[c|8].bvAttacks[BLACK].uSmall |
pos->rgSquare[c|8].bvAttacks[BLACK].uXray |
((pos->bbPawnAttacks[BLACK] & COOR_TO_BB(c)) ?
@@ -1582,7 +1616,8 @@ Return value:
(_IsSquareAttackedByRook(pos, BLACK, c) ? ROOK_BIT : 0) |
(_IsSquareXrayedByRook(pos, BLACK, c) ? ROOK_BIT : 0) |
(_IsSquareAttackedByQueen(pos, BLACK, c) ? QUEEN_BIT : 0) |
- (_IsSquareXrayedByQueen(pos, BLACK, c) ? QUEEN_BIT : 0);
+ (_IsSquareXrayedByQueen(pos, BLACK, c) ? QUEEN_BIT : 0) |
+ (_IsSquareAttackedByKing(pos, BLACK, c) ? KING_BIT : 0);
ULONG u;
PIECE p;
CHAR ch;
@@ -1658,6 +1693,7 @@ _ClearAttackTables(IN OUT POSITION *pos)
pos->bbRookXrayAttacks[WHITE] = pos->bbRookXrayAttacks[BLACK] = 0;
pos->bbQueenAttacks[WHITE] = pos->bbQueenAttacks[BLACK] = 0;
pos->bbQueenXrayAttacks[WHITE] = pos->bbQueenXrayAttacks[BLACK] = 0;
+ pos->bbKingAttacks[WHITE] = pos->bbKingAttacks[BLACK] = 0;
#if 1
CLEAR_A_SQ; c += 16;
CLEAR_A_SQ; c += 16;
@@ -4132,18 +4168,12 @@ Return value:
ASSERT(u >= VALUE_KING);
if (u < DO_KING_SAFETY_THRESHOLD)
{
- u = 0;
- ASSERT(g_iQKDeltas[u] != 0);
- do
- {
- cSquare = c + g_iQKDeltas[u];
- if (IS_ON_BOARD(cSquare))
- {
- pos->rgSquare[cSquare|8].bvAttacks[uColor].small.uKing = 1;
- }
- u++;
- }
- while(g_iQKDeltas[u] != 0);
+ // board_representation/EVAL.md section 9: g_KingAttacksBB[c]
+ // (generate.c's precomputed table, already used by move
+ // 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;
}
@@ -4159,6 +4189,16 @@ Return value:
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];
+
uFlightSquares = 0;
u = 0;
ASSERT(KingSafetyDeltas[u] != 0);
@@ -4175,11 +4215,18 @@ Return value:
// _PopulatePawnAttackBits) -- OR it back in here from the
// bitboard, keyed off the real board square (cSquare,
// before the |8 below flips it into the invisible-half
- // storage index bvAttacks itself uses). bvDefend's OR-in
- // (and the .small.uKing = 1 write) must stay after this
- // point, same order as before -- bvDefend is meant to
- // include this king's own just-set attack/defend bit on
- // the square.
+ // storage index bvAttacks itself uses).
+ //
+ // No enemy-king contribution here, by direct instruction
+ // (2026-09-05): the old mailbox version was already
+ // asymmetric here (kings evaluate black-then-white, so
+ // white's computation could see black's already-written
+ // king bit but black's could never see white's, since
+ // white hadn't run yet) -- rather than preserve or
+ // "upgrade" that asymmetry now that both colors go through
+ // an explicit helper either way, neither side sees the
+ // enemy king as a threat here, matching the side that
+ // already couldn't.
//
bvAttack = pos->rgSquare[cSquare|8].bvAttacks[ufColor].uSmall |
((pos->bbPawnAttacks[ufColor] & COOR_TO_BB(cSquare)) ?
@@ -4200,7 +4247,14 @@ Return value:
ROOK_BIT : 0) |
(_IsSquareXrayedByQueen(pos, ufColor, cRealSquare) ?
QUEEN_BIT : 0);
- pos->rgSquare[cSquare].bvAttacks[uColor].small.uKing = 1;
+ //
+ // 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 this
+ // one keeps its own-color check.
+ //
bvDefend = pos->rgSquare[cSquare].bvAttacks[uColor].uSmall |
((pos->bbPawnAttacks[uColor] & COOR_TO_BB(cRealSquare)) ?
PAWN_BIT : 0) |
@@ -4209,7 +4263,9 @@ Return value:
(_IsSquareAttackedByRook(pos, uColor, cRealSquare) ?
ROOK_BIT : 0) |
(_IsSquareAttackedByQueen(pos, uColor, cRealSquare) ?
- QUEEN_BIT : 0);
+ QUEEN_BIT : 0) |
+ (_IsSquareAttackedByKing(pos, uColor, cRealSquare) ?
+ KING_BIT : 0);
}
//