diff options
| author | Scott Gasch <[email protected]> | 2026-08-27 07:41:41 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-08-27 07:42:25 -0700 |
| commit | 4ce6a76b0946e4ba943d29c506c9e2fb00601efd (patch) | |
| tree | b072c6fe2a5e8527a28f5c821d76591e93fa86f6 /src/search.c | |
| parent | 7857096f39e16619a42ee85b4aa593abd846b74a (diff) | |
Baseline: uPositional data-calibrated fix, enprise/trapped hints, EBF/beta-cutoff/counter-move stats, script.c FPE fix.
No LMR, no counter-move-driven move ordering (both explored separately,
kept out for now -- counter-move measured worse, ~655->647 solved on
ecm879 @ sn=4M with a leaner tree beforehand). Futility pruning restored.
Verified: 647/879 solved, EBF 4.609 @ sn=4M; 684/879 solved, EBF 3.995
@ 20s/move, 1cpu, 256m hash (typhoon_baseline.log).
The counter-move table is still written and its stats still tracked
(dynamic.c) for diagnostic purposes, but generate.c no longer reads it
for move ordering, so it has no effect on search behavior in this
commit.
lmr_testing/ holds the in-flight graded-LMR + counter-move code (not
applied here) with notes on what was already tried and measured, so a
future session can resume without re-deriving it.
Diffstat (limited to 'src/search.c')
| -rwxr-xr-x | src/search.c | 154 |
1 files changed, 102 insertions, 52 deletions
diff --git a/src/search.c b/src/search.c index 4ea09b0..ce867b6 100755 --- a/src/search.c +++ b/src/search.c @@ -199,6 +199,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, } DTEnterNode(ctx, uDepth, FALSE, iAlpha, iBeta); iInitialAlpha = iAlpha; + pi->fPvNode = (iBeta != iAlpha + 1); ASSERT((IS_CHECKING_MOVE(mvLast) && (TRUE == pi->fInCheck)) || (!IS_CHECKING_MOVE(mvLast) && (FALSE == pi->fInCheck))); @@ -328,7 +329,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, goto end; } } - + // Maybe increment positional extension level b/c of nullmove search // or hash table results. if (fThreat) @@ -336,7 +337,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, iOrigExtend += THREE_QUARTERS_PLY; INC(ctx->sCounters.extension.uMateThreat); } - + // Main search loop, try moves under this position. Before we get // into the move loop, save the extensions merited by this // position in the tree (pre-move) and the original search flags. @@ -441,7 +442,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, (ctx->uPly >= 2) && (iOrigExtend == 0) && (ctx->sPlyInfo[ctx->uPly - 2].iExtensionAmount <= 0) && - (ValueOfMaterialInTroubleDespiteMove(pos, pos->uToMove))) + (ValueOfMaterialInTroubleDespiteMove(ctx, pos->uToMove))) { uFutilityMargin = (iAlpha - iRoughEval) / 2; ASSERT(uFutilityMargin); @@ -451,7 +452,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, // fall through case TRY_GENERATED_MOVES: - if (x < ctx->sMoveStack.uEnd[ctx->uPly]) + if (x < ctx->sMoveStack.uEnd[ctx->uPly]) { ASSERT(x >= ctx->sMoveStack.uBegin[ctx->uPly]); if (uLegalMoves < SEARCH_SORT_LIMIT(ctx->uPly)) @@ -480,9 +481,12 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, ASSERT(SanityCheckMove(pos, mv)); #ifdef MP - // Can we search the remaining moves in parallel? - ASSERT((uDepth / ONE_PLY - 1) >= 0); - ASSERT((uDepth / ONE_PLY - 1) < MAX_PLY_PER_SEARCH); + // Can we search the remaining moves in parallel? Note: + // uDepth can legitimately be < ONE_PLY here (fractional + // depth from a reduction) -- uDepth/ONE_PLY - 1 would + // underflow (ULONG) in that case, which is exactly why the + // uDepth >= ONE_PLY check below short-circuits before the + // g_fCanSplit[] indexing ever evaluates it. if (((uLegalMoves >= 2)) && (0 != g_uNumHelpersAvailable) && (0 == uFutilityMargin) && @@ -568,20 +572,24 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, MAX(MAX_EXTEND_PER_LINE - pf->iCumulativeExtend, 0)); } - // Decide whether or not to do history pruning - if (TRUE == WeShouldDoHistoryPruning(iRoughEval, - iAlpha, - iBeta, - ctx, - uDepth, - uLegalMoves, - mv, - (x - 1), // Note: x==0 if hash - iExtend)) + // Decide how much (if any) to reduce this move's depth -- + // graded LMR. { - ASSERT(iExtend == 0); - iExtend = -ONE_PLY; - pi->iExtensionAmount = -ONE_PLY; + INT iLMR = GetLMRReduction(iRoughEval, + iAlpha, + iBeta, + ctx, + uDepth, + uLegalMoves, + mv, + (x - 1), // Note: x==0 if hash + iExtend); + if (iLMR < 0) + { + ASSERT(iExtend == 0); + iExtend = iLMR; + pi->iExtensionAmount = iLMR; + } } // Maybe even "futility prune" this move away. @@ -623,7 +631,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, // Research deeper if history pruning failed if ((iExtend < 0) && (iScore >= iBeta)) { - uNextDepth += ONE_PLY; + uNextDepth -= iExtend; // undo the full reduction, whatever its magnitude pi->iExtensionAmount = 0; iScore = -Search(ctx, -iBeta, -iAlpha, uNextDepth); } @@ -651,12 +659,31 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, { // Update history and killers list and store in // the transposition table. - UpdateDynamicMoveOrdering(ctx, - uDepth, - mv, - iScore, + UpdateDynamicMoveOrdering(ctx, + uDepth, + mv, + iScore, x); StoreLowerBound(mv, pos, iScore, uDepth, fThreat); + + // A fail-high capturing a non-pawn piece is + // search-proven evidence that piece was en + // prise -- but the victim belongs to the + // *other* side, i.e. whoever is to move at + // ctx->uPly - 1 (ply parity), not here -- + // "despite the move you're about to make, + // this piece stays in trouble." Skip near + // mate: a fail-high there means the whole + // subtree is winning regardless of this + // particular piece, not that it was + // specifically hanging. + if (mv.pCaptured && !IS_PAWN(mv.pCaptured) && + (iBeta < +NMATE)) + { + ASSERT(ctx->uPly > 0); + RecordEnprisePieceAtPly(ctx, ctx->uPly - 1, + mv.cTo); + } KEEP_TRACK_OF_FIRST_MOVE_FHs(uLegalMoves == 1); ASSERT(SanityCheckMoves(ctx, x, VERIFY_BEFORE)); goto end; @@ -1112,33 +1139,38 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx, } ASSERT(!InCheck(pos, pos->uToMove)); - // If we get here then side on move is not in check and this - // position looks ok enough to allow him the option to stand pat - // -or- we missed when we probed the dangerhash. Also remember - // that this side has had the option to stand pat when searching - // below this point. This also means the other side is not - // allowed to generate checks on this side because even if it - // discovers a mate, there's no force since a stand pat - // opportunity exists right here. - uLegalMoves = 0; iEval = iBestScore = Eval(ctx, iAlpha, iBeta, &iPositional); - if (iBestScore > iAlpha) + + // If that Eval (above) was full (i.e. not lazy) it may have set + // en prise and trapped piece indicators. Likewise, other nodes + // at this depth may have set en prise piece hints. If these are + // set and valid, it means this is not a "quiet" position. If the + // side on the move has not been able to stand pat yet, don't let + // them now -- force them to play a move and recurse. + if (0 != ValueOfMaterialInTroubleDespiteMove(ctx, pos->uToMove)) + { + iBestScore = iAlpha; + } + else { - iAlpha = iBestScore; - ASSERT(ctx->sPlyInfo[ctx->uPly].PV[ctx->uPly].uMove == 0); - ASSERT(pi->mvBest.uMove == 0); - if (iBestScore >= iBeta) + if (iBestScore > iAlpha) { - goto end; + iAlpha = iBestScore; + ASSERT(ctx->sPlyInfo[ctx->uPly].PV[ctx->uPly].uMove == 0); + ASSERT(pi->mvBest.uMove == 0); + if (iBestScore >= iBeta) + { + goto end; + } } + ctx->sSearchFlags.fCouldStandPat[pos->uToMove] = TRUE; } - ctx->sSearchFlags.fCouldStandPat[pos->uToMove] = TRUE; - // He did not choose to stand pat here; we will be generating - // moves and searching recursively. Compute a futility score: - // any move less than this will not be searched because it will - // just cause a lazy eval answer; is has no shot to bring the - // score close enough to alpha to even consider. + // He did not choose to stand pat here or we did not allow it. We + // will be generating moves and searching recursively. Compute a + // futility score: any move less than this will not be searched + // because it will just cause a lazy eval answer; is has no shot + // to bring the score close enough to alpha to even consider. // // iEval + move_value + margin < alpha // move_value < alpha - margin - iEval @@ -1150,13 +1182,18 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx, } // We know we are not in check. If we are early in the qsearch, - // and the other side has not yet been able to stand pat, and we - // have material, generate checks here too. - fIncludeChecks = - ((pf->uQsearchDepth < pf->uQsearchCheckDepth) && - (pf->fCouldStandPat[FLIP(pos->uToMove)] == FALSE) && - (pos->uNonPawnMaterial[pos->uToMove] > (VALUE_KING + VALUE_BISHOP))); + // and the other side has not yet been able to stand pat yet, and + // we have material OR we have hanging pieces, generate checks + // here too. Checks are a "good way" to escape from "trouble". + fIncludeChecks = ((pf->uQsearchDepth < pf->uQsearchCheckDepth) && + (((pf->fCouldStandPat[FLIP(pos->uToMove)] == FALSE) && + (pos->uNonPawnMaterial[pos->uToMove] > + (VALUE_KING + VALUE_BISHOP))) || + (FALSE == ctx->sSearchFlags.fCouldStandPat[pos->uToMove]))); + GenerateMoves(ctx, NULLMOVE, _WhatToGen[fIncludeChecks]); + + uLegalMoves = 0; for (x = ctx->sMoveStack.uBegin[ctx->uPly]; x < ctx->sMoveStack.uEnd[ctx->uPly]; x++) @@ -1217,6 +1254,19 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx, { if (iScore >= iBeta) { + // A fail-high capturing a non-pawn piece is + // search-proven evidence that piece was en + // prise -- victim belongs to the mover at + // ctx->uPly - 1, not here (see the same + // reasoning in the main Search() fail-high + // branch). Skip near mate. + if (mv.pCaptured && !IS_PAWN(mv.pCaptured) && + (iBeta < +NMATE)) + { + ASSERT(ctx->uPly > 0); + RecordEnprisePieceAtPly(ctx, ctx->uPly - 1, + mv.cTo); + } KEEP_TRACK_OF_FIRST_MOVE_FHs(uLegalMoves == 1); ASSERT(iBestScore > -NMATE); ASSERT(SanityCheckMoves(ctx, x, VERIFY_BEFORE)); |
