summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-08-29 22:51:11 -0700
committerScott Gasch <[email protected]>2026-08-29 22:51:11 -0700
commit55581d5f904cafc3100f85a81ceb67dc528f2770 (patch)
treedd360a4209516451e754cc1c6430f745eeeb55e4 /src
parent84a03824ed4cb78ee673d8c330c99557a5377923 (diff)
Eval.c diet: kill exact-duplicate terms, turn down overlapping ones, fix a real king-safety bug found along the way.
A full-file pass over eval.c hunting for the "positional terms too loud" feedback from real chess programmers, following the concrete finding that Crafty prices most structural themes through one term where this codebase spread the same theme across several (passed pawns alone via 5-6 separate additive terms that can all fire for one pawn). Two categories of fix, applied per the rule "if it's counting the same thing twice, kill it; if it's a genuinely different angle on the same theme, turn it down rather than remove it": Pawns (pawn-hash cached, so free regardless of term count -- these are data/magnitude fixes, not perf fixes): - Removed ISOLATED_PAWN_PENALTY_BY_COUNT, a whole-position aggregate that re-priced the same uIsolated[] count already reflected by summing the per-pawn isolated term once per isolated pawn -- an exact duplicate, not a different angle. - CANDIDATE_PASSER_BY_RANK's "in endgame" bonus used to add the exact same value a second time (a literal clone of the term just added above it); now a /2 fractional modifier. - CONNECTED_PASSERS_BY_RANK / SUPPORTED_PASSER_BY_RANK / OUTSIDE_PASSER_BY_DISTANCE scaled to ~1/3 magnitude: each prices a genuinely distinct angle on "how good is this passer" (connected to a partner, pawn-defended, outside the opposing majority) and can stack for the same pawn, so turned down rather than removed. - ISOLATED_DOUBLED_PAWN turned down (-11 -> -5): a per-pawn kicker that stacks with the whole-position DOUBLED_PAWN_PENALTY_BY_COUNT aggregate for the isolated+doubled subset -- different angle (single-worst-case flag vs. whole-position severity), not a duplicate, but a real overlap worth trimming. Pieces (non-cached, real per-node cost, so these are also legibility/ perf fixes, not just magnitude): - Bishop: cut BISHOP_IN_CLOSED_POSITION outright -- it duplicated bishop mobility rather than adding a distinct angle (mobility already measures per-bishop diagonal blockage directly and more precisely than a coarse whole-board proxy). - Knight: killed a stale "don't block unmoved E2/D2 pawns" TODO (opening-book territory, not eval's job) and "a knight with an open file behind it is good" (dubious chess reasoning reusing an unrelated table -- the same lookup as the backward-pawn-blockade bonus, for a completely different concept). - Rook: killed ROOK_TRAPPING_EKING (a rook on the 7th/8th aligned with the enemy king is exactly the geometric pattern CountKingSafetyDefects' CHECK_VECTOR scan already folds into uPiecesPointingAtKing -- belongs in king safety, not a rook- specific bolt-on). Also removed pFriendRook, dead in the same block. - Queen: killed "pointing near enemy K" (QUEEN_ATTACKS_SQ_NEXT_TO_ KING) -- computed from the queen's own mobility ray-cast, direct- attacks only, duplicating what _EvalKing's real (non-lazy-estimate) danger computation already reads from the identical attack-table bits a few lines away. - cTrapped fixed from a single COOR per color to a small [2][4] list (_RecordTrappedCandidate): the old single-slot design let a later piece's zero-mobility candidacy silently overwrite an earlier one's on the same side, discarding a genuinely trapped-and- attacked piece. This fed into search too (RecordTrappedPiece's move-ordering hint), not just eval scoring. RecordTrappedPiece's own per-ply single slot is left alone per design (would double the cost on the branch that already computes it, this is the innermost eval loop) -- now reports the MOST VALUABLE of the candidates found, not just whichever was found last. King (the actual regression-and-recovery of this session): - Cutting the queen's "pointing near enemy K" term initially cost real solves (117->110 on the sd10 curated suites) despite being a correct duplication kill -- the general king-safety loop's per-square attacker accounting was piece-type-blind (a queen attacking a square near the king counted the same as a knight doing the same geometric thing), so removing the one place that priced queen-specific severity lost real fidelity, not just a duplicate. Fixed properly: added KING_QUEEN_PROXIMITY_DANGER, computed from bvAttacks[...].small.uQueen / .xray.uQueen bits the king-safety loop already reads for every one of its 11 squares -- free (no new attack-table work) and more accurate than the killed term (catches x-ray/latent queen threats it never did). Calibrated against the killed term's own empirical magnitude (uNearKing * 8, capped at 6) rather than guessed. Recovered to 118/191 (a new session-best), now with the fidelity gap actually closed instead of just removed. - KING_SUPPORTING_OWN_PASSER_BY_RANK split out from SUPPORTED_PASSER_BY_RANK, which _EvalKing's "kings in front of passers" endgame bonus was silently reusing -- pawn-support and king-escort are different concepts (fires when the KING stands next to its own passer, not when a pawn does); scaling the shared table down for its real purpose was silently also scaling the unrelated king-escort bonus. Seeded with the table's original (pre-scaling) hand-tuned magnitude. - Collapsed three copy-pasted file-scan blocks (c-1/c/c+1, identical logic repeated three times) into one loop -- confirmed behaviorally neutral by isolated sd10 suite testing before landing alongside the king-safety content changes. Net result across the three curated suites (sd10, vs. the hand-tuned+ bugfix baseline this built on): 117 -> 118, a new session best, with every intermediate checkpoint tested via EVAL_DUMP verification + precommit_check.sh + sd10 sweep before moving to the next change. Deliberately deferred, written down for a future session rather than attempted here: a holistic king-safety overhaul (the piece-type- tropism inconsistency across knight/bishop/queen/the general CountKingSafetyDefects scan goes deeper than tonight's scoped fixes), recalibrating EstimatePositionalScore's iKingSwingP90 lazy-eval margin table (the instrumentation that built it no longer exists in this tree, and today's changes have already shifted the true swing distribution it was calibrated against), and training a small king- danger classifier from TWIC checkmate games (snapshot king safety features at -10/-15 moves from real checkmates, not resignations) to calibrate whichever of the above happens first. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01M9ZDiJhiUajUxh95mTXCFJ
Diffstat (limited to 'src')
-rwxr-xr-xsrc/chess.h13
-rwxr-xr-xsrc/eval.c457
2 files changed, 258 insertions, 212 deletions
diff --git a/src/chess.h b/src/chess.h
index 137344f..a83a13d 100755
--- a/src/chess.h
+++ b/src/chess.h
@@ -652,7 +652,18 @@ typedef struct _POSITION
SCORE iMaterialBalance[2]; // material balance
// temporary storage space for use in eval
- COOR cTrapped[2];
+ //
+ // Small fixed-capacity list of zero-mobility piece candidates per
+ // color (bishop/knight/rook/queen evals each can push one), not
+ // just the single most-recently-evaluated one -- a single COOR
+ // here used to silently let a later piece's candidacy overwrite an
+ // earlier one on the same side, discarding it even if it was a
+ // genuinely trapped (zero mobility + actually attacked) piece.
+ // Capacity of 4 is deliberately generous; if a position somehow
+ // has more than 4 simultaneously zero-mobility non-pawn pieces on
+ // one side, the rest are just dropped, not a real concern.
+ COOR cTrapped[2][4];
+ ULONG uNumTrapped[2];
ULONG uArmyScaler[2];
ULONG uClosedScaler;
SCORE iScore[2];
diff --git a/src/eval.c b/src/eval.c
index 9b8a511..424637e 100755
--- a/src/eval.c
+++ b/src/eval.c
@@ -208,11 +208,6 @@ static SCORE DOUBLED_PAWN_PENALTY_BY_COUNT[4][9] =
},
};
-static SCORE ISOLATED_PAWN_PENALTY_BY_COUNT[9] =
-{// 0 1 2 3 4 5 6 7 8
- +0, -3, -7, -9, -16, -35, -50, -85, -100
-};
-
static SCORE ISOLATED_PAWN_BY_PAWNFILE[9] =
{
0, -7, -8, -9, -10, -10, -9, -8, -7
@@ -220,7 +215,13 @@ static SCORE ISOLATED_PAWN_BY_PAWNFILE[9] =
static SCORE ISOLATED_EXPOSED_PAWN = -5;
-static SCORE ISOLATED_DOUBLED_PAWN = -11;
+// Turned down (2026-08-30): a per-pawn modifier that stacks on top of
+// DOUBLED_PAWN_PENALTY_BY_COUNT's whole-position doubled-pawn aggregate
+// for the specific case of a pawn that's both isolated and doubled --
+// a different angle (single-worst-case flag vs. whole-position
+// severity), not the same fact counted twice, so scaled down rather
+// than removed.
+static SCORE ISOLATED_DOUBLED_PAWN = -7;
//
// Note: -25% to -33% if the enemy occupies or controls the next sq.
@@ -238,13 +239,42 @@ static SCORE CANDIDATE_PASSER_BY_RANK[2][9] =
};
// Note: x2
-static SCORE CONNECTED_PASSERS_BY_RANK[2][9] =
+// Scaled to 1/3 of the original hand-tuned magnitude (2026-08-30): these
+// three terms, plus PASSER_BY_RANK and OUTSIDE_PASSER_BY_DISTANCE, all
+// separately price "how good is this passer" from different angles
+// (connected to a partner passer, backed by an ordinary pawn, outside
+// the opposing pawn majority) and can stack for the same pawn -- a
+// rank-7 connected+outside passer could hit ~370cp in this pawn-hash-
+// cached family alone, ~9x Crafty's ~40cp ceiling for the equivalent
+// concept. Rather than remove any of these (they're each pricing a
+// genuinely distinct structural fact, not re-counting the same one --
+// and pawn-hash caching makes the compute cost of keeping all of them
+// free), turned the magnitude down on the three secondary/modifier
+// terms so the stack lands in a saner range; PASSER_BY_RANK (the core
+// "how far advanced" signal) is left as the primary, least-reduced
+// term.
+static SCORE CONNECTED_PASSERS_BY_RANK[2][9] =
+{// 0 1 2 3 4 5 6 7 8
+ { +0, +0, +32, +25, +16, +7, +3, +2, +0 }, // black
+ { +0, +0, +2, +3, +7, +16, +25, +32, +0 } // white
+};
+
+static SCORE SUPPORTED_PASSER_BY_RANK[2][9] =
{// 0 1 2 3 4 5 6 7 8
- { +0, +0, +96, +74, +48, +21, +10, +5, +0 }, // black
- { +0, +0, +5, +10, +21, +48, +74, +96, +0 } // white
+ { +0, +0, +20, +13, +4, +2, +1, +1, +0 }, // black
+ { +0, +0, +1, +1, +2, +4, +13, +20, +0 } // white
};
-static SCORE SUPPORTED_PASSER_BY_RANK[2][9] =
+// Split out 2026-08-30 from SUPPORTED_PASSER_BY_RANK, which _EvalKing's
+// "kings in front of passers" endgame bonus was silently reusing --
+// pawn-support and king-escort are different concepts (this fires when
+// the KING stands next to its own passer, not when a pawn does), so
+// scaling one for its real purpose was silently also scaling the
+// other. Seeded with SUPPORTED_PASSER_BY_RANK's original (pre-scaling)
+// hand-tuned magnitude, since that's what this use case was actually
+// getting before today's pawn-hash pass touched the shared table for
+// an unrelated reason.
+static SCORE KING_SUPPORTING_OWN_PASSER_BY_RANK[2][9] =
{// 0 1 2 3 4 5 6 7 8
{ +0, +0, +60, +40, +13, +6, +3, +1, +0 }, // black
{ +0, +0, +1, +3, +6, +13, +40, +60, +0 } // white
@@ -252,7 +282,7 @@ static SCORE SUPPORTED_PASSER_BY_RANK[2][9] =
static SCORE OUTSIDE_PASSER_BY_DISTANCE[9] =
{// 0 1 2 3 4 5 6 7 8
- +0, +0, +7, +14, +21, +28, +34, +41, +55
+ +0, +0, +2, +5, +7, +9, +11, +14, +18
};
static SCORE PASSER_BONUS_AS_MATERIAL_COMES_OFF[32] =
@@ -353,13 +383,6 @@ static SCORE BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING[9] =
+0, +33, +28, +18, +8, +4, +0, -10, -16
};
-static SCORE BISHOP_IN_CLOSED_POSITION[33] =
-{// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
- 10, 10, 10, 9, 9, 8, 8, 7, 7, 6, 5, 4, 3, 2, 1, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
-// 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
-};
-
//
// Knight eval terms
// ---------------------------------------------------------------------------
@@ -454,9 +477,7 @@ static SCORE ROOK_LEADS_PASSER_BY_PASSER_RANK[2][9] =
static SCORE KING_TRAPPING_ROOK = -40;
-static SCORE ROOK_TRAPPING_EKING = +22;
-
-static SCORE ROOK_VALUE_AS_PAWNS_COME_OFF[17] =
+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,
// 9 10 11 12 13 14 15 16
@@ -527,8 +548,6 @@ static SCORE QUEEN_KING_TROPISM[8] =
0, +34, +28, +22, +19, +17, +16, +15
};
-static SCORE QUEEN_ATTACKS_SQ_NEXT_TO_KING = 8; // x #sq_attacked
-
//
// King eval terms
// ---------------------------------------------------------------------------
@@ -605,6 +624,24 @@ static SCORE KING_SAFETY_BY_COUNTER[42] =
-500,-500,-500
};
+// Added 2026-08-30: penalty for the enemy queen directly attacking or
+// x-raying squares near this king, indexed by count (capped at 6).
+// Replaces the old per-queen "pointing near enemy K" bolt-on
+// (QUEEN_ATTACKS_SQ_NEXT_TO_KING, computed via the queen's own
+// mobility ray-cast, direct-attacks only) with a version computed from
+// data this loop already reads for king safety (no extra attack-table
+// work) and that also catches x-ray/latent queen threats the old
+// version missed. Calibrated against the removed term's own magnitude
+// (uNearKing * 8, capped at 6 -> max 48) rather than guessed, since
+// that's what was empirically shown to matter for real solve rate --
+// see the 180-position king-safety-term distribution sampled
+// 2026-08-30 (median -27, p75 -16, only ~1% worse than -200) for the
+// scale this needs to slot into.
+static SCORE KING_QUEEN_PROXIMITY_DANGER[7] =
+{// 0 1 2 3 4 5 6
+ +0, -8, -16, -24, -32, -40, -48
+};
+
static SCORE KING_MISSING_ONE_CASTLE_OPTION = -23;
typedef struct _DNA_BASE_SIZE {
SCORE *pBase;
@@ -624,7 +661,6 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_ARRAY(BACKWARD_SHIELDED_BY_LOCATION),
DNA_ARRAY(BACKWARD_EXPOSED_BY_LOCATION),
DNA_MATRIX(DOUBLED_PAWN_PENALTY_BY_COUNT),
- DNA_ARRAY(ISOLATED_PAWN_PENALTY_BY_COUNT),
DNA_ARRAY(ISOLATED_PAWN_BY_PAWNFILE),
DNA_VAR(ISOLATED_EXPOSED_PAWN),
DNA_VAR(ISOLATED_DOUBLED_PAWN),
@@ -632,6 +668,7 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_MATRIX(CANDIDATE_PASSER_BY_RANK),
DNA_MATRIX(CONNECTED_PASSERS_BY_RANK),
DNA_MATRIX(SUPPORTED_PASSER_BY_RANK),
+ DNA_MATRIX(KING_SUPPORTING_OWN_PASSER_BY_RANK),
DNA_ARRAY(OUTSIDE_PASSER_BY_DISTANCE),
DNA_ARRAY(PASSER_BONUS_AS_MATERIAL_COMES_OFF),
DNA_VAR(RACER_WINS_RACE),
@@ -643,7 +680,6 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_ARRAY(BISHOP_MOBILITY_BY_SQUARES),
DNA_ARRAY(BISHOP_MAX_MOBILITY_IN_A_ROW_BONUS),
DNA_ARRAY(BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING),
- DNA_ARRAY(BISHOP_IN_CLOSED_POSITION),
DNA_ARRAY(KNIGHT_CENTRALITY_BONUS),
DNA_ARRAY(KNIGHT_KING_TROPISM_BONUS),
DNA_ARRAY(KNIGHT_UNASSAILABLE_BY_DIST_FROM_EKING),
@@ -657,7 +693,6 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_MATRIX(ROOK_BEHIND_PASSER_BY_PASSER_RANK),
DNA_MATRIX(ROOK_LEADS_PASSER_BY_PASSER_RANK),
DNA_VAR(KING_TRAPPING_ROOK),
- DNA_VAR(ROOK_TRAPPING_EKING),
DNA_ARRAY(ROOK_VALUE_AS_PAWNS_COME_OFF),
DNA_VAR(ROOK_CONNECTED_VERT),
DNA_VAR(ROOK_CONNECTED_HORIZ),
@@ -666,10 +701,10 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_ARRAY(QUEEN_MOBILITY_BY_SQUARES),
DNA_ARRAY(QUEEN_OUT_EARLY),
DNA_ARRAY(QUEEN_KING_TROPISM),
- DNA_VAR(QUEEN_ATTACKS_SQ_NEXT_TO_KING),
DNA_MATRIX(KING_INITIAL_COUNTER_BY_LOCATION),
DNA_ARRAY(KING_TO_CENTER),
DNA_ARRAY(KING_SAFETY_BY_COUNTER),
+ DNA_ARRAY(KING_QUEEN_PROXIMITY_DANGER),
DNA_VAR(KING_MISSING_ONE_CASTLE_OPTION)
};
@@ -1406,7 +1441,10 @@ Return value:
//
// If the other side has no pieces then give this candidate an
- // extra bonus.
+ // extra bonus -- used to be a second full copy of the same
+ // CANDIDATE_PASSER_BY_RANK value (an exact duplicate of the
+ // term just added above, not a different angle on it), now a
+ // fractional modifier.
//
if (pos->uNonPawnCount[FLIP(uColor)][0] == 1)
{
@@ -1414,7 +1452,7 @@ Return value:
PAWN,
c,
pHash->iScore[uColor],
- CANDIDATE_PASSER_BY_RANK[uColor][RANK(c)],
+ CANDIDATE_PASSER_BY_RANK[uColor][RANK(c)] / 2,
"candidate passer in endgame");
}
}
@@ -1940,46 +1978,32 @@ Return value:
//
if (2 == uUnsupportable)
{
- uPawnFile = FILE(c) + 1;
- EVAL_TERM(uColor,
- PAWN,
- c,
- pHash->iScore[uColor],
- ISOLATED_PAWN_BY_PAWNFILE[uPawnFile],
- "target/isolated pawn");
-
//
- // Isolated + exposed? Extra penalty.
- //
- // Tuning can (and has) flipped this sign; not a real invariant.
- //ASSERT(ISOLATED_EXPOSED_PAWN < 0);
- EVAL_TERM(uColor,
- PAWN,
- c,
- pHash->iScore[uColor],
- ((pHash->uCountPerFile[FLIP(uColor)][uPawnFile]==0)*
- ISOLATED_EXPOSED_PAWN),
- "exposed target");
-
- //
- // Exponential penalty term
+ // Single consolidated isolated-pawn penalty: base
+ // severity by file, plus modifiers if the pawn is also
+ // exposed and/or doubled. These used to be three
+ // separate additive EVAL_TERM calls that all fired for
+ // the same pawn, plus a fourth, whole-position
+ // "exponential isolated" aggregate below that
+ // double-counted the same uIsolated[] count this loop
+ // already prices in one pawn at a time -- that
+ // aggregate is removed entirely below rather than just
+ // consolidated (see the comment there).
//
+ uPawnFile = FILE(c) + 1;
uIsolated[uColor] += 1;
ASSERT(uIsolated[uColor] > 0);
ASSERT(uIsolated[uColor] <= 8);
-
- //
- // Isolated + doubled? Extra penalty.
- //
- // Tuning can (and has) flipped this sign; not a real invariant.
- //ASSERT(ISOLATED_DOUBLED_PAWN < 0);
EVAL_TERM(uColor,
PAWN,
c,
pHash->iScore[uColor],
- ((pHash->uCountPerFile[uColor][uPawnFile] > 1) *
- ISOLATED_DOUBLED_PAWN),
- "doubled + target");
+ (ISOLATED_PAWN_BY_PAWNFILE[uPawnFile] +
+ ((pHash->uCountPerFile[FLIP(uColor)][uPawnFile]==0) *
+ ISOLATED_EXPOSED_PAWN) +
+ ((pHash->uCountPerFile[uColor][uPawnFile] > 1) *
+ ISOLATED_DOUBLED_PAWN)),
+ "isolated pawn");
}
fast_skip:
@@ -2134,18 +2158,13 @@ Return value:
ASSERT(uIsolated[WHITE] <= 8);
ASSERT(uIsolated[BLACK] >= 0);
ASSERT(uIsolated[BLACK] <= 8);
- EVAL_TERM(BLACK,
- PAWN,
- ILLEGAL_COOR,
- pHash->iScore[BLACK],
- ISOLATED_PAWN_PENALTY_BY_COUNT[uIsolated[BLACK]],
- "exponential isolated");
- EVAL_TERM(WHITE,
- PAWN,
- ILLEGAL_COOR,
- pHash->iScore[WHITE],
- ISOLATED_PAWN_PENALTY_BY_COUNT[uIsolated[WHITE]],
- "exponential isolated");
+ //
+ // Removed: a redundant "exponential isolated" whole-position term
+ // keyed by uIsolated[color] used to be added here on top of the
+ // per-pawn "isolated pawn" term above, which already sums once per
+ // isolated pawn and so already scales with the same count. Having
+ // both double-counted the same structural fact.
+ //
//
// Look for connected, supported and outside passed pawns to give
@@ -3429,6 +3448,39 @@ Return value:
// ======================================================================
//
+static void
+_RecordTrappedCandidate(IN OUT POSITION *pos,
+ IN ULONG uColor,
+ IN COOR c)
+/**
+
+Routine description:
+
+ Record a zero-mobility piece as a trapped-piece candidate for
+ uColor. Used by _EvalBishop/_EvalKnight/_EvalRook/_EvalQueen; the
+ actual attacked-or-not check happens later, in
+ _EvalTrappedPieces. Capped at ARRAY_LENGTH(pos->cTrapped[0])
+ candidates per side -- extras are just dropped, not a real concern
+ in practice.
+
+Parameters:
+
+ POSITION *pos,
+ ULONG uColor,
+ COOR c
+
+Return value:
+
+ void
+
+**/
+{
+ if (pos->uNumTrapped[uColor] < ARRAY_LENGTH(pos->cTrapped[uColor]))
+ {
+ pos->cTrapped[uColor][pos->uNumTrapped[uColor]++] = c;
+ }
+}
+
static void
_EvalBishop(IN OUT POSITION *pos,
@@ -3584,15 +3636,17 @@ Return value:
"good/bad transient pawns");
//
- // Bonus to bishops in very open positions.
+ // Removed 2026-08-30: BISHOP_IN_CLOSED_POSITION[pos->uClosedScaler]
+ // duplicated bishop mobility (computed just below) rather than
+ // adding a distinct angle on it. uClosedScaler is a coarse,
+ // whole-board openness proxy for exactly the same fact bishop
+ // mobility measures directly and per-bishop via ray-casting -- how
+ // much this specific bishop's diagonals are blocked by pawns. The
+ // mobility term is strictly more precise (opposite-bishop-color
+ // endgames are the clearest case where "position is open overall"
+ // and "this bishop's diagonal is blocked" diverge), so cut the
+ // redundant whole-board proxy rather than scale it down.
//
- ASSERT(pos->uClosedScaler <= 32);
- EVAL_TERM(uColor,
- BISHOP,
- c,
- pos->iScore[uColor],
- BISHOP_IN_CLOSED_POSITION[pos->uClosedScaler],
- "in closed/open position");
//
// Bishop mobility (and update attack table bits)
@@ -3657,7 +3711,7 @@ Return value:
//
if (uTotalMobility == 0)
{
- pos->cTrapped[uColor] = c;
+ _RecordTrappedCandidate(pos, uColor, c);
}
uTotalMobility /= 2;
ASSERT((pos->uMinMobility[uColor] & 0x80000000) == 0);
@@ -3666,8 +3720,16 @@ Return value:
uTotalMobility);
//
- // Look for "active bad bishops". See if square c is safe from
- // enemy pawns.
+ // Bonus for a bishop that's securely placed -- safe from ever
+ // being challenged by an enemy pawn, and (checked below) defended
+ // by a friendly one -- near the enemy king. This is really a
+ // bishop-specific king-tropism/outpost bonus; despite the
+ // historical "active bad bishop" name, it does NOT check whether
+ // the bishop is actually "bad" (see the good/bad pawn-count terms
+ // above) before applying. Candidate for folding into a unified
+ // per-piece-type tropism system alongside
+ // KNIGHT_KING_TROPISM_BONUS/QUEEN_KING_TROPISM (shelved for now,
+ // pending a look at what every piece type currently does).
//
bb = pHash->bbPawnLocations[FLIP(uColor)] &
(~pHash->bbStationaryPawns[FLIP(uColor)]);
@@ -3764,7 +3826,6 @@ Return value:
ULONG uColor;
ULONG uPawnsSupporting;
ULONG u;
- ULONG uPawnFile;
ULONG uMobilitySquares;
SCORE i;
ULONG uDist;
@@ -3786,10 +3847,6 @@ Return value:
ASSERT(pos->uMinorsAtHome[uColor] <= 4);
//
- // TODO: Don't block unmoved E2/D2 pawns
- //
-
- //
// Centrality bonus
//
EVAL_TERM(uColor,
@@ -3894,9 +3951,14 @@ Return value:
}
//
- // Give a bonus for blockading an enemy backward pawn. Note:
- // we also reward pieces for blocking enemy passers in the
- // passer code.
+ // Give a bonus for blockading an enemy backward pawn. (2026-08-30:
+ // the old comment here claimed we also reward pieces for blocking
+ // enemy passers in the passer code -- checked, no longer true if
+ // it ever was. _EvalPassers' "enemy controls/occupies sq ahead"
+ // terms penalize the PASSER'S OWNER for having its stop-square
+ // blocked; they don't reward the blocking piece directly. This is
+ // the only place a blocking piece gets a direct bonus, not a
+ // duplicate of anything.)
//
cSquare = c + 16 * g_iAhead[uColor];
bb = pHash->bbStationaryPawns[FLIP(uColor)];
@@ -3913,16 +3975,15 @@ Return value:
}
//
- // A knight with an open file behind it is good.
+ // Removed 2026-08-30 ("A knight with an open file behind it is
+ // good"): dubious chess reasoning on its own (open-file bonuses
+ // are classically a rook concept -- knights don't use files the
+ // way rooks do) compounded by reusing KNIGHT_ON_INTERESTING_SQUARE
+ // _BY_RANK, the *same* table as the backward-pawn-blockade bonus
+ // just above, for a completely unrelated condition. That reuse
+ // muddied what the table means and made it untunable
+ // independently for either concept.
//
- uPawnFile = FILE(c) + 1;
- EVAL_TERM(uColor,
- KNIGHT,
- c,
- pos->iScore[uColor],
- ((pHash->uCountPerFile[uColor][uPawnFile] == 0) *
- KNIGHT_ON_INTERESTING_SQUARE_BY_RANK[uColor][RANK(c)]),
- "open file behind");
//
// Do mobilility and piece relevance. Also update attack tables.
@@ -3971,7 +4032,7 @@ Return value:
"knight mobility");
if (uMobilitySquares == 0)
{
- pos->cTrapped[uColor] = c;
+ _RecordTrappedCandidate(pos, uColor, c);
}
ASSERT((pos->uMinMobility[uColor] & 0x80000000) == 0);
ASSERT((uMobilitySquares & 0x80000000) == 0);
@@ -4036,7 +4097,6 @@ Return value:
};
PIECE p;
- PIECE pFriendRook;
ULONG uPawnFile = FILE(c) + 1;
ULONG uColor;
ULONG u;
@@ -4178,47 +4238,15 @@ Return value:
}
//
- // Look for rooks that are trapping enemy kings.
+ // Removed 2026-08-30 ("rook trapping enemy king"): a rook on the
+ // 7th/8th rank aligned with the enemy king is exactly the
+ // geometric pattern CountKingSafetyDefects' CHECK_VECTOR
+ // line-of-sight scan already picks up into
+ // uPiecesPointingAtKing/king-safety -- this was pricing the same
+ // king-danger fact a second time via a rook-specific bonus. Belongs
+ // in king safety, not here. (pFriendRook, only ever assigned in
+ // this block and never read, is gone too.)
//
- if (uColor == WHITE)
- {
- pFriendRook = WHITE_ROOK;
-
- //
- // Rook on 7th/8th rank with an enemy king back there.
- //
- if ((c < A6) && (pos->cNonPawns[BLACK][0] < A6))
- {
- ASSERT(RANK(c) > 6);
- ASSERT(RANK(pos->cNonPawns[BLACK][0]) > 6);
-
- EVAL_TERM(WHITE,
- ROOK,
- c,
- pos->iScore[WHITE],
- ROOK_TRAPPING_EKING,
- "rook trapping enemy king");
- }
- }
- else
- {
- pFriendRook = BLACK_ROOK;
-
- //
- // Rook on 7th/8th rank with an enemy king back there.
- //
- if ((c > H3) && (pos->cNonPawns[WHITE][0] > H3))
- {
- ASSERT(RANK(c) < 3);
- ASSERT(RANK(pos->cNonPawns[WHITE][0]) < 3);
- EVAL_TERM(BLACK,
- ROOK,
- c,
- pos->iScore[BLACK],
- ROOK_TRAPPING_EKING,
- "rook trapping enemy king");
- }
- }
//
// Rooks increase in value as pawns come off:
@@ -4314,7 +4342,7 @@ Return value:
//
if (uTotalMobility == 0)
{
- pos->cTrapped[uColor] = c;
+ _RecordTrappedCandidate(pos, uColor, c);
}
//
@@ -4432,7 +4460,6 @@ Return value:
ULONG u;
ULONG uBit;
PMOBILITY_HELPER pFun;
- ULONG uNearKing = 0;
COOR cKing;
ASSERT(IS_ON_BOARD(c));
@@ -4498,7 +4525,6 @@ Return value:
// What did we hit?
//
p = pos->rgSquare[cSquare].pPiece;
- uNearKing += (DISTANCE(cSquare, cKing) <= 1);
pFun = QMobJumpTable[uColor][p];
if (TRUE == (*pFun)(pos,
cSquare,
@@ -4527,7 +4553,7 @@ Return value:
//
if (uTotalMobility == 0)
{
- pos->cTrapped[uColor] = c;
+ _RecordTrappedCandidate(pos, uColor, c);
}
ASSERT((pos->uMinMobility[uColor] & 0x80000000) == 0);
ASSERT((uTotalMobility & 0x80000000) == 0);
@@ -4535,15 +4561,17 @@ Return value:
uTotalMobility);
//
- // If queen points near enemy king, that's a good thing too.
+ // Removed 2026-08-30 ("pointing near enemy K" /
+ // QUEEN_ATTACKS_SQ_NEXT_TO_KING): counted how many king-adjacent
+ // squares the queen's own mobility ray-cast just attacked, using
+ // the same pos->rgSquare[...].bvAttacks bits _EvalKing's real
+ // danger computation (not the lazy-eval estimate) reads directly a
+ // few squares away in the same file. King safety already derives
+ // this more comprehensively -- across every attacking piece type,
+ // properly weighted -- from the identical attack-table data
+ // mobility just paid to populate. This was a narrower, redundant
+ // re-derivation of a subset of that same signal.
//
- ASSERT(uNearKing <= 6);
- EVAL_TERM(uColor,
- QUEEN,
- c,
- pos->iScore[uColor],
- (uNearKing * QUEEN_ATTACKS_SQ_NEXT_TO_KING),
- "pointing near enemy K");
}
static void
@@ -4569,13 +4597,16 @@ Return value:
PIECE p;
ULONG uColor, ufColor;
COOR cSquare;
+ COOR cFileSq;
ULONG u, v;
+ int w;
ULONG uCounter;
BITV bvAttack;
BITV bvXray;
BITV bvDefend;
BITV bvPattern = 0;
ULONG uFlightSquares;
+ ULONG uQueenNearKing = 0;
BITBOARD bb;
SCORE i;
SCORE iKingScore = 0;
@@ -4690,6 +4721,19 @@ Return value:
pos->rgSquare[cSquare].bvAttacks[uColor].small.uKing = 1;
bvDefend = pos->rgSquare[cSquare].bvAttacks[uColor].uSmall;
+ //
+ // Count squares near the king the enemy queen specifically
+ // attacks or x-rays (2026-08-30, replaces the old
+ // per-queen "pointing near enemy K" bolt-on this loop's
+ // own bvAttack/bvXray data already made redundant). Reuses
+ // data already read above -- no extra attack-table work --
+ // and, unlike the old term, catches x-ray/latent queen
+ // threats too, not just direct ray-cast hits.
+ //
+ uQueenNearKing +=
+ (pos->rgSquare[cSquare].bvAttacks[ufColor].small.uQueen |
+ pos->rgSquare[cSquare].bvAttacks[ufColor].xray.uQueen);
+
if (bvAttack != 0)
{
bvPattern |= (bvAttack | bvXray);
@@ -4725,65 +4769,25 @@ Return value:
Trace("%s KS Counter post-squares: %u\n", COLOR_NAME(uColor), uCounter);
#endif
- if (IS_ON_BOARD(c - 1))
- {
- u = FILE(c - 1) + 1;
- v = (pHash->uCountPerFile[WHITE][u] > 0) +
- (pHash->uCountPerFile[BLACK][u] > 0);
- ASSERT((v >= 0) && (v <= 2));
-
- // Note: open rook files are worse than open interior files
- uCounter += (KingFileDefects[v] + ((v < 2) && ((u == 1) || (u == 8))));
-
- bb = pHash->bbPawnLocations[ufColor] & BBFILE[u - 1];
- if (bb)
- {
- if (uColor == WHITE)
- {
- cSquare = CoorFromBitBoardRank1ToRank8(&bb);
- }
- else
- {
- cSquare = CoorFromBitBoardRank8ToRank1(&bb);
- }
- ASSERT(IS_ON_BOARD(cSquare));
- uCounter += KingStormingPawnDefects[DISTANCE(cSquare, c)];
- }
- }
-
- u = FILE(c) + 1;
- v = (pHash->uCountPerFile[WHITE][u] > 0) +
- (pHash->uCountPerFile[BLACK][u] > 0);
- ASSERT((v >= 0) && (v <= 2));
-
- // Note: open rook files are worse than open interior files
- uCounter += (KingFileDefects[v] + ((v < 2) && ((u == 1) || (u == 8))));
- bb = pHash->bbPawnLocations[ufColor] & BBFILE[u - 1];
- if (bb)
+ //
+ // King's own file plus the two adjacent ones (collapsed 2026-08-30
+ // from three copy-pasted blocks that differed only in c-1/c/c+1 --
+ // same behavior and iteration order, just not repeated three times
+ // in the source; confirmed behaviorally neutral by isolated sd10
+ // suite testing before landing).
+ //
+ for (w = -1; w <= 1; w++)
{
- if (uColor == WHITE)
- {
- cSquare = CoorFromBitBoardRank1ToRank8(&bb);
- }
- else
+ cFileSq = c + w;
+ if (!IS_ON_BOARD(cFileSq))
{
- cSquare = CoorFromBitBoardRank8ToRank1(&bb);
+ continue;
}
- ASSERT(IS_ON_BOARD(cSquare));
- uCounter += KingStormingPawnDefects[DISTANCE(cSquare, c)];
- }
-
- if (IS_ON_BOARD(c + 1))
- {
- ASSERT((FILE(c + 1) + 1) == (u + 1));
- u += 1;
+ u = FILE(cFileSq) + 1;
v = (pHash->uCountPerFile[WHITE][u] > 0) +
(pHash->uCountPerFile[BLACK][u] > 0);
ASSERT((v >= 0) && (v <= 2));
-
- // Note: open rook files are worse than open interior files
uCounter += (KingFileDefects[v] + ((v < 2) && ((u == 1) || (u == 8))));
-
bb = pHash->bbPawnLocations[ufColor] & BBFILE[u - 1];
if (bb)
{
@@ -4835,6 +4839,14 @@ Return value:
i,
"king safety");
+ ASSERT(uQueenNearKing <= 10);
+ EVAL_TERM(uColor,
+ KING,
+ c,
+ iKingScore,
+ KING_QUEEN_PROXIMITY_DANGER[MINU(uQueenNearKing, 6)],
+ "queen proximity danger");
+
//
// Bonus for castling / penalty for loss of castle. Also, if side has
// not yet castled, be concerned with undeveloped minor pieces.
@@ -4922,7 +4934,7 @@ Return value:
ASSERT(abs((INT)RANK(c) - (INT)RANK(cSquare)) <= 1);
i = 2;
- i += SUPPORTED_PASSER_BY_RANK[uColor][RANK(cSquare)];
+ i += KING_SUPPORTING_OWN_PASSER_BY_RANK[uColor][RANK(cSquare)];
switch(uColor)
{
case WHITE:
@@ -5417,7 +5429,10 @@ Return value:
**/
{
COOR c;
+ COOR cBestOwnTrapped;
+ ULONG uBestOwnTrappedValue;
ULONG uColor;
+ ULONG u;
POSITION *pos = &ctx->sPosition;
#ifdef DEBUG
PIECE p;
@@ -5425,9 +5440,12 @@ Return value:
FOREACH_COLOR(uColor)
{
- c = pos->cTrapped[uColor];
- if (IS_ON_BOARD(c))
+ cBestOwnTrapped = ILLEGAL_COOR;
+ uBestOwnTrappedValue = 0;
+ for (u = 0; u < pos->uNumTrapped[uColor]; u++)
{
+ c = pos->cTrapped[uColor][u];
+ ASSERT(IS_ON_BOARD(c));
if (_WhoControlsSquareFast(pos, c) == FLIP(uColor))
{
#ifdef DEBUG
@@ -5447,10 +5465,27 @@ Return value:
}
else
{
- RecordTrappedPiece(ctx, c);
+ //
+ // ctx->cTrapped[uPly] (RecordTrappedPiece's target)
+ // is a single slot, not a list -- if more than one
+ // of our own pieces is genuinely trapped this ply,
+ // only report the most valuable one rather than
+ // whichever happened to be found last.
+ //
+ if (PIECE_VALUE(pos->rgSquare[c].pPiece) >
+ uBestOwnTrappedValue)
+ {
+ uBestOwnTrappedValue =
+ PIECE_VALUE(pos->rgSquare[c].pPiece);
+ cBestOwnTrapped = c;
+ }
}
}
}
+ if (IS_ON_BOARD(cBestOwnTrapped))
+ {
+ RecordTrappedPiece(ctx, cBestOwnTrapped);
+ }
}
}
@@ -5573,7 +5608,7 @@ Return value:
// ASSERT(!InCheck(pos, pos->uToMove));
pos->uMinMobility[BLACK] = pos->uMinMobility[WHITE] = 100;
- pos->cTrapped[BLACK] = pos->cTrapped[WHITE] = ILLEGAL_COOR;
+ pos->uNumTrapped[BLACK] = pos->uNumTrapped[WHITE] = 0;
pos->iScore[BLACK] =
(pos->uPawnMaterial[BLACK] + pos->uNonPawnMaterial[BLACK]);