diff options
| author | Scott Gasch <[email protected]> | 2026-10-07 08:17:51 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-07 08:17:51 -0700 |
| commit | d14dbe2a50c220b2adac758c3d4f1a1fa67ece0b (patch) | |
| tree | cbf6d9928bcc41ef833b62b70479113f7d419925 | |
| parent | 447130db3df87b40d080dc26429d45e9213c19f7 (diff) | |
Speed up eval/move-gen/SEE; remove EvalLookForDanger, overhaul _EvalKing
SPRT-confirmed improvement (1814 games, st 4.0): candidate nps=1498118
vs head=1378448, avg depth 14.30 vs 13.87, ELO=+24.2+/-16.0,
LOWER95=0.5118, LLR=+3.126 crossing the H1 bound.
Most of the diff is non-semantic speed work across eval.c, generate.c,
movesup.c, see.c, probe.c and friends. Two real semantic changes:
removed EvalLookForDanger and overhauled _EvalKing's king-safety scoring.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
| -rw-r--r-- | src/GNUmakefile | 10 | ||||
| -rwxr-xr-x | src/chess.h | 135 | ||||
| -rw-r--r-- | src/data.c | 5 | ||||
| -rw-r--r-- | src/eval.c | 582 | ||||
| -rw-r--r-- | src/generate.c | 725 | ||||
| -rw-r--r-- | src/main.c | 6 | ||||
| -rw-r--r-- | src/movesup.c | 275 | ||||
| -rw-r--r-- | src/probe.c | 35 | ||||
| -rw-r--r-- | src/recogn.c | 4 | ||||
| -rw-r--r-- | src/root.c | 5 | ||||
| -rw-r--r-- | src/search.c | 37 | ||||
| -rw-r--r-- | src/searchsup.c | 29 | ||||
| -rw-r--r-- | src/see.c | 111 | ||||
| -rw-r--r-- | src/split.c | 7 | ||||
| -rw-r--r-- | src/testgenerate.c | 322 | ||||
| -rw-r--r-- | src/testsee.c | 155 | ||||
| -rw-r--r-- | src/util.c | 6 |
17 files changed, 2019 insertions, 430 deletions
diff --git a/src/GNUmakefile b/src/GNUmakefile index 14fa65a..8a4904b 100644 --- a/src/GNUmakefile +++ b/src/GNUmakefile @@ -9,7 +9,6 @@ # BOUNDS_CHECKING=1: make version with bounds checking enabled # MP=1: make version for multi-processor machine # DUMP_TREE=1: internal tree dumping version -# SEE_HEAPS=1: use minheaps in the SEE code # OSX=1: build for an Intel-based Apple Mac (omit this for FreeBSD/Linux) # USE_READLINE=1: link against the GNU readline library # SIXTYFOUR=1: make an X64 binary @@ -26,12 +25,12 @@ # match_play.py). The mailbox code is still fully present and # compiled either way -- this flag only flips which side of each # macro-swap wins, it doesn't remove anything. -# EVERYTHING=1: everything everything everything everything # GPROF=1: instrument with -pg for gprof(1)-based profiling (see # PROFILE.md) -- root-free on both FreeBSD and Linux, unlike # hardware-counter-based profiling (pmcstat/perf). Requires a # `gmake clean` first/after, since -pg changes codegen for every # translation unit. +# EVERYTHING=1: everything everything everything everything # # $Id$ # @@ -132,7 +131,8 @@ ifndef DISABLE_BITBOARD_MOVEGEN -DGENERATE_ROOK_BITBOARD -DGENERATE_BISHOP_BITBOARD \ -DGENERATE_QUEEN_BITBOARD -DGENERATE_PAWN_BITBOARD \ -DGENERATE_ESCAPES_KING_BITBOARD -DGENERATE_ESCAPES_BLOCK_BITBOARD \ - -DEXPOSESCHECK_BITBOARD -DISATTACKED_BITBOARD + -DEXPOSESCHECK_BITBOARD -DISATTACKED_BITBOARD \ + -DUNBLOCKED_BITBOARD endif ifdef EVERYTHING @@ -324,7 +324,7 @@ FATHOM_CFLAGS = -std=gnu99 -O2 -Ifathom $(filter -m32 -m64,$(PROFILE)) # # ---> .o, not .c! <--- -# +# OBJS = main.o root.o search.o searchsup.o draw.o dynamic.o \ hash.o eval.o x86.o pawnhash.o bitboard.o \ generate.o see.o move.o movesup.o command.o script.o \ @@ -334,7 +334,7 @@ OBJS = main.o root.o search.o searchsup.o draw.o dynamic.o \ ifdef TEST # ---> .o, not .c! <--- -# +# OBJS += testdraw.o testmove.o testfen.o testgenerate.o \ testsan.o testics.o testeval.o testbitboard.o \ testhash.o testsee.o testsearch.o testsup.o \ diff --git a/src/chess.h b/src/chess.h index d637905..fc2ef85 100755 --- a/src/chess.h +++ b/src/chess.h @@ -778,7 +778,6 @@ typedef struct _POSITION BITBOARD bbTrappedEscapes[2][6]; ULONG uNumTrapped[2]; ULONG uArmyScaler[2]; - ULONG uClosedScaler; SCORE iScore[2]; SCORE iReducedMaterialDownScaler[2]; ULONG uMinorsAtHome[2]; @@ -833,7 +832,7 @@ typedef struct _KEY_POINTER } KEY_POINTER; -typedef union _GENERATOR_FLAGS +typedef union _GENERATOR_FLAGS { struct { UCHAR uMoveCount; @@ -868,6 +867,31 @@ typedef struct _MOVE_STACK // ULONG uUnblockedKeyValue[MAX_PLY_PER_SEARCH]; KEY_POINTER sUnblocked[MAX_PLY_PER_SEARCH][128]; + // _FindUnblockedSquaresBB's output: one bit per square unblocked + // for the opposing king this ply (membership only -- the + // direction-back-to-king half of the old sUnblocked table is + // recomputed on demand by DIRECTION_BETWEEN_SQUARES, a table + // lookup in release builds, so it needs no storage of its own). + // Kept unconditional (not #ifdef UNBLOCKED_BITBOARD-gated) so + // testgenerate.c's harness can exercise _FindUnblockedSquaresBB + // and compare it against the mailbox table regardless of which + // toggle this build was compiled with. + BITBOARD bbUnblocked[MAX_PLY_PER_SEARCH]; + // Which generation (uUnblockedKeyValue[uPly] value) bbUnblocked[uPly] + // was last actually built for -- WouldGiveCheck (generate.c) compares + // this against the live uUnblockedKeyValue[uPly] to decide whether + // the cached bitboard is still valid for the node now occupying this + // ply, or needs a lazy rebuild. GenerateMoves no longer builds + // bbUnblocked eagerly; it just bumps uUnblockedKeyValue[uPly], same + // as root.c's init comment there describes -- the actual build is + // deferred to WouldGiveCheck's first call this generation, so a node + // whose every candidate move gets rejected before ever needing + // WouldGiveCheck (e.g. every capture futility-pruned in QSearch) + // never pays for it at all. Starts zeroed; every real ticket in + // uUnblockedKeyValue starts at 1 or higher (root.c) and only + // increases, so 0 is a permanently safe "never built yet" sentinel + // -- no special seeding needed here, unlike that field. + ULONG uUnblockedBuiltFor[MAX_PLY_PER_SEARCH]; // // The main move list, a long series of moves, their values and some @@ -922,6 +946,20 @@ MOVE_STACK; #define GENERATE_CAPTURES_PROMS_CHECKS (3) #define GENERATE_CAPTURES_PROMS (4) #define GENERATE_DONT_SCORE (5) + +// generate.c's _GenerateCapturesAndPromotions (GENERATE_CAPTURES_PROMS's +// direct, non-filtered generator) exists only when every piece type is +// on its bitboard generator -- see that function's block comment in +// generate.c. Declared here, not in generate.c, so every translation +// unit that needs to know whether it exists (main.c's test-registration +// list, testgenerate.c's own tests) sees the same answer generate.c's +// GenerateMoves dispatch does. +#if defined(GENERATE_KNIGHT_BITBOARD) && defined(GENERATE_BISHOP_BITBOARD) && \ + defined(GENERATE_ROOK_BITBOARD) && defined(GENERATE_QUEEN_BITBOARD) && \ + defined(GENERATE_KING_BITBOARD) && defined(GENERATE_PAWN_BITBOARD) +#define CAPTURES_PROMS_BITBOARD_PATH 1 +#endif + #ifdef TEST #define GENERATE_ALL_MOVES_CHECK_OK (6) #endif @@ -1138,6 +1176,18 @@ typedef struct _COUNTERS } kingsafetyhash; + // Separate from kingsafetyhash above -- _GetKingActionBonus's + // cache (eval.c) is keyed and populated completely independently + // (see KING_SAFETY_HASH_ENTRY's u64ActionKey comment), so its own + // hit rate needs its own counters rather than conflating with + // _GetFileStormDefects'. + struct + { + UINT64 u64Probes; + UINT64 u64Hits; + } + kingactionhash; + struct { UINT64 u64TotalNodeCount; @@ -1746,6 +1796,21 @@ typedef struct _PAWN_HASH_ENTRY UCHAR uCountPerFile[2][10]; UCHAR uNumRammedPawns; UCHAR uNumUnmovedPawns[2]; + // Total pawns on the board, both colors -- _EvalRook indexes + // ROOK_VALUE_AS_PAWNS_COME_OFF with this once per rook (up to 4x + // per Eval() call); also feeds uClosedScaler's own formula below, + // in both cases replacing a fresh FastCountBits(bbPawns[WHITE] | + // bbPawns[BLACK]) with a value this entry already has cached. + UCHAR uTotalPawnCount; + // "How closed/busy is the board" scaler _EvalKnight indexes + // KNIGHT_IN_CLOSED_POSITION with (eval.c's EvalPawns computes the + // formula) -- used to live as a POSITION field recomputed from + // scratch on every Eval() call even though every input (total + // pawn count, uNumUnmovedPawns, uNumRammedPawns) is already pure + // pawn-structure data this same entry is keyed on. Same "finish + // moving a pawn-only term into the pawn hash" pattern as + // iBishopColorScore below and 5fbd935's original move. + UCHAR uClosedScaler; SHORT iBishopColorScore[2][2]; // [color][IS_SQUARE_WHITE(bishop's // square)] -- good/bad-bishop // pawn-structure score, pure @@ -1771,6 +1836,17 @@ typedef struct _KING_SAFETY_HASH_ENTRY // reports old counter units into KING_DEFECT_DUMP_INFO, unrelated // to this cache. SCORE iFileStormDefects[2]; + // Independent validity tag for iActionBonus below, decoupled from + // u64Key above on purpose: _GetKingActionBonus (eval.c) is called + // from the opposite half of _EvalKing's safety/activity split from + // _GetFileStormDefects (only when the ENEMY lacks mating material, + // where file-storm's own populate-on-miss never runs), so a hit on + // u64Key would say nothing about whether iActionBonus was ever + // actually computed for this key. Per-king (keyed on pawn sig + + // just THIS king's own square, not both kings'), unlike + // iFileStormDefects -- see _GetKingActionBonus's header comment. + UINT64 u64ActionKey; + SCORE iActionBonus; } KING_SAFETY_HASH_ENTRY; @@ -2799,7 +2875,7 @@ InCheck(POSITION *pos, ULONG uSide); FLAG IsAttackedBB(COOR cTest, POSITION *pos, ULONG uSide); -FLAG +FLAG FORCEINLINE InCheckBB(POSITION *pos, ULONG uSide); // ISATTACKED_BITBOARD defined -> the BB versions above resolve plain @@ -2828,10 +2904,10 @@ StripMove(CHAR *szMove); ULONG LooksLikeMove(CHAR *szData); -void FASTCALL +void FORCEINLINE SelectBestWithHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u); -void FASTCALL +void FORCEINLINE SelectBestNoHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u); void FASTCALL @@ -2942,6 +3018,24 @@ _RookAttacksBB(COOR c, BITBOARD bbOccupied); BITBOARD _BishopAttacksBB(COOR c, BITBOARD bbOccupied); +// generate.c's bitboard replacement for _FindUnblockedSquares, exposed +// non-static so testgenerate.c's correctness/speed harness can call it +// directly regardless of whether UNBLOCKED_BITBOARD is defined for +// this build. +void +_FindUnblockedSquaresBB(MOVE_STACK *pStack, + POSITION *pos); + +// The mailbox original, exposed non-static (compiled whenever +// !UNBLOCKED_BITBOARD, or under DEBUG/TEST for the cross-check and +// the test harness above) under the same build conditions generate.c +// itself uses -- see generate.c. +#if !defined(UNBLOCKED_BITBOARD) || defined(DEBUG) || defined(TEST) +void +_FindUnblockedSquares(MOVE_STACK *pStack, + POSITION *pos); +#endif + void _GenerateRookBB(MOVE_STACK *pStack, POSITION *pos, @@ -3057,7 +3151,7 @@ GenerateWhiteKing(MOVE_STACK *pStack, POSITION *pos, COOR cKing); -FLAG +FLAG FORCEINLINE WouldGiveCheck(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv); @@ -3095,6 +3189,9 @@ TestGenerateQueenSpeed(void); void TestGenerateAllMovesSpeed(void); +void +TestFindUnblockedSquaresSpeed(void); + #endif // @@ -3148,6 +3245,7 @@ extern MOVE HASHMOVE; extern MOVE RECOGNMOVE; extern MOVE DRAWMOVE; extern MOVE MATEMOVE; +extern MOVE EGTBMOVE; extern FLAG g_fIsWhiteSquare[128]; extern BITBOARD BBFILE[8]; extern BITBOARD BBRANK[9]; @@ -3707,7 +3805,7 @@ CanWeReduceOrPruneMove(IN SEARCHER_THREAD_CONTEXT *ctx, IN FLAG fThreat, IN FLAG fIsLeftoverMove); -INT +INT FORCEINLINE GetLMRReduction(IN SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uRemainingDepth, IN ULONG uLegalMoves, @@ -3789,7 +3887,7 @@ GetMoveSee(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv, IN ULONG uMoveNum); -void +void FORCEINLINE ComputeMoveExtension(IN OUT SEARCHER_THREAD_CONTEXT *ctx, IN SCORE iAlpha, IN SCORE iBeta, @@ -3813,7 +3911,7 @@ MateDistancePruningCutoff(IN ULONG uPly, IN OUT SCORE *piAlpha, IN OUT SCORE *piBeta); -FLAG +FLAG FORCEINLINE CommonSearchInit(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piAlpha, IN OUT SCORE *piBeta, @@ -3849,14 +3947,25 @@ TestQSearchSanity(void); // #define SEE_HEAPS -SCORE +SCORE FORCEINLINE SEE(POSITION *pos, MOVE mv); +// The real exchange-walk SEE implementation, with no early exit -- +// see SEE()'s own header comment in see.c for the cheap no-defender +// check that now sits in front of this. Exposed non-static (unlike +// its historical merged-into-SEE status) so testsee.c's +// TestSEENoDefenderFastPath can call it directly by name for an +// exact-value correctness/speed comparison against SEE(), same +// convention as _GenerateAllMoves/_GenerateAllMovesBB. +SCORE +_SEEFull(POSITION *pos, + MOVE mv); + // Sign-only fast path for SEE(pos, mv) >= 0 -- see its own header // comment in see.c. Exposed non-static so GetMoveSee (searchsup.c) can // call it and testsee.c can diff it against real SEE(). -FLAG +FLAG FORCEINLINE SeeFastSign(POSITION *pos, MOVE mv); @@ -4449,7 +4558,7 @@ _GetAttacksBB(SEE_LIST *pList, // call it with a king-vacated occupancy bitboard. Deliberately // excludes pawns -- see its own header comment in see.c; callers // needing pawn attacks must check g_PawnAttackOriginBB separately. -BITBOARD +BITBOARD FORCEINLINE _WhoAttacksSquareBB(POSITION *pos, COOR cSquare, ULONG uSide, @@ -4807,7 +4916,7 @@ GenerateRandomLegalSymetricPosition(POSITION *pos); void InitializeInteriorNodeRecognizers(void); -ULONG +ULONG FORCEINLINE RecognLookup(SEARCHER_THREAD_CONTEXT *ctx, SCORE *piScore, ULONG uEgtbProbeMode); @@ -40,6 +40,7 @@ MOVE HASHMOVE = {0x11118888}; MOVE RECOGNMOVE = {0x22228888}; MOVE DRAWMOVE = {0x33338888}; MOVE MATEMOVE = {0x44448888}; +MOVE EGTBMOVE = {0x55558888}; // Is a square white? FLAG g_fIsWhiteSquare[128]; @@ -1056,7 +1057,7 @@ _MagicFindAndBuildForSquare(COOR c, BITBOARD mask, const int iDelta[4], BITBOARD *pMagic, ULONG *pShift, BITBOARD **ppTable) { - ULONG uBits = CountBits(mask); + ULONG uBits = FastCountBits(mask); ULONG uSize = 1UL << uBits; ULONG uShift = 64 - uBits; BITBOARD *rgbbOccupancy = malloc(sizeof(BITBOARD) * uSize); @@ -1089,7 +1090,7 @@ _MagicFindAndBuildForSquare(COOR c, BITBOARD mask, const int iDelta[4], // when multiplied against the mask rarely yields a // collision-free hash -- a cheap filter to skip obviously bad // candidates before paying for the full uSize-entry pass. - if (CountBits((UINT64)(mask * uMagic) & 0xFF00000000000000ULL) < 6) + if (FastCountBits((UINT64)(mask * uMagic) & 0xFF00000000000000ULL) < 6) { continue; } @@ -280,13 +280,13 @@ static SCORE DOUBLED_PAWN_PENALTY_BY_COUNT[4][9] = static SCORE ISOLATED_PAWN_BY_PAWNFILE[9] = { - 0, -7, -8, -9, -10, -10, -9, -8, -7 + 0, -8, -9, -10, -10, -10, -10, -9, -8 }; static SCORE ISOLATED_EXPOSED_PAWN = -5; // Turned down (2026-08-30): a per-pawn modifier that stacks on top of -// DOUBLED_PAWN_PENALTY_BY_COUNT's whole-position doubled-pawn aggregate +//< DOUBLED_PAWN_PENALTY_BY_COUNT's whole-position doubled-pawn aggregate // for the specific case of a pawn that's both isolated and doubled -- // a different angle (single-worst-case flag vs. whole-position // severity), not the same fact counted twice, so scaled down rather @@ -385,7 +385,7 @@ SCORE RACER_WINS_RACE = +300; static SCORE UNDEVELOPED_MINORS_IN_OPENING[5] = {// 0 1 2 3 4 - 0, -6, -10, -16, -22 + 0, -8, -14, -20, -23 }; // @@ -722,16 +722,19 @@ static SCORE KING_TO_CENTER[128] = // the other side's king safety. Note: this doesn't incl pawn // material. // +// 2026-10-06: also now the single dividing line for _EvalKing's whole +// safety-vs-activity split (see that function's header comment) -- +// below this, the king is actively encouraged out (KING_TO_CENTER) +// instead of just not being penalized for leaving. The separate, +// stricter KEEP_KING_AT_HOME_THRESHOLD used to gate the stay-home +// bonus on its own, independently of this one, leaving a dead band +// between the two thresholds where safety was scored but neither the +// stay-home bonus nor any come-out encouragement applied. Retired -- +// the stay-home bonus now uses this same threshold, so "safety scored" +// and "stay-home bonus applies" are exactly the same condition. #define DO_KING_SAFETY_THRESHOLD \ (VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1) -// -// If a side has less than this much material the other side's king -// can come out. Note: this doesn't incl pawn material. -// -#define KEEP_KING_AT_HOME_THRESHOLD \ - (VALUE_ROOK + VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1) - static ULONG KING_COUNTER_BY_ATTACK_PATTERN[32] = { //p 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 @@ -2269,6 +2272,17 @@ Return value: FastCountBits(pos->bbPawns[WHITE] & (pos->bbPawns[BLACK] << 8)); ASSERT(pHash->uNumRammedPawns <= 8); + pHash->uTotalPawnCount = (UCHAR)FastCountBits(bbAllPawns); + ASSERT(pHash->uTotalPawnCount <= 16); + + // _EvalKnight's closed-position scaler, computed once per unique + // pawn structure instead of once per Eval() call -- see this + // field's comment in chess.h. + pHash->uClosedScaler = (UCHAR)(pHash->uTotalPawnCount - + (pHash->uNumUnmovedPawns[WHITE] + pHash->uNumUnmovedPawns[BLACK]) + + pHash->uNumRammedPawns); + ASSERT(pHash->uClosedScaler <= 32); + // Good/bad bishop pawn-structure score, cached per (color, color- // complex) instead of rescanned by every bishop of that color/ // complex in _EvalBishop (board_representation/EVAL.md-style note: @@ -3002,6 +3016,120 @@ Return value: } +static SCORE +_GetKingActivityBonus(IN SEARCHER_THREAD_CONTEXT *ctx, + IN POSITION *pos, + IN const PAWN_HASH_ENTRY *pHash, + IN ULONG uColor, + IN COOR cKing) +/** + +Routine description: + + Cache front end for _EvalKing's activity half (2026-10-06 safety/ + activity split): combines "supporting own passer" and "is this + king out of the action" into one cached lookup, since both only + ever apply together (same caller, same branch) and both depend on + exactly the same inputs -- pawn structure plus uColor's own king + square, nothing else. One key computation and one hash probe + serve both instead of two. Only called when the ENEMY (xColor) is + down to at most one non-king piece -- see _EvalKing. + + Per-king, not per-position: unlike _GetFileStormDefects (keyed on + both kings' squares, since "is the king's position safe" depends + on both), out-of-action's old formula subtracted the enemy king's + own distance to make a symmetric "who's closer" value, but that + only made sense paired with the old combined-both-sides gate + (uTotalNonPawnsBothSides==2, true or false identically for both + kings' calls). The new gate is per-call (keyed on xColor, each + king's own enemy), so the two calls can legitimately disagree -- + one king's own position might qualify while the other's doesn't -- + and a shared symmetric value can no longer be "added once, during + white's call" the way the old code assumed. Each call now computes + and caches its own independent value instead. + + Narrower key (just this king's square, not both) than + _GetFileStormDefects's as a direct consequence -- only this king's + own moves can invalidate it, not the opponent's too, so this + should measure a *better* hit rate than file-storm's own ~75-85% + in this same low-material regime, not a worse one. + + PROVISIONAL, like the formulas it combines: neither weight has + been through any self-play validation -- see the old out-of-action + formula's own removed comment (git history) for the outlier bug + that originally motivated shrinking its weight. Needs the same + calibration discipline as every other king-safety magnitude in + this file. Also trades away per-passer-square EVAL_TERM attribution + (summed into one combined value instead) -- EVAL_DUMP tracing + only, not used in release builds, by direct request for fewer + instructions in this inner loop. + +Parameters: + + SEARCHER_THREAD_CONTEXT *ctx + POSITION *pos + PAWN_HASH_ENTRY *pHash + ULONG uColor : whose king this bonus is for + COOR cKing : uColor's own king square (caller already has it -- + KingPosition(pos, uColor) redundantly) + +Return value: + + SCORE + +**/ +{ + UINT64 u64Key = pos->u64PawnSig ^ g_u64SigSeeds[cKing][KING][uColor]; + ULONG uSlot = (ULONG)u64Key & (KING_SAFETY_HASH_TABLE_SIZE - 1); + KING_SAFETY_HASH_ENTRY *pEntry = &ctx->rgKingSafetyHash[uSlot]; + SCORE i, iTotal; + COOR cSquare; + BITBOARD bb; + + INC(ctx->sCounters.kingactionhash.u64Probes); + if (pEntry->u64ActionKey == u64Key) + { + INC(ctx->sCounters.kingactionhash.u64Hits); + return pEntry->iActionBonus; + } + + // Supporting own passer -- bb here is exactly the own-passer + // squares at king-move distance 1 (g_KingAttacksBB is that mask + // by construction), so every popped square counts; no separate + // DISTANCE() filter or ASSERTs needed. The extra "leading the + // pawn toward promotion" bonus applies iff the passer square is + // in the king's own back ring (g_KingRingBackBB) -- side- and + // front-ring passers still get the base rank-scaled support + // bonus, just not this extra. + iTotal = 0; + bb = pHash->bbPasserLocations[uColor] & g_KingAttacksBB[cKing]; + while (IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) + { + i = 2; + i += KING_SUPPORTING_OWN_PASSER_BY_RANK[uColor][RANK(cSquare)]; + i += 8 * ((COOR_TO_BB(cSquare) & g_KingRingBackBB[uColor][cKing]) != 0); + iTotal += i; + } + + // Out of action in KP-ish endgames, e.g. + // 8/8/1p6/p6k/P3K3/8/1P6/8 w - - 0 11 -- how close this king is + // to the whole pawn mass. The *2 preserves the old formula's own + // scale (it used to be a differential, this is now a plain sum, + // but the per-pawn magnitude is unchanged). + i = 0; + bb = pos->bbPawns[WHITE] | pos->bbPawns[BLACK]; + while (IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) + { + i -= (SCORE)DISTANCE(cSquare, cKing); + } + iTotal += i * 2; + + pEntry->u64ActionKey = u64Key; + pEntry->iActionBonus = iTotal; + return iTotal; +} + + #ifdef CALIBRATE_POSITIONAL // // board_representation/EVAL.md section 9 recalibration (2026-09-05): @@ -4557,12 +4685,12 @@ Return value: "board centrality"); // Give a bonus to knights on a closed / busy board. - ASSERT(pos->uClosedScaler <= 32); + ASSERT(pHash->uClosedScaler <= 32); EVAL_TERM(uColor, KNIGHT, c, pos->iScore[uColor], - KNIGHT_IN_CLOSED_POSITION[pos->uClosedScaler], + KNIGHT_IN_CLOSED_POSITION[pHash->uClosedScaler], "in closed/open position"); // Outposted knights. See if square c is safe from enemy pawns; @@ -4786,12 +4914,10 @@ Return value: ROOK, c, pos->iScore[uColor], - ROOK_VALUE_AS_PAWNS_COME_OFF[ - FastCountBits(pos->bbPawns[BLACK] | pos->bbPawns[WHITE]) - ], + ROOK_VALUE_AS_PAWNS_COME_OFF[pHash->uTotalPawnCount], "increase in value/pawns"); - ASSERT((FastCountBits(pos->bbPawns[uColor]) + FastCountBits(pos->bbPawns[xColor])) == - (FastCountBits(pos->bbPawns[uColor] | pos->bbPawns[xColor]))); + ASSERT(pHash->uTotalPawnCount == + (FastCountBits(pos->bbPawns[uColor]) + FastCountBits(pos->bbPawns[xColor]))); // // Rook mobility (and update attack tables) -- board_representation/ @@ -5104,47 +5230,52 @@ Return value: } -static void -_EvalKing(IN OUT POSITION *pos, - IN const COOR c, - IN const PAWN_HASH_ENTRY *pHash, - IN SEARCHER_THREAD_CONTEXT *ctx, - IN const ULONG uTotalNonPawnsBothSides) +static SCORE FORCEINLINE +_EvalKingSafety(IN POSITION *pos, + IN const COOR c, + IN const PAWN_HASH_ENTRY *pHash, + IN SEARCHER_THREAD_CONTEXT *ctx, + IN const ULONG uColor, + IN const ULONG xColor, + IN const BITBOARD bbRing) /** Routine description: + Safety half of _EvalKing's 2026-10-06 safety-vs-activity split -- + see that function's header comment. Full ring-attack/danger- + pattern scoring, file-storm defects (cached), stay-on-initial- + square bonus, castling/development. Only called when the enemy + has enough material (DO_KING_SAFETY_THRESHOLD) to make an attack + worth scoring. FORCEINLINE: this and _EvalKingActivity are split + out of _EvalKing purely for readability -- _EvalKing calls exactly + one of the two per invocation (never both), so forcing both + inline reproduces the exact same generated code as the old single + monolithic function, not a real call. + Parameters: POSITION *pos, COOR c, PAWN_HASH_ENTRY *pHash, - ULONG uTotalNonPawnsBothSides : TotalNonPawnCount(pos, WHITE) + - TotalNonPawnCount(pos, BLACK), hoisted up to Eval() (chess.h's - TotalNonPawnCount is a POPCNT now, not a stored field -- see - its comment) since this function runs twice per Eval() call - (once per king) and used to recompute the identical sum both - times. + SEARCHER_THREAD_CONTEXT *ctx, + ULONG uColor, + ULONG xColor, + BITBOARD bbRing : g_KingAttacksBB[c], already computed by the caller Return value: - FLAG + SCORE **/ { - PIECE p; - ULONG uColor, xColor; - COOR cSquare; - ULONG u; BITV bvPattern; ULONG uQueenNearKing; - BITBOARD bb; - SCORE i; - SCORE iKingScore = 0; - #ifdef KING_DEFECT_DUMP - KING_DEFECT_DUMP_INFO *pDump; + COOR cSquare; + KING_DEFECT_DUMP_INFO *pDump = &ctx->sKingDefectDump[uColor]; #endif + SCORE iKingScore = 0; // 2026-09-15: linear-SCORE rewrite -- see king_safety_data/ // HANDOFF.md's 2026-09-15 addendum for the full rationale. Replaces @@ -5185,30 +5316,6 @@ Return value: // self-play regression, not the underlying feature signal itself. const ULONG uKingDangerAcceleratorBucketWidth = 56; - ASSERT(IS_ON_BOARD(c)); - p = pos->rgSquare[c].pPiece; - ASSERT(p && IS_KING(p)); - uColor = GET_COLOR(p); - xColor = FLIP(uColor); - ASSERT(IS_VALID_COLOR(uColor)); - - // Update attack bitvs for squares around the king. - BITBOARD bbRing = g_KingAttacksBB[c]; - pos->bbKingAttacks[uColor] |= bbRing; -#ifdef KING_DEFECT_DUMP - pDump = &ctx->sKingDefectDump[uColor]; - memset(pDump, 0, sizeof(KING_DEFECT_DUMP_INFO)); -#endif - - // Don't bother with king safety when the other side has no mating - // material. - u = pos->uNonPawnMaterial[xColor]; - ASSERT(u >= VALUE_KING); - if (u < DO_KING_SAFETY_THRESHOLD) - { - goto skip_safety; - } - // Depending on whether we are assessing a black or white king, // the "front" and "back" directions swap. Front is "towards // the center of the board at game start time". @@ -5377,11 +5484,6 @@ Return value: #endif // - // King's own file plus the two adjacent ones. EXPERIMENTAL, - // prototype-only (2026-09-05, board_representation/EVAL.md - // section 9's king-safety-hash discussion): now routed through - // _GetFileStormDefects's cache instead of computed inline. - // Already a direct SCORE as of the 2026-09-15 rewrite -- see // _ComputeFileStormDefects's comment. // iSafety += _GetFileStormDefects(ctx, pos, pHash, uColor); @@ -5393,15 +5495,15 @@ Return value: pDump->uFileStormStormingPawn; #endif - if (u >= KEEP_KING_AT_HOME_THRESHOLD) - { - iSafety += KING_INITIAL_LOCATION_SCALE * - KING_EVAL_INITIAL_LOCATION[uColor][c]; - } + // Unconditional within this whole function now -- KEEP_KING_AT_HOME_ + // THRESHOLD (a separate, stricter bar than DO_KING_SAFETY_THRESHOLD) + // used to gate this specifically, leaving a dead band between the + // two. Retired; see DO_KING_SAFETY_THRESHOLD's comment. + iSafety += KING_INITIAL_LOCATION_SCALE * + KING_EVAL_INITIAL_LOCATION[uColor][c]; #ifdef KING_DEFECT_DUMP pDump->uInitialCounterByLocation = - (u >= KEEP_KING_AT_HOME_THRESHOLD) ? - (ULONG)KING_EVAL_INITIAL_LOCATION[uColor][c] : 0; + (ULONG)KING_EVAL_INITIAL_LOCATION[uColor][c]; #endif // Danger-magnitude acceleration -- see KING_DANGER_ACCELERATOR's @@ -5454,138 +5556,255 @@ Return value: pDump->uAttackerArmyScaler = pos->uArmyScaler[xColor]; #endif - // Bonus for castling / penalty for loss of castle. Also, if side has - // not yet castled, be concerned with undeveloped minor pieces. + // Bonus for castling / penalty for loss of castle. Also, if + // side has not yet castled AND castling is still a live + // option, be concerned with undeveloped minor pieces. + // + // 2026-10-06: split the development penalty's gate from the + // missing-castle-option penalty's -- they need different + // conditions. Development should stop once the castling + // decision is permanently settled, whether that's by + // castling (the outer !fCastled check) or by losing both + // rights without ever castling (not caught by that check + // alone -- a side that never castles and never loses the + // right would otherwise be flagged "undeveloped" forever). + // The missing-castle-option penalty must do the opposite -- + // persist *after* a right is lost, not stop then -- since + // its whole purpose is discouraging throwing the right away + // in the first place; gating it the same way development is + // gated would silently disable it exactly when it's supposed + // to fire. + // + // Color-indexed instead of a WHITE/BLACK branch over two + // copy-pasted blocks -- one fewer per-piece branch for the + // processor to mispredict (board_representation/EVAL.md + // section 9). if (FALSE == pos->fCastled[uColor]) { ULONG uDevelopmentScaler = CASTLE_AND_DEVELOPMENT_SCALER[pos->uArmyScaler[uColor]]; if (uDevelopmentScaler > 0) { + static const BITV CASTLE_SHORT[2] = + { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT }; + static const BITV CASTLE_LONG[2] = + { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG }; + BITV bvBothRights = CASTLE_SHORT[uColor] | CASTLE_LONG[uColor]; + // One popcount answers both "can this side still + // castle at all" (>0, gates development below) and + // "how many options are missing" (2-count, scales + // the persistent penalty below) -- replaces two + // separate (==0) branches/multiplies/EVAL_TERM calls + // with one shared count. + ULONG uRightsRemaining = + FastCountBits(pos->bvCastleInfo & bvBothRights); + ASSERT(uRightsRemaining <= 2); + + // Branchless: (uRightsRemaining > 0) is 0 or 1, multiplied + // straight into the term instead of an `if` -- nothing + // left to mispredict. ASSERT(pos->uMinorsAtHome[uColor] <= 4); EVAL_TERM(uColor, 0, ILLEGAL_COOR, pos->iScore[uColor], - (SCORE)((UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] * + (SCORE)(((uRightsRemaining > 0) * + UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] * (SCORE)uDevelopmentScaler) / 8), "development"); - // Color-indexed instead of a WHITE/BLACK branch over - // two copy-pasted blocks -- one fewer per-piece branch - // for the processor to mispredict (board_representation/ - // EVAL.md section 9). - static const BITV CASTLE_SHORT[2] = - { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT }; - static const BITV CASTLE_LONG[2] = - { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG }; - EVAL_TERM(uColor, - KING, - c, - iKingScore, - (SCORE)((KING_MISSING_ONE_CASTLE_OPTION * - ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0) * - (SCORE)uDevelopmentScaler) / 8), - "can't castle short"); + // Deliberately NOT gated on uRightsRemaining > 0 -- + // see this block's header comment. Fires at full + // weight (both options gone) even long after the + // rights were lost, for as long as uDevelopmentScaler + // itself stays nonzero. EVAL_TERM(uColor, KING, c, iKingScore, (SCORE)((KING_MISSING_ONE_CASTLE_OPTION * - ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0) * + (SCORE)(2 - uRightsRemaining) * (SCORE)uDevelopmentScaler) / 8), - "can't castle long"); + "missing castle option(s)"); #ifdef KING_DEFECT_DUMP pDump->fMissingCastleShort = ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0); pDump->fMissingCastleLong = ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0); #endif } } + return iKingScore; +} + + +static SCORE FORCEINLINE +_EvalKingActivity(IN POSITION *pos, + IN const COOR c, + IN const PAWN_HASH_ENTRY *pHash, + IN SEARCHER_THREAD_CONTEXT *ctx, + IN const ULONG uColor, + IN const ULONG xColor) +/** + +Routine description: + + Activity half of _EvalKing's 2026-10-06 safety-vs-activity split -- + see that function's header comment. KING_TO_CENTER unconditionally; + once the enemy is down to at most one non-king piece, the sharper + passer-support/in-the-action guidance too. Only called when the + enemy lacks mating material (below DO_KING_SAFETY_THRESHOLD). + FORCEINLINE -- see _EvalKingSafety's comment for why this costs + nothing over the old monolithic function. + +Parameters: + + POSITION *pos, + COOR c, + PAWN_HASH_ENTRY *pHash, + SEARCHER_THREAD_CONTEXT *ctx, + ULONG uColor, + ULONG xColor + +Return value: + + SCORE + +**/ +{ + SCORE i; + SCORE iKingScore = 0; - skip_safety: // - // Special code for kings in the endgame + // Come out. KING_TO_CENTER applies unconditionally (no longer + // gated by uTotalNonPawnsBothSides<8 -- that was a different, + // combined-both-sides unit than this whole split now uses, and + // left a gap between "safety stops mattering" and "centralizing + // is actually encouraged"; see _EvalKing's header comment). // - u = uTotalNonPawnsBothSides; - if (u < 8) + i = KING_TO_CENTER[c]; + EVAL_TERM(uColor, + KING, + c, + iKingScore, + i, + "centralize king"); + + // Sharper late-game guidance once the enemy is down to at most + // one non-king piece (lone king, K+minor, K+rook, even K+queen + // -- though a lone queen's own material value already exceeds + // DO_KING_SAFETY_THRESHOLD on its own, VALUE_QUEEN=975 > + // VALUE_ROOK+VALUE_BISHOP=800, so that specific case can never + // actually reach this function at all; the count check below is + // correct on its own terms regardless). Per-call/per-king + // (xColor's own material), not the old combined-both-sides + // uTotalNonPawnsBothSides==2 -- the two kings' own enemies can + // legitimately disagree now. + if (TotalNonPawnCount(pos, xColor) <= 2) { - // Encourage kings to come to the center when safe. - i = KING_TO_CENTER[c]; + // Supporting-own-passer and out-of-action-in-KP-endgames, + // combined into one cached lookup (_GetKingActivityBonus) + // -- both depend only on (pawn structure + this king's own + // square), both only ever apply in this same branch, so + // one key computation/hash probe serves both instead of + // two. Trades away per-passer-square EVAL_TERM attribution + // (EVAL_DUMP tracing only, not used in release builds) for + // fewer instructions in the inner loop, by direct request + // -- see git history for the old separated version if + // finer dump granularity is ever needed again. + i = _GetKingActivityBonus(ctx, pos, pHash, uColor, c); EVAL_TERM(uColor, KING, c, iKingScore, i, - "centralize king"); + "king activity (passer support + in/out of action)"); + } + return iKingScore; +} - // Kings in front of passers in the late endgame are strong... - // -- bb here is exactly the own-passer squares at king-move - // distance 1 (g_KingAttacksBB is that mask by construction), so - // every popped square counts; no separate DISTANCE() filter or - // ASSERTs needed. The extra "leading the pawn toward promotion" - // bonus applies iff the passer square is in the king's own - // back ring (g_KingRingBackBB) -- side- and front-ring passers - // still get the base rank-scaled support bonus, just not this - // extra. - bb = pHash->bbPasserLocations[uColor] & g_KingAttacksBB[c]; - while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) - { - i = 2; - i += KING_SUPPORTING_OWN_PASSER_BY_RANK[uColor][RANK(cSquare)]; - i += 8 * ((COOR_TO_BB(cSquare) & g_KingRingBackBB[uColor][c]) != 0); - EVAL_TERM(uColor, - KING, - c, - iKingScore, - i, - "supporting own passer"); - } - // - // Detect kings way out of the action in KP endgames: - // - // 8/8/1p6/p6k/P3K3/8/1P6/8 w - - 0 11 - // - if ((u == 2) && (uColor == WHITE)) - { - // 2026-09-16: this used to sum the raw per-pawn king- - // distance differential (each term in [-7,+7]) over EVERY - // pawn on the board, then multiply the whole sum by 12 -- - // unbounded in pawn count, so a genuine K+P endgame with - // 10+ pawns still on the board could reach several hundred - // centipawns from this one soft "my king is a bit more - // active" term (confirmed directly: CALIBRATE_MARGIN_ - // SAFETY-adjacent sampling turned up real +391/-384 - // outliers traced to exactly this line). That's worth more - // than a whole extra rook or bishop would be anywhere else - // in this eval, for a term that only ever fires when - // NEITHER side has a single piece left besides pawns (u==2 - // gate) -- clearly wrong on its face regardless of the - // exact right number. Kept the raw per-pawn sum (rather - // than normalizing to an average) deliberately -- more - // pawns still on the board is a real reason this term - // could matter more, not just noise to average away -- and - // instead shrunk the per-pawn weight from 12 to 2, so even - // an unrealistic worst case (16 pawns, every one at the - // max +/-7 distance differential) caps at 224 and realistic - // cases land around 40-80, versus the old 500-700+. - // PROVISIONAL: the "2" stops the runaway magnitude but - // isn't derived from any calibration. Needs the same self- - // play validation discipline as every other king-safety - // magnitude in this file before trusting the new ceiling. - i = 0; - bb = (pos->bbPawns[WHITE] | pos->bbPawns[BLACK]); - while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) - { - i += (DISTANCE(cSquare, KingPosition(pos, BLACK)) - - DISTANCE(cSquare, KingPosition(pos, WHITE))); - } - EVAL_TERM(WHITE, - KING, - c, - iKingScore, - i * 2, - "in/out of \"action\""); - } +static void +_EvalKing(IN OUT POSITION *pos, + IN const COOR c, + IN const PAWN_HASH_ENTRY *pHash, + IN SEARCHER_THREAD_CONTEXT *ctx) +/** + +Routine description: + + 2026-10-06: restructured into an explicit safety-vs-activity split + on pos->uNonPawnMaterial[xColor] (this king's own enemy's + material) against DO_KING_SAFETY_THRESHOLD -- above it, stay + hidden (_EvalKingSafety: full ring-attack/danger scoring, stay- + on-initial-square bonus); below it, come out (_EvalKingActivity: + KING_TO_CENTER unconditionally, plus, once the enemy is down to + at most one non-king piece, the sharper passer-support/in-the- + action guidance). Replaces the old `goto skip_safety` plus two + *separate*, independently-thresholded gates (KEEP_KING_AT_HOME_ + THRESHOLD for the stay-home bonus, uTotalNonPawnsBothSides<8/==2 + -- a different, combined-both-sides unit -- for the endgame + terms) that didn't line up with each other or with this one, + leaving a dead band where safety was scored but neither a stay- + home nor a come-out incentive applied. This single threshold now + drives both halves, so the two outcomes are exact mirror images + of one decision rather than three loosely related ones. + + Split into two FORCEINLINE helpers purely for readability -- see + _EvalKingSafety's comment for why this costs nothing versus the + old single monolithic function. + + No longer takes uTotalNonPawnsBothSides -- the old combined-both- + sides gate it fed is gone, replaced by a per-call check + (TotalNonPawnCount(pos, xColor), this king's own enemy only, not a + hoisted both-sides sum), so there's no shared value left worth + hoisting out to Eval(). + +Parameters: + + POSITION *pos, + COOR c, + PAWN_HASH_ENTRY *pHash, + SEARCHER_THREAD_CONTEXT *ctx + +Return value: + + FLAG + +**/ +{ + PIECE p; + ULONG uColor, xColor; + ULONG u; + SCORE iKingScore; + + ASSERT(IS_ON_BOARD(c)); + p = pos->rgSquare[c].pPiece; + ASSERT(p && IS_KING(p)); + uColor = GET_COLOR(p); + xColor = FLIP(uColor); + ASSERT(IS_VALID_COLOR(uColor) && IS_VALID_COLOR(xColor)); + + // Update attack bitvs for squares around the king. + BITBOARD bbRing = g_KingAttacksBB[c]; + pos->bbKingAttacks[uColor] |= bbRing; +#ifdef KING_DEFECT_DUMP + { + KING_DEFECT_DUMP_INFO *pDump = &ctx->sKingDefectDump[uColor]; + memset(pDump, 0, sizeof(KING_DEFECT_DUMP_INFO)); + } +#endif + + // Safety-vs-activity split -- see this function's header comment + // (2026-10-06 restructuring). Don't bother with king safety when + // the other side has low material; instead, actively encourage + // this king out. + u = pos->uNonPawnMaterial[xColor]; + ASSERT(u >= VALUE_KING); + if (u >= DO_KING_SAFETY_THRESHOLD) + { + iKingScore = _EvalKingSafety(pos, c, pHash, ctx, uColor, xColor, bbRing); + } + else + { + iKingScore = _EvalKingActivity(pos, c, pHash, ctx, uColor, xColor); } pos->iScore[uColor] += iKingScore; ctx->sPlyInfo[ctx->uPly].iKingScore[uColor] = iKingScore; @@ -6483,7 +6702,7 @@ Return value: } -FLAG +FLAG FORCEINLINE SideHasWinningChances(IN POSITION *pos, IN ULONG uSide) /** @@ -7694,11 +7913,6 @@ Return value: pos->uNumTrapped[BLACK] = pos->uNumTrapped[WHITE] = 0; pos->iReducedMaterialDownScaler[BLACK] = pos->iReducedMaterialDownScaler[WHITE] = 0; - pos->uClosedScaler = (FastCountBits(pos->bbPawns[WHITE]) + FastCountBits(pos->bbPawns[BLACK])) - - (pHash->uNumUnmovedPawns[WHITE] + pHash->uNumUnmovedPawns[BLACK]) + - (pHash->uNumRammedPawns); - ASSERT(pos->uClosedScaler >= 0); - ASSERT(pos->uClosedScaler <= 32); pos->uMinorsAtHome[BLACK] = pos->uMinorsAtHome[WHITE] = 0; // @@ -7854,13 +8068,7 @@ Return value: #endif } - // Evaluate the two kings last. Compute both sides' non-pawn count - // once here rather than in _EvalKing itself -- it's called twice - // below (once per king) and used to recompute the identical - // TotalNonPawnCount(WHITE)+TotalNonPawnCount(BLACK) sum both times. - ULONG uTotalNonPawnsBothSides = - TotalNonPawnCount(pos, WHITE) + TotalNonPawnCount(pos, BLACK); - + // Evaluate the two kings last. c = KingPosition(pos, uColor); #ifdef DEBUG ASSERT(IS_ON_BOARD(c)); @@ -7872,7 +8080,7 @@ Return value: } #endif TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, - _EvalKing(pos, c, pHash, ctx, uTotalNonPawnsBothSides)); + _EvalKing(pos, c, pHash, ctx)); #ifdef EVAL_DUMP Trace("After our K at %s:\n%d\t\t%d\t\t(%d for %s)\n", CoorToString(c), @@ -7893,7 +8101,7 @@ Return value: } #endif TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, - _EvalKing(pos, c, pHash, ctx, uTotalNonPawnsBothSides)); + _EvalKing(pos, c, pHash, ctx)); #ifdef EVAL_DUMP Trace("After enemy K at %s:\n%d\t\t%d\t\t(%d for %s)\n", CoorToString(c), @@ -7929,7 +8137,7 @@ Return value: // slots first so a clean full eval reliably means a clean slate. ctx->cEnprise[ctx->uPly][0] = ctx->cEnprise[ctx->uPly][1] = ILLEGAL_COOR; ctx->cTrapped[ctx->uPly] = ILLEGAL_COOR; - _EvalLookForDanger(ctx); + //_EvalLookForDanger(ctx); _EvalTrappedPieces(ctx); // TODO: endgame-specific knowledge? e.g. B over N in an endgame diff --git a/src/generate.c b/src/generate.c index a2fc9c4..89796b4 100644 --- a/src/generate.c +++ b/src/generate.c @@ -56,7 +56,16 @@ Revision History: const INT g_iQKDeltas[] = { -17, -16, -15, -1, +1, +17, +15, +16, 0 }; -static void +// Only needed as a standalone call site when UNBLOCKED_BITBOARD is +// off; under that toggle it's still referenced (DEBUG builds only) by +// _FindUnblockedSquaresBB's cross-check, so it can't simply be +// deleted yet -- see board_representation/MOVEGEN_MIGRATION.md. +#if !defined(UNBLOCKED_BITBOARD) || defined(DEBUG) || defined(TEST) +// Exposed non-static (like generate.c's other migration-era helpers, +// e.g. _GenerateRookBB) whenever anything other than this file might +// need it: the DEBUG cross-check inside _FindUnblockedSquaresBB below, +// or testgenerate.c's correctness/speed harness under TEST. +void _FindUnblockedSquares(IN MOVE_STACK *pStack, IN POSITION *pos) /** @@ -153,20 +162,138 @@ Return value: ASSERT(uCount > 2); ASSERT(uCount < 28); } +#endif // !defined(UNBLOCKED_BITBOARD) || defined(DEBUG) + +// board_representation/MOVEGEN_MIGRATION.md's never-started +// _FindUnblockedSquares/WouldGiveCheck candidate. Membership (is this +// square unblocked for the opposing king) is exactly the set of +// squares the rook+bishop magic tables already reach from the king's +// square, stopping at (and including) the first blocker in each of +// the 8 directions -- precisely the queen-ray walk the mailbox +// version does by hand, one lookup+OR instead of an 8-direction loop. +// The direction-back-to-king half of the old table needs no storage +// at all: generate.c's own ASSERT two lines below (mailbox version) +// proves DIR_FROM_SQ_TO_KING(c) is always identical to +// DIRECTION_BETWEEN_SQUARES(c, xKing), which is a g_pVectorDelta[] +// table lookup in release builds (see chess.h), not a divide/branch +// chain -- so recomputing it on demand costs the same as reading a +// stored value would have, with no table to populate up front. +void +_FindUnblockedSquaresBB(IN MOVE_STACK *pStack, + IN POSITION *pos) +/** + +Routine description: + + Bitboard replacement for _FindUnblockedSquares -- see that + function's header comment for what "unblocked" means. Builds + pStack->bbUnblocked[uPly], one bit per square unblocked for the + king of the side not on move. + +Parameters: + + MOVE_STACK *pStack : move stack pointer + POSITION *pos : position pointer + +Return value: + + void + +**/ +{ + COOR cKing = KingPosition(pos, FLIP(pos->uToMove)); + ULONG uPly = pStack->uPly; + + ASSERT(IS_ON_BOARD(cKing)); + ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) != pos->uToMove); + pStack->bbUnblocked[uPly] = + _RookAttacksBB(cKing, pos->bbOccupied) | + _BishopAttacksBB(cKing, pos->bbOccupied); + +#ifdef DEBUG + { + // Cross-check against the mailbox walk every single call -- + // this is the correctness gate for bring-up, not a one-time + // test: membership must match exactly, and for every square + // the bitboard claims is unblocked, the direction back to the + // king recomputed via DIRECTION_BETWEEN_SQUARES must match + // what the mailbox ray walk actually stored. + COOR c; + BITBOARD bb = pStack->bbUnblocked[uPly]; + + _FindUnblockedSquares(pStack, pos); + FOREACH_SQUARE(c) + { + if (!IS_ON_BOARD(c)) continue; + FLAG fBB = (0 != (bb & COOR_TO_BB(c))); + FLAG fMailbox = (pStack->sUnblocked[uPly][c].uKey == + pStack->uUnblockedKeyValue[uPly]); + ASSERT(fBB == fMailbox); + if (fBB) + { + ASSERT(DIRECTION_BETWEEN_SQUARES(c, cKing) == + pStack->sUnblocked[uPly][c].iPointer); + } + } + // Restore: the mailbox call above just bumped + // uUnblockedKeyValue[uPly] a second time this ply, which is + // harmless (it's just a generation counter) but leave the + // real bbUnblocked value as computed above, not whatever the + // mailbox call's side effects might suggest. + pStack->bbUnblocked[uPly] = bb; + } +#endif +} + +#ifdef UNBLOCKED_BITBOARD +// GenerateMoves no longer builds bbUnblocked[uPly] eagerly -- it just +// bumps this ply's generation ticket (uUnblockedKeyValue[uPly], see +// its comment in chess.h) so WouldGiveCheck's own lazy check (below) +// knows a new node has arrived here and the cached bbUnblocked[uPly] +// (built for some earlier generation, if ever) is no longer trusted. +// The real build is deferred to WouldGiveCheck's first call this +// generation, if there is one -- a node whose every candidate gets +// rejected before ever needing WouldGiveCheck now never pays for it. +#define MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly) \ + ((void)(pos), (pStack)->uUnblockedKeyValue[(uPly)]++) +#define SQUARE_IS_UNBLOCKED(c) \ + (0 != (pStack->bbUnblocked[ctx->uPly] & COOR_TO_BB(c))) +#define DIR_FROM_SQ_TO_KING(c) \ + DIRECTION_BETWEEN_SQUARES((c), xKing) +#else +// Mailbox path has no lazy build to defer to -- its own per-square +// sUnblocked[][].uKey scheme (SQUARE_IS_UNBLOCKED/DIR_FROM_SQ_TO_KING +// below) is written directly by _FindUnblockedSquares itself, so this +// stays eager, same as it always was. +#define MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly) \ + ((void)(uPly), _FindUnblockedSquares((pStack), (pos))) #define SQUARE_IS_UNBLOCKED(c) (uUnblocked == kp[(c)].uKey) #define DIR_FROM_SQ_TO_KING(c) (kp[(c)].iPointer) +#endif -FLAG +FLAG FORCEINLINE WouldGiveCheck(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv) /** Routine description: - Determine whether a move is a checking move or not. For this - function to work properly _FindUnblockedSquares must have been - called at the start of generation. + Determine whether a move is a checking move or not. + + Under UNBLOCKED_BITBOARD (this build's active path): self- + sufficient -- builds bbUnblocked[ctx->uPly] itself, lazily, on the + first call for a given generation (see the lazy-rebuild block + below and chess.h's uUnblockedBuiltFor comment). GenerateMoves no + longer builds it eagerly, only marks the generation stale + (MARK_UNBLOCKED_SQUARES_STALE); callers no longer need to remember + to call _FindUnblockedSquares[BB] first. + + Under the mailbox fallback (no UNBLOCKED_BITBOARD): the older + precondition still holds -- _FindUnblockedSquares must have been + called at the start of generation (MARK_UNBLOCKED_SQUARES_STALE + still does this eagerly for that path, same as it always did). Note: this is one of the most called codepaths in the engine, speed is of the essence here. @@ -193,8 +320,10 @@ Return value: COOR cFrom = mv.cFrom; PIECE pPiece = mv.pMoved; COOR xKing = KingPosition(pos, FLIP(GET_COLOR(pPiece))); +#ifndef UNBLOCKED_BITBOARD KEY_POINTER *kp = &(pStack->sUnblocked[ctx->uPly][0]); ULONG uUnblocked = pStack->uUnblockedKeyValue[ctx->uPly]; +#endif COOR cEpSquare; int iDelta; @@ -206,6 +335,28 @@ Return value: ASSERT(IS_ON_BOARD(cFrom)); ASSERT(pPiece); +#ifdef UNBLOCKED_BITBOARD + // Lazy rebuild: GenerateMoves only bumps uUnblockedKeyValue[uPly] + // (MARK_UNBLOCKED_SQUARES_STALE above) instead of building + // bbUnblocked[uPly] eagerly. The first WouldGiveCheck call for a + // given generation discovers its mismatch here and does the real + // build; every later call this same node/generation matches and + // skips straight to SQUARE_IS_UNBLOCKED/DIR_FROM_SQ_TO_KING below. + // A node whose every candidate gets rejected before ever calling + // WouldGiveCheck (e.g. every capture futility-pruned in QSearch) + // now never pays for the build at all -- see chess.h's + // uUnblockedBuiltFor comment for why this needs its own field + // rather than reusing uUnblockedKeyValue for both jobs. Measured + // cost of the added branch itself (mock A/B, same shape as this + // one, three curated suites): within this box's own run-to-run + // noise band, no consistent direction -- see conversation. + if (pStack->uUnblockedBuiltFor[ctx->uPly] != pStack->uUnblockedKeyValue[ctx->uPly]) + { + _FindUnblockedSquaresBB(pStack, pos); + pStack->uUnblockedBuiltFor[ctx->uPly] = pStack->uUnblockedKeyValue[ctx->uPly]; + } +#endif + // // Phase 1: Does this move directly attack the king? // @@ -3302,6 +3453,489 @@ Return value: } +// -------------------------------------------------------------------- +// Captures/promotions-only generators (QSearch's GENERATE_CAPTURES_PROMS +// path). QSearch's _ScoreQSearchMovesNoChecks assigns a score of 0 to +// every quiet non-promoting move, and search.c's QSearch loop bails out +// of its move loop on the first move it sees scored <= 0 -- so a quiet +// move that _GenerateAllMoves would have produced for this uType was +// always going to be generated, scored 0, and never searched. These +// generators skip producing it in the first place: same mask-and- +// serialize shape as the normal _Generate*BB functions, but masking the +// attack bitboard with the ENEMY-occupied bitboard instead of the +// complement of the friendly one, so quiet destinations never reach +// _AddNormalMove at all. Bitboard-only (no mailbox fallback) -- every +// piece type's GENERATE_*_BITBOARD flag is on in this project's only +// build profile (GNUmakefile's PROFILE), and the mailbox generators this +// would otherwise have to mirror are slated for removal separately. +// +// Castling is skipped entirely (a castle is never a capture or +// promotion). Quiet pawn pushes are skipped except the single push onto +// the back rank, which is a (quiet) promotion and still must be scored. +// +// These must stay pseudo-legal-equivalent to _GenerateAllMoves filtered +// to IS_CAPTURE_OR_PROMOTION(mv), as a set -- see testgenerate.c's +// TestCapturesAndPromotionsGenerator for the TEST=1 gate that checks +// exactly this, over a corpus of positions, before any piece type here +// is trusted. +// -------------------------------------------------------------------- + +// CAPTURES_PROMS_BITBOARD_PATH is declared in chess.h (not defined +// here) so every translation unit that needs to know whether this path +// exists sees the same answer -- see its comment there. +#if defined(CAPTURES_PROMS_BITBOARD_PATH) + +void +_GenerateKnightCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cKnight) +/** + +Routine description: + + Captures-only sibling of _GenerateKnightBB: masks the knight's + attack bitboard with the enemy-occupied bitboard instead of the + complement of the friendly one, so only captures are serialized. + Knights never promote, so this is the whole move set this piece + type can contribute to GENERATE_CAPTURES_PROMS. + +Parameters: + + MOVE_STACK *pStack : the move stack + POSITION *pos : the board position + COOR cKnight : the knight's location + +Return value: + + void + +**/ +{ + BITBOARD bbDest = g_KnightAttacksBB[cKnight] & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_KNIGHT(pos->rgSquare[cKnight].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cKnight].pPiece) == pos->uToMove); + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cKnight].pPiece)); + _AddNormalMove(pStack, pos, cKnight, c, p); + } +} + + +void +_GenerateKingCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cKing) +/** + +Routine description: + + Captures-only sibling of _GenerateKingBB. No castling here -- a + castle can never be a capture. + +Parameters: + + MOVE_STACK *pStack : the move stack + POSITION *pos : the board position + COOR cKing : the king's location + +Return value: + + void + +**/ +{ + BITBOARD bbDest = g_KingAttacksBB[cKing] & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) == pos->uToMove); + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cKing].pPiece)); + _AddNormalMove(pStack, pos, cKing, c, p); + } +} + + +void +_GenerateBishopCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cBishop) +/** + +Routine description: + + Captures-only sibling of _GenerateBishopBB. + +Parameters: + + MOVE_STACK *pStack : the move stack (bbOccupied must already be + set, same precondition as _GenerateBishopBB) + POSITION *pos : the board position + COOR cBishop : the bishop's location + +Return value: + + void + +**/ +{ + ULONG uMagicIndex; + BITBOARD bbDest; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_BISHOP(pos->rgSquare[cBishop].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cBishop].pPiece) == pos->uToMove); + ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos)); + + uMagicIndex = (ULONG) + (((pStack->bbOccupied & g_BishopOccupancyMask[cBishop]) * + g_BishopMagic[cBishop]) >> g_BishopMagicShift[cBishop]); + bbDest = g_BishopAttackTable[cBishop][uMagicIndex] & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cBishop].pPiece)); + _AddNormalMove(pStack, pos, cBishop, c, p); + } +} + + +void +_GenerateRookCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cRook) +/** + +Routine description: + + Captures-only sibling of _GenerateRookBB. + +Parameters: + + MOVE_STACK *pStack : the move stack (bbOccupied must already be + set, same precondition as _GenerateRookBB) + POSITION *pos : the board position + COOR cRook : the rook's location + +Return value: + + void + +**/ +{ + ULONG uMagicIndex; + BITBOARD bbDest; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_ROOK(pos->rgSquare[cRook].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cRook].pPiece) == pos->uToMove); + ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos)); + + uMagicIndex = (ULONG) + (((pStack->bbOccupied & g_RookOccupancyMask[cRook]) * + g_RookMagic[cRook]) >> g_RookMagicShift[cRook]); + bbDest = g_RookAttackTable[cRook][uMagicIndex] & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cRook].pPiece)); + _AddNormalMove(pStack, pos, cRook, c, p); + } +} + + +void +_GenerateQueenCapturesBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN COOR cQueen) +/** + +Routine description: + + Captures-only sibling of _GenerateQueenBB. + +Parameters: + + MOVE_STACK *pStack : the move stack (bbOccupied must already be + set, same precondition as _GenerateQueenBB) + POSITION *pos : the board position + COOR cQueen : the queen's location + +Return value: + + void + +**/ +{ + ULONG uRookMagicIndex, uBishopMagicIndex; + BITBOARD bbDest; + ULONG uBitIndex; + COOR c; + PIECE p; + + ASSERT(IS_QUEEN(pos->rgSquare[cQueen].pPiece)); + ASSERT(GET_COLOR(pos->rgSquare[cQueen].pPiece) == pos->uToMove); + ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos)); + + uRookMagicIndex = (ULONG) + (((pStack->bbOccupied & g_RookOccupancyMask[cQueen]) * + g_RookMagic[cQueen]) >> g_RookMagicShift[cQueen]); + uBishopMagicIndex = (ULONG) + (((pStack->bbOccupied & g_BishopOccupancyMask[cQueen]) * + g_BishopMagic[cQueen]) >> g_BishopMagicShift[cQueen]); + bbDest = (g_RookAttackTable[cQueen][uRookMagicIndex] | + g_BishopAttackTable[cQueen][uBishopMagicIndex]) & + pos->bbOccupiedSide[FLIP(pos->uToMove)]; + + while (bbDest) + { + uBitIndex = FastFirstBit(bbDest) - 1; + bbDest &= (bbDest - 1); + c = BIT_NUMBER_TO_COOR(uBitIndex); + p = pos->rgSquare[c].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cQueen].pPiece)); + _AddNormalMove(pStack, pos, cQueen, c, p); + } +} + + +void +_GenerateCapturesAndPromotionsPawnsBB(IN MOVE_STACK *pStack, + IN POSITION *pos, + IN ULONG uSide) +/** + +Routine description: + + Captures/promotions-only sibling of _GenerateAllPawnMovesBB: both + diagonal captures (promoting or not), en passant, and the single + quiet push onto the back rank (a promotion, so it still must be + scored) -- but never a double push or a non-promoting single push, + neither of which can be a capture or promotion. + +Parameters: + + MOVE_STACK *pStack : the move stack + POSITION *pos : the board position + ULONG uSide : which side's pawns to generate for (pos->uToMove) + +Return value: + + void + +**/ +{ + BITBOARD bbPawns = pos->bbPawns[uSide]; + BITBOARD bbEmpty = ~pos->bbOccupied; + BITBOARD bbEnemy = pos->bbOccupiedSide[FLIP(uSide)]; + BITBOARD bbPushPromote, bbCapLeft, bbCapRight, bb; + ULONG uBitIndex; + COOR cTo, cFrom, cEp; + PIECE p; + + if (uSide == WHITE) + { + bbPushPromote = ((bbPawns & BBRANK[7]) >> 8) & bbEmpty; + bbCapLeft = ((bbPawns & ~BBFILE[0]) >> 9) & bbEnemy; + bbCapRight = ((bbPawns & ~BBFILE[7]) >> 7) & bbEnemy; + } + else + { + ASSERT(uSide == BLACK); + bbPushPromote = ((bbPawns & BBRANK[2]) << 8) & bbEmpty; + bbCapLeft = ((bbPawns & ~BBFILE[0]) << 7) & bbEnemy; + bbCapRight = ((bbPawns & ~BBFILE[7]) << 9) & bbEnemy; + } + + // Quiet promotions only -- the single push from the 7th/2nd rank. + bb = bbPushPromote; + while (bb) + { + uBitIndex = FastFirstBit(bb) - 1; + bb &= (bb - 1); + cTo = BIT_NUMBER_TO_COOR(uBitIndex); + cFrom = (uSide == WHITE) ? + BIT_NUMBER_TO_COOR(uBitIndex + 8) : + BIT_NUMBER_TO_COOR(uBitIndex - 8); + ASSERT(IS_PAWN(pos->rgSquare[cFrom].pPiece)); + ASSERT(RANK8(cTo) || RANK1(cTo)); + _AddPromote(pStack, pos, cFrom, cTo); + } + + // Capture left (+ promotion if landing on the far rank). + bb = bbCapLeft; + while (bb) + { + uBitIndex = FastFirstBit(bb) - 1; + bb &= (bb - 1); + cTo = BIT_NUMBER_TO_COOR(uBitIndex); + cFrom = (uSide == WHITE) ? + BIT_NUMBER_TO_COOR(uBitIndex + 9) : + BIT_NUMBER_TO_COOR(uBitIndex - 7); + ASSERT(IS_PAWN(pos->rgSquare[cFrom].pPiece)); + p = pos->rgSquare[cTo].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cFrom].pPiece)); + if (RANK8(cTo) || RANK1(cTo)) + { + _AddPromote(pStack, pos, cFrom, cTo); + } + else + { + _AddNormalMove(pStack, pos, cFrom, cTo, p); + } + } + + // Capture right (+ promotion if landing on the far rank). + bb = bbCapRight; + while (bb) + { + uBitIndex = FastFirstBit(bb) - 1; + bb &= (bb - 1); + cTo = BIT_NUMBER_TO_COOR(uBitIndex); + cFrom = (uSide == WHITE) ? + BIT_NUMBER_TO_COOR(uBitIndex + 7) : + BIT_NUMBER_TO_COOR(uBitIndex - 9); + ASSERT(IS_PAWN(pos->rgSquare[cFrom].pPiece)); + p = pos->rgSquare[cTo].pPiece; + ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cFrom].pPiece)); + if (RANK8(cTo) || RANK1(cTo)) + { + _AddPromote(pStack, pos, cFrom, cTo); + } + else + { + _AddNormalMove(pStack, pos, cFrom, cTo, p); + } + } + + // En passant -- same per-side-per-node (not per-pawn) check as + // _GenerateAllPawnMovesBB. + cEp = pos->cEpSquare; + if (IS_ON_BOARD(cEp)) + { + int iBehindDelta = (uSide == WHITE) ? 16 : -16; + + cFrom = cEp + iBehindDelta - 1; + if (IS_ON_BOARD(cFrom) && + IS_PAWN(pos->rgSquare[cFrom].pPiece) && + (GET_COLOR(pos->rgSquare[cFrom].pPiece) == uSide)) + { + _AddEnPassant(pStack, pos, cFrom, cEp); + } + cFrom = cEp + iBehindDelta + 1; + if (IS_ON_BOARD(cFrom) && + IS_PAWN(pos->rgSquare[cFrom].pPiece) && + (GET_COLOR(pos->rgSquare[cFrom].pPiece) == uSide)) + { + _AddEnPassant(pStack, pos, cFrom, cEp); + } + } +} + + +// Non-static so testgenerate.c's correctness/speed harness can call it +// directly by name, same convention as _GenerateAllMoves below. +void +_GenerateCapturesAndPromotions(IN MOVE_STACK *pStack, + IN POSITION *pos) +/** + +Routine description: + + Generates exactly the pseudo-legal moves that are captures or + promotions in this position -- the subset of _GenerateAllMoves' + output that IS_CAPTURE_OR_PROMOTION(mv) is true for, produced + directly rather than generated-then-filtered. Used by GenerateMoves + for GENERATE_CAPTURES_PROMS (QSearch's no-checks path) in place of + _GenerateAllMoves, since every quiet move _GenerateAllMoves would + have produced there is scored 0 by _ScoreQSearchMovesNoChecks and + never searched. + + Relies on pStack->bbOccupied being set by the caller for the + sliding-piece magic lookups, same precondition as _GenerateAllMoves + itself. + +Parameters: + + MOVE_STACK *pStack : the move stack + POSITION *pos : the board position + +Return value: + + void + +**/ +{ + COOR c; + static void (*JumpTable[])(MOVE_STACK *, POSITION *, COOR) = + { + InvalidGenerator, // EMPTY + InvalidGenerator, // EMPTY | WHITE + InvalidGenerator, // 2 (BLACK_PAWN) + InvalidGenerator, // 3 (WHITE_PAWN) + _GenerateKnightCapturesBB, // 4 (BLACK_KNIGHT) + _GenerateKnightCapturesBB, // 5 (WHITE_KNIGHT) + _GenerateBishopCapturesBB, // 6 (BLACK_BISHOP) + _GenerateBishopCapturesBB, // 7 (WHITE_BISHOP) + _GenerateRookCapturesBB, // 8 (BLACK_ROOK) + _GenerateRookCapturesBB, // 9 (WHITE_ROOK) + _GenerateQueenCapturesBB, // 10 (BLACK_QUEEN) + _GenerateQueenCapturesBB, // 11 (WHITE_QUEEN) + _GenerateKingCapturesBB, // 12 (BLACK_KING) + _GenerateKingCapturesBB // 13 (WHITE_KING) + }; + + pStack->bbOccupied = pos->bbOccupied; + + { + BITBOARD bbNonPawns = NON_KING_PIECES_BB(pos, pos->uToMove) | + COOR_TO_BB(KingPosition(pos, pos->uToMove)); + while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bbNonPawns))) + { + (JumpTable[pos->rgSquare[c].pPiece])(pStack, pos, c); + } + } + + _GenerateCapturesAndPromotionsPawnsBB(pStack, pos, pos->uToMove); +} + +#endif // captures/promotions bitboard path + + // Non-static (unlike its historical internal-only status) so // testgenerate.c's whole-node dispatch benchmark can call it directly // by name -- see _GenerateAllMovesBB's block comment for why that @@ -4574,9 +5208,7 @@ Routine description: Like _ScoreSearchMoves, this function takes "hints" from the searcher context about what squares are in danger and tries to - order moves away from those squares sooner. Unlike - _ScoreSearchMoves this function also attempts to capture enemies - that are in danger sooner. + order moves away from those squares sooner. Parameters: @@ -4599,7 +5231,7 @@ Return value: COOR cEnprise = FindEnprisePiece(ctx, uColor); // - // We have generate all moves or just legal escapes from check + // We have generated all moves or just legal escapes from check // here. There can be a hash move -- if there is then the search // has already tried it and we should not generate it again. // @@ -4611,9 +5243,31 @@ Return value: ctx->sPlyInfo[uPly].eMoveScoringScheme = MOVE_SCORING_QSEARCH_CHECKS; - for (u = pStack->uBegin[uPly]; - u < pStack->uEnd[uPly]; - u++) + // uLiveEnd shrinks as moves are rejected, instead of leaving them in + // place scored <= 0 for QSearch's move loop (search.c) to discover + // and bail on. Every move QSearch's loop will ever consider calls + // SelectBestNoHistory, which rescans the WHOLE remaining [u, + // uEnd[uPly]) range -- so a quiet, non-checking move sitting in + // that range gets rescanned by every single one of those calls + // until the bail, not generated/scored once and ignored. Instead, + // a rejected move (iValue <= 0) is swapped with the last live + // element and uLiveEnd drops by one -- same whole-struct-swap + // idiom GenerateMoves already uses to retire a found hash move + // (see its comment above) -- and pStack->uEnd[uPly] is written + // once at the end, after every move has been classified. The + // index being examined is NOT advanced on a reject, since the + // element just swapped into that slot hasn't been scored yet. + // + // Net effect: every move left in [uBegin[uPly], uEnd[uPly]) after + // this function returns has iValue > 0, so search.c's QSearch loop + // will never actually hit its own `iValue <= 0` bail for this + // scoring scheme -- that check stays (it's shared with + // _ScoreQSearchMovesNoChecks, which still needs it), it just + // becomes unreachable here, which is harmless. + { + ULONG uLiveEnd = pStack->uEnd[uPly]; + u = pStack->uBegin[uPly]; + while (u < uLiveEnd) { mv = pStack->mvf[u].mv; ASSERT(GET_COLOR(mv.pMoved) == uColor); @@ -4702,10 +5356,33 @@ Return value: s += (mv.cFrom == cEnprise) * (PIECE_VALUE(mv.pMoved) / 4); ASSERT(s > 0); } + else + { + // + // Reject: this move will never be searched (QSearch's + // caller would bail on it the moment it became the best + // remaining candidate) -- drop it from the live range now + // instead of leaving it for SelectBestNoHistory to keep + // rescanning. Swap the last live element into this slot + // and shrink uLiveEnd; u is deliberately NOT advanced so + // the freshly-swapped-in element gets classified on the + // next iteration. + // + uLiveEnd--; + if (u != uLiveEnd) + { + pStack->mvf[u] = pStack->mvf[uLiveEnd]; + } + continue; + } + pStack->mvf[u].iValue = s; ASSERT(pStack->mvf[u].iValue != -MAX_INT); + u++; } // next move + pStack->uEnd[uPly] = uLiveEnd; + } } @@ -4761,7 +5438,6 @@ Return value: ASSERT(MOVE_COUNT(ctx, uPly) <= MAX_MOVES_PER_PLY); ctx->sPlyInfo[uPly].eMoveScoringScheme = MOVE_SCORING_QSEARCH_NO_CHECKS; - #ifdef DEBUG uColor = pos->uToMove; #endif @@ -4791,6 +5467,7 @@ Return value: ASSERT((mv.pCaptured) || (mv.pPromoted)); s = PIECE_VALUE(mv.pCaptured) - PIECE_VALUE(mv.pMoved); + // // TODO: how does this handle positions like K *p *k ? // @@ -4912,27 +5589,41 @@ Return value: case GENERATE_CAPTURES_PROMS_CHECKS: ASSERT(!InCheck(pos, pos->uToMove)); ASSERT(mvHash.uMove == 0); - _FindUnblockedSquares(pStack, pos); + MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly); _GenerateAllMoves(pStack, pos); _ScoreQSearchMovesInclChecks(pStack, ctx); break; case GENERATE_CAPTURES_PROMS: ASSERT(!InCheck(pos, pos->uToMove)); ASSERT(mvHash.uMove == 0); - _FindUnblockedSquares(pStack, pos); + // The unblocked-squares table itself is no longer built + // here -- see WouldGiveCheck's lazy rebuild (generate.c): + // this just marks the ply's generation ticket so the + // first WouldGiveCheck call this node (if any) knows to + // build it. search.c's QSearch loop calls WouldGiveCheck + // itself, deferred, on whichever single move it selects + // and decides to search (see fIncludeChecks == FALSE + // there) -- most nodes whose candidates are all futility- + // pruned away never reach that call at all, and now never + // pay for the table either. + MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly); +#if defined(CAPTURES_PROMS_BITBOARD_PATH) + _GenerateCapturesAndPromotions(pStack, pos); +#else _GenerateAllMoves(pStack, pos); +#endif _ScoreQSearchMovesNoChecks(pStack, ctx); break; case GENERATE_ALL_MOVES: ASSERT(!InCheck(pos, pos->uToMove)); - _FindUnblockedSquares(pStack, pos); + MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly); _GenerateAllMoves(pStack, pos); _ScoreAllMoves(pStack, ctx, mvHash); uReturn = MOVE_COUNT(ctx, uPly); break; case GENERATE_ESCAPES: ASSERT(InCheck(pos, pos->uToMove)); - _FindUnblockedSquares(pStack, pos); + MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly); uReturn = _GenerateEscapes(pStack, pos); ASSERT(uReturn); pStack->sGenFlags[uPly].uKingMoveCount = uReturn >> 16; @@ -620,6 +620,7 @@ Return value: TestGetAttacks(); TestIsAttackedBB(); TestSeeFastSign(); + TestSEENoDefenderFastPath(); TestWhoAttacksSquareMagic(); TestMoveGenerator(); TestLegalMoveGenerator(); @@ -629,6 +630,11 @@ Return value: TestGenerateBishopSpeed(); TestGenerateQueenSpeed(); TestGenerateAllMovesSpeed(); +#if defined(CAPTURES_PROMS_BITBOARD_PATH) + TestCapturesAndPromotionsGenerator(); + TestGenerateCapturesAndPromotionsSpeed(); +#endif + TestFindUnblockedSquaresSpeed(); TestFenCode(); TestLiftPlaceSlidePiece(); TestExposesCheck(); diff --git a/src/movesup.c b/src/movesup.c index e39e52a..d3d2a74 100644 --- a/src/movesup.c +++ b/src/movesup.c @@ -385,10 +385,10 @@ Return value: 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); - + for (cBlockIndex = cRemove + iDelta; IS_ON_BOARD(cBlockIndex); cBlockIndex += iDelta) @@ -400,12 +400,12 @@ Return value: { // // Is it an enemy? - // + // if (OPPOSITE_COLORS(xPiece, uColor)) { // // Does it move the right way to attack? - // + // iIndex = (int)cBlockIndex - (int)cLocation; if (0 != (CHECK_VECTOR_WITH_INDEX(iIndex, WHITE) & (1 << (PIECE_TYPE(xPiece))))) @@ -413,26 +413,26 @@ Return value: return(cBlockIndex); } } - + // // Either we found another friendly piece or an enemy piece // that was unable to reach us. Either way we are safe. - // + // return(ILLEGAL_COOR); } } - + // // We fell off the board without seeing another piece. - // + // return(ILLEGAL_COOR); } -COOR -ExposesCheck(POSITION *pos, - COOR cRemove, +COOR +ExposesCheck(POSITION *pos, + COOR cRemove, COOR cLocation) /** @@ -452,7 +452,7 @@ Parameters: Return value: - COOR : the location of an attacker piece or 0x88 (!IS_ON_BOARD) if + COOR : the location of an attacker piece or 0x88 (!IS_ON_BOARD) if the removal of cRemove does not expose check. **/ @@ -461,13 +461,13 @@ Return value: register COOR cBlockIndex; int iIndex; PIECE xPiece; - + ASSERT(IS_ON_BOARD(cRemove)); ASSERT(IS_ON_BOARD(cLocation)); ASSERT(!IS_EMPTY(pos->rgSquare[cLocation].pPiece)); - + iIndex = (int)cLocation - (int)cRemove; - + // // 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 @@ -479,11 +479,11 @@ Return value: return(ILLEGAL_COOR); } iDelta = CHECK_DELTA_WITH_INDEX(iIndex); - for (cBlockIndex = cLocation + iDelta; + for (cBlockIndex = cLocation + iDelta; IS_ON_BOARD(cBlockIndex); cBlockIndex += iDelta) { - if (cBlockIndex == cRemove) + if (cBlockIndex == cRemove) { continue; } @@ -492,12 +492,12 @@ Return value: { // // Is it an enemy? - // + // if (OPPOSITE_COLORS(xPiece, pos->rgSquare[cLocation].pPiece)) { // // Does it move the right way to attack? - // + // iIndex = (int)cBlockIndex - (int)cLocation; if (0 != (CHECK_VECTOR_WITH_INDEX(iIndex, GET_COLOR(xPiece)) & (1 << (PIECE_TYPE(xPiece))))) @@ -505,28 +505,28 @@ Return value: return(cBlockIndex); } } - + // // Either we found another friendly piece or an enemy piece // that was unable to reach us. Either way we are safe. - // + // return(ILLEGAL_COOR); } } - + // // We fell off the board without seeing another piece. - // + // return(ILLEGAL_COOR); } -COOR -ExposesCheckEp(POSITION *pos, - COOR cTest, - COOR cIgnore, - COOR cBlock, - COOR cKing) +COOR +ExposesCheckEp(POSITION *pos, + COOR cTest, + COOR cIgnore, + COOR cBlock, + COOR cKing) /** Routine description: @@ -562,13 +562,13 @@ Return value: // reach the square we are testing (i.e. the two squares are not // on the same rank, file, or diagonal) then no problemo. // - if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN))) + if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN))) { return(ILLEGAL_COOR); } - + iDelta = CHECK_DELTA_WITH_INDEX(iIndex); - for (cBlockIndex = cKing + iDelta; + for (cBlockIndex = cKing + iDelta; IS_ON_BOARD(cBlockIndex); cBlockIndex += iDelta) { @@ -583,7 +583,7 @@ Return value: { // // Does it move the right way to attack? - // + // iIndex = (int)cBlockIndex - (int)cKing; if (0 != (CHECK_VECTOR_WITH_INDEX(iIndex, GET_COLOR(xPiece)) & (1 << (PIECE_TYPE(xPiece))))) @@ -595,14 +595,14 @@ Return value: // // Either we found another friendly piece or an enemy piece // that was unable to reach us. Either way we are safe. - // + // return(ILLEGAL_COOR); } } - + // // We fell off the board without seeing another piece. - // + // return(ILLEGAL_COOR); } @@ -657,7 +657,7 @@ Return value: return(0 != (g_PawnAttackOriginBB[uSide][cTest] & pos->bbPawns[uSide])); } -FLAG +FLAG FORCEINLINE InCheckBB(IN POSITION *pos, IN ULONG uSide) /** @@ -740,7 +740,7 @@ Return value: { continue; } - + // // These pieces do not slide, we don't need to look for // blockers. If they can get there then there is nothing @@ -775,16 +775,16 @@ Return value: // // Check the locations a pawn could attack cTest from. - // + // cLocation = cTest + iPawnLoc[uSide]; - if (IS_ON_BOARD(cLocation) && + if (IS_ON_BOARD(cLocation) && (pos->rgSquare[cLocation].pPiece == pPawn[uSide])) { return(TRUE); } - + cLocation += 2; - if (IS_ON_BOARD(cLocation) && + if (IS_ON_BOARD(cLocation) && (pos->rgSquare[cLocation].pPiece == pPawn[uSide])) { return(TRUE); @@ -793,7 +793,7 @@ Return value: } -FLAG +FLAG InCheck(POSITION *pos, ULONG uSide) /** @@ -818,7 +818,7 @@ Return value: #endif ASSERT(IS_VALID_COLOR(uSide)); - + cKingLoc = KingPosition(pos, uSide); #ifdef DEBUG pKing = pos->rgSquare[cKingLoc].pPiece; @@ -830,7 +830,7 @@ Return value: } -static FLAG +static FLAG _SanityCheckPawnMove(POSITION *pos, MOVE mv) /** @@ -856,15 +856,15 @@ Return value: // // General sanity checking... - // - if (!IS_ON_BOARD(cFrom) || !IS_ON_BOARD(cTo)) + // + if (!IS_ON_BOARD(cFrom) || !IS_ON_BOARD(cTo)) { return(FALSE); } - + // // Sanity check promotions. - // + // if (mv.pPromoted) { if (GET_COLOR(p) == WHITE) @@ -875,43 +875,43 @@ Return value: { if ((RANK(cTo) != 1) && (RANK(cFrom) != 2)) return(FALSE); } - + if (!IS_KNIGHT(mv.pPromoted) && !IS_BISHOP(mv.pPromoted) && !IS_ROOK(mv.pPromoted) && !IS_QUEEN(mv.pPromoted)) { return(FALSE); - } + } } - + // // Handle capture moves. - // + // if (mv.pCaptured) { // // Must be capturing something - // + // if (IS_SPECIAL_MOVE(mv) && (!mv.pPromoted)) { // // en passant - // + // if (!IS_EMPTY(pos->rgSquare[cTo].pPiece)) return(FALSE); } else { // // normal capture - // + // if (IS_EMPTY(pos->rgSquare[cTo].pPiece)) return(FALSE); if (!OPPOSITE_COLORS(mv.pMoved, mv.pCaptured)) return(FALSE); } // // Must be in the capturing position. - // + // if (WHITE == GET_COLOR(p)) { if (((cFrom - 17) != cTo) && ((cFrom - 15) != cTo)) return(FALSE); @@ -920,27 +920,27 @@ Return value: { if (((cFrom + 17) != cTo) && ((cFrom + 15) != cTo)) return(FALSE); } - - if ((IS_SPECIAL_MOVE(mv)) && + + if ((IS_SPECIAL_MOVE(mv)) && (!mv.pPromoted) && (pos->cEpSquare != cTo)) return(FALSE); return(TRUE); } else { - // + // // If the pawn is not capturing, the TO square better be empty. - // + // if (!IS_EMPTY(pos->rgSquare[cTo].pPiece)) return(FALSE); - - // + + // // A non-capturing pawn can only push forward. One or two squares // based on whether or not its the initial move. // if (WHITE == GET_COLOR(p)) { if (cTo == cFrom - 16) return(TRUE); - if ((cTo == cFrom - 32) && + if ((cTo == cFrom - 32) && (RANK2(cFrom)) && (IS_EMPTY(pos->rgSquare[cFrom - 16].pPiece))) return(TRUE); } @@ -982,42 +982,42 @@ Return value: COOR cAttack; COOR cKing; int iDelta; - + ASSERT(!IS_PAWN(p)); // // Handle castling - // + // if (IS_CASTLE(mv)) { ASSERT(IS_KING(mv.pMoved)); ASSERT(IS_SPECIAL_MOVE(mv)); return(TRUE); } - + // // Make sure the piece moves in the way the move describes. - // + // iIndex = (int)cFrom - (int)cTo; if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << pPieceType))) { return(FALSE); } - + // // If we get here the piece can move in the way described by the // move but we still have to check to see if there are any other // pieces in the way if the piece moved is not a king or knight // (pawns handled in their own routine, see above). - // + // if ((pPieceType == QUEEN) || (pPieceType == ROOK) || (pPieceType == BISHOP)) { iDelta = CHECK_DELTA_WITH_INDEX(iIndex); for (cBlock = cFrom + iDelta; - cBlock != cTo; + cBlock != cTo; cBlock += iDelta) { if (!IS_EMPTY(pos->rgSquare[cBlock].pPiece)) @@ -1027,15 +1027,15 @@ Return value: } if (cBlock != cTo) return(FALSE); } - + // // Ok the piece can move the way they asked and there are no other // pieces in the way... but it cannot expose its own king to check // by doing so! // cKing = KingPosition(pos, pos->uToMove); - cAttack = ExposesCheck(pos, - cFrom, + cAttack = ExposesCheck(pos, + cFrom, cKing); if ((ILLEGAL_COOR != cAttack) && (cTo != cAttack)) { @@ -1044,7 +1044,7 @@ Return value: // does expose the king to check and the move does not capture the // checking piece. But it's still ok as long as the TO location // still blocks the checking piece. - // + // iIndex = cAttack - cKing; iDelta = CHECK_DELTA_WITH_INDEX(iIndex); for (cBlock = cAttack + iDelta; @@ -1059,12 +1059,12 @@ Return value: // // Legal! - // + // return(TRUE); } -FLAG +FLAG SanityCheckMove(POSITION *pos, MOVE mv) /** @@ -1084,7 +1084,7 @@ Return value: **/ { register PIECE p = mv.pMoved; - + if (0 == mv.uMove) { return(FALSE); @@ -1106,7 +1106,7 @@ Return value: { return(FALSE); } - + if (!IS_SPECIAL_MOVE(mv)) { if (mv.pCaptured) @@ -1122,7 +1122,7 @@ Return value: return(FALSE); } } - + if (!IS_EMPTY(pos->rgSquare[mv.cTo].pPiece) && !mv.pCaptured) { @@ -1136,7 +1136,7 @@ Return value: return(FALSE); } } - + if (IS_PAWN(p)) { return(_SanityCheckPawnMove(pos, mv)); @@ -1148,7 +1148,7 @@ Return value: } -FLAG +FLAG LooksLikeFile(CHAR c) /** @@ -1198,9 +1198,9 @@ Return value: } return(FALSE); } - -FLAG + +FLAG LooksLikeCoor(char *szData) /** @@ -1220,7 +1220,7 @@ Return value: { char f = szData[0]; char r = szData[1]; - + if (LooksLikeFile(f) && LooksLikeRank(r)) { @@ -1252,7 +1252,7 @@ Return value: static CHAR szStripped[10]; ULONG y; CHAR *p; - + memset(szStripped, 0, sizeof(szStripped)); p = szMove; y = 0; @@ -1289,11 +1289,11 @@ Return value: CHAR *szStripped = StripMove(szData); ULONG u; static CHAR cPieces[] = { 'P', 'N', 'B', 'R', 'Q', 'K', '\0' }; - + // // A (stripped) move must be at least two characters long even in // SAN. - // + // if ((strlen(szStripped) < 2) || (strlen(szStripped) > 7)) { return(NOT_MOVE); @@ -1309,7 +1309,7 @@ Return value: { return(MOVE_SAN); } - + // // COOR COOR (d2d4) // @@ -1318,37 +1318,37 @@ Return value: { return(MOVE_ICS); } - + // // COOR (d4) - // + // if (LooksLikeCoor(szStripped) && strlen(szStripped) == 2) { return(MOVE_SAN); } - + // // RANK COOR - // + // if ((LooksLikeRank(szStripped[0]) || LooksLikeFile(szStripped[0])) && LooksLikeCoor(&szStripped[1])) { return(MOVE_SAN); } - + // // PIECE COOR COOR // PIECE COOR // PIECE FILE COOR // PIECE RANK COOR - // + // for (u = 0; u < ARRAY_LENGTH(cPieces); u++) { if (szStripped[0] == cPieces[u]) // must be uppercase! { - if (LooksLikeCoor(&szStripped[1])) + if (LooksLikeCoor(&szStripped[1])) { return(MOVE_SAN); } @@ -1367,7 +1367,7 @@ Return value: // // (COORD)=(PIECE) - // + // if ((strlen(szStripped) == 4) && LooksLikeCoor(szStripped) && szStripped[2] == '=') @@ -1392,7 +1392,7 @@ Return value: } -void FASTCALL +void FORCEINLINE SelectBestWithHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u) /** @@ -1424,7 +1424,7 @@ Return value: MOVE mvLast; ULONG uPrevContKey = 0; FLAG fHaveContinuation = FALSE; - COOR cEnprise; + // COOR cEnprise; ASSERT(ctx->sMoveStack.uBegin[ctx->uPly] <= uEnd); ASSERT(u >= ctx->sMoveStack.uBegin[ctx->uPly]); @@ -1442,7 +1442,7 @@ Return value: // answer than an earlier one. Recomputed every call (2026-09-16: // briefly hoisted to the caller, reverted after a DEBUG-build // ASSERT here caught the mismatch -- see git log). - cEnprise = FindEnprisePiece(ctx, ctx->sPosition.uToMove); + // cEnprise = FindEnprisePiece(ctx, ctx->sPosition.uToMove); // Continuation history: "did a move like this tend to fail high // right after a move like that?", keyed by (previous move, this @@ -1468,16 +1468,14 @@ Return value: if (!IS_CAPTURE_OR_PROMOTION(mv)) { iBestVal += g_HistoryCounters[mv.pMoved][mv.cTo]; - if (TRUE == fHaveContinuation) - { - iBestVal += CONTINUATION_SCALE * - g_ContinuationHistory[(uPrevContKey * CONT_KEY_RANGE) + - MOVE_TO_CONT_KEY(mv)]; - } - if (mv.cFrom == cEnprise) - { - iBestVal += FLEE_BONUS; - } + iBestVal += ( + fHaveContinuation * + CONTINUATION_SCALE * + g_ContinuationHistory[(uPrevContKey * CONT_KEY_RANGE) + MOVE_TO_CONT_KEY(mv)]); + //if (mv.cFrom == cEnprise) + //{ + // iBestVal += FLEE_BONUS; + //} } uLoc = u; @@ -1488,15 +1486,14 @@ Return value: if (!IS_CAPTURE_OR_PROMOTION(mv)) { iVal += g_HistoryCounters[mv.pMoved][mv.cTo]; - if (TRUE == fHaveContinuation) - { - iVal += g_ContinuationHistory[(uPrevContKey * CONT_KEY_RANGE) + - MOVE_TO_CONT_KEY(mv)]; - } - if (mv.cFrom == cEnprise) - { - iVal += FLEE_BONUS; - } + iVal += ( + fHaveContinuation * + CONTINUATION_SCALE * + g_ContinuationHistory[(uPrevContKey * CONT_KEY_RANGE) + MOVE_TO_CONT_KEY(mv)]); +// if (mv.cFrom == cEnprise) +// { +// iVal += FLEE_BONUS; +// } } if (iVal > iBestVal) { @@ -1514,7 +1511,7 @@ Return value: } -void FASTCALL +void FORCEINLINE SelectBestNoHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u) /** @@ -1542,11 +1539,11 @@ Return value: ULONG uLoc; SCORE iVal; MOVE_STACK_MOVE_VALUE_FLAGS mvfTemp; - + ASSERT(ctx->sMoveStack.uBegin[ctx->uPly] <= uEnd); ASSERT(u >= ctx->sMoveStack.uBegin[ctx->uPly]); ASSERT(u < uEnd); - + // // Linear search from u..ctx->sMoveStack.uEnd[ctx->uPly] for the // move with the best value. @@ -1571,7 +1568,7 @@ Return value: ctx->sMoveStack.mvf[uLoc] = mvfTemp; } -void FASTCALL +void FASTCALL SelectMoveAtRoot(SEARCHER_THREAD_CONTEXT *ctx, ULONG u) /** @@ -1595,12 +1592,12 @@ Return value: ULONG uLoc = v; SCORE iVal; MOVE_STACK_MOVE_VALUE_FLAGS mvfTemp; - + ASSERT(ctx->sMoveStack.uBegin[ctx->uPly] <= uEnd); ASSERT(u >= ctx->sMoveStack.uBegin[ctx->uPly]); ASSERT(u < uEnd); ASSERT(MOVE_COUNT(ctx, ctx->uPly) >= 1); - + // // Find the first move that we have not already searched. It will // provide our initial iBestVal. @@ -1648,7 +1645,7 @@ static UINT64 g_uPerftNodeCount; static UINT64 g_uPerftTotalNodes; static UINT64 g_uPerftGenerates; -void +void Perft(SEARCHER_THREAD_CONTEXT *ctx, ULONG uDepth) { @@ -1658,16 +1655,16 @@ Perft(SEARCHER_THREAD_CONTEXT *ctx, g_uPerftTotalNodes += 1; ASSERT(uDepth < MAX_PLY_PER_SEARCH); - if (uDepth == 0) + if (uDepth == 0) { g_uPerftNodeCount += 1; return; } - + mv.uMove = 0; g_uPerftGenerates += 1; GenerateMoves(ctx, mv, GENERATE_DONT_SCORE); - + uPly = ctx->uPly; for (u = ctx->sMoveStack.uBegin[uPly]; u < ctx->sMoveStack.uEnd[uPly]; @@ -1688,13 +1685,13 @@ COMMAND(PerftCommand) Routine description: This function implements the 'perft' engine command. I have no - idea what 'perft' means but some people use this command to + idea what 'perft' means but some people use this command to benchmark the speed of the move generator code. Note: the way this is implemented here does nothing whatsoever with the move scoring code (i.e. the SEE) Usage: - + perft <depth> Parameters: @@ -1718,30 +1715,30 @@ Return value: double dBegin, dTime; double dNps; - if (argc < 2) + if (argc < 2) { Trace("Usage: perft <required_depth>\n"); return; } uDepth = atoi(argv[1]); - if (uDepth >= MAX_DEPTH_PER_SEARCH) + if (uDepth >= MAX_DEPTH_PER_SEARCH) { Trace("Error: invalid depth.\n"); return; } InitializeLightweightSearcherContext(pos, &ctx); - + g_uPerftTotalNodes = g_uPerftGenerates = 0ULL; dBegin = SystemTimeStamp(); - for (u = 1; u <= uDepth; u++) + for (u = 1; u <= uDepth; u++) { g_uPerftNodeCount = 0ULL; Perft((SEARCHER_THREAD_CONTEXT *)&ctx, u); Trace("%u. %" COMPILER_LONGLONG_UNSIGNED_FORMAT " node%s, " "%" COMPILER_LONGLONG_UNSIGNED_FORMAT " generate%s.\n", - u, - g_uPerftNodeCount, + u, + g_uPerftNodeCount, (g_uPerftNodeCount > 1) ? "s" : "", g_uPerftGenerates, (g_uPerftGenerates > 1) ? "s" : ""); @@ -1751,8 +1748,8 @@ Return value: dNps /= dTime; Trace("%" COMPILER_LONGLONG_UNSIGNED_FORMAT " total nodes, " "%" COMPILER_LONGLONG_UNSIGNED_FORMAT " total generates " - "in %6.2f seconds.\n", - g_uPerftTotalNodes, + "in %6.2f seconds.\n", + g_uPerftTotalNodes, g_uPerftGenerates, dTime); Trace("That's approx %.0f moves/sec\n", dNps); diff --git a/src/probe.c b/src/probe.c index 269a1b9..d08ef75 100644 --- a/src/probe.c +++ b/src/probe.c @@ -515,28 +515,21 @@ Return value: (mv.cTo == cTo) && (mv.pPromoted == pPromotes)) { - // Root-only: use the DTZ ply count tb_probe_root already - // packed into uResult (no extra probe) to report a real, - // ply-distance-aware mate score instead of the flat - // EGTB_WIN_SCORE/LOSS_SCORE used by in-search probes - // (ProbeEGTB/RecognLookup) -- those stay WDL-only and - // untouched. util.c's SCORE-to-string formatter already - // treats any |score| >= NMATE as a mate score and derives - // the move count as (INFINITY_SCORE - iScore) / 2, so - // INFINITY_SCORE -/+ uDtz round-trips through that - // unmodified as "+MATE<n>"/"-MATE<n>". Clamp defensively: - // a uDtz large enough to push the score below NMATE would - // just silently fall back to plain-decimal display, not - // corrupt anything, but keep it a real mate score at every - // legal DTZ value we could plausibly see (TB_MAX_PLY=256 - // exceeds NMATE's 200-point margin in principle, however - // unlikely in practice for <=7-man tables). - ULONG uDtz = TB_GET_DTZ(uResult); - if (uDtz >= (+INFINITY_SCORE - NMATE)) uDtz = (+INFINITY_SCORE - NMATE - 1); + // DTZ (distance to the next fifty-move-rule-zeroing move) + // is not the same thing as distance to mate -- a previous + // version of this code reported (INFINITY_SCORE -/+ uDtz) + // as a mate score, which is wrong in general: DTZ can be + // small in a dead-won position that's still many plies + // from actual mate (e.g. right as pos->uFifty climbs + // towards 100, a reset becomes forced soon even though + // mate itself is nowhere near), producing a bogus + // "+MATE<n>"/"-MATE<n>" display untethered from reality. + // The draw/cursed-win/blessed-loss cases already returned + // FALSE above, so uWdl here is only ever TB_WIN or + // TB_LOSS -- report the same flat mate-magnitude score + // ProbeEGTB/RecognLookup use at interior nodes instead. *pmv = mv; - *piScore = (TB_WIN == uWdl) ? - (SCORE)(+INFINITY_SCORE - uDtz) : - (SCORE)(-INFINITY_SCORE + uDtz); + *piScore = (TB_WIN == uWdl) ? +NMATE : -NMATE; return(TRUE); } } diff --git a/src/recogn.c b/src/recogn.c index 427297b..3263908 100644 --- a/src/recogn.c +++ b/src/recogn.c @@ -2124,7 +2124,7 @@ Return value: } -ULONG +ULONG FORCEINLINE RecognLookup(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piScore, IN ULONG uEgtbProbeMode) @@ -2245,7 +2245,7 @@ Return value: // mate distance to adjust for the present ply here (see // EGTB_WIN_SCORE / EGTB_LOSS_SCORE in probe.c). *piScore = iScore; - return(RECOGN_EXACT); + return(RECOGN_EGTB); } } } @@ -1453,7 +1453,7 @@ Return value: ctx->uRootDepth = 0; ctx->sPlyInfo[0].PV[0] = mv; ctx->sPlyInfo[0].PV[1] = NULLMOVE; - strcpy(ctx->szLastPV, "tb"); + strcpy(ctx->szLastPV, "<EGTB>"); if (!g_Options.fPondering) { Trace("TABLEBASE MOVE --> stop searching now\n"); g_MoveTimer.bvFlags |= TIMER_STOPPING; @@ -1710,8 +1710,6 @@ Return value: ctx->sCounters.tree.u64NCRealLoopNodeSum[u]; } #endif - g_MoveTimer.dEndTime = SystemTimeStamp(); - // // Here we are at the end of a search. If we were: // @@ -1737,6 +1735,7 @@ Return value: ret.eResult = RESULT_IN_PROGRESS; ret.szDescription[0] = '\0'; end: + g_MoveTimer.dEndTime = SystemTimeStamp(); return ret; } diff --git a/src/search.c b/src/search.c index d8ece8d..fcf8907 100644 --- a/src/search.c +++ b/src/search.c @@ -725,13 +725,19 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, case UNRECOGNIZED: break; case RECOGN_EXACT: - case RECOGN_EGTB: if ((iAlpha < iScore) && (iScore < iBeta)) { UpdatePV(ctx, RECOGNMOVE); } iBestScore = iScore; goto end; + case RECOGN_EGTB: + if ((iAlpha < iScore) && (iScore < iBeta)) + { + UpdatePV(ctx, EGTBMOVE); + } + iBestScore = iScore; + goto end; case RECOGN_LOWER: if (iScore >= iBeta) { @@ -1073,14 +1079,18 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, // leftover (matches pre-retirement behavior). if (MOVE_SCORING_IID_RESCORED == pi->eMoveScoringScheme) { - SelectBestNoHistory(ctx, x); + SelectBestNoHistory(ctx, x); // mv's score is search-based + // WouldGiveCheck already computed in RescoreMovesViaSearch + mv = ctx->sMoveStack.mvf[x].mv; } else { SelectBestWithHistory(ctx, x); + mv = ctx->sMoveStack.mvf[x].mv; + mv.bvFlags |= WouldGiveCheck(ctx, mv); + ctx->sMoveStack.mvf[x].mv = mv; // re-store WouldGiveCheck's result fIsLeftoverMove = (ctx->sMoveStack.mvf[x].iValue < GOOD_MOVE); } - mv = ctx->sMoveStack.mvf[x].mv; #ifdef DEBUG ASSERT(0 == (ctx->sMoveStack.mvf[x].bvFlags & MVF_MOVE_SEARCHED)); @@ -1107,7 +1117,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, uCMEvidenceBucket = _CMEvidenceBucket(uEvidence); } #endif - mv.bvFlags |= WouldGiveCheck(ctx, mv); // Note: x is the index of the NEXT move to be // considered, this move's index is (x-1). @@ -3021,13 +3030,19 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx, case UNRECOGNIZED: break; case RECOGN_EXACT: - case RECOGN_EGTB: if ((iAlpha < iScore) && (iScore < iBeta)) { UpdatePV(ctx, RECOGNMOVE); } iBestScore = iScore; goto end; + case RECOGN_EGTB: + if ((iAlpha < iScore) && (iScore < iBeta)) + { + UpdatePV(ctx, EGTBMOVE); + } + iBestScore = iScore; + goto end; case RECOGN_LOWER: if (iScore >= iBeta) { @@ -3238,7 +3253,17 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx, // If fIncludeChecks is FALSE then we still need to see if // this move is going to check the opponent; GenerateMoves // didn't do it for us to save time in the event of a fail - // high. + // high. Tried swapping this for a post-MakeMove InCheck call + // instead (the theory: at most one query, nothing to amortize + // WouldGiveCheck's table-build against) -- measured worse on + // 2 of 3 curated suites (ringers +7.2%, confident_quick +5.9%, + // hard_quick -1.2%, sd 10, node counts identical either way). + // The assumption was wrong: this loop tries more than one move + // per node whenever an earlier candidate raises alpha without + // failing high, and the lazy-built bbUnblocked[uPly] (see + // WouldGiveCheck) gets reused cheaply across all of those, + // while a fresh InCheck call pays full price every time with + // no reuse at all. Reverted -- WouldGiveCheck stays here. if (FALSE == fIncludeChecks) { mv.bvFlags |= WouldGiveCheck(ctx, mv); diff --git a/src/searchsup.c b/src/searchsup.c index ca75c92..726bc9f 100644 --- a/src/searchsup.c +++ b/src/searchsup.c @@ -51,7 +51,7 @@ Return value: this->PV[u] = mv; u++; - // HASHMOVE/RECOGNMOVE/DRAWMOVE/MATEMOVE are sentinels, not real + // HASHMOVE/RECOGNMOVE/DRAWMOVE/MATEMOVE/EGTBMOVE are sentinels, not real // moves -- WalkPV (util.c) treats any of them as a hard PV // truncation point and asserts the slot right after one is // always a zero terminator. Copying the child ply's PV tail @@ -67,7 +67,8 @@ Return value: if ((mv.uMove == HASHMOVE.uMove) || (mv.uMove == RECOGNMOVE.uMove) || (mv.uMove == DRAWMOVE.uMove) || - (mv.uMove == MATEMOVE.uMove)) + (mv.uMove == MATEMOVE.uMove) || + (mv.uMove == EGTBMOVE.uMove)) { if (u < MAX_PLY_PER_SEARCH) { @@ -805,7 +806,7 @@ Return value: return TRUE; } -INT +INT FORCEINLINE GetLMRReduction(IN SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uRemainingDepth, IN ULONG uLegalMoves, @@ -1282,15 +1283,16 @@ Return value: } -void ComputeMoveExtension(IN SEARCHER_THREAD_CONTEXT *ctx, - IN SCORE iAlpha, - IN SCORE iBeta, - IN ULONG uMoveNum, - IN SCORE iRoughEval, - IN ULONG uDepth, - IN SCORE iMoveSee, - IN FLAG fIsLeftoverMove, - IN OUT INT *piExtend) +void FORCEINLINE +ComputeMoveExtension(IN SEARCHER_THREAD_CONTEXT *ctx, + IN SCORE iAlpha, + IN SCORE iBeta, + IN ULONG uMoveNum, + IN SCORE iRoughEval, + IN ULONG uDepth, + IN SCORE iMoveSee, + IN FLAG fIsLeftoverMove, + IN OUT INT *piExtend) /** Routine description: @@ -1629,6 +1631,7 @@ Return value: SelectBestNoHistory(ctx, x); mv = ctx->sMoveStack.mvf[x].mv; mv.bvFlags |= WouldGiveCheck(ctx, mv); + ctx->sMoveStack.mvf[x].mv = mv; // <-- update the move's flags, WouldGiveCheck is slow if (TRUE == MakeMove(ctx, mv)) { if (-INFINITY_SCORE == iBestScore) @@ -1886,7 +1889,7 @@ Return value: } -FLAG +FLAG FORCEINLINE CommonSearchInit(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piAlpha, IN OUT SCORE *piBeta, @@ -206,7 +206,7 @@ Return value: // single-bit bitboard of the actual king square is either that bit or // zero, same O(1)-table-lookup shape the knight line already uses, no // DISTANCE()==1 branch needed. -BITBOARD +BITBOARD FORCEINLINE _WhoAttacksSquareBB(IN POSITION *pos, IN COOR cSquare, IN ULONG uSide, @@ -258,7 +258,7 @@ Return value: return bbAttackers; } -void CDECL +void FORCEINLINE _GetAttacksBB(IN OUT SEE_LIST *pList, IN POSITION *pos, IN COOR cSquare, @@ -319,17 +319,14 @@ Return value: // no mailbox load -- entirely replacing what iSeeDelta/pPawn[] // used to do at runtime; only the (0-2) actual hits still need a // COOR to populate the SEE_LIST. + BITBOARD bbPawnHits = g_PawnAttackOriginBB[uSide][cSquare] & + pos->bbPawns[uSide]; + ULONG uPawnBit; + while (bbPawnHits) { - BITBOARD bbPawnHits = g_PawnAttackOriginBB[uSide][cSquare] & - pos->bbPawns[uSide]; - ULONG uPawnBit; - - while (bbPawnHits) - { - uPawnBit = FastFirstBit(bbPawnHits) - 1; - bbPawnHits &= (bbPawnHits - 1); - ADD_ATTACKER(pPawn[uSide], BIT_NUMBER_TO_COOR(uPawnBit), VALUE_PAWN); - } + uPawnBit = FastFirstBit(bbPawnHits) - 1; + bbPawnHits &= (bbPawnHits - 1); + ADD_ATTACKER(pPawn[uSide], BIT_NUMBER_TO_COOR(uPawnBit), VALUE_PAWN); } // @@ -640,7 +637,6 @@ Return value: { pList->uCount--; } - #else // !SEE_HEAPS // @@ -698,7 +694,7 @@ Return value: #ifdef SEE_HEAPS -static PIECE +static PIECE FORCEINLINE _MinLegalPiece(IN POSITION *pos, IN ULONG uColor, IN SEE_LIST *pList, @@ -1008,7 +1004,7 @@ Return value: } -FLAG +FLAG FORCEINLINE SeeFastSign(IN POSITION *pos, IN MOVE mv) /** @@ -1084,15 +1080,18 @@ Return value: SCORE -SEE(IN POSITION *pos, - IN MOVE mv) +_SEEFull(IN POSITION *pos, + IN MOVE mv) /** Routine description: - Given a board and a move on the board, estimate the value of the - move by considering the friend/enemy pieces that attack the move's - destination square. + The real exchange-walk implementation -- see SEE()'s header + comment (below) for the cheap early-exit that now sits in front of + this, and _SEEFull's own non-static, un-inlined status so + testsee.c's TestSEENoDefenderFastPath can call it directly by name + for a correctness/speed comparison against SEE(), same convention + as _GenerateAllMoves/_GenerateAllMovesBB. Parameters: @@ -1244,3 +1243,75 @@ Return value: #endif return(rgiList[0]); } + +SCORE FORCEINLINE +SEE(IN POSITION *pos, + IN MOVE mv) +/** + +Routine description: + + Given a board and a move on the board, estimate the value of the + move by considering the friend/enemy pieces that attack the move's + destination square. + + Cheap early exit before paying for any of _SEEFull's work (two + GetAttacks calls, sort/heap of both, _AddXRays): if the defending + side has NO attacker on mv.cTo -- checked the same x-ray-aware way + SeeFastSign already does (mv.cFrom removed from occupancy, so a + slider the mover was itself blocking gets counted) -- the exchange + can never continue past our own capture, so the true SEE value is + exactly PIECE_VALUE(mv.pCaptured) + PIECE_VALUE(mv.pPromoted). This + is not an approximation: it is the exact value _SEEFull's own + do/while loop would compute on this input (its first + _MinLegalPiece call for the defender would find nothing and break + immediately, returning rgiList[0] unchanged) -- see + TestSEENoDefenderFastPath (testsee.c) for the exact-value + correctness gate against _SEEFull, not just a sign check. + + Unlike SeeFastSign (which only ever needs the sign and so can + throw the magnitude away), callers of SEE() need the real value -- + _ScoreAllMoves/_ScoreQSearchMoves* add an ordering bias on top of + it and DecodeMoveValueEstimate (searchsup.c) later subtracts that + bias back out to recover this exact number for real pruning + decisions in _ShouldWeConsiderThisMove. So this early exit has to + return the true value, not just resolve a sign and hand back a + placeholder -- which is exactly what it does here, no different + from what the full exchange walk would have produced. + + Pawn defenders can't be x-ray-revealed by the mover's own + departure (a pawn's attack squares are a fixed 2-square delta, not + a blockable ray), so that check alone uses the raw board, matching + SeeFastSign's identical reasoning. + +Parameters: + + POSITION *pos, + MOVE mv + +Return value: + + SCORE : the estimate of the move's score + +**/ +{ + ULONG uDefender = FLIP(GET_COLOR(mv.pMoved)); + BITBOARD bbOccupiedWithoutMover; + + if (g_PawnAttackOriginBB[uDefender][mv.cTo] & pos->bbPawns[uDefender]) + { + return _SEEFull(pos, mv); + } + bbOccupiedWithoutMover = pos->bbOccupied & ~COOR_TO_BB(mv.cFrom); + if (_WhoAttacksSquareBB(pos, mv.cTo, uDefender, bbOccupiedWithoutMover)) + { + return _SEEFull(pos, mv); + } + + // + // No defender at all -- the exchange can't continue past our own + // move, so the result is exactly the material gained (captured + + // promoted). + // + return (SCORE)(PIECE_VALUE(mv.pCaptured) + PIECE_VALUE(mv.pPromoted)); +} diff --git a/src/split.c b/src/split.c index abb6a11..0f6958f 100644 --- a/src/split.c +++ b/src/split.c @@ -649,8 +649,11 @@ Return value: // NumLeftoverMovesToSelect budget comment). // SelectBestWithHistory(ctx, v); - ctx->sMoveStack.mvf[v].mv.bvFlags |= - WouldGiveCheck(ctx, ctx->sMoveStack.mvf[v].mv); + if (v != uMoveNum) // First one has WouldGiveCheck already computed + { + ctx->sMoveStack.mvf[v].mv.bvFlags |= + WouldGiveCheck(ctx, ctx->sMoveStack.mvf[v].mv); + } ASSERT(!(ctx->sMoveStack.mvf[v].bvFlags & MVF_MOVE_SEARCHED)); g_SplitInfo[u].mvf[g_SplitInfo[u].uRemainingMoves] = ctx->sMoveStack.mvf[v]; diff --git a/src/testgenerate.c b/src/testgenerate.c index c6947d2..95c5000 100644 --- a/src/testgenerate.c +++ b/src/testgenerate.c @@ -697,4 +697,326 @@ TestGenerateAllMovesSpeed(void) } SystemFreeMemory(ctx); } + +// +// board_representation/MOVEGEN_MIGRATION.md's never-started +// _FindUnblockedSquares/WouldGiveCheck candidate. Unlike every other +// benchmark in this file, this does NOT measure the generator call in +// isolation: _FindUnblockedSquares writes ~20 square entries that may +// never be read, and WouldGiveCheck reads only 1-3 of them per move, +// so a bitboard version that looks like a big win building the table +// can give some of it back at query time. The real per-node cost is +// the pair -- one _FindUnblockedSquares(BB) call followed by +// WouldGiveCheck over every move actually generated at that node -- +// so that's what gets timed here, same interleaved-call methodology +// as the rest of this file. WouldGiveCheck's own body is selected at +// compile time by UNBLOCKED_BITBOARD (SQUARE_IS_UNBLOCKED/ +// DIR_FROM_SQ_TO_KING), so unlike TestGenerateAllMovesSpeed this can't +// compare both implementations by name in one binary -- run this test +// in two builds (with and without -DUNBLOCKED_BITBOARD) and compare +// the printed cycles/pair across them. +// +void +TestFindUnblockedSquaresSpeed(void) +{ + static const char *rgszFen[3] = + { + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1", + "6k1/8/8/3K4/8/2NBRQ2/8/8 w - - 0 1", + }; + static const char *rgszLabel[3] = + { + "opening ", "middlegame", "endgame ", + }; + POSITION posBench; + SEARCHER_THREAD_CONTEXT *ctx; + UINT64 u64Total, u64Start; + ULONG uIter, u, uMove, uNumMoves; + ULONG uPly; + MOVE_STACK *pStack; + const ULONG uCallsPerPosition = 50000; + + Trace("Benchmarking _FindUnblockedSquares%s + WouldGiveCheck over " + "a generated move list (%lu calls/position) -- compare this " + "build's numbers against a build with the opposite " + "-DUNBLOCKED_BITBOARD setting...\n", +#ifdef UNBLOCKED_BITBOARD + "BB", +#else + " (mailbox)", +#endif + uCallsPerPosition); + + ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT)); + ASSERT(ctx); + pStack = &(ctx->sMoveStack); + uPly = ctx->uPly; + + for (u = 0; u < 3; u++) + { + FenToPosition(&posBench, (char *)rgszFen[u]); + InitializeSearcherContext(&posBench, ctx); + + // Generate once, outside the timed loop, to get a realistic + // move list for this position -- WouldGiveCheck's cost is + // what's under test here, not generation itself. + pStack->uEnd[uPly] = pStack->uBegin[uPly]; + pStack->sGenFlags[uPly].uAllGenFlags = 0; +#ifdef UNBLOCKED_BITBOARD + _FindUnblockedSquaresBB(pStack, &posBench); +#else + _FindUnblockedSquares(pStack, &posBench); +#endif + _GenerateAllMoves(pStack, &posBench); + uNumMoves = MOVE_COUNT(ctx, uPly); + ASSERT(uNumMoves > 0); + + u64Total = 0; + for (uIter = 0; uIter < uCallsPerPosition; uIter++) + { + u64Start = SystemReadTimeStampCounter(); +#ifdef UNBLOCKED_BITBOARD + _FindUnblockedSquaresBB(pStack, &posBench); +#else + _FindUnblockedSquares(pStack, &posBench); +#endif + for (uMove = pStack->uBegin[uPly]; + uMove < pStack->uEnd[uPly]; + uMove++) + { + WouldGiveCheck(ctx, pStack->mvf[uMove].mv); + } + u64Total += (SystemReadTimeStampCounter() - u64Start); + } + printf(" %s: %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " cycles/pair (%lu moves checked per call)\n", + rgszLabel[u], + u64Total / uCallsPerPosition, + uNumMoves); + } + SystemFreeMemory(ctx); +} + +#if defined(CAPTURES_PROMS_BITBOARD_PATH) + +static int +_CompareU32(const void *pA, const void *pB) +{ + const ULONG *a = (const ULONG *)pA; + const ULONG *b = (const ULONG *)pB; + if (*a < *b) return -1; + if (*a > *b) return 1; + return 0; +} + +// +// Correctness gate for _GenerateCapturesAndPromotions +// (generate.c, the GENERATE_CAPTURES_PROMS bitboard path): its output +// must equal _GenerateAllMoves' output filtered down to +// IS_CAPTURE_OR_PROMOTION(mv), as a SET (mv.uMove already encodes +// cFrom/cTo/pMoved/pCaptured/pPromoted/flags, so set membership is a +// direct ULONG compare -- no need to decode fields individually). Both +// generators read the identical board, so a true mismatch here means +// one of them is wrong, not a representational difference. Covers +// ordinary captures, capture-promotions, quiet push-promotions, en +// passant, and (implicitly, by their absence from both outputs) that +// castling and ordinary quiet pushes never show up in either side of +// the comparison. +// +void +TestCapturesAndPromotionsGenerator(void) +{ + static const char *rgszFen[] = + { + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1", + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R b KQkq - 0 1", + "8/PPP4k/8/8/8/8/4Kppp/8 w - - 0 1", + "8/PPP4k/8/8/8/8/4Kppp/8 b - - 0 1", + "3Q4/1Q4Q1/4Q3/2Q4R/Q4Q2/3Q4/1Q4Rp/1K1BBNNk w - - 0 1", + "rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3", + "rnbqkbnr/pp1p1ppp/8/2pPp3/8/8/PPP1PPPP/RNBQKBNR w KQkq c6 0 3", + "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPP1PPPP/RNBQKBNR b KQkq c6 0 2", + "8/1P6/8/8/8/8/7p/1k4K1 w - - 0 1", + "8/1P6/8/8/8/8/7p/1k4K1 b - - 0 1", + "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", + "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 b - - 0 1", + }; + POSITION pos; + SEARCHER_THREAD_CONTEXT *ctx; + MOVE_STACK *pStack; + ULONG uPly; + ULONG u, v; + ULONG uRefCount, uNewCount; + static ULONG rgRef[MAX_MOVES_PER_PLY]; + static ULONG rgNew[MAX_MOVES_PER_PLY]; + MOVE mv; + + Trace("Testing captures/promotions-only generator against " + "_GenerateAllMoves filtered to IS_CAPTURE_OR_PROMOTION...\n"); + + ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT)); + ASSERT(ctx); + pStack = &(ctx->sMoveStack); + uPly = ctx->uPly; + + for (u = 0; u < ARRAY_LENGTH(rgszFen); u++) + { + if (FALSE == FenToPosition(&pos, (char *)rgszFen[u])) + { + UtilPanic(TESTCASE_FAILURE, + NULL, "FenToPosition", NULL, NULL, + __FILE__, __LINE__); + } + InitializeSearcherContext(&pos, ctx); + + // GENERATE_CAPTURES_PROMS' own precondition (see GenerateMoves' + // ASSERT) -- none of the fixtures above should trip this, but + // don't assume it of a future addition. + if (TRUE == InCheck(&pos, pos.uToMove)) + { + continue; + } + + // Reference: full generation, filtered to captures/promotions. + pStack->uEnd[uPly] = pStack->uBegin[uPly]; + pStack->sGenFlags[uPly].uAllGenFlags = 0; + _GenerateAllMoves(pStack, &pos); + uRefCount = 0; + for (v = pStack->uBegin[uPly]; v < pStack->uEnd[uPly]; v++) + { + mv = pStack->mvf[v].mv; + if (IS_CAPTURE_OR_PROMOTION(mv)) + { + ASSERT(uRefCount < MAX_MOVES_PER_PLY); + rgRef[uRefCount++] = mv.uMove; + } + } + + // Candidate: the new generator, directly. + pStack->uEnd[uPly] = pStack->uBegin[uPly]; + pStack->sGenFlags[uPly].uAllGenFlags = 0; + _GenerateCapturesAndPromotions(pStack, &pos); + uNewCount = 0; + for (v = pStack->uBegin[uPly]; v < pStack->uEnd[uPly]; v++) + { + mv = pStack->mvf[v].mv; + ASSERT(IS_CAPTURE_OR_PROMOTION(mv)); + ASSERT(uNewCount < MAX_MOVES_PER_PLY); + rgNew[uNewCount++] = mv.uMove; + } + + if (uRefCount != uNewCount) + { + Trace("TestCapturesAndPromotionsGenerator: FEN %s: " + "reference produced %lu move(s), new produced %lu\n", + rgszFen[u], uRefCount, uNewCount); + UtilPanic(TESTCASE_FAILURE, NULL, + "TestCapturesAndPromotionsGenerator", NULL, NULL, + __FILE__, __LINE__); + } + + // Set equality -- the two generators walk pieces in different + // orders, so sort both before the element-by-element compare. + qsort(rgRef, uRefCount, sizeof(rgRef[0]), _CompareU32); + qsort(rgNew, uNewCount, sizeof(rgNew[0]), _CompareU32); + for (v = 0; v < uRefCount; v++) + { + if (rgRef[v] != rgNew[v]) + { + Trace("TestCapturesAndPromotionsGenerator: FEN %s: " + "move set mismatch at sorted index %lu " + "(ref 0x%08lx, new 0x%08lx)\n", + rgszFen[u], v, + (unsigned long)rgRef[v], (unsigned long)rgNew[v]); + UtilPanic(TESTCASE_FAILURE, NULL, + "TestCapturesAndPromotionsGenerator", NULL, NULL, + __FILE__, __LINE__); + } + } + } + SystemFreeMemory(ctx); +} + +// +// Speed gate: _GenerateCapturesAndPromotions must be faster than +// _GenerateAllMoves for the same position, interleaved-call +// methodology matching every other *Speed test in this file. Unlike +// the per-piece-type speed tests above, this isn't comparing two +// implementations of the same move set -- the new generator produces +// fewer moves by design -- so this is purely "does skipping the quiet +// moves up front actually cost less than generating and discarding +// them," the actual claim behind building this generator at all. +// +void +TestGenerateCapturesAndPromotionsSpeed(void) +{ + static const char *rgszFen[3] = + { + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1", + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", + }; + static const char *rgszLabel[3] = + { + "opening ", "middlegame", "tactical ", + }; + POSITION posBench; + SEARCHER_THREAD_CONTEXT *ctx; + UINT64 u64AllTotal, u64CapsTotal, u64Start; + ULONG uIter, u; + ULONG uPly; + const ULONG uCallsPerPosition = 200000; + + Trace("Benchmarking whole-node move generation: _GenerateAllMoves " + "vs _GenerateCapturesAndPromotions (interleaved, %lu " + "calls/position)...\n", + uCallsPerPosition); + + ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT)); + ASSERT(ctx); + uPly = ctx->uPly; + + for (u = 0; u < 3; u++) + { + FenToPosition(&posBench, (char *)rgszFen[u]); + InitializeSearcherContext(&posBench, ctx); + ASSERT(!InCheck(&posBench, posBench.uToMove)); + + u64AllTotal = 0; + u64CapsTotal = 0; + for (uIter = 0; uIter < uCallsPerPosition; uIter++) + { + ctx->sMoveStack.uEnd[uPly] = ctx->sMoveStack.uBegin[uPly]; + u64Start = SystemReadTimeStampCounter(); + _GenerateAllMoves(&ctx->sMoveStack, &posBench); + u64AllTotal += (SystemReadTimeStampCounter() - u64Start); + + ctx->sMoveStack.uEnd[uPly] = ctx->sMoveStack.uBegin[uPly]; + u64Start = SystemReadTimeStampCounter(); + _GenerateCapturesAndPromotions(&ctx->sMoveStack, &posBench); + u64CapsTotal += (SystemReadTimeStampCounter() - u64Start); + } + printf(" %s: _GenerateAllMoves %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " cycles/call, _GenerateCapturesAndPromotions %" + COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call " + "(captures-only is %.2fx all-moves)\n", + rgszLabel[u], + u64AllTotal / uCallsPerPosition, + u64CapsTotal / uCallsPerPosition, + (double)u64CapsTotal / (double)u64AllTotal); + if (u64CapsTotal >= u64AllTotal) + { + UtilPanic(TESTCASE_FAILURE, NULL, + "TestGenerateCapturesAndPromotionsSpeed: not faster", + NULL, NULL, __FILE__, __LINE__); + } + } + SystemFreeMemory(ctx); +} + +#endif // captures/promotions bitboard path + #endif diff --git a/src/testsee.c b/src/testsee.c index bbcd7e0..240ac39 100644 --- a/src/testsee.c +++ b/src/testsee.c @@ -624,4 +624,159 @@ TestWhoAttacksSquareMagic(void) } } } + +// +// Correctness + speed gate for SEE()'s no-defender early exit (see.c): +// SEE() must return the EXACT same value as _SEEFull() (not just the +// same sign -- TestSeeFastSign above already covers sign-only) on +// every move, since real callers (_ScoreAllMoves/_ScoreQSearchMoves*) +// trust the magnitude, not just the sign, for a winning capture. +// +// Speed deliberately uses four benchmark positions spanning the +// defended/undefended spectrum, not just a "best case": a dense +// tactical middlegame where nearly every capture has a defender (the +// new check's overhead pays for nothing -- this is the case to watch +// for a net LOSS, since every call still pays for the early check +// before falling through to the exact same _SEEFull work SEE() used +// to do directly), a bare-kings-plus-one-piece endgame where almost +// nothing is defended (the case the early exit is for), plus the +// standard opening/middlegame pair for a realistic middle ground. +// +void +TestSEENoDefenderFastPath(void) +{ + SEARCHER_THREAD_CONTEXT *ctx; + POSITION posBench; + MOVE rgmv[MAX_MOVE_STACK]; + ULONG uMoveCount; + ULONG u, x; + SCORE iFull, iFast; + const ULONG uPositions = 3000; + + Trace("Testing SEE() no-defender fast path against _SEEFull " + "(exact value, not just sign)...\n"); + ctx = (SEARCHER_THREAD_CONTEXT *)malloc(sizeof(SEARCHER_THREAD_CONTEXT)); + if (NULL == ctx) + { + return; + } + + for (u = 0; u < uPositions; u++) + { + GenerateRandomLegalPosition(&posBench); + InitializeSearcherContext(&posBench, ctx); + GenerateMoves(ctx, (MOVE){0}, GENERATE_DONT_SCORE); + + uMoveCount = 0; + for (x = ctx->sMoveStack.uBegin[ctx->uPly]; + x < ctx->sMoveStack.uEnd[ctx->uPly]; + x++) + { + rgmv[uMoveCount++] = ctx->sMoveStack.mvf[x].mv; + } + + for (x = 0; x < uMoveCount; x++) + { + iFull = _SEEFull(&(ctx->sPosition), rgmv[x]); + iFast = SEE(&(ctx->sPosition), rgmv[x]); + if (iFull != iFast) + { + Trace("move=0x%x from=%d to=%d moved=%d captured=%d " + "promoted=%d _SEEFull=%d SEE=%d\n", + rgmv[x].uMove, rgmv[x].cFrom, rgmv[x].cTo, + rgmv[x].pMoved, rgmv[x].pCaptured, rgmv[x].pPromoted, + iFull, iFast); + DumpPosition(&(ctx->sPosition)); + UtilPanic(TESTCASE_FAILURE, + &(ctx->sPosition), + "SEE/_SEEFull exact-value mismatch", + NULL, NULL, + __FILE__, __LINE__); + } + } + } + free(ctx); + + // + // Speed: same isolated, interleaved cycles/call methodology as the + // other benchmarks above. + // + { + static const char *rgszFen[4] = + { + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1", + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", + "8/5k2/8/3K4/8/8/8/4R3 w - - 0 1", + }; + static const char *rgszLabel[4] = + { + "opening ", "middlegame ", + "tactical/dense ", "sparse endgame ", + }; + SEARCHER_THREAD_CONTEXT *ctx2; + POSITION posBench2; + MOVE rgmv2[MAX_MOVE_STACK]; + ULONG uMoveCount2; + UINT64 u64FullTotal, u64FastTotal, u64Start; + ULONG uIter; + SCORE iSink; + const ULONG uCallsPerPosition = 200000; + + Trace("Benchmarking SEE(): _SEEFull (no early exit) vs SEE " + "(no-defender fast path), interleaved, %lu calls/" + "position...\n", + uCallsPerPosition); + ctx2 = (SEARCHER_THREAD_CONTEXT *)malloc(sizeof(SEARCHER_THREAD_CONTEXT)); + if (NULL == ctx2) + { + return; + } + + for (u = 0; u < 4; u++) + { + FenToPosition(&posBench2, (char *)rgszFen[u]); + InitializeSearcherContext(&posBench2, ctx2); + GenerateMoves(ctx2, (MOVE){0}, GENERATE_DONT_SCORE); + + uMoveCount2 = 0; + for (x = ctx2->sMoveStack.uBegin[ctx2->uPly]; + x < ctx2->sMoveStack.uEnd[ctx2->uPly]; + x++) + { + rgmv2[uMoveCount2++] = ctx2->sMoveStack.mvf[x].mv; + } + if (uMoveCount2 == 0) + { + continue; + } + + u64FullTotal = 0; + u64FastTotal = 0; + for (uIter = 0; uIter < uCallsPerPosition; uIter++) + { + ULONG uPair = uIter % uMoveCount2; + + u64Start = SystemReadTimeStampCounter(); + iSink = _SEEFull(&(ctx2->sPosition), rgmv2[uPair]); + u64FullTotal += (SystemReadTimeStampCounter() - u64Start); + + u64Start = SystemReadTimeStampCounter(); + iSink = SEE(&(ctx2->sPosition), rgmv2[uPair]); + u64FastTotal += (SystemReadTimeStampCounter() - u64Start); + (void)iSink; + } + printf(" %s: _SEEFull %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " cycles/call, SEE %" + COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call " + "(SEE is %.2fx _SEEFull, %u distinct moves)\n", + rgszLabel[u], + u64FullTotal / uCallsPerPosition, + u64FastTotal / uCallsPerPosition, + (double)u64FastTotal / (double)u64FullTotal, + uMoveCount2); + } + free(ctx2); + } +} #endif // TEST @@ -1111,6 +1111,12 @@ Return value: ASSERT(ctx->sPlyInfo[v].PV[u+1].uMove == 0); break; } + else if (mv.uMove == EGTBMOVE.uMove) + { + strncat(buf, "<EGTB>", ARRAY_LENGTH(buf) - strlen(buf) - 1); + ASSERT(ctx->sPlyInfo[v].PV[u+1].uMove == 0); + break; + } else { strncat(buf, MoveToSan(mv, &ctx->sPosition), |
