summaryrefslogtreecommitdiff
path: root/src/eval.c
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/eval.c
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/eval.c')
-rwxr-xr-xsrc/eval.c106
1 files changed, 81 insertions, 25 deletions
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);
}
//