summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-09 14:18:52 -0700
committerScott Gasch <[email protected]>2026-10-09 14:18:52 -0700
commitfbdcb9d76dd2444d8bc631331eaeb9435cc247f8 (patch)
tree602ab360462d6f23188ba51f962c4abb2dc6c7e5
parent73b4afb12abf7cc442831dea42b41b63a2ce07be (diff)
* Grab bag of eval.c changes.
* Don't bother not hashing when moves are pruned. * Prune moves move librerally with LMP. Inexplicably, this is slightly shallower than HEAD (despite pruning MORE). It may be because it is slightly slower. It consistently beats HEAD in match_play.py though (3x now).
-rw-r--r--src/TODO44
-rwxr-xr-xsrc/chess.h53
-rw-r--r--src/data.c74
-rw-r--r--src/eval.c298
-rw-r--r--src/generate.c91
-rw-r--r--src/hash.c90
-rw-r--r--src/main.c1
-rw-r--r--src/root.c74
-rw-r--r--src/search.c315
-rw-r--r--src/searchsup.c90
-rw-r--r--src/testsearch.c178
-rw-r--r--src/unix.c2
12 files changed, 376 insertions, 934 deletions
diff --git a/src/TODO b/src/TODO
index 48b42cf..0dbdb8d 100644
--- a/src/TODO
+++ b/src/TODO
@@ -4,8 +4,6 @@ Pawnhash size tunable from cmdline again
Think about when to avoid nullmove pruning based on checks in the line
-Think about futility pruning when a >= +NMATE
-
r2r4/p4pk1/2p3p1/1p1nPR2/5p1Q/2N5/PPPq4/1K4R1 w - - 0 0 -- check extension crazy
2026-09-17: search.c's extended futility pruning (VALUE_KNIGHT/VALUE_ROOK
@@ -26,42 +24,6 @@ whether the tiers actually need different margins here too). Not yet
built -- deferred behind self-play validation of today's iPositional/
futility-margin changes landing first.
-2026-09-18: build two eval.c lazy-margin variants and compare all three
-(current/baseline, "messy but fast", "super conservative") head to head.
-"Messy but fast": tune iResidualP2/98ByOwnArmy + iSuperLazyResidualP2/98-
-ByOwnArmy to accept ~5% exceeded rate against tests/cross_sectional_200k.ep_
-(search under each root position, not just eval in isolation -- want
-real search-tree exceeded rate, not a static-position read). "Super
-conservative": tune the same tables so the 200k set hardly ever misses.
-Then: (1) self-play match_play.py between baseline and each variant, and
-between the two variants directly; (2) full tests/ecm.ep_ run (not a
-curated subset) under each of the three binaries, same sd, compare
-solve counts. Interesting because it's a direct speed-vs-soundness
-tradeoff measurement on the exact margin mechanism this session spent a
-day mistakenly blaming for the 2026-09-17 regression (see recent git
-log/session notes) -- the real bug was searchsup.c's CommonSearchInit
-PV staleness, not the margins, so this is now safe to actually explore
-properly with a real experiment instead of blind percentile-guessing.
- Cheaper derivation than 11 rounds of guess-rebuild-rerun (see eval.c's
- guess #1-11 history comments): PersistMarginSafetyHistogramsToFile
- (calibrate marginsafety, CALIBRATE_MARGIN_SAFETY build) persists the
- raw per-bucket SWING distribution (real full-eval score minus the
- lazy/super-lazy estimate), not just a boolean exceeded count -- so
- one calibration run against the 200k pool lets a script sweep any
- candidate margin M against the persisted histogram and read "fraction
- of swings > M" directly, giving the M for ~5% exceeded and the M for
- ~0% exceeded from the SAME run. eval_tune/lazy_eval_margins/
- aggregate_marginsafety_shards.py already sums bins across shards;
- needs a small "exceeded rate at threshold M" query added alongside
- hist_percentile.py's percentile query. Caveat (documented repeatedly
- in eval.c's guess history): the recorded swing distribution is itself
- shaped by whatever margin was actually compiled in during that
- calibration run -- a different final M reshapes the search tree,
- which changes which nodes even reach the swing-recording code -- so
- this gives a very good first estimate for both variants, not a
- substitute for the real match_play/ecm validation once each variant
- is actually built.
-
2026-09-18: sharded ASSERT-fuzzing harness -- the technique that found
the CommonSearchInit PV-staleness bug (DEBUG binary + gdb, batch mode,
`script <shard>.ep_` at a shallow sd, let it run until either an
@@ -88,8 +50,7 @@ the pawn eval specifically. Maybe do eval learning on only the pawn
hashed eval terms.
2026-10-04: We need to go faster.
- - Can we get rid of rgSquares[] completely and use bitboards everywher?
- - Profile and speed hunt again?
+ - Can we get rid of rgSquares[] completely and use bitboards everywhere?
- Eliminate SEE calls?
- profile a super-lazy-always (material-only Eval() exit, forced
unconditionally, throwaway diagnostic build a la the zero-
@@ -116,4 +77,5 @@ hashed eval terms.
2026-10-04: Reign in extensions the make the tree smaller.
2026-10-04: Modify self-play harness to record eval score / PVs per
-move in the PGN output.
+move in the PGN output. Make self-play harness able to play crafty
+and then test the HEAD/candidate against crafty.
diff --git a/src/chess.h b/src/chess.h
index fc2ef85..c66dbc0 100755
--- a/src/chess.h
+++ b/src/chess.h
@@ -1202,56 +1202,6 @@ typedef struct _COUNTERS
UINT64 u64TooDeepPlyLimit;
UINT64 u64TooDeepQsearchExtendLimit;
UINT64 u64LeafCount;
- // How often a node's actual outcome contradicts its assumed
- // NODE_TYPE (chess.h) -- a real, if imperfect, sanity check on
- // the classic PVS node typing. Counted at every Search()/
- // QSearch() exit, using the window it was actually asked to
- // search (iAlpha/iBeta at that point, not whatever the initial
- // caller-supplied window was, since mate-distance pruning can
- // narrow it before this node's own outcome is known). Some
- // "surprises" (e.g. an immediate hash-table cutoff) are
- // legitimate and expected, not actual instability, so these
- // counts are a rough signal, not a bug indicator by themselves.
- // 2026-10-03: denominators for the "node-type surprises" line
- // (root.c). Incremented in CommonSearchInit right where
- // eNodeType is assigned, so they count EVERY node that got a
- // type -- which includes QSEARCH nodes, because
- // CommonSearchInit is the single funnel both Search() and
- // QSearch() pass through. That matters for reading the
- // percentages: see root.c's comment on why an "ALL node failed
- // high" rate near 100% is routine rather than alarming.
- UINT64 u64NodeTypeCount[3];
- // 2026-10-03: the qsearch half of the same accounting. QSearch's
- // copy of the surprise counters was removed so the full-width
- // rates would stop being dominated by stand-pat cutoffs -- but
- // that broke a real cross-check, because the PVS identity it
- // should satisfy spans BOTH populations:
- //
- // a CUT node failing low returns score <= alpha_C, so its
- // parent P (an ALL node) sees -score_C >= beta_P and fails
- // HIGH. P cannot have had an earlier child fail low -- it
- // would have cut off and never reached C. So #(CUT fails low)
- // and #(ALL fails high) should run ~1:1.
- //
- // With qsearch dropped the identity read 3.6:1 (25,097 ALL-FH
- // vs 7,009 CUT-FL), because an ALL node at remaining depth 1
- // takes its fail-high from a QSEARCH child failing low, and
- // that child was counted nowhere. The same artifact shows in
- // the raw counts: 36,306 ALL vs 57,813 CUT nodes -- fewer ALL
- // than CUT, impossible when every ALL node is a CUT node's
- // child.
- //
- // So count qsearch separately rather than not at all. Ignore
- // these for the full-width rates; add them in to check the
- // identity. Residual imbalance after that is real signal:
- // recognizer/EGTB fail-highs and null-move cutoffs resolve a
- // node with no child searched at all. NODE_TYPE itself is
- // untouched -- these are counter slots, not a new node type.
- UINT64 u64QNodeTypeCount[3];
- UINT64 u64QNodeSurprise[3];
- UINT64 u64PVNodeSurprised; // PV node failed high or low
- UINT64 u64AllNodeFailedHigh; // ALL node (expected fail-low) failed high
- UINT64 u64CutNodeFailedLow; // CUT node (expected fail-high) failed low
UINT64 u64TerminalPositionCount;
UINT64 u64BetaCutoffs;
UINT64 u64BetaCutoffsOnFirstMove;
@@ -3937,9 +3887,6 @@ FLAG
TestSearch(void);
FLAG
-TestSearchHashConsistency(void);
-
-FLAG
TestQSearchSanity(void);
//
diff --git a/src/data.c b/src/data.c
index 711e35c..717e0e9 100644
--- a/src/data.c
+++ b/src/data.c
@@ -46,7 +46,7 @@ MOVE EGTBMOVE = {0x55558888};
FLAG g_fIsWhiteSquare[128];
#ifdef DEBUG
-COOR
+COOR
DistanceBetweenSquares(COOR a, COOR b)
{
int i = (int)a - (int)b + 128;
@@ -75,7 +75,7 @@ DistanceBetweenSquares(COOR a, COOR b)
(((x) >= -128) && ((x) <= 127))
ULONG
-CheckVectorWithIndex(int i, ULONG uColor)
+CheckVectorWithIndex(int i, ULONG uColor)
{
ULONG u;
ASSERT(VALID_INDEX(i));
@@ -95,7 +95,7 @@ CheckVectorWithIndex(int i, ULONG uColor)
((x) == +17) || ((x) == -17))
int
-DirectionBetweenSquaresWithIndex(int i)
+DirectionBetweenSquaresWithIndex(int i)
{
int iDir;
ASSERT(VALID_INDEX(i));
@@ -107,14 +107,14 @@ DirectionBetweenSquaresWithIndex(int i)
return(iDir);
}
-int
-DirectionBetweenSquaresFromTo(COOR from, COOR to)
+int
+DirectionBetweenSquaresFromTo(COOR from, COOR to)
{
int i = (int)from - (int)to;
return DirectionBetweenSquaresWithIndex(i);
}
-int
+int
NegativeDirectionBetweenSquaresWithIndex(int i)
{
int iDir;
@@ -126,7 +126,7 @@ NegativeDirectionBetweenSquaresWithIndex(int i)
ASSERT(iDir == -g_pVectorDelta[i].iDelta);
return(iDir);
}
-
+
int
DirectionBetweenSquares(COOR cFrom, COOR cTo)
{
@@ -134,8 +134,8 @@ DirectionBetweenSquares(COOR cFrom, COOR cTo)
return(DirectionBetweenSquaresWithIndex(i));
}
-FLAG
-IsSquareWhite(COOR c)
+FLAG
+IsSquareWhite(COOR c)
{
BITBOARD bb;
FLAG f = IS_WHITE_SQUARE_COOR(c);
@@ -153,7 +153,7 @@ VerifyVectorDelta(void)
{
int iIndex;
int iChecksum = 0;
-
+
for (iIndex = 0;
iIndex < 256;
iIndex++)
@@ -162,33 +162,32 @@ VerifyVectorDelta(void)
ASSERT(VALID_VECTOR(g_VectorDelta[iIndex].iVector[WHITE]));
ASSERT(VALID_DELTA(g_VectorDelta[iIndex].iDelta));
ASSERT(VALID_DELTA(g_VectorDelta[iIndex].iNegDelta));
- ASSERT(-g_VectorDelta[iIndex].iDelta ==
+ ASSERT(-g_VectorDelta[iIndex].iDelta ==
g_VectorDelta[iIndex].iNegDelta);
iChecksum += g_VectorDelta[iIndex].iVector[BLACK] * iIndex;
iChecksum += g_VectorDelta[iIndex].iVector[WHITE] * iIndex;
iChecksum += g_VectorDelta[iIndex].iDelta * iIndex;
}
-
if (iChecksum != 0xb1b58)
{
UtilPanic(DETECTED_INCORRECT_INITIALIZATION,
NULL,
"vector/delta",
(void *)(size_t)iChecksum,
- (void *)(size_t)0xb1b58,
+ (void *)(size_t)0xb1b58,
__FILE__, __LINE__);
}
}
void
-InitializeWhiteSquaresTable(void)
+InitializeWhiteSquaresTable(void)
{
COOR c;
FOREACH_SQUARE(c)
{
g_fIsWhiteSquare[c] = FALSE;
- if (!IS_ON_BOARD(c))
+ if (!IS_ON_BOARD(c))
{
continue;
}
@@ -206,10 +205,10 @@ InitializeVectorDeltaTable(void)
PIECE p;
int iDir;
ULONG u;
- static const BYTE _LOCATIONS[] =
+ static const BYTE _LOCATIONS[] =
{
- D4, A5, A3, G3, C1, B5, H4, E5, E3, A6, C5, A5, A5,
- G5, A6, D3, D2, A5, E1, E1, A6, H4, B2, D1, D2, E5
+ D4, A5, A3, G3, C1, B5, H4, E5, E3, A6, C5, A5, A5,
+ G5, A6, D3, D2, A5, E1, E1, A6, H4, B2, D1, D2, E5
};
static int iNumDirs[7] = { 0, 0, 8, 4, 4, 8, 8 };
static int iDelta[7][8] = { { 0, 0, 0, 0, 0, 0, 0, 0 },
@@ -224,14 +223,14 @@ InitializeVectorDeltaTable(void)
FOREACH_SQUARE(cStart)
{
if (!IS_ON_BOARD(cStart)) continue;
- for (u = 0; u < ARRAY_LENGTH(_LOCATIONS); u++)
+ for (u = 0; u < ARRAY_LENGTH(_LOCATIONS); u++)
{
- if (cStart == _LOCATIONS[u])
+ if (cStart == _LOCATIONS[u])
{
iIndex = 0;
}
}
-
+
//
// Do the pawn bits
//
@@ -239,19 +238,19 @@ InitializeVectorDeltaTable(void)
g_VectorDelta[iIndex].iVector[WHITE] |= (1 << PAWN);
iIndex = (int)cStart - ((int)cStart - 15) + 128;
g_VectorDelta[iIndex].iVector[WHITE] |= (1 << PAWN);
-
+
iIndex = (int)cStart - ((int)cStart + 17) + 128;
g_VectorDelta[iIndex].iVector[BLACK] |= (1 << PAWN);
iIndex = (int)cStart - ((int)cStart + 15) + 128;
g_VectorDelta[iIndex].iVector[BLACK] |= (1 << PAWN);
-
+
//
// Do the piece bits
//
for (p = KNIGHT; p <= KING; p++)
{
- for (iDir = 0;
- iDir < iNumDirs[p];
+ for (iDir = 0;
+ iDir < iNumDirs[p];
iDir++)
{
cRay = cStart;
@@ -259,13 +258,13 @@ InitializeVectorDeltaTable(void)
while (IS_ON_BOARD(cNew))
{
iIndex = (int)cStart - (int)cNew + 128;
-
+
//
// Fill in the vector
//
g_VectorDelta[iIndex].iVector[WHITE] |= (1 << p);
g_VectorDelta[iIndex].iVector[BLACK] |= (1 << p);
-
+
//
// Fill in the delta
//
@@ -280,7 +279,7 @@ InitializeVectorDeltaTable(void)
{
g_VectorDelta[iIndex].iDelta = -16;
g_VectorDelta[iIndex].iNegDelta = 16;
- }
+ }
}
else if (RANK(cStart) == RANK(cNew))
{
@@ -322,7 +321,7 @@ InitializeVectorDeltaTable(void)
}
}
cNew += iDelta[p][iDir];
-
+
if ((KING == p) || (KNIGHT == p))
{
break;
@@ -331,7 +330,7 @@ InitializeVectorDeltaTable(void)
}
}
}
-
+
(void)VerifyVectorDelta();
#ifdef OSX
//
@@ -350,13 +349,10 @@ InitializeVectorDeltaTable(void)
#endif
}
-//
-// TODO: fix this to use attack counts also
-//
void
InitializeSwapTable(void)
{
- ULONG GET_HIGH_BIT[32] =
+ ULONG GET_HIGH_BIT[32] =
{
0, // 00000 = 0
1, // 00001 = 1
@@ -523,7 +519,7 @@ InitializeSwapTable(void)
}
void
-InitializeDistanceTable(void)
+InitializeDistanceTable(void)
{
COOR x, y;
int i;
@@ -531,10 +527,10 @@ InitializeDistanceTable(void)
int j;
#endif
- for (x = 0; x < 128; x++)
+ for (x = 0; x < 128; x++)
{
if (!IS_ON_BOARD(x)) continue;
- for (y = 0; y < 128; y++)
+ for (y = 0; y < 128; y++)
{
if (!IS_ON_BOARD(y)) continue;
i = (int)x - (int)y;
@@ -546,10 +542,10 @@ InitializeDistanceTable(void)
}
#ifdef DEBUG
- for (x = 0; x < 128; x++)
+ for (x = 0; x < 128; x++)
{
if (!IS_ON_BOARD(x)) continue;
- for (y = 0; y < 128; y++)
+ for (y = 0; y < 128; y++)
{
if (!IS_ON_BOARD(y)) continue;
i = (int)x - (int)y + 128;
diff --git a/src/eval.c b/src/eval.c
index e102daa..50a5e6f 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -95,7 +95,7 @@ static ULONG REDUCED_MATERIAL_DOWN_SCALER[32] =
0, 0, 0, 0, 0, 0, 0, 0,
// Rm 3m/Q 2R R2m 4m/Qm 2Rm R3m/QR Q2m
- 0, 1, 1, 1, 1, 1, 2, 3,
+ 0, 1, 1, 1, 2, 2, 2, 3,
// 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m
// Q2R Q2Rm
@@ -385,14 +385,13 @@ SCORE RACER_WINS_RACE = +300;
static SCORE UNDEVELOPED_MINORS_IN_OPENING[5] =
{// 0 1 2 3 4
- 0, -8, -14, -20, -23
+ 0, -14, -18, -24, -30
};
//
// Bishop eval terms
// ---------------------------------------------------------------------------
//
-static SCORE BISHOP_OVER_KNIGHT_IN_ENDGAME = +33;
//
// "The bishop pair has an average value of half a pawn (more when the
@@ -527,6 +526,7 @@ static SCORE KNIGHT_IN_CLOSED_POSITION[33] =
static SCORE ROOK_ON_FULL_OPEN = 18;
static SCORE ROOK_ON_HALF_OPEN_WITH_ENEMY = 9;
static SCORE ROOK_ON_HALF_OPEN_WITH_FRIEND = 10;
+
// Cache of the three constants above, indexed [friend pawn on file?]
// [enemy pawn on file?] so _EvalRook's hot path is a single array read
// instead of rebuilding a local array every call. Not `static const`
@@ -553,18 +553,6 @@ static SCORE ROOK_WITH_FRIEND_PASSER = 10;
static const BITBOARD BBROOK_7TH_RANK[2] = { BBRANK22, BBRANK77 }; // [BLACK], [WHITE]
static SCORE ROOK_ON_SEVENTH_BONUS = 9;
-static SCORE ROOK_BEHIND_PASSER_BY_PASSER_RANK[2][9] =
-{// 0 1 2 3 4 5 6 7 8
- { +0, +0, +25, +17, +12, +6, +1, +0, +0 }, // black
- { +0, +0, +0, +1, +6, +12, +17, +25, +0 } // white
-};
-
-static SCORE ROOK_LEADS_PASSER_BY_PASSER_RANK[2][9] =
-{// 0 1 2 3 4 5 6 7 8
- { +0, +0, -22, -16, -13, -9, -5, -3, +0 }, // black
- { +0, +0, -3, -5, -9, -13, -16, -22, +0 } // white
-};
-
static SCORE ROOK_VALUE_AS_PAWNS_COME_OFF[17] =
{// 0 1 2 3 4 5 6 7 8
+55, +51, +44, +38, +33, +27, +22, +16, +7,
@@ -598,7 +586,6 @@ static SCORE ROOK_MOBILITY_BY_SQUARES[15] =
// ---------------------------------------------------------------------------
//
-
//
// A queen can move to between 0..27 squares
// -----------------------------------------
@@ -733,7 +720,7 @@ static SCORE KING_TO_CENTER[128] =
// the stay-home bonus now uses this same threshold, so "safety scored"
// and "stay-home bonus applies" are exactly the same condition.
#define DO_KING_SAFETY_THRESHOLD \
- (VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1)
+ (VALUE_ROOK + VALUE_BISHOP + VALUE_BISHOP + VALUE_KING)
static ULONG KING_COUNTER_BY_ATTACK_PATTERN[32] =
{
@@ -798,7 +785,7 @@ static SCORE KING_SAFETY_BY_COUNTER[42] =
// another ~33% to -14/count.
static SCORE KING_QUEEN_PROXIMITY_DANGER[7] =
{// 0 1 2 3 4 5 6
- +0, -14, -28, -42, -56, -70, -84
+ +0, -10, -18, -25, -32, -39, -55
};
static SCORE KING_MISSING_ONE_CASTLE_OPTION = -23;
@@ -942,7 +929,6 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_ARRAY(PASSER_BONUS_AS_MATERIAL_COMES_OFF),
DNA_VAR(RACER_WINS_RACE),
DNA_ARRAY(UNDEVELOPED_MINORS_IN_OPENING),
- DNA_VAR(BISHOP_OVER_KNIGHT_IN_ENDGAME),
DNA_VAR(BISHOP_PAIR_BONUS),
DNA_ARRAY(STATIONARY_PAWN_ON_BISHOP_COLOR),
DNA_ARRAY(TRANSIENT_PAWN_ON_BISHOP_COLOR),
@@ -961,8 +947,6 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_VAR(ROOK_WITH_ENEMY_PASSER),
DNA_VAR(ROOK_WITH_FRIEND_PASSER),
DNA_VAR(ROOK_ON_SEVENTH_BONUS),
- DNA_MATRIX(ROOK_BEHIND_PASSER_BY_PASSER_RANK),
- DNA_MATRIX(ROOK_LEADS_PASSER_BY_PASSER_RANK),
DNA_ARRAY(ROOK_VALUE_AS_PAWNS_COME_OFF),
DNA_VAR(ROOK_CONNECTED_VERT),
DNA_VAR(ROOK_CONNECTED_HORIZ),
@@ -2773,9 +2757,19 @@ Return value:
{
+0, +6, +3, +1, 0, 0, 0, 0
};
- static SCORE KingFileDefects[3] =
+ // Indexed by (fOwn << 1) | fEnemy: 0=open (neither side has a pawn
+ // here), 1=half-open toward the enemy only (their pawn gone, ours
+ // still there), 2=half-open toward us only (ours gone, theirs still
+ // there), 3=closed (both present). Previously collapsed 1 and 2
+ // into a single "half-open" bucket (uCountPerFile[WHITE][u]>0) +
+ // (uCountPerFile[BLACK][u]>0) summed to 1 either way) -- split
+ // 2026-10-08 since the two cases aren't symmetric (an enemy pawn
+ // left behind is a real storming threat already priced by
+ // KingStormingPawnDefects below, whereas our own pawn being the
+ // only one left is comparatively mild).
+ static SCORE KingFileDefects[4] =
{
- -10, -5, 0
+ -18, -10, -5, 0
};
// Old code's edge-file (a/h) kicker was a flat +1 counter-unit;
// scaled by the same filestormopenfile-derived factor.
@@ -2796,14 +2790,11 @@ Return value:
continue;
}
u = FILE(cFileSq) + 1;
- v = (pHash->uCountPerFile[WHITE][u] > 0) +
- (pHash->uCountPerFile[BLACK][u] > 0);
- ASSERT((v >= 0) && (v <= 2));
+ v = ((pHash->uCountPerFile[uColor][u] > 0) << 1) |
+ (pHash->uCountPerFile[ufColor][u] > 0);
+ ASSERT(v <= 3);
iScore += KingFileDefects[v];
- if ((v < 2) && ((u == 1) || (u == 8)))
- {
- iScore += KING_FILE_EDGE_KICKER;
- }
+ iScore += ((v != 3) && ((u == 1) || (u == 8))) * KING_FILE_EDGE_KICKER;
bb = pos->bbPawns[ufColor] & BBFILE[u - 1];
if (bb)
{
@@ -5100,19 +5091,6 @@ Return value:
ASSERT(IS_ON_BOARD(cKing));
ASSERT(IS_KING(pos->rgSquare[cKing].pPiece));
- // Encourage enemy king tropism, but not too early.
- if ((TRUE == pos->fCastled[uColor]) || (pos->uMinorsAtHome[uColor] <= 1))
- {
- u = DISTANCE(c, cKing);
- ASSERT((u <= 7) && (u > 0));
- EVAL_TERM(uColor,
- QUEEN,
- c,
- pos->iScore[uColor],
- QUEEN_KING_TROPISM[u],
- "enemy king tropism");
- }
-
//
// Do queen mobility (and update attack tables) -- two-pass
// rook-direction/bishop-direction magic lookups, board_
@@ -5134,85 +5112,82 @@ Return value:
// one case that's a *fewer*-cases asymmetry versus bishop, not
// more), so the enemy side of each pass's continue-set is empty.
//
- {
- BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, xColor);
- BITBOARD bbEnemyGE = pos->bbPieces[xColor][QUEEN] |
- COOR_TO_BB(cKing);
- //
- // Enemy king attacks are also unsafe for the queen -- a free
- // O(1) table lookup by king square, independent of eval order
- // (unlike the enemy minor/rook attacks above, which do need
- // both colors' knight/bishop/rook passes to finish first, and
- // already have by queen's turn: see the eval call order in
- // Eval() below). Only unsafe when the queen wouldn't be
- // recaptured by something of our own first -- if a friendly
- // pawn/minor/rook already covers that square, the king can't
- // just take the queen for free. Every one of those tables (ours
- // AND the enemy's) is fully populated by now: pawns, minors,
- // and rooks all run their own eval pass before queen does.
- //
- BITBOARD bbUnsafeForQueen = pos->bbPawnAttacks[xColor] |
- pos->bbMinorAttacks[xColor] |
- pos->bbRookAttacks[xColor] |
- (g_KingAttacksBB[cKing] &
- ~(pos->bbPawnAttacks[uColor] |
- pos->bbMinorAttacks[uColor] |
- pos->bbRookAttacks[uColor]));
- BITBOARD bbAttackTotal = 0;
- BITBOARD bbXrayAccum = 0;
- BITBOARD bbMobility = 0;
- ULONG uPass;
+ BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, xColor);
+ BITBOARD bbEnemyGE = pos->bbPieces[xColor][QUEEN] |
+ COOR_TO_BB(cKing);
+ //
+ // Enemy king attacks are also unsafe for the queen -- a free
+ // O(1) table lookup by king square, independent of eval order
+ // (unlike the enemy minor/rook attacks above, which do need
+ // both colors' knight/bishop/rook passes to finish first, and
+ // already have by queen's turn: see the eval call order in
+ // Eval() below). Only unsafe when the queen wouldn't be
+ // recaptured by something of our own first -- if a friendly
+ // pawn/minor/rook already covers that square, the king can't
+ // just take the queen for free. Every one of those tables (ours
+ // AND the enemy's) is fully populated by now: pawns, minors,
+ // and rooks all run their own eval pass before queen does.
+ //
+ BITBOARD bbUnsafeForQueen = pos->bbPawnAttacks[xColor] |
+ pos->bbMinorAttacks[xColor] |
+ pos->bbRookAttacks[xColor] |
+ (g_KingAttacksBB[cKing] &
+ ~(pos->bbPawnAttacks[uColor] |
+ pos->bbMinorAttacks[uColor] |
+ pos->bbRookAttacks[uColor]));
+ BITBOARD bbAttackTotal = 0;
+ BITBOARD bbXrayAccum = 0;
+ BITBOARD bbMobility = 0;
- for (uPass = 0; uPass < 2; uPass++)
- {
- BITBOARD bbFriendContinue = (uPass == 0) ?
- (pos->bbPieces[uColor][ROOK] | pos->bbPieces[uColor][QUEEN]) :
- (pos->bbPieces[uColor][BISHOP] | pos->bbPieces[uColor][QUEEN]);
- BITBOARD bbInitial = (uPass == 0) ?
- _RookAttacksBB(c, pos->bbOccupied) :
- _BishopAttacksBB(c, pos->bbOccupied);
- BITBOARD bbExclude = 0;
- BITBOARD bbSeen = 0;
- BITBOARD bbLayer = bbInitial;
- BITBOARD bbFirstLayerMask = 0;
+ ULONG uPass;
+ for (uPass = 0; uPass < 2; uPass++)
+ {
+ BITBOARD bbFriendContinue = (uPass == 0) ?
+ (pos->bbPieces[uColor][ROOK] | pos->bbPieces[uColor][QUEEN]) :
+ (pos->bbPieces[uColor][BISHOP] | pos->bbPieces[uColor][QUEEN]);
+ BITBOARD bbInitial = (uPass == 0) ?
+ _RookAttacksBB(c, pos->bbOccupied) :
+ _BishopAttacksBB(c, pos->bbOccupied);
+ BITBOARD bbExclude = 0;
+ BITBOARD bbSeen = 0;
+ BITBOARD bbLayer = bbInitial;
+ BITBOARD bbFirstLayerMask = 0;
- bbAttackTotal |= bbInitial;
+ bbAttackTotal |= bbInitial;
- for (;;)
- {
- BITBOARD bbNew = bbLayer & ~bbSeen;
- BITBOARD bbBlockersHere = bbNew & pos->bbOccupied;
- BITBOARD bbEmptyHere = bbNew & ~pos->bbOccupied;
- BITBOARD bbFriendContinueHere = bbBlockersHere &
- bbFriendContinue;
- BITBOARD bbEnemyGEHere = bbBlockersHere & bbEnemyGE;
- BITBOARD bbEnemyLEHere = bbBlockersHere & bbEnemyOcc &
- ~bbEnemyGEHere;
+ for (;;)
+ {
+ BITBOARD bbNew = bbLayer & ~bbSeen;
+ BITBOARD bbBlockersHere = bbNew & pos->bbOccupied;
+ BITBOARD bbEmptyHere = bbNew & ~pos->bbOccupied;
+ BITBOARD bbFriendContinueHere = bbBlockersHere &
+ bbFriendContinue;
+ BITBOARD bbEnemyGEHere = bbBlockersHere & bbEnemyGE;
+ BITBOARD bbEnemyLEHere = bbBlockersHere & bbEnemyOcc &
+ ~bbEnemyGEHere;
- bbSeen |= bbNew;
- bbXrayAccum |= (bbNew & bbFirstLayerMask);
- bbFirstLayerMask = ~(BITBOARD)0;
+ bbSeen |= bbNew;
+ bbXrayAccum |= (bbNew & bbFirstLayerMask);
+ bbFirstLayerMask = ~(BITBOARD)0;
- bbMobility |= (bbEmptyHere & ~bbUnsafeForQueen) |
- bbEnemyGEHere |
- (bbEnemyLEHere & ~bbUnsafeForQueen);
+ bbMobility |= (bbEmptyHere & ~bbUnsafeForQueen) |
+ bbEnemyGEHere |
+ (bbEnemyLEHere & ~bbUnsafeForQueen);
- if (!bbFriendContinueHere)
- {
- break;
- }
- bbExclude |= bbFriendContinueHere;
- bbLayer = (uPass == 0) ?
- _RookAttacksBB(c, pos->bbOccupied & ~bbExclude) :
- _BishopAttacksBB(c, pos->bbOccupied & ~bbExclude);
+ if (!bbFriendContinueHere)
+ {
+ break;
}
+ bbExclude |= bbFriendContinueHere;
+ bbLayer = (uPass == 0) ?
+ _RookAttacksBB(c, pos->bbOccupied & ~bbExclude) :
+ _BishopAttacksBB(c, pos->bbOccupied & ~bbExclude);
}
- pos->bbQueenAttacks[uColor] |= bbAttackTotal;
- pos->bbQueenXrayAttacks[uColor] |= bbXrayAccum;
-
- uTotalMobility = FastCountBits(bbMobility);
}
+ pos->bbQueenAttacks[uColor] |= bbAttackTotal;
+ pos->bbQueenXrayAttacks[uColor] |= bbXrayAccum;
+ uTotalMobility = FastCountBits(bbMobility);
ASSERT(uTotalMobility <= 27);
EVAL_TERM(uColor,
QUEEN,
@@ -5353,6 +5328,8 @@ Return value:
(SCORE)KING_COUNTER_BY_ATTACK_PATTERN[bvPattern];
// Count how many ring squares are attacked by the enemy queen.
+ // Note: we remove queen-king tropism bonus from EvalQueen in
+ // favor of this.
uQueenNearKing = FastCountBits(
bbRing & (pos->bbQueenAttacks[xColor] | pos->bbQueenXrayAttacks[xColor])
);
@@ -5495,12 +5472,7 @@ Return value:
pDump->uFileStormStormingPawn;
#endif
- // Unconditional within this whole function now -- KEEP_KING_AT_HOME_
- // THRESHOLD (a separate, stricter bar than DO_KING_SAFETY_THRESHOLD)
- // used to gate this specifically, leaving a dead band between the
- // two. Retired; see DO_KING_SAFETY_THRESHOLD's comment.
- iSafety += KING_INITIAL_LOCATION_SCALE *
- KING_EVAL_INITIAL_LOCATION[uColor][c];
+ iSafety += KING_INITIAL_LOCATION_SCALE * KING_EVAL_INITIAL_LOCATION[uColor][c];
#ifdef KING_DEFECT_DUMP
pDump->uInitialCounterByLocation =
(ULONG)KING_EVAL_INITIAL_LOCATION[uColor][c];
@@ -5607,8 +5579,7 @@ Return value:
0,
ILLEGAL_COOR,
pos->iScore[uColor],
- (SCORE)(((uRightsRemaining > 0) *
- UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] *
+ (SCORE)((UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] *
(SCORE)uDevelopmentScaler) / 8),
"development");
@@ -5798,7 +5769,7 @@ Return value:
// this king out.
u = pos->uNonPawnMaterial[xColor];
ASSERT(u >= VALUE_KING);
- if (u >= DO_KING_SAFETY_THRESHOLD)
+ if (u > DO_KING_SAFETY_THRESHOLD)
{
iKingScore = _EvalKingSafety(pos, c, pHash, ctx, uColor, xColor, bbRing);
}
@@ -6379,7 +6350,7 @@ Return value:
// KNB vs KRP - the side with the pawn has the winning chances.
//
ULONG uAheadPawnCount = FastCountBits(pos->bbPawns[uAhead]);
- ULONG uInverseBehindPieceCount = 9 - MINU(9, TotalNonPawnCount(pos, uBehind));
+ ULONG uInverseBehindPieceCount = 11 - MINU(9, TotalNonPawnCount(pos, uBehind));
//
// "No mating material" here means the SAME lone-minor-piece test
// SideHasWinningChances (above) already uses: king + exactly one
@@ -6426,6 +6397,7 @@ Return value:
"don't trade pawns");
}
+#if 0
static void
_EvalLookForDanger(IN OUT SEARCHER_THREAD_CONTEXT *ctx)
/**
@@ -6479,6 +6451,7 @@ Return value:
}
}
}
+#endif
static void
@@ -6704,7 +6677,7 @@ Return value:
FLAG FORCEINLINE
SideHasWinningChances(IN POSITION *pos,
- IN ULONG uSide)
+ IN ULONG uColor)
/**
Routine description:
@@ -6744,20 +6717,13 @@ Return value:
**/
{
- ULONG uEnemy = FLIP(uSide);
+ ULONG xColor = FLIP(uColor);
+ ASSERT(IS_VALID_COLOR(uColor) && IS_VALID_COLOR(xColor));
INT iMajorDiff;
INT iMinorDiff;
- ULONG uSideKnights;
- ULONG uSideBishops;
-
- // uSide's bishop count is needed by the wrong-bishop-fortress check
- // immediately below (which must run BEFORE the "any pawn -> TRUE"
- // early return that follows it) and is reused again later by the
- // lone-minor/iMinorDiff/two-knights checks -- compute it once, up
- // front.
- uSideBishops = FastCountBits(pos->bbPieces[uSide][BISHOP]);
+ ULONG uColorKnights;
+ ULONG uColorBishops = FastCountBits(pos->bbPieces[uColor][BISHOP]);
- //
// Wrong-bishop rook-pawn fortress: a lone bishop + rook-pawn(s) (all
// on the A- or H-file), with the bishop unable to control the
// promotion square, is a known draw once the defending bare king
@@ -6777,43 +6743,42 @@ Return value:
// shrink), never used as a search-terminating bound the way
// recogn.c's tighter dist<=1-only version is, so a wrong FALSE here
// is tolerable in a way it would not be in recogn.c.
- //
- if ((2 == TotalNonPawnCount(pos, uSide)) &&
- (1 == uSideBishops) &&
- (1 == TotalNonPawnCount(pos, uEnemy)) &&
- (0 == FastCountBits(pos->bbPawns[uEnemy])) &&
- (FastCountBits(pos->bbPawns[uSide]) > 0))
+ if ((2 == TotalNonPawnCount(pos, uColor)) &&
+ (1 == uColorBishops) &&
+ (1 == TotalNonPawnCount(pos, xColor)) &&
+ (0 == FastCountBits(pos->bbPawns[xColor])) &&
+ (FastCountBits(pos->bbPawns[uColor]) > 0))
{
- BITBOARD bbPawns = pos->bbPawns[uSide];
+ BITBOARD bbPawns = pos->bbPawns[uColor];
COOR cPromote = ILLEGAL_COOR;
+ ULONG uColorWhiteSquareBishopCount = WhiteSqBishopCount(pos, uColor);
- if (uSide == WHITE)
+ if (uColor == WHITE)
{
- if (!(bbPawns & ~BBFILE[A]) && (WhiteSqBishopCount(pos, WHITE) == 0))
+ if (!(bbPawns & ~BBFILE[A]) && (uColorWhiteSquareBishopCount == 0))
{
cPromote = A8;
}
- else if (!(bbPawns & ~BBFILE[H]) && (WhiteSqBishopCount(pos, WHITE) == 1))
+ else if (!(bbPawns & ~BBFILE[H]) && (uColorWhiteSquareBishopCount == 1))
{
cPromote = H8;
}
}
else
{
- if (!(bbPawns & ~BBFILE[A]) && (WhiteSqBishopCount(pos, BLACK) == 1))
+ if (!(bbPawns & ~BBFILE[A]) && (uColorWhiteSquareBishopCount == 1))
{
cPromote = A1;
}
- else if (!(bbPawns & ~BBFILE[H]) && (WhiteSqBishopCount(pos, BLACK) == 0))
+ else if (!(bbPawns & ~BBFILE[H]) && (uColorWhiteSquareBishopCount == 0))
{
cPromote = H1;
}
}
-
if (IS_ON_BOARD(cPromote))
{
- COOR cWeakKing = KingPosition(pos, uEnemy);
- int iTempo = (pos->uToMove == uEnemy) ? 1 : 0;
+ COOR cWeakKing = KingPosition(pos, xColor);
+ int iTempo = (pos->uToMove == xColor) ? 1 : 0;
if (((int)DISTANCE(cWeakKing, cPromote) - iTempo) < 1)
{
return(FALSE);
@@ -6825,7 +6790,7 @@ Return value:
// A pawn always gives some winning chances (it can always try to
// queen with support) -- nothing below applies.
//
- if (FastCountBits(pos->bbPawns[uSide]) > 0)
+ if (FastCountBits(pos->bbPawns[uColor]) > 0)
{
return(TRUE);
}
@@ -6833,15 +6798,15 @@ Return value:
// uSide's knight count is read twice below (the lone-minor check
// and the two-knights check); uSideBishops was already computed
// above.
- uSideKnights = FastCountBits(pos->bbPieces[uSide][KNIGHT]);
+ uColorKnights = FastCountBits(pos->bbPieces[uColor][KNIGHT]);
//
// No pawns and the only piece besides the king is a single minor:
// never enough material to force mate (K+N or K+B vs anything is
// never a forced win on material alone).
//
- if ((2 == TotalNonPawnCount(pos, uSide)) &&
- (1 == (uSideKnights + uSideBishops)))
+ if ((2 == TotalNonPawnCount(pos, uColor)) &&
+ (1 == (uColorKnights + uColorBishops)))
{
return(FALSE);
}
@@ -6852,15 +6817,15 @@ Return value:
// force a win either -- e.g. KRB vs KR can be held by the
// defender with correct play.
//
- iMajorDiff = (INT)(FastCountBits(pos->bbPieces[uSide][ROOK]) +
- 2 * FastCountBits(pos->bbPieces[uSide][QUEEN])) -
- (INT)(FastCountBits(pos->bbPieces[uEnemy][ROOK]) +
- 2 * FastCountBits(pos->bbPieces[uEnemy][QUEEN]));
+ iMajorDiff = (INT)(FastCountBits(pos->bbPieces[uColor][ROOK]) +
+ 2 * FastCountBits(pos->bbPieces[uColor][QUEEN])) -
+ (INT)(FastCountBits(pos->bbPieces[xColor][ROOK]) +
+ 2 * FastCountBits(pos->bbPieces[xColor][QUEEN]));
if ((1 == iMajorDiff) || (-1 == iMajorDiff))
{
- iMinorDiff = (INT)(FastCountBits(pos->bbPieces[uEnemy][KNIGHT]) +
- FastCountBits(pos->bbPieces[uEnemy][BISHOP])) -
- (INT)(uSideKnights + uSideBishops);
+ iMinorDiff = (INT)(FastCountBits(pos->bbPieces[xColor][KNIGHT]) +
+ FastCountBits(pos->bbPieces[xColor][BISHOP])) -
+ (INT)(uColorKnights + uColorBishops);
if (iMajorDiff == iMinorDiff)
{
return(FALSE);
@@ -6875,16 +6840,15 @@ Return value:
// excluded, since that has one bishop, not zero) -- both are
// genuine, if sometimes technique-heavy, forced wins.
//
- if ((2 == uSideKnights) &&
- (0 == uSideBishops) &&
- (0 == FastCountBits(pos->bbPieces[uSide][ROOK])) &&
- (0 == FastCountBits(pos->bbPieces[uSide][QUEEN])) &&
- (1 == TotalNonPawnCount(pos, uEnemy)) &&
- (0 == FastCountBits(pos->bbPawns[uEnemy])))
+ if ((2 == uColorKnights) &&
+ (0 == uColorBishops) &&
+ (0 == FastCountBits(pos->bbPieces[uColor][ROOK])) &&
+ (0 == FastCountBits(pos->bbPieces[uColor][QUEEN])) &&
+ (1 == TotalNonPawnCount(pos, xColor)) &&
+ (0 == FastCountBits(pos->bbPawns[xColor])))
{
return(FALSE);
}
-
return(TRUE);
}
diff --git a/src/generate.c b/src/generate.c
index 89796b4..fdfdc79 100644
--- a/src/generate.c
+++ b/src/generate.c
@@ -4740,40 +4740,35 @@ Return value:
//
sKillers[0].mv = ctx->mvKiller[uPly][0];
sKillers[0].uBonus = FIRST_KILLER;
+ sKillers[0].uBonus |=
+ (SORT_THESE_FIRST * (IS_KILLERMATE_MOVE(sKillers[0].mv) != 0));
sKillers[1].mv = ctx->mvKiller[uPly][1];
sKillers[1].uBonus = SECOND_KILLER;
+ sKillers[1].uBonus |=
+ (SORT_THESE_FIRST * (IS_KILLERMATE_MOVE(sKillers[1].mv) != 0));
sKillers[2].mv.uMove = sKillers[3].mv.uMove = 0;
if (uPly > 1)
{
sKillers[2].mv = ctx->mvKiller[uPly - 2][0];
sKillers[2].uBonus = THIRD_KILLER;
+ sKillers[2].uBonus |=
+ (SORT_THESE_FIRST * (IS_KILLERMATE_MOVE(sKillers[2].mv) != 0));
sKillers[3].mv = ctx->mvKiller[uPly - 2][1];
sKillers[3].uBonus = FOURTH_KILLER;
- }
- sKillers[0].uBonus |=
- (SORT_THESE_FIRST * (IS_KILLERMATE_MOVE(sKillers[0].mv) != 0));
- sKillers[1].uBonus |=
- (SORT_THESE_FIRST * (IS_KILLERMATE_MOVE(sKillers[1].mv) != 0));
- sKillers[2].uBonus |=
- (SORT_THESE_FIRST * (IS_KILLERMATE_MOVE(sKillers[2].mv) != 0));
- sKillers[3].uBonus |=
- (SORT_THESE_FIRST * (IS_KILLERMATE_MOVE(sKillers[3].mv) != 0));
+ sKillers[3].uBonus |=
+ (SORT_THESE_FIRST * (IS_KILLERMATE_MOVE(sKillers[3].mv) != 0));
- // EXPERIMENT: countermove-match tier. Precompute the previous
- // move's countermove-table index once per node (mirrors killer
- // precompute above) so the per-move loop below is just an
- // IS_SAME_MOVE check, not a recompute.
- fHaveCounterMove = FALSE;
- if ((uPly > 0) && (0 != (ctx->sPlyInfo[uPly - 1]).mv.uMove))
- {
- uCounterMoveIdx = MOVE_TO_INDEX((ctx->sPlyInfo[uPly - 1]).mv);
- uCounterMoveContKey = MOVE_TO_CONT_KEY((ctx->sPlyInfo[uPly - 1]).mv);
- fHaveCounterMove = TRUE;
+ // EXPERIMENT: countermove-match tier. Precompute the previous
+ // move's countermove-table index once per node (mirrors killer
+ // precompute above) so the per-move loop below is just an
+ // IS_SAME_MOVE check, not a recompute.
+ MOVE mvLast = (ctx->sPlyInfo[uPly - 1]).mv;
+ fHaveCounterMove = (0 != mvLast.uMove);
+ uCounterMoveIdx = MOVE_TO_INDEX(mvLast);
+ uCounterMoveContKey = MOVE_TO_CONT_KEY(mvLast);
}
- //
// Score moves
- //
ASSERT(MOVE_COUNT(ctx, uPly) <= MAX_MOVES_PER_PLY);
for (u = pStack->uBegin[uPly];
u < pStack->uEnd[uPly];
@@ -4846,25 +4841,6 @@ Return value:
ASSERT(s > 0);
ASSERT((s & STRIP_OFF_FLAGS) < VALUE_KING);
}
-#ifdef DEBUG
- else
- {
- // 2026-10-04: no longer subtracts VALUE_KING here.
- // That made this capture's stored iValue differ
- // between DEBUG and release builds for no checked
- // reason (the ASSERT it fed was tautological --
- // already true by construction, this being the
- // else of `s >= 0` above) -- a silent landmine:
- // ComputeMoveScore/DecodeMoveValueEstimate's
- // MOVE_SCORING_NORMAL case trusts this value
- // directly for a losing capture (MIN0(iRawValue)),
- // so a DEBUG build fed EstimateHangingMaterialCp a
- // ~VALUE_KING-inflated "hang" estimate for an
- // ordinary losing capture, release builds never
- // saw it. The real check is the bound just above.
- ASSERT((s & STRIP_OFF_FLAGS) > -VALUE_KING);
- }
-#endif
}
//
@@ -5073,8 +5049,8 @@ Return value:
ASSERT(s >= SORT_THESE_FIRST);
ASSERT(s > 0);
ASSERT((s & STRIP_OFF_FLAGS) < VALUE_KING);
- }
- else if (s == 0)
+ }
+ else if (s == 0)
{
// Even capture -- can't be capturing w/ king
ASSERT(!IS_KING(mv.pMoved));
@@ -5083,23 +5059,6 @@ Return value:
s |= FIRST_KILLER;
ASSERT(s > 0);
}
-#ifdef DEBUG
- else
- {
- // Losing capture. No `s -= VALUE_KING` here --
- // same landmine, same fix as _ScoreAllMoves' twin
- // of this block (2026-10-04): currently harmless in
- // practice (DecodeMoveValueEstimate's
- // MOVE_SCORING_ESCAPE case recomputes a capture's
- // value straight from mv.pCaptured/mv.pMoved rather
- // than trusting iValue), but there's no reason for
- // this capture's stored iValue to differ between
- // DEBUG and release builds when nothing checks the
- // difference.
- ASSERT(!IS_KING(mv.pMoved));
- ASSERT((s & STRIP_OFF_FLAGS) > -VALUE_KING);
- }
-#endif
}
//
@@ -5117,7 +5076,7 @@ Return value:
else
{
s = g_iPSQT[mv.pMoved][mv.cTo]; // 0..1000
- if (!IS_KING(mv.pMoved))
+ if (!IS_KING(mv.pMoved))
{
// 0..2400
s += (VALUE_QUEEN - PIECE_VALUE(mv.pMoved)) * 4;
@@ -5128,21 +5087,23 @@ Return value:
s |= THIRD_KILLER;
ASSERT(s >= 0);
}
- else
+ else
{
// Losing block of check
s -= VALUE_KING;
ASSERT(s < 0);
}
- }
+ }
else
{
// If the king is fleeing, encourage a spot next
// to friendly pieces.
+ //
+ // TODO: does this matter enough for how slow it is?
v = 0;
do {
c = mv.cTo + g_iQKDeltas[v++];
- if (IS_ON_BOARD(c))
+ if (IS_ON_BOARD(c))
{
p = pos->rgSquare[c].pPiece;
s += _bonus[OPPOSITE_COLORS(pos->uToMove, p)]
@@ -5150,7 +5111,7 @@ Return value:
ASSERT(_bonus[OPPOSITE_COLORS(pos->uToMove, p)]
[PIECE_TYPE(p)] >= 0);
}
- }
+ }
while(g_iQKDeltas[v] != 0);
}
}
@@ -5264,7 +5225,6 @@ Return value:
// scoring scheme -- that check stays (it's shared with
// _ScoreQSearchMovesNoChecks, which still needs it), it just
// becomes unreachable here, which is harmless.
- {
ULONG uLiveEnd = pStack->uEnd[uPly];
u = pStack->uBegin[uPly];
while (u < uLiveEnd)
@@ -5382,7 +5342,6 @@ Return value:
} // next move
pStack->uEnd[uPly] = uLiveEnd;
- }
}
diff --git a/src/hash.c b/src/hash.c
index 887b6a1..28d85cd 100644
--- a/src/hash.c
+++ b/src/hash.c
@@ -8,20 +8,20 @@ Module Name:
Abstract:
- The main transposition table code. The table is a large range of
+ The main transposition table code. The table is a large range of
memory organized into lines (64 bytes). Each line contains four
entries that are 16 bytes each (index 0..3).
-
+
After a search, data about the position's score and the depth we
searched to determine this score can be stored in the hash table.
When information is stored a hash replacement scheme is used to
overwrite one of the four entries on a line.
-
+
Later, before embarking on a deep recursive search, we can check
- the hash table to see if information relevant to this position
+ the hash table to see if information relevant to this position
has been stored already (and can be reused). If so, we can avoid
repeating the same search and save time.
-
+
A hash entry is 16 bytes long and looks like this:
typedef struct _HASH_ENTRY
@@ -74,7 +74,7 @@ Abstract:
When screwing around with the hash code, turn on PERF_COUNTERS and
watch the hash hit rate (and usable hash hit rate). Also here is
Fine position #70 which is a great sanity check for hash code:
-
+
8/k7/3p4/p2P1p2/P2P1P2/8/8/K7/ w - - 0 0 bm 1. Ka1-b1 Ka7-b7 2. Kb1-c1
Author:
@@ -104,7 +104,7 @@ Revision History:
#define HASH_ENTRY_IS_DIRTY(x) \
(((x).bvFlags & 0xF0) != g_cDirty)
-
+
#undef ADJUST_MATE_IN_N
ULONG g_uHashTableSizeEntries = 0;
@@ -112,15 +112,15 @@ ULONG g_uHashTableSizeBytes = 0;
HASH_ENTRY *g_pHashTable = NULL;
UCHAR g_cDirty = 0;
-void
+void
DirtyHashTable(void)
/**
Routine description:
- The dirty pattern is bitwise ored onto the bvFlags part of a
+ The dirty pattern is bitwise ored onto the bvFlags part of a
hash table entry. Since the low nibble is used to hold flags
- like upper bound, lower bound, exact score, and threat the
+ like upper bound, lower bound, exact score, and threat the
dirty indicator is limited to operating in the upper nibble
of bvFlags (giving it a range of 16 values)
@@ -146,13 +146,13 @@ Return value:
}
-void
+void
CleanupHashSystem(void)
/**
Routine description:
- Cleanup the hash table by freeing the memory and zeroing some
+ Cleanup the hash table by freeing the memory and zeroing some
variables. This is called before building an opening book in
order to free some virtual address space up.
@@ -176,7 +176,7 @@ Return value:
}
}
-FLAG
+FLAG
InitializeHashSystem(void)
/**
@@ -196,11 +196,11 @@ Return value:
**/
{
ULONG uBytesToAlloc;
-
+
if (!IS_A_POWER_OF_2(g_Options.uNumHashTableEntries))
{
return(FALSE);
- }
+ }
g_uHashTableSizeEntries = g_Options.uNumHashTableEntries;
if (g_uHashTableSizeEntries != 0)
{
@@ -214,8 +214,8 @@ Return value:
}
-void
-ClearHashTable (void)
+void
+ClearHashTable (void)
/**
Routine description:
@@ -398,9 +398,9 @@ Return value:
}
-void
-StoreUpperBound(//MOVE mvBest,
- POSITION *pos,
+void
+StoreUpperBound(//MOVE mvBest,
+ POSITION *pos,
SCORE iValue,
ULONG uDepth,
FLAG fThreat)
@@ -426,10 +426,10 @@ Return value:
{
UINT64 u64Key;
HASH_ENTRY *pHash;
-
+
ASSERT(IS_VALID_SCORE(iValue));
ASSERT(IS_VALID_DEPTH(uDepth));
-
+
//
// It does not make sense to store hash positions that are worth
// at best ~INFINITY_SCORE. Every position is and this is a waste of
@@ -491,19 +491,19 @@ Return value:
}
-void
-StoreExactScore(MOVE mvBestMove,
- POSITION *pos,
- SCORE iValue,
- ULONG uDepth,
- FLAG fThreat,
+void
+StoreExactScore(MOVE mvBestMove,
+ POSITION *pos,
+ SCORE iValue,
+ ULONG uDepth,
+ FLAG fThreat,
ULONG uPly)
/**
-
+
Routine description:
Store a best move and its exact score in the hash table.
-
+
Parameters:
MOVE mvBestMove,
@@ -521,7 +521,7 @@ Return value:
{
HASH_ENTRY *pHash;
UINT64 u64Key;
-
+
ASSERT(IS_VALID_SCORE(iValue));
ASSERT(IS_VALID_DEPTH(uDepth));
if (NULL == g_pHashTable) return;
@@ -585,11 +585,11 @@ Return value:
}
-void
-StoreLowerBound(MOVE mvBestMove,
- POSITION *pos,
+void
+StoreLowerBound(MOVE mvBestMove,
+ POSITION *pos,
SCORE iValue,
- ULONG uDepth,
+ ULONG uDepth,
FLAG fThreat)
/**
@@ -621,10 +621,10 @@ Return value:
// Do not bother to hash this node if it is telling us that the
// value of this position is worth at least ~-INFINITY_SCORE. This is
// useless information and a waste of hash space.
- //
+ //
if (iValue <= -NMATE) return;
if (NULL == g_pHashTable) return;
-
+
CONVERT_SEARCH_DEPTH_TO_HASH_DEPTH(uDepth);
u64Key = pos->u64NonPawnSig ^ pos->u64PawnSig;
@@ -678,7 +678,7 @@ Return value:
}
-MOVE
+MOVE
GetPonderMove(POSITION *pos)
/**
@@ -700,7 +700,7 @@ Return value:
ULONG x;
UINT64 u64Key = pos->u64NonPawnSig ^ pos->u64PawnSig;
MOVE mv;
-
+
mv.uMove = 0;
if (NULL == g_pHashTable) return(mv);
@@ -789,7 +789,7 @@ Return value:
ULONG uMoveScore = 0;
ULONG uThisScore;
FLAG fFoundUsableBound = FALSE;
-
+
#define COMPUTE_MOVE_SCORE(mv, depth) \
((depth) * 8 + PIECE_VALUE_OVER_100((mv).pCaptured) + 1)
@@ -858,15 +858,7 @@ Return value:
//
*pfThreat |= ((sLocal.bvFlags & HASH_FLAG_THREAT) > 0);
- //
- // If we have an exact match and sufficient depth to
- // satisfy the depth to which we will later search after a
- // nullmove, see if the known value of this node is less
- // than beta. If so, doing nothing in this position will
- // probably not be better than doing something therefore
- // its not wise to perform the nullmove search. It's just
- // a waste of nodes and time.
- //
+ // See if we can advise against performing a nullmove search.
if ((sLocal.uDepth >= uNextDepth) &&
((sLocal.bvFlags & HASH_FLAG_UPPER) ||
(sLocal.bvFlags & HASH_FLAG_EXACT)) &&
diff --git a/src/main.c b/src/main.c
index 75c74c9..89d41f4 100644
--- a/src/main.c
+++ b/src/main.c
@@ -642,7 +642,6 @@ Return value:
TestMakeUnmakeMove();
TestSearch();
TestLocklessHashAlgebra();
- TestSearchHashConsistency();
TestQSearchSanity();
return(TRUE);
diff --git a/src/root.c b/src/root.c
index e9e82d4..f95bc2f 100644
--- a/src/root.c
+++ b/src/root.c
@@ -508,80 +508,6 @@ Return value:
}
Trace("\n");
}
- // 2026-10-03: percentages added, each against the count of nodes of
- // THAT TYPE (u64NodeTypeCount, incremented in CommonSearchInit),
- // not against total nodes -- dividing by total nodes understates
- // every rate by whatever share of the tree is some other type.
- //
- // 2026-10-03: this line now measures what its name says. Three
- // separate defects were fixed to get there, all of which had been
- // inflating it:
- // 1. Denominator was total nodes, not nodes of that type.
- // 2. Qsearch was included (~84% of the tree) -- a qsearch
- // stand-pat that beats beta looks like "ALL failed high" while
- // being the most ordinary event there. Now excluded from both
- // numerator (QSearch's copy of the counters was deleted) and
- // denominator (u64NodeTypeCount is incremented in Search()
- // only).
- // 3. Nodes short-circuited by CommonSearchInit reached the
- // counters without ever having been typed for that visit, and
- // were scored against a stale eNodeType -- which produced an
- // impossible "106.2% of PV nodes". Now guarded by
- // fNodeTypedThisVisit.
- // And a fourth in the fail-low test itself: it compared against the
- // IN-PLACE-RAISED iAlpha rather than iInitialAlpha, so every node
- // that successfully raised alpha was counted as a fail-low (the PV
- // row read 99.1%). See search.c's comment at the counters.
- {
- double dPV = (double)ctx->sCounters.tree.u64NodeTypeCount[NODE_PV] + 1.0;
- double dCUT = (double)ctx->sCounters.tree.u64NodeTypeCount[NODE_CUT] + 1.0;
- double dALL = (double)ctx->sCounters.tree.u64NodeTypeCount[NODE_ALL] + 1.0;
-
- Trace("Node-type surprises: PV failed high/low=%llu (%.1f%% of %.0f PV "
- "nodes), ALL failed high=%llu (%.1f%% of %.0f ALL nodes), "
- "CUT failed low=%llu (%.1f%% of %.0f CUT nodes).\n"
- " (full-width nodes only; qsearch excluded from both "
- "numerator and denominator)\n",
- ctx->sCounters.tree.u64PVNodeSurprised,
- 100.0 * (double)ctx->sCounters.tree.u64PVNodeSurprised / dPV, dPV - 1.0,
- ctx->sCounters.tree.u64AllNodeFailedHigh,
- 100.0 * (double)ctx->sCounters.tree.u64AllNodeFailedHigh / dALL, dALL - 1.0,
- ctx->sCounters.tree.u64CutNodeFailedLow,
- 100.0 * (double)ctx->sCounters.tree.u64CutNodeFailedLow / dCUT, dCUT - 1.0);
-
- // Qsearch half, plus the PVS identity cross-check it exists to
- // make possible: every CUT node that fails low forces its ALL
- // parent to fail high, and that parent cannot have had an
- // earlier child fail low (it would have cut off first), so the
- // two should run ~1:1 ACROSS BOTH POPULATIONS. Any residual is
- // ALL nodes resolved with no child searched at all --
- // recognizer/EGTB fail-highs and null-move cutoffs.
- {
- double dQPV = (double)ctx->sCounters.tree.u64QNodeTypeCount[NODE_PV] + 1.0;
- double dQCUT = (double)ctx->sCounters.tree.u64QNodeTypeCount[NODE_CUT] + 1.0;
- double dQALL = (double)ctx->sCounters.tree.u64QNodeTypeCount[NODE_ALL] + 1.0;
- UINT64 u64AllFH = ctx->sCounters.tree.u64AllNodeFailedHigh +
- ctx->sCounters.tree.u64QNodeSurprise[NODE_ALL];
- UINT64 u64CutFL = ctx->sCounters.tree.u64CutNodeFailedLow +
- ctx->sCounters.tree.u64QNodeSurprise[NODE_CUT];
-
- Trace(" qsearch half: PV=%llu (%.1f%% of %.0f), ALL fh=%llu "
- "(%.1f%% of %.0f), CUT fl=%llu (%.1f%% of %.0f)\n",
- ctx->sCounters.tree.u64QNodeSurprise[NODE_PV],
- 100.0 * (double)ctx->sCounters.tree.u64QNodeSurprise[NODE_PV] / dQPV,
- dQPV - 1.0,
- ctx->sCounters.tree.u64QNodeSurprise[NODE_ALL],
- 100.0 * (double)ctx->sCounters.tree.u64QNodeSurprise[NODE_ALL] / dQALL,
- dQALL - 1.0,
- ctx->sCounters.tree.u64QNodeSurprise[NODE_CUT],
- 100.0 * (double)ctx->sCounters.tree.u64QNodeSurprise[NODE_CUT] / dQCUT,
- dQCUT - 1.0);
- Trace(" PVS identity (should be ~1:1): CUT-fails-low=%llu vs "
- "ALL-fails-high=%llu ratio=%.2f\n",
- u64CutFL, u64AllFH,
- (double)u64AllFH / ((double)u64CutFL + 1.0));
- }
- }
#endif
if (ctx->sCounters.egtb.uProbes > 0)
{
diff --git a/src/search.c b/src/search.c
index fcf8907..0ac0037 100644
--- a/src/search.c
+++ b/src/search.c
@@ -447,10 +447,43 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
{
POSITION *pos = &ctx->sPosition;
PLY_INFO *pi = &ctx->sPlyInfo[ctx->uPly];
+#ifdef DEBUG
+ ASSERT(IS_VALID_SCORE(iAlpha));
+ ASSERT(IS_VALID_SCORE(iBeta));
+ ASSERT(iAlpha < iBeta);
+ ASSERT(ctx->uPly > 0);
+ memcpy(&pi->sPosition, pos, sizeof(POSITION));
+#endif
CUMULATIVE_SEARCH_FLAGS *pf = &ctx->sSearchFlags;
+ ASSERT(IS_VALID_FLAG(pf->fAvoidNullmove));
+ ASSERT(IS_VALID_FLAG(pf->fVerifyNullmove));
+
+ // Jump directly to Qsearch if remaining depth is low enough.
+ // This is the only place Qsearch is entered. Lowered from ONE_PLY to
+ // THREE_QUARTERS_PLY to match the check extension's new flat amount
+ // (searchsup.c's ComputeMoveExtension) -- a lone check (or short run
+ // of them) still buys exactly one extra full-width ply as before, but
+ // a long unbroken chain now pays QUARTER_PLY of real cost per check
+ // instead of extending for free. root.c compensates by trimming the
+ // same QUARTER_PLY off the per-iteration depth budget so this doesn't
+ // just add a blanket 1/4 ply to every search.
+ SCORE iBestScore;
+ if (uDepth < THREE_QUARTERS_PLY)
+ {
+ pf->fCouldStandPat[BLACK] = pf->fCouldStandPat[WHITE] = FALSE;
+ pf->uQsearchNodes = pf->uQsearchDepth = 0;
+ pf->uQsearchCheckDepth = QPLIES_OF_NON_CAPTURE_CHECKS;
+ pi->fInQsearch = TRUE;
+ iBestScore = QSearch(ctx, iAlpha, iBeta);
+ ASSERT(pf->uQsearchNodes < 20000);
+ ASSERT(pf->uQsearchDepth == 0);
+ goto end;
+ }
+ pi->fInQsearch = FALSE;
+ iBestScore = -INFINITY_SCORE;
MOVE mvLast = (pi-1)->mv;
- SCORE iBestScore = -INFINITY_SCORE;
- SCORE iInitialAlpha;
+ ASSERT((mvLast.uMove != 0) || (pf->fAvoidNullmove == TRUE));
+ SCORE iInitialAlpha = iAlpha;
SCORE iEval;
MOVE mv, mvBest, mvHash;
ULONG x = 0;
@@ -507,12 +540,8 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
// LMR-specific rather than part of the shared "unremarkable move"
// definition gate-1 describes.
FLAG fMoveIsPruneReduceEligible = FALSE;
+ ULONG uRawDepthB;
#ifdef PERF_COUNTERS
- // See the node-type counter comment below: guards the surprise
- // counters at end: so a node short-circuited by CommonSearchInit
- // (and therefore never typed for this visit) cannot be scored
- // against a stale eNodeType left by a previous node at this ply.
- FLAG fNoteTypedThisCall = FALSE;
FLAG fThisMoveIsCountermoveMatch = FALSE;
ULONG uCMEvidenceBucket = 0;
ULONG uThisMoveHangBucket = 0;
@@ -536,7 +565,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
ULONG uStage = TRY_HASH_MOVE;
ULONG u;
FLAG fTryEFPHere = FALSE;
- FLAG fAnyMovePruned = FALSE;
FLAG fThisMoveEFPPruned;
FLAG fThisMoveLMPPruned;
SCORE iMoveSee;
@@ -549,40 +577,8 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
UINT64 uNullCalibRealLoopNodeCost = 0;
UINT64 uNullCalibNodesBeforeRealLoop = 0;
#endif
-#ifdef DEBUG
- ASSERT(IS_VALID_SCORE(iAlpha));
- ASSERT(IS_VALID_SCORE(iBeta));
- ASSERT(iAlpha < iBeta);
- ASSERT(ctx->uPly > 0);
- ASSERT((mvLast.uMove != 0) || (pf->fAvoidNullmove == TRUE));
- ASSERT(IS_VALID_FLAG(pf->fAvoidNullmove));
- ASSERT(IS_VALID_FLAG(pf->fVerifyNullmove));
- memcpy(&pi->sPosition, pos, sizeof(POSITION));
-#endif
mvBest.uMove = 0;
- // Jump directly to Qsearch if remaining depth is low enough.
- // This is the only place Qsearch is entered. Lowered from ONE_PLY to
- // THREE_QUARTERS_PLY to match the check extension's new flat amount
- // (searchsup.c's ComputeMoveExtension) -- a lone check (or short run
- // of them) still buys exactly one extra full-width ply as before, but
- // a long unbroken chain now pays QUARTER_PLY of real cost per check
- // instead of extending for free. root.c compensates by trimming the
- // same QUARTER_PLY off the per-iteration depth budget so this doesn't
- // just add a blanket 1/4 ply to every search.
- if (uDepth < THREE_QUARTERS_PLY)
- {
- pf->fCouldStandPat[BLACK] = pf->fCouldStandPat[WHITE] = FALSE;
- pf->uQsearchNodes = pf->uQsearchDepth = 0;
- pf->uQsearchCheckDepth = QPLIES_OF_NON_CAPTURE_CHECKS;
- pi->fInQsearch = TRUE;
- iBestScore = QSearch(ctx, iAlpha, iBeta);
- ASSERT(pf->uQsearchNodes < 20000);
- ASSERT(pf->uQsearchDepth == 0);
- goto end;
- }
- pi->fInQsearch = FALSE;
-
// Common initialization code (which may cause a cutoff or change the
// bounds or decide that we need to stop searching now).
if (TRUE == CommonSearchInit(ctx,
@@ -593,41 +589,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
goto end;
}
-#ifdef PERF_COUNTERS
- // 2026-10-03: denominator for the node-type surprise line, counted
- // HERE rather than inside CommonSearchInit, for two reasons that
- // were both silently corrupting that metric:
- //
- // 1. FULL-WIDTH ONLY. CommonSearchInit is the single funnel both
- // Search() and QSearch() pass through, so counting there swept
- // in qsearch -- ~84% of the tree -- and a qsearch node whose
- // stand-pat already beats beta returns iBestScore >= iBeta
- // immediately. Typed ALL, that lands in "ALL failed high",
- // making the headline number mostly a measure of stand-pat
- // frequency. The question the line's name asks is about
- // full-width node-type prediction, so qsearch is excluded from
- // both numerator (below) and denominator (here).
- // 2. NEVER STALE. Every early-exit path in CommonSearchInit (hash
- // cutoff, repetition draw, ply/depth limit, EGTB hit) returns
- // TRUE *before* pi->eNodeType is assigned, and the caller's
- // `goto end` lands squarely on the surprise counters -- which
- // then scored the node against whatever type the PREVIOUS node
- // at this ply happened to leave behind. That is what produced
- // a "PV failed high/low = 106.2% of PV nodes" reading: more
- // surprises than nodes of that type, which is impossible
- // unless some were attributed to a type the node never had.
- // Reaching this line means CommonSearchInit returned FALSE,
- // i.e. the type really was assigned for THIS visit.
- //
- // NOTE: the NODE_TYPE enum and every behavioral consumer of it
- // (all of which only ever test against NODE_PV) are untouched.
- fNoteTypedThisCall = TRUE;
- ASSERT((ULONG)pi->eNodeType < 3);
- INC(ctx->sCounters.tree.u64NodeTypeCount[(ULONG)pi->eNodeType]);
-#endif
-
DTEnterNode(ctx, uDepth, FALSE, iAlpha, iBeta);
- iInitialAlpha = iAlpha;
pi->eMoveScoringScheme = MOVE_SCORING_NORMAL;
ASSERT((IS_CHECKING_MOVE(mvLast) && (TRUE == pi->fInCheck)) ||
(!IS_CHECKING_MOVE(mvLast) && (FALSE == pi->fInCheck)));
@@ -1025,6 +987,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
fTryEFPHere = TRUE;
}
uStage++;
+ uRawDepthB = DepthBucket(uDepth / ONE_PLY);
ASSERT(x == ctx->sMoveStack.uBegin[ctx->uPly]);
// fall through
@@ -1032,8 +995,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
if (x < ctx->sMoveStack.uEnd[ctx->uPly])
{
ASSERT(x >= ctx->sMoveStack.uBegin[ctx->uPly]);
-
- fIsLeftoverMove = FALSE;
#ifdef PERF_COUNTERS
// Reset every iteration, BEFORE the LMR decision
// block (below) can set it for this move -- without
@@ -1297,18 +1258,23 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
ULONG uFHAttempts = 0;
ULONG uFHPctThousandths = GetMoveFailHighPctThousandths(mv, &uFHAttempts);
+ // Our 4D prediction table, given: late, hang,
+ // remaining depth, and gap to alpha will give us a
+ // pessimistic FH% prediction for the move. Compute
+ // the buckets needed to look it up. Note: depth
+ // bucket is per-node and was already computed (once)
+ // above.
+
// The amount of material this move seems to hang.
SCORE iHangCp = EstimateHangingMaterialCp(ctx, mv, (x - 1),
bbMinorAttacksSnapshot,
bbRookAttacksSnapshot);
-
- // Our 4D prediction table, given: late, hang,
- // remaining depth, and gap to alpha will give us a
- // pessimistic FH% prediction for the move. Compute
- // the buckets needed to look it up.
ULONG uRawDumbB = HangCpBucket(iHangCp);
- ULONG uRawDepthB = DepthBucket(uDepth / ONE_PLY);
+
+ // Gap from iEval to current iAlpha bucket.
ULONG uRawGapB = GapCpBucket((SCORE)(iAlpha - iEval));
+
+ // Late move number bucket.
ULONG uRawLateB = MoveNumBucket(uLegalMoves);
// Given the above signals, the (pessimistic)
@@ -1340,12 +1306,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
uFHAttempts,
uFHPctThousandths,
uPredictedFH);
-#ifdef PERF_COUNTERS
- if (TRUE == fThisMoveLMPPruned)
- {
- INC(ctx->sCounters.tree.u64LMPPruned);
- }
-#endif
// Extended futility pruning (EFP); this is an idea
// proposed by Ernst Heinz (Scalable Search in
@@ -1376,10 +1336,10 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
(uFHPctThousandths <= EFP_FH_PRUNE_THRESHOLD_PCT_THOUSANDTHS);
fThisMoveEFPPruned =
fThisMoveEFPPruned &&
- (uPredictedFH == 0xFFFF || uPredictedFH < 100);
+ (uPredictedFH == 0xFFFF || uPredictedFH < 200);
#if 0
if (fThisMoveEFPPruned &&
- uPredictedFH != 0xFFFF && // JOINT_TABLE_SENTINEL -- fix me
+ uPredictedFH != 0xFFFF &&
uPredictedFH > 100)
{
// Which branch of EstimateHangingMaterialCp
@@ -1414,7 +1374,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
default: szHangSource = "UNKNOWN eMoveScoringScheme"; break;
}
}
-
Trace("Board _AFTER_ the move (%s):\n", MoveToIcs(mv));
DumpPosition(pos);
Trace("iEval = %d\n", iEval);
@@ -1431,12 +1390,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
#endif
}
}
-#ifdef PERF_COUNTERS
- if (TRUE == fThisMoveEFPPruned)
- {
- INC(ctx->sCounters.tree.u64EFPPruned);
- }
-#endif
// If neither LMP nor EFP fired, we are going to search below
// this move. But if the move is very late, we might reduce
@@ -1473,7 +1426,16 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
if ((TRUE == fThisMoveEFPPruned) ||
(TRUE == fThisMoveLMPPruned))
{
- fAnyMovePruned = TRUE;
+#ifdef PERF_COUNTERS
+ if (fThisMoveEFPPruned)
+ {
+ INC(ctx->sCounters.tree.u64EFPPruned);
+ }
+ else if (fThisMoveLMPPruned)
+ {
+ INC(ctx->sCounters.tree.u64LMPPruned);
+ }
+#endif
UnmakeMove(ctx, mv);
ASSERT(PositionsAreEquivalent(pos, &pi->sPosition));
continue;
@@ -2008,33 +1970,19 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
if (iAlpha != iInitialAlpha)
{
ASSERT(mvBest.uMove != 0);
- if (!IS_CAPTURE_OR_PROMOTION(mvBest))
- {
- UpdateDynamicMoveOrdering(ctx,
- uDepth,
- mvBest,
- iBestScore,
- 0);
- }
- if (FALSE == fAnyMovePruned)
- {
- StoreExactScore(mvBest,
- pos,
- iBestScore,
- uDepth,
- fThreat,
- ctx->uPly);
- }
- else
- {
- StoreLowerBound(mvBest,
- pos,
- iBestScore,
- uDepth,
- fThreat);
- }
+ UpdateDynamicMoveOrdering(ctx,
+ uDepth,
+ mvBest,
+ iBestScore,
+ 0);
+ StoreExactScore(mvBest,
+ pos,
+ iBestScore,
+ uDepth,
+ fThreat,
+ ctx->uPly);
}
- else if (FALSE == fAnyMovePruned)
+ else
{
// IDEA: "I am very well aware of the fact, that the scores
// you get back outside of the window, are not trustable at
@@ -2066,54 +2014,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
ASSERT(IS_VALID_SCORE(iAlpha));
ASSERT(IS_VALID_SCORE(iBestScore) || WE_SHOULD_STOP_SEARCHING);
ASSERT(PositionsAreEquivalent(pos, &pi->sPosition));
-#ifdef PERF_COUNTERS
- // fNodeTypedThisVisit: a node short-circuited by CommonSearchInit
- // (hash cutoff, repetition draw, ply/depth limit, EGTB hit) returns
- // here via `goto end` WITHOUT pi->eNodeType having been assigned for
- // this visit, so scoring it would attribute the outcome to whatever
- // type the previous node at this ply left behind. That is what made
- // this line report "PV failed high/low = 106.2% of PV nodes" --
- // more surprises than nodes of that type.
- if ((!WE_SHOULD_STOP_SEARCHING) && IS_VALID_SCORE(iBestScore) &&
- (TRUE == fNoteTypedThisCall))
- {
- // 2026-10-03: compare against iInitialAlpha, NOT iAlpha.
- // search.c:2048 raises iAlpha IN PLACE as moves improve the
- // score, so by the time we reach end: a node that SUCCESSFULLY
- // raised alpha has iAlpha == iBestScore -- and the old
- // (iBestScore <= iAlpha) test then read TRUE, scoring the
- // ordinary success case as a "fail low". That is why the PV row
- // reported 99.1% surprised: it was measuring "alpha was raised"
- // rather than "the node-type prediction was wrong".
- // iInitialAlpha is the node's alpha on entry (stashed at
- // search.c:626 and already used for exactly this distinction at
- // search.c:2105), which is what a fail-low must be judged
- // against.
- FLAG fFailedLow = (iBestScore <= iInitialAlpha);
- FLAG fFailedHigh = (iBestScore >= iBeta);
- switch (pi->eNodeType)
- {
- case NODE_PV:
- if (fFailedLow || fFailedHigh)
- {
- INC(ctx->sCounters.tree.u64PVNodeSurprised);
- }
- break;
- case NODE_ALL:
- if (fFailedHigh)
- {
- INC(ctx->sCounters.tree.u64AllNodeFailedHigh);
- }
- break;
- case NODE_CUT:
- if (fFailedLow)
- {
- INC(ctx->sCounters.tree.u64CutNodeFailedLow);
- }
- break;
- }
- }
-#endif
#ifdef TEST_NULL
// Commit the calibration record stashed at the diagnostic probe
// site above, now that iBestScore's fate against iBeta (this
@@ -2973,15 +2873,6 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx,
#endif
FLAG fIncludeChecks;
FLAG fOrigStandPat = ctx->sSearchFlags.fCouldStandPat[pos->uToMove];
-#ifdef PERF_COUNTERS
- // Entry alpha, for the qsearch node-type accounting at end:. QSearch
- // raises iAlpha IN PLACE (the stand-pat branch and the move loop
- // both do), so a fail-low test against the live iAlpha would score
- // every successful alpha-raise as a fail-low -- the same defect that
- // made Search()'s PV row read 99.1%.
- SCORE iQEntryAlpha = iAlpha;
- FLAG fQNodeTyped = FALSE;
-#endif
static ULONG _WhatToGen[] =
{
GENERATE_CAPTURES_PROMS,
@@ -3006,13 +2897,6 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx,
{
goto end;
}
-#ifdef PERF_COUNTERS
- // Reaching here means CommonSearchInit returned FALSE, i.e. it
- // really assigned pi->eNodeType for THIS visit.
- fQNodeTyped = TRUE;
- ASSERT((ULONG)pi->eNodeType < 3);
- INC(ctx->sCounters.tree.u64QNodeTypeCount[(ULONG)pi->eNodeType]);
-#endif
DTEnterNode(ctx, 0, TRUE, iAlpha, iBeta);
// Probe interior node recognizers. Qsearch runs at the deepest,
@@ -3349,61 +3233,6 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx,
ASSERT(PositionsAreEquivalent(pos, &pi->sPosition));
ASSERT(IS_VALID_SCORE(iBestScore) || WE_SHOULD_STOP_SEARCHING);
DTLeaveNode(ctx, TRUE, iBestScore, pi->mvBest);
-#ifdef PERF_COUNTERS
- // Qsearch half of the node-type accounting -- see chess.h's
- // u64QNodeTypeCount comment for why this is counted separately
- // rather than folded into the full-width counters (it used to be
- // folded in, which made those rates mostly a measure of stand-pat
- // frequency) and why dropping it entirely broke the CUT-FL/ALL-FH
- // identity.
- if ((!WE_SHOULD_STOP_SEARCHING) && IS_VALID_SCORE(iBestScore) &&
- (TRUE == fQNodeTyped))
- {
- FLAG fQLow = (iBestScore <= iQEntryAlpha);
- FLAG fQHigh = (iBestScore >= iBeta);
-
- switch (pi->eNodeType)
- {
- case NODE_PV:
- if (fQLow || fQHigh)
- {
- INC(ctx->sCounters.tree.u64QNodeSurprise[NODE_PV]);
- }
- break;
- case NODE_ALL:
- if (fQHigh)
- {
- INC(ctx->sCounters.tree.u64QNodeSurprise[NODE_ALL]);
- }
- break;
- case NODE_CUT:
- if (fQLow)
- {
- INC(ctx->sCounters.tree.u64QNodeSurprise[NODE_CUT]);
- }
- break;
- }
- }
-#endif
-// 2026-10-03: QSearch's copy of the node-type surprise counters was
-// MOVED to its own slots (above), not deleted. CommonSearchInit types qsearch nodes too (it is the one
-// funnel both Search() and QSearch() pass through), and qsearch is ~84%
-// of the tree -- so these counters were dominated by it. Specifically: a
-// qsearch node whose stand-pat score already beats beta returns
-// iBestScore >= iBeta immediately, and if that node happened to be typed
-// ALL it was scored as "ALL failed high". A stand-pat cutoff is the most
-// ordinary event in qsearch, not a node-type misprediction, which is why
-// the line reported ~47% of all nodes as ALL-failed-high and looked
-// absurd. The question the line's name asks is about FULL-WIDTH
-// node-type prediction, so qsearch is now excluded from both the
-// numerator (here) and the denominator (u64NodeTypeCount, incremented in
-// Search() only).
-//
-// The NODE_TYPE enum itself is deliberately unchanged -- adding a
-// NODE_QNODE value was considered and rejected, because eNodeType feeds
-// eParentType chaining in CommonSearchInit and every behavioral consumer
-// tests against NODE_PV; excluding qsearch from the instrumentation gets
-// the same signal with zero risk to any of that.
// Note: iBestScore can be +INFINITY_SCORE or -INFINITY_SCORE here even in the
// absence of a legitimate mate detected if we disallowed stand
diff --git a/src/searchsup.c b/src/searchsup.c
index 726bc9f..69c87a2 100644
--- a/src/searchsup.c
+++ b/src/searchsup.c
@@ -631,7 +631,6 @@ _FlatTableExtraPlies(IN ULONG uRemainingDepth, IN ULONG uMoveNum)
ULONG uMoveIdx = MINU(uMoveNum, LMR_TABLE_MAX_MOVES);
SCORE iTotalTicks = g_iLMRQuietReduction[uDepthIdx][uMoveIdx];
SCORE iExtraTicks = iTotalTicks - ONE_PLY;
-
if (iExtraTicks <= 0)
{
return 0;
@@ -854,10 +853,6 @@ Return value:
ASSERT(!InCheck(&ctx->sPosition, ctx->sPosition.uToMove));
ULONG uExtraPlies = 0;
- if (NULL != puExtraTier)
- {
- *puExtraTier = 0;
- }
#ifdef DISABLE_LMR
// See GNUmakefile's DISABLE_LMR comment. Bail before any signal is
// consulted so the no-LMR arm is a clean control: no reduction, and
@@ -889,37 +884,43 @@ Return value:
}
if (FALSE == fBaseEligible)
{
+ if (NULL != puExtraTier)
+ {
+ *puExtraTier = 0;
+ }
return(0);
}
// We are going to reduce it; let's see if we want to pile on extra
// reduction because we're pretty sure it's "safe".
- uExtraPlies = (fBaseEligible && (FALSE == fDynamicDanger)) ?
+ uExtraPlies = ((FALSE == fDynamicDanger)) ?
_FlatTableExtraPlies(uRemainingDepth, uLegalMoves) : 0;
// Clamp. Combine with the baseline -ONE_PLY and floor the total
// so it never leaves less than JOINT_EXTRA_MIN_REMAINING_DEPTH
// remaining -- never rely on the move loop's own unsigned-
// wraparound clamp to do this implicitly.
- iLocalExtend = iExtend + (-ONE_PLY);
+ //
+ // iExtend is guaranteed 0 here (see routine description; the sole
+ // caller asserts this itself at search.c:1416 right after a
+ // reduction comes back), so iLocalExtend is always exactly
+ // -ONE_PLY -- no need to fold a variable iExtend into it.
+ ASSERT(0 == iExtend);
+ iLocalExtend = -ONE_PLY;
if (uExtraPlies > 0)
{
- INT iMaxExtra =
- ((INT)uRemainingDepth - (INT)ONE_PLY + iLocalExtend -
- (INT)JOINT_EXTRA_MIN_REMAINING_DEPTH) /
- (INT)ONE_PLY;
- if (iMaxExtra < 0)
- {
- iMaxExtra = 0;
- }
- if ((INT)uExtraPlies > iMaxExtra)
- {
- uExtraPlies = (ULONG)iMaxExtra;
- }
- if (uExtraPlies > JOINT_EXTRA_MAX_PLIES)
- {
- uExtraPlies = JOINT_EXTRA_MAX_PLIES;
- }
+ // MAX0 branchlessly floors the numerator at 0 before the
+ // divide (it can go negative when uRemainingDepth is small),
+ // so uMaxExtra is safely non-negative going into MINU -- both
+ // macros need their inputs' high-order bit clear, true here
+ // since none of uRemainingDepth/ONE_PLY/JOINT_EXTRA_MAX_PLIES
+ // come anywhere near 2^31.
+ INT iExtraNumerator =
+ (INT)uRemainingDepth - 2 * (INT)ONE_PLY -
+ (INT)JOINT_EXTRA_MIN_REMAINING_DEPTH;
+ ULONG uMaxExtra = (ULONG)MAX0(iExtraNumerator) / (ULONG)ONE_PLY;
+ uExtraPlies = MINU(uExtraPlies, uMaxExtra);
+ uExtraPlies = MINU(uExtraPlies, (ULONG)JOINT_EXTRA_MAX_PLIES);
iLocalExtend -= (INT)(uExtraPlies * ONE_PLY);
}
if (NULL != puExtraTier)
@@ -929,23 +930,11 @@ Return value:
return(iLocalExtend);
}
-#define LMP_BASE_MOVENUM (12) // never prune before this many
- // legal moves already tried
-#define LMP_PER_PLY_MOVENUM (4) // + this many moves of extra
- // slack per remaining ply
-#define LMP_STATIC_MAX_PCT_THOUSANDTHS JOINT_EXTRA_TIER3_PCT_THOUSANDTHS // the
- // strictest static tier only
- // (~p5 of real traffic)
-#define LMP_DYNAMIC_MIN_SAMPLES (1) // well-evidenced, not a guess
-// 0.100% -- near-zero observed rate. 2026-10-03 unit unification: was
-// (1) in per-mille, i.e. the same nominal 0.1%. Same truncation caveat
-// as LMR_FH_BASELINE_PCT_THOUSANDTHS above (the old `> 1` could only
-// fire at fh/attempts >= 0.002, so the shipped gate behaved like 0.2%);
-// moot in practice since LMP is disabled at its call site
-// (search.c:1228). Note this constant and LMP_STATIC_MAX_PCT_THOUSANDTHS
-// used to be the two reasons this unit mismatch was dangerous: both
-// were literally (1), 100x apart in meaning, four lines apart in this
-// same block.
+#define LMP_BASE_MOVENUM (8) // never prune before this many
+ // legal moves already tried
+#define LMP_PER_PLY_MOVENUM (1) // + this many moves of extra
+ // slack per remaining ply
+#define LMP_DYNAMIC_MIN_SAMPLES (1) // well-evidenced, not a guess
#define LMP_DYNAMIC_MAX_PCT_THOUSANDTHS (1)
@@ -1008,17 +997,22 @@ Return value:
**/
{
- ULONG uMoveNumFloor;
-
+ static ULONG uLMPStaticMaxPctThousandths[] = {
+ 200, 200, 125, 75, 36, 12, 6, 3, 2, 1, 0
+ };
ASSERT(ctx->uPly > 0);
+ ASSERT(IS_VALID_DEPTH(uRemainingDepth));
+ ULONG uRemainingPlies = uRemainingDepth / ONE_PLY;
- // Only consider very late moves.
- uMoveNumFloor = LMP_BASE_MOVENUM +
- (LMP_PER_PLY_MOVENUM * (uRemainingDepth / ONE_PLY));
+#if 0
+ // Only consider very late moves -- removed on the rationale that
+ // the 4D FH% table already takes move lateness into account.
+ ULONG uMoveNumFloor = LMP_BASE_MOVENUM + uRemainingPlies;
if (uLegalMoves <= uMoveNumFloor)
{
return FALSE;
}
+#endif
// Dynamic signal: if there's real, well-evidenced history on this
// exact move identity and it's not essentially zero, don't
@@ -1029,12 +1023,8 @@ Return value:
return FALSE;
}
- // Static signal: must have real data AND be in the strictest
- // (safest) tier -- no data (sentinel) is a reason to NOT gamble
- // on overt pruning, unlike GetLMRReduction's more permissive
- // treatment of that case for mere reduction.
if ((JOINT_TABLE_SENTINEL == uPredictedFH) ||
- (uPredictedFH >= LMP_STATIC_MAX_PCT_THOUSANDTHS))
+ (uPredictedFH >= uLMPStaticMaxPctThousandths[MINU(uRemainingPlies, 10)]))
{
return FALSE;
}
diff --git a/src/testsearch.c b/src/testsearch.c
index 9b3129c..afd2733 100644
--- a/src/testsearch.c
+++ b/src/testsearch.c
@@ -159,12 +159,7 @@ TestSearch(void)
// cousin nodes are visited -- so two calls that visit nodes in a
// different order (which a warm vs. cold hash table, by design, makes
// them do) can legitimately reduce/prune the same move differently,
-// producing a different score with no bug involved anywhere. A full
-// clear here is what actually isolates "did the hash table change the
-// answer" (what TestSearchHashConsistency, below, means to test) from
-// "did ordinary order-dependent move-ordering heuristics change the
-// answer" (expected, unrelated to hash-table correctness, and true of
-// this engine with or without LMR).
+// producing a different score with no bug involved anywhere.
static SCORE
_FixedDepthSearchScore(SEARCHER_THREAD_CONTEXT *ctx,
POSITION *pos,
@@ -198,150 +193,33 @@ _FixedDepthSearchScore(SEARCHER_THREAD_CONTEXT *ctx,
}
-//
-// Hash-table correctness, exercised through real search rather than
-// synthetic store/probe calls -- the actual bug class this is meant
-// to catch (stale entries surviving a generation boundary, a bound
-// getting misapplied, ADJUST_MATE_IN_N misfiring on an entry that
-// shouldn't get it) only shows up when a search's outcome actually
-// depends on what it reads back out of the table, which a
-// hand-constructed store()-then-probe() unit test can't exercise --
-// see TESTING_PLAN.md's "hash correctness" section for the case for a
-// synthetic store/probe test too; this is the complementary
-// behavioral half, not a replacement for it.
-//
-// Two invariants, intended to care only about the hash table being
-// *transparent* (its presence changes performance, never the answer),
-// not about search internals (move ordering, PV, node count) changing
-// across future search work -- but see invariant 2's own note below,
-// 2026-10-04: that intent does not actually hold for invariant 2 once
-// any move-order-sensitive heuristic (this engine has several; LMR is
-// the one that exposed it) is in the search, so it is diagnostic-only
-// now, not asserted.
-//
-// 1. Searching the same position to the same depth twice in a row
-// (second time with the table already populated by the first)
-// must produce the same score both times. Still asserted: the
-// second run's hash move at every node is the SAME move the
-// first run itself discovered as best there (same position, same
-// depth), so warming the table cannot promote a DIFFERENT move
-// to be tried first and thereby shift anything's ordinal
-// position -- there is no move-order-sensitivity window for this
-// invariant the way there is for invariant 2.
-// 2. Searching to depth D2 with the table pre-populated by a
-// shallower depth-D1 search of the same position was meant to
-// produce the same score as searching directly to D2 from a
-// freshly cleared table -- catching a depth-D1 entry wrongly
-// trusted as exact at the deeper D2 probe. Downgraded to
-// diagnostic-only (logged, not asserted) 2026-10-04: proven (via
-// bisection against DISABLE_LMR, with g_HistoryCounters/
-// g_ContinuationHistory/g_FailHighs already fully cleared between
-// every search below -- see _FixedDepthSearchScore's own comment)
-// to fail even on the symmetric starting position, for a reason
-// that has nothing to do with any hash-table bug: the shallower
-// warm-up leaves a DIFFERENT move in the table as this node's
-// hash move than a cold search would pick on its own, which
-// promotes a different move to be tried first, which shifts
-// every OTHER move's ordinal position by one -- and LMR's own
-// "lateness" admission signal (MoveNumBucket(uLegalMoves),
-// feeding the joint table's uPredictedFH) depends on exactly that
-// ordinal position. Two equally-legitimate searches of the same
-// nominal depth can therefore reduce a given move in one and not
-// the other, with zero dependence on any learned/dynamic table --
-// this is a property of combining a hash table with ANY
-// move-order-sensitive pruning/reduction heuristic, not a defect
-// in this one. The genuine bug class this invariant was meant to
-// catch (a shallow bound trusted as exact at a deeper probe) is
-// real and still worth watching for by eye in the log; it is just
-// no longer something this test can assert without false
-// positives from ordinary LMR behavior.
-//
-FLAG
-TestSearchHashConsistency(void)
-{
- static const char *rgszFen[] =
- {
- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
- "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1",
- "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
- "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
- };
- const ULONG uD1 = 4;
- const ULONG uD2 = 6;
- SEARCHER_THREAD_CONTEXT *ctx;
- POSITION pos;
- SCORE iFirst, iSecond, iShallow, iDirect;
- ULONG u;
- FLAG fRet = TRUE;
-
- Trace("Testing hash table consistency through search...\n");
- ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT));
- ASSERT(ctx);
-
- for (u = 0; u < ARRAY_LENGTH(rgszFen); u++)
- {
- if (FALSE == FenToPosition(&pos, (char *)rgszFen[u]))
- {
- UtilPanic(INCONSISTENT_STATE,
- NULL, NULL, NULL, NULL,
- __FILE__, __LINE__);
- }
-
- // Invariant 1: repeat-with-warm-table stability.
- ClearHashTable();
- DirtyHashTable();
- iFirst = _FixedDepthSearchScore(ctx, &pos, uD1);
- DirtyHashTable();
- iSecond = _FixedDepthSearchScore(ctx, &pos, uD1);
- if ((INVALID_SCORE != iFirst) &&
- (INVALID_SCORE != iSecond) &&
- (iFirst != iSecond))
- {
- Trace("TestSearchHashConsistency: position %lu: same-depth "
- "rerun with warm hash table diverged (%d vs %d) -- "
- "FEN: %s\n", u, iFirst, iSecond, rgszFen[u]);
- fRet = FALSE;
- }
-
- // Invariant 2: diagnostic-only now -- see this function's
- // top-of-file comment for why a mismatch here is expected
- // (LMR's move-order-sensitive lateness signal) and does not
- // indicate a hash-table bug. Logged so a wildly outlying case
- // still catches a human's eye, but never fails the suite.
- ClearHashTable();
- DirtyHashTable();
- iShallow = _FixedDepthSearchScore(ctx, &pos, uD1);
- DirtyHashTable();
- iSecond = _FixedDepthSearchScore(ctx, &pos, uD2);
-
- ClearHashTable();
- DirtyHashTable();
- iDirect = _FixedDepthSearchScore(ctx, &pos, uD2);
-
- if ((INVALID_SCORE != iSecond) &&
- (INVALID_SCORE != iDirect) &&
- (iSecond != iDirect))
- {
- Trace("TestSearchHashConsistency: position %lu: depth-%lu "
- "search seeded by a depth-%lu warm-up (%d) disagreed "
- "with a from-scratch depth-%lu search (%d) -- FEN: "
- "%s (diagnostic only, not a failure -- see top-of-"
- "file comment)\n", u, uD2, uD1, iSecond, uD2, iDirect,
- rgszFen[u]);
- }
- (void)iShallow;
- }
-
- if (FALSE == fRet)
- {
- UtilPanic(TESTCASE_FAILURE,
- NULL, "TestSearchHashConsistency", NULL, NULL,
- __FILE__, __LINE__);
- }
- ClearHashTable();
- SystemFreeMemory(ctx);
- return(fRet);
-}
+// 2026-10-08: TestSearchHashConsistency (both its "invariants") was
+// removed. Its entire premise -- that a hash table should be provably
+// *transparent* (changes performance, never the answer) and that this
+// is checkable by comparing scores across searches that visit
+// different subsets of the tree -- does not hold in this engine and
+// cannot be patched into holding. EFP/LMR pruning decisions depend on
+// GLOBAL, LIVE-ACCUMULATING evidence tables (g_HistoryCounters,
+// g_ContinuationHistory, g_FailHighs) that update as a side effect of
+// actually VISITING a node, not as a side effect of that node's final
+// score. A hash cutoff resolves a subtree with a perfectly sound bound
+// but without visiting it -- so two searches that visit different
+// subsets of the same tree (which is the entire point of a hash table:
+// a cold run visits everything, a warm run skips subtrees a previous
+// identical run already resolved) can legitimately reach a later
+// sibling move with different accumulated evidence, prune/reduce it
+// differently, and return a different final score -- with zero
+// hash-table bug anywhere. This was already known and documented for
+// "invariant 2" (depth-D1-warmup vs depth-D2-direct, downgraded to
+// diagnostic-only 2026-10-04); what was missed is that the identical
+// mechanism applies just as much to "invariant 1" (same-depth
+// back-to-back rerun), which was still asserted and panicking the
+// self-test suite on nothing but this structural, expected effect. A
+// real hash-table correctness test needs a synthetic store/probe unit
+// test (see TESTING_PLAN.md's "hash correctness" section) that can
+// control exactly what the table returns, not a real search compared
+// against another real search -- any two of which are free to visit
+// different nodes by design.
// Null-move soundness is intentionally NOT tested here as a C unit
diff --git a/src/unix.c b/src/unix.c
index f400ebb..fdfce5f 100644
--- a/src/unix.c
+++ b/src/unix.c
@@ -1071,7 +1071,7 @@ Return value:
}
g_fSigintAbortPending = 1;
}
- Trace("SIGINT/HUP -- STOP SEARCHING NOW");
+ Trace("SIGINT/SIGHUP -- STOP SEARCHING NOW\n");
g_MoveTimer.bvFlags |= TIMER_STOPPING;
}