diff options
| author | Scott Gasch <[email protected]> | 2026-09-05 01:54:59 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-05 01:54:59 -0700 |
| commit | 7e3e6b69969cf6657f518102be515702da6a7343 (patch) | |
| tree | e050341206349907ea14fe7f1f37f11d0b54dac9 /src | |
| parent | 6e8645015f73a09f57e592fbf911e66db845b327 (diff) | |
Knight mobility: bitboard rewrite, first bbMinorAttacks contributor
_EvalKnight's mobility loop (per-square g_iNDeltas delta-walk +
NMobCaseTable switch dispatch) replaced entirely with
g_KnightAttacksBB[c] (generate.c's precomputed table, already used by
move generation) plus two bitboard masks -- knights never x-ray or
have a battery partner, so the old four-case table collapses to
"enemy non-pawn: count unconditionally" and "empty-or-enemy-pawn:
count unless pawn-unsafe."
Adds POSITION::bbMinorAttacks[2] (chess.h), the first of the
bbMinorAttacks/bbRookAttacks/bbQueenAttacks accumulators from
board_representation/EVAL.md section 2 -- knight ORs its full attack
set in directly, no per-square bit-scan needed. Bishop is not
converted yet and still writes its own minor-bit contribution into
the old per-square rgSquare[c|8].bvAttacks structure.
Since knight stopped writing that old structure, every consumer that
needs "does any minor attack this square" (rook/queen's mobility-
safety check, king's danger computation, _WhoControlsSquareFast) now
goes through a new transitional helper, _IsSquareAttackedByMinor,
which ORs the new bitboard (knight) with the old bvAttacks bit
(bishop) in exactly one place rather than each call site hand-rolling
its own combination -- this collapses to a plain bbMinorAttacks read
once bishop converts too, and the helper goes away entirely.
_IsSquareAttackedByMinor is DEBUG-asserted against an independent
recomputation (g_KnightAttacksBB / _BishopAttacksBB, both already-
trusted primitives from move generation, unrelated to either the old
ray-walk's bit bookkeeping or the new accumulator) so a bug in either
mechanism fails loudly in any DEBUG build/smoke-test run rather than
silently drifting into a wrong score deep in search.
Verified via precommit_check.sh and a direct before/after node-count
comparison (sd10, r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/
R1BQ1RK1 w - - 0 1): byte-identical, 3125335 nodes both builds.
This landed as a deliberately small, single-piece-type step after an
earlier attempt to convert knight+bishop+rook+queen+king in one
combined change produced a real bug (a byte-scale mismatch in xray
bit reconstruction) that was hard to isolate with five things changed
at once, and was reverted back to 6e86450 rather than debugged
further. Bishop, rook, queen, and _EvalKing's own conversion are
follow-up steps, each to be landed and verified the same way, one at
a time.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P6g6iF6mD1Hau6nCZCzwYj
Diffstat (limited to 'src')
| -rwxr-xr-x | src/chess.h | 13 | ||||
| -rwxr-xr-x | src/eval.c | 185 |
2 files changed, 108 insertions, 90 deletions
diff --git a/src/chess.h b/src/chess.h index 7ff9994..2e9189a 100755 --- a/src/chess.h +++ b/src/chess.h @@ -716,6 +716,19 @@ typedef struct _POSITION // bbRookAttacks/bbQueenAttacks that will retire the rest of it. BITBOARD bbPawnAttacks[2]; + // First mover of the bbMinorAttacks pair (knight only for now -- + // bishop is a separate, later step; see board_representation/ + // EVAL.md section 2). "Which squares does this side's knight(s) + // attack" -- computed via g_KnightAttacksBB[c] and OR'd in per + // knight, 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. Bishop still writes its own minor-bit + // contribution into the old rgSquare[c|8].bvAttacks structure for + // now -- every consumer that needs "any minor's attack," not just + // knight's, ORs both sources together until bishop converts too. + BITBOARD bbMinorAttacks[2]; + ULONG uWhiteSqBishopCount[2]; // num bishops on white squares SCORE iMaterialBalance[2]; // material balance @@ -1152,6 +1152,59 @@ Return value: } +// +// Transitional helper, board_representation/EVAL.md section 2: during +// the piece-by-piece migration off bvAttacks, knight now writes its +// attacks into pos->bbMinorAttacks directly while bishop (not +// converted yet) still writes the old per-square +// rgSquare[c|8].bvAttacks[color].small.uMinor bit. Every consumer +// that needs "does any minor (knight or bishop) attack this square" +// goes through this one function instead of each hand-rolling its own +// OR of the two sources -- once bishop converts too, this collapses +// to a plain pos->bbMinorAttacks[color] read and this whole function +// goes away; it is not meant to be a permanent fixture. +// +// DEBUG-only: cross-checked against an independent recomputation +// (g_KnightAttacksBB / _BishopAttacksBB, both already-trusted +// primitives used elsewhere in move generation, entirely separate +// code from either the ray-walk's uBit bookkeeping or the new +// bitboard population) so a bug in either mechanism fails loudly here +// instead of silently drifting into a wrong score several plies deep +// in search -- exactly the failure mode a prior, larger version of +// this same migration hit and had to be rolled back for. +// +static FLAG +_IsSquareAttackedByMinor(IN POSITION *pos, + IN ULONG uColor, + IN COOR c) +{ + BITBOARD sq = COOR_TO_BB(c); + FLAG fResult = ((pos->bbMinorAttacks[uColor] & sq) != 0) || + (pos->rgSquare[c|8].bvAttacks[uColor].small.uMinor != 0); +#ifdef DEBUG + { + BITBOARD bbTrueMinorAttacks = 0; + ULONG u; + for (u = 1; u < pos->uNonPawnCount[uColor][0]; u++) + { + COOR cPiece = pos->cNonPawns[uColor][u]; + PIECE p = pos->rgSquare[cPiece].pPiece; + if (IS_KNIGHT(p)) + { + bbTrueMinorAttacks |= g_KnightAttacksBB[cPiece]; + } + else if (IS_BISHOP(p)) + { + bbTrueMinorAttacks |= _BishopAttacksBB(cPiece, pos->bbOccupied); + } + } + ASSERT(((bbTrueMinorAttacks & sq) != 0) == (fResult != 0)); + } +#endif + return fResult; +} + + static ULONG _WhoControlsSquareFast(IN POSITION *pos, IN COOR c) @@ -1186,11 +1239,13 @@ Return value: ULONG uWhite = pos->rgSquare[c|8].bvAttacks[WHITE].uSmall | pos->rgSquare[c|8].bvAttacks[WHITE].uXray | ((pos->bbPawnAttacks[WHITE] & COOR_TO_BB(c)) ? - PAWN_BIT : 0); + PAWN_BIT : 0) | + (_IsSquareAttackedByMinor(pos, WHITE, c) ? MINOR_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)) ? - PAWN_BIT : 0); + PAWN_BIT : 0) | + (_IsSquareAttackedByMinor(pos, BLACK, c) ? MINOR_BIT : 0); ULONG u; PIECE p; CHAR ch; @@ -1260,6 +1315,7 @@ static void _ClearAttackTables(IN OUT POSITION *pos) { register COOR c = 8; + pos->bbMinorAttacks[WHITE] = pos->bbMinorAttacks[BLACK] = 0; #if 1 CLEAR_A_SQ; c += 16; CLEAR_A_SQ; c += 16; @@ -2840,41 +2896,6 @@ Return value: **/ { static const int iPawnStart[2] = { -17, +15 }; - static const UCHAR NMobCaseTable[2][14] = - { - {// (black) - NMOB_MOBILE_SQUARE, // EMPTY_SQUARE (0) - NMOB_INVALID, // INVALID_PIECE (1) - NMOB_FRIEND, // BLACK_PAWN (2) - NMOB_MOBILE_SQUARE, // WHITE_PAWN (3) - NMOB_FRIEND, // BLACK_KNIGHT (4) - NMOB_ENEMY_OTHER, // WHITE_KNIGHT (5) - NMOB_FRIEND, // BLACK_BISHOP (6) - NMOB_ENEMY_OTHER, // WHITE_BISHOP (7) - NMOB_FRIEND, // BLACK_ROOK (8) - NMOB_ENEMY_OTHER, // WHITE_ROOK (9) - NMOB_FRIEND, // BLACK_QUEEN (10) - NMOB_ENEMY_OTHER, // WHITE_QUEEN (11) - NMOB_FRIEND, // BLACK_KING (12) - NMOB_ENEMY_OTHER, // WHITE_KING (13) - }, - {// (white) - NMOB_MOBILE_SQUARE, // EMPTY_SQUARE (0) - NMOB_INVALID, // INVALID_PIECE (1) - NMOB_MOBILE_SQUARE, // BLACK_PAWN (2) - NMOB_FRIEND, // WHITE_PAWN (3) - NMOB_ENEMY_OTHER, // BLACK_KNIGHT (4) - NMOB_FRIEND, // WHITE_KNIGHT (5) - NMOB_ENEMY_OTHER, // BLACK_BISHOP (6) - NMOB_FRIEND, // WHITE_BISHOP (7) - NMOB_ENEMY_OTHER, // BLACK_ROOK (8) - NMOB_FRIEND, // WHITE_ROOK (9) - NMOB_ENEMY_OTHER, // BLACK_QUEEN (10) - NMOB_FRIEND, // WHITE_QUEEN (11) - NMOB_ENEMY_OTHER, // BLACK_KING (12) - NMOB_FRIEND, // WHITE_KING (13) - } - }; static const COOR cKnightAtHome[2][2] = { { B8, G8 }, // BLACK @@ -2885,7 +2906,6 @@ Return value: BITBOARD bb; ULONG uColor; ULONG uPawnsSupporting; - ULONG u; ULONG uMobilitySquares; SCORE i; ULONG uDist; @@ -3045,57 +3065,34 @@ Return value: // // - // Do mobilility and piece relevance. Also update attack tables. + // Do mobility and piece relevance. Also update attack tables. + // + // board_representation/EVAL.md section 1b/2: g_KnightAttacksBB[c] + // (generate.c's precomputed table, already used by move + // generation) is exactly the old per-square g_iNDeltas walk's + // destination set, IS_ON_BOARD baked in at table-build time -- no + // per-square branch, no mailbox read, no switch. Knights never + // x-ray or have a battery partner, so the old NMobCaseTable's four + // cases collapse to two bitboard masks: NMOB_ENEMY_OTHER (any + // enemy non-pawn -- count unconditionally) and NMOB_MOBILE_SQUARE + // (empty or enemy pawn -- count unless pawn-unsafe). NMOB_FRIEND + // needs no term at all, it's just "neither of the above." // - uMobilitySquares = 0; - u = 0; - ASSERT(g_iNDeltas[u] != 0); - do { - cSquare = c + g_iNDeltas[u]; - if (IS_ON_BOARD(cSquare)) - { - // - // Always update the attack bits - // - ASSERT((cSquare + 8) == (cSquare | 8)); - pos->rgSquare[cSquare|8].bvAttacks[uColor].small.uMinor = 1; - - // - // See what we hit. Dispatched via switch instead of an - // indirect call through a function pointer -- same - // rationale as the bishop ray-walk above. - // - p = pos->rgSquare[cSquare].pPiece; - switch (NMobCaseTable[uColor][p]) - { - case NMOB_MOBILE_SQUARE: - uMobilitySquares += - !(pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)); - break; + BITBOARD bbAttack = g_KnightAttacksBB[c]; + BITBOARD bbFriendOcc = _BuildFriendlySideBB(pos, uColor); + BITBOARD bbEnemyNonPawnOcc = _BuildFriendlySideBB(pos, FLIP(uColor)) & + ~pos->bbPawns[FLIP(uColor)]; + BITBOARD bbUnsafeForMinor = pos->bbPawnAttacks[FLIP(uColor)]; + BITBOARD bbMobility; - case NMOB_ENEMY_OTHER: - uMobilitySquares += 1; - break; - - case NMOB_FRIEND: - break; - - case NMOB_INVALID: - default: - UtilPanic(SHOULD_NOT_GET_HERE, - NULL, NULL, NULL, NULL, - __FILE__, __LINE__); - break; - } + pos->bbMinorAttacks[uColor] |= bbAttack; - // - // IDEA: bonus for hitting friendly pawn? - // - } - u++; + bbMobility = (bbAttack & bbEnemyNonPawnOcc) | + (bbAttack & ~bbFriendOcc & ~bbEnemyNonPawnOcc & + ~bbUnsafeForMinor); + uMobilitySquares = CountBits(bbMobility); } - while(g_iNDeltas[u] != 0); ASSERT(uMobilitySquares >= 0); ASSERT(uMobilitySquares <= 8); EVAL_TERM(uColor, @@ -3381,14 +3378,14 @@ Return value: case RMOB_EMPTY: uCurrentMobility += !((pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) || - UNSAFE_FOR_ROOK(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)])); + _IsSquareAttackedByMinor(pos, FLIP(uColor), cSquare)); fStop = FALSE; break; case RMOB_ENEMY_LESS: uCurrentMobility += !((pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) || - UNSAFE_FOR_ROOK(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)])); + _IsSquareAttackedByMinor(pos, FLIP(uColor), cSquare)); fStop = TRUE; break; @@ -3684,14 +3681,18 @@ Return value: case QMOB_EMPTY: uTotalMobility += !((pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) || - UNSAFE_FOR_QUEEN(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)])); + _IsSquareAttackedByMinor(pos, FLIP(uColor), cSquare) || + (pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)].uWholeThing & + ROOK_BIT)); fStop = FALSE; break; case QMOB_ENEMY_LESS: uTotalMobility += !((pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) || - UNSAFE_FOR_QUEEN(pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)])); + _IsSquareAttackedByMinor(pos, FLIP(uColor), cSquare) || + (pos->rgSquare[cSquare|8].bvAttacks[FLIP(uColor)].uWholeThing & + ROOK_BIT)); fStop = TRUE; break; @@ -3935,7 +3936,9 @@ Return value: // bvAttack = pos->rgSquare[cSquare|8].bvAttacks[ufColor].uSmall | ((pos->bbPawnAttacks[ufColor] & COOR_TO_BB(cSquare)) ? - PAWN_BIT : 0); + PAWN_BIT : 0) | + (_IsSquareAttackedByMinor(pos, ufColor, cSquare) ? + MINOR_BIT : 0); { COOR cRealSquare = cSquare; cSquare |= 8; @@ -3943,7 +3946,9 @@ Return value: pos->rgSquare[cSquare].bvAttacks[uColor].small.uKing = 1; bvDefend = pos->rgSquare[cSquare].bvAttacks[uColor].uSmall | ((pos->bbPawnAttacks[uColor] & COOR_TO_BB(cRealSquare)) ? - PAWN_BIT : 0); + PAWN_BIT : 0) | + (_IsSquareAttackedByMinor(pos, uColor, cRealSquare) ? + MINOR_BIT : 0); } // |
