/** Copyright (c) Scott Gasch Module Name: probe.c Abstract: Routines for probing endgame tablebases. This uses Ronald de Man's Syzygy tablebase format via Jon Dart's "Fathom" probing library (see fathom/) instead of Eugene Nalimov's original tablebases. Syzygy tables store WDL (win/draw/loss) + DTZ (distance to zeroing move) rather than Nalimov's DTM (distance to mate); they are dramatically smaller on disk and the probing code is far simpler as a result. We lose literal mate-distance information from interior-node probes, so a probed win/loss is reported as a large-but-not-mate-range score rather than a precise mate score. Author: Scott Gasch (scott.gasch@gmail.com) 02 Jul 2004 Converted to Syzygy/Fathom 23 Aug 2026 Revision History: $Id: probe.c 355 2008-07-01 15:46:43Z scott $ **/ #include "chess.h" #include "fathom/tbprobe.h" ULONG g_uEgtbLock = 0; // // A probed tablebase win/loss doesn't carry a real distance-to-mate // (Syzygy WDL doesn't encode one), so we report a large decisive // score that stays safely below NMATE. This keeps it from being // mistaken for (and ply-adjusted as) a literal forced mate elsewhere // in the engine while still dominating ordinary evaluation scores. // #define EGTB_WIN_SCORE (NMATE - VALUE_QUEEN) #define EGTB_LOSS_SCORE (-EGTB_WIN_SCORE) static FLAG g_fEgtbInitialized = FALSE; void InitializeEGTB(void) /** Routine description: [Re]Initialize the Syzygy EGTB system. Called during system startup and when the user uses "set" to change the EGTB path. Parameters: void (uses g_Options.szEGTBPath) Return value: void **/ { CHAR *szPath = g_Options.szEGTBPath; if (TRUE == g_fEgtbInitialized) { tb_free(); g_fEgtbInitialized = FALSE; } if ((szPath != NULL) && strlen(szPath) > 0) { if (TRUE == tb_init(szPath)) { g_fEgtbInitialized = TRUE; if (TB_LARGEST > 0) { Trace("Found Syzygy endgame tablebases (up to %u men).\n\n", TB_LARGEST); } } } } void CleanupEGTB(void) /** Routine description: Cleanup the Syzygy EGTB system. Parameters: void Return value: void **/ { if (TRUE == g_fEgtbInitialized) { tb_free(); g_fEgtbInitialized = FALSE; } } static void _BuildFathomBitboards(IN POSITION *pos, OUT UINT64 *pu64White, OUT UINT64 *pu64Black, OUT UINT64 *pu64Kings, OUT UINT64 *pu64Queens, OUT UINT64 *pu64Rooks, OUT UINT64 *pu64Bishops, OUT UINT64 *pu64Knights, OUT UINT64 *pu64Pawns) /** Routine description: Build the set of bitboards Fathom's tb_probe_wdl wants out of our POSITION. Fathom expects the standard tablebase square numbering (a1 == bit 0 ... h8 == bit 63), which is exactly what our TO64() macro produces -- note this is *not* the same square numbering as our engine's own internal BBSQUARE/COOR_TO_BB bitboards, so we must not use those macros here. Parameters: POSITION *pos, UINT64 *pu64White ... pu64Pawns Return value: static void **/ { ULONG uColor; ULONG u; COOR c; PIECE p; UINT64 bb; *pu64White = *pu64Black = 0ULL; *pu64Kings = *pu64Queens = *pu64Rooks = 0ULL; *pu64Bishops = *pu64Knights = *pu64Pawns = 0ULL; for (uColor = BLACK; uColor <= WHITE; uColor++) { UINT64 *pu64Side = (uColor == WHITE) ? pu64White : pu64Black; for (u = 0; u < pos->uPawnCount[uColor]; u++) { c = pos->cPawns[uColor][u]; ASSERT(IS_ON_BOARD(c)); bb = (1ULL << (TO64(c))); *pu64Side |= bb; *pu64Pawns |= bb; } for (u = 0; u < pos->uNonPawnCount[uColor][0]; u++) { c = pos->cNonPawns[uColor][u]; ASSERT(IS_ON_BOARD(c)); bb = (1ULL << (TO64(c))); *pu64Side |= bb; if (0 == u) { ASSERT(IS_KING(pos->rgSquare[c].pPiece)); *pu64Kings |= bb; continue; } p = pos->rgSquare[c].pPiece; ASSERT(p && !IS_PAWN(p) && !IS_KING(p)); if (IS_KNIGHT(p)) *pu64Knights |= bb; else if (IS_BISHOP(p)) *pu64Bishops |= bb; else if (IS_ROOK(p)) *pu64Rooks |= bb; else { ASSERT(IS_QUEEN(p)); *pu64Queens |= bb; } } } } FLAG ProbeEGTB(SEARCHER_THREAD_CONTEXT *ctx, SCORE *piScore) /** Routine description: Search for a board position in the Syzygy EGTB files on disk via Fathom. This only returns a WDL (win/draw/loss) result, not a literal mate distance -- see EGTB_WIN_SCORE / EGTB_LOSS_SCORE. Parameters: SEARCHER_THREAD_CONTEXT *ctx, SCORE *piScore Return value: FLAG **/ { POSITION *pos = &(ctx->sPosition); UINT64 u64White, u64Black, u64Kings, u64Queens; UINT64 u64Rooks, u64Bishops, u64Knights, u64Pawns; unsigned uEp; unsigned uWdl; ULONG wcount, bcount; FLAG fResult; if (FALSE == g_fEgtbInitialized || 0 == TB_LARGEST) { return(FALSE); } // // Syzygy WDL probing requires no castling rights on the board and // a zero halfmove (fifty move rule) clock; the WDL tables don't // encode either. // if ((0 != pos->bvCastleInfo) || (0 != pos->uFifty)) { return(FALSE); } wcount = pos->uNonPawnCount[WHITE][0] + pos->uPawnCount[WHITE]; bcount = pos->uNonPawnCount[BLACK][0] + pos->uPawnCount[BLACK]; if ((wcount + bcount) > TB_LARGEST) { return(FALSE); } INC(ctx->sCounters.egtb.uProbes); _BuildFathomBitboards(pos, &u64White, &u64Black, &u64Kings, &u64Queens, &u64Rooks, &u64Bishops, &u64Knights, &u64Pawns); uEp = 0; if (IS_ON_BOARD(pos->cEpSquare)) { uEp = TO64(pos->cEpSquare); } AcquireSpinLock(&g_uEgtbLock); uWdl = tb_probe_wdl(u64White, u64Black, u64Kings, u64Queens, u64Rooks, u64Bishops, u64Knights, u64Pawns, /* rule50 */ 0, /* castling */ 0, uEp, (pos->uToMove == WHITE)); ReleaseSpinLock(&g_uEgtbLock); if (TB_RESULT_FAILED == uWdl) { return(FALSE); } switch (uWdl) { case TB_WIN: *piScore = EGTB_WIN_SCORE; break; case TB_LOSS: *piScore = EGTB_LOSS_SCORE; break; case TB_CURSED_WIN: case TB_BLESSED_LOSS: case TB_DRAW: default: *piScore = 0; // g_iDrawValue[pos->uToMove]; break; } fResult = TRUE; #ifdef PERF_COUNTERS INC(ctx->sCounters.egtb.uHits); #endif return(fResult); } // // Convert a Fathom tablebase square index (a1==0 ... h8==63) back into // our own COOR encoding. Inverse of TO64(). // #define FROM64(s) \ ((((7 - ((s) >> 3)) << 4)) | ((s) & 0x7)) static PIECE _TbPromotesToPiece(IN unsigned uPromotes) /** Routine description: Map a TB_PROMOTES_* value from a Fathom root probe result to our own PIECE constant. Parameters: unsigned uPromotes Return value: static PIECE **/ { switch (uPromotes) { case TB_PROMOTES_QUEEN: return(QUEEN); case TB_PROMOTES_ROOK: return(ROOK); case TB_PROMOTES_BISHOP: return(BISHOP); case TB_PROMOTES_KNIGHT: return(KNIGHT); default: return(0); } } FLAG ProbeEGTBRoot(SEARCHER_THREAD_CONTEXT *ctx, MOVE *pmv, SCORE *piScore) /** Routine description: Probe the Syzygy DTZ tables for the best move at the root. Unlike ProbeEGTB (WDL-only, used at interior nodes), this uses DTZ to pick a move that actually makes progress towards mate / the fifty-move-rule reset -- Syzygy WDL alone has no notion of "closer to mate" so relying on it exclusively could shuffle forever inside a won position. This is expected to be called once at the root, before the normal iterative search, when <= TB_LARGEST pieces remain on the board. We ask Fathom for its suggested (from, to, promotion) and then find the matching move in our own already-generated root move list, rather than reconstructing a MOVE by hand, so we inherit our engine's own move flags (capture, en passant, special, etc). Parameters: SEARCHER_THREAD_CONTEXT *ctx, MOVE *pmv, SCORE *piScore Return value: FLAG : TRUE if a move was found and *pmv / *piScore were set. **/ { POSITION *pos = &(ctx->sPosition); UINT64 u64White, u64Black, u64Kings, u64Queens; UINT64 u64Rooks, u64Bishops, u64Knights, u64Pawns; unsigned uEp; unsigned uResult; unsigned uWdl; COOR cFrom, cTo; PIECE pPromotes; ULONG wcount, bcount; ULONG u; if (FALSE == g_fEgtbInitialized || 0 == TB_LARGEST) { return(FALSE); } wcount = pos->uNonPawnCount[WHITE][0] + pos->uPawnCount[WHITE]; bcount = pos->uNonPawnCount[BLACK][0] + pos->uPawnCount[BLACK]; if ((wcount + bcount) > TB_LARGEST) { return(FALSE); } _BuildFathomBitboards(pos, &u64White, &u64Black, &u64Kings, &u64Queens, &u64Rooks, &u64Bishops, &u64Knights, &u64Pawns); uEp = 0; if (IS_ON_BOARD(pos->cEpSquare)) { uEp = TO64(pos->cEpSquare); } AcquireSpinLock(&g_uEgtbLock); uResult = tb_probe_root(u64White, u64Black, u64Kings, u64Queens, u64Rooks, u64Bishops, u64Knights, u64Pawns, pos->uFifty, pos->bvCastleInfo, uEp, (pos->uToMove == WHITE), NULL); ReleaseSpinLock(&g_uEgtbLock); if ((TB_RESULT_FAILED == uResult) || (TB_RESULT_CHECKMATE == uResult) || (TB_RESULT_STALEMATE == uResult)) { // // No usable move (either failed, or the earlier terminal-state // check already handles checkmate/stalemate). // return(FALSE); } // // A blessed win / cursed loss / plain draw at the root: don't // steer the search away from its own move choice, since these // are all draws under the fifty move rule and normal search // handles that correctly already. Only override the root move // when this is a clear win or a position we must play carefully // to avoid losing (both cases: follow the DTZ suggestion). // uWdl = TB_GET_WDL(uResult); if ((TB_DRAW == uWdl) || (TB_CURSED_WIN == uWdl) || (TB_BLESSED_LOSS == uWdl)) { return(FALSE); } cFrom = FROM64(TB_GET_FROM(uResult)); cTo = FROM64(TB_GET_TO(uResult)); pPromotes = _TbPromotesToPiece(TB_GET_PROMOTES(uResult)); for (u = ctx->sMoveStack.uBegin[ctx->uPly]; u < ctx->sMoveStack.uEnd[ctx->uPly]; u++) { MOVE mv = ctx->sMoveStack.mvf[u].mv; if ((mv.cFrom == cFrom) && (mv.cTo == cTo) && (mv.pPromoted == pPromotes)) { *pmv = mv; *piScore = (TB_WIN == uWdl) ? EGTB_WIN_SCORE : EGTB_LOSS_SCORE; return(TRUE); } } // // Fathom suggested a move we couldn't find in our own move list; // this shouldn't happen for a legally-generated root, but don't // trust it if it does. // ASSERT(FALSE); return(FALSE); }