From 5405191f8c519333006417a5a3c31bc3e186e42e Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 5 Sep 2026 10:37:55 -0700 Subject: Retire ATTACK_BITV/bvAttacks and the c|8 shadow-index mechanism entirely Now that king (the last piece writing it) has converted to a bitboard accumulator, nothing writes rgSquare[c|8].bvAttacks any more -- deletes the whole mechanism rather than leaving a known-dead struct around: - chess.h: ATTACK_BITV union gone. SQUARE collapses from a union-with-ATTACK_BITV to a plain {pPiece, uIndex} struct (the #pragma pack(1) that only existed for ATTACK_BITV's bitfield layout goes too). UNSAFE_FOR_ROOK/UNSAFE_FOR_QUEEN and the whole-word MINOR_XRAY_BIT/ ROOK_XRAY_BIT/QUEEN_XRAY_BIT constants deleted -- confirmed unused (only ever referenced in stale comments, not code) now that every consumer reads bbXAttacks bitboards directly. PAWN_BIT/MINOR_BIT/ ROOK_BIT/QUEEN_BIT/KING_BIT stay: they're a separate, still-live local bit-packing scheme _EvalKing/_WhoControlsSquareFast use to build a per-square attack-pattern index into KING_COUNTER_BY_ ATTACK_PATTERN/g_SwapTable, unrelated to the retired storage struct. - eval.c: _ClearAttackTables drops its entire macro-unrolled, 128-square clearing loop (CLEAR_A_SQ/CLEAR_A_RANK/CLEAR_SHORT_RANK, all deleted) -- it only ever existed to zero the old per-square struct; clearing the 7 bbXAttacks accumulators is the whole function now. The transitional _IsSquareAttackedByX/_IsSquareXrayedByX helpers (minor/rook/queen/king, 8 functions total) are deleted outright, not just simplified -- their only remaining purpose was bridging to the now-gone struct, and their DEBUG cross-checks were explicitly migration-only scaffolding, not a permanent invariant. Call sites (_WhoControlsSquareFast, _EvalKing's bvAttack/bvXray/bvDefend) read the bbXAttacks bitboards directly instead. _WhoControlsSquareFast simplifies to a flat OR of 8 bitboard membership tests per color, down from raw struct reads plus 7 helper calls each. Found and fixed one real, pre-existing bug while doing this (flagged and confirmed with the user before touching it, kept as its own documented change rather than silently folded into the mechanical rename): _EvaluateCandidatePasser's helper-pawn-safety gate read rgSquare[c1+8].bvAttacks[...].uWholeThing, but this function runs from _EvalPawns -- the first piece type Eval() evaluates each call, before any non-pawn piece (or, since commit 57502d6 retired pawns' own bvAttacks write, even pawns) has written anything there. That word has therefore been unconditionally zero, and the gate unconditionally true (a silent no-op), since 57502d6 landed -- not something today's cleanup introduced. Left exactly as dead/unconditional (deleted the now-meaningless condition, kept the body it always ran anyway) rather than fixed, since a real fix changes eval scoring and deserves its own before/after check, documented inline for a future session. Verification: precommit_check.sh (self-test + DEBUG smoke test) passes. tests/ecm_ringers.ep_ at sd10 vs. the immediately preceding commit: solve parity holds exactly (10/11 both), and final (depth-10) node counts are byte-identical for all 11 positions -- the bar for a change meant to be purely mechanical, unlike king's own conversion. One harmless artifact noted: ECM.750 has a different depth-6 *intermediate* best move (a shallow tie-break flip) that already resolves to the identical PV and node count by depth 7 and holds through depth 10 -- not chased further since the actual (depth-10) result matches exactly and search is deterministic at --cpus 1. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01XxmVi2sTMwpPp4i6WYFjan --- src/chess.h | 242 ++++++++++++++++++------------------------------------------ 1 file changed, 73 insertions(+), 169 deletions(-) (limited to 'src/chess.h') diff --git a/src/chess.h b/src/chess.h index aababc0..acab153 100755 --- a/src/chess.h +++ b/src/chess.h @@ -542,105 +542,55 @@ typedef union _MOVE // ---------------------------------------------------------------------- -#pragma pack(1) -typedef union _ATTACK_BITV -{ - ULONG uWholeThing; - struct - { - union - { - UCHAR uSmall; - struct - { - UCHAR uNumAttacks : 3; // 0..2 - UCHAR uKing : 1; // 4..7 - UCHAR uQueen : 1; - UCHAR uRook : 1; - UCHAR uMinor : 1; - UCHAR uPawn : 1; - } small; - }; - - // -------------------- - union - { - USHORT uBig; - struct - { - USHORT uKing : 1; // 8..23 - USHORT uQueens : 4; - USHORT uRooks : 4; - USHORT uMinors : 4; - USHORT uPawns : 2; - USHORT uUnusedFlag1 : 1; - } big; - }; - - // ------------------- - union - { - UCHAR uXray; - struct - { - UCHAR uNumXrays : 3; - UCHAR uUnusedFlag2 : 1; - UCHAR uQueen : 1; // 24..28 - UCHAR uRook : 1; - UCHAR uBishop : 1; - UCHAR uUnusedFlag3 : 1; - } xray; - }; - }; -} -ATTACK_BITV; - -// UNSAFE_FOR_MINOR retired as a bvAttacks-based macro 2026-09-05: -// pawns no longer write PAWN_BIT into bvAttacks (see POSITION's -// bbPawnAttacks[2] comment, chess.h) -- it was PAWN_BIT alone, so -// every former call site now just tests -// (pos->bbPawnAttacks[enemy] & COOR_TO_BB(sq)) directly, no macro -// needed. UNSAFE_FOR_ROOK/_QUEEN's masks below are narrowed to drop -// PAWN_BIT (0x80), which would otherwise silently always read 0 now -// that nothing sets it -- callers combine these with an explicit -// bbPawnAttacks test instead (see eval.c's _EvalRook/_EvalQueen). -#define UNSAFE_FOR_ROOK(x) ((ULONG)((x).uWholeThing) & 0x00000040UL) -#define UNSAFE_FOR_QUEEN(x) ((ULONG)((x).uWholeThing) & 0x00000060UL) - -#define PAWN_BIT 0x00000080UL -#define MINOR_BIT 0x00000040UL -#define MINOR_XRAY_BIT 0x40000000UL -#define ROOK_BIT 0x00000020UL -#define ROOK_XRAY_BIT 0x20000000UL -#define QUEEN_BIT 0x00000010UL -#define QUEEN_XRAY_BIT 0x10000000UL +// ATTACK_BITV/bvAttacks retired entirely 2026-09-05 (board_ +// representation/EVAL.md section 9): the per-square, per-color +// attack-bit union that used to live at rgSquare[c|8] (the "c|8" +// shadow-index trick -- reusing the same 128-entry array the 0x88 +// board layout already needs for its own off-board sentinels, at +// the indices real piece coordinates never use) is gone now that +// king -- the last piece type writing it -- has converted to a +// bitboard accumulator like every other piece type before it. Every +// consumer reads a pos->bbXAttacks[color] bitboard directly instead. +// +// PAWN_BIT/MINOR_BIT/ROOK_BIT/QUEEN_BIT/KING_BIT below are NOT part +// of that retired mechanism -- they're a separate, still-live +// byte-scale bit-packing scheme _EvalKing/_WhoControlsSquareFast use +// locally (BITV-typed variables, not struct fields) to build a +// per-square "which piece types attack/defend/x-ray this square" +// 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 // 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 +// KING_XRAY_BIT to go with this. +#define KING_BIT 0x00000008UL #define INVALID_PIECE_INDEX (17) #define IS_VALID_PIECE_INDEX(x) ((x) < INVALID_PIECE_INDEX) -typedef union _SQUARE +typedef struct _SQUARE { - struct - { - PIECE pPiece; - ULONG uIndex; - }; - ATTACK_BITV bvAttacks[2]; + PIECE pPiece; + ULONG uIndex; } SQUARE; -#pragma pack() // // POSITION // typedef struct _POSITION { - SQUARE rgSquare[128]; // where the pieces are, - // also, the attack table + SQUARE rgSquare[128]; // where the pieces are + // (0x88 board layout -- the + // upper half of each rank's + // 16-entry span is off-board + // sentinel space, no longer + // double-purposed as attack- + // bit storage now that + // ATTACK_BITV is retired) UINT64 u64NonPawnSig; // hash signature UINT64 u64PawnSig; // pawn hash signature ULONG uToMove; // whose turn? @@ -700,97 +650,51 @@ typedef struct _POSITION // not as something callers should call directly anymore. BITBOARD bbOccupied; - // First mover of board_representation/EVAL.md section 2's - // bvAttacks replacement, added 2026-09-05: "which squares does - // this side's pawns attack," computed fresh once per Eval() call - // from bbPawns via a single shift-and-mask (see - // _PopulatePawnAttackBits in eval.c, same technique as - // generate.c's _GenerateAllPawnMovesBB) -- zero per-pawn mailbox - // iteration, so unlike bbPieces/bbPawns/bbOccupied above this is - // NOT incrementally maintained across moves; it's plain Eval()- - // scoped scratch space, recomputed every call the same way - // pos->iScore[] is. Pawns no longer write their attack bit into - // rgSquare[c|8].bvAttacks at all -- every consumer of "does an - // enemy/friendly pawn attack this square" reads this bitboard - // directly instead (UNSAFE_FOR_MINOR/_ROOK/_QUEEN's pawn - // component, _EvalKing's bvAttack/bvDefend). Knight/bishop/rook/ - // queen/king still populate/read bvAttacks for their own bits - // (uMinor/uRook/uQueen/uKing) until their own conversions land -- - // see EVAL.md section 2 for the planned bbMinorAttacks/ - // bbRookAttacks/bbQueenAttacks that will retire the rest of it. + // 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 + // _EvalKing/_WhoControlsSquareFast to answer "which side attacks + // this square, and with what." Landed piece-by-piece as + // board_representation/EVAL.md's bitboard-eval migration replaced + // the old per-square, per-color rgSquare[c|8] ATTACK_BITV + // mechanism (2026-09-04 through 2026-09-05, pawns first, king + // last) -- see EVAL.md for that history. All are cleared once per + // Eval() call in _ClearAttackTables and accumulated via |= as each + // piece evaluates (not incrementally maintained across moves, the + // way bbPieces/bbPawns/bbOccupied above are). + // + // bbPawnAttacks is the odd one out: computed in one shot from + // bbPawns via a shift-and-mask (_PopulatePawnAttackBits, same + // technique as generate.c's _GenerateAllPawnMovesBB), not + // per-piece, and assigned wholesale rather than OR-accumulated. + // + // Only bishop/rook/queen have a matching bbXXrayAttacks: squares + // seen *through* a friendly same-or-greater-value battery partner + // (and, for rook/bishop specifically, through an x-rayable enemy + // piece too -- queen's own case never x-rays past an enemy piece). + // Knight and king can't x-ray at all (no blocker-piercing move). + // Bishop's x-ray chains arbitrarily deep, same as rook's/queen's, + // not the single-hop approximation it originally shipped with (see + // EVAL.md's progress log for why that was revisited). BITBOARD bbPawnAttacks[2]; - - // "Which squares does this side's knight(s) or bishop(s) attack" -- - // knight ORs in g_KnightAttacksBB[c] directly (no per-square - // bit-scan needed); bishop ORs in _BishopAttacksBB(c, - // pos->bbOccupied), generate.c's magic-bitboard slider lookup. - // Eval()-scoped scratch like bbPawnAttacks above, cleared once per - // Eval() call in _ClearAttackTables since (unlike bbPawnAttacks) - // it's accumulated across multiple pieces via |=, not assigned - // wholesale. Rook/queen/king are not converted yet and still write - // their own bits into the old rgSquare[c|8].bvAttacks structure -- - // see _IsSquareAttackedByMinor (eval.c), the transitional helper - // every consumer goes through until they convert too. BITBOARD bbMinorAttacks[2]; - - // Squares seen *through* a friendly bishop/queen battery partner - // or an x-rayable enemy rook/queen/king, for bishops only (knights - // never x-ray) -- see _EvalBishop's mobility loop. Same lifetime/ - // clearing discipline as bbMinorAttacks above. Nothing else writes - // this yet (rook/queen have their own xray bits still living in - // the old bvAttacks structure), so unlike bbMinorAttacks' direct- - // bit case there's no old-structure fallback to combine with -- - // see _IsSquareXrayedByMinor. BITBOARD bbMinorXrayAttacks[2]; - - // Rook's turn to convert (board_representation/EVAL.md section - // 9, 2026-09-05): "which squares does this side's rook(s) attack," - // ORed in via _RookAttacksBB(c, pos->bbOccupied) straight from - // _EvalRook's mobility computation, same Eval()-scoped/cleared- - // per-call lifetime as bbMinorAttacks. bbRookXrayAttacks mirrors - // bbMinorXrayAttacks: squares seen past a friendly rook/queen - // battery partner or an x-rayable enemy queen/king, single-hop - // only (same deliberate simplification bishop already made, see - // _EvalRook's mobility comment). Queen/king haven't converted yet - // and still write their own bits into the old bvAttacks structure. BITBOARD bbRookAttacks[2]; BITBOARD bbRookXrayAttacks[2]; - - // Queen's turn to convert (board_representation/EVAL.md section - // 9, 2026-09-05): "which squares does this side's queen(s) - // attack," ORed in from the two-pass rook-direction/bishop- - // direction magic lookups _EvalQueen's mobility computation - // already needs (MOVEGEN_MIGRATION.md's own already-tested - // "combined 8-ray table measured slower than reusing the - // rook/bishop tables in two passes" finding -- reuse that - // structure here too). bbQueenXrayAttacks mirrors - // bbRookXrayAttacks/bbMinorXrayAttacks: squares seen past a - // friendly queen/rook/bishop battery partner, chained arbitrarily - // deep like rook's (not single-hop like bishop's original cut) -- - // queen's own case table never x-rays past an enemy piece (unlike - // rook/bishop), only through friendly battery partners. King - // hasn't converted yet and still writes its own bits into the old - // bvAttacks structure. 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. + // bbKingAttacks reproduces, for free, an existing order-dependent + // asymmetry _EvalKing already had before this bitboard conversion: + // kings are evaluated black-then-white (Eval()'s fixed order), so + // by the time white's king-safety computation runs it can see + // black's already-written attacks but black's own computation + // never could (white hadn't run yet). _EvalKing's own bvAttack + // deliberately keeps this asymmetric (drops the enemy king's + // contribution entirely, matching the side that already couldn't + // see it, by direct instruction) -- but every other bbKingAttacks + // reader (_WhoControlsSquareFast, only ever called after *both* + // kings finish evaluating) sees a fully populated, symmetric + // bitboard for both colors. BITBOARD bbKingAttacks[2]; ULONG uWhiteSqBishopCount[2]; // num bishops on white squares -- cgit v1.3