summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-10 10:29:48 -0700
committerScott Gasch <[email protected]>2026-10-10 10:29:48 -0700
commit03545c4600df81b1a47dbc63fc606c065e857d8b (patch)
treea433fdce69fd27359679ad91e0ebedd52e2506cd
parentfbdcb9d76dd2444d8bc631331eaeb9435cc247f8 (diff)
Eval mobility/pseudo-mobility tweaks; drop unproductive move-ordering experiments.
eval.c: add bishop/rook x-ray pseudo-mobility terms (squares seen past a directly-attacked higher-value piece), trim pawn-centrality edge penalty and bishop-pair bonus. chess.h/dynamic.c/generate.c/search.c/searchsup.c: revert three move-ordering experiments (recapture bonus, flee-to-safety GOOD_MOVE promotion, ply-2 move-pair bonus) that calibrated with real fail-high signal but never converted into a measurable node-count or solve-rate win on the curated suites -- see MOVE_ORDERING_IDEAS.md for the evidence and reasoning behind each drop. Add MOVE_ORDERING_EVIDENCE_COLLECTION as its own GNUmakefile flag, independent of PERF_COUNTERS, for any future such harness. command.c: remove the KING_DEFECT_DUMP-gated evalkingdump command, dead since the king-safety remine work moved to eval_tune/king_safety_data/. Co-Authored-By: Claude Sonnet 5 <[email protected]>
-rw-r--r--src/GNUmakefile12
-rwxr-xr-xsrc/chess.h95
-rw-r--r--src/command.c110
-rw-r--r--src/dynamic.c108
-rw-r--r--src/eval.c890
-rw-r--r--src/generate.c38
-rw-r--r--src/search.c37
-rw-r--r--src/searchsup.c14
-rw-r--r--tests/ECM.md6
9 files changed, 525 insertions, 785 deletions
diff --git a/src/GNUmakefile b/src/GNUmakefile
index 8a4904b..5879d31 100644
--- a/src/GNUmakefile
+++ b/src/GNUmakefile
@@ -6,6 +6,13 @@
# EVAL_DUMP=1: make a version that can dump eval breakdowns
# EVAL_TIME=1: make a version that counts cycles spent in eval
# PERF_COUNTERS=1: make version with perf counters enabled
+# MOVE_ORDERING_EVIDENCE_COLLECTION=1: instrument the move-ordering
+# evidence-bucket harnesses (countermove/move-pair hit tracking,
+# CM/MP evidence-bucket tries/FH -- see MOVE_ORDERING_IDEAS.md) --
+# a dedicated flag, independent of PERF_COUNTERS, since this
+# collection (per-move IS_SAME_MOVE checks + bucket lookups on every
+# quiet move scored) is its own cost, not bundled into the general
+# perf-counter set by default.
# BOUNDS_CHECKING=1: make version with bounds checking enabled
# MP=1: make version for multi-processor machine
# DUMP_TREE=1: internal tree dumping version
@@ -136,7 +143,7 @@ ifndef DISABLE_BITBOARD_MOVEGEN
endif
ifdef EVERYTHING
-PROFILE += -DEVAL_DUMP -DEVAL_TIME -DPERF_COUNTERS -DMP -DSMP -DTEST_NULL -DDUMP_TREE -fbounds-checking
+PROFILE += -DEVAL_DUMP -DEVAL_TIME -DPERF_COUNTERS -DMP -DSMP -DTEST_NULL -DDUMP_TREE -fbounds-checking -DMOVE_ORDERING_EVIDENCE_COLLECTION
else
ifdef EVAL_DUMP
PROFILE += -DEVAL_DUMP
@@ -147,6 +154,9 @@ endif
ifdef PERF_COUNTERS
PROFILE += -DPERF_COUNTERS
endif
+ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
+PROFILE += -DMOVE_ORDERING_EVIDENCE_COLLECTION
+endif
ifdef BOUNDS_CHECKING
PROFILE += -fbounds-checking
endif
diff --git a/src/chess.h b/src/chess.h
index c66dbc0..7e37245 100755
--- a/src/chess.h
+++ b/src/chess.h
@@ -2163,6 +2163,18 @@ _assert(CHAR *szFile, ULONG uLine);
#define INC(x)
#endif
+// Dedicated gate for the move-ordering evidence-bucket harnesses
+// (countermove/move-pair hit tracking, CM/MP evidence buckets -- see
+// MOVE_ORDERING_IDEAS.md) -- deliberately a separate flag/macro from
+// INC/PERF_COUNTERS above, not nested under it, so this collection's
+// own cost (per-move IS_SAME_MOVE checks + bucket lookups) can be
+// toggled independently of the general perf-counter set.
+#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
+#define MOI_INC(x) ((x) += 1)
+#else
+#define MOI_INC(x)
+#endif
+
#define BREAKPOINT SystemDebugBreakpoint()
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
@@ -2910,6 +2922,27 @@ TestMakeUnmakeMove(void);
// moving it, don't just nudge it up.
#define COUNTERMOVE_TIER_BONUS (0x03000000)
+// DROPPED (MOVE_ORDERING_IDEAS.md item 4, recapture bonus): a capture
+// landing on the opponent's last move's destination square showed a
+// real, well-calibrated FH% signal at SEE >= 0 (64-65% vs. a 49-52%
+// all-captures-at-the-same-SEE-sign baseline, consistent across all
+// three curated suites, large samples -- MOVE_ORDERING_EVIDENCE_
+// COLLECTION's RECAPTURE_BUCKETS/u64RecaptureTries/FH vs.
+// u64CaptureBaselineTries/FH, both removed along with this). Two
+// magnitudes tried as a same-tier nudge in generate.c's
+// _ScoreAllMoves (s >= 0 capture branch), neither converted into a
+// tree-size win: 50 (over 3x the ~16-unit-wide MVV/LVA spread this
+// branch actually sorts by -- PIECE_VALUE_OVER_100(captured) -
+// PIECE_VALUE_OVER_100(mover), ranges ~112-128 across every capture
+// combination -- a near-dominant override, not a tiebreak) caused a
+// real, confirmed tactical loss (ECM.735, see MOVE_ORDERING_IDEAS.md);
+// 4 (inside that band) fixed the regression but still produced no
+// node-count improvement on any of the three curated suites. Same
+// shape as the move-pair backlog entry: a real FH% edge that
+// SelectBestWithHistory/MVV/LVA's existing ordering apparently already
+// captures most of, with no mechanism tried converting the residual
+// signal into a leaner tree.
+//
// Evidence threshold gating the promotion above -- the same
// history+continuation sum already used to score ordinary leftovers,
// evaluated for the specific countermove-matched move. Calibration
@@ -2920,14 +2953,45 @@ TestMakeUnmakeMove(void);
// three curated suites, the strongest defensible bar found.
#define COUNTERMOVE_EVIDENCE_THRESHOLD (10000)
-// EXPERIMENT: flee-to-safety same-tier nudge -- a flat bonus added at
-// selection time (movesup.c's SelectBestWithHistory) for a quiet
-// leftover move whose origin square is currently en prise, alongside
-// history/continuation. Unlike the retired GOOD_MOVE-tier promotion,
-// this never changes a move's classification -- it only nudges its
-// rank among other leftovers. Swept empirically via the zero-selection-
-// budget diagnostic, same methodology as CONTINUATION_SCALE.
-#define FLEE_BONUS (100000)
+// DROPPED (MOVE_ORDERING_IDEAS.md item 6, flee-to-safety revival): a
+// quiet move whose origin matched the cached en-prise hint
+// (FindEnprisePiece, dynamic.c), evidence-gated with the exact same
+// formula/threshold as COUNTERMOVE_TIER_BONUS/COUNTERMOVE_EVIDENCE_
+// THRESHOLD above. Calibration found a real signal (evidence >= 10000
+// hit 56-68% FH vs. a ~1.35-1.54% unprivileged-leftover baseline,
+// comparable to or exceeding countermove's own bar) -- but the
+// qualifying population was too rare (2,444-9,209 tries across an
+// entire suite run, vs. countermove's tens-to-hundreds-of-thousands)
+// to register any measurable sd10 node-count or solve-count effect
+// either way once implemented as a GOOD_MOVE-tier escape. Not a
+// magnitude-tuning problem like move-pair/recapture -- there simply
+// isn't enough volume for any mechanism acting on this signal to move
+// the aggregate needle. u64FleeTries/FH and the scoring/precompute
+// code were fully reverted.
+//
+// DROPPED (MOVE_ORDERING_IDEAS.md item 3, ply-2 move-pair bonus): three
+// mechanisms tried against a real, strongly-calibrated signal
+// (butterfly-history-gated match against the move two plies back hit
+// 53-66% FH in its top evidence band vs. ~1.2-1.4% unprivileged-
+// leftover baseline, on large samples across all three curated
+// suites) -- a GOOD_MOVE-tier escape (+1.4-1.6% nodes at sd10), a
+// dominant same-tier nudge at FLEE_BONUS's old magnitude (+2-20%
+// nodes, worse), and a scaled-down proportionate nudge (mixed,
+// net flat-to-negative, worst specifically on the smallest/noisiest
+// curated suite). No mechanism converted the signal into a tree-size
+// win; SelectBestWithHistory's existing PSQT+history+continuation
+// scoring apparently already captures most of what it offers. The
+// mvMovePair table and its evidence-bucket harness were removed along
+// with it -- re-derive from git history if this is ever revisited
+// with a different approach.
+//
+// FLEE_BONUS itself (flee-to-safety same-tier nudge, a quiet leftover
+// move whose origin square is currently en prise) is separately dead
+// code in movesup.c's SelectBestWithHistory (commented out) -- retired
+// after the GOOD_MOVE-tier version it replaced was shown, by evidence-
+// calibration, to perform at the plain-leftover baseline for the vast
+// majority of its (huge) zero-evidence population. Revive either only
+// if re-measured and shown to actually help.
// EXPERIMENT: is history+continuation evidence actually predictive of
// a countermove match's FH%? Bucket every tried countermove-matched
@@ -2953,10 +3017,10 @@ GenerateMoves(SEARCHER_THREAD_CONTEXT *ctx,
// testgenerate.c's correctness/speed harness can call it directly
// regardless of whether GENERATE_KNIGHT_BITBOARD is defined for this
// build.
-BITBOARD
+BITBOARD FORCEINLINE
_BuildFriendlySideBB(POSITION *pos, ULONG uSide);
-BITBOARD
+BITBOARD FORCEINLINE
_BuildFullOccupiedBB(POSITION *pos);
// board_representation/MOVEGEN_MIGRATION.md section 6b -- exposed so
@@ -4563,6 +4627,17 @@ TestBitboards(void);
//
// dynamic.c
//
+// EXPERIMENT: flat (depth-independent) saturating history update, after
+// Crafty's history.c -- *pu += (HISTORY_MAX - *pu) >> HISTORY_UPDATE_SHIFT
+// on fail-high, *pu -= *pu >> HISTORY_UPDATE_SHIFT on a miss. Replaces the
+// old (depth+1)^2-scaled growth (which biases toward root-near nodes,
+// where depth is large, over the far more numerous near-tip nodes where
+// it's small) with a flat update whose magnitude never depends on depth
+// at all. Self-bounding by construction (asymptotically approaches
+// HISTORY_MAX, never exceeds it), so the old overflow-triggered global
+// >>=4 rescale is no longer needed for this table.
+#define HISTORY_MAX (8192)
+#define HISTORY_UPDATE_SHIFT (5)
extern ULONG g_HistoryCounters[14][128];
// Continuation history: same growth/decay math as g_HistoryCounters
diff --git a/src/command.c b/src/command.c
index 1902784..b30de2b 100644
--- a/src/command.c
+++ b/src/command.c
@@ -547,108 +547,6 @@ Return value:
}
-#ifdef KING_DEFECT_DUMP
-COMMAND(EvalKingDumpCommand)
-/**
-
-Routine description:
-
- This function implements the 'evalkingdump' engine command, only
- built under KING_DEFECT_DUMP. Runs a full static eval (same
- persistent-context pattern as EvalCommand) then prints one
- space-separated key=value line per side with _EvalKing's raw,
- per-defect breakdown (chess.h's KING_DEFECT_DUMP_INFO) instead of
- the single collapsed score -- feeds an offline regression against
- real mate outcomes. See CLAUDE.md's king-safety discussion.
-
- Usage:
-
- evalkingdump
-
-Parameters:
-
- The COMMAND macro hides four arguments from the input parser:
-
- CHAR *szInput : the full line of input
- ULONG argc : number of argument chunks
- CHAR *argv[] : array of ptrs to each argument chunk
- POSITION *pos : a POSITION pointer to operate on
-
-Return value:
-
- void
-
-**/
-{
- static SEARCHER_THREAD_CONTEXT *ctx = NULL;
- static ULONG uLastSeenDnaGeneration = (ULONG)-1;
- static const CHAR *szPieceName[KING_RING_NUM_PIECE_TYPES] =
- { "pawn", "minor", "rook", "queen" };
- ULONG uColor;
- ULONG uSlot, uPieceType;
- KING_DEFECT_DUMP_INFO *p;
-
- if (NULL == ctx)
- {
- ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT));
- if (NULL == ctx)
- {
- Trace("Out of memory.\n");
- return;
- }
- InitializeSearcherContext(pos, ctx);
- uLastSeenDnaGeneration = g_uDnaGeneration;
- }
- else if (uLastSeenDnaGeneration != g_uDnaGeneration)
- {
- InitializeSearcherContext(pos, ctx);
- uLastSeenDnaGeneration = g_uDnaGeneration;
- }
- else
- {
- ReInitializeSearcherContext(pos, ctx);
- }
- (void)Eval(ctx, -INFINITY_SCORE, +INFINITY_SCORE, NULL);
-
- for (uColor = 0; uColor < 2; uColor++)
- {
- p = &ctx->sKingDefectDump[uColor];
- Trace("KINGDEFECTDUMP color=%s", COLOR_NAME(uColor));
- for (uSlot = 0; uSlot < KING_RING_NUM_SLOTS; uSlot++)
- {
- if (!p->fOnBoard[uSlot])
- {
- Trace(" sq%u_offboard=1", uSlot);
- continue;
- }
- for (uPieceType = 0; uPieceType < KING_RING_NUM_PIECE_TYPES; uPieceType++)
- {
- Trace(" sq%u_atk_%s=%u",
- uSlot, szPieceName[uPieceType],
- p->fAttackedByPieceType[uSlot][uPieceType]);
- }
- Trace(" sq%u_undef=%u sq%u_def=%u sq%u_xray=%u sq%u_flight=%u",
- uSlot, p->fAttackedUndefended[uSlot],
- uSlot, p->fAttackedDefended[uSlot],
- uSlot, p->fXrayedUnattacked[uSlot],
- uSlot, p->fEmptyFlight[uSlot]);
- }
- Trace(" filestorm=%u filestormopenfile=%u filestormpawn=%u "
- "queenproximity=%u initloc=%u countertotal=%u "
- "safetyvalue=%u attackerarmyscaler=%u castled=%u minorsathome=%u "
- "misscastleshort=%u misscastlelong=%u realsafety=%d\n",
- p->uFileStormDefects, p->uFileStormOpenFile, p->uFileStormStormingPawn,
- p->uQueenNearKing,
- p->uInitialCounterByLocation, p->uCounterTotal,
- p->uSafetyTableValue, p->uAttackerArmyScaler,
- p->fCastled, p->uMinorsAtHome,
- p->fMissingCastleShort, p->fMissingCastleLong,
- p->iRealSafety);
- }
-}
-#endif
-
-
COMMAND(QSearchCommand)
/**
@@ -2579,14 +2477,6 @@ COMMAND_PARSER_ENTRY g_ParserTable[] =
FALSE,
FALSE,
"Run a static eval" },
-#ifdef KING_DEFECT_DUMP
- { "evalkingdump",
- EvalKingDumpCommand,
- FALSE,
- FALSE,
- FALSE,
- "Dump _EvalKing's raw per-defect breakdown for both sides" },
-#endif
{ "evaldna",
EvalDnaCommand,
FALSE,
diff --git a/src/dynamic.c b/src/dynamic.c
index b0bad91..a56cf29 100644
--- a/src/dynamic.c
+++ b/src/dynamic.c
@@ -389,6 +389,7 @@ Return value:
}
u = MOVE_TO_INDEX(mvLast);
+#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
//
// Hit-rate instrumentation: did the counter-move slot for this
// previous move already predict the move that just won here?
@@ -397,13 +398,14 @@ Return value:
if ((ctx->mvCounter[u][0].uMove != 0) ||
(ctx->mvCounter[u][1].uMove != 0))
{
- INC(ctx->sCounters.tree.u64CounterMoveTries);
+ MOI_INC(ctx->sCounters.tree.u64CounterMoveTries);
if (IS_SAME_MOVE(mv, ctx->mvCounter[u][0]) ||
IS_SAME_MOVE(mv, ctx->mvCounter[u][1]))
{
- INC(ctx->sCounters.tree.u64CounterMoveHits);
+ MOI_INC(ctx->sCounters.tree.u64CounterMoveHits);
}
}
+#endif
// A/B test: write-gating by depth (reject shallower overwrites)
// measured *worse* hit rate with no EBF gain -- this table is
@@ -686,37 +688,18 @@ Return value:
**/
{
- ULONG x, y;
- ULONG uVal;
ULONG *pu;
ASSERT(!IS_CAPTURE_OR_PROMOTION(mv));
- uVal = uDepth / ONE_PLY;
- ASSERT(uVal >= 0);
- ASSERT(uVal <= MAX_PLY_PER_SEARCH);
- uVal += 1;
- uVal *= uVal;
- ASSERT(uVal > 0);
-
- pu = &(g_HistoryCounters[mv.pMoved][mv.cTo]);
- LOCK_DYN;
- *pu += uVal;
//
- // Make sure that the history weight doesn't get large enough to
- // affect the move flags or else our move ordering algorithm is
- // screwed.
+ // EXPERIMENT: flat/saturating update (see HISTORY_MAX/
+ // HISTORY_UPDATE_SHIFT comment, chess.h) -- depth-independent,
+ // asymptotically approaches HISTORY_MAX, never exceeds it.
//
- while (*pu & ~STRIP_OFF_FLAGS)
- {
- for (x = 0; x <= WHITE_KING; x++)
- {
- FOREACH_SQUARE(y)
- {
- g_HistoryCounters[x][y] >>= 4;
- }
- }
- }
+ pu = &(g_HistoryCounters[mv.pMoved][mv.cTo]);
+ LOCK_DYN;
+ *pu += (HISTORY_MAX - *pu) >> HISTORY_UPDATE_SHIFT;
#ifdef MP
//
@@ -734,14 +717,16 @@ Return value:
UNLOCK_DYN;
}
-static void
-_DecrementMoveHistoryCounter(MOVE mv,
+static void
+_DecrementMoveHistoryCounter(MOVE mv,
ULONG uDepth)
/**
-
+
Routine description:
Decrease a move's history counter in the global history table.
+ EXPERIMENT: flat/saturating decay, depth-independent -- see
+ HISTORY_MAX/HISTORY_UPDATE_SHIFT comment in chess.h.
Parameters:
@@ -754,27 +739,14 @@ Return value:
**/
{
- ULONG uVal;
ULONG *pu;
ASSERT(!IS_CAPTURE_OR_PROMOTION(mv));
- uVal = uDepth / ONE_PLY;
- ASSERT(uVal >= 0);
- ASSERT(uVal <= MAX_PLY_PER_SEARCH);
- uVal /= 4;
- uVal += 1;
- ASSERT(uVal > 0);
+ ASSERT(uDepth <= MAX_PLY_PER_SEARCH * ONE_PLY); // param kept for signature parity w/ increment
pu = &(g_HistoryCounters[mv.pMoved][mv.cTo]);
LOCK_DYN;
- if (*pu >= uVal)
- {
- *pu -= uVal;
- }
- else
- {
- *pu = 0;
- }
+ *pu -= (*pu >> HISTORY_UPDATE_SHIFT);
_RecordMoveFailure(mv);
UNLOCK_DYN;
}
@@ -787,9 +759,9 @@ _IncrementContinuationCounter(MOVE mvPrev, MOVE mv, ULONG uRemainingDepth)
Routine description:
Continuation-history counterpart to _IncrementMoveHistoryCounter --
- same (depth+1)^2 growth, same overflow-driven rescale, but keyed by
- (mvPrev, mv) instead of mv alone. No-op if there was no previous
- move (root).
+ EXPERIMENT: same flat/saturating depth-independent update (see
+ HISTORY_MAX/HISTORY_UPDATE_SHIFT, chess.h), keyed by (mvPrev, mv)
+ instead of mv alone. No-op if there was no previous move (root).
Parameters:
@@ -803,33 +775,19 @@ Return value:
**/
{
- ULONG uVal;
ULONG *pu;
- ULONG x;
ASSERT(!IS_CAPTURE_OR_PROMOTION(mv));
if (0 == mvPrev.uMove)
{
return;
}
- uVal = uRemainingDepth / ONE_PLY;
- ASSERT(uVal >= 0);
- ASSERT(uVal <= MAX_PLY_PER_SEARCH);
- uVal += 1;
- uVal *= uVal;
- ASSERT(uVal > 0);
+ ASSERT(uRemainingDepth <= MAX_PLY_PER_SEARCH * ONE_PLY); // unused by design now
pu = &(g_ContinuationHistory[(MOVE_TO_CONT_KEY(mvPrev) * CONT_KEY_RANGE) +
MOVE_TO_CONT_KEY(mv)]);
LOCK_DYN;
- *pu += uVal;
- while (*pu & ~STRIP_OFF_FLAGS)
- {
- for (x = 0; x < CONTINUATION_TABLE_SIZE; x++)
- {
- g_ContinuationHistory[x] >>= 4;
- }
- }
+ *pu += (HISTORY_MAX - *pu) >> HISTORY_UPDATE_SHIFT;
UNLOCK_DYN;
}
@@ -840,8 +798,9 @@ _DecrementContinuationCounter(MOVE mvPrev, MOVE mv, ULONG uRemainingDepth)
Routine description:
- Continuation-history counterpart to _DecrementMoveHistoryCounter.
- No-op if there was no previous move (root).
+ Continuation-history counterpart to _DecrementMoveHistoryCounter --
+ EXPERIMENT: flat/saturating decay, depth-independent. No-op if there
+ was no previous move (root).
Parameters:
@@ -855,7 +814,6 @@ Return value:
**/
{
- ULONG uVal;
ULONG *pu;
ASSERT(!IS_CAPTURE_OR_PROMOTION(mv));
@@ -863,24 +821,12 @@ Return value:
{
return;
}
- uVal = uRemainingDepth / ONE_PLY;
- ASSERT(uVal >= 0);
- ASSERT(uVal <= MAX_PLY_PER_SEARCH);
- uVal /= 4;
- uVal += 1;
- ASSERT(uVal > 0);
+ ASSERT(uRemainingDepth <= MAX_PLY_PER_SEARCH * ONE_PLY); // unused by design now
pu = &(g_ContinuationHistory[(MOVE_TO_CONT_KEY(mvPrev) * CONT_KEY_RANGE) +
MOVE_TO_CONT_KEY(mv)]);
LOCK_DYN;
- if (*pu >= uVal)
- {
- *pu -= uVal;
- }
- else
- {
- *pu = 0;
- }
+ *pu -= (*pu >> HISTORY_UPDATE_SHIFT);
UNLOCK_DYN;
}
diff --git a/src/eval.c b/src/eval.c
index 50a5e6f..a909953 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -192,13 +192,13 @@ COOR JUMPING_RANK[2] = { 7, 2 };
static SCORE PAWN_CENTRALITY_BONUS[128] =
{
0, 0, 0, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,
- -8, 0, 0, 0, 0, 0, 0, -8, 0,0,0,0,0,0,0,0,
- -8, 0, 5, 5, 5, 5, 0, -8, 0,0,0,0,0,0,0,0,
- -8, 0, 5, 8, 8, 5, 0, -8, 0,0,0,0,0,0,0,0,
+ -5, 0, 0, 0, 0, 0, 0, -5, 0,0,0,0,0,0,0,0,
+ -5, 0, 5, 5, 5, 5, 0, -5, 0,0,0,0,0,0,0,0,
+ -5, 0, 5, 8, 8, 5, 0, -5, 0,0,0,0,0,0,0,0,
// -------------------------------------------------------------------
- -8, 0, 5, 8, 8, 5, 0, -8, 0,0,0,0,0,0,0,0,
- -8, 0, 5, 5, 5, 5, 0, -8, 0,0,0,0,0,0,0,0,
- -8, 0, 0, 0, 0, 0, 0, -8, 0,0,0,0,0,0,0,0,
+ -5, 0, 5, 8, 8, 5, 0, -5, 0,0,0,0,0,0,0,0,
+ -5, 0, 5, 5, 5, 5, 0, -5, 0,0,0,0,0,0,0,0,
+ -5, 0, 0, 0, 0, 0, 0, -5, 0,0,0,0,0,0,0,0,
0, 0, 0, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0
};
@@ -412,7 +412,7 @@ static SCORE UNDEVELOPED_MINORS_IN_OPENING[5] =
// pieces on the board except two minors each."
// --Larry Kaufman, IM
// "Evaluation of Material Imbalance"
-static SCORE BISHOP_PAIR_BONUS = 40;
+static SCORE BISHOP_PAIR_BONUS = 30;
static SCORE STATIONARY_PAWN_ON_BISHOP_COLOR[128] =
{
@@ -449,6 +449,22 @@ static SCORE BISHOP_MOBILITY_BY_SQUARES[14] =
};// ^ |
+//
+// Pseudo-mobility: squares this bishop can "see" past a directly-attacked
+// higher-value enemy piece (rook/queen/king) -- real positional pressure
+// (the squares a capture or a moving-away enemy piece would open up), not
+// real mobility (the bishop cannot actually move there this ply). No
+// attempt to model whether the bishop itself is defended -- that's a
+// search-shaped question (SEE/exchange-accurate, whose-move-is-it), not
+// an eval one; see MOBILITY_BITBOARD_REWRITE.md. Deliberately smaller
+// than BISHOP_MOBILITY_BY_SQUARES throughout.
+//
+static SCORE BISHOP_XRAY_MOBILITY_BY_SQUARES[9] =
+{// 0 1 2 3 4 5 6 7 8
+ +0, +1, +1, +2, +2, +3, +3, +4, +4
+};
+
+
static SCORE BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING[9] =
{// 0 1 2 3 4 5 6 7 8
+0, +33, +28, +18, +8, +4, +0, -10, -16
@@ -582,6 +598,19 @@ static SCORE ROOK_MOBILITY_BY_SQUARES[15] =
//
+// Pseudo-mobility: squares this rook can "see" past a directly-attacked
+// enemy queen or king -- same idea as BISHOP_XRAY_MOBILITY_BY_SQUARES
+// above, same caveats (no defended/undefended modeling -- that's
+// search's job). Deliberately smaller than ROOK_MOBILITY_BY_SQUARES
+// throughout.
+//
+static SCORE ROOK_XRAY_MOBILITY_BY_SQUARES[9] =
+{// 0 1 2 3 4 5 6 7 8
+ +0, +1, +1, +2, +2, +3, +3, +4, +4
+};
+
+
+//
// Queen eval terms
// ---------------------------------------------------------------------------
//
@@ -933,6 +962,7 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_ARRAY(STATIONARY_PAWN_ON_BISHOP_COLOR),
DNA_ARRAY(TRANSIENT_PAWN_ON_BISHOP_COLOR),
DNA_ARRAY(BISHOP_MOBILITY_BY_SQUARES),
+ DNA_ARRAY(BISHOP_XRAY_MOBILITY_BY_SQUARES),
DNA_ARRAY(BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING),
DNA_ARRAY(KNIGHT_CENTRALITY_BONUS),
DNA_ARRAY(KNIGHT_KING_TROPISM_BONUS),
@@ -951,6 +981,7 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_VAR(ROOK_CONNECTED_VERT),
DNA_VAR(ROOK_CONNECTED_HORIZ),
DNA_ARRAY(ROOK_MOBILITY_BY_SQUARES),
+ DNA_ARRAY(ROOK_XRAY_MOBILITY_BY_SQUARES),
DNA_ARRAY(QUEEN_MOBILITY_BY_SQUARES),
DNA_ARRAY(QUEEN_KING_TROPISM),
DNA_MATRIX(KING_INITIAL_COUNTER_BY_LOCATION),
@@ -2833,88 +2864,6 @@ Return value:
}
-#ifdef KING_DEFECT_DUMP
-static void
-_ComputeFileStormDefectsSplit(IN POSITION *pos,
- IN const PAWN_HASH_ENTRY *pHash,
- IN ULONG uColor,
- OUT ULONG *puOpenFile,
- OUT ULONG *puStormingPawn)
-/**
-
-Routine description:
-
- Debug-only sibling of _ComputeFileStormDefects that reports its two
- independent sub-ideas (open/half-open file bonus vs. storming-pawn
- distance penalty) separately instead of pre-summed, so a regression
- can weigh them independently -- they already live in two distinct
- hand-picked tables (KingFileDefects / KingStormingPawnDefects) with
- no structural change needed to retune them individually once this
- tells us their relative importance. See CLAUDE.md's king-safety
- discussion.
-
- 2026-09-15: DELIBERATELY DIVERGED from _ComputeFileStormDefects as
- of the linear-SCORE rewrite (king_safety_data/HANDOFF.md's
- 2026-09-15 addendum) -- the real path now returns signed centipawns
- off a differently-shaped table, but this dump sibling must keep
- reporting the OLD unsigned counter-style values so
- king_features_v4.csv-shaped mining data stays interpretable/
- comparable across sessions (extract_king_features.py, this project's
- whole regression pipeline, assumes these units). Traversal shape
- (own file + two adjacent, same v/distance logic) still matches --
- only the two tables' actual values are now intentionally frozen at
- their pre-rewrite originals, not kept in sync.
-
-**/
-{
- static ULONG KingStormingPawnDefects[8] =
- {
- +0, +7, +5, +2, +0, +0, +0, +0
- };
- static ULONG KingFileDefects[3] =
- {
- +4, +2, +0
- };
- ULONG ufColor = FLIP(uColor);
- COOR c = KingPosition(pos, uColor);
- COOR cFileSq, cSquare;
- ULONG u, v;
- BITBOARD bb;
- int w;
-
- *puOpenFile = 0;
- *puStormingPawn = 0;
- for (w = -1; w <= 1; w++)
- {
- cFileSq = c + w;
- if (!IS_ON_BOARD(cFileSq))
- {
- continue;
- }
- u = FILE(cFileSq) + 1;
- v = (pHash->uCountPerFile[WHITE][u] > 0) +
- (pHash->uCountPerFile[BLACK][u] > 0);
- ASSERT((v >= 0) && (v <= 2));
- *puOpenFile += (KingFileDefects[v] + ((v < 2) && ((u == 1) || (u == 8))));
- bb = pos->bbPawns[ufColor] & BBFILE[u - 1];
- if (bb)
- {
- if (uColor == WHITE)
- {
- cSquare = CoorFromBitBoardRank1ToRank8(&bb);
- }
- else
- {
- cSquare = CoorFromBitBoardRank8ToRank1(&bb);
- }
- ASSERT(IS_ON_BOARD(cSquare));
- *puStormingPawn += KingStormingPawnDefects[DISTANCE(cSquare, c)];
- }
- }
-}
-#endif
-
-
static SCORE
_GetFileStormDefects(IN SEARCHER_THREAD_CONTEXT *ctx,
IN POSITION *pos,
@@ -4377,7 +4326,8 @@ Return value:
static void
_EvalBishop(IN OUT POSITION *pos,
IN COOR c,
- IN PAWN_HASH_ENTRY *pHash)
+ IN PAWN_HASH_ENTRY *pHash,
+ IN BITBOARD bbEnemyRQK)
/**
Routine description:
@@ -4387,6 +4337,10 @@ Parameters:
POSITION *pos,
COOR c,
PAWN_HASH_ENTRY *pHash,
+ BITBOARD bbEnemyRQK -- enemy rook|queen|king, precomputed once per
+ color by Eval()'s driver instead of rebuilt by every bishop of
+ that color (see MOBILITY_BITBOARD_REWRITE.md) -- depends only on
+ static piece locations, no eval-ordering dependency.
Return value:
@@ -4444,98 +4398,79 @@ Return value:
pHash->bbStationaryPawns[BLACK]) & bbMask;
//
- // Bishop mobility (and update attack tables) -- board_representation/
- // EVAL.md section 1b: _BishopAttacksBB(c, pos->bbOccupied)
- // (generate.c's magic-bitboard slider lookup, already used by move
- // generation) gives the whole ray-to-first-blocker attack set in
- // one shot -- no per-square delta walk, no mailbox read, no switch
- // dispatch. The old BMobCaseTable's seven cases collapse to:
- // - terminal enemy non-pawn (opposing minor/rook/queen/king):
- // counts unconditionally -- covers both BMOB_ENEMY_SAME (no
- // x-ray) and BMOB_ENEMY_GREATER (x-rays past too); the
- // x-ray/no-x-ray distinction only matters for attack-bit
- // population below, not the mobility count itself.
- // - empty or terminal enemy pawn: counts unless pawn-unsafe
- // (BMOB_EMPTY / BMOB_ENEMY_PAWN) -- both terminal-square
- // categories together are exactly "not friend-occupied and not
- // enemy-non-pawn-occupied," the same partition knight's
- // reduction already uses.
+ // Bishop mobility (and update attack tables) -- single magic-bitboard
+ // probe, no chain walk (see MOBILITY_BITBOARD_REWRITE.md). The old
+ // seven-case BMobCaseTable collapses to pure mask arithmetic over the
+ // one-shot first-blocker attack set:
+ // - terminal enemy non-pawn (minor/rook/queen/king): counts
+ // unconditionally -- this is a capture, always "real" mobility.
+ // - empty or terminal enemy pawn: counts unless pawn-unsafe -- both
+ // terminal-square categories together are exactly "not
+ // friend-occupied and not enemy-non-pawn-occupied," the same
+ // partition knight's reduction uses.
// - terminal friendly, non-stationary pawn on this bishop's own
- // color complex (BMOB_FRIEND_PAWN): still counts, the one case
- // that isn't a pure occupancy mask -- bbPc (already computed
- // above for the good/bad transient-pawn scoring) is exactly
- // "non-stationary pawns of either color on this color complex";
- // ANDing with pos->bbPawns[uColor] and the attack set isolates
- // just this bishop's own transient pawns.
- // - any other friendly piece (knight/rook/king): blocks, no
- // count, no x-ray -- simply excluded by the friendly-occupied
- // mask, nothing else needed.
+ // color complex: still counts -- bbPc (already computed above for
+ // the good/bad transient-pawn scoring) is exactly "non-stationary
+ // pawns of either color on this color complex."
+ // - any other friendly piece (knight/rook/king): blocks, no count.
+ //
+ // Deliberately NOT replicated: mobility/connectivity credit for
+ // squares beyond a friendly bishop/queen or enemy rook/queen/king --
+ // a bishop cannot physically move past a piece it would capture in
+ // one ply, so that was never "real mobility" to begin with (dropping
+ // it is a correctness fix, not just a simplification). See
+ // MOBILITY_BITBOARD_REWRITE.md for the king-ring x-ray replacement,
+ // which is the one piece of information this loses that's worth
+ // deliberately replacing below.
//
BITBOARD bbAttack = _BishopAttacksBB(c, pos->bbOccupied);
BITBOARD bbEnemySame = pos->bbPieces[xColor][BISHOP] |
pos->bbPieces[xColor][KNIGHT];
- BITBOARD bbEnemyGEContinue = pos->bbPieces[xColor][ROOK] |
- pos->bbPieces[xColor][QUEEN] |
- COOR_TO_BB(KingPosition(pos, xColor));
- BITBOARD bbFriendBQ = pos->bbPieces[uColor][BISHOP] |
- pos->bbPieces[uColor][QUEEN];
BITBOARD bbUnsafeForMinor = pos->bbPawnAttacks[xColor];
- BITBOARD bbExclude = 0;
- BITBOARD bbSeen = 0;
- BITBOARD bbLayer = bbAttack;
- BITBOARD bbMobility = 0;
- BITBOARD bbXrayAccum = 0;
- BITBOARD bbFirstLayerMask = 0;
+ BITBOARD bbEnemyPawnHere = bbAttack & pos->bbPawns[xColor];
+ BITBOARD bbTransientHere = bbAttack & pos->bbPawns[uColor] & bbPc;
+ BITBOARD bbEnemyGEHere = bbAttack & bbEnemyRQK;
+ BITBOARD bbMobility;
+ BITBOARD bbXray;
pos->bbMinorAttacks[uColor] |= bbAttack;
+ bbMobility = (bbAttack & ~pos->bbOccupied & ~bbUnsafeForMinor) |
+ (bbAttack & bbEnemySame) |
+ bbEnemyGEHere |
+ (bbEnemyPawnHere & ~bbUnsafeForMinor) |
+ bbTransientHere;
+
//
- // Unified chain walk over both mobility and x-ray population
- // in one pass -- same technique and same reason as rook's
- // identical rewrite (board_representation/EVAL.md section 9):
- // the old mailbox walk's BMOB_FRIEND_XRAY/BMOB_ENEMY_GREATER
- // cases don't just x-ray past their blocker for attack-bit
- // purposes, they keep walking (fStop=FALSE) and keep crediting
- // mobility for whatever lies beyond. The originally-committed
- // bitboard version (and its single-hop-then-chain-following
- // x-ray fix) only ever handled the near side for *mobility*,
- // an under-count caught the same way rook's was: a two-bishop
- // battery on one diagonal with open squares beyond (old
- // mailbox: 12/8 mobility for the two bishops; near-side-only
- // bitboard version: 11/5).
+ // King-ring x-ray, replacing the old chain's attack-bit population:
+ // only pay for an extra magic probe when this bishop already
+ // directly attacks an enemy rook/queen/king square (bbEnemyGEHere,
+ // already computed above for mobility -- free to test). Every bit
+ // in bbEnemyGEHere is, by construction, the terminal blocker of a
+ // *different* diagonal ray, so masking all of them off at once and
+ // re-probing in a single magic lookup gives the exact same union a
+ // per-blocker loop would (removing one ray's blocker can't affect a
+ // different ray's result) -- no need to isolate and remove them one
+ // at a time. No deeper chaining needed either way, since the only
+ // consumer (_EvalKingSafety) only ever ANDs this against the king's
+ // ring, never walks further.
//
- for (;;)
- {
- BITBOARD bbNew = bbLayer & ~bbSeen;
- BITBOARD bbBlockersHere = bbNew & pos->bbOccupied;
- BITBOARD bbEmptyHere = bbNew & ~pos->bbOccupied;
- BITBOARD bbFriendBQHere = bbBlockersHere & bbFriendBQ;
- BITBOARD bbEnemySameHere = bbBlockersHere & bbEnemySame;
- BITBOARD bbEnemyGEHere = bbBlockersHere & bbEnemyGEContinue;
- BITBOARD bbFriendPawnHere = bbBlockersHere & pos->bbPawns[uColor];
- BITBOARD bbEnemyPawnHere = bbBlockersHere &
- pos->bbPawns[xColor];
- BITBOARD bbTransientHere = bbFriendPawnHere & bbPc;
- BITBOARD bbContinueHere;
+ bbXray = bbEnemyGEHere ?
+ _BishopAttacksBB(c, pos->bbOccupied & ~bbEnemyGEHere) : 0;
+ pos->bbMinorXrayAttacks[uColor] |= bbXray;
- bbSeen |= bbNew;
- bbXrayAccum |= (bbNew & bbFirstLayerMask);
- bbFirstLayerMask = ~(BITBOARD)0;
-
- bbMobility |= (bbEmptyHere & ~bbUnsafeForMinor) |
- bbEnemySameHere | bbEnemyGEHere |
- (bbEnemyPawnHere & ~bbUnsafeForMinor) |
- bbTransientHere;
-
- bbContinueHere = bbFriendBQHere | bbEnemyGEHere;
- if (!bbContinueHere)
- {
- break;
- }
- bbExclude |= bbContinueHere;
- bbLayer = _BishopAttacksBB(c, pos->bbOccupied & ~bbExclude);
- }
- pos->bbMinorXrayAttacks[uColor] |= bbXrayAccum;
+ // Pseudo-mobility bonus for squares seen past the higher-value
+ // blocker -- see BISHOP_XRAY_MOBILITY_BY_SQUARES's comment. bbXray
+ // already includes the blocker's own square (redundant with
+ // bbAttack, which already counts it as a capture), so subtracting
+ // bbAttack leaves exactly the net-new squares revealed behind it.
+ EVAL_TERM(uColor,
+ BISHOP,
+ c,
+ pos->iScore[uColor],
+ BISHOP_XRAY_MOBILITY_BY_SQUARES[
+ MINU(FastCountBits(bbXray & ~bbAttack), 8)],
+ "xray pseudo-mobility");
uTotalMobility = FastCountBits(bbMobility);
@@ -4845,7 +4780,9 @@ Return value:
static void
_EvalRook(IN OUT POSITION *pos,
IN COOR c,
- IN PAWN_HASH_ENTRY *pHash)
+ IN PAWN_HASH_ENTRY *pHash,
+ IN BITBOARD bbEnemyQK,
+ IN BITBOARD bbUnsafeForRook)
/**
Routine description:
@@ -4855,6 +4792,12 @@ Parameters:
IN POSITION *pos,
IN COOR c,
IN PAWN_HASH_ENTRY *pHash,
+ IN BITBOARD bbEnemyQK -- enemy queen|king, precomputed once per color
+ by Eval()'s driver (see MOBILITY_BITBOARD_REWRITE.md) -- depends
+ only on static piece locations.
+ IN BITBOARD bbUnsafeForRook -- enemy pawn|minor attacks, precomputed
+ once per color right before the rook loops start, valid because
+ both colors' knight+bishop passes are already complete by then.
Return value:
@@ -4911,125 +4854,88 @@ Return value:
(FastCountBits(pos->bbPawns[uColor]) + FastCountBits(pos->bbPawns[xColor])));
//
- // Rook mobility (and update attack tables) -- board_representation/
- // EVAL.md section 1b/9: _RookAttacksBB(c, pos->bbOccupied)
- // (generate.c's magic-bitboard slider lookup) gives the whole
- // ray-to-first-blocker attack set in one shot, same technique
- // already landed for knight/bishop. The old RMobCaseTable's six
- // live cases collapse to:
- // - terminal enemy rook/queen/king (RMOB_ENEMY_SAME/_GREATER):
- // counts unconditionally, no unsafe check -- capturing an
- // equal-or-higher piece is never "unsafe" in the mobility
- // sense. Queen/king additionally x-ray past (_GREATER); rook
- // does not (_SAME).
- // - empty or terminal enemy pawn/knight/bishop (RMOB_EMPTY /
- // RMOB_ENEMY_LESS): counts unless the square is attacked by an
- // enemy pawn or minor -- both terminal-square categories
- // together are exactly "not friend-occupied and not
- // enemy-rook/queen/king-occupied," the same partition
- // bishop/knight's reductions already use with their own value
- // thresholds.
- // - any friendly piece (RMOB_FRIEND_BLOCK/_ROOK/_QUEEN): never
- // counts, simply excluded by the friendly-occupied mask.
- // Friendly rook/queen additionally x-ray past (no mobility
- // credit at the blocker itself, matching the old table); a
- // friendly rook blocker also earns the connected-rook bonus.
+ // Rook mobility (and update attack tables) -- single magic-bitboard
+ // probe, no chain walk (see MOBILITY_BITBOARD_REWRITE.md). The old
+ // RMobCaseTable's six cases collapse to mask arithmetic over the
+ // one-shot first-blocker attack set:
+ // - terminal enemy rook/queen/king: counts unconditionally -- a
+ // capture is always "real" mobility regardless of value.
+ // - empty or terminal enemy pawn/knight/bishop: counts unless the
+ // square is attacked by an enemy pawn or minor -- both
+ // terminal-square categories together are exactly "not
+ // friend-occupied and not enemy-rook/queen/king-occupied," the
+ // same partition bishop/knight's reductions use.
+ // - any friendly piece: blocks, no count. A friendly rook
+ // specifically also earns the connected-rook bonus (direct
+ // attack only now, not chained through a further blocker).
+ //
+ // Deliberately NOT replicated: mobility/connectivity credit for
+ // squares beyond a friendly rook/queen or enemy queen/king -- a
+ // rook cannot physically move past a piece it would capture in one
+ // ply, so that was never "real mobility" to begin with (dropping it
+ // is a correctness fix, not just a simplification). See
+ // MOBILITY_BITBOARD_REWRITE.md for the king-ring x-ray replacement,
+ // which is the one piece of information this loses that's worth
+ // deliberately replacing below.
//
-
BITBOARD bbAttack = _RookAttacksBB(c, pos->bbOccupied);
BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, xColor);
BITBOARD bbEnemySame = pos->bbPieces[xColor][ROOK];
- BITBOARD bbEnemyGEContinue = pos->bbPieces[xColor][QUEEN] |
- COOR_TO_BB(KingPosition(pos, xColor));
- BITBOARD bbFriendRQ = pos->bbPieces[uColor][ROOK] |
- pos->bbPieces[uColor][QUEEN];
- BITBOARD bbUnsafeForRook = pos->bbPawnAttacks[xColor] |
- pos->bbMinorAttacks[xColor];
- BITBOARD bbExclude = 0;
- BITBOARD bbSeen = 0;
- BITBOARD bbLayer = bbAttack;
- BITBOARD bbXrayAccum = 0;
- BITBOARD bbFirstLayerMask = 0;
+ BITBOARD bbEnemyGEHere = bbAttack & bbEnemyQK;
+ BITBOARD bbEnemyLEHere = bbAttack & bbEnemyOcc &
+ ~bbEnemySame & ~bbEnemyGEHere;
+ BITBOARD bbFriendRookHere = bbAttack & pos->bbPieces[uColor][ROOK];
+ BITBOARD bbXray;
- bbMobility = 0;
pos->bbRookAttacks[uColor] |= bbAttack;
- //
- // Unified chain walk over both mobility and x-ray population
- // in one pass, board_representation/EVAL.md section 9. The
- // old mailbox walk's RMOB_FRIEND_ROOK/_QUEEN and
- // RMOB_ENEMY_GREATER cases don't just x-ray past their blocker
- // for attack-bit purposes -- they keep walking (fStop=FALSE)
- // and keep crediting mobility for whatever safe/empty squares
- // and further captures lie beyond, however many such blockers
- // are stacked on one ray. A first version of this conversion
- // (committed, since fixed) only handled the near side (up to
- // the first blocker) for mobility and treated the chain purely
- // as an attack-bit population exercise -- caught by comparing
- // against the old mailbox walk directly on a battery position
- // (two same-color rooks on an open file: old code credited 13
- // mobility to the far rook's near companion via squares beyond
- // it, the near-side-only version credited only 9). Fixed by
- // reusing the exact same "recompute with blockers excluded"
- // chain used for x-ray population to also accumulate mobility
- // credit at every layer, not just the first.
- //
- // Each iteration classifies the newly-revealed blockers on
- // each still-open ray: friendly rook/queen and enemy queen/
- // king continue the chain (recompute with them excluded too);
- // everything else (friendly non-R/Q, enemy pawn/knight/bishop,
- // enemy rook, or the board edge) stops that ray. Bounded: each
- // iteration either terminates or consumes at least one new
- // piece into the exclusion set, so this can loop at most
- // once per piece on the board.
- //
- for (;;)
- {
- BITBOARD bbNew = bbLayer & ~bbSeen;
- BITBOARD bbBlockersHere = bbNew & pos->bbOccupied;
- BITBOARD bbEmptyHere = bbNew & ~pos->bbOccupied;
- BITBOARD bbFriendRQHere = bbBlockersHere & bbFriendRQ;
- BITBOARD bbEnemySameHere = bbBlockersHere & bbEnemySame;
- BITBOARD bbEnemyGEHere = bbBlockersHere & bbEnemyGEContinue;
- BITBOARD bbEnemyLEHere = bbBlockersHere & bbEnemyOcc &
- ~bbEnemySameHere & ~bbEnemyGEHere;
- BITBOARD bbFriendRookHere = bbBlockersHere &
- pos->bbPieces[uColor][ROOK];
- BITBOARD bbContinueHere;
-
- bbSeen |= bbNew;
- bbXrayAccum |= (bbNew & bbFirstLayerMask);
- bbFirstLayerMask = ~(BITBOARD)0;
+ bbMobility = (bbAttack & ~pos->bbOccupied & ~bbUnsafeForRook) |
+ (bbAttack & bbEnemySame) |
+ bbEnemyGEHere |
+ (bbEnemyLEHere & ~bbUnsafeForRook);
- bbMobility |= (bbEmptyHere & ~bbUnsafeForRook) |
- bbEnemySameHere | bbEnemyGEHere |
- (bbEnemyLEHere & ~bbUnsafeForRook);
+ // Connected-rook bonus -- direct attack only now; see the comment
+ // above.
+ while (bbFriendRookHere)
+ {
+ COOR cBlocker = CoorFromBitBoardRank8ToRank1(&bbFriendRookHere);
+ FLAG fHoriz = (RANK(cBlocker) == RANK(c));
+ EVAL_TERM(uColor,
+ ROOK,
+ cBlocker,
+ pos->iScore[uColor],
+ (ROOK_CONNECTED_HORIZ * fHoriz +
+ ROOK_CONNECTED_VERT * FLIP(fHoriz)),
+ "rooks connected");
+ }
- // Connected-rook bonus fires at every friendly rook found
- // along the chain, not just the first (matches the old
- // walk: each RMOB_FRIEND_ROOK hit its own EVAL_TERM call).
- while (bbFriendRookHere)
- {
- COOR cBlocker = CoorFromBitBoardRank8ToRank1(&bbFriendRookHere);
- FLAG fHoriz = (RANK(cBlocker) == RANK(c));
- EVAL_TERM(uColor,
- ROOK,
- cBlocker,
- pos->iScore[uColor],
- (ROOK_CONNECTED_HORIZ * fHoriz +
- ROOK_CONNECTED_VERT * FLIP(fHoriz)),
- "rooks connected");
- }
+ //
+ // King-ring x-ray, replacing the old chain's attack-bit population:
+ // only pay for an extra magic probe when this rook already directly
+ // attacks the enemy queen or king square (bbEnemyGEHere, already
+ // computed above for mobility -- free to test). bbEnemyGEHere has
+ // at most two bits (queen, king), each the terminal blocker of a
+ // *different* ray -- masking both off at once and re-probing in a
+ // single magic lookup gives the same union a per-blocker loop would
+ // (one ray's blocker removal can't affect the other ray's result).
+ // No deeper chaining needed either way, since the only consumer
+ // (_EvalKingSafety) only ever ANDs this against the king's ring,
+ // never walks further.
+ //
+ bbXray = bbEnemyGEHere ?
+ _RookAttacksBB(c, pos->bbOccupied & ~bbEnemyGEHere) : 0;
+ pos->bbRookXrayAttacks[uColor] |= bbXray;
- bbContinueHere = bbFriendRQHere | bbEnemyGEHere;
- if (!bbContinueHere)
- {
- break;
- }
- bbExclude |= bbContinueHere;
- bbLayer = _RookAttacksBB(c, pos->bbOccupied & ~bbExclude);
- }
- pos->bbRookXrayAttacks[uColor] |= bbXrayAccum;
+ // Pseudo-mobility bonus for squares seen past the queen/king blocker
+ // -- see ROOK_XRAY_MOBILITY_BY_SQUARES's comment, same reasoning as
+ // bishop's identical term.
+ EVAL_TERM(uColor,
+ ROOK,
+ c,
+ pos->iScore[uColor],
+ ROOK_XRAY_MOBILITY_BY_SQUARES[
+ MINU(FastCountBits(bbXray & ~bbAttack), 8)],
+ "xray pseudo-mobility");
uTotalMobility = FastCountBits(bbMobility);
ASSERT(uTotalMobility <= 14);
@@ -5060,7 +4966,9 @@ Return value:
static void
_EvalQueen(IN OUT POSITION *pos,
IN COOR c,
- IN PAWN_HASH_ENTRY *pHash)
+ IN PAWN_HASH_ENTRY *pHash,
+ IN BITBOARD bbEnemyQK,
+ IN BITBOARD bbUnsafeForQueen)
/**
Parameters:
@@ -5068,6 +4976,13 @@ Parameters:
POSITION *pos,
COOR c,
PAWN_HASH_ENTRY *pHash,
+ BITBOARD bbEnemyQK -- enemy queen|king, precomputed once per color by
+ Eval()'s driver -- identical formula to _EvalRook's bbEnemyQK, so
+ the same precomputed value is reused for both (see
+ MOBILITY_BITBOARD_REWRITE.md).
+ BITBOARD bbUnsafeForQueen -- precomputed once per color right before
+ the queen loops start, valid because both colors' pawn/minor/rook
+ passes are already complete by then.
Return value:
@@ -5079,113 +4994,54 @@ Return value:
ULONG uColor;
ULONG xColor;
ULONG uTotalMobility;
- ULONG u;
- COOR cKing;
ASSERT(IS_ON_BOARD(c));
ASSERT(p && IS_QUEEN(p));
uColor = GET_COLOR(p);
ASSERT(IS_VALID_COLOR(uColor));
xColor = FLIP(uColor);
- cKing = KingPosition(pos, xColor);
- ASSERT(IS_ON_BOARD(cKing));
- ASSERT(IS_KING(pos->rgSquare[cKing].pPiece));
+ ASSERT(IS_ON_BOARD(KingPosition(pos, xColor)));
+ ASSERT(IS_KING(pos->rgSquare[KingPosition(pos, xColor)].pPiece));
//
- // Do queen mobility (and update attack tables) -- two-pass
- // rook-direction/bishop-direction magic lookups, board_
- // representation/EVAL.md section 1b/9: MOVEGEN_MIGRATION.md
- // already found (in the stashed first bitboard-eval attempt's
- // _EvalQueenOccupancyBB PoC) that a combined 8-ray table measures
- // *slower* than reusing the rook/bishop tables in two passes --
- // reuse that structure here too, don't rediscover the regression.
- // Each pass is its own unified chain walk, identical technique to
- // rook's/bishop's own conversions (including their fix for
- // crediting mobility beyond a battery partner, not just x-ray
- // attack bits): continue through a friendly queen in both passes,
- // a friendly rook only in the rook-direction pass, a friendly
- // bishop only in the bishop-direction pass (QMOB_FRIEND_ROOK/
- // _BISHOP's old per-ray-family split falls out for free from
- // *which* lookup a blocker shows up in -- no fOrthogonalRay flag
- // needed). Unlike rook/bishop, queen never x-rays through *any*
- // enemy piece (QMOB_ENEMY_GE always stopped in the old table, the
- // one case that's a *fewer*-cases asymmetry versus bishop, not
- // more), so the enemy side of each pass's continue-set is empty.
+ // Queen mobility (and update attack tables) -- two single magic
+ // probes (rook-direction + bishop-direction), unioned, then one
+ // shared mask pass. No chain walk (see MOBILITY_BITBOARD_REWRITE.md):
+ // - terminal enemy queen/king: counts unconditionally -- a capture
+ // is always "real" mobility.
+ // - empty or terminal enemy pawn/minor/rook: counts unless the
+ // square is attacked by an enemy pawn, minor, rook, or (modulo
+ // our own defense) the enemy king -- same partition
+ // bishop/knight/rook's reductions use, just with a bigger
+ // "cheaper piece" set since the queen is the most valuable.
+ // - any friendly piece: blocks, no count.
//
- 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.
+ // Deliberately NOT replicated: mobility credit for squares beyond a
+ // friendly rook/bishop/queen battery partner -- a queen cannot
+ // physically move past a piece it shares a square with in one ply,
+ // so (same as bishop/rook) that was never "real mobility" to begin
+ // with. Also NOT replicated, and needing no replacement: the old
+ // code's x-ray-attack population. Unlike bishop/rook, queen's old
+ // chain only ever continued through a FRIENDLY blocker (it never
+ // x-rayed through any enemy piece at all -- QMOB_ENEMY_GE always
+ // stopped the ray in the original mailbox table). By the same
+ // redundancy argument used to drop bishop/rook's friendly-blocker
+ // x-ray case, that information was always a duplicate of the
+ // blocking piece's own direct-attack pass -- there's no enemy-GE
+ // case here to replace, so _EvalKingSafety's queen-x-ray reads
+ // (pos->bbQueenXrayAttacks[xColor]) see nothing lost by simply never
+ // populating it.
//
- 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;
- 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;
-
- 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;
-
- bbMobility |= (bbEmptyHere & ~bbUnsafeForQueen) |
- bbEnemyGEHere |
- (bbEnemyLEHere & ~bbUnsafeForQueen);
+ BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, xColor);
+ BITBOARD bbAttackTotal = _RookAttacksBB(c, pos->bbOccupied) |
+ _BishopAttacksBB(c, pos->bbOccupied);
+ BITBOARD bbEnemyGEHere = bbAttackTotal & bbEnemyQK;
+ BITBOARD bbEnemyLEHere = bbAttackTotal & bbEnemyOcc & ~bbEnemyGEHere;
+ BITBOARD bbMobility = (bbAttackTotal & ~pos->bbOccupied & ~bbUnsafeForQueen) |
+ bbEnemyGEHere |
+ (bbEnemyLEHere & ~bbUnsafeForQueen);
- 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);
ASSERT(uTotalMobility <= 27);
@@ -5246,10 +5102,6 @@ Return value:
{
BITV bvPattern;
ULONG uQueenNearKing;
-#ifdef KING_DEFECT_DUMP
- COOR cSquare;
- KING_DEFECT_DUMP_INFO *pDump = &ctx->sKingDefectDump[uColor];
-#endif
SCORE iKingScore = 0;
// 2026-09-15: linear-SCORE rewrite -- see king_safety_data/
@@ -5378,105 +5230,9 @@ Return value:
iSafety += KING_BACK_FLIGHT_BONUS *
(SCORE)FastCountBits(bbBack & bbEmpty & ~bbEnemyAtkAny);
-#ifdef KING_DEFECT_DUMP
- {
- // Debug-only, per-square breakdown for the mining pipeline
- // (extract_king_features.py) -- kept fully independent of
- // the bitboard computation above, same traversal shape as
- // the pre-rewrite loop so king_features_v4.csv's column
- // semantics stay unchanged. Does not feed iSafety.
- static INT KingSafetyDeltas[9] =
- {
- -17, -16, -15,
- -1, +1,
- +15, +16, +17, 0
- };
- ULONG uSlot = 0;
- BITV bvSlotAttack, bvSlotXray, bvSlotDefend;
-
- do
- {
- cSquare = c + KingSafetyDeltas[uSlot];
- if (IS_ON_BOARD(cSquare))
- {
- BITBOARD sq = COOR_TO_BB(cSquare);
- BITV bvEither;
-
- bvSlotAttack = ((pos->bbPawnAttacks[xColor] & sq) ? PAWN_BIT : 0) |
- ((pos->bbMinorAttacks[xColor] & sq) ? MINOR_BIT : 0) |
- ((pos->bbRookAttacks[xColor] & sq) ? ROOK_BIT : 0) |
- ((pos->bbQueenAttacks[xColor] & sq) ? QUEEN_BIT : 0);
- bvSlotXray = ((pos->bbMinorXrayAttacks[xColor] & sq) ? MINOR_BIT : 0) |
- ((pos->bbRookXrayAttacks[xColor] & sq) ? ROOK_BIT : 0) |
- ((pos->bbQueenXrayAttacks[xColor] & sq) ? QUEEN_BIT : 0);
- bvSlotDefend = ((pos->bbPawnAttacks[uColor] & sq) ? PAWN_BIT : 0) |
- ((pos->bbMinorAttacks[uColor] & sq) ? MINOR_BIT : 0) |
- ((pos->bbRookAttacks[uColor] & sq) ? ROOK_BIT : 0) |
- ((pos->bbQueenAttacks[uColor] & sq) ? QUEEN_BIT : 0) |
- ((pos->bbKingAttacks[uColor] & sq) ? KING_BIT : 0);
-
- bvEither = bvSlotAttack | bvSlotXray;
- ASSERT(uSlot < KING_RING_NUM_SLOTS);
- pDump->fOnBoard[uSlot] = TRUE;
- pDump->fAttackedByPieceType[uSlot][KING_RING_PAWN] =
- ((bvEither & PAWN_BIT) != 0);
- pDump->fAttackedByPieceType[uSlot][KING_RING_MINOR] =
- ((bvEither & MINOR_BIT) != 0);
- pDump->fAttackedByPieceType[uSlot][KING_RING_ROOK] =
- ((bvEither & ROOK_BIT) != 0);
- pDump->fAttackedByPieceType[uSlot][KING_RING_QUEEN] =
- ((bvEither & QUEEN_BIT) != 0);
- if (bvSlotAttack != 0)
- {
- BITV bvDefendForDump = bvSlotDefend;
- if (bvSlotXray || (bvSlotAttack & (bvSlotAttack - 1)))
- {
- bvDefendForDump &= ~KING_BIT;
- }
- if (bvDefendForDump == 0)
- {
- pDump->fAttackedUndefended[uSlot] = TRUE;
- }
- else
- {
- pDump->fAttackedDefended[uSlot] = TRUE;
- }
- }
- else
- {
- if (bvSlotXray != 0)
- {
- pDump->fXrayedUnattacked[uSlot] = TRUE;
- }
- if (!(pos->bbOccupied & sq))
- {
- pDump->fEmptyFlight[uSlot] = TRUE;
- }
- }
- }
- uSlot++;
- }
- while (KingSafetyDeltas[uSlot] != 0);
- }
-#endif
-
- //
// _ComputeFileStormDefects's comment.
- //
iSafety += _GetFileStormDefects(ctx, pos, pHash, uColor);
-#ifdef KING_DEFECT_DUMP
- _ComputeFileStormDefectsSplit(pos, pHash, uColor,
- &pDump->uFileStormOpenFile,
- &pDump->uFileStormStormingPawn);
- pDump->uFileStormDefects = pDump->uFileStormOpenFile +
- pDump->uFileStormStormingPawn;
-#endif
-
iSafety += KING_INITIAL_LOCATION_SCALE * KING_EVAL_INITIAL_LOCATION[uColor][c];
-#ifdef KING_DEFECT_DUMP
- pDump->uInitialCounterByLocation =
- (ULONG)KING_EVAL_INITIAL_LOCATION[uColor][c];
-#endif
// Danger-magnitude acceleration -- see KING_DANGER_ACCELERATOR's
// comment above. Applied here (as part of the real per-king
@@ -5513,21 +5269,6 @@ Return value:
iSafety,
"king safety");
-#ifdef KING_DEFECT_DUMP
- pDump->iRealSafety = iSafety;
- pDump->uQueenNearKing = uQueenNearKing;
- // uCounterTotal/uSafetyTableValue no longer have a coherent
- // meaning post-rewrite (no more shared uCounter/KING_SAFETY_
- // BY_COUNTER lookup) -- left zeroed by the memset above rather
- // than populated with something misleading. Both were REFERENCE-
- // only columns in the training pipeline (train_king_safety_
- // model.py explicitly excludes them from the feature set), so
- // this doesn't affect the regression itself.
- pDump->fCastled = pos->fCastled[uColor];
- pDump->uMinorsAtHome = pos->uMinorsAtHome[uColor];
- pDump->uAttackerArmyScaler = pos->uArmyScaler[xColor];
-#endif
-
// Bonus for castling / penalty for loss of castle. Also, if
// side has not yet castled AND castling is still a live
// option, be concerned with undeveloped minor pieces.
@@ -5551,56 +5292,51 @@ Return value:
// copy-pasted blocks -- one fewer per-piece branch for the
// processor to mispredict (board_representation/EVAL.md
// section 9).
- if (FALSE == pos->fCastled[uColor])
+ ULONG uDevelopmentScaler = CASTLE_AND_DEVELOPMENT_SCALER[pos->uArmyScaler[uColor]];
+ if (uDevelopmentScaler > 0)
{
- ULONG uDevelopmentScaler = CASTLE_AND_DEVELOPMENT_SCALER[pos->uArmyScaler[uColor]];
- if (uDevelopmentScaler > 0)
- {
- static const BITV CASTLE_SHORT[2] =
- { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT };
- static const BITV CASTLE_LONG[2] =
- { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG };
- BITV bvBothRights = CASTLE_SHORT[uColor] | CASTLE_LONG[uColor];
- // One popcount answers both "can this side still
- // castle at all" (>0, gates development below) and
- // "how many options are missing" (2-count, scales
- // the persistent penalty below) -- replaces two
- // separate (==0) branches/multiplies/EVAL_TERM calls
- // with one shared count.
- ULONG uRightsRemaining =
- FastCountBits(pos->bvCastleInfo & bvBothRights);
- ASSERT(uRightsRemaining <= 2);
+ static const BITV CASTLE_SHORT[2] =
+ { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT };
+ static const BITV CASTLE_LONG[2] =
+ { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG };
+ static const BITV bvBothRights[2] =
+ { CASTLE_SHORT[BLACK] | CASTLE_LONG[BLACK],
+ CASTLE_SHORT[WHITE] | CASTLE_LONG[WHITE] };
- // Branchless: (uRightsRemaining > 0) is 0 or 1, multiplied
- // straight into the term instead of an `if` -- nothing
- // left to mispredict.
- ASSERT(pos->uMinorsAtHome[uColor] <= 4);
- EVAL_TERM(uColor,
- 0,
- ILLEGAL_COOR,
- pos->iScore[uColor],
- (SCORE)((UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] *
- (SCORE)uDevelopmentScaler) / 8),
- "development");
+ ASSERT(pos->uMinorsAtHome[uColor] <= 4);
+ EVAL_TERM(uColor,
+ 0,
+ ILLEGAL_COOR,
+ pos->iScore[uColor],
+ (SCORE)((UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] *
+ (SCORE)uDevelopmentScaler) / 8),
+ "development");
- // Deliberately NOT gated on uRightsRemaining > 0 --
- // see this block's header comment. Fires at full
- // weight (both options gone) even long after the
- // rights were lost, for as long as uDevelopmentScaler
- // itself stays nonzero.
- EVAL_TERM(uColor,
- KING,
- c,
- iKingScore,
- (SCORE)((KING_MISSING_ONE_CASTLE_OPTION *
- (SCORE)(2 - uRightsRemaining) *
- (SCORE)uDevelopmentScaler) / 8),
- "missing castle option(s)");
-#ifdef KING_DEFECT_DUMP
- pDump->fMissingCastleShort = ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0);
- pDump->fMissingCastleLong = ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0);
-#endif
- }
+ // One popcount answers both "can this side still
+ // castle at all" (>0, gates development below) and
+ // "how many options are missing" (2-count, scales
+ // the persistent penalty below) -- replaces two
+ // separate (==0) branches/multiplies/EVAL_TERM calls
+ // with one shared count.
+ ULONG uRightsRemaining =
+ FastCountBits(pos->bvCastleInfo & bvBothRights[uColor]);
+ ASSERT(uRightsRemaining <= 2);
+
+ // Deliberately NOT gated on uRightsRemaining > 0 --
+ // see this block's header comment. Fires at full
+ // weight (both options gone) even long after the
+ // rights were lost, for as long as uDevelopmentScaler
+ // itself stays nonzero.
+ EVAL_TERM(uColor,
+ KING,
+ c,
+ iKingScore,
+ (SCORE)(
+ (FALSE == pos->fCastled[uColor]) *
+ (KING_MISSING_ONE_CASTLE_OPTION *
+ (SCORE)(2 - uRightsRemaining) *
+ (SCORE)uDevelopmentScaler) / 8),
+ "missing castle option(s)");
}
return iKingScore;
}
@@ -5756,12 +5492,6 @@ Return value:
// Update attack bitvs for squares around the king.
BITBOARD bbRing = g_KingAttacksBB[c];
pos->bbKingAttacks[uColor] |= bbRing;
-#ifdef KING_DEFECT_DUMP
- {
- KING_DEFECT_DUMP_INFO *pDump = &ctx->sKingDefectDump[uColor];
- memset(pDump, 0, sizeof(KING_DEFECT_DUMP_INFO));
- }
-#endif
// Safety-vs-activity split -- see this function's header comment
// (2026-10-06 restructuring). Don't bother with king safety when
@@ -7883,6 +7613,29 @@ Return value:
// Evaluate individual pieces.
//
+ // Precomputed once per color instead of rebuilt by every bishop/
+ // rook/queen of that color (see MOBILITY_BITBOARD_REWRITE.md's
+ // "squeeze it" follow-up). bbEnemyRQK/bbEnemyQK depend only on
+ // static piece locations -- no ordering dependency, safe to build
+ // right here before any piece loop runs. bbUnsafeForRook/
+ // bbUnsafeForQueen are filled in below, at the loop boundary where
+ // their inputs are guaranteed complete (both colors' knight+bishop
+ // passes before rook; both colors' pawn/minor/rook passes before
+ // queen) -- NOT incrementally mutated during a loop, which would
+ // let one piece's own attack leak into its same-color sibling's
+ // "unsafe" mask.
+ BITBOARD bbEnemyRQK[2];
+ BITBOARD bbEnemyQK[2];
+ BITBOARD bbUnsafeForRook[2];
+ BITBOARD bbUnsafeForQueen[2];
+
+ bbEnemyQK[WHITE] = pos->bbPieces[WHITE][QUEEN] |
+ COOR_TO_BB(pos->cKingSq[WHITE]);
+ bbEnemyQK[BLACK] = pos->bbPieces[BLACK][QUEEN] |
+ COOR_TO_BB(pos->cKingSq[BLACK]);
+ bbEnemyRQK[WHITE] = bbEnemyQK[WHITE] | pos->bbPieces[WHITE][ROOK];
+ bbEnemyRQK[BLACK] = bbEnemyQK[BLACK] | pos->bbPieces[BLACK][ROOK];
+
// Knights.
bb = pos->bbPieces[uColor][KNIGHT];
while (bb)
@@ -7930,7 +7683,7 @@ Return value:
bb &= (bb - 1);
c = BIT_NUMBER_TO_COOR(u);
TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop,
- _EvalBishop(pos, c, pHash));
+ _EvalBishop(pos, c, pHash, bbEnemyRQK[xColor]));
#ifdef EVAL_DUMP
Trace("After our B at %s:\n%d\t\t%d\t\t(%d for %s)\n",
CoorToString(c),
@@ -7949,7 +7702,7 @@ Return value:
bb &= (bb - 1);
c = BIT_NUMBER_TO_COOR(u);
TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop,
- _EvalBishop(pos, c, pHash));
+ _EvalBishop(pos, c, pHash, bbEnemyRQK[uColor]));
#ifdef EVAL_DUMP
Trace("After enemy B at %s:\n%d\t\t%d\t\t(%d for %s)\n",
CoorToString(c),
@@ -7960,6 +7713,14 @@ Return value:
#endif
}
+ // Both colors' knight+bishop passes are complete now -- safe to
+ // build both colors' "unsafe for rook" masks once, here, instead of
+ // every rook rebuilding its own color's mask from scratch.
+ bbUnsafeForRook[WHITE] = pos->bbPawnAttacks[BLACK] |
+ pos->bbMinorAttacks[BLACK];
+ bbUnsafeForRook[BLACK] = pos->bbPawnAttacks[WHITE] |
+ pos->bbMinorAttacks[WHITE];
+
// Rooks.
bb = pos->bbPieces[uColor][ROOK];
while (bb)
@@ -7967,7 +7728,9 @@ Return value:
u = FastFirstBit(bb) - 1;
bb &= (bb - 1);
c = BIT_NUMBER_TO_COOR(u);
- TIMED_EVAL_CALL(ctx, u64CyclesEvalRook, _EvalRook(pos, c, pHash));
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalRook,
+ _EvalRook(pos, c, pHash, bbEnemyQK[xColor],
+ bbUnsafeForRook[uColor]));
#ifdef EVAL_DUMP
Trace("After our R at %s:\n%d\t\t%d\t\t(%d for %s)\n",
CoorToString(c),
@@ -7985,7 +7748,9 @@ Return value:
u = FastFirstBit(bb) - 1;
bb &= (bb - 1);
c = BIT_NUMBER_TO_COOR(u);
- TIMED_EVAL_CALL(ctx, u64CyclesEvalRook, _EvalRook(pos, c, pHash));
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalRook,
+ _EvalRook(pos, c, pHash, bbEnemyQK[uColor],
+ bbUnsafeForRook[xColor]));
#ifdef EVAL_DUMP
Trace("After enemy R at %s:\n%d\t\t%d\t\t(%d for %s)\n",
CoorToString(c),
@@ -7996,6 +7761,27 @@ Return value:
#endif
}
+ // Both colors' pawn/minor/rook passes are complete now -- safe to
+ // build both colors' "unsafe for queen" masks once, here, instead
+ // of every queen rebuilding its own color's mask from scratch (most
+ // positions only have one queen per side anyway, so this mainly
+ // pays off on promoted-extra-queen positions, but it's free either
+ // way).
+ bbUnsafeForQueen[WHITE] = pos->bbPawnAttacks[BLACK] |
+ pos->bbMinorAttacks[BLACK] |
+ pos->bbRookAttacks[BLACK] |
+ (g_KingAttacksBB[pos->cKingSq[BLACK]] &
+ ~(pos->bbPawnAttacks[WHITE] |
+ pos->bbMinorAttacks[WHITE] |
+ pos->bbRookAttacks[WHITE]));
+ bbUnsafeForQueen[BLACK] = pos->bbPawnAttacks[WHITE] |
+ pos->bbMinorAttacks[WHITE] |
+ pos->bbRookAttacks[WHITE] |
+ (g_KingAttacksBB[pos->cKingSq[WHITE]] &
+ ~(pos->bbPawnAttacks[BLACK] |
+ pos->bbMinorAttacks[BLACK] |
+ pos->bbRookAttacks[BLACK]));
+
// Our queen(s).
bb = pos->bbPieces[uColor][QUEEN];
while (bb)
@@ -8003,7 +7789,9 @@ Return value:
u = FastFirstBit(bb) - 1;
bb &= (bb - 1);
c = BIT_NUMBER_TO_COOR(u);
- TIMED_EVAL_CALL(ctx, u64CyclesEvalQueen, _EvalQueen(pos, c, pHash));
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalQueen,
+ _EvalQueen(pos, c, pHash, bbEnemyQK[xColor],
+ bbUnsafeForQueen[uColor]));
#ifdef EVAL_DUMP
Trace("After our Q at %s:\n%d\t\t%d\t\t(%d for %s)\n",
CoorToString(c),
@@ -8021,7 +7809,9 @@ Return value:
u = FastFirstBit(bb) - 1;
bb &= (bb - 1);
c = BIT_NUMBER_TO_COOR(u);
- TIMED_EVAL_CALL(ctx, u64CyclesEvalQueen, _EvalQueen(pos, c, pHash));
+ TIMED_EVAL_CALL(ctx, u64CyclesEvalQueen,
+ _EvalQueen(pos, c, pHash, bbEnemyQK[uColor],
+ bbUnsafeForQueen[xColor]));
#ifdef EVAL_DUMP
Trace("After enemy Q at %s:\n%d\t\t%d\t\t(%d for %s)\n",
CoorToString(c),
diff --git a/src/generate.c b/src/generate.c
index fdfdc79..d202384 100644
--- a/src/generate.c
+++ b/src/generate.c
@@ -908,7 +908,7 @@ GenerateWhiteKnight(IN MOVE_STACK *pStack,
// ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos)) calls
// below) and for testgenerate.c's benchmark harness. Non-static so
// both of those can call it directly.
-BITBOARD
+BITBOARD FORCEINLINE
_BuildFullOccupiedBB(IN POSITION *pos)
/**
@@ -1004,7 +1004,7 @@ _ComputeCheckTargetMaskBB(IN COOR cKing, IN COOR cAttacker,
// Non-static so testgenerate.c's harness can call it directly to set
// up MOVE_STACK.bbFriendlyOccupied when calling a _Generate*BB
// function outside of _GenerateAllMoves.
-BITBOARD INLINE
+BITBOARD FORCEINLINE
_BuildFriendlySideBB(IN POSITION *pos, IN ULONG uSide)
/**
@@ -4833,10 +4833,18 @@ Return value:
s -= PIECE_VALUE_OVER_100(mv.pMoved);
s += SORT_THESE_FIRST;
- //
- // IDEA: bonus for capturing opponent's last moved
- // piece to encourage quicker fail highs?
- //
+ // DROPPED: recapture bonus (MOVE_ORDERING_IDEAS.md
+ // item 4) -- a real, well-calibrated FH% signal
+ // (64-65% vs. a 49-52% all-captures-at-the-same-
+ // SEE-sign baseline) that never converted into a
+ // tree-size win at two magnitudes tried (50: a
+ // dominant override of MVV/LVA's own ~16-unit
+ // spread, confirmed real tactical loss on
+ // ecm_hard_quick; 4: fixed the regression but
+ // still no node-count improvement on any suite).
+ // See chess.h's DROPPED comment near
+ // COUNTERMOVE_EVIDENCE_THRESHOLD for the full
+ // backlog history.
ASSERT(s >= SORT_THESE_FIRST);
ASSERT(s > 0);
ASSERT((s & STRIP_OFF_FLAGS) < VALUE_KING);
@@ -4899,6 +4907,24 @@ Return value:
s |= COUNTERMOVE_TIER_BONUS;
}
}
+
+ // DROPPED: flee-to-safety tier (MOVE_ORDERING_IDEAS.md
+ // item 6) -- a real, well-calibrated FH% signal
+ // (56-68% vs. ~1.35-1.54% baseline at evidence >=
+ // 10000) that turned out to fire too rarely (2,444-
+ // 9,209 qualifying tries across an entire suite run)
+ // to register any measurable node-count or solve-count
+ // effect either way. See chess.h's DROPPED comment
+ // near COUNTERMOVE_EVIDENCE_THRESHOLD for the full
+ // backlog history.
+
+ // DROPPED: ply-2 move-pair match (MOVE_ORDERING_IDEAS.md
+ // item 3) -- a real, well-calibrated signal (53-66% FH
+ // vs. ~1.2-1.4% baseline) that never converted into a
+ // tree-size win across three mechanisms tried (tier
+ // escape, dominant nudge, proportionate nudge). See
+ // chess.h's DROPPED comment near COUNTERMOVE_EVIDENCE_
+ // THRESHOLD for the full history.
ASSERT(s >= 0);
}
pStack->mvf[u].iValue = s;
diff --git a/src/search.c b/src/search.c
index 0ac0037..89db55c 100644
--- a/src/search.c
+++ b/src/search.c
@@ -244,7 +244,7 @@ _PostReduceDepthBucketEfpAligned(ULONG uNextDepthRaw)
// countermove match's own FH%? See chess.h's CM_EVIDENCE_BUCKETS
// comment. Buckets by log-ish bands rather than linear, since evidence
// values span 0 to ~STRIP_OFF_FLAGS*2 (~16.7M).
-#ifdef PERF_COUNTERS
+#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
static ULONG
_CMEvidenceBucket(ULONG uEvidence)
{
@@ -261,6 +261,7 @@ _CMEvidenceBucket(ULONG uEvidence)
}
return(0);
}
+
#endif
#ifdef TEST_NULL
@@ -541,9 +542,11 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
// definition gate-1 describes.
FLAG fMoveIsPruneReduceEligible = FALSE;
ULONG uRawDepthB;
-#ifdef PERF_COUNTERS
+#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
FLAG fThisMoveIsCountermoveMatch = FALSE;
ULONG uCMEvidenceBucket = 0;
+#endif
+#ifdef PERF_COUNTERS
ULONG uThisMoveHangBucket = 0;
ULONG uThisMoveGapBucket = 0;
ULONG uThisMoveNumBucket = 0;
@@ -864,7 +867,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
// or hash table results.
if (fThreat)
{
- iPositionalExtend += THREE_QUARTERS_PLY;
+ iPositionalExtend += HALF_PLY;
INC(ctx->sCounters.extension.uMateThreat);
}
@@ -1057,7 +1060,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
MVF_MOVE_SEARCHED));
ctx->sMoveStack.mvf[x].bvFlags |= MVF_MOVE_SEARCHED;
#endif
-#ifdef PERF_COUNTERS
+#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
// Countermove evidence calibration -- ongoing check
// that COUNTERMOVE_EVIDENCE_THRESHOLD (chess.h) is
// still well-calibrated: log every countermove-
@@ -1160,14 +1163,11 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
// mvBest). The killer/counter-move/history-increment
// side of the update below is unaffected.
//
- if (!IS_CAPTURE_OR_PROMOTION(mvBest))
- {
- UpdateDynamicMoveOrdering(ctx,
- uDepth,
- mvBest,
- iBestScore,
- 0);
- }
+ UpdateDynamicMoveOrdering(ctx,
+ uDepth,
+ mvBest,
+ iBestScore,
+ 0);
StoreLowerBound(mvBest, pos, iBestScore, uDepth, fThreat);
if (mvBest.pCaptured && !IS_PAWN(mvBest.pCaptured) &&
(iBeta < +NMATE))
@@ -1336,7 +1336,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
(uFHPctThousandths <= EFP_FH_PRUNE_THRESHOLD_PCT_THOUSANDTHS);
fThisMoveEFPPruned =
fThisMoveEFPPruned &&
- (uPredictedFH == 0xFFFF || uPredictedFH < 200);
+ (uPredictedFH == 0xFFFF || uPredictedFH < 300);
#if 0
if (fThisMoveEFPPruned &&
uPredictedFH != 0xFFFF &&
@@ -1525,9 +1525,14 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
}
#endif
}
+#endif // PERF_COUNTERS
+#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
+ // Deliberately its own gate, not nested under PERF_COUNTERS
+ // above -- see MOI_INC/MOVE_ORDERING_EVIDENCE_COLLECTION
+ // comment (chess.h).
if (TRUE == fThisMoveIsCountermoveMatch)
{
- INC(ctx->sCounters.tree.u64CMEvidenceTries[uCMEvidenceBucket]);
+ MOI_INC(ctx->sCounters.tree.u64CMEvidenceTries[uCMEvidenceBucket]);
}
#endif
// Record the extension/reduction actually applied to
@@ -1877,9 +1882,11 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
[uThisMoveGapBucket][uThisMoveNumBucket]);
}
}
+#endif // PERF_COUNTERS
+#ifdef MOVE_ORDERING_EVIDENCE_COLLECTION
if (TRUE == fThisMoveIsCountermoveMatch)
{
- INC(ctx->sCounters.tree.u64CMEvidenceFH[uCMEvidenceBucket]);
+ MOI_INC(ctx->sCounters.tree.u64CMEvidenceFH[uCMEvidenceBucket]);
}
#endif
// Update history and killers list and store in
diff --git a/src/searchsup.c b/src/searchsup.c
index 69c87a2..decce3b 100644
--- a/src/searchsup.c
+++ b/src/searchsup.c
@@ -998,22 +998,12 @@ Return value:
**/
{
static ULONG uLMPStaticMaxPctThousandths[] = {
- 200, 200, 125, 75, 36, 12, 6, 3, 2, 1, 0
+ 200, 200, 150, 75, 50, 25, 12, 6, 3, 1, 0
};
ASSERT(ctx->uPly > 0);
ASSERT(IS_VALID_DEPTH(uRemainingDepth));
ULONG uRemainingPlies = 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
// gamble -- this is the ultra-safe tail only, not "probably fine."
@@ -1024,7 +1014,7 @@ Return value:
}
if ((JOINT_TABLE_SENTINEL == uPredictedFH) ||
- (uPredictedFH >= uLMPStaticMaxPctThousandths[MINU(uRemainingPlies, 10)]))
+ (uPredictedFH > uLMPStaticMaxPctThousandths[MINU(uRemainingPlies, 10)]))
{
return FALSE;
}
diff --git a/tests/ECM.md b/tests/ECM.md
index d3077a3..33bc905 100644
--- a/tests/ECM.md
+++ b/tests/ECM.md
@@ -60,6 +60,10 @@ session).
| ECM.746 | ringers | `r3k2r/p1qb1p2/1p2p2p/3pPpN1/P1nP3Q/8/2P2PPP/R1B1R1K1 w kq - 0 0` | Nxf7 | `++`-confirmed at depth 10 (+2.27), same move held through the final depth-10 re-search (+2.62) -- no wavering. Lost by the f698c02 eval-margin-redesign build (2026-09-18) |
| ECM.280 | confident_quick | `r1b2rk1/1pqp1ppp/p1n1pn2/5N2/8/2PBB3/P1P2PPP/R2QR1K1 w - - 0 0` | Nxg7 | Same first move (`Nxg7`) held stable across all 4 depth-8/9/10 iterations including a `++` confirm at depth 9, despite the score itself swinging (-0.29 to +0.93 to +0.89) -- move choice never wavered. Lost by the f698c02 eval-margin-redesign build (2026-09-18) |
| ECM.597 | hard_quick | `rnb2rk1/pp4pp/2pb1n2/3N1p1q/2P5/3N2P1/P1Q1PPBP/R1B2RK1 w - - 0 0` | Bf4 | Same move (`Bf4`) held stable across all 3 iterations (depths 8-10), no wavering, though never gets an explicit `++` mark. Lost by the f698c02 eval-margin-redesign build (2026-09-18) |
+| ECM.339 | confident_quick | `r1b1r1k1/pp3p1p/1q2p1pQ/2b1P1B1/8/P2B3P/1P3PP1/2R1R1K1 w - -` | Rxc5 | Same shape as ECM.442/672: HEAD solves at depth 10 (310k nodes); the bishop-mobility-rewrite build (2026-10-10) still finds it, just one ply later (depth 11, 1.3M nodes) -- a depth-budget delay, not a blind spot |
+| ECM.450 | confident_quick | `6r1/6rk/3p3p/p2Rp3/R1PqPp1b/2N2Q2/6PP/7K b - -` | Rxg2 | Same depth-budget-delay shape: HEAD solves before depth 10 even prints (<278k nodes); the bishop-mobility-rewrite build (2026-10-10) solves at depth 11 (454k nodes) -- later, not missed |
+| ECM.496 | confident_quick | `r2q1rk1/pp1n2pp/2pb4/1P1p3n/P1PPpP2/4P2b/1BNNBP1P/R2QR1K1 b - -` | Nxf4 | Same depth-budget-delay shape: HEAD solves at depth 10 (2.0M nodes); the bishop-mobility-rewrite build (2026-10-10) solves at depth 11 (2.43M nodes). Neither build actually understands the tactic until depth 14-15 (score jumps 1.4 -> 3.1 -> 3.8) -- the depth-10 "solve" on either side is itself a shallow, barely-there read |
+| ECM.672 | confident_quick | `5rk1/pbrn1ppp/1ppN1q2/2P1p3/3P4/1PRB4/P3QPPP/5RK1 w - - 0 0` | Bxh7+ | Same shape again, pushed back further this time: the rook-mobility-rewrite build (2026-10-10) never touches `Bxh7+` within the sd10 window at all (stays on a quiet `dxe5` line throughout, +1.05), unlike the bishop-only intermediate build which still solved it. Re-run to `sd 16` on the same position: `Bxh7+` appears and gets `++`-confirmed twice at depth 11 (+1.80, +3.30), climbing stably through depth 16 (+6.96) -- a 3-ply depth-budget delay, not a blind spot. Consistent with this position's existing track record (already shown depth-delay-prone under the unrelated 2026-09-11 LMR graded-curve change too, per that session's note above) |
## Flaky / garbage (suite noise -- don't read a loss here as a real regression)
@@ -91,6 +95,8 @@ session).
| ECM.770 | hard_quick | `rnb2rk1/pp3pbp/2p3pB/2q5/4P3/1BN5/PPP3PP/R2Q1R1K w - -` | Rxf7 | HEAD locks onto (an unconfirmed but stable) `Rxf7` through depth 12, then abandons it for `Bf4` from depth 12 (second entry) through depth 16+ (+2.60 -> +2.98 -> +2.57). **Crafty independently confirms `Bf4`, not `Rxf7`**, stably from depth 22 through 28 (+1.78 -> +2.61, six-plus stable iterations) -- the labeled "solution" appears to be wrong, not a search miss. Same wobble reproduced identically under both `JOINT_EXTRA_TIER1_THOUSANDTHS=72` and `=150` (2026-09-30 threshold-sweep investigation), confirming it predates and is unrelated to that change |
| ECM.826 | confident_quick | `1rbq2k1/b1p3pp/p1Pp4/3Ppr2/8/5N2/PPQ2PPP/R1B2RK1 b - -` | Rxf3 | Sits on `Bd4` (-0.73) through depth 10, only switches to (and then stably confirms) `Rxf3` at depth 11-14. Identical behavior, same scores, same switch point under both `JOINT_EXTRA_TIER1_THOUSANDTHS=72` and `=150` (2026-09-30) -- a depth-budget/horizon effect on both thresholds, not a regression |
| ECM.491 | hard_quick | `1r2r1k1/p4ppb/q1pbnn1p/4p3/N1P1P3/pPB2NP1/2Q1RPBP/R5K1 b - -` | Nxe4 | Wobbles `c5` (depths 9-11) -> `Bxe4` (depth 11-13) -> `Nxe4` (matching "solution", depth 13-14), never settling before the sd10/sn5M test budget. Identical wobble, same depths, same scores under both `JOINT_EXTRA_TIER1_THOUSANDTHS=72` and `=150` (2026-09-30) -- predates the threshold change |
+| ECM.298 | hard_quick | `2b1qr1k/7p/p1pR2p1/1pP1p1Q1/4P3/1B6/PP4PP/6K1 w - -` | Re6 | Re-checked under the rook-mobility-rewrite build (2026-10-10): both HEAD (`Re6`, matching the suite label) and the new build (`Rf6`) print *zero* per-iteration lines before hitting the depth limit -- the exact "no iteration lines printed at all, no confirmation of any kind" signature this doc already calls flaky elsewhere (see ECM.601). HEAD's own "solve" here is just as unconfirmed as the new build's "miss" -- not a real loss. (Separately, an unrelated 2026-09-14 session found a *different* candidate playing `h4` here, independently confirmed stronger than the suite's own labeled `Re6` by a deep Crafty check -- see that session's note below. The suite's expected answer for this position may simply be wrong.) |
+| ECM.186 | hard_quick | `1R3b2/3r3k/2p1bp1p/r1q1pNpQ/2PpP2P/6B1/P4PP1/1R4K1 w - -` | Nxh6 | Same depth-budget-delay shape as 339/450/496/672: HEAD solves at depth 11 (+5.75), climbing cleanly through depth 13 (`++` confirmed, +8.13). The queen-mobility-rewrite build (2026-10-10, bishop+rook+queen combined) still prefers `Qe8` at depth 11, but switches to and solves with `Nxh6` at depth 12 (+7.26) -- pushed out one ply, not missed |
## Gains seen (candidate solves, HEAD doesn't) -- not deeply vetted