diff options
| author | Scott Gasch <[email protected]> | 2026-09-04 16:46:27 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-04 16:46:27 -0700 |
| commit | 92fc41226f784b251f41eab7e75c13075e980a54 (patch) | |
| tree | 201d61c44dea1925b30c1d78ce2a3ac9d98e688e /src/movesup.c | |
| parent | 550ea81a5a2ee2561c3feb91dc55686f4d3cb872 (diff) | |
Land bitboard move generation (Part A+B) and movesup.c bitboard queries; default on
Implements the full board_representation/MOVEGEN_MIGRATION.md scope:
bitboard-backed generators for all six not-in-check piece types plus
the JumpTable-avoiding whole-node dispatch fork (_GenerateAllMovesBB),
the in-check escape path (king flight + block/capture), and
movesup.c's ExposesCheck/FasterExposesCheck/ExposesCheckEp/IsAttacked/
InCheck bitboard equivalents. Nine toggles total
(GENERATE_{KNIGHT,KING,ROOK,BISHOP,QUEEN,PAWN}_BITBOARD,
GENERATE_ESCAPES_{KING,BLOCK}_BITBOARD, EXPOSESCHECK_BITBOARD,
ISATTACKED_BITBOARD), all now on by default in GNUmakefile --
DISABLE_BITBOARD_MOVEGEN=1 opts back into the mailbox path, which
remains fully present and compiled either way.
Correctness verified via perft (Kiwipete, Position 4), the move-set
comparison harness across 20,000 random positions, all nine toggles
combined cleanly (15/15 runs, after fixing a GenerateRandomLegalPosition
en-passant-sentinel bug in the test harness), and sd10 on all three
curated suites showing zero solve-count regression vs head_reference
(the ecm_hard_quick delta traced to unrelated intervening commits).
Speed: most individual generators land near parity by design (mailbox's
per-square walk was already close to O(destination count)); the real,
consistent wins are the dispatch-layer fork (up to 23% in dense
positions) and IsAttackedBB (0.73x-0.93x of mailbox).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AbHkVrm5KUyzLwWd3GHmo6
Diffstat (limited to 'src/movesup.c')
| -rwxr-xr-x | src/movesup.c | 399 |
1 files changed, 397 insertions, 2 deletions
diff --git a/src/movesup.c b/src/movesup.c index 33deae2..22bf0c1 100755 --- a/src/movesup.c +++ b/src/movesup.c @@ -23,7 +23,324 @@ Revision History: #include "chess.h" -COOR +// Unlike GetAttacks (a real asm symbol in a different file, so +// chess.h's macro-swap never touches its own definition), +// ExposesCheck/FasterExposesCheck/ExposesCheckEp's mailbox +// implementations live in *this* file, right below their BB +// counterparts -- chess.h's #define would otherwise rename these +// functions' own definitions too, colliding with the real +// ExposesCheckBB/etc. symbols. #undef restores the real names for +// this file's own definitions; every other translation unit that +// includes chess.h still sees the macro-renamed calls. +#if defined(EXPOSESCHECK_BITBOARD) +#undef ExposesCheck +#undef FasterExposesCheck +#undef ExposesCheckEp +#endif +#if defined(ISATTACKED_BITBOARD) +#undef IsAttacked +#undef InCheck +#endif + +// board_representation/MOVEGEN_MIGRATION.md section 6b: bitboard +// equivalents of ExposesCheck/FasterExposesCheck/ExposesCheckEp. +// ExposesCheck is called from MakeMove (move.c) on essentially every +// move actually played during search -- the pin-legality safety net +// every generator's header comment references -- making it a much +// hotter target than anything in generate.c's own Part A/B work. +// +// Design: keep the mailbox version's own O(1) alignment pre-check +// (CHECK_VECTOR_WITH_INDEX -- already a table lookup, nothing to +// improve) to bail out on the common "not even aligned" case before +// paying for any bitboard work at all. Only when aligned: exclude the +// hypothetically-removed square(s) from occupancy, magic-lookup the +// attack set from cLocation against that occupancy, and mask to the +// single ray through cRemove via g_RookRayToEdge/g_BishopRayToEdge +// (so an unrelated attacker on a *different* ray through cLocation +// can't falsely register) -- then apply the same enemy-color/ +// piece-type check the mailbox version does on whatever single square +// survives. +// Finds the single nearest occupied square along one specific +// direction from c, or ILLEGAL_COOR if that ray is empty all the way +// to the edge. This is *not* "magic attack bitboard ANDed with a ray +// mask" -- that was tried first and is wrong: an unblocked ray's magic +// attack set contains every empty square out to the edge, and +// FastFirstBit on that intersection picks the lowest square index +// overall, which is not necessarily the square nearest c (bit index +// order and "distance from c" only coincide for one of the two +// possible directions along any given ray). The correct technique, +// mirrored exactly from _WhoAttacksSquareBB (see.c): isolate the +// lowest set bit for a "positive" direction (bb & -bb, no bit-scan +// needed) or the highest set bit for a "negative" direction +// (1ULL << (FastLastBit-1)) -- g_RookRayPositiveDir/ +// g_BishopRayPositiveDir already record which is which per direction. +static COOR +_NearestBlockerAlongRayBB(IN COOR c, IN int iDeltaFromC, + IN BITBOARD bbOccupied) +/** + +Routine description: + + Nearest occupied square from c along the single queen-direction + iDeltaFromC, or ILLEGAL_COOR if none. + +Parameters: + + COOR c + int iDeltaFromC : one of g_iQKDeltas's 8 values + BITBOARD bbOccupied + +Return value: + + COOR + +**/ +{ + BITBOARD bbRay; + BITBOARD bbBlockers; + BITBOARD bbBlockerBit; + FLAG fPositiveDir; + ULONG uBitIndex; + + switch (iDeltaFromC) + { + case 16: + bbRay = g_RookRayToEdge[0][c]; + fPositiveDir = g_RookRayPositiveDir[0]; + break; + case -16: + bbRay = g_RookRayToEdge[1][c]; + fPositiveDir = g_RookRayPositiveDir[1]; + break; + case 1: + bbRay = g_RookRayToEdge[2][c]; + fPositiveDir = g_RookRayPositiveDir[2]; + break; + case -1: + bbRay = g_RookRayToEdge[3][c]; + fPositiveDir = g_RookRayPositiveDir[3]; + break; + case 17: + bbRay = g_BishopRayToEdge[0][c]; + fPositiveDir = g_BishopRayPositiveDir[0]; + break; + case -17: + bbRay = g_BishopRayToEdge[1][c]; + fPositiveDir = g_BishopRayPositiveDir[1]; + break; + case 15: + bbRay = g_BishopRayToEdge[2][c]; + fPositiveDir = g_BishopRayPositiveDir[2]; + break; + case -15: + bbRay = g_BishopRayToEdge[3][c]; + fPositiveDir = g_BishopRayPositiveDir[3]; + break; + default: + ASSERT(FALSE); + return(ILLEGAL_COOR); + } + + bbBlockers = bbRay & bbOccupied; + if (0 == bbBlockers) + { + return(ILLEGAL_COOR); + } + bbBlockerBit = fPositiveDir ? + (bbBlockers & (0ULL - bbBlockers)) : + (1ULL << (FastLastBit(bbBlockers) - 1)); + uBitIndex = FastFirstBit(bbBlockerBit) - 1; + return BIT_NUMBER_TO_COOR(uBitIndex); +} + +// Shared tail: given a candidate blocker square, apply the same +// enemy-color / piece-type-can-reach-us check the mailbox versions do, +// and return it (or ILLEGAL_COOR). +static COOR +_ValidateExposedBlockerBB(IN POSITION *pos, IN COOR cBlocker, + IN COOR cLocation) +{ + PIECE xPiece; + int iIndex; + + if (!IS_ON_BOARD(cBlocker)) + { + return(ILLEGAL_COOR); + } + xPiece = pos->rgSquare[cBlocker].pPiece; + ASSERT(!IS_EMPTY(xPiece)); + + if (OPPOSITE_COLORS(xPiece, pos->rgSquare[cLocation].pPiece)) + { + iIndex = (int)cBlocker - (int)cLocation; + if (0 != (CHECK_VECTOR_WITH_INDEX(iIndex, GET_COLOR(xPiece)) & + (1 << (PIECE_TYPE(xPiece))))) + { + return(cBlocker); + } + } + return(ILLEGAL_COOR); +} + +COOR +FasterExposesCheckBB(IN POSITION *pos, + IN COOR cRemove, + IN COOR cLocation) +/** + +Routine description: + + Bitboard equivalent of FasterExposesCheck -- see that function's + comment. Caller already knows exposure is geometrically possible + (skips the alignment pre-check FasterExposesCheck also skips). + +Parameters: + + POSITION *pos, + COOR cRemove, + COOR cLocation + +Return value: + + COOR + +**/ +{ + int iIndex = (int)cLocation - (int)cRemove; + int iDelta; + BITBOARD bbOccupiedWithoutRemove; + COOR cBlocker; + + ASSERT(IS_KING(pos->rgSquare[cLocation].pPiece)); + ASSERT(IS_ON_BOARD(cRemove)); + ASSERT(IS_ON_BOARD(cLocation)); + ASSERT(!IS_EMPTY(pos->rgSquare[cLocation].pPiece)); + ASSERT(0 != (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN))); + + iDelta = CHECK_DELTA_WITH_INDEX(iIndex); + ASSERT(iDelta != 0); + + bbOccupiedWithoutRemove = _BuildFullOccupiedBB(pos) & ~COOR_TO_BB(cRemove); + cBlocker = _NearestBlockerAlongRayBB(cLocation, iDelta, + bbOccupiedWithoutRemove); + return _ValidateExposedBlockerBB(pos, cBlocker, cLocation); +} + +COOR +ExposesCheckBB(IN POSITION *pos, + IN COOR cRemove, + IN COOR cLocation) +/** + +Routine description: + + Bitboard equivalent of ExposesCheck -- see that function's comment. + +Parameters: + + POSITION *pos : the board + COOR cRemove : the square where a piece hypothetically removed from + COOR cLocation : the square where the attackee is sitting + +Return value: + + COOR : the location of an attacker piece or 0x88 (!IS_ON_BOARD) if + the removal of cRemove does not expose check. + +**/ +{ + int iIndex = (int)cLocation - (int)cRemove; + int iDelta; + BITBOARD bbOccupiedWithoutRemove; + COOR cBlocker; + + ASSERT(IS_ON_BOARD(cRemove)); + ASSERT(IS_ON_BOARD(cLocation)); + ASSERT(!IS_EMPTY(pos->rgSquare[cLocation].pPiece)); + + // + // If there is no way for a queen sitting at the square removed to + // reach the square we are testing (i.e. the two squares are not + // on the same rank, file, or diagonal) then there is no way + // removing it can expose cLocation to check. + // + if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN))) + { + return(ILLEGAL_COOR); + } + iDelta = CHECK_DELTA_WITH_INDEX(iIndex); + + bbOccupiedWithoutRemove = _BuildFullOccupiedBB(pos) & ~COOR_TO_BB(cRemove); + cBlocker = _NearestBlockerAlongRayBB(cLocation, iDelta, + bbOccupiedWithoutRemove); + return _ValidateExposedBlockerBB(pos, cBlocker, cLocation); +} + +COOR +ExposesCheckEpBB(IN POSITION *pos, + IN COOR cTest, + IN COOR cIgnore, + IN COOR cBlock, + IN COOR cKing) +/** + +Routine description: + + Bitboard equivalent of ExposesCheckEp -- see that function's + comment. Two squares are excluded from occupancy (cTest, the + captured pawn; cIgnore, the capturing pawn's origin) and one is + forced occupied (cBlock, the capturing pawn's destination) -- + matching the mailbox version's own "pretend the en passant capture + already happened" bookkeeping. + +Parameters: + + POSITION *pos, + COOR cTest : the square the attack would come from + COOR cIgnore : ignore this square, the pawn moved + COOR cBlock : this square is where the pawn moved to and now blocks + COOR cKing : the square under attack + +Return value: + + COOR + +**/ +{ + int iIndex = (int)cKing - (int)cTest; + int iDelta; + BITBOARD bbOccupied; + COOR cBlocker; + + ASSERT(IS_ON_BOARD(cTest)); + ASSERT(IS_ON_BOARD(cIgnore)); + ASSERT(IS_ON_BOARD(cBlock)); + ASSERT(IS_ON_BOARD(cKing)); + + if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN))) + { + return(ILLEGAL_COOR); + } + iDelta = CHECK_DELTA_WITH_INDEX(iIndex); + + bbOccupied = (_BuildFullOccupiedBB(pos) & + ~COOR_TO_BB(cTest) & ~COOR_TO_BB(cIgnore)) | + COOR_TO_BB(cBlock); + cBlocker = _NearestBlockerAlongRayBB(cKing, iDelta, bbOccupied); + + // cBlock is forced-occupied above purely to stop the ray there -- + // it may not really hold a piece yet (pre-move state), so it must + // be recognized and treated as "safe" directly, not run through + // pos->rgSquare's real (stale) contents, exactly like the mailbox + // version's explicit early check. + if (cBlocker == cBlock) + { + return(ILLEGAL_COOR); + } + return _ValidateExposedBlockerBB(pos, cBlocker, cKing); +} + +COOR FasterExposesCheck(POSITION *pos, COOR cRemove, COOR cLocation) @@ -282,7 +599,85 @@ Return value: } -FLAG +// +// board_representation/MOVEGEN_MIGRATION.md section 6b: bitboard +// equivalent of IsAttacked/InCheck. IsAttacked's own direct callers +// (move.c/san.c castling-through-check legality) are cold, but InCheck +// -- a thin wrapper around it -- has ~70 call sites across +// search.c/move.c/eval.c/root.c/dynamic.c, called constantly through +// search; this is a much hotter target than its direct-caller count +// alone would suggest. +// +// Design: _WhoAttacksSquareBB (see.c, already exposed) already answers +// "which of uSide's knight/bishop/rook/queen/king pieces attack this +// square" via the same magic-table substrate as everything else in +// this migration -- IsAttackedBB is just that function reduced to a +// boolean, plus a separate pawn-attack check via g_PawnAttackOriginBB +// (since _WhoAttacksSquareBB deliberately excludes pawns -- see its +// own header comment). +// +FLAG +IsAttackedBB(IN COOR cTest, IN POSITION *pos, IN ULONG uSide) +/** + +Routine description: + + Bitboard equivalent of IsAttacked -- see that function's comment. + +Parameters: + + COOR cTest : the square we want to determine if is under attack + POSITION *pos : the board + ULONG uSide : the side we want to see if is attacking cTest + +Return value: + + FLAG : TRUE if uSide attacks cTest, FALSE otherwise + +**/ +{ + BITBOARD bbOccupied; + + ASSERT(IS_ON_BOARD(cTest)); + ASSERT(IS_VALID_COLOR(uSide)); + + bbOccupied = _BuildFullOccupiedBB(pos); + if (0 != _WhoAttacksSquareBB(pos, cTest, uSide, bbOccupied)) + { + return(TRUE); + } + return(0 != (g_PawnAttackOriginBB[uSide][cTest] & pos->bbPawns[uSide])); +} + +FLAG +InCheckBB(IN POSITION *pos, IN ULONG uSide) +/** + +Routine description: + + Bitboard equivalent of InCheck -- see that function's comment. + +Parameters: + + POSITION *pos : the board + ULONG uSide : the side we want to determine if is in check + +Return value: + + FLAG : TRUE if side is in check, FALSE otherwise. + +**/ +{ + COOR cKingLoc = pos->cNonPawns[uSide][0]; + + ASSERT(IS_VALID_COLOR(uSide)); + ASSERT(IS_KING(pos->rgSquare[cKingLoc].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cKingLoc].pPiece) == uSide); + + return IsAttackedBB(cKingLoc, pos, FLIP(uSide)); +} + +FLAG IsAttacked(COOR cTest, POSITION *pos, ULONG uSide) /** |
