diff options
| author | Scott Gasch <[email protected]> | 2026-09-08 16:50:55 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-09-08 16:50:55 -0700 |
| commit | fbb138cc1dd13da2f30129206cdcd3d128344f54 (patch) | |
| tree | 7055d2344a378c5f9f064490f6b25822166518b6 | |
| parent | 434fa0406e1b01395a2b7f0aa481ca5dc367fa09 (diff) | |
Retire asm GetAttacks, recogn.c/fen.c bugfixes, misc bugfixes verified at parity
Confirmed self-play regression traced to a stale test_vs_head.sh reference
binary (typhoon_allbitboards/550ea81, deleted): every "vs head" comparison
since 92fc412 (Sep 4) was checking new work against that fixed Sep-4
snapshot, never against real HEAD or the working tree. Rebuilt clean
reference binaries directly from git and re-verified everything from
scratch.
This commit lands only the pieces confirmed safe against clean 434fa04
(fast st1 match, ~30-40 games, score ~0.44-0.55, consistent with parity;
plus a DEBUG-build smoke test pass):
- recogn.c, fen.c: real bugfixes
- data.c, draw.c, ics.c: whitespace only
- x64.asm: retires the asm GetAttacks implementation now that chess.h's
GetAttacks macro unconditionally selects the already-verified-faster
_GetAttacksBB bitboard version instead of a three-way build-flag
toggle (GETATTACKS_BITBOARD/CROUTINES/asm default)
- see.c, testsee.c: SEE/test-harness updates supporting that default
- root.c: per-tier eval-exit reporting (super-lazy counters currently
always read 0 -- accurate, since no super-lazy exit exists yet)
- main.c: startup banner update, InitEval() call, TestRecogn() added to
the #ifdef TEST self-test sequence
- command.c: InitEval() DNA-reload hook, new qsearchfutility diagnostic
- dynamic.c: minor changes
- chess.h: the GetAttacks default change above, three
FUTILITY_BASE_MARGIN_* compatibility aliases (all still equal to the
original flat FUTILITY_BASE_MARGIN -- search.c has not been split into
per-tier margins here), placeholder super-lazy counters, and an
EvalPasserRaces -> _EvalPasserRacesAgainstLoneKings rename (confirmed
byte-identical body) to match recogn.c's call site
- eval.c: the same rename, plus a no-op InitEval() stub (nothing to
initialize until the ROOK_FULL_HALF_OPEN_BONUS cache below exists)
Deliberately NOT included: the full eval.c overhaul (~1770 lines) and
search.c's qsearch-futility rework (~650 lines), including yesterday's
loosened SUPER_LAZY_MARGIN_BY_ARMY/FUTILITY_BASE_MARGIN_BY_SOURCE tables.
Reverting just those two tables while keeping the rest of the eval.c
overhaul still lost badly to 434fa04 (0.20 over 10 games), so the
regression isn't fully explained by the margins alone -- the eval.c
overhaul needs careful, incremental re-verification against this commit
as the new baseline, not a bulk re-apply. Full original work preserved in
git stash (stash@{0} as of this commit) for that follow-up.
Note: two pre-existing, position/state-dependent assertion crashes were
found during this verification (util.c:1093 WalkPV, recogn.c:1359
_SanityCheckRecognizers), both reproducing on unmodified 434fa04 -- not
introduced by anything here, not yet root-caused.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ka9o3S2eKqh4jNfmxVZ6fH
| -rwxr-xr-x | src/chess.h | 43 | ||||
| -rwxr-xr-x | src/command.c | 18 | ||||
| -rwxr-xr-x | src/data.c | 16 | ||||
| -rwxr-xr-x | src/draw.c | 12 | ||||
| -rwxr-xr-x | src/dynamic.c | 3 | ||||
| -rwxr-xr-x | src/eval.c | 15 | ||||
| -rwxr-xr-x | src/fen.c | 84 | ||||
| -rwxr-xr-x | src/ics.c | 2 | ||||
| -rwxr-xr-x | src/main.c | 12 | ||||
| -rw-r--r-- | src/recogn.c | 388 | ||||
| -rwxr-xr-x | src/root.c | 66 | ||||
| -rwxr-xr-x | src/see.c | 175 | ||||
| -rw-r--r-- | src/testsee.c | 106 | ||||
| -rw-r--r-- | src/x64.asm | 7 |
14 files changed, 491 insertions, 456 deletions
diff --git a/src/chess.h b/src/chess.h index 8d16837..7d01e64 100755 --- a/src/chess.h +++ b/src/chess.h @@ -945,6 +945,16 @@ typedef struct _COUNTERS UINT64 u64EvalHashHits; UINT64 u64LazyEvals; UINT64 u64FullEvals; + // Placeholder counters for root.c's per-tier eval-exit + // reporting -- eval.c's super-lazy exit itself hasn't been + // re-applied yet (see stash), so these always read 0 for now; + // that's accurate, not a stub bug, since no super-lazy exit + // exists in this build to increment them. + UINT64 u64SuperLazyEvals; + UINT64 u64CyclesSuperLazyExit; + UINT64 u64CyclesLazyExit; + UINT64 u64CyclesFullEvalExit; + UINT64 u64CyclesEvalSuperLazy; UINT64 u64CyclesInEval; // @@ -2812,6 +2822,13 @@ IsDraw(SEARCHER_THREAD_CONTEXT *ctx); // #define QPLIES_OF_NON_CAPTURE_CHECKS (2) #define FUTILITY_BASE_MARGIN (50) +// Compatibility aliases for root.c/main.c's per-tier reporting, which +// expects these three names -- search.c hasn't been split into +// per-tier margins yet (still one flat FUTILITY_BASE_MARGIN), so all +// three alias the same value until that split is re-applied. +#define FUTILITY_BASE_MARGIN_FULL FUTILITY_BASE_MARGIN +#define FUTILITY_BASE_MARGIN_LAZY FUTILITY_BASE_MARGIN +#define FUTILITY_BASE_MARGIN_SUPERLAZY FUTILITY_BASE_MARGIN // Measured: disabling this entirely (see lmr_testing/RESULTS.md) is a // clear net loss across ringers/confident_quick/hard_quick, so IID itself // is load-bearing. The "is the top move crappy" gate in search.c's DO_IID @@ -3146,7 +3163,14 @@ PawnHashLookup(SEARCHER_THREAD_CONTEXT *ctx); extern const int g_iAhead[2]; extern const int g_iBehind[2]; -ULONG +// No-op placeholder: eval.c's ROOK_FULL_HALF_OPEN_BONUS static cache +// (the thing InitEval() is meant to (re)build, called at startup and +// after every DNA reload) hasn't been re-applied yet -- see stash. +// Nothing to initialize until that cache exists. +void +InitEval(void); + +ULONG DNABufferSizeBytes(); char * @@ -3167,7 +3191,7 @@ SCORE Eval(SEARCHER_THREAD_CONTEXT *, SCORE, SCORE, SCORE *); FLAG -EvalPasserRaces(POSITION *, +_EvalPasserRacesAgainstLoneKings(POSITION *, PAWN_HASH_ENTRY *); ULONG @@ -3362,17 +3386,12 @@ _WhoAttacksSquareBB(POSITION *pos, ULONG uSide, BITBOARD bbOccupied); -// Three-way choice for which GetAttacks implementation is actually -// live -- see MIGRATION.md section 6: -// GETATTACKS_BITBOARD defined -> _GetAttacksBB (bitboard, new) -// else CROUTINES defined -> SlowGetAttacks (C mailbox) -// else (default) -> GetAttacks (asm x86/x64 mailbox, -// the literal function declared above) -#if defined(GETATTACKS_BITBOARD) +// _GetAttacksBB is the only implementation as of 2026-09-06 (see +// MIGRATION.md section 6) -- verified correct and faster than the old +// asm/CROUTINES mailbox versions (SlowGetAttacks, asm GetAttacks), +// which have been retired. Every call site written against the name +// "GetAttacks" didn't need touching when the default changed. #define GetAttacks _GetAttacksBB -#elif defined(CROUTINES) -#define GetAttacks SlowGetAttacks -#endif #ifdef _X86_ // diff --git a/src/command.c b/src/command.c index cf8f939..e6bf9fc 100755 --- a/src/command.c +++ b/src/command.c @@ -732,6 +732,13 @@ Return value: { Trace("Error reading dna file.\n"); } else { + // ROOK_FULL_HALF_OPEN_BONUS is a static cache of + // ROOK_ON_FULL_OPEN/ROOK_ON_HALF_OPEN_WITH_ENEMY/ + // ROOK_ON_HALF_OPEN_WITH_FRIEND, kept static rather than + // rebuilt every _EvalRook call since it's speed-critical + // code -- a DNA reload has to explicitly refresh it. + InitEval(); + // EvalCommand's persistent SEARCHER_THREAD_CONTEXT keeps // its embedded eval hash alive across calls for speed; a // DNA reload must invalidate stale cached scores from the @@ -807,11 +814,20 @@ Return value: #endif return; } + if (argc >= 2 && !STRCMPI(argv[1], "qsearchfutility")) + { +#ifdef CALIBRATE_QSEARCH_FUTILITY + DumpQSearchFutilityCalibration(); +#else + Trace("This binary was not built with CALIBRATE_QSEARCH_FUTILITY.\n"); +#endif + return; + } #ifdef CALIBRATE_POSITIONAL if ((argc < 2) || STRCMPI(argv[1], "dump")) { Trace("Usage: calibrate dump | calibrate basemargin | " - "calibrate marginsafety\n"); + "calibrate marginsafety | calibrate qsearchfutility\n"); return; } DumpPositionalCalibration(); @@ -363,15 +363,15 @@ InitializeSwapTable(void) 2, // 00011 = 3 4, // 00100 = 4 4, // 00101 = 5 - 4, // 00110 = 6 + 4, // 00110 = 6 4, // 00111 = 7 - 8, // 01000 = 8 + 8, // 01000 = 8 8, // 01001 = 9 8, // 01010 = 10 8, // 01011 = 11 8, // 01100 = 12 8, // 01101 = 13 - 8, // 01110 = 14 + 8, // 01110 = 14 8, // 01111 = 15 16, // 10000 = 16 16, // 10001 = 17 @@ -386,7 +386,7 @@ InitializeSwapTable(void) 16, // 11010 = 26 16, // 11011 = 27 16, // 11100 = 28 - 16, // 11101 = 29 + 16, // 11101 = 29 16, // 11110 = 30 16 // 11111 = 31 }; @@ -399,7 +399,7 @@ InitializeSwapTable(void) ULONG uGains[2]; ULONG uAttacks[2]; INT iDiff; - + for (p = 0; p <= WHITE_KING; p++) { if (p > 1) @@ -419,7 +419,7 @@ InitializeSwapTable(void) } // - // Ok, if the side on move has an attack, play it + // Ok, if the side on move has an attack, play it // and give them credit for some plunder. // while(uAttacks[uOnMove]) @@ -464,7 +464,7 @@ InitializeSwapTable(void) uAttacks[uOnMove] &= ~GET_HIGH_BIT[uAttacks[uOnMove]]; uOnMove = FLIP(uOnMove); } - + // // The side on move doesn't have an attack... does // the side not on move have an attack? If so they @@ -517,7 +517,7 @@ InitializeSwapTable(void) } } } - } + } } } @@ -29,10 +29,10 @@ IsDraw(SEARCHER_THREAD_CONTEXT *ctx) { ULONG uPly; UINT64 u64CurrentSig; - + // // Recognize 50-moves w/o progress draw rule - // + // if (ctx->sPosition.uFifty >= 100) { return(TRUE); @@ -40,7 +40,7 @@ IsDraw(SEARCHER_THREAD_CONTEXT *ctx) // // Check for repeated positions if needed. - // + // if (ctx->sPosition.uFifty < 4) { return(FALSE); @@ -52,7 +52,7 @@ IsDraw(SEARCHER_THREAD_CONTEXT *ctx) while(uPly < MAX_PLY_PER_SEARCH) { #ifdef DEBUG - if ((GET_COLOR(ctx->sPlyInfo[uPly].mv.pMoved) != + if ((GET_COLOR(ctx->sPlyInfo[uPly].mv.pMoved) != ctx->sPosition.uToMove) && (ctx->sPlyInfo[uPly].mv.uMove != 0)) { @@ -65,7 +65,7 @@ IsDraw(SEARCHER_THREAD_CONTEXT *ctx) { return(TRUE); } - + if (IS_PAWN(ctx->sPlyInfo[uPly].mv.pMoved) || (ctx->sPlyInfo[uPly].mv.pCaptured)) { @@ -73,7 +73,7 @@ IsDraw(SEARCHER_THREAD_CONTEXT *ctx) } uPly -= 2; } - + // // Keep looking in the official game record. // diff --git a/src/dynamic.c b/src/dynamic.c index a923e1a..992b842 100755 --- a/src/dynamic.c +++ b/src/dynamic.c @@ -618,6 +618,7 @@ Return value: FLAG fHaveValue = FALSE; COOR c, c1; + // Two different pieces hanging. if (_EnpriseSlotValid(ctx, 0, uSide) && _EnpriseSlotValid(ctx, 1, uSide)) { c = ctx->cEnprise[uPly][0]; @@ -627,6 +628,8 @@ Return value: PIECE_VALUE(ctx->sPosition.rgSquare[c1].pPiece)); fHaveValue = TRUE; } + + // A trapped piece under attack with nowhere to run. if (_TrappedSlotValid(ctx, uSide)) { c = ctx->cTrapped[uPly]; @@ -4386,7 +4386,7 @@ Return value: FLAG -EvalPasserRaces(IN OUT POSITION *pos, +_EvalPasserRacesAgainstLoneKings(IN OUT POSITION *pos, IN PAWN_HASH_ENTRY *pHash) /** @@ -4427,7 +4427,7 @@ Return value: // Both deferred past the no-passer early return above -- no need to // pay for this on the common case (most positions have no passer at - // all, and EvalPasserRaces itself is only even called when a side + // all, and _EvalPasserRacesAgainstLoneKings itself is only even called when a side // is down to a bare king, so this function runs unconditionally in // the pre-lazy-exit segment whenever that's true). uRacerDist[BLACK] = 99; @@ -4962,6 +4962,15 @@ Return value: +void +InitEval(void) +{ + // No-op placeholder: nothing to initialize until the + // ROOK_FULL_HALF_OPEN_BONUS static cache (from the eval.c overhaul, + // not yet re-applied here -- see stash) exists. +} + + SCORE Eval(IN SEARCHER_THREAD_CONTEXT *ctx, IN SCORE iAlpha, @@ -5074,7 +5083,7 @@ Return value: if ((pos->uNonPawnCount[WHITE][0] == 1) || (pos->uNonPawnCount[BLACK][0] == 1)) { - (void)EvalPasserRaces(pos, pHash); + (void)_EvalPasserRacesAgainstLoneKings(pos, pHash); #ifdef EVAL_DUMP Trace("After passer races:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); @@ -588,32 +588,94 @@ Return value: **/ { - ULONG u = 0; + ULONG u = 1; + ULONG uChunkIndex = 0; int i; CHAR *q, *op; - + p->uFifty = 0; + // + // A FEN that omits the halfmove-clock/fullmove-number suffix + // entirely (routine in EPD test suites like tests/ecm.ep_ -- e.g. + // "... w - -" with nothing after the en passant field) makes + // szFifty NULL here (FindChunk(szCapturedFen, 5) found no 5th + // chunk at all). The original code happened to survive that + // silently because FindChunk(NULL, 0) hits its "0 means whole + // string" fast path and returns NULL without ever dereferencing + // sz; starting at chunk 1 below (see the comment further down) + // loses that accidental safety net since chunk-1 lookups always + // dereference sz. Handle it explicitly instead of relying on + // FindChunk's early-return shape again -- found live by + // precommit_check.sh's debug_smoke_test.sh crashing on the very + // first random ecm.ep_ position after the chunk-index fix below + // was added. + // + if (NULL == szFifty) + { + return; + } + // + // Start at chunk 1 (FindChunk's convention: 0 means "whole + // remaining string", 1 means "the first token"), not 0. Starting + // at 0 here made this loop visit the first token twice -- once via + // the u==0 "whole string" fetch below, again via the very first + // u==1 fetch inside the loop -- throwing off the uChunkIndex + // bookkeeping used to tell the halfmove clock (real chunk 1) apart + // from the fullmove number (real chunk 2) below. + // q = FindChunk(szFifty, u); u++; while(NULL != q) { //printf("%u: %s\n", u-1, q); - - i = atoi(q); - if ((i > 0) && (i < 100)) + + if (0 == uChunkIndex) + { + // + // In a real FEN string this chunk is always the halfmove + // (fifty-move-rule) clock -- unlike every later chunk, 0 is + // a legitimate value here (a fresh game, or right after a + // capture/pawn move), not "absent". The generic i>0 check + // below would skip a genuine 0 and fall through to the + // *next* chunk (the fullmove number) instead, silently + // assigning that to uFifty. Confirmed live: the engine's + // own hardcoded starting-position FEN ("... - 0 1") was + // parsed as uFifty=1 (the fullmove number) rather than 0. + // + i = atoi(q); + if ((i >= 0) && (i < 100)) + { + p->uFifty = (ULONG)i; + } + } + else if (1 == uChunkIndex) { - p->uFifty = (ULONG)i; + // + // The second chunk is the fullmove number, not used by + // this engine -- just consumed here so the generic + // EPD-opcode scan below doesn't misread it as a repeated + // (and wrong) fifty-move value. + // } else { - if (!STRCMPI(q, "bm")) + i = atoi(q); + if ((i > 0) && (i < 100)) { - op = FindChunk(szFifty, u); - u++; - if (NULL == op) break; + p->uFifty = (ULONG)i; + } + else + { + if (!STRCMPI(q, "bm")) + { + op = FindChunk(szFifty, u); + u++; + if (NULL == op) break; + } + } - } + uChunkIndex++; q = FindChunk(szFifty, u); u++; } @@ -60,7 +60,7 @@ Return value: *p++ = RANK(mv.cFrom) + '0'; *p++ = FILE(mv.cTo) + 'a'; *p++ = RANK(mv.cTo) + '0'; - + if (mv.pPromoted) { if (IS_QUEEN(mv.pPromoted)) @@ -83,7 +83,9 @@ Return value: (g_uHashTableSizeEntries * sizeof(HASH_ENTRY)) / MB, PAWN_HASH_TABLE_SIZE * sizeof(PAWN_HASH_ENTRY) / MB); Trace(" QCheckPlies: %u\n", QPLIES_OF_NON_CAPTURE_CHECKS); - Trace(" FutilityBase: %u\n", FUTILITY_BASE_MARGIN); + Trace(" FutilityBase: full=%u lazy=%u superlazy=%u\n", + FUTILITY_BASE_MARGIN_FULL, FUTILITY_BASE_MARGIN_LAZY, + FUTILITY_BASE_MARGIN_SUPERLAZY); p = ExportEvalDNA(); Trace(" Logging Eval DNA.\n"); Log(p); @@ -475,6 +477,7 @@ Return value: InitializeDynamicMoveOrdering(); InitLMRTable(); InitializeHashSystem(); + InitEval(); #ifdef MP InitializeParallelSearch(); #endif @@ -586,6 +589,13 @@ Return value: } #endif + // TODO(temporary): TestRecogn hoisted to the front of the self-test + // sequence while iterating on recogn.c so a failure doesn't require + // waiting through the slow move-gen speed benchmarks first. Move + // back down next to TestMakeUnmakeMove (its natural home) once + // recogn.c work settles. + TestRecogn(); + TestDraw(); #ifdef EVAL_DUMP TestEval(); diff --git a/src/recogn.c b/src/recogn.c index 0ab9514..32a618d 100644 --- a/src/recogn.c +++ b/src/recogn.c @@ -12,7 +12,7 @@ Abstract: Interior-Node Recognition" * ICCA Journal Volume 21, No. 3, pp 156-167 (also "Scalable Search in Computer Chess" pp 65-81). This code also borrows ideas from Thorsten Greiner's AMY chess program. - + Author: Scott Gasch ([email protected]) 16 Oct 2005 @@ -24,10 +24,10 @@ Revision History: #include "chess.h" extern ULONG g_uIterateDepth; -static COOR QUEENING_SQUARE_BY_COLOR_FILE[2][8] = -{ +static COOR QUEENING_SQUARE_BY_COLOR_FILE[2][8] = +{ { A1, B1, C1, D1, E1, F1, G1, H1 }, - { A8, B8, C8, D8, E8, F8, G8, H8 } + { A8, B8, C8, D8, E8, F8, G8, H8 } }; #define RECOGN_INDEX(w, b) \ @@ -38,12 +38,12 @@ typedef ULONG RECOGNIZER(SEARCHER_THREAD_CONTEXT *ctx, SCORE *piScore); static RECOGNIZER *g_pRecognizers[64]; static BITV g_bvRecognizerAvailable[32]; -static ULONG -_MakeMaterialSig(IN FLAG fPawn, +static ULONG +_MakeMaterialSig(IN FLAG fPawn, IN FLAG fKnight, IN FLAG fBishop, - IN FLAG fRook, - IN FLAG fQueen) + IN FLAG fRook, + IN FLAG fQueen) /** Routine description: @@ -66,7 +66,7 @@ Return value: **/ { ULONG x; - + ASSERT(IS_VALID_FLAG(fPawn)); ASSERT(IS_VALID_FLAG(fKnight)); ASSERT(IS_VALID_FLAG(fBishop)); @@ -74,7 +74,7 @@ Return value: ASSERT(IS_VALID_FLAG(fQueen)); x = fPawn | (fKnight << 1) | (fBishop << 2) | (fRook << 3) | (fQueen << 4); - + ASSERT((0 <= x) && (x <= 31)); return(x); } @@ -84,29 +84,29 @@ Return value: static FLAG -_TablebasesSaySideWins(IN SEARCHER_THREAD_CONTEXT *ctx, +_TablebasesSaySideWins(IN SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uSide) { SCORE iScore; if (TRUE == ProbeEGTB(ctx, &iScore)) { - if (ctx->sPosition.uToMove == uSide) + if (ctx->sPosition.uToMove == uSide) { return iScore > 0; - } - else + } + else { return iScore < 0; } - } + } return TRUE; } static FLAG -_TablebasesSayDraw(IN SEARCHER_THREAD_CONTEXT *ctx) +_TablebasesSayDraw(IN SEARCHER_THREAD_CONTEXT *ctx) { SCORE iScore; - if (TRUE == ProbeEGTB(ctx, &iScore)) + if (TRUE == ProbeEGTB(ctx, &iScore)) { return iScore == 0; } @@ -114,13 +114,13 @@ _TablebasesSayDraw(IN SEARCHER_THREAD_CONTEXT *ctx) } static FLAG -_TablebasesSayDrawOrWin(IN SEARCHER_THREAD_CONTEXT *ctx, - IN ULONG uSide) +_TablebasesSayDrawOrWin(IN SEARCHER_THREAD_CONTEXT *ctx, + IN ULONG uSide) { SCORE iScore; - if (TRUE == ProbeEGTB(ctx, &iScore)) + if (TRUE == ProbeEGTB(ctx, &iScore)) { - return ((iScore == 0) || + return ((iScore == 0) || ((iScore > 0) && (ctx->sPosition.uToMove == uSide)) || ((iScore < 0) && (ctx->sPosition.uToMove != uSide))); } @@ -128,9 +128,9 @@ _TablebasesSayDrawOrWin(IN SEARCHER_THREAD_CONTEXT *ctx, } -static FLAG -_SanityCheckRecognizers(IN SEARCHER_THREAD_CONTEXT *ctx, - IN SCORE iScore, +static FLAG +_SanityCheckRecognizers(IN SEARCHER_THREAD_CONTEXT *ctx, + IN SCORE iScore, IN ULONG uVal) { ULONG uToMove = ctx->sPosition.uToMove; switch(uVal) { @@ -146,22 +146,34 @@ _SanityCheckRecognizers(IN SEARCHER_THREAD_CONTEXT *ctx, return _TablebasesSaySideWins(ctx, !uToMove); } case RECOGN_LOWER: + // + // iScore is only a LOWER bound: the true score is >= + // iScore, so we can only make a directional claim when the + // bound itself pins one down. iScore > 0 forces a genuine + // win for uToMove; iScore == 0 forces at least a draw. A + // negative lower bound ("at least this bad, could be + // better or worse") licenses no claim about who's actually + // winning, so don't assert one. if (iScore == 0) { return _TablebasesSayDrawOrWin(ctx, uToMove); } else if (iScore > 0) { return _TablebasesSaySideWins(ctx, uToMove); } else { - ASSERT(iScore < 0); - return _TablebasesSaySideWins(ctx, !uToMove); + return TRUE; } case RECOGN_UPPER: + // + // Symmetric reasoning: iScore is only an UPPER bound (true + // score <= iScore). iScore < 0 forces a genuine win for the + // opponent; iScore == 0 forces at least a draw for the + // opponent. A positive upper bound doesn't preclude uToMove + // still winning by less than iScore, so no claim there. if (iScore == 0) { return _TablebasesSayDrawOrWin(ctx, !uToMove); - } else if (iScore > 0) { - return _TablebasesSayDrawOrWin(ctx, !uToMove); - } else { - ASSERT(iScore < 0); + } else if (iScore < 0) { return _TablebasesSaySideWins(ctx, !uToMove); + } else { + return TRUE; } default: ASSERT(FALSE); @@ -169,9 +181,9 @@ _SanityCheckRecognizers(IN SEARCHER_THREAD_CONTEXT *ctx, } } -static FLAG -_NothingBut(IN POSITION *pos, - IN PIECE p, +static FLAG +_NothingBut(IN POSITION *pos, + IN PIECE p, IN ULONG uColor) /** @@ -196,7 +208,7 @@ static FLAG { static PIECE q[] = { KNIGHT, BISHOP, ROOK, QUEEN }; ULONG u; - + if (!(p & PAWN)) { if (pos->uPawnCount[uColor] > 0) return(FALSE); @@ -213,8 +225,8 @@ static FLAG } #endif -static ULONG -_RecognizeKK(IN SEARCHER_THREAD_CONTEXT *ctx, +static ULONG +_RecognizeKK(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piScore) /** @@ -237,8 +249,8 @@ Return value: return(RECOGN_EXACT); } -static ULONG -_RecognizeKBK(IN SEARCHER_THREAD_CONTEXT *ctx, +static ULONG +_RecognizeKBK(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piScore) /** @@ -263,7 +275,7 @@ Return value: ULONG u; ULONG uAdjacent; POSITION *pos = &ctx->sPosition; - + ASSERT((pos->uNonPawnCount[WHITE][0] <= 3) && (pos->uNonPawnCount[BLACK][0] <= 3)); ASSERT(_NothingBut(pos, BISHOP, WHITE)); @@ -272,7 +284,7 @@ Return value: // // Recognize KBKB as a draw unless there's a cornered king (in // which case it may be a mate-in-1) - // + // if ((pos->uNonPawnCount[WHITE][0] == 2) && (pos->uNonPawnCount[BLACK][0] == 2)) { @@ -283,20 +295,20 @@ Return value: return(RECOGN_EXACT); } } - + // // Otherwise we want to deal with KB+ vs lone K. KBKBB etc are // too hard to recognize. - // + // if ((pos->uNonPawnCount[WHITE][0] != 1) && (pos->uNonPawnCount[BLACK][0] != 1)) { return(UNRECOGNIZED); } - + // // If we get here then one side has no pieces (except the king). - // + // uStrong = BLACK; if (pos->uNonPawnCount[WHITE][0] > 1) { @@ -308,7 +320,7 @@ Return value: // // KB vs K is a draw, KB+ vs K is still a draw if all bishops are the // same color. - // + // uBishops = pos->uNonPawnCount[uStrong][BISHOP]; if ((uBishops == 1) || (pos->uWhiteSqBishopCount[uStrong] == 0) || @@ -317,18 +329,18 @@ Return value: *piScore = 0; return(RECOGN_EXACT); } - + // // If we get here the strong side has more than one bishop and has // at least one bishop on each color. - // + // // // If the weak king is next to a strong side piece, fail to // recognize since the weak king may take the bishop with the // move. Note: we allow the weak king to be adjacent to up to one // enemy bishop as long as it's the strong side's turn to move. - // + // cWeakKing = pos->cNonPawns[FLIP(uStrong)][0]; ASSERT(DISTANCE(cWeakKing, pos->cNonPawns[uStrong][0]) > 1); uAdjacent = 0; @@ -366,8 +378,8 @@ Return value: // This is a recognized win for the strong side. Compute a score // that encourages cornering the weak king and making progress // towards a checkmate. - // - *piScore = (pos->iMaterialBalance[uStrong] + VALUE_QUEEN - + // + *piScore = (pos->iMaterialBalance[uStrong] + VALUE_QUEEN - (u * 16) - (CORNER_DISTANCE(cWeakKing) * 32)); ASSERT(IS_VALID_SCORE(*piScore)); if (pos->uToMove != uStrong) @@ -378,8 +390,8 @@ Return value: return(RECOGN_LOWER); } -static ULONG -_RecognizeKNK(IN SEARCHER_THREAD_CONTEXT *ctx, +static ULONG +_RecognizeKNK(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piScore) /** @@ -405,11 +417,11 @@ Return value: (pos->uNonPawnCount[BLACK][0] <= 3)); ASSERT(_NothingBut(pos, KNIGHT, WHITE)); ASSERT(_NothingBut(pos, KNIGHT, BLACK)); - + // // KNKN is a draw unless someone has a K in the corner (in which case, // with the friend knight in the way, there's a possible mate) - // + // if ((pos->uNonPawnCount[WHITE][0] == 2) && (pos->uNonPawnCount[BLACK][0] == 2)) { @@ -421,20 +433,20 @@ Return value: } return(UNRECOGNIZED); } - + // // KNNKN etc... unrecognized. Heinz says "exceptional wins possible for // any side by mates in seven or less moves." TODO: add this knowledge. - // + // if ((pos->uNonPawnCount[WHITE][0] != 1) || (pos->uNonPawnCount[BLACK][0] != 1)) { return(UNRECOGNIZED); } - + // // If we get here somebody has no pieces (except a lone king). - // + // uStrong = WHITE; if (pos->uNonPawnCount[BLACK][0] > 1) { @@ -449,7 +461,7 @@ Return value: // Everything else in here is a draw. // ASSERT(pos->uNonPawnCount[uStrong][0] < 4); - if (ON_EDGE(pos->cNonPawns[FLIP(uStrong)][0])) + if (ON_EDGE(pos->cNonPawns[FLIP(uStrong)][0])) { return(UNRECOGNIZED); } @@ -458,8 +470,18 @@ Return value: } -static ULONG -_RecognizeKBNK(IN SEARCHER_THREAD_CONTEXT *ctx, +// +// DISABLED -- not currently registered in InitializeInteriorNodeRecognizers +// (see the comment there). testrecogn.c's EGTB cross-check found a +// counterexample in the bare-lone-king mating branch below (1k6/8/8/8/ +// 1bn5/8/1K6/8 w): the classic KBN-vs-K "wrong corner" subtlety -- +// mate is only forceable in the corner matching the bishop's square +// color, and this function's bail-out conditions don't fully capture +// that. Left in place as a starting point; validate any future fix +// against testrecogn.c's KNBK case before re-registering. +// +static ULONG __attribute__((unused)) +_RecognizeKBNK(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piScore) /** @@ -488,22 +510,22 @@ Return value: (pos->uNonPawnCount[BLACK][0] <= 3)); ASSERT(_NothingBut(pos, BISHOP | KNIGHT, WHITE)); ASSERT(_NothingBut(pos, BISHOP | KNIGHT, BLACK)); - + if ((pos->uNonPawnCount[WHITE][0] > 1) && (pos->uNonPawnCount[BLACK][0] > 1)) { // // Do not recognize stuff like KNNKB or KNKBB etc... - // + // if (pos->uNonPawnCount[WHITE][0] + pos->uNonPawnCount[BLACK][0] > 4) { return(UNRECOGNIZED); } - + // // This is KNKB; unless someone's king is on the edge, // recognize a draw. - // + // ASSERT((pos->uNonPawnCount[WHITE][0] == 2) && (pos->uNonPawnCount[BLACK][0] == 2)); if (ON_EDGE(pos->cNonPawns[WHITE][0]) || @@ -517,7 +539,7 @@ Return value: // // If we get here we are in a KBNK endgame. - // + // uStrong = WHITE; if (pos->uNonPawnCount[BLACK][0] > 1) { @@ -540,11 +562,11 @@ Return value: { return(UNRECOGNIZED); } - + // // Don't recognize anything if the weak king is next to a strong side's // piece. - // + // uAdjacent = 0; for (u = 1; u < pos->uNonPawnCount[uStrong][0]; u++) { @@ -559,7 +581,7 @@ Return value: // Don't recognize if the two kings are close enough to each other // that there might be a stalemate if the weak side is on move and // on the edge. - // + // ASSERT(IS_ON_BOARD(pos->cNonPawns[uStrong][0])); ASSERT(IS_KING(pos->rgSquare[pos->cNonPawns[uStrong][0]].pPiece)); u = DISTANCE(cWeakKing, pos->cNonPawns[uStrong][0]); @@ -577,7 +599,7 @@ Return value: // Calculate a score that grabs the search's attention and makes // progress towards driving the weak king to the correct corner to // mate him. - // + // if (pos->uWhiteSqBishopCount[uStrong] > 0) { uDist = WHITE_CORNER_DISTANCE(cWeakKing); @@ -587,7 +609,7 @@ Return value: uDist = BLACK_CORNER_DISTANCE(cWeakKing); } ASSERT((0 <= uDist) && (uDist <= 7)); - + *piScore = (pos->iMaterialBalance[uStrong] + (7 * VALUE_PAWN) - (uDist * 32) - (u * 16)); ASSERT(IS_VALID_SCORE(*piScore)); @@ -600,14 +622,35 @@ Return value: } -static ULONG -_RecognizeKNKP(IN SEARCHER_THREAD_CONTEXT *ctx, +// +// RE-ENABLED for the exact single-knight/single-pawn case only, backed +// by exhaustive (not sampled) proof: testrecogn.c's +// TestRecognExhaustiveKNKP enumerates every legal KNKP position with +// exactly one knight and one pawn -- 10.2M raw square placements, 5.2M +// of them actually checked against real Syzygy EGTB data (the rest +// UNRECOGNIZED or outside coverage) -- and found zero disagreements. +// The claim this function makes ("at best a draw for the pawn side") +// genuinely is a two-knights-can't-force-mate fact in that exact +// sub-case. +// +// It is FALSE once a second knight or a second pawn enters the +// picture, though -- testrecogn.c's random sampling found two live +// counterexamples in those cases (a real forced loss for the pawn +// side despite its king being right next to its own pawn) before this +// function was tightened to exclude them via the count==1 checks +// below. Don't loosen those checks back to "<=2 knights" / "any pawn +// count" without first extending the exhaustive verifier to cover +// whatever case is being added and confirming zero disagreements the +// same way. +// +ULONG +_RecognizeKNKP(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piScore) /** Routine description: - Recognize KN+KP+ positions. + Recognize KNKP positions: exactly one knight vs exactly one pawn. Parameters: @@ -622,14 +665,14 @@ Return value: { ULONG uStrong; POSITION *pos = &ctx->sPosition; - + ASSERT((pos->uNonPawnCount[WHITE][0] <= 3) && (pos->uNonPawnCount[BLACK][0] <= 3)); ASSERT(_NothingBut(pos, PAWN | KNIGHT, WHITE)); ASSERT(_NothingBut(pos, PAWN | KNIGHT, BLACK)); // - // Call the side with knight(s) "strong" + // Call the side with the knight "strong" // uStrong = WHITE; if (pos->uNonPawnCount[BLACK][0] > 1) @@ -640,18 +683,21 @@ Return value: ASSERT(pos->uNonPawnCount[FLIP(uStrong)][0] == 1); // - // Don't recognize KNNKP or KNKP with K on edge - // - if ((pos->uNonPawnCount[uStrong][KNIGHT] > 2) || + // Exhaustively proven correct only for exactly one knight and + // exactly one pawn (see the comment above) -- also still exclude + // K on the edge, per the original ON_EDGE reasoning. + // + if ((pos->uNonPawnCount[uStrong][KNIGHT] != 1) || + (pos->uPawnCount[FLIP(uStrong)] != 1) || (ON_EDGE(pos->cNonPawns[FLIP(uStrong)][0]))) { return(UNRECOGNIZED); } // - // This is at least a draw for the side with the pawn(s) and at - // best a draw for the side with the knight(s) - // + // This is at least a draw for the side with the pawn and at best a + // draw for the side with the knight + // *piScore = 0; if (pos->uToMove == uStrong) { @@ -662,7 +708,7 @@ Return value: static ULONG -_RecognizeKBKP(IN SEARCHER_THREAD_CONTEXT *ctx, +_RecognizeKBKP(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piScore) /** @@ -711,7 +757,7 @@ Return value: // // Construct a strong side bitboard of pawn locations - // + // bb = 0ULL; for (u = 0; u < pos->uPawnCount[uStrong]; u++) { @@ -719,25 +765,25 @@ Return value: ASSERT(IS_ON_BOARD(c)); bb |= COOR_TO_BB(c); } - + if ((pos->uNonPawnCount[BLACK][0] + pos->uPawnCount[BLACK] > 1) && (pos->uNonPawnCount[WHITE][0] + pos->uPawnCount[WHITE] > 1)) { // // Neither side has a lone king. This is either KBKP+ or // KBP+KP+. - // + // if (pos->uPawnCount[uStrong] > 0) { // // Strong side can maybe take an adjacent pawn and survive the // bad bishop. - // + // if (uStrong == pos->uToMove) { return(UNRECOGNIZED); } - + // // Make sure the strong side has the right color bishop // for his pawns. @@ -750,9 +796,9 @@ Return value: { goto at_best_draw_for_strong; } - + if (!(bb & ~BBFILE[H]) && - (pos->uWhiteSqBishopCount[WHITE] == + (pos->uWhiteSqBishopCount[WHITE] == pos->uNonPawnCount[WHITE][BISHOP]) && (DISTANCE(cWeakKing, H8) <= 1)) { @@ -762,13 +808,13 @@ Return value: else { if (!(bb & ~BBFILE[A]) && - (pos->uWhiteSqBishopCount[BLACK] == + (pos->uWhiteSqBishopCount[BLACK] == pos->uNonPawnCount[BLACK][BISHOP]) && (DISTANCE(cWeakKing, A1) <= 1)) { goto at_best_draw_for_strong; } - + if (!(bb & ~BBFILE[H]) && (pos->uWhiteSqBishopCount[BLACK] == 0) && (DISTANCE(cWeakKing, H1) <= 1)) @@ -791,13 +837,13 @@ Return value: } goto at_best_draw_for_strong; } - } - else + } + else { // // KBPK: make sure the bishop is the right color. This time // there is no need to check for on-move. - // + // ASSERT(pos->uNonPawnCount[FLIP(uStrong)][0] == 1); ASSERT(pos->uNonPawnCount[uStrong][0] > 1); @@ -816,11 +862,11 @@ Return value: { goto draw; } - } - else + } + else { if (!(bb & ~BBFILE[A]) && - (pos->uWhiteSqBishopCount[BLACK] == + (pos->uWhiteSqBishopCount[BLACK] == pos->uNonPawnCount[BLACK][BISHOP]) && (DISTANCE(cWeakKing, A1) <= 1)) { @@ -836,7 +882,7 @@ Return value: return(UNRECOGNIZED); } #ifdef DEBUG - UtilPanic(SHOULD_NOT_GET_HERE, + UtilPanic(SHOULD_NOT_GET_HERE, NULL, NULL, NULL, NULL, __FILE__, __LINE__); #endif @@ -856,7 +902,7 @@ Return value: static void _GetPassersCriticalSquares(IN ULONG uColor, - IN COOR cPawn, + IN COOR cPawn, IN OUT COOR *cSquare) /** @@ -884,9 +930,9 @@ Return value: **/ { - static COOR cCriticalSquare[2][128] = + static COOR cCriticalSquare[2][128] = { - { + { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0,0,0,0,0,0,0,0, 0x61, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x66, 0,0,0,0,0,0,0,0, 0x61, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x66, 0,0,0,0,0,0,0,0, @@ -896,7 +942,7 @@ Return value: 0x61, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x66, 0,0,0,0,0,0,0,0, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0,0,0,0,0,0,0,0, }, - { + { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0,0,0,0,0,0,0,0, 0x11, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x16, 0,0,0,0,0,0,0,0, 0x11, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x16, 0,0,0,0,0,0,0,0, @@ -905,14 +951,14 @@ Return value: 0x11, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x16, 0,0,0,0,0,0,0,0, 0x11, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x16, 0,0,0,0,0,0,0,0, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0,0,0,0,0,0,0,0, - } + } }; ULONG uFile = FILE(cPawn); ASSERT(IS_VALID_COLOR(uColor)); ASSERT(IS_ON_BOARD(cPawn)); ASSERT((RANK(cPawn) != 1) && (RANK(cPawn) != 8)); - + if ((uFile == A) || (uFile == H)) { cSquare[0] = cCriticalSquare[uColor][cPawn]; @@ -923,19 +969,22 @@ Return value: cSquare[1] = cCriticalSquare[uColor][cPawn]; cSquare[0] = cSquare[1] - 1; cSquare[2] = cSquare[1] + 1; - + end: - ASSERT(cSquare[0] != 0); - ASSERT(cSquare[1] != 0); - ASSERT(cSquare[2] != 0); + // + // Note: don't assert cSquare[n] != 0 here -- COOR value 0 is A8, a + // perfectly legal critical square (e.g. a black pawn's B-file + // critical square at rank 7 has its adjacent/rook-file neighbor at + // A8), not a sentinel for "uninitialized". IS_ON_BOARD is the + // correct validity check. ASSERT(IS_ON_BOARD(cSquare[0])); ASSERT(IS_ON_BOARD(cSquare[1])); ASSERT(IS_ON_BOARD(cSquare[2])); } -static ULONG -_RecognizeKPK(IN SEARCHER_THREAD_CONTEXT *ctx, +static ULONG +_RecognizeKPK(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piScore) /** @@ -976,7 +1025,7 @@ Return value: if (pHash->u64Key == pos->u64PawnSig) { pos->iScore[BLACK] = pos->iScore[WHITE] = 0; - if (TRUE == EvalPasserRaces(pos, pHash)) + if (TRUE == _EvalPasserRacesAgainstLoneKings(pos, pHash)) { // // Someone wins. @@ -1034,7 +1083,7 @@ Return value: ASSERT(pos->uPawnCount[uStrong] > 0); uWeak = FLIP(uStrong); ASSERT(pos->uPawnCount[uWeak] == 0); - + if (pos->uPawnCount[uStrong] > 1) { *piScore = 0; @@ -1049,12 +1098,12 @@ Return value: // The side with pawns has only one pawn, do some more // sophisticated analysis here to spot winning KPK // configurations earlier by using "critical squares" - // + // ASSERT(pos->uPawnCount[uStrong] == 1); cPawn = pos->cPawns[uStrong][0]; ASSERT(IS_ON_BOARD(cPawn)); ASSERT(IS_PAWN(pos->rgSquare[cPawn].pPiece)); - + // // Step 1: the strong king must be closer to the pawn than // the weak king. @@ -1072,12 +1121,12 @@ Return value: _GetPassersCriticalSquares(uStrong, cPawn, cCritical); for (u = 0; u < 3; u++) { - uDist[uStrong] = DISTANCE(pos->cNonPawns[uStrong][0], + uDist[uStrong] = DISTANCE(pos->cNonPawns[uStrong][0], cCritical[u]); ASSERT((0 <= uDist[uStrong]) && (uDist[uStrong] <= 7)); - uDist[uWeak] = DISTANCE(pos->cNonPawns[uWeak][0], + uDist[uWeak] = DISTANCE(pos->cNonPawns[uWeak][0], cCritical[u]); - + // // Assume if the weak side is on move he will move // towards the critical square. Also assume that @@ -1094,7 +1143,7 @@ Return value: ASSERT((0 <= uDist[uWeak]) && (uDist[uWeak] <= 7)); if (uDist[uStrong] < uDist[uWeak]) { - cQueen = + cQueen = QUEENING_SQUARE_BY_COLOR_FILE[uStrong][FILE(cPawn)]; *piScore = (pos->iMaterialBalance[uStrong] + VALUE_QUEEN + (2 * VALUE_PAWN) - @@ -1127,9 +1176,9 @@ Return value: } -static void -_NewRecognizer(IN RECOGNIZER *pFunct, - IN ULONG uWhiteSig, +static void +_NewRecognizer(IN RECOGNIZER *pFunct, + IN ULONG uWhiteSig, IN ULONG uBlackSig) /** @@ -1154,7 +1203,7 @@ Return value: g_pRecognizers[RECOGN_INDEX(uWhiteSig, uBlackSig)] = pFunct; } -void +void InitializeInteriorNodeRecognizers(void) /** @@ -1180,7 +1229,7 @@ Return value: _MakeMaterialSig(0, 0, 0, 0, 0), _MakeMaterialSig(0, 0, 0, 0, 0)); - // KB+K P N B R Q + // KB+K P N B R Q _NewRecognizer(_RecognizeKBK, _MakeMaterialSig(0, 0, 1, 0, 0), _MakeMaterialSig(0, 0, 0, 0, 0)); @@ -1189,61 +1238,67 @@ Return value: _NewRecognizer(_RecognizeKBK, _MakeMaterialSig(0, 0, 1, 0, 0), _MakeMaterialSig(0, 0, 1, 0, 0)); - - // KN+K P N B R Q + + // KN+K P N B R Q _NewRecognizer(_RecognizeKNK, - _MakeMaterialSig(0, 1, 0, 0, 0), + _MakeMaterialSig(0, 1, 0, 0, 0), _MakeMaterialSig(0, 0, 0, 0, 0)); - // KN+KN+ P N B R Q - _NewRecognizer(_RecognizeKNK, - _MakeMaterialSig(0, 1, 0, 0, 0), + // KN+KN+ P N B R Q + _NewRecognizer(_RecognizeKNK, + _MakeMaterialSig(0, 1, 0, 0, 0), _MakeMaterialSig(0, 1, 0, 0, 0)); - // KN+KB+ P N B R Q - _NewRecognizer(_RecognizeKBNK, - _MakeMaterialSig(0, 1, 0, 0, 0), - _MakeMaterialSig(0, 0, 1, 0, 0)); - - // KN+B+K P N B R Q - _NewRecognizer(_RecognizeKBNK, - _MakeMaterialSig(0, 1, 1, 0, 0), - _MakeMaterialSig(0, 0, 0, 0, 0)); + // KN+KB+ and KN+B+K disabled: _RecognizeKBNK's lone-king mating + // branch was found wrong by testrecogn.c's EGTB cross-check (the + // KBN-vs-K "wrong corner" subtlety -- see the comment on + // _RecognizeKBNK above). Not registered until that's fixed. + // + // _NewRecognizer(_RecognizeKBNK, + // _MakeMaterialSig(0, 1, 0, 0, 0), + // _MakeMaterialSig(0, 0, 1, 0, 0)); + // + // _NewRecognizer(_RecognizeKBNK, + // _MakeMaterialSig(0, 1, 1, 0, 0), + // _MakeMaterialSig(0, 0, 0, 0, 0)); - // KN+KP+ P N B R Q - _NewRecognizer(_RecognizeKNKP, - _MakeMaterialSig(1, 0, 0, 0, 0), + // KN+KP+ -- re-enabled for exactly one knight vs exactly one pawn + // only (the function itself bails to UNRECOGNIZED for anything + // else); see the comment on _RecognizeKNKP for the exhaustive + // proof backing this. P N B R Q + _NewRecognizer(_RecognizeKNKP, + _MakeMaterialSig(1, 0, 0, 0, 0), _MakeMaterialSig(0, 1, 0, 0, 0)); - // KB+KP+ P N B R Q - _NewRecognizer(_RecognizeKBKP, - _MakeMaterialSig(1, 0, 0, 0, 0), + // KB+KP+ P N B R Q + _NewRecognizer(_RecognizeKBKP, + _MakeMaterialSig(1, 0, 0, 0, 0), _MakeMaterialSig(0, 0, 1, 0, 0)); - - // KP+B+KP+ P N B R Q - _NewRecognizer(_RecognizeKBKP, - _MakeMaterialSig(1, 0, 1, 0, 0), + + // KP+B+KP+ P N B R Q + _NewRecognizer(_RecognizeKBKP, + _MakeMaterialSig(1, 0, 1, 0, 0), _MakeMaterialSig(1, 0, 0, 0, 0)); - + // KP+B+K P N B R Q - _NewRecognizer(_RecognizeKBKP, - _MakeMaterialSig(1, 0, 1, 0, 0), + _NewRecognizer(_RecognizeKBKP, + _MakeMaterialSig(1, 0, 1, 0, 0), _MakeMaterialSig(0, 0, 0, 0, 0)); - // KP+K P N B R Q - _NewRecognizer(_RecognizeKPK, - _MakeMaterialSig(0, 0, 0, 0, 0), + // KP+K P N B R Q + _NewRecognizer(_RecognizeKPK, + _MakeMaterialSig(0, 0, 0, 0, 0), _MakeMaterialSig(1, 0, 0, 0, 0)); // KP+KP+ P N B R Q - _NewRecognizer(_RecognizeKPK, - _MakeMaterialSig(1, 0, 0, 0, 0), + _NewRecognizer(_RecognizeKPK, + _MakeMaterialSig(1, 0, 0, 0, 0), _MakeMaterialSig(1, 0, 0, 0, 0)); } -ULONG +ULONG RecognLookup(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piScore, IN FLAG fProbeEGTB) @@ -1277,7 +1332,7 @@ Return value: // // Try interior node recognizers - // + // if ((pos->uNonPawnCount[WHITE][0] <= 3) && (pos->uNonPawnCount[BLACK][0] <= 3)) { @@ -1309,11 +1364,16 @@ Return value: } // - // Try EGTB probe as long as some conditions are met - // - if ((FALSE != fProbeEGTB) && - ((pos->uNonPawnCount[WHITE][0] + pos->uNonPawnCount[BLACK][0] + - pos->uPawnCount[WHITE] + pos->uPawnCount[BLACK]) <= 5)) + // Try EGTB probe. No piece-count gate here: ProbeEGTB already + // checks the position's piece count against the dynamic + // TB_LARGEST (set from whatever tablebase files Fathom actually + // found at init), so hardcoding a ceiling here would only ever + // make this stricter than what's really installed, silently + // capping us below the on-disk tables (e.g. if 6-man WDL files are + // ever added alongside the 5-man set already present -- see + // CLAUDE.md). + // + if (FALSE != fProbeEGTB) { if (TRUE == ProbeEGTB(ctx, &iScore)) { @@ -549,15 +549,47 @@ Return value: Trace("First move beta cutoff rate was %5.3f percent.\n", ((n / d) * 100.0)); #ifdef LAZY_EVAL - d = (double)ctx->sCounters.tree.u64LazyEvals; - d += (double)ctx->sCounters.tree.u64FullEvals; - d += (double)ctx->sCounters.tree.u64EvalHashHits; - d += 1; - ASSERT(d); - Trace("Eval percentages: (%5.2f hash, %5.2f lazy, %5.2f full)\n", - ((double)ctx->sCounters.tree.u64EvalHashHits / d) * 100.0, - ((double)ctx->sCounters.tree.u64LazyEvals / d) * 100.0, - ((double)ctx->sCounters.tree.u64FullEvals / d) * 100.0); + { + // u64CyclesSuperLazyExit/u64CyclesLazyExit/u64CyclesFullEvalExit + // are each the *total call cost* (entry to exit) of Eval() + // calls that left via that specific path -- mutually exclusive + // and summing to u64CyclesInEval, so dividing each by its own + // matching call count gives a real per-path average, and + // u64CyclesInEval / dRealTotal gives a real overall average -- + // unlike averaging u64CyclesInEval (which used to only + // accumulate on the full-eval path) against the total call + // count across all three paths. + double dSuperLazy = (double)ctx->sCounters.tree.u64SuperLazyEvals; + double dLazy = (double)ctx->sCounters.tree.u64LazyEvals; + double dFull = (double)ctx->sCounters.tree.u64FullEvals; + double dRealTotal = dSuperLazy + dLazy + dFull; + d = dRealTotal + 1; + Trace("Eval exit breakdown: (%5.2f%% super lazy, %5.2f%% lazy, " + "%5.2f%% full)\n", + (dSuperLazy / d) * 100.0, + (dLazy / d) * 100.0, + (dFull / d) * 100.0); +#ifdef EVAL_TIME + { + UINT64 u64SLCyc = ctx->sCounters.tree.u64CyclesSuperLazyExit; + UINT64 u64LCyc = ctx->sCounters.tree.u64CyclesLazyExit; + UINT64 u64FCyc = ctx->sCounters.tree.u64CyclesFullEvalExit; + UINT64 u64AllCyc = ctx->sCounters.tree.u64CyclesInEval; + Trace("Avg. cpu cycles in eval, by exit path:\n" + " super lazy: %8.1f (%5.1f%% of total eval cycles)\n" + " lazy: %8.1f (%5.1f%% of total eval cycles)\n" + " full: %8.1f (%5.1f%% of total eval cycles)\n" + " overall: %8.1f\n", + (dSuperLazy ? (double)u64SLCyc / dSuperLazy : 0.0), + (u64AllCyc ? 100.0 * (double)u64SLCyc / (double)u64AllCyc : 0.0), + (dLazy ? (double)u64LCyc / dLazy : 0.0), + (u64AllCyc ? 100.0 * (double)u64LCyc / (double)u64AllCyc : 0.0), + (dFull ? (double)u64FCyc / dFull : 0.0), + (u64AllCyc ? 100.0 * (double)u64FCyc / (double)u64AllCyc : 0.0), + (dRealTotal ? (double)u64AllCyc / dRealTotal : 0.0)); + } +#endif + } #endif Trace("Extensions: (%u +, %u q+, %u 1mv, %u !kmvs, %u mult+, %u pawn\n" " %u threat, %u zug, %u sing, %u endg, %u bm, %u recap)\n", @@ -574,8 +606,6 @@ Return value: ctx->sCounters.extension.uBotvinnikMarkoff, ctx->sCounters.extension.uRecapture); #ifdef EVAL_TIME - n = (double)ctx->sCounters.tree.u64CyclesInEval; - Trace("Avg. cpu cycles in eval: %8.1f.\n", (n / d)); { // // Per-term breakdown of the average above -- board_ @@ -631,9 +661,14 @@ Return value: UINT64 u64LazyDecision = ctx->sCounters.tree.u64CyclesEvalLazyDecision; UINT64 u64CKSD = ctx->sCounters.tree.u64CyclesEvalCountKingSafetyDefects; UINT64 u64Storm = ctx->sCounters.tree.u64CyclesEvalFileStormDefects; + UINT64 u64SuperLazy = ctx->sCounters.tree.u64CyclesEvalSuperLazy; UINT64 u64PreLazyRest = (u64PreLazyOther >= u64LazyDecision) ? (u64PreLazyOther - u64LazyDecision) : 0; + u64PreLazyRest = (u64PreLazyRest >= u64SuperLazy) ? + (u64PreLazyRest - u64SuperLazy) : 0; Trace(" -- of which, pre-lazy breakdown --\n"); + Trace(" super lazy check: %5.1f%%\n", + (u64Total ? (100.0 * (double)u64SuperLazy / (double)u64Total) : 0.0)); Trace(" material/passers/badtrades/bishoppairs: %5.1f%%\n", (u64Total ? (100.0 * (double)u64PreLazyRest / (double)u64Total) : 0.0)); Trace(" lazy gate + EstimatePositionalScore: %5.1f%% " @@ -662,6 +697,15 @@ Return value: } #endif #endif + // Unconditional, build-flag-independent end-of-report marker. Every + // block above this point is gated behind some #ifdef (PERF_COUNTERS, + // LAZY_EVAL, EVAL_TIME, ...), so the exact shape/length of this + // report varies build to build -- a tool driving this engine over + // the xboard protocol (e.g. eval_tune/match_play.py) has no + // build-flag-independent way to know the report is fully drained + // before sending its next command otherwise. This line is always + // printed exactly once, always last, regardless of build profile. + Trace("ReportEnd\n"); } @@ -28,144 +28,17 @@ Revision History: pList->data[pList->uCount].uVal = (v); \ pList->uCount++; -void CDECL -SlowGetAttacks(IN OUT SEE_LIST *pList, - IN POSITION *pos, - IN COOR cSquare, - IN ULONG uSide) -/*++ - -Routine description: - - SlowGetAttacks is the C version of GetAttacks; it should be - identical to the GetAttacks code in x86.asm. The job of the - function is, given a position, square and side, to populate the - SEE_LIST with the locations and types of enemy pieces attacking - the square. - -Parameters: - - SEE_LIST *pList : list to populate - POSITION *pos : the board - COOR cSquare : square in question - ULONG uSide : side we are looking for attacks from - -Return value: - - void - ---*/ -{ - register ULONG x; - PIECE p; - COOR c; - int iIndex; - COOR cBlockIndex; - int iDelta; - static PIECE pPawn[2] = { BLACK_PAWN, WHITE_PAWN }; - static int iSeeDelta[2] = { -17, +15 }; - -#ifdef DEBUG - ASSERT(IS_ON_BOARD(cSquare)); - ASSERT(IS_VALID_COLOR(uSide)); - VerifyPositionConsistency(pos, FALSE); -#endif - pList->uCount = 0; - - // - // Check for pawns attacking cSquare - // - c = cSquare + (iSeeDelta[uSide]); - if (IS_ON_BOARD(c)) - { - p = pos->rgSquare[c].pPiece; - if (p == pPawn[uSide]) - { - // - // N.B. Don't use ADD_ATTACKER here because we know we're - // at element zero. - // - pList->data[0].pPiece = p; - pList->data[0].cLoc = c; - pList->data[0].uVal = VALUE_PAWN; - pList->uCount = 1; - } - } - - c += 2; - if (IS_ON_BOARD(c)) - { - p = pos->rgSquare[c].pPiece; - if (p == pPawn[uSide]) - { - ADD_ATTACKER(p, c, VALUE_PAWN); - } - } - - // - // Check for pieces attacking cSquare - // - for (x = pos->uNonPawnCount[uSide][0] - 1; - x != (ULONG)-1; - x--) - { - c = pos->cNonPawns[uSide][x]; - ASSERT(IS_ON_BOARD(c)); - - p = pos->rgSquare[c].pPiece; - ASSERT(p && !IS_PAWN(p)); - ASSERT(GET_COLOR(p) == uSide); - - iIndex = (int)c - (int)cSquare; - if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, GET_COLOR(p)) & - (1 << PIECE_TYPE(p)))) - { - continue; - } - - if (IS_KNIGHT_OR_KING(p)) - { - ASSERT(IS_KNIGHT(p) || IS_KING(p)); - ADD_ATTACKER(p, c, PIECE_VALUE(p)); - continue; - } - - // - // Check to see if there is a piece in the path from cSquare - // to c that blocks the attack. - // - iDelta = NEG_DELTA_WITH_INDEX(iIndex); - ASSERT(iDelta == -1 * CHECK_DELTA_WITH_INDEX(iIndex)); - ASSERT(iDelta != 0); - for (cBlockIndex = cSquare + iDelta; - cBlockIndex != c; - cBlockIndex += iDelta) - { - if (!IS_EMPTY(pos->rgSquare[cBlockIndex].pPiece)) - { - goto done; - } - } - - // - // Nothing in the way. - // - ADD_ATTACKER(p, c, PIECE_VALUE(p)); - - done: - ; - } -} - // -// board_representation/MIGRATION.md section 3: bbPieces-backed -// "who attacks square X" primitive, and a GetAttacks PoC built on -// it. Not wired into the GetAttacks macro yet -- see MIGRATION.md -// section 6 for the eventual toggle. Uses chess.h's FastFirstBit/ -// FastLastBit (static inline bsf/bsr wrappers) rather than the real -// out-of-line FirstBit/LastBit -- worth avoiding call overhead in a -// per-move-generated, per-node hot path like this one. +// board_representation/MIGRATION.md sections 3/6/7: bbPieces-backed +// "who attacks square X" primitive. _GetAttacksBB below is now the +// only GetAttacks implementation -- the old mailbox SlowGetAttacks/ +// asm GetAttacks were retired 2026-09-06 once all of section 7's +// retirement criteria (correctness sweep, isolated + whole-engine +// benchmarks, match_play.py gate) cleared. Uses chess.h's +// FastFirstBit/FastLastBit (static inline bsf/bsr wrappers) rather +// than the real out-of-line FirstBit/LastBit -- worth avoiding call +// overhead in a per-move-generated, per-node hot path like this one. // // (This file used to have its own static _BuildOccupiedBB here, // byte-for-byte identical to generate.c's _BuildFullOccupiedBB -- @@ -191,8 +64,8 @@ Routine description: Return a bitboard of every uSide knight/bishop/rook/queen/king that attacks cSquare in the current position, blockers included. - Pawns are deliberately excluded -- see GetAttacksBB, which handles - them the same 2-square-delta way SlowGetAttacks always has (already + Pawns are deliberately excluded -- see _GetAttacksBB, which handles + them the same 2-square-delta way the old mailbox code did (already O(1), nothing to improve). Knights and the king are pure O(1) table/delta lookups (no @@ -319,19 +192,21 @@ _GetAttacksBB(IN OUT SEE_LIST *pList, Routine description: - PROOF OF CONCEPT -- not called from anywhere yet, and not a - replacement for GetAttacks/SlowGetAttacks until section 4/5/6 of - board_representation/MIGRATION.md (correctness sweep, benchmark, - toggle) are done. Reproduces SlowGetAttacks's exact semantics - (same deliberately-approximate no-pin/no-en-passant contract) via - _WhoAttacksSquareBB instead of the O(non-pawn-piece-count) mailbox - walk -- pawns handled identically to SlowGetAttacks (2-square - delta, unchanged, already O(1)). + The only GetAttacks implementation as of 2026-09-06 -- the old + mailbox SlowGetAttacks/asm GetAttacks retired once all of + board_representation/MIGRATION.md section 7's retirement criteria + cleared (correctness sweep, isolated + whole-engine benchmarks, + match_play.py gate). Reproduces the old mailbox code's exact + semantics (same deliberately-approximate no-pin/no-en-passant + contract) via _WhoAttacksSquareBB instead of an + O(non-pawn-piece-count) mailbox walk -- pawns handled the same way + the mailbox version always did (2-square delta, unchanged, already + O(1)). - Attacker order is not guaranteed to match SlowGetAttacks -- see() - sorts/heaps the list immediately after GetAttacks returns, so only - the *set* of attackers needs to match, not the sequence - (board_representation/MIGRATION.md section 4). + Attacker order is not guaranteed to match the old mailbox + implementation -- see() sorts/heaps the list immediately after + GetAttacks returns, so only the *set* of attackers needs to match, + not the sequence (board_representation/MIGRATION.md section 4). Parameters: diff --git a/src/testsee.c b/src/testsee.c index d196d54..978e1c6 100644 --- a/src/testsee.c +++ b/src/testsee.c @@ -26,19 +26,6 @@ Revision History: #include "chess.h" -// This harness's job is to validate _GetAttacksBB against a fixed -// baseline (the real asm/CROUTINES implementation), not to compare -// the engine's own current GetAttacks macro target against itself -- -// but chess.h's GETATTACKS_BITBOARD toggle (board_representation/ -// MIGRATION.md section 6) can make that macro resolve to -// _GetAttacksBB. Undefine it here so every "GetAttacks(...)" call -// below always reaches the real asm/CROUTINES function (still -// declared under that name in chess.h, just no longer macro-routed), -// regardless of which implementation is live in production. -#ifdef GetAttacks -#undef GetAttacks -#endif - // Same reasoning, for IsAttacked/InCheck (board_representation/ // MOVEGEN_MIGRATION.md section 6b's ISATTACKED_BITBOARD toggle): // TestIsAttackedBB below must always be able to call the real mailbox @@ -197,21 +184,21 @@ SeeListsAreEqual(SEE_LIST *pA, SEE_LIST *pB) return TRUE; } +// This used to diff _GetAttacksBB against the mailbox asm GetAttacks and +// the C SlowGetAttacks reference implementation; both were retired +// 2026-09-06 (board_representation/MIGRATION.md section 7) once that +// three-way comparison had run clean for long enough. With no second +// implementation left to diff against, this is now just a crash/ +// no-degenerate-output smoke test plus a standalone cycles/call number. void -TestGetAttacks(void) +TestGetAttacks(void) { POSITION pos; ULONG u; COOR c; - SEE_LIST rgSlowList; - SEE_LIST rgAsmList; SEE_LIST rgBBList; ULONG color; -#if !defined(_X86_) && !defined(_X64_) - return; -#endif - Trace("Testing GetAttacks...\n"); for (u = 0; u < 20000; u++) { @@ -221,51 +208,17 @@ TestGetAttacks(void) if (!IS_ON_BOARD(c)) continue; for (color = BLACK; color <= WHITE; color++) { - SlowGetAttacks(&rgSlowList, - &pos, - c, - color); - GetAttacks(&rgAsmList, - &pos, - c, - color); - if (!SeeListsAreEqual(&rgSlowList, &rgAsmList)) - { - UtilPanic(TESTCASE_FAILURE, - &pos, - "SEE_LIST mismatch", &rgSlowList, &rgAsmList, - __FILE__, __LINE__); - } - - // board_representation/MIGRATION.md section 3/4: - // bbPieces-backed GetAttacks PoC, same correctness - // gate as the asm/C comparison above. - _GetAttacksBB(&rgBBList, - &pos, - c, - color); - if (!SeeListsAreEqual(&rgSlowList, &rgBBList)) - { - UtilPanic(TESTCASE_FAILURE, - &pos, - "SEE_LIST mismatch (_GetAttacksBB)", - &rgSlowList, &rgBBList, - __FILE__, __LINE__); - } + _GetAttacksBB(&rgBBList, &pos, c, color); + ASSERT(rgBBList.uCount <= ARRAY_LENGTH(rgBBList.data)); } } } // - // Speed: board_representation/MIGRATION.md section 5's isolated - // cycles/call microbenchmark, pulled forward here since it's cheap - // to add right alongside the correctness gate that just proved the - // two implementations equivalent. Three positions spanning piece - // density (opening/middlegame/endgame), SlowGetAttacks vs - // _GetAttacksBB interleaved call-by-call (not phase-by-phase) to - // cancel shared-box noise -- a red flag (flat or inverted result) - // here would mean stopping before wiring this in any further, same - // as the Eval occupancy-bitboard work that motivated this file. + // Speed: standalone cycles/call number, three positions spanning + // piece density (opening/middlegame/endgame). No longer a + // comparison (nothing left to compare against), just a number to + // watch for regressions over time. { static const char *rgszFen[3] = { @@ -279,26 +232,18 @@ TestGetAttacks(void) }; POSITION posBench; SEE_LIST rgList; - UINT64 u64SlowTotal, u64AsmTotal, u64BBTotal, u64Start; + UINT64 u64BBTotal, u64Start; ULONG uIter; ULONG uSq; COOR cBench; ULONG uSide; const ULONG uCallsPerPosition = 200000; - // GetAttacks (unqualified) is the real production entry point -- - // the hand-tuned x86/x64 asm routine, not SlowGetAttacks (the C - // reference used only for correctness comparison above). That's - // the actual competitor _GetAttacksBB has to beat; SlowGetAttacks - // is included only as a third data point, not the bar to clear. - Trace("Benchmarking GetAttacks: asm GetAttacks vs SlowGetAttacks " - "vs _GetAttacksBB (interleaved, %lu calls/position)...\n", + Trace("Benchmarking _GetAttacksBB (%lu calls/position)...\n", uCallsPerPosition); for (u = 0; u < 3; u++) { FenToPosition(&posBench, (char *)rgszFen[u]); - u64SlowTotal = 0; - u64AsmTotal = 0; u64BBTotal = 0; for (uIter = 0; uIter < uCallsPerPosition; uIter++) { @@ -308,28 +253,13 @@ TestGetAttacks(void) if (!IS_ON_BOARD(cBench)) continue; u64Start = SystemReadTimeStampCounter(); - GetAttacks(&rgList, &posBench, cBench, uSide); - u64AsmTotal += (SystemReadTimeStampCounter() - u64Start); - - u64Start = SystemReadTimeStampCounter(); - SlowGetAttacks(&rgList, &posBench, cBench, uSide); - u64SlowTotal += (SystemReadTimeStampCounter() - u64Start); - - u64Start = SystemReadTimeStampCounter(); _GetAttacksBB(&rgList, &posBench, cBench, uSide); u64BBTotal += (SystemReadTimeStampCounter() - u64Start); } - printf(" %s: asm GetAttacks %" COMPILER_LONGLONG_UNSIGNED_FORMAT - " cycles/call, SlowGetAttacks %" - COMPILER_LONGLONG_UNSIGNED_FORMAT - " cycles/call, _GetAttacksBB %" - COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call " - "(BB is %.2fx asm)\n", + printf(" %s: _GetAttacksBB %" COMPILER_LONGLONG_UNSIGNED_FORMAT + " cycles/call\n", rgszLabel[u], - u64AsmTotal / uCallsPerPosition, - u64SlowTotal / uCallsPerPosition, - u64BBTotal / uCallsPerPosition, - (double)u64BBTotal / (double)u64AsmTotal); + u64BBTotal / uCallsPerPosition); } } } diff --git a/src/x64.asm b/src/x64.asm index 89351f2..fb42ff3 100644 --- a/src/x64.asm +++ b/src/x64.asm @@ -83,6 +83,12 @@ _CountBits: .done: ret int 3 +%if 0 +;; Retired 2026-09-06 (board_representation/MIGRATION.md section 7): +;; _GetAttacksBB (see.c) is now the only GetAttacks implementation -- +;; all retirement criteria (correctness sweep, isolated + whole-engine +;; benchmarks, match_play.py gate) cleared. Left assembled-out rather +;; than deleted. [GLOBAL GetAttacks] [GLOBAL _GetAttacks] @@ -262,6 +268,7 @@ _GetAttacks: pop rbx pop rbp ret +%endif ; 0 (retired GetAttacks) %endif ; !CROUTINES [GLOBAL LockCompareExchange] |
