/** Copyright (c) Scott Gasch Module Name: root.c Abstract: Routines dealing with the root of the search tree. Specifically move ordering at the root node, searching, search support, pondering, pondering support, and the main iterative deepening loop. Author: Scott Gasch (scott.gasch@gmail.com) 21 May 2004 Revision History: $Id: root.c 356 2008-07-02 16:18:12Z scott $ **/ #include "chess.h" volatile MOVE_TIMER g_MoveTimer; ULONG g_uIterateDepth; ULONG g_uSoftExtendLimit; ULONG g_uHardExtendLimit; ULONG g_uExtensionReduction[MAX_PLY_PER_SEARCH]; FLAG g_fCanSplit[MAX_PLY_PER_SEARCH]; SCORE g_iRootScore[2] = {0, 0}; SCORE g_iScore; static void _SetMoveTimerForPonder(void) /** Routine description: Set the move timer before a ponder operation. A time limit of -1 means search forever... so the only way out of the ponder is for new user input to interrupt it. Parameters: void Return value: static void **/ { g_MoveTimer.dStartTime = SystemTimeStamp(); g_MoveTimer.dSoftTimeLimit = -1; g_MoveTimer.dHardTimeLimit = -1; g_MoveTimer.uNodeCheckMask = 0x200 - 1; g_MoveTimer.bvFlags = 0; } void SetMoveTimerToThinkForever(void) /** Routine description: Parameters: void Return value: void **/ { g_MoveTimer.dStartTime = SystemTimeStamp(); g_MoveTimer.dSoftTimeLimit = -1; g_MoveTimer.dHardTimeLimit = -1; g_MoveTimer.uNodeCheckMask = 0x1000000 - 1; g_MoveTimer.bvFlags = 0; } void SetMoveTimerForSearch(FLAG fSwitchOver, ULONG uColor) /** Routine description: Set the move timer for a search operation. Parameters: FLAG fSwitchOver : if TRUE then this search is a converted ponder (i.e. they made the ponder move and now we are searching). Return value: void **/ { ULONG uMovesDone = GetMoveNumber(uColor); double dClock = (double)g_Options.uMyClock; double dSec = 0; ULONG uMargin = 0; ULONG uMovesToDo; // // If switching over from a ponder search leave the start time and // move flags alone (i.e. N seconds in the past already). // if (FALSE == fSwitchOver) { g_MoveTimer.dStartTime = SystemTimeStamp(); g_MoveTimer.bvFlags = 0; } // // Check the clock more often in search if there is little time // left on the clock -- we can't afford an oversearch when the // bullet game is down to the wire. // #ifdef DEBUG g_MoveTimer.uNodeCheckMask = 0x800 - 1; #else if (WeAreRunningASuite()) { g_MoveTimer.uNodeCheckMask = 0x20000 - 1; } else if (g_Options.u64MaxNodeCount) { g_MoveTimer.uNodeCheckMask = 0x1000 - 1; } else { if (dClock < 10.0) { g_MoveTimer.uNodeCheckMask = 0x400 - 1; } else { g_MoveTimer.uNodeCheckMask = 0x8000 - 1; } } #endif switch (g_Options.eClock) { // // Fixed time per move. Think for g_uMyIncrement seconds exactly and // no hard time limit. // case CLOCK_FIXED: dSec = (double)g_Options.uMyIncrement; uMargin = 0; break; // // N moves per time control or entire game in time control. // case CLOCK_NORMAL: if (g_Options.uMovesPerTimePeriod != 0) { // // N moves per time control. // ASSERT(g_Options.uMovesPerTimePeriod < 256); uMovesToDo = (g_Options.uMovesPerTimePeriod - ((uMovesDone - 1) % g_Options.uMovesPerTimePeriod)); #ifdef DEBUG Trace("SetMoveTimer: %u moves left to do this in %s sec.\n", uMovesToDo, TimeToString(dClock)); #endif ASSERT(uMovesToDo <= g_Options.uMovesPerTimePeriod); dSec = (dClock / (double)uMovesToDo); dSec *= 0.95; uMargin = (ULONG)(dSec * 2.2); if (uMovesToDo <= 5) { dSec *= 0.75; uMargin /= 2; } break; } else { // // Fixed time finish entire game. // #ifdef DEBUG Trace("SetMoveTimer: finish the game in %s sec.\n", TimeToString(dClock)); #endif dSec = dClock / 60; uMargin = (ULONG)(dSec * 2.2); } break; // // We get back a certain number of seconds with each move made. // case CLOCK_INCREMENT: dSec = g_Options.uMyIncrement; dSec += (dClock / 50); uMargin = (int)(dSec * 2.2); #ifdef DEBUG Trace("SetMoveTimer: finish the game in %s sec (+%u per move).\n", TimeToString(dClock), g_Options.uMyIncrement); #endif // // If we are running out of time, think for less than the // increment. // if (dClock < 20.0) { dSec -= g_Options.uMyIncrement; uMargin = 0; } ASSERT(dSec > 0); break; case CLOCK_NONE: #ifdef DEBUG Trace("SetMoveTimer: no time limit, think until interrupted.\n"); #endif g_MoveTimer.dHardTimeLimit = -1; g_MoveTimer.dSoftTimeLimit = -1; goto post; default: ASSERT(FALSE); break; } if ((dSec + uMargin) >= (dClock * 7 / 8)) uMargin = 0; g_MoveTimer.dSoftTimeLimit = g_MoveTimer.dStartTime + dSec; g_MoveTimer.dHardTimeLimit = g_MoveTimer.dStartTime + dSec + uMargin; post: if (TRUE == g_Options.fShouldPost) { if (g_MoveTimer.dSoftTimeLimit > 0) { Trace("SetMoveTimer: Soft time limit %s seconds.\n", TimeToString(g_MoveTimer.dSoftTimeLimit - g_MoveTimer.dStartTime)); ASSERT(g_MoveTimer.dHardTimeLimit > 0); Trace("SetMoveTimer: Hard time limit %s seconds.\n", TimeToString(g_MoveTimer.dHardTimeLimit - g_MoveTimer.dStartTime)); if (TRUE == fSwitchOver) { Trace("SetMoveTimer: Already searched %s seconds.\n", TimeToString(SystemTimeStamp() - g_MoveTimer.dStartTime)); } } #ifdef DEBUG Trace("SetMoveTimer: Checking input and timer every %u nodes.\n", g_MoveTimer.uNodeCheckMask); #endif } } void ReInitializeSearcherContext(POSITION *pos, SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: Reinitializes a SEARCHER_THREAD_CONTEXT structure. Parameters: POSITION *pos, SEARCHER_THREAD_CONTEXT *ctx Return value: void **/ { if (NULL != pos) { memmove(&(ctx->sPosition), pos, sizeof(POSITION)); // Establish a correct NNUE accumulator baseline for whatever // position was just bound in -- MakeMove/UnmakeMove only // maintain sNnueAccum *incrementally* from here on; nothing // else ever (re)establishes it from scratch. This is the one // choke point every real "go"/"ponder" (root.c's own two // InitializeSearcherContext call sites), every MP=1 split // worker (split.c), and setboard's downstream effect (the // position SetRootPosition commits to g_RootPosition only // actually reaches a context, and therefore an accumulator, // via this function at the next go) all pass through. // Unconditional, not gated on whether NNUE eval is currently // toggled on (EvalCommand's g_fUseNnueEval) -- cheap (32 // active features x 2 colors, not per-node), and keeps // toggling NNUE on mid-game correct for free instead of // needing its own separate re-sync path. // // Defensive, though: this function is also called from many // test-only sites (testrecogn.c especially) that construct a // POSITION by hand, sometimes mid-construction, not // guaranteed to have valid king placement for both colors at // the exact moment they call this. NnueAccumRecompute's own // ASSERTs are deliberately strict for its direct, deliberate // callers (the "nnue eval" command, testnnue.c) -- this one // call site instead checks first and silently skips (leaving // sNnueAccum stale) rather than crashing every caller that // doesn't care about NNUE correctness at all. Found exactly // this way: TestRecogn tripped the ASSERT on its first run // after this hook was added. if (IS_ON_BOARD(ctx->sPosition.cKingSq[WHITE]) && IS_KING(ctx->sPosition.rgSquare[ctx->sPosition.cKingSq[WHITE]].pPiece) && (PIECE_COLOR(ctx->sPosition.rgSquare[ctx->sPosition.cKingSq[WHITE]].pPiece) == WHITE) && IS_ON_BOARD(ctx->sPosition.cKingSq[BLACK]) && IS_KING(ctx->sPosition.rgSquare[ctx->sPosition.cKingSq[BLACK]].pPiece) && (PIECE_COLOR(ctx->sPosition.rgSquare[ctx->sPosition.cKingSq[BLACK]].pPiece) == BLACK)) { NnueAccumRecompute(&ctx->sNnueAccum[WHITE], &ctx->sPosition, WHITE); NnueAccumRecompute(&ctx->sNnueAccum[BLACK], &ctx->sPosition, BLACK); } } ctx->uPly = 0; memset(&(ctx->sCounters), 0, sizeof(COUNTERS)); } void InitializeSearcherContext(POSITION *pos, SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: Initialize a SEARCHER_THREAD_CONTEXT structure. Parameters: POSITION *pos, SEARCHER_THREAD_CONTEXT *ctx Return value: void **/ { ULONG u; #ifdef FH_REGRESSION_COLLECTION // ctx is frequently a function-static struct reused across many // Think()/Ponder() calls within one process (root.c) -- the // unconditional memset just below would otherwise silently close // this thread's collection file and drop any buffered-but- // unflushed records on every single search, not just at process // startup. Flush (don't lose data) and preserve the open file // across the wipe; uThreadNumber is NOT preserved here on purpose // -- a helper thread's real uThreadNumber isn't known yet at this // call site (split.c sets it on the line immediately after // calling this), so FhCollectEmit opens the file lazily on first // real use instead, by which point uThreadNumber is correct. See // fhcollect.c's own comments. FILE *pSavedFhFile = ctx->sFhCollectWriter.pFile; if (NULL != pSavedFhFile) { FhCollectFlush(ctx); } #endif memset(ctx, 0, sizeof(SEARCHER_THREAD_CONTEXT)); ReInitializeSearcherContext(pos, ctx); ctx->sMoveStack.uUnblockedKeyValue[0] = 1; #ifdef FH_REGRESSION_COLLECTION ctx->sFhCollectWriter.pFile = pSavedFhFile; #endif for (u = 1; u < MAX_PLY_PER_SEARCH; u++) { ctx->sMoveStack.uUnblockedKeyValue[u] = ctx->sMoveStack.uUnblockedKeyValue[u-1] + 0x28F5C28; } // // A zeroed COOR (0x00) is on-board (a8), not "none" -- unlike // MOVE's uMove==0 sentinel, COOR needs an explicit reset to // ILLEGAL_COOR so enprise/trapped hint slots start out invalid. // for (u = 0; u < MAX_PLY_PER_SEARCH; u++) { ctx->cEnprise[u][0] = ctx->cEnprise[u][1] = ILLEGAL_COOR; ctx->cTrapped[u] = ILLEGAL_COOR; } } void InitializeLightweightSearcherContext(POSITION *pos, LIGHTWEIGHT_SEARCHER_CONTEXT *ctx) /** Routine description: Initialize a LIGHTWEIGHT_SEARCHER_CONTEXT structure. Parameters: POSITION *pos, LIGHTWEIGHT_SEARCHER_CONTEXT *ctx Return value: void **/ { ULONG u; memset(ctx, 0, sizeof(LIGHTWEIGHT_SEARCHER_CONTEXT)); if (NULL != pos) { memmove(&(ctx->sPosition), pos, sizeof(POSITION)); } ctx->uPly = 0; ctx->sMoveStack.uUnblockedKeyValue[0] = 1; for (u = 1; u < MAX_PLY_PER_SEARCH; u++) { ctx->sMoveStack.uUnblockedKeyValue[u] = ctx->sMoveStack.uUnblockedKeyValue[u-1] + 0x28F5C28; } } void PostMoveSearchReport(SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: Dump a report to stdout after every move. Parameters: SEARCHER_THREAD_CONTEXT *ctx, Return value: void **/ { double d; char buf[256]; #ifdef PERF_COUNTERS double n; #endif d = (double)g_MoveTimer.dEndTime - (double)g_MoveTimer.dStartTime + 0.01; ASSERT(d); Trace("---------------------------------------------\n" "Searched for %5.1f seconds, saw %" COMPILER_LONGLONG_UNSIGNED_FORMAT " nodes (%" COMPILER_LONGLONG_UNSIGNED_FORMAT " qnodes) (%6.0f nps).\n", d, ctx->sCounters.tree.u64TotalNodeCount, ctx->sCounters.tree.u64QNodeCount, ((double)ctx->sCounters.tree.u64TotalNodeCount / d)); if (g_Options.fThinking) { snprintf(buf, ARRAY_LENGTH(buf), "d%u, %s, %3.1fs, %6.0fknps, PV=%s", ctx->uRootDepth / ONE_PLY, ScoreToString(ctx->iRootScore), d, ((double)ctx->sCounters.tree.u64TotalNodeCount / d / 1000), ctx->szLastPV); Trace("tellothers %s\n", buf); } #ifdef MP #ifdef PERF_COUNTERS if (ctx->sCounters.parallel.uNumSplits > 0) { n = (double)ctx->sCounters.parallel.uNumSplits; Trace("Split %u times total ", ctx->sCounters.parallel.uNumSplits); Trace("(~%7.2fx/sec).\n", (n / d)); d = n + 1; ASSERT(d); n = (double)ctx->sCounters.parallel.uNumSplitsTerminated; Trace(" ...%u (%5.2f percent) terminated early.\n", ctx->sCounters.parallel.uNumSplitsTerminated, ((n/d) * 100.0)); DumpHelperIdlenessReport(); } #endif #endif #ifdef TEST AnalyzeFullHashTable(); #endif #ifdef PERF_COUNTERS d = (double)(ctx->sCounters.hash.u64Probes) + 1; ASSERT(d); Trace("Hashing percentages: (%5.3f total, %5.3f useful)\n", ((double)(ctx->sCounters.hash.u64OverallHits) / d) * 100.0, ((double)(ctx->sCounters.hash.u64UsefulHits) / d) * 100.0); d = (double)(ctx->sCounters.pawnhash.u64Probes) + 1; ASSERT(d); n = (double)(ctx->sCounters.pawnhash.u64Hits); Trace("Pawn hash hitrate: %5.3f percent.\n", (n/d) * 100.0); // [EXPERIMENTAL] King safety hash hit/miss cycle detail is printed // below, next to the pawn hash cycle breakdown (EVAL_TIME section), // rather than here. n = (double)(ctx->sCounters.tree.u64NullMoveSuccess); d = (double)(ctx->sCounters.tree.u64NullMoves) + 1; ASSERT(d); Trace("Null move cutoff rate: %5.3f percent.\n", ((n / d) * 100.0)); #ifdef PERF_COUNTERS Trace("Overt move skips: LMP=%llu moves.\n", ctx->sCounters.tree.u64LMPPruned); { ULONG uR; UINT64 u64TotalTries = 0; for (uR = 0; uR < NULL_R_BUCKETS; uR++) { u64TotalTries += ctx->sCounters.tree.u64NullMoveTriesByR[uR]; } u64TotalTries += (0 == u64TotalTries); // avoid /0 if never tried Trace("Null move (R): tries/successes, successes%% of ALL null-move " "tries (sums to the overall cutoff rate above):\n"); for (uR = 0; uR < NULL_R_BUCKETS; uR++) { UINT64 u64Tries = ctx->sCounters.tree.u64NullMoveTriesByR[uR]; if (0 == u64Tries) continue; Trace(" %u/%" COMPILER_LONGLONG_UNSIGNED_FORMAT "/%" COMPILER_LONGLONG_UNSIGNED_FORMAT "(%.1f%%)", uR, u64Tries, ctx->sCounters.tree.u64NullMoveSuccessByR[uR], 100.0 * (double)ctx->sCounters.tree.u64NullMoveSuccessByR[uR] / (double)u64TotalTries); } Trace("\n"); } { ULONG uTier; Trace("LMR reductions (plies):"); for (uTier = 0; uTier < JOINT_EXTRA_TIER_COUNT; uTier++) { UINT64 u64Tries = ctx->sCounters.tree.u64JointExtraTries[uTier]; if (0 == u64Tries) continue; Trace(" %u/%" COMPILER_LONGLONG_UNSIGNED_FORMAT, uTier + 1, u64Tries); } Trace("\n"); } #endif if (ctx->sCounters.egtb.uProbes > 0) { n = (double)(ctx->sCounters.egtb.uHits); d = (double)(ctx->sCounters.egtb.uProbes); Trace("EGTB probes: %u (%5.3f percent hit rate).\n", ctx->sCounters.egtb.uProbes, ((n / d) * 100.0)); } #ifdef TEST_ENDGAME n = (double)(ctx->sCounters.tree.u64EndgamePruningSuccess); d = n + (double)(ctx->sCounters.tree.u64EndgamePruningFailures); if (d != 0.0) { Trace("Endgame pruning heuristic success rate was %5.3f percent.\n" "Endgame pruning heuristic was " "%"COMPILER_LONGLONG_UNSIGNED_FORMAT " / " "%"COMPILER_LONGLONG_UNSIGNED_FORMAT ".\n", ((n / d) * 100.0), ctx->sCounters.tree.u64EndgamePruningSuccess, (ctx->sCounters.tree.u64EndgamePruningSuccess + ctx->sCounters.tree.u64EndgamePruningFailures)); } else { Trace("Never used endgame pruning heuristic.\n"); } #endif n = (double)ctx->sCounters.tree.u64BetaCutoffsOnFirstMove; d = (double)ctx->sCounters.tree.u64BetaCutoffs + 1; Trace("First move beta cutoff rate was %5.3f percent.\n", ((n / d) * 100.0)); #ifdef LAZY_EVAL { // 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; #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; // Eval exit breakdown (call-count share) folded in here // alongside the cycle-cost share, so both facets of "how // much does each exit path matter" live in one place // instead of two separate, easy-to-miss Trace blocks. Trace("Avg. cpu cycles in eval, by exit path:\n" " super lazy: %8.1f cyc/call (%5.2f%% of calls, %5.1f%% of total eval cycles)\n" " lazy: %8.1f cyc/call (%5.2f%% of calls, %5.1f%% of total eval cycles)\n" " full: %8.1f cyc/call (%5.2f%% of calls, %5.1f%% of total eval cycles)\n" " overall: %8.1f cyc/call\n", (dSuperLazy ? (double)u64SLCyc / dSuperLazy : 0.0), (dSuperLazy / d) * 100.0, (u64AllCyc ? 100.0 * (double)u64SLCyc / (double)u64AllCyc : 0.0), (dLazy ? (double)u64LCyc / dLazy : 0.0), (dLazy / d) * 100.0, (u64AllCyc ? 100.0 * (double)u64LCyc / (double)u64AllCyc : 0.0), (dFull ? (double)u64FCyc / dFull : 0.0), (dFull / d) * 100.0, (u64AllCyc ? 100.0 * (double)u64FCyc / (double)u64AllCyc : 0.0), (dRealTotal ? (double)u64AllCyc / dRealTotal : 0.0)); } #else 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); #endif } #endif #ifdef EVAL_TIME // 2026-10-20: NNUE's own per-call latency, independent of the // LAZY_EVAL block above (NNUE is a runtime g_fUseNnueEval switch, // not a #ifdef LAZY_EVAL path, so it never fed that block's // counters). Direct answer to "is the lazy-probe gate paying for // itself": the gate costs u64CyclesNnueProbeCall on EVERY attempt // (hit or miss -- u64NnueProbeAttempts, not just u64LazyEvals' // hit count) and only SAVES a full-forward call on a hit, so the // gate is worth it only if (hit rate) * (forward cost - probe // cost) exceeds (miss rate) * (probe cost). // // Caveat printed alongside rather than silently baked in: each // TIMED_EVAL_CALL is a pair of back-to-back SystemReadTimeStamp- // Counter() (rdtsc) reads, and rdtsc itself isn't free (commonly // cited around ~80 cycles, more with the serializing behavior // some cores give it) -- for NnueForward's thousands of cycles // that overhead is noise, but for NnueProbeScore's much smaller // 512-wide dot product it may not be. Measure the pair's own // overhead right here (an empty back-to-back read, no call in // between) so the raw numbers below can be read against it rather // than taken as exact. { UINT64 u64OverheadStart = SystemReadTimeStampCounter(); UINT64 u64OverheadEnd = SystemReadTimeStampCounter(); UINT64 u64PairOverhead = u64OverheadEnd - u64OverheadStart; UINT64 u64ProbeCyc = ctx->sCounters.tree.u64CyclesNnueProbeCall; UINT64 u64ProbeAttempts = ctx->sCounters.tree.u64NnueProbeAttempts; UINT64 u64FwdCyc = ctx->sCounters.tree.u64CyclesNnueForwardCall; UINT64 u64FwdCalls = ctx->sCounters.tree.u64FullEvals; double dProbeAvg = u64ProbeAttempts ? (double)u64ProbeCyc / (double)u64ProbeAttempts : 0.0; double dFwdAvg = u64FwdCalls ? (double)u64FwdCyc / (double)u64FwdCalls : 0.0; if ((0 != u64ProbeAttempts) || (0 != u64FwdCalls)) { Trace("NNUE eval primitive latency (rdtsc pair overhead ~%" COMPILER_LONGLONG_UNSIGNED_FORMAT" cyc, subtract for a " "rough net estimate):\n" " NnueProbeScore: %10.1f cyc/call (raw, %" COMPILER_LONGLONG_UNSIGNED_FORMAT" attempts, net ~%10.1f)\n" " NnueForward: %10.1f cyc/call (raw, %" COMPILER_LONGLONG_UNSIGNED_FORMAT" calls, net ~%10.1f)\n", u64PairOverhead, dProbeAvg, u64ProbeAttempts, (dProbeAvg > (double)u64PairOverhead) ? (dProbeAvg - (double)u64PairOverhead) : 0.0, dFwdAvg, u64FwdCalls, (dFwdAvg > (double)u64PairOverhead) ? (dFwdAvg - (double)u64PairOverhead) : 0.0); } } #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" " check by amount (1/4,1/2,3/4,1 ply): %u %u %u %u\n" " 1mv by amount (1/4,1/2,3/4,1 ply): %u %u %u %u\n" " pawn push by amount (1/4,1/2,3/4,1 ply): %u %u %u %u\n" " %u / %llu qsearch nodes (%.1f%%) were in-check " "evasion nodes\n" " too-deep bailout: %llu ply-limit, %llu " "qsearch-hard-extend-limit\n", ctx->sCounters.extension.uCheck, ctx->sCounters.extension.uQExtend, ctx->sCounters.extension.uOneLegalMove, ctx->sCounters.extension.uNoLegalKingMoves, ctx->sCounters.extension.uMultiCheck, ctx->sCounters.extension.uPawnPush, ctx->sCounters.extension.uMateThreat, ctx->sCounters.extension.uZugzwang, ctx->sCounters.extension.uSingularReplyToCheck, ctx->sCounters.extension.uEndgame, ctx->sCounters.extension.uBotvinnikMarkoff, ctx->sCounters.extension.uRecapture, ctx->sCounters.extension.uCheckByQuarterPly[1], ctx->sCounters.extension.uCheckByQuarterPly[2], ctx->sCounters.extension.uCheckByQuarterPly[3], ctx->sCounters.extension.uCheckByQuarterPly[4], ctx->sCounters.extension.uOneLegalMoveByQuarterPly[1], ctx->sCounters.extension.uOneLegalMoveByQuarterPly[2], ctx->sCounters.extension.uOneLegalMoveByQuarterPly[3], ctx->sCounters.extension.uOneLegalMoveByQuarterPly[4], ctx->sCounters.extension.uPawnPushByQuarterPly[1], ctx->sCounters.extension.uPawnPushByQuarterPly[2], ctx->sCounters.extension.uPawnPushByQuarterPly[3], ctx->sCounters.extension.uPawnPushByQuarterPly[4], ctx->sCounters.extension.uQExtendChaseNodes, ctx->sCounters.tree.u64QNodeCount, (ctx->sCounters.tree.u64QNodeCount ? (100.0 * ctx->sCounters.extension.uQExtendChaseNodes / (double)ctx->sCounters.tree.u64QNodeCount) : 0.0), ctx->sCounters.tree.u64TooDeepPlyLimit, ctx->sCounters.tree.u64TooDeepQsearchExtendLimit); #ifdef EVAL_TIME { // // Per-term breakdown of the average above -- board_ // representation/EVAL.md section 0b. "other" covers every // term not individually timed (pawn structure detail beyond // _EvalPawns' own bucket, bishop pairs, bad-trade/passer-race // detection, lazy-eval margin estimation, etc.) computed as // the remainder rather than its own counter, so this always // sums exactly to the total above regardless of what future // terms get their own bucket. // UINT64 u64Pawns = ctx->sCounters.tree.u64CyclesEvalPawns; UINT64 u64Knight = ctx->sCounters.tree.u64CyclesEvalKnight; UINT64 u64Bishop = ctx->sCounters.tree.u64CyclesEvalBishop; UINT64 u64Rook = ctx->sCounters.tree.u64CyclesEvalRook; UINT64 u64Queen = ctx->sCounters.tree.u64CyclesEvalQueen; UINT64 u64King = ctx->sCounters.tree.u64CyclesEvalKing; // // u64PreLazy already contains u64Pawns as a subset (pawns // runs nested inside the pre-lazy segment, not after it) -- // it's used below only for u64Named's total, not displayed // directly (its sub-segments are each measured and displayed // on their own: super-lazy, material setup, passer/badtrade/ // bishop, pawns, lazy decision). // UINT64 u64PreLazy = ctx->sCounters.tree.u64CyclesEvalPreLazy; UINT64 u64PostMisc = ctx->sCounters.tree.u64CyclesEvalPostLazyMisc; UINT64 u64AttackPop = ctx->sCounters.tree.u64CyclesEvalAttackTablePop; UINT64 u64Drawish = ctx->sCounters.tree.u64CyclesEvalDrawish; UINT64 u64Named = (u64PreLazy + u64Knight + u64Bishop + u64Rook + u64Queen + u64King + u64PostMisc + u64AttackPop + u64Drawish); UINT64 u64Total = ctx->sCounters.tree.u64CyclesInEval; UINT64 u64Unaccounted = (u64Total >= u64Named) ? (u64Total - u64Named) : 0; UINT64 u64LazyDecision = ctx->sCounters.tree.u64CyclesEvalLazyDecision; UINT64 u64CKSD = ctx->sCounters.tree.u64CyclesEvalCountKingSafetyDefects; UINT64 u64SuperLazy = ctx->sCounters.tree.u64CyclesEvalSuperLazy; UINT64 u64MaterialSetup = ctx->sCounters.tree.u64CyclesEvalMaterialSetup; UINT64 u64PasserBadtradeBishop = ctx->sCounters.tree.u64CyclesEvalPasserBadtradeBishop; // // Reorganized (2026-09-09) into one major section per exit // path, each listing its own segments in the chronological // order Eval() actually executes them, since a flat dump of // every segment side by side no longer maps to which exit // path pays for what -- a super-lazy exit call never reaches // pawns/pieces at all, and a lazy exit call never reaches the // per-piece/attack-table segments. Percentages are still each // segment's share of u64Total (total cycles across every // Eval() call of any exit path), not renormalized per // section, since these segments' own counters are accumulated // across all calls that reach them (not split out per exit // path) -- e.g. "pawns" below is paid by both lazy-exit and // full-eval calls combined. // Trace("eval breakdown (%% of total cycles in eval), " "by exit-path phase, chronological within each:\n"); Trace(" [super-lazy] (paid by every call)\n"); Trace(" material/army-scaler setup: %5.1f%%\n", (u64Total ? (100.0 * (double)u64MaterialSetup / (double)u64Total) : 0.0)); Trace(" super-lazy material check: %5.1f%%\n", (u64Total ? (100.0 * (double)u64SuperLazy / (double)u64Total) : 0.0)); Trace(" [lazy+] (paid by lazy-exit and full-eval calls)\n"); Trace(" pawns: %5.1f%%\n", (u64Total ? (100.0 * (double)u64Pawns / (double)u64Total) : 0.0)); Trace(" passer-races/bad-trades/bishop-pairs: %5.1f%%\n", (u64Total ? (100.0 * (double)u64PasserBadtradeBishop / (double)u64Total) : 0.0)); Trace(" lazy gate + EstimatePositionalScore: %5.1f%% " "(of which CountKingSafetyDefects: %5.1f%%)\n", (u64Total ? (100.0 * (double)u64LazyDecision / (double)u64Total) : 0.0), (u64Total ? (100.0 * (double)u64CKSD / (double)u64Total) : 0.0)); Trace(" [full-only] (paid only by full-eval calls)\n"); Trace(" attack-table population: %5.1f%%\n", (u64Total ? (100.0 * (double)u64AttackPop / (double)u64Total) : 0.0)); Trace(" knight: %5.1f%% bishop: %5.1f%% rook: %5.1f%%\n", (u64Total ? (100.0 * (double)u64Knight / (double)u64Total) : 0.0), (u64Total ? (100.0 * (double)u64Bishop / (double)u64Total) : 0.0), (u64Total ? (100.0 * (double)u64Rook / (double)u64Total) : 0.0)); Trace(" queen: %5.1f%% king: %5.1f%%\n", (u64Total ? (100.0 * (double)u64Queen / (double)u64Total) : 0.0), (u64Total ? (100.0 * (double)u64King / (double)u64Total) : 0.0)); Trace(" post-lazy misc (passers/danger/trapped/scalers): %5.1f%%\n", (u64Total ? (100.0 * (double)u64PostMisc / (double)u64Total) : 0.0)); Trace(" drawish-situation scaling: %5.1f%%\n", (u64Total ? (100.0 * (double)u64Drawish / (double)u64Total) : 0.0)); // "Unaccounted" here is mostly EVAL_TIME's own instrumentation // overhead, not missing eval work: each per-piece // TIMED_EVAL_CALL (knight/bishop/rook/queen/king) reads the // cycle counter twice, and on a full board that's 2 reads x // up to ~16 pieces per Eval() call. A one-off diagnostic // (2026-09-09, since reverted) wrapped the whole piece-eval // section in a single outer timer and compared it against the // sum of the per-piece buckets -- the difference (i.e. the // extra reads' own cost, not algorithmic eval time) accounted // for the large majority of this percentage on a full board; // true untimed code (bit-scan loop control, small setup/tail // arithmetic) was a small remainder. Re-run that diagnostic // (wrap the piece-eval loops end to end in one blanket timer, // diff against the named per-piece sum) before assuming a // change to this number reflects a real difference in eval // cost. Trace(" unaccounted: %5.1f%%\n", (u64Total ? (100.0 * (double)u64Unaccounted / (double)u64Total) : 0.0)); { UINT64 u64PHits = ctx->sCounters.tree.u64CountEvalPawnsHit; UINT64 u64PMisses = ctx->sCounters.tree.u64CountEvalPawnsMiss; UINT64 u64PHitCycles = ctx->sCounters.tree.u64CyclesEvalPawnsHit; UINT64 u64PMissCycles = ctx->sCounters.tree.u64CyclesEvalPawnsMiss; UINT64 u64KSProbes = ctx->sCounters.kingsafetyhash.u64Probes; UINT64 u64KSHits = ctx->sCounters.kingsafetyhash.u64Hits; UINT64 u64KSMisses = (u64KSProbes >= u64KSHits) ? (u64KSProbes - u64KSHits) : 0; Trace("hash probes (avg cycles per probe):\n"); Trace(" pawn hash: %" COMPILER_LONGLONG_UNSIGNED_FORMAT " hits (avg %.1f cyc), %" COMPILER_LONGLONG_UNSIGNED_FORMAT " misses (avg %.1f cyc), hit rate %.2f%%\n", u64PHits, (u64PHits ? (double)u64PHitCycles / (double)u64PHits : 0.0), u64PMisses, (u64PMisses ? (double)u64PMissCycles / (double)u64PMisses : 0.0), ((u64PHits + u64PMisses) ? (100.0 * (double)u64PHits / (double)(u64PHits + u64PMisses)) : 0.0)); // Keeping this counter around: see eval.c's Eval() / // rgKingSafetyHashProbe. Trace(" king safety hash: %" COMPILER_LONGLONG_UNSIGNED_FORMAT " hits (avg %.1f cyc), %" COMPILER_LONGLONG_UNSIGNED_FORMAT " misses (avg %.1f cyc), hit rate %.2f%%\n", u64KSHits, (u64KSHits ? (double)ctx->sCounters.kingsafetyhash.u64CyclesHit / (double)u64KSHits : 0.0), u64KSMisses, (u64KSMisses ? (double)ctx->sCounters.kingsafetyhash.u64CyclesMiss / (double)u64KSMisses : 0.0), (u64KSProbes ? (100.0 * (double)u64KSHits / (double)u64KSProbes) : 0.0)); } } #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"); } SCORE RootSearch(SEARCHER_THREAD_CONTEXT *ctx, SCORE iAlpha, SCORE iBeta, ULONG uDepth) /** Routine description: Search at the root of the whole tree. Parameters: SEARCHER_THREAD_CONTEXT *ctx, SCORE iAlpha, SCORE iBeta, ULONG uDepth, Return value: SCORE **/ { PLY_INFO *pi = &ctx->sPlyInfo[ctx->uPly]; SCORE iBestScore = -INFINITY_SCORE; SCORE iInitialAlpha = iAlpha; MOVE mv; ULONG x, y; SCORE iScore; ULONG uNextDepth; UINT64 u64StartingNodeCount; ULONG uNumLegalMoves; // This function's own move loop overwrites sMoveStack[ctx->uPly]'s // iValue with a node-count/score composite for next-iteration move // ordering -- not a generate.c tier encoding, and not meaningful to // ComputeMoveScore's decoder (see MOVE_SCORING_SCHEME's own comment, // chess.h). pi->eMoveScoringScheme = MOVE_SCORING_ROOT; #ifdef DEBUG POSITION *pos = &ctx->sPosition; ASSERT(IS_VALID_SCORE(iAlpha)); ASSERT(IS_VALID_SCORE(iBeta)); ASSERT(iAlpha < iBeta); ASSERT((ctx->uPly == 0) || (ctx->uPly == 1)); // 1 is for under a ponder memcpy(&pi->sPosition, pos, sizeof(POSITION)); #endif ctx->sCounters.tree.u64TotalNodeCount++; pi->PV[ctx->uPly] = NULLMOVE; pi->mvBest = NULLMOVE; g_MoveTimer.bvFlags |= TIMER_SEARCHING_FIRST_MOVE; #ifdef DEBUG Trace("---- depth=%u, a=%d, b=%d ----\n", uDepth / ONE_PLY, iAlpha, iBeta); #endif uNumLegalMoves = 0; for (x = ctx->sMoveStack.uBegin[ctx->uPly]; x < ctx->sMoveStack.uEnd[ctx->uPly]; x++) { SelectMoveAtRoot(ctx, x); if (ctx->sMoveStack.mvf[x].bvFlags & MVF_MOVE_SEARCHED) break; ctx->sMoveStack.mvf[x].bvFlags |= MVF_MOVE_SEARCHED; mv = ctx->sMoveStack.mvf[x].mv; mv.bvFlags |= WouldGiveCheck(ctx, mv); if (MakeMove(ctx, mv)) { uNumLegalMoves++; u64StartingNodeCount = ctx->sCounters.tree.u64TotalNodeCount; // // TODO: Fancy recap shit here? // // RootSearch has its own move loop here, separate from // searchsup.c's SearchOneMove (which stashes this on // every OTHER recursion into Search() -- see chess.h's // PLY_INFO.uParentMoveFHEstimate comment). Without this, // a root child's ply slot reads whatever was last left in // it, caught by valgrind as a real uninitialised-value // read inside search.c's _RefineNodeTypeGuess. Root tries // every legal move in full, not a leftover-ordered // subset, so there is no real per-move FH% prediction to // report here -- MOVE_FH_ESTIMATE_NO_DATA ("unknown"), not // MOVE_FH_ESTIMATE_NONLEFTOVER (which would falsely claim // high confidence). ctx->sPlyInfo[ctx->uPly].uParentMoveFHEstimate = MOVE_FH_ESTIMATE_NO_DATA; uNextDepth = uDepth - ONE_PLY; if (IS_CHECKING_MOVE(mv)) { uNextDepth += HALF_PLY; ctx->sPlyInfo[ctx->uPly].iExtensionAmount = HALF_PLY; } ctx->sSearchFlags.fVerifyNullmove = TRUE; ctx->sSearchFlags.uQsearchDepth = 0; ctx->sSearchFlags.iCumulativeExtend = 0; if (uNumLegalMoves == 1) { // // Move 1, full a..b window // iScore = -Search(ctx, -iBeta, -iAlpha, uNextDepth); } else { // // Moves 2..N, minimal window search // ASSERT(x > 0); ASSERT(uNumLegalMoves > 1); iScore = -Search(ctx, -iAlpha - 1, -iAlpha, uNextDepth); if ((iAlpha < iScore) && (iScore < iBeta)) { iScore = -Search(ctx, -iBeta, -iAlpha, uNextDepth); } } ctx->sSearchFlags.fVerifyNullmove = FALSE; TDMove(ctx, uDepth, iAlpha, iBeta, (1 == uNumLegalMoves) ? "FIRST" : (IS_CAPTURE_OR_PROMOTION(mv) ? "CAP" : "QUIET"), "FULL", 0.0, (double)(uNextDepth - (uDepth - ONE_PLY)) / ONE_PLY, iScore, FALSE); UnmakeMove(ctx, mv); if (WE_SHOULD_STOP_SEARCHING) { iBestScore = INVALID_SCORE; goto end; } #ifdef DEBUG Trace("%2u. %-8s => node=%" COMPILER_LONGLONG_UNSIGNED_FORMAT ", predict=%d, actual=%d, ", x + 1, MoveToSan(mv, &ctx->sPosition), u64StartingNodeCount, ctx->sMoveStack.mvf[x].iValue, iScore); ASSERT(PositionsAreEquivalent(&pi->sPosition, &ctx->sPosition)); #endif // // Keep track of how many moves are under each one we // search and use this as the basis for root move ordering // next iteration. // u64StartingNodeCount = (ctx->sCounters.tree.u64TotalNodeCount - u64StartingNodeCount); u64StartingNodeCount >>= 9; u64StartingNodeCount &= (MAX_INT / 4); ctx->sMoveStack.mvf[x].iValue = (SCORE)(u64StartingNodeCount + iScore); #ifdef DEBUG Trace("next_predict: %d\n", ctx->sMoveStack.mvf[x].iValue); ASSERT(iBestScore <= iAlpha); ASSERT(iAlpha < iBeta); #endif if (iScore > iBestScore) { iBestScore = iScore; pi->mvBest = mv; if (iScore > iAlpha) { // // We have a best move so far. As of now it is // the one we'll be playing. Also make sure to // search it first at the next depth. // #ifdef DEBUG Trace("Best so far... root score=%d.\n", iScore); #endif ctx->mvRootMove = mv; ctx->iRootScore = iScore; ctx->uRootDepth = uDepth; ctx->sMoveStack.mvf[x].iValue = MAX_INT; // // If there was a previous PV move then knock its // score down so that it will be considered second // on the next depth. // for (y = ctx->sMoveStack.uBegin[ctx->uPly]; y < x; y++) { if (ctx->sMoveStack.mvf[y].iValue == MAX_INT) { ctx->sMoveStack.mvf[y].iValue /= 2; } } UpdatePV(ctx, mv); if (iScore >= iBeta) { // // Root fail high... // UpdateDynamicMoveOrdering(ctx, uDepth, mv, iScore, x + 1); UtilPrintPV(ctx, iAlpha, iBeta, iScore, mv); KEEP_TRACK_OF_FIRST_MOVE_FHs(iBestScore == -INFINITY_SCORE); ctx->sMoveStack.mvf[x].bvFlags &= ~MVF_MOVE_SEARCHED; goto end; } else { // // Root PV change... // UtilPrintPV(ctx, iAlpha, iBeta, iScore, mv); iAlpha = iScore; } } } g_MoveTimer.bvFlags &= ~TIMER_SEARCHING_FIRST_MOVE; } } if (iAlpha == iInitialAlpha) { // // Root fail low... // ASSERT(iBestScore <= iAlpha); ASSERT(PositionsAreEquivalent(pos, &pi->sPosition)); UtilPrintPV(ctx, iAlpha, iBeta, iBestScore, mv); } end: if ((INVALID_SCORE != iBestScore) && !WE_SHOULD_STOP_SEARCHING) { TDResult(ctx, uDepth, iInitialAlpha, iBeta, pi->iEval, iBestScore); } return(iBestScore); } static GAME_RESULT _DetectPreMoveTerminalStates(SEARCHER_THREAD_CONTEXT *ctx, FLAG fInCheck, MOVE *pmv) { ULONG u; MOVE mv; MOVE mvLegal; ULONG uNumLegal = 0; GAME_RESULT ret; POSITION *pos = &ctx->sPosition; ULONG uMajors[2], uMinors[2], uWhiteBishops[2], uBlackBishops[2]; ret.eResult = RESULT_IN_PROGRESS; ret.szDescription[0] = '\0'; pmv->uMove = 0; // Look for insufficient material. FOREACH_COLOR(u) { uMajors[u] = (FastCountBits(pos->bbPieces[u][ROOK]) + FastCountBits(pos->bbPieces[u][QUEEN])); uMinors[u] = (FastCountBits(pos->bbPieces[u][KNIGHT]) + FastCountBits(pos->bbPieces[u][BISHOP])); uWhiteBishops[u] = WhiteSqBishopCount(pos, u); uBlackBishops[u] = FastCountBits(pos->bbPieces[u][BISHOP]) - uWhiteBishops[u]; } FOREACH_COLOR(u) { ULONG uNonKing = FastCountBits(NON_KING_PIECES_BB(pos, u)); ULONG uNonKingFlip = FastCountBits(NON_KING_PIECES_BB(pos, FLIP(u))); if ((pos->bbPawns[u] != 0) || (pos->bbPawns[FLIP(u)] != 0)) { continue; } if (uNonKing == 0) { if (uMajors[FLIP(u)] == 0 && uMinors[FLIP(u)] == 1) { ret.eResult = RESULT_DRAW; strcpy(ret.szDescription, "insufficient material"); return ret; } else if (uMajors[FLIP(u)] == 0 && (uMinors[FLIP(u)] == uWhiteBishops[FLIP(u)] || uMinors[FLIP(u)] == uBlackBishops[FLIP(u)])) { ret.eResult = RESULT_DRAW; strcpy(ret.szDescription, "insufficient material"); return ret; } } else if ((uNonKing == uWhiteBishops[u] && uNonKingFlip == uWhiteBishops[FLIP(u)]) || (uNonKing == uBlackBishops[u] && uNonKingFlip == uBlackBishops[FLIP(u)])) { ret.eResult = RESULT_DRAW; strcpy(ret.szDescription, "insufficient material"); return ret; } } // Count the number of legal moves the side on move has in this position. for (u = ctx->sMoveStack.uBegin[ctx->uPly]; u < ctx->sMoveStack.uEnd[ctx->uPly]; u++) { mv = ctx->sMoveStack.mvf[u].mv; if (MakeMove(ctx, mv)) { mvLegal = mv; uNumLegal += 1; UnmakeMove(ctx, mv); if (uNumLegal > 1) { return ret; } } } // If there's only one legal move then tell the caller what it is. if (uNumLegal == 1) { *pmv = mvLegal; return ret; } // There are no legal moves so its checkmate or stalemate. ASSERT(uNumLegal == 0); if (TRUE == fInCheck) { if (ctx->sPosition.uToMove == WHITE) { ret.eResult = RESULT_BLACK_WON; strcpy(ret.szDescription, "black checkmated white"); return ret; } ret.eResult = RESULT_WHITE_WON; strcpy(ret.szDescription, "white checkmated black"); return ret; } ret.eResult = RESULT_DRAW; sprintf(ret.szDescription, "%s stalemated %s", (ctx->sPosition.uToMove == WHITE) ? "black" : "white", (ctx->sPosition.uToMove == WHITE) ? "while" : "black"); return ret; } static GAME_RESULT _DetectPostMoveTerminalStates(SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: This function is called by Iterate and MakeTheMove to declare wins/losses/draws. Parameters: SEARCHER_THREAD_CONTEXT *ctx Return value: FLAG **/ { GAME_RESULT ret; FLAG fInCheck = InCheck(&ctx->sPosition, ctx->sPosition.uToMove); // Did we just make the 50th+ move without progress? if (ctx->sPosition.uFifty >= 100) { ret.eResult = RESULT_DRAW; strcpy(ret.szDescription, "fifty moves without progress"); return ret; } GenerateMoves(ctx, NULLMOVE, (fInCheck ? GENERATE_ESCAPES : GENERATE_ALL_MOVES)); // Did we just repeat a position for the 3rd time? if (IsLegalDrawByRepetition()) { ret.eResult = RESULT_DRAW; strcpy(ret.szDescription, "third repetition of position"); return ret; } // Otherwise look for checkmates, stalemates and insufficient material. MOVE mvUnused; return _DetectPreMoveTerminalStates(ctx, fInCheck, &mvUnused); } static void _IterateSetSearchGlobals(ULONG uDepth) { ULONG u, v; ULONG uDontSplitLessThan; // // Set some globals that will be used throughout this search. // g_uIterateDepth = uDepth; ASSERT(g_uIterateDepth > 0); ASSERT(g_uIterateDepth < MAX_PLY_PER_SEARCH); // // For use in scaling back extensions that are very far from // the root. // // 2026-09-10: onset moved in from 2x/4x iterate depth to 1.5x/3x // (a uniform 0.75x scaling of the old 2x..4x span) -- a forcing // chain of check/singular-reply extensions could previously run // completely unthrottled out to double the nominal search depth // before any taper at all kicked in, which is exactly the shape // of runaway seen on check-heavy positions (see CLAUDE.md's // 2026-09 extension-tuning notes). Same taper shape, just starts // sooner. // // Iterate g_Soft g_Hard // | | | | | | | | // 0 1 1.5 1.875 2.25 3 (x Iterate) // |----------------|-----|-----|-------|----... // |full full full |-1/4 |-1/2 |-3/4 |none... // g_uSoftExtendLimit = g_uIterateDepth * 3 / 2; g_uHardExtendLimit = g_uIterateDepth * 3; // g_uExtensionReduction[]'s own bands, below, must stay reachable // within [0, MAX_PLY_PER_SEARCH) regardless of how deep g_uIterateDepth // is -- naive g_uSoftExtendLimit/g_uHardExtendLimit (2x/4x iterate // depth) can hugely exceed MAX_PLY_PER_SEARCH for a deep requested // search (e.g. sd=30 -> hard limit 120 >> 64), which would leave every // reachable u in the loop below permanently in the "0 penalty" band -- // no taper at all for the entire representable ply range. Deliberately // a LOCAL, separately-capped depth for this table only -- // g_uSoftExtendLimit/g_uHardExtendLimit themselves stay uncapped (raw // g_uIterateDepth * 2/4) since g_uHardExtendLimit is also read directly // elsewhere (searchsup.c's QSearch runaway-depth cutoff) where capping // it would be an unrelated, unintended behavior change. { ULONG uEffIterateDepth = MIN(g_uIterateDepth, (MAX_PLY_PER_SEARCH - 4) / 4); ULONG uEffSoftLimit = uEffIterateDepth * 3 / 2; ULONG uEffHardLimit = uEffIterateDepth * 3; for (u = 0; u < MAX_PLY_PER_SEARCH; u++) { if (u < uEffSoftLimit) { g_uExtensionReduction[u] = 0; } else if (u < (uEffSoftLimit + (uEffIterateDepth * 3 / 8))) { g_uExtensionReduction[u] = QUARTER_PLY; } else if (u < (uEffSoftLimit + (uEffIterateDepth * 3 / 4))) { g_uExtensionReduction[u] = HALF_PLY; } else if (u < uEffHardLimit) { g_uExtensionReduction[u] = THREE_QUARTERS_PLY; } else { g_uExtensionReduction[u] = 5 * ONE_PLY; } } } // // Determine how much depth we need below a point in the tree in // order to split the search (which is based on iteration depth). // uDontSplitLessThan = (ULONG)((double)(uDepth) * 0.20); for (v = 0; v < MAX_PLY_PER_SEARCH; v++) { g_fCanSplit[v] = ((v + 1) >= uDontSplitLessThan); } g_fCanSplit[0] = FALSE; } #define SKIP_GRADUAL_OPENING 600 #define INITIAL_HALF_WINDOW 75 #define FIRST_FAIL_STEP 150 #define SECOND_FAIL_STEP 375 void _IterateWidenWindow(ULONG uColor, SCORE iScore, SCORE *piBound, ULONG *puState, int iDir) { SCORE iRoughScore = g_iRootScore[uColor]; static SCORE _Steps[] = { FIRST_FAIL_STEP, SECOND_FAIL_STEP, INFINITY_SCORE }; // Trace("State = %u.\n", *puState); // Trace("Bound from %d to ", *piBound); if ((abs(iRoughScore - iScore) > SKIP_GRADUAL_OPENING) || (*puState == 2)) { *piBound = iDir * INFINITY_SCORE; *puState = 2; } else { ASSERT(*puState < 2); *piBound = iScore; *piBound += iDir * _Steps[*puState]; *puState += 1; } // Trace("%d.\n", *piBound); // Trace("State = %u\n", *puState); } GAME_RESULT Iterate(SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: Main iterative deepening loop for both thinking and pondering. Call RootSearch with progressively deeper depths while there is remaining time, we haven't found a forced mate, and there's no user interruption. Parameters: SEARCHER_THREAD_CONTEXT *ctx Return value: FLAG : is the game over? **/ { ULONG uNumRootFailLows = 0; ULONG uDepth; ULONG uFailStateLo = 0; ULONG uFailStateHi = 0; ULONG uColor; ULONG u; SCORE iAlpha = -INFINITY_SCORE; SCORE iBeta = +INFINITY_SCORE; SCORE iScore; FLAG fInCheck = InCheck(&(ctx->sPosition), ctx->sPosition.uToMove); MOVE mv; GAME_RESULT ret; #ifdef DEBUG POSITION board; memcpy(&board, &(ctx->sPosition), sizeof(POSITION)); VerifyPositionConsistency(&board, FALSE); #endif uColor = ctx->sPosition.uToMove; g_iRootScore[uColor] = Eval(ctx, iAlpha, iBeta, NULL); g_iRootScore[FLIP(uColor)] = -g_iRootScore[uColor]; // // Generate moves here so that RootSearch doesn't have to. // ASSERT((ctx->uPly == 0) || (ctx->uPly == 1)); // 1 is under a ponder ctx->sPlyInfo[ctx->uPly].fInCheck = fInCheck; GenerateMoves(ctx, NULLMOVE, (fInCheck ? GENERATE_ESCAPES : GENERATE_ALL_MOVES)); ASSERT(ctx->sMoveStack.uBegin[0] == 0); // // See if we are sitting at a checkmate or stalemate position; no // sense searching if so. // ret = _DetectPreMoveTerminalStates(ctx, fInCheck, &mv); if (ret.eResult != RESULT_IN_PROGRESS) { goto end; } // Game still in progress but only one legal move available? if (mv.uMove != 0) { ctx->mvRootMove = mv; ctx->iRootScore = 0; ctx->uRootDepth = 0; ctx->sPlyInfo[0].PV[0] = mv; ctx->sPlyInfo[0].PV[1] = NULLMOVE; strcpy(ctx->szLastPV, "only"); if (!g_Options.fPondering) { Trace("ONLY MOVE --> stop searching now\n"); g_MoveTimer.bvFlags |= TIMER_STOPPING; g_MoveTimer.bvFlags |= TIMER_CURRENT_OBVIOUS; } else { Trace("ONLY PONDER --> move immediately if prediction correct.\n"); g_MoveTimer.bvFlags |= TIMER_MOVE_IMMEDIATELY; g_MoveTimer.bvFlags |= TIMER_CURRENT_OBVIOUS; } goto end; } // // If we're within Syzygy tablebase range at the root, let DTZ pick // our move directly instead of relying on ordinary search. WDL // alone (used at interior nodes via RecognLookup/ProbeEGTB) has no // notion of "closer to mate", so relying on it exclusively could // shuffle forever inside a won position; DTZ is what actually // drives progress towards mate / the fifty-move-rule reset. // if (TRUE == ProbeEGTBRoot(ctx, &mv, &iScore)) { ctx->mvRootMove = mv; ctx->iRootScore = iScore; ctx->uRootDepth = 0; ctx->sPlyInfo[0].PV[0] = mv; ctx->sPlyInfo[0].PV[1] = NULLMOVE; strcpy(ctx->szLastPV, ""); if (!g_Options.fPondering) { Trace("TABLEBASE MOVE --> stop searching now\n"); g_MoveTimer.bvFlags |= TIMER_STOPPING; g_MoveTimer.bvFlags |= TIMER_CURRENT_OBVIOUS; } else { g_MoveTimer.bvFlags |= TIMER_MOVE_IMMEDIATELY; g_MoveTimer.bvFlags |= TIMER_CURRENT_OBVIOUS; } goto end; } for (uDepth = 1; uDepth <= g_Options.uMaxDepth; uDepth++) { // // Re-rank the moves based on nodecounts and mark all moves as // not yet searched at this depth. // for (u = ctx->sMoveStack.uBegin[ctx->uPly]; u < ctx->sMoveStack.uEnd[ctx->uPly]; u++) { ctx->sMoveStack.mvf[u].bvFlags &= ~MVF_MOVE_SEARCHED; } // // Make sure a..b window makes sense and set some other per-depth // globals used by the search. // _IterateSetSearchGlobals(uDepth); // // Try to get a PV for this depth before we're out of time... // do { if (iBeta > INFINITY_SCORE) iBeta = +INFINITY_SCORE; if (iAlpha < -INFINITY_SCORE) iAlpha = -INFINITY_SCORE; if (iAlpha >= iBeta) iAlpha = iBeta - 1; // Was + HALF_PLY. Trimmed by QUARTER_PLY to compensate for // search.c's qsearch-entry threshold moving from ONE_PLY down // to THREE_QUARTERS_PLY -- that change alone would otherwise // hand every line an extra ~1/4 ply of full-width search for // free, not just check-heavy ones. iScore = RootSearch(ctx, iAlpha, iBeta, uDepth * ONE_PLY + QUARTER_PLY); if (g_MoveTimer.bvFlags & TIMER_STOPPING) break; mv = ctx->mvRootMove; // // Got a PV, we're done with this depth. // if ((iAlpha < iScore) && (iScore < iBeta)) { ASSERT(iScore == ctx->iRootScore); ASSERT(mv.uMove); ASSERT(SanityCheckMove(&ctx->sPosition, mv)); // // Don't center a cp-sized aspiration window on a mate // score. A mate found at this depth may be a search // artifact (a defense pruned away by LMR/null-move/ // futility) that evaporates one ply deeper, in which case // the next iteration's score isn't a nearby mate value at // all -- it's an ordinary eval thousands of points away. // That swing blows straight past SKIP_GRADUAL_OPENING and // forces an immediate jump to the infinite window (and a // full re-search of every root move) anyway, so just go // wide on purpose instead of narrow-then-blown-open. // if ((ULONG)abs(iScore) >= NMATE) { iAlpha = -INFINITY_SCORE; iBeta = +INFINITY_SCORE; } else { iAlpha = iScore - INITIAL_HALF_WINDOW; iBeta = iScore + INITIAL_HALF_WINDOW; } g_iRootScore[uColor] = iScore; g_iRootScore[FLIP(uColor)] = -iScore; g_MoveTimer.bvFlags &= ~TIMER_RESOLVING_ROOT_FH; g_MoveTimer.bvFlags &= ~TIMER_RESOLVING_ROOT_FL; uFailStateLo = 0; uFailStateHi = 0; (void)CheckTestSuiteMove(mv, iScore, uDepth); break; } // // Root fail low? We'll have to research with a wider // window. // else if (iScore <= iAlpha) { ASSERT(ctx->mvRootMove.uMove); g_MoveTimer.bvFlags |= TIMER_RESOLVING_ROOT_FL; uNumRootFailLows++; if (uNumRootFailLows > 3) { g_MoveTimer.bvFlags |= TIMER_MANY_ROOT_FLS; } _IterateWidenWindow(uColor, iScore, &iAlpha, &uFailStateLo, -1); // // Consider all moves again with wider window... // for (u = ctx->sMoveStack.uBegin[ctx->uPly]; u < ctx->sMoveStack.uEnd[ctx->uPly]; u++) { ctx->sMoveStack.mvf[u].bvFlags &= ~MVF_MOVE_SEARCHED; } } // // Must be a root fail high. // else { ASSERT(iScore >= iBeta); ASSERT(mv.uMove); ASSERT(SanityCheckMove(&ctx->sPosition, mv)); g_MoveTimer.bvFlags |= TIMER_RESOLVING_ROOT_FH; _IterateWidenWindow(uColor, iScore, &iBeta, &uFailStateHi, +1); } } while(1); // repeat until we run out of time or fall inside a..b // // We are here because either: // // 1. we completed a search depth, // or 2. we ran out of time, // or 3. there is one legal move and we want to make it immediately, // or 4. there was user input and we unrolled the search. // // // TODO: Scale back history between iterative depths? // // // Mate-in-n when we have searched n? Stop searching. // if (IS_VALID_SCORE(iScore) && (ULONG)abs(iScore) >= (INFINITY_SCORE - uDepth)) { Trace("FORCED MATE --> stop searching now\n"); g_MoveTimer.bvFlags |= TIMER_STOPPING; } // // Have we reached user's maxdepth option? Stop searching. // if (uDepth >= g_Options.uMaxDepth) { Trace("DEPTH LIMIT --> stop searching now\n"); g_MoveTimer.bvFlags |= TIMER_STOPPING; } // // Did the move timer expire? Stop searching. // if (g_MoveTimer.bvFlags & TIMER_STOPPING) break; } g_Options.u64NodesSearched = ctx->sCounters.tree.u64TotalNodeCount; g_Options.u64BetaCutoffs = ctx->sCounters.tree.u64BetaCutoffs; g_Options.u64BetaCutoffsOnFirstMove = ctx->sCounters.tree.u64BetaCutoffsOnFirstMove; g_Options.u64CounterMoveTries = ctx->sCounters.tree.u64CounterMoveTries; g_Options.u64CounterMoveHits = ctx->sCounters.tree.u64CounterMoveHits; g_Options.u64LeftoverTries = ctx->sCounters.tree.u64LeftoverTries; g_Options.u64LeftoverAlpha = ctx->sCounters.tree.u64LeftoverAlpha; g_Options.u64LeftoverFH = ctx->sCounters.tree.u64LeftoverFH; g_Options.u64QuickNullMoves = ctx->sCounters.tree.u64QuickNullMoves; g_Options.u64QuickNullMoveSuccess = ctx->sCounters.tree.u64QuickNullMoveSuccess; #ifdef HASHCAP_COLLECTION for (u = 0; u < 2; u++) { ULONG v; for (v = 0; v < 2; v++) { g_Options.u64HashCapTries[u][v] = ctx->sCounters.tree.u64HashCapTries[u][v]; g_Options.u64HashCapFH[u][v] = ctx->sCounters.tree.u64HashCapFH[u][v]; } } #endif for (u = 0; u < CM_EVIDENCE_BUCKETS; u++) { g_Options.u64CMEvidenceTries[u] = ctx->sCounters.tree.u64CMEvidenceTries[u]; g_Options.u64CMEvidenceFH[u] = ctx->sCounters.tree.u64CMEvidenceFH[u]; } for (u = 0; u < HANG_CP_BUCKETS; u++) { g_Options.u64HangEstimateTries[u] = ctx->sCounters.tree.u64HangEstimateTries[u]; g_Options.u64HangEstimateFH[u] = ctx->sCounters.tree.u64HangEstimateFH[u]; } g_Options.u64EligibleLeftoverTries = ctx->sCounters.tree.u64EligibleLeftoverTries; g_Options.u64EligibleLeftoverFH = ctx->sCounters.tree.u64EligibleLeftoverFH; for (u = 0; u < GAP_CP_BUCKETS; u++) { g_Options.u64GapEstimateTries[u] = ctx->sCounters.tree.u64GapEstimateTries[u]; g_Options.u64GapEstimateFH[u] = ctx->sCounters.tree.u64GapEstimateFH[u]; } for (u = 0; u < MOVE_NUM_BUCKETS; u++) { g_Options.u64MoveNumEstimateTries[u] = ctx->sCounters.tree.u64MoveNumEstimateTries[u]; g_Options.u64MoveNumEstimateFH[u] = ctx->sCounters.tree.u64MoveNumEstimateFH[u]; } for (u = 0; u < SEARCH_DEPTH_BUCKETS; u++) { g_Options.u64DepthEstimateTries[u] = ctx->sCounters.tree.u64DepthEstimateTries[u]; g_Options.u64DepthEstimateFH[u] = ctx->sCounters.tree.u64DepthEstimateFH[u]; } memcpy(g_Options.u64JointTries, ctx->sCounters.tree.u64JointTries, sizeof(g_Options.u64JointTries)); memcpy(g_Options.u64JointFH, ctx->sCounters.tree.u64JointFH, sizeof(g_Options.u64JointFH)); memcpy(g_Options.u64JointExtraTries, ctx->sCounters.tree.u64JointExtraTries, sizeof(g_Options.u64JointExtraTries)); memcpy(g_Options.u64JointExtraResearch, ctx->sCounters.tree.u64JointExtraResearch, sizeof(g_Options.u64JointExtraResearch)); g_Options.u64LMPPruned = ctx->sCounters.tree.u64LMPPruned; memcpy(g_Options.u64NullMoveTriesByR, ctx->sCounters.tree.u64NullMoveTriesByR, sizeof(g_Options.u64NullMoveTriesByR)); memcpy(g_Options.u64NullMoveSuccessByR, ctx->sCounters.tree.u64NullMoveSuccessByR, sizeof(g_Options.u64NullMoveSuccessByR)); // FH_REGRESSION_COLLECTION needs no per-search copy-out here, unlike // the old FH_TABLE_COLLECTION histogram design -- FhCollectEmit // (fhcollect.c) writes each record straight to this thread's own file // as it's produced, during the search itself, so there's nothing left // to accumulate into g_Options after the fact. #ifdef JOINT_EXTRA_VERIFY memcpy(g_Options.u64JointExtraVerifyTries, ctx->sCounters.tree.u64JointExtraVerifyTries, sizeof(g_Options.u64JointExtraVerifyTries)); memcpy(g_Options.u64JointExtraVerifySilentMiss, ctx->sCounters.tree.u64JointExtraVerifySilentMiss, sizeof(g_Options.u64JointExtraVerifySilentMiss)); memcpy(g_Options.u64JointExtraVerifyNullBailoutSignature, ctx->sCounters.tree.u64JointExtraVerifyNullBailoutSignature, sizeof(g_Options.u64JointExtraVerifyNullBailoutSignature)); #endif #ifdef TEST_NULL for (u = 0; u < NC_TOTAL_BUCKETS; u++) { g_Options.u64NCAttempts[u] = ctx->sCounters.tree.u64NCAttempts[u]; g_Options.u64NCNullFH[u] = ctx->sCounters.tree.u64NCNullFH[u]; g_Options.u64NCRealFH[u] = ctx->sCounters.tree.u64NCRealFH[u]; g_Options.u64NCBothFH[u] = ctx->sCounters.tree.u64NCBothFH[u]; g_Options.u64NCNullProbeNodeSum[u] = ctx->sCounters.tree.u64NCNullProbeNodeSum[u]; g_Options.u64NCFirstMoveNodeSum[u] = ctx->sCounters.tree.u64NCFirstMoveNodeSum[u]; g_Options.u64NCRealLoopNodeSum[u] = ctx->sCounters.tree.u64NCRealLoopNodeSum[u]; } #endif // // Here we are at the end of a search. If we were: // // ...Searching then: ...Pondering then: // // 1. we ran out of time 1. they made a different move than // the predicted move and we aborted // 2. we ran out of depth // 2. we ran out of depth // 3. we found a mate-in-n // 3. we found a mate-in-n // TODO: node count limit? // (4. if they made the predicted move // then we converted to a search) // ASSERT(ctx->mvRootMove.uMove); ASSERT(SanityCheckMove(&board, ctx->mvRootMove)); if (g_MoveTimer.bvFlags & TIMER_RESOLVING_ROOT_FL) { ctx->sPlyInfo[ctx->uPly].PV[ctx->uPly] = ctx->mvRootMove; ctx->sPlyInfo[ctx->uPly].PV[ctx->uPly+1] = NULLMOVE; } ret.eResult = RESULT_IN_PROGRESS; ret.szDescription[0] = '\0'; end: g_MoveTimer.dEndTime = SystemTimeStamp(); return ret; } void MakeTheMove(SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: Make the move that search selected in the official game list. Parameters: SEARCHER_THREAD_CONTEXT *ctx Return value: FLAG **/ { MOVE mv = ctx->mvRootMove; ASSERT(mv.uMove); if (TRUE == g_Options.fThinking) { // // TODO: keep track of how many moves in a row we are at or // below a draw score and use this to respond to draw offers. // // // Was this a search that converted from a ponder? // if (TRUE == g_Options.fSuccessfulPonder) { if (FALSE == OfficiallyMakeMove(g_Options.mvPonder, 0, FALSE)) { UtilPanic(CANNOT_OFFICIALLY_MAKE_MOVE, GetRootPosition(), (void *)(size_t)g_Options.mvPonder.uMove, NULL, NULL, __FILE__, __LINE__); } g_Options.fSuccessfulPonder = FALSE; } // Only do d2d4 stuff if we're under xboard -- it's the standard. if (g_Options.fRunningUnderXboard) { Trace("move %s\n", MoveToIcs(mv)); } else { Trace("move %s\n", MoveToSan(mv, &ctx->sPosition)); } if (FALSE == OfficiallyMakeMove(mv, g_iScore, FALSE)) { UtilPanic(CANNOT_OFFICIALLY_MAKE_MOVE, GetRootPosition(), (void *)(size_t)mv.uMove, NULL, NULL, __FILE__, __LINE__); } if (g_Options.fVerbosePosting) { PostMoveSearchReport(ctx); PostMoveTestSuiteReport(ctx); } } g_Options.fPondering = g_Options.fThinking = FALSE; } GAME_RESULT Ponder(POSITION *pos) /** Routine description: Prepare to ponder. Parameters: POSITION *pos Return value: FLAG **/ { static SEARCHER_THREAD_CONTEXT ctx; GAME_RESULT ret; InitializeSearcherContext(pos, &ctx); g_Options.fPondering = TRUE; g_Options.fThinking = FALSE; g_Options.fSuccessfulPonder = FALSE; ret.eResult = RESULT_IN_PROGRESS; ret.szDescription[0] = '\0'; // // When do we not want to ponder // if ((g_Options.ePlayMode == FORCE_MODE) || (g_Options.uMyClock < 10)) { g_Options.fPondering = FALSE; return ret; } // // What should we ponder? // g_Options.mvPonder = GetPonderMove(pos); if (g_Options.mvPonder.uMove == 0) { g_Options.fPondering = FALSE; return ret; } ASSERT(SanityCheckMove(pos, g_Options.mvPonder)); // // Prepare to ponder by doing some maintenance on the dynamic move // ordering scheme counters, changing the dirty tag in the hash // code, and clearing the root nodes-per-move hash. // MaintainDynamicMoveOrdering(); DirtyHashTable(); // // Make the move ponder move. // if (FALSE == MakeMove(&ctx, g_Options.mvPonder)) { ASSERT(FALSE); g_Options.fPondering = FALSE; return ret; } // // TODO: probe the book // _SetMoveTimerForPonder(); // // TODO: Any preEval? // // // TODO: Set draw value // MakeStatusLine(); #if (PERF_COUNTERS && MP) ClearHelperThreadIdleness(); #endif ASSERT(ctx.uPly == 1); ret = Iterate(&ctx); ASSERT(ctx.uPly == 1); if (ret.eResult == RESULT_IN_PROGRESS) { MakeTheMove(&ctx); return _DetectPostMoveTerminalStates(&ctx); } else { return ret; } } GAME_RESULT Think(POSITION *pos) /** Routine description: Prepare to think. Parameters: POSITION *pos Return value: FLAG **/ { static SEARCHER_THREAD_CONTEXT ctx; static ULONG _resign_count = 0; MOVE mvBook; CHAR *p; // // 2026-09-16: barrier against a real, confirmed race (split.c's // per-split rendezvous only guarantees that *that split's own* // recruited helpers have finished before its slot is recycled -- // nothing previously stitched that into a *global* guarantee that // every helper thread anywhere is idle before a brand-new search // begins reusing g_SplitInfo[] slots). Concretely reproduced: a // helper thread recruited by one Think() call's search (e.g. an // engine-startup analysis on the default position) can still be // mid-flight when a *later*, unrelated Think() call starts, // recycles the same slot index, and overwrites its mvPathToHere/ // sRootPosition -- the straggler helper then replays a stale move // from the earlier search against the new search's root position // (observed directly: split.c's helper-side SanityCheckMove failed // replaying "Ng1-f3" -- an opening-book-shaped move from the // default starting position -- against an unrelated mid-game FEN). // g_uNumHelpersAvailable/g_uNumHelperThreads are always valid (0/0 // in non-MP or single-cpu builds), so this is unconditional and a // no-op there. Deliberately unbounded, not a bounded best-effort // retry like QuitCommand's shutdown wait (command.c) -- silently // proceeding without this invariant is exactly the bug being // fixed, so giving up early here would just reintroduce it; a // warning fires if it's taking unexpectedly long so a genuine // deadlock elsewhere is still observable rather than a silent hang. // g_uNumHelpersAvailable/g_uNumHelperThreads only exist when split.c // is actually compiled in (its whole body is #ifdef MP) -- guard // this the same way, or a non-MP profile (e.g. TEST=1) fails to // link against symbols that were never defined. #ifdef MP if (g_uNumHelpersAvailable != g_uNumHelperThreads) { double dWaitStart = SystemTimeStamp(); FLAG fWarned = FALSE; // Tight busy-spin, no sleep -- this should normally resolve in // microseconds (same idiom as split.c's own per-split // "wait for uNumThreadsHelping" loop), so sleeping even 1ms per // iteration here would add real, gratuitous latency to every // single move on the ordinary (non-racing) path. while (g_uNumHelpersAvailable != g_uNumHelperThreads) { if ((FALSE == fWarned) && ((SystemTimeStamp() - dWaitStart) > 1.0)) { Trace("Think(): still waiting for %lu/%lu helper " "thread(s) to go idle before starting a new " "search (%.1fs so far) -- possible stuck helper?\n", (unsigned long)(g_uNumHelperThreads - g_uNumHelpersAvailable), (unsigned long)g_uNumHelperThreads, SystemTimeStamp() - dWaitStart); fWarned = TRUE; } } } #endif InitializeSearcherContext(pos, &ctx); g_MoveTimer.bvFlags = 0; g_Options.fPondering = FALSE; g_Options.fThinking = TRUE; g_Options.fSuccessfulPonder = FALSE; // // Prepare to think by doing some maintenance on the dynamic move // ordering scheme counters, changing the dirty tag in the hash // code, and clearing the root nodes-per-move hash. // if (NULL != (p = PositionToFen(&(ctx.sPosition)))) { Trace("The root position is: %s\n", p); SystemFreeMemory(p); } MaintainDynamicMoveOrdering(); DirtyHashTable(); // // Check the opening book, maybe it has a move for us. // if (g_uBookProbeFailures < 3 && !WeAreRunningASuite() && g_Options.szBookName[0] != '\0') { mvBook = BookMove(pos, BOOKMOVE_SELECT_MOVE); if (mvBook.uMove == 0) { g_uBookProbeFailures++; } else { g_uBookProbeFailures = 0; ASSERT(SanityCheckMove(pos, mvBook)); ctx.mvRootMove = mvBook; MakeTheMove(&ctx); return _DetectPostMoveTerminalStates(&ctx); } } // // TODO: Any preEval? // // // Set time limit for move // SetMoveTimerForSearch(FALSE, pos->uToMove); // // TODO: Set draw value // #if (PERF_COUNTERS && MP) ClearHelperThreadIdleness(); #endif GAME_RESULT ret = Iterate(&ctx); if (ret.eResult == RESULT_IN_PROGRESS) { if (g_Options.iResignThreshold != 0 && g_iRootScore[ctx.sPosition.uToMove] < g_Options.iResignThreshold) { _resign_count++; if (_resign_count > 2) { if (ctx.sPosition.uToMove == WHITE) { ret.eResult = RESULT_BLACK_WON; strcpy(ret.szDescription, "white resigned"); } else { ret.eResult = RESULT_WHITE_WON; strcpy(ret.szDescription, "black resigned"); } Trace("tellics resign\n"); } return ret; } else { _resign_count = 0; } MakeTheMove(&ctx); return _DetectPostMoveTerminalStates(&ctx); } else { return ret; } }