diff options
| -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] |
