summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-11 08:17:43 -0700
committerScott Gasch <[email protected]>2026-09-11 08:17:43 -0700
commitddf932b885e08a47c88a66c3888f127f1c9d31c2 (patch)
treebc9e390917204645c41d2f543bd3aad13f7105cc
parent5f43bcffff123924099d061d475b2de4ed3f65e3 (diff)
CommonSearchInit: stop returning an unearned fail-high on give-up,
check draws before bailing, cache pi->iEval consistently, and delete the now-fully-inlined GetRoughEvalScore wrapper. - searchsup.c: the "too deep" bailout (MAX_PLY_PER_SEARCH-1 array ceiling or g_uHardExtendLimit, the latter now reachable much sooner given the tightened extension taper landed in the previous commit) used to unconditionally return *piBeta -- an unearned fail-high claim ("this line is at least beta") with zero evidence behind it, right at the moment the search is giving up because it ran out of room to determine anything. Now returns a fail-low (*piAlpha, the honest "couldn't prove this helps" posture every other give-up path in the engine already uses) when in check, or a real Eval() call (matching normal QSearch's own stand-pat convention) when not -- never Eval() while in check, since that's exactly what QSearchFromCheckNoStandPat's whole design exists to avoid trusting. Split the ply-limit vs. qsearch-extend-limit triggers into separate counters (u64TooDeepPlyLimit / u64TooDeepQsearchExtendLimit, now in root.c's per-search report) to see which is actually firing -- confirmed live it's essentially all the latter now, not the rare array-safety case. - searchsup.c: moved the IsDraw() check to run before the too-deep bailout, not after -- a position can be simultaneously "too deep" and genuinely drawn (repetition/50-move), and a draw is a known, exact fact about the position, not an estimate; it was strictly worse to throw that away for an alpha/Eval() approximation just because the search also happened to run out of room at that node. - search.c/searchsup.c/root.c: pi->iEval now gets cached consistently everywhere a real Eval() call is made for a node (the too-deep fallback, normal QSearch's stand-pat, and GetRoughEvalScore's former call sites) -- was previously only set in the main Search() function, leaving stale/uninitialized values in other paths. - chess.h/evalhash.c/root.c/search.c: deleted GetRoughEvalScore -- already a one-line pass-through to Eval() (fUseHash unused, kept only "for call-site compatibility" per its own header comment from an earlier cleanup that gutted its real logic) -- inlined at both call sites instead of keeping a pointless indirection layer. evalhash.c's only content was this function; deleted the file too and dropped it from GNUmakefile's OBJS. Validated: precommit_check.sh passing; overnight self-play (match_play.py --st 1) against HEAD came back ~even. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TBtwX9aPrvwiM3gh4VnnZi
-rw-r--r--src/GNUmakefile2
-rwxr-xr-xsrc/chess.h6
-rw-r--r--src/evalhash.c64
-rw-r--r--src/root.c2
-rw-r--r--src/search.c4
-rw-r--r--src/searchsup.c114
6 files changed, 79 insertions, 113 deletions
diff --git a/src/GNUmakefile b/src/GNUmakefile
index 802b568..e3ae855 100644
--- a/src/GNUmakefile
+++ b/src/GNUmakefile
@@ -182,7 +182,7 @@ FATHOM_CFLAGS = -std=gnu99 -O2 -Ifathom $(filter -m32 -m64,$(PROFILE))
# ---> .o, not .c! <---
#
OBJS = main.o root.o search.o searchsup.o draw.o dynamic.o \
- hash.o eval.o evalhash.o x86.o pawnhash.o bitboard.o \
+ hash.o eval.o x86.o pawnhash.o bitboard.o \
generate.o see.o move.o movesup.o command.o script.o \
input.o vars.o util.o unix.o gamelist.o mersenne.o \
sig.o piece.o ics.o san.o fen.o book.o bench.o board.o \
diff --git a/src/chess.h b/src/chess.h
index 430c7e5..5f9f10c 100755
--- a/src/chess.h
+++ b/src/chess.h
@@ -3791,10 +3791,4 @@ ULONG
_RecognizeKNKP(SEARCHER_THREAD_CONTEXT *ctx,
SCORE *piScore);
-SCORE
-GetRoughEvalScore(IN SEARCHER_THREAD_CONTEXT *ctx,
- IN SCORE iAlpha,
- IN SCORE iBeta,
- IN FLAG fUseHash);
-
#endif // CHESS
diff --git a/src/evalhash.c b/src/evalhash.c
deleted file mode 100644
index 4df6e23..0000000
--- a/src/evalhash.c
+++ /dev/null
@@ -1,64 +0,0 @@
-/*++
-
-Copyright (c) Scott Gasch
-
-Module Name:
-
- evalhash.c
-
-Abstract:
-
- Rough (mid-tree) eval estimation. Used to hold the eval-score
- hashing code (EVAL_HASH) too; that subsystem was cut for good
- (measured as slow as or slower than just computing eval) and is
- not coming back in this form, so this file is now just
- GetRoughEvalScore().
-
-Author:
-
- Scott Gasch ([email protected]) 11 Jul 2006
-
-Revision History:
-
---*/
-
-#include "chess.h"
-
-SCORE
-GetRoughEvalScore(IN OUT SEARCHER_THREAD_CONTEXT *ctx,
- IN SCORE iAlpha,
- IN SCORE iBeta,
- IN FLAG fUseHash)
-/*++
-
-Routine description:
-
- This is meant to be the "one place" that code looks when it needs to
- get an idea of the estimated evaluation of a position (in mid tree,
- not at the leaves... at the leaves just call Eval directly).
-
- Just calls Eval() -- its own lazy-exit machinery already is the
- cheap, calibrated estimate this function exists to provide, so
- there's no separate "rough" estimator to maintain here. Used to
- fall back to material + a stale ctx->uPositional EWMA left over
- from whatever full eval last ran anywhere in the tree, fed by an
- eval-hash probe (fUseHash); both are gone now (see the commit
- removing ctx->uPositional and EVAL_HASH), and this is the direct
- replacement.
-
-Parameters:
-
- IN OUT SEARCHER_THREAD_CONTEXT *ctx,
- IN SCORE iAlpha,
- IN SCORE iBeta,
- IN FLAG fUseHash : unused, kept for call-site compatibility
-
-Return value:
-
- SCORE
-
---*/
-{
- (void)fUseHash;
- return Eval(ctx, iAlpha, iBeta, NULL);
-}
diff --git a/src/root.c b/src/root.c
index d5780e4..702dd47 100644
--- a/src/root.c
+++ b/src/root.c
@@ -1296,7 +1296,7 @@ Return value:
#endif
uColor = ctx->sPosition.uToMove;
- g_iRootScore[uColor] = GetRoughEvalScore(ctx, iAlpha, iBeta, TRUE);
+ g_iRootScore[uColor] = Eval(ctx, iAlpha, iBeta, NULL);
g_iRootScore[FLIP(uColor)] = -g_iRootScore[uColor];
//
diff --git a/src/search.c b/src/search.c
index 55d2006..8ee2d3b 100644
--- a/src/search.c
+++ b/src/search.c
@@ -349,7 +349,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
}
// Maybe do nullmove pruning
- pi->iEval = iEval = GetRoughEvalScore(ctx, iAlpha, iBeta, FALSE);
+ pi->iEval = iEval = Eval(ctx, iAlpha, iBeta, NULL);
SCORE iImprovement = 0;
if (ctx->uPly > 1)
{
@@ -1881,7 +1881,7 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx,
}
ASSERT(!InCheck(pos, pos->uToMove));
- iEval = iBestScore = Eval(ctx, iAlpha, iBeta, &rgiPositional);
+ pi->iEval = iEval = iBestScore = Eval(ctx, iAlpha, iBeta, &rgiPositional);
// If that Eval (above) was full (i.e. not lazy) it may have set
// en prise and trapped piece indicators. Likewise, other nodes
diff --git a/src/searchsup.c b/src/searchsup.c
index b4b9d1f..41415dc 100644
--- a/src/searchsup.c
+++ b/src/searchsup.c
@@ -1008,22 +1008,82 @@ Return value:
}
//
+ // Set fInCheck in this ply info struct
+ //
+ pi->fInCheck = (IS_CHECKING_MOVE((pi-1)->mv) != 0);
+ ASSERT((pi->fInCheck &&
+ (InCheck(&ctx->sPosition, ctx->sPosition.uToMove))) ||
+ (!pi->fInCheck &&
+ (!InCheck(&ctx->sPosition, ctx->sPosition.uToMove))));
+
+ //
+ // See if this position is a draw -- checked before the too-deep
+ // bailout below on purpose: a position can be simultaneously
+ // "too deep" (hit the ply/qsearch-extend-limit ceiling) and
+ // genuinely drawn (repetition/50-move), and a draw is a known,
+ // exact fact about the position, not an estimate -- it would be
+ // strictly worse to throw that away for an alpha/Eval()
+ // approximation just because we also happened to run out of
+ // search room at the same node.
+ //
+ if (TRUE == IsDraw(ctx))
+ {
+ INC(ctx->sCounters.tree.u64LeafCount);
+ *piScore = g_iDrawScore[ctx->sPosition.uToMove];
+ if ((*piAlpha < *piScore) && (*piScore < *piBeta))
+ {
+ UpdatePV(ctx, DRAWMOVE);
+ }
+ DTTRACE("<i bold=\"draw\"/>");
+ goto end;
+ }
+
+ //
// Make sure we have not exceeded max ply; note that since this
// is checked at the beginning of search/qsearch and if we let
// the past they will call MakeMove this must cut off one ply
// before the hard limit.
//
- if (ctx->uPly >= (MAX_PLY_PER_SEARCH - 1))
- {
- INC(ctx->sCounters.tree.u64TooDeepPlyLimit);
- *piScore = *piBeta;
- DTTRACE("<toodeep/>");
- goto end;
- }
- if (ctx->sSearchFlags.uQsearchDepth >= g_uHardExtendLimit)
+ if ((ctx->uPly >= (MAX_PLY_PER_SEARCH - 1)) ||
+ (ctx->sSearchFlags.uQsearchDepth >= g_uHardExtendLimit))
{
- INC(ctx->sCounters.tree.u64TooDeepQsearchExtendLimit);
- *piScore = *piBeta;
+#ifdef PERF_COUNTERS
+ if (ctx->uPly >= (MAX_PLY_PER_SEARCH - 1))
+ {
+ INC(ctx->sCounters.tree.u64TooDeepPlyLimit);
+ }
+ else
+ {
+ INC(ctx->sCounters.tree.u64TooDeepQsearchExtendLimit);
+ }
+#endif
+
+ // Was an unconditional *piScore = *piBeta -- an unearned
+ // fail-high claim ("this line is at least beta") with zero
+ // evidence behind it, right at the moment we're giving up
+ // because we ran out of room to actually determine anything.
+ // A fail-low (return alpha) is the honest "couldn't prove
+ // this helps" posture every other give-up path in this engine
+ // uses. When NOT in check we can do better than a bare
+ // fail-low, though: fall back to a real Eval() call, the same
+ // convention normal QSearch's own stand-pat uses at its
+ // horizon. NOT when in check -- IS_CHECKING_MOVE((pi-1)->mv)
+ // is the same cheap (no board scan) check pi->fInCheck itself
+ // uses a few lines below -- Eval() on an in-check position is
+ // exactly the thing QSearchFromCheckNoStandPat's entire
+ // design exists to avoid trusting (see its header comment):
+ // static material/positional terms can't tell "checked with a
+ // totally safe escape" from "checked and actually in danger."
+ if (pi->fInCheck)
+ {
+ *piScore = *piAlpha;
+ pi->iEval = INVALID_SCORE;
+ }
+ else
+ {
+ *piScore = Eval(ctx, *piAlpha, *piBeta, NULL);
+ pi->iEval = *piScore;
+ }
DTTRACE("<toodeep/>");
goto end;
}
@@ -1038,15 +1098,6 @@ Return value:
}
//
- // Set fInCheck in this ply info struct
- //
- pi->fInCheck = (IS_CHECKING_MOVE((pi-1)->mv) != 0);
- ASSERT((pi->fInCheck &&
- (InCheck(&ctx->sPosition, ctx->sPosition.uToMove))) ||
- (!pi->fInCheck &&
- (!InCheck(&ctx->sPosition, ctx->sPosition.uToMove))));
-
- //
// This code is for a debug option where I dump paths from root
// to leaf along with extensions and evals at each node in the
// path for debugging/brainstorming purposes.
@@ -1064,21 +1115,6 @@ Return value:
#endif
//
- // See if this position is a draw
- //
- if (TRUE == IsDraw(ctx))
- {
- INC(ctx->sCounters.tree.u64LeafCount);
- *piScore = g_iDrawScore[ctx->sPosition.uToMove];
- if ((*piAlpha < *piScore) && (*piScore < *piBeta))
- {
- UpdatePV(ctx, DRAWMOVE);
- }
- DTTRACE("<i bold=\"draw\"/>");
- goto end;
- }
-
- //
// Do mate-distance pruning
//
if (TRUE == MateDistancePruningCutoff(ctx->uPly,
@@ -1314,7 +1350,7 @@ TryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
return TRUE;
}
}
-
+
// If we do nothing we are mated-in-n, therefore this is a
// dangerous position so extend. This is Bruce Moreland's idea
// originally.
@@ -1324,10 +1360,10 @@ TryNullmovePruning(SEARCHER_THREAD_CONTEXT *ctx,
{
ASSERT(SanityCheckMove(pos, mv));
ASSERT(GET_COLOR(mv.pMoved) == pos->uToMove);
- UpdateDynamicMoveOrdering(ctx,
- uNullDepth + TWO_PLY,
- mv,
- *piNullScore,
+ UpdateDynamicMoveOrdering(ctx,
+ uNullDepth + TWO_PLY,
+ mv,
+ *piNullScore,
0);
}
*pfThreat = TRUE;