/** Copyright (c) Scott Gasch Module Name: testsee.c Abstract: This code is meant to sanity check the SEE routine. It works by using the generator and MakeMove/UnmakeMove to play out a sequence of moves on a single square. It's somewhat useful code but right now I don't really use it because it is too good: it detects things like pieces that are pinned against the king whereas the SEE itself does not. Still, I'd want to run it and verify the discrepencies if I was doing much work in the SEE code. Author: Scott Gasch (scott.gasch@gmail.com) 21 Oct 2004 Revision History: **/ #include "chess.h" // 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 // IsAttacked by name and compare it against IsAttackedBB explicitly, // regardless of which one chess.h's macro currently routes plain // "IsAttacked(...)" calls to elsewhere in the engine. #ifdef IsAttacked #undef IsAttacked #endif #ifdef InCheck #undef InCheck #endif #ifdef TEST_BROKEN ULONG g_uRootOnMove; SCORE DebugSEERecursive(SEARCHER_THREAD_CONTEXT *ctx, MOVE mv) { SCORE i, iMinMax; ULONG x; MOVE mvReply; ULONG uToMove = ctx->sPosition.uToMove; // // Make the move, this is not optional // ASSERT(GET_COLOR(mv.pMoved) == uToMove); if (FALSE == MakeMove(ctx, mv)) { return(INVALID_SCORE); } // // Initialize MinMax to the stand pat score here so that the reply // can be "something else". // iMinMax = ((ctx->sPosition.uPawnMaterial[g_uRootOnMove] + ctx->sPosition.uNonPawnMaterial[g_uRootOnMove]) - (ctx->sPosition.uPawnMaterial[FLIP(g_uRootOnMove)] + ctx->sPosition.uNonPawnMaterial[FLIP(g_uRootOnMove)])); // // Generate the reply moves. // GenerateMoves(ctx, (MOVE){0}, GENERATE_DONT_SCORE); for (x = ctx->sMoveStack.uBegin[ctx->uPly]; x < ctx->sMoveStack.uEnd[ctx->uPly]; x++) { // // Only consider replies that end up on the same sq as the // move. // mvReply = ctx->sMoveStack.mvf[x].mv; ASSERT(SanityCheckMove(&ctx->sPosition, mvReply)); if (mvReply.cTo != mv.cTo) { continue; } i = DebugSEERecursive(ctx, mvReply); if (INVALID_SCORE != i) { if ((ctx->uPly % 2) == 0) { if (i > iMinMax) iMinMax = i; } else { if (i < iMinMax) iMinMax = i; } } } // // Unmake the original move // UnmakeMove(ctx, mv); return(iMinMax); } SCORE DebugSEE(POSITION *pos, MOVE mv) { SCORE iRootBalance = ((pos->uPawnMaterial[pos->uToMove] + pos->uNonPawnMaterial[pos->uToMove]) - (pos->uPawnMaterial[FLIP(pos->uToMove)] + pos->uNonPawnMaterial[FLIP(pos->uToMove)])); SCORE iAfterExchange = iRootBalance; SEARCHER_THREAD_CONTEXT *ctx = malloc(sizeof(SEARCHER_THREAD_CONTEXT)); g_uRootOnMove = pos->uToMove; if (NULL != ctx) { pos->uDangerCount[BLACK] = pos->uDangerCount[WHITE] = 0; InitializeSearcherContext(pos, ctx); GenerateMoves(ctx, (MOVE){0}, GENERATE_DONT_SCORE); mv.bvFlags |= WouldGiveCheck(ctx, mv); if (InCheck(pos, pos->uToMove)) { mv.bvFlags |= MOVE_FLAG_ESCAPING_CHECK; } iAfterExchange = DebugSEERecursive(ctx, mv); if (iAfterExchange != INVALID_SCORE) { iAfterExchange -= iRootBalance; } free(ctx); } return(iAfterExchange); } #endif #ifdef TEST static int _SeeListEntryCompare(const void *pA, const void *pB) { const SEE_THREESOME *a = (const SEE_THREESOME *)pA; const SEE_THREESOME *b = (const SEE_THREESOME *)pB; if (a->cLoc != b->cLoc) return ((int)a->cLoc - (int)b->cLoc); return ((int)a->pPiece - (int)b->pPiece); } FLAG SeeListsAreEqual(SEE_LIST *pA, SEE_LIST *pB) { // Order-independent: GetAttacks's caller (SEE()) sorts/heaps the // list immediately after it's populated, so a bitboard-based // GetAttacks returning the same *set* of attackers in a different // order is a correct match, not a bug (board_representation/ // MIGRATION.md section 4). Sort a scratch copy of each by // (cLoc, pPiece) before comparing field-by-field. SEE_LIST sA = *pA; SEE_LIST sB = *pB; ULONG u; if (sA.uCount != sB.uCount) return FALSE; qsort(sA.data, sA.uCount, sizeof(sA.data[0]), _SeeListEntryCompare); qsort(sB.data, sB.uCount, sizeof(sB.data[0]), _SeeListEntryCompare); for (u = 0; u < sA.uCount; u++) { if ((sA.data[u].pPiece != sB.data[u].pPiece) || (sA.data[u].cLoc != sB.data[u].cLoc) || (sA.data[u].uVal != sB.data[u].uVal)) { return FALSE; } } 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) { POSITION pos; ULONG u; COOR c; SEE_LIST rgBBList; ULONG color; Trace("Testing GetAttacks...\n"); for (u = 0; u < 20000; u++) { GenerateRandomLegalPosition(&pos); FOREACH_SQUARE(c) { if (!IS_ON_BOARD(c)) continue; for (color = BLACK; color <= WHITE; color++) { _GetAttacksBB(&rgBBList, &pos, c, color); ASSERT(rgBBList.uCount <= ARRAY_LENGTH(rgBBList.data)); } } } // // 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] = { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1", "8/5k2/8/3K4/8/8/8/4R3 w - - 0 1", }; static const char *rgszLabel[3] = { "opening ", "middlegame", "endgame ", }; POSITION posBench; SEE_LIST rgList; UINT64 u64BBTotal, u64Start; ULONG uIter; ULONG uSq; COOR cBench; ULONG uSide; const ULONG uCallsPerPosition = 200000; Trace("Benchmarking _GetAttacksBB (%lu calls/position)...\n", uCallsPerPosition); for (u = 0; u < 3; u++) { FenToPosition(&posBench, (char *)rgszFen[u]); u64BBTotal = 0; for (uIter = 0; uIter < uCallsPerPosition; uIter++) { uSq = uIter % 64; cBench = BIT_NUMBER_TO_COOR(uSq); uSide = uIter & 1; if (!IS_ON_BOARD(cBench)) continue; u64Start = SystemReadTimeStampCounter(); _GetAttacksBB(&rgList, &posBench, cBench, uSide); u64BBTotal += (SystemReadTimeStampCounter() - u64Start); } printf(" %s: _GetAttacksBB %" COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call\n", rgszLabel[u], u64BBTotal / uCallsPerPosition); } } } // board_representation/MOVEGEN_MIGRATION.md section 6b: direct // comparison harness for IsAttacked/IsAttackedBB, same pattern as // TestGetAttacks above -- mailbox vs. bitboard, same inputs, must // match exactly. This is the piece ExposesCheckBB didn't get before // shipping and had to be debugged the hard way instead (two real bugs // found via TestSan/perft/DEBUG-assert failures rather than a direct // comparison); doing it properly here from the start. void TestIsAttackedBB(void) { POSITION pos; ULONG u; COOR c; ULONG uSide; FLAG fMailbox, fBB; Trace("Testing IsAttacked...\n"); for (u = 0; u < 20000; u++) { GenerateRandomLegalPosition(&pos); FOREACH_SQUARE(c) { if (!IS_ON_BOARD(c)) continue; for (uSide = BLACK; uSide <= WHITE; uSide++) { fMailbox = IsAttacked(c, &pos, uSide); fBB = IsAttackedBB(c, &pos, uSide); if (fMailbox != fBB) { UtilPanic(TESTCASE_FAILURE, &pos, "IsAttacked/IsAttackedBB mismatch", NULL, NULL, __FILE__, __LINE__); } } } for (uSide = BLACK; uSide <= WHITE; uSide++) { fMailbox = InCheck(&pos, uSide); fBB = InCheckBB(&pos, uSide); if (fMailbox != fBB) { UtilPanic(TESTCASE_FAILURE, &pos, "InCheck/InCheckBB mismatch", NULL, NULL, __FILE__, __LINE__); } } } // // Speed: same isolated cycles/call methodology as TestGetAttacks // above, interleaved call-by-call to cancel shared-box noise. // { static const char *rgszFen[3] = { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1", "8/5k2/8/3K4/8/8/8/4R3 w - - 0 1", }; static const char *rgszLabel[3] = { "opening ", "middlegame", "endgame ", }; POSITION posBench; UINT64 u64MailboxTotal, u64BBTotal, u64Start; ULONG uIter; ULONG uSq; COOR cBench; ULONG uSideBench; const ULONG uCallsPerPosition = 200000; FLAG fSink; Trace("Benchmarking IsAttacked: mailbox vs IsAttackedBB " "(interleaved, %lu calls/position)...\n", uCallsPerPosition); for (u = 0; u < 3; u++) { FenToPosition(&posBench, (char *)rgszFen[u]); u64MailboxTotal = 0; u64BBTotal = 0; for (uIter = 0; uIter < uCallsPerPosition; uIter++) { uSq = uIter % 64; cBench = BIT_NUMBER_TO_COOR(uSq); uSideBench = uIter & 1; if (!IS_ON_BOARD(cBench)) continue; u64Start = SystemReadTimeStampCounter(); fSink = IsAttacked(cBench, &posBench, uSideBench); u64MailboxTotal += (SystemReadTimeStampCounter() - u64Start); u64Start = SystemReadTimeStampCounter(); fSink = IsAttackedBB(cBench, &posBench, uSideBench); u64BBTotal += (SystemReadTimeStampCounter() - u64Start); (void)fSink; } printf(" %s: mailbox %" COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call, IsAttackedBB %" COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call " "(BB is %.2fx mailbox)\n", rgszLabel[u], u64MailboxTotal / uCallsPerPosition, u64BBTotal / uCallsPerPosition, (double)u64BBTotal / (double)u64MailboxTotal); } } } #endif // TEST