summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-09-20 08:13:57 -0700
committerScott Gasch <[email protected]>2026-09-20 08:13:57 -0700
commit8c0d66f13f08cb5e760d787a878260ad67d17c13 (patch)
treef779fb622a18e0d676da76736d9826ddda5dfdb9
parentb70be6fde0fef0694e404e16d70f00c73dd971a2 (diff)
Makes recogn.c use g_iDrawScore. Some refactoring in the pawn
hash / _EvalPawn including some scoring changes. Self-play positive.
-rw-r--r--src/TODO52
-rw-r--r--src/eval.c514
-rw-r--r--src/recogn.c27
-rw-r--r--tests/ECM.md2
4 files changed, 335 insertions, 260 deletions
diff --git a/src/TODO b/src/TODO
index dce08d2..25b7214 100644
--- a/src/TODO
+++ b/src/TODO
@@ -125,3 +125,55 @@ the same way this session did. Needs a full core budget (N cores) so
it collides with self-play match runs -- schedule it for whenever the
box isn't already saturated by match_play.py workers, not concurrently
with them.
+
+2026-09-18: We should experiment with more aggressive R factors for
+nullmove at deep search depth remaining levels.
+
+2026-09-18: blunders.pgn review (typhoon vs Crafty losses). Two distinct
+loss shapes, different fixes:
+ (1) "Slow grind" -- the majority of losses. Score sits near equal for
+ many moves then drifts 0.1-0.3 pawns/move in Crafty's favor with no
+ single bad move visible, compounding to -3/-4 over 20-40 moves.
+ Annotated depths run ~10-17 ply for us vs ~18-30 for Crafty the whole
+ game, consistently -- this is the NPS/depth gap compounding over time,
+ not an eval or tactics bug. Only real fix is search efficiency (nodes
+ needed per ply) or raw nps, not eval richness -- eval doesn't look wrong
+ here, it's just arriving late.
+ (2) "Cliff" losses -- a handful of moves per game where the score drops
+ 3-4 pawns in one move at a suspiciously shallow depth vs the moves
+ around it (e.g. game 3, move 20: -0.5 for several moves then
+ `20. Nc4 {-4.26/12}`, depth 12 vs neighboring 20-ply reads), mostly in
+ 40/60 (fast) time controls. Looks like a time-management gap, not
+ search quality: candidate fix is EBF-based "don't start ply N+1 if
+ there's no time to finish it" (cheap, Scott: "easy") PLUS a
+ root-instability-triggered "buy more time" signal for volatile
+ positions (PV changing across iterations, last iteration failed
+ high/low, score still swinging over the last few completed depths, or
+ a narrow best-vs-second-best gap) -- recognizing "this position is
+ tactically critical" from scratch is hard/rejected, but search-
+ instability is a cheap proxy already sitting in the ID loop. Not built.
+
+2026-09-18: profile a super-lazy-always (material-only Eval() exit,
+forced unconditionally, throwaway diagnostic build a la the zero-
+selection-budget move-ordering trick -- never commit it) binary against
+Crafty at matched depth/positions to find out where our nps actually
+goes once eval is ~free. Scott's educated guess is SEE and the
+is-this-move-a-check / legal-reply-to-check generators dominate once eval
+is out of the way, not eval itself -- also suspect given the earlier
+`<=`-to-`<` SEE-skip experiment on line ~4178 (skip SEE on same-piece-
+for-same-piece captures) barely changed speed but made trees fatter
+(more nodes needed), i.e. that SEE call was doing real pruning work, not
+just costing time. Before assuming check-detection costs are irreducible,
+confirm "is this move a check" is answered via O(1) bitboard AND against
+the enemy king square from the post-move attack bitboard, not via a
+generate-and-filter pass -- Scott believes he already does this "cheaply"
+but isn't sure it's the fastest available method. Single-legal-reply-to-
+check detection is likely the genuinely unavoidable cost of the three
+(needs a real legal-move count in the child position) -- worth checking
+whether it early-exits as soon as a 2nd legal move is found rather than
+generating the full list.
+
+2026-09-18: consider adding cLikelyUnstoppablePasser to the POSITION
+structure and using it to extend searches. Just like we already do
+with cTrapped and cEnprise -- signals from eval.c to affect search.
+
diff --git a/src/eval.c b/src/eval.c
index 7d224e3..02dab53 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -156,6 +156,7 @@ COOR JUMPING_RANK[2] = { 7, 2 };
// Pawn eval terms
// ---------------------------------------------------------------------------
//
+
// "There is one case which can be treated as positional or material,
// namely the rook's pawn, which differs from other pawns in that it
// can only capture one way instead of two. Since this handicap cannot
@@ -170,14 +171,14 @@ COOR JUMPING_RANK[2] = { 7, 2 };
// --Larry Kaufman, IM
// "Evaluation of Material Imbalance"
//
-static SCORE PAWN_CENTRALITY_BONUS[128] =
+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, 9, 9, 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,
// -------------------------------------------------------------------
- -8, 0, 5, 9, 9, 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,
-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,
0, 0, 0, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0
@@ -336,8 +337,8 @@ static SCORE OUTSIDE_PASSER_BY_DISTANCE[9] =
+0, +0, +2, +5, +7, +9, +11, +14, +18
};
-static SCORE PASSER_BONUS_AS_MATERIAL_COMES_OFF[32] =
-{
+static SCORE PASSER_BONUS_AS_MATERIAL_COMES_OFF[32] =
+{
// none na na m na R 2m na
+60, -1, -1, +37, -1, +43, +19, -1,
@@ -505,7 +506,7 @@ static SCORE KNIGHT_IN_CLOSED_POSITION[33] =
// average bonus, not scaled by distance from the enemy king -- by direct
// instruction. Each was a [8]-by-distance table; these are that table's
// simple mean, rounded.
-static SCORE ROOK_ON_FULL_OPEN = 16;
+static SCORE ROOK_ON_FULL_OPEN = 18;
static SCORE ROOK_ON_HALF_OPEN_WITH_ENEMY = 9;
static SCORE ROOK_ON_HALF_OPEN_WITH_FRIEND = 10;
// Cache of the three constants above, indexed [friend pawn on file?]
@@ -524,6 +525,16 @@ static SCORE ROOK_FULL_HALF_OPEN_BONUS[2][2] = { { 16, 9 }, { 10, 0 } };
static SCORE ROOK_WITH_ENEMY_PASSER = 17;
static SCORE ROOK_WITH_FRIEND_PASSER = 10;
+// A rook on the 7th (White) or 2nd (Black) rank attacks along the
+// enemy's back-ranks-minus-one, hitting undeveloped/unmoved pawns and
+// cutting the enemy king off from that rank -- a small, classic bonus
+// independent of the file-openness/passer bonuses above. BBRANK22/
+// BBRANK77 are compile-time constants (chess.h), so this table (unlike
+// ROOK_FULL_HALF_OPEN_BONUS above) can be a real `static const`: it
+// depends on the board geometry, not on any DNA-tunable value.
+static const BITBOARD BBROOK_7TH_RANK[2] = { BBRANK22, BBRANK77 }; // [BLACK], [WHITE]
+static SCORE ROOK_ON_SEVENTH_BONUS = 9;
+
static SCORE ROOK_BEHIND_PASSER_BY_PASSER_RANK[2][9] =
{// 0 1 2 3 4 5 6 7 8
{ +0, +0, +25, +17, +12, +6, +1, +0, +0 }, // black
@@ -595,7 +606,7 @@ static SCORE QUEEN_MOBILITY_BY_SQUARES[28] =
// point that queens are worth more then the standard nine "points".
// Thus, this scale is biased upwards by about 0.15 pawn.
//
-static SCORE QUEEN_KING_TROPISM[8] =
+static SCORE QUEEN_KING_TROPISM[8] =
{// 0 1 2 3 4 5 6 7
0, +34, +28, +22, +19, +17, +16, +15
};
@@ -619,7 +630,7 @@ static SCORE QUEEN_KING_TROPISM[8] =
static ULONG KING_INITIAL_COUNTER_BY_LOCATION[2][128] =
{
{
- 1, 0, 0, 0, 1, 0, 0, 1, 0,0,0,0,0,0,0,0,
+ 2, 0, 0, 0, 1, 0, 0, 2, 0,0,0,0,0,0,0,0,
1, 1, 1, 1, 1, 1, 1, 1, 0,0,0,0,0,0,0,0,
3, 3, 3, 3, 3, 3, 3, 3, 0,0,0,0,0,0,0,0,
4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0,
@@ -638,7 +649,7 @@ static ULONG KING_INITIAL_COUNTER_BY_LOCATION[2][128] =
4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0,
3, 3, 3, 3, 3, 3, 3, 3, 0,0,0,0,0,0,0,0,
1, 1, 1, 1, 1, 1, 1, 1, 0,0,0,0,0,0,0,0,
- 1, 0, 0, 0, 1, 0, 0, 1, 0,0,0,0,0,0,0,0
+ 2, 0, 0, 0, 1, 0, 0, 2, 0,0,0,0,0,0,0,0
}
};
@@ -930,6 +941,7 @@ static DNA_BASE_SIZE g_EvalDNA[] = {
DNA_VAR(ROOK_ON_HALF_OPEN_WITH_FRIEND),
DNA_VAR(ROOK_WITH_ENEMY_PASSER),
DNA_VAR(ROOK_WITH_FRIEND_PASSER),
+ DNA_VAR(ROOK_ON_SEVENTH_BONUS),
DNA_MATRIX(ROOK_BEHIND_PASSER_BY_PASSER_RANK),
DNA_MATRIX(ROOK_LEADS_PASSER_BY_PASSER_RANK),
DNA_VAR(KING_TRAPPING_ROOK),
@@ -1966,6 +1978,151 @@ Return value:
}
+static INLINE FLAG
+_FileCanSupportPawn(IN POSITION *pos,
+ IN PAWN_HASH_ENTRY *pHash,
+ IN ULONG uColor,
+ IN ULONG uPawnFile,
+ IN COOR c,
+ IN BITBOARD bbBlockingRanks)
+/**
+
+Routine description:
+
+ Can a friendly pawn on uPawnFile ever defend the pawn on square c
+ -- either right now (already within defending range) or later
+ (nothing permanently blocks it from advancing into range)?
+
+ NOTE: on a doubled adjacent file, this only looks at whether *a*
+ helper pawn on uPawnFile can reach c -- if the nearest one's only
+ route there is an unsafe one-step push, this returns FALSE even
+ though a second, more distant pawn on the same file might still
+ reach c safely later. Rare (requires doubled pawns on the adjacent
+ file specifically) and consistent with the rest of this function's
+ approximate, snapshot-in-time nature.
+
+Parameters:
+
+ POSITION *pos
+ PAWN_HASH_ENTRY *pHash
+ ULONG uColor : color of the pawn on c (and of the potential helper)
+ ULONG uPawnFile : the adjacent file to test (0-7)
+ COOR c : the pawn being supported
+ BITBOARD bbBlockingRanks : c's rank plus every rank behind it, from
+ uColor's forward direction (see caller)
+
+Return value:
+
+ FLAG
+
+**/
+{
+ if (0 == pHash->uCountPerFile[uColor][uPawnFile + 1])
+ {
+ return FALSE; // no friendly pawn on that file at all
+ }
+ if (pos->bbPawns[uColor] & BBFILE[uPawnFile] & BBADJACENT_RANKS[RANK(c)])
+ {
+ return TRUE; // already defending
+ }
+
+ // A friendly pawn one step short of defending range would have to
+ // push onto its destination square to get there. If a live enemy
+ // pawn attacks that destination and no friendly pawn attack of
+ // our own covers it too (pos->bbPawnAttacks[] is already
+ // populated by _PopulatePawnAttackBits before either _EvalPawns
+ // pass runs, so both are real, current facts, not a guess about
+ // some future position), pushing there just loses the pawn for
+ // nothing -- not a safe way to arrive as a defender. If we also
+ // attack the destination, the push is at worst an even trade, so
+ // it doesn't disqualify this file. WHITE pushes toward decreasing
+ // bitboard index (see _PopulatePawnAttackBits), so a pawn one
+ // push short of the defending band sits at (band << 8), and its
+ // destination is (that square >> 8) -- i.e. the original band
+ // itself; BLACK is mirrored.
+ {
+ BITBOARD bbOneStepShort = (uColor == WHITE) ?
+ (BBADJACENT_RANKS[RANK(c)] << 8) :
+ (BBADJACENT_RANKS[RANK(c)] >> 8);
+ BITBOARD bbHelper = pos->bbPawns[uColor] & BBFILE[uPawnFile] & bbOneStepShort;
+ if (bbHelper)
+ {
+ BITBOARD bbDestination = (uColor == WHITE) ?
+ (bbHelper >> 8) : (bbHelper << 8);
+ if ((bbDestination & pos->bbPawnAttacks[FLIP(uColor)]) &&
+ !(bbDestination & pos->bbPawnAttacks[uColor]))
+ {
+ return FALSE;
+ }
+ }
+ }
+
+ // A friendly pawn is on the file but not yet close enough to
+ // defend -- it can still march up and defend later, unless an
+ // enemy pawn on this same file already sits at c's rank or behind
+ // it (from our own forward direction), permanently blocking the
+ // way.
+ return (0 == (pos->bbPawns[FLIP(uColor)] & BBFILE[uPawnFile] & bbBlockingRanks));
+}
+
+
+static INLINE FLAG
+_IsBackwardPawn(IN POSITION *pos,
+ IN ULONG uColor,
+ IN COOR c,
+ IN COOR cSquare)
+/**
+
+Routine description:
+
+ Is the pawn on square c "backward"? See "Pawn Power in Chess"
+ pp. 25-27. The caller has already established the one condition
+ that needs pos->bbPawnAttacks[]: the enemy, not a friendly pawn,
+ controls c's stop square (cSquare). This checks the three
+ remaining, independently-necessary conditions:
+
+ 1. cSquare isn't itself occupied by a pawn (either color) --
+ otherwise this is a blocked/rammed pawn, not a backward one.
+ 2. c has advanced far enough for the classification to apply.
+ 3. Neither neighbor on an adjacent file, one rank ahead of c,
+ is a friendly pawn -- i.e. our neighbors have already
+ advanced past c, so they can no longer shield it. (0x88
+ coordinates make cSquare +/- 1 safe to test directly:
+ IS_ON_BOARD() catches the file-edge case for free, unlike a
+ bitboard shift which would need explicit BBFILE[0]/BBFILE[7]
+ masking.)
+
+Parameters:
+
+ POSITION *pos
+ ULONG uColor : color of the pawn on c
+ COOR c : the pawn being tested
+ COOR cSquare : c's stop square (one square ahead of c)
+
+Return value:
+
+ FLAG
+
+**/
+{
+ PIECE p;
+
+ if (IS_PAWN(pos->rgSquare[cSquare].pPiece))
+ {
+ return FALSE; // stop square is blocked by a pawn
+ }
+ if (!(((WHITE == uColor) && (RANK(c) < 4)) ||
+ ((BLACK == uColor) && (RANK(c) > 5))))
+ {
+ return FALSE; // not advanced enough yet
+ }
+
+ p = (BLACK_PAWN | uColor); // uColor's own pawn, regardless of color
+ return ((IS_ON_BOARD(cSquare - 1) && (pos->rgSquare[cSquare - 1].pPiece == p)) ||
+ (IS_ON_BOARD(cSquare + 1) && (pos->rgSquare[cSquare + 1].pPiece == p)));
+}
+
+
static PAWN_HASH_ENTRY *
_EvalPawns(IN OUT SEARCHER_THREAD_CONTEXT *ctx,
OUT FLAG *pfDeferred)
@@ -1989,22 +2146,19 @@ Return value:
POSITION *pos = &ctx->sPosition;
PAWN_HASH_ENTRY *pHash;
COOR c, cSquare;
- PIECE p;
ULONG uIsolated[2];
ULONG uDoubled[2];
SCORE iDuos[2];
ULONG u, uPawnFile;
ULONG uColor;
- ULONG uUnsupportable;
BITBOARD bb;
+ BITBOARD bbBlockingRanks;
int d1;
#ifdef DEBUG
SCORE t;
#endif
- //
- // Now, look up the hash entry for this position.
- //
+ // Look up the hash entry for this position.
INC(ctx->sCounters.pawnhash.u64Probes);
pHash = PawnHashLookup(ctx);
ASSERT(NULL != pHash);
@@ -2015,11 +2169,9 @@ Return value:
return(pHash);
}
- //
// We need the attack table to evaluate the pawns... and we missed
// the hash table so we have to populate it now. Can't defer this
- // one. :(
- //
+ // one.
*pfDeferred = FALSE;
_InitializePawnHashEntry(pHash, pos);
_PopulatePawnAttackBits(pos);
@@ -2027,201 +2179,102 @@ Return value:
uDoubled[BLACK] = uDoubled[WHITE] = 0;
iDuos[BLACK] = iDuos[WHITE] = 0;
- //
// First pass -- everything here except the per-square centrality
// bonus is a closed-form aggregate over pos->bbPawns[uColor]
// (uCountPerFile, uNumUnmovedPawns, bbStationaryPawns/
// uNumRammedPawns), so none of it needs a per-pawn mailbox walk;
// only the centrality lookup genuinely needs one (it's a per-square
- // table, not summable any other way). Replaces the old single loop
- // over pos->cPawns[uColor][] that did all of these one pawn at a
- // time, including a "counted rammed pawns twice, divide by 2 to fix
- // it" hack that a closed-form popcount doesn't need in the first
- // place (see below).
- //
- {
- BITBOARD bbAllPawns = pos->bbPawns[WHITE] | pos->bbPawns[BLACK];
+ // table, not summable any other way).
+ BITBOARD bbAllPawns = pos->bbPawns[WHITE] | pos->bbPawns[BLACK];
- FOREACH_COLOR(uColor)
- {
- ASSERT(IS_VALID_COLOR(uColor));
+ FOREACH_COLOR(uColor)
+ {
+ ASSERT(IS_VALID_COLOR(uColor));
- for (u = A; u <= H; u++)
- {
- pHash->uCountPerFile[uColor][u + 1] =
- CountBits(pos->bbPawns[uColor] & BBFILE[u]);
- ASSERT(pHash->uCountPerFile[uColor][u + 1] < 6);
- }
+ // Pre-count number of pawns per file, unrolled.
+ ASSERT(pHash->uCountPerFile[uColor][0] == 0);
+ pHash->uCountPerFile[uColor][A + 1] = CountBits(pos->bbPawns[uColor] & BBFILE[A]);
+ pHash->uCountPerFile[uColor][B + 1] = CountBits(pos->bbPawns[uColor] & BBFILE[B]);
+ pHash->uCountPerFile[uColor][C + 1] = CountBits(pos->bbPawns[uColor] & BBFILE[C]);
+ pHash->uCountPerFile[uColor][D + 1] = CountBits(pos->bbPawns[uColor] & BBFILE[D]);
+ pHash->uCountPerFile[uColor][E + 1] = CountBits(pos->bbPawns[uColor] & BBFILE[E]);
+ pHash->uCountPerFile[uColor][F + 1] = CountBits(pos->bbPawns[uColor] & BBFILE[F]);
+ pHash->uCountPerFile[uColor][G + 1] = CountBits(pos->bbPawns[uColor] & BBFILE[G]);
+ pHash->uCountPerFile[uColor][H + 1] = CountBits(pos->bbPawns[uColor] & BBFILE[H]);
+ ASSERT(pHash->uCountPerFile[uColor][9] == 0);
- pHash->uNumUnmovedPawns[uColor] =
- CountBits(pos->bbPawns[uColor] &
- BBRANK[RANK(uUnmovedRank[uColor])]);
- ASSERT(pHash->uNumUnmovedPawns[uColor] <= 8);
+ // Count number of unmoved pawns.
+ pHash->uNumUnmovedPawns[uColor] = CountBits(pos->bbPawns[uColor] &
+ BBRANK[RANK(uUnmovedRank[uColor])]);
+ ASSERT(pHash->uNumUnmovedPawns[uColor] <= 8);
- //
- // Detect rammed and other stationary pawns. A pawn is
- // "stationary" if *any* pawn (either color -- e.g. a
- // doubled own-pawn block counts too) sits on its stop
- // square; it's specifically "rammed" only when that
- // blocker is the enemy's. WHITE's stop square is one rank
- // toward rank8 (bit index -8 in the linear 0-63 bitboard
- // numbering -- see _PopulatePawnAttackBits' >>8/<<8 shifts
- // for the same convention), BLACK's is +8.
- //
- pHash->bbStationaryPawns[uColor] =
- (uColor == WHITE) ?
- (pos->bbPawns[WHITE] & (bbAllPawns << 8)) :
- (pos->bbPawns[BLACK] & (bbAllPawns >> 8));
- ASSERT(CountBits(pHash->bbStationaryPawns[uColor]) <= 8);
+ // Detect rammed and other stationary pawns. A pawn is
+ // "stationary" if *any* pawn (either color -- e.g. a doubled
+ // own-pawn block counts too) sits on its "stop square". It's
+ // specifically "rammed" only when that blocker is the
+ // enemy's. WHITE's stop square is one rank toward rank8 (bit
+ // index -8 in the linear 0-63 bitboard numbering -- see
+ // _PopulatePawnAttackBits' >>8/<<8 shifts for the same
+ // convention), BLACK's is +8.
+ pHash->bbStationaryPawns[uColor] =
+ (uColor == WHITE) ?
+ (pos->bbPawns[WHITE] & (bbAllPawns << 8)) :
+ (pos->bbPawns[BLACK] & (bbAllPawns >> 8));
+ ASSERT(CountBits(pHash->bbStationaryPawns[uColor]) <= 8);
- //
- // Give central pawns a small bonus; penalize rook pawns.
- // This is the one term above that's genuinely per-square
- // and can't be folded into a popcount.
- //
- bb = pos->bbPawns[uColor];
- while(IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb)))
- {
- EVAL_TERM(uColor,
- PAWN,
- c,
- pHash->iScore[uColor],
- PAWN_CENTRALITY_BONUS[c],
- "centrality");
- }
+ // Give central pawns a small bonus and rook pawns a small
+ // penalty.
+ bb = pos->bbPawns[uColor];
+ while(IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb)))
+ {
+ EVAL_TERM(uColor,
+ PAWN,
+ c,
+ pHash->iScore[uColor],
+ PAWN_CENTRALITY_BONUS[c],
+ "location");
}
-
- //
- // Rammed pairs (an enemy pawn, not just any pawn, on the stop
- // square): each pair shows up as exactly one bit in each
- // side's own stationary-pawn count, so no double-counting
- // (and no /2 fixup) is needed the way the old per-pawn,
- // per-color loop required.
- //
- pHash->uNumRammedPawns =
- CountBits(pos->bbPawns[WHITE] & (pos->bbPawns[BLACK] << 8));
- ASSERT(pHash->uNumRammedPawns <= 8);
}
- //
- // Second pass
- //
+ // Rammed pairs (an enemy pawn, not just any pawn, on the stop
+ // square): each pair shows up as exactly one bit in each
+ // side's own stationary-pawn count, so no double-counting
+ // (and no /2 fixup) is needed the way the old per-pawn,
+ // per-color loop required.
+ pHash->uNumRammedPawns =
+ CountBits(pos->bbPawns[WHITE] & (pos->bbPawns[BLACK] << 8));
+ ASSERT(pHash->uNumRammedPawns <= 8);
+
+ // Second pass, requires pHash->uCountPerFile[] populated for both
+ // colors.
FOREACH_COLOR(uColor)
{
ASSERT(IS_VALID_COLOR(uColor));
+ ULONG xColor = FLIP(uColor);
+ ASSERT(IS_VALID_COLOR(xColor));
ASSERT(CountBits(pos->bbPawns[uColor]) <= 8);
d1 = 16 * g_iAhead[uColor];
- {
- BITBOARD bbPawnsToWalk = pos->bbPawns[uColor];
+ BITBOARD bbPawnsToWalk = pos->bbPawns[uColor];
- while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bbPawnsToWalk)))
- {
- ASSERT(IS_ON_BOARD(c));
+ while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bbPawnsToWalk)))
+ {
ASSERT(IS_PAWN(pos->rgSquare[c].pPiece));
ASSERT((1 < RANK(c)) && (RANK(c) < 8));
- //
// Find weak pawns... pawns that are isolated are weak as
// are pawns that have advanced beyond their support.
// Both of these can tie up one or more pieces in order to
- // defend them.
- //
- uUnsupportable = 0;
- uPawnFile = FILE(c) - 1;
- if (pHash->uCountPerFile[uColor][uPawnFile + 1] > 0)
- {
- bb = (pos->bbPawns[uColor] &
- BBFILE[uPawnFile] &
- BBADJACENT_RANKS[RANK(c)]);
- if (!bb)
- {
- bb = (pos->bbPawns[FLIP(uColor)] &
- BBFILE[uPawnFile]);
- while(IS_ON_BOARD(cSquare =
- CoorFromBitBoardRank8ToRank1(&bb)))
- {
- ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece));
- if (uColor == WHITE)
- {
- if ((cSquare & 0xF0) >= (c & 0xF0))
- {
- ASSERT(RANK(cSquare) <= RANK(c));
- uUnsupportable = 1;
- break;
- }
- }
- else
- {
- ASSERT(uColor == BLACK);
- if ((cSquare & 0xF0) <= (c & 0xF0))
- {
- ASSERT(RANK(cSquare) >= RANK(c));
- uUnsupportable = 1;
- break;
- }
- }
- }
- }
- }
- else
- {
- uUnsupportable = 1; // no friend pawn on that file
- }
- if (0 == uUnsupportable)
- {
- goto fast_skip;
- }
- ASSERT(1 == uUnsupportable);
-
- uPawnFile = FILE(c) + 1;
- if (pHash->uCountPerFile[uColor][uPawnFile + 1] > 0)
- {
- bb = (pos->bbPawns[uColor] &
- BBFILE[uPawnFile] &
- BBADJACENT_RANKS[RANK(c)]);
- if (!bb)
- {
- bb = (pos->bbPawns[FLIP(uColor)] &
- BBFILE[uPawnFile]);
- while(IS_ON_BOARD(cSquare =
- CoorFromBitBoardRank8ToRank1(&bb)))
- {
- ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece));
- if (uColor == WHITE)
- {
- if ((cSquare & 0xF0) >= (c & 0xF0))
- {
- ASSERT(RANK(cSquare) <= RANK(c));
- uUnsupportable = 2;
- break;
- }
- }
- else
- {
- ASSERT(uColor == BLACK);
- if ((cSquare & 0xF0) <= (c & 0xF0))
- {
- ASSERT(RANK(cSquare) >= RANK(c));
- uUnsupportable = 2;
- break;
- }
- }
- }
- }
- }
- else
- {
- uUnsupportable = 2;
- }
-
- //
- // If this pawn is not supportable from either side then
- // it is either isolated or has been pushed too far ahead
- // of its support. Either way it's a target -- penalize
- // it.
- //
- if (2 == uUnsupportable)
+ // defend them. A pawn is a target unless a friendly pawn
+ // on *either* adjacent file can defend it, now or later
+ // (see _FileCanSupportPawn). bbBlockingRanks is c's rank
+ // plus every rank behind it (from uColor's forward
+ // direction) -- an enemy pawn there on an adjacent file
+ // permanently blocks that file from ever producing a
+ // defender.
+ bbBlockingRanks = BBRANK[RANK(c)] | BBPRECEEDING_RANKS[c >> 4][uColor];
+ if (!_FileCanSupportPawn(pos, pHash, uColor, FILE(c) - 1, c, bbBlockingRanks) &&
+ !_FileCanSupportPawn(pos, pHash, uColor, FILE(c) + 1, c, bbBlockingRanks))
{
//
// Single consolidated isolated-pawn penalty: base
@@ -2244,14 +2297,13 @@ Return value:
c,
pHash->iScore[uColor],
(ISOLATED_PAWN_BY_PAWNFILE[uPawnFile] +
- ((pHash->uCountPerFile[FLIP(uColor)][uPawnFile]==0) *
+ ((pHash->uCountPerFile[xColor][uPawnFile]==0) *
ISOLATED_EXPOSED_PAWN) +
((pHash->uCountPerFile[uColor][uPawnFile] > 1) *
ISOLATED_DOUBLED_PAWN)),
"isolated pawn");
}
- fast_skip:
uPawnFile = FILE(c) + 1;
ASSERT((1 <= uPawnFile) && (uPawnFile <= 8));
ASSERT(CountBits(pos->bbPawns[uColor] & BBFILE[FILE(c)])
@@ -2285,7 +2337,7 @@ Return value:
//
#ifdef DEBUG
t = ((uColor * RANK(cSquare)) +
- (FLIP(uColor) * (9 - RANK(cSquare))));
+ (xColor * (9 - RANK(cSquare))));
if (uColor == WHITE)
{
ASSERT(t == RANK(cSquare));
@@ -2297,54 +2349,41 @@ Return value:
}
#endif
iDuos[uColor] += ((uColor * RANK(cSquare)) +
- (FLIP(uColor) * (9 - RANK(cSquare))));
+ (xColor * (9 - RANK(cSquare))));
ASSERT(iDuos[uColor] <= (7 * 8));
}
- else if (pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare))
+ else if (pos->bbPawnAttacks[xColor] & COOR_TO_BB(cSquare))
{
//
// Detect backwards pawns. See "Pawn Power in Chess" pp 25-27
//
- if (!IS_PAWN(pos->rgSquare[cSquare].pPiece))
+ if (_IsBackwardPawn(pos, uColor, c, cSquare))
{
- p = (BLACK_PAWN | uColor);
- if (((WHITE == uColor) && (RANK(c) < 4)) ||
- ((BLACK == uColor) && (RANK(c) > 5)))
+ //
+ // The pawn at c is backward. Determine whether
+ // it is shielded or exposed.
+ //
+ pHash->bbStationaryPawns[uColor] |= COOR_TO_BB(c);
+ ASSERT(CountBits(pHash->bbStationaryPawns[uColor]) <= 8);
+ if (pHash->uCountPerFile[xColor][uPawnFile])
{
- if ((IS_ON_BOARD(cSquare - 1) &&
- pos->rgSquare[cSquare - 1].pPiece == p) ||
- (IS_ON_BOARD(cSquare + 1) &&
- pos->rgSquare[cSquare + 1].pPiece == p))
- {
- //
- // The pawn at c is backward. Determine
- // whether it is shielded or exposed.
- //
- pHash->bbStationaryPawns[uColor] |= COOR_TO_BB(c);
- ASSERT(CountBits(pHash->bbStationaryPawns[uColor])
- <= 8);
- if (pHash->uCountPerFile[FLIP(uColor)][uPawnFile])
- {
- // Tuning can (and has) flipped this sign; not a real invariant.
- //ASSERT(BACKWARD_SHIELDED_BY_LOCATION[c] < 0);
- EVAL_TERM(uColor,
- PAWN,
- c,
- pHash->iScore[uColor],
- BACKWARD_SHIELDED_BY_LOCATION[c],
- "shielded backward pawn");
- }
- else
- {
- //ASSERT(BACKWARD_EXPOSED_BY_LOCATION[c] < 0);
- EVAL_TERM(uColor,
- PAWN,
- c,
- pHash->iScore[uColor],
- BACKWARD_EXPOSED_BY_LOCATION[c],
- "exposed backward pawn");
- }
- }
+ ASSERT(BACKWARD_SHIELDED_BY_LOCATION[c] < 0);
+ EVAL_TERM(uColor,
+ PAWN,
+ c,
+ pHash->iScore[uColor],
+ BACKWARD_SHIELDED_BY_LOCATION[c],
+ "shielded backward pawn");
+ }
+ else
+ {
+ ASSERT(BACKWARD_EXPOSED_BY_LOCATION[c] < 0);
+ EVAL_TERM(uColor,
+ PAWN,
+ c,
+ pHash->iScore[uColor],
+ BACKWARD_EXPOSED_BY_LOCATION[c],
+ "exposed backward pawn");
}
}
//
@@ -2357,7 +2396,6 @@ Return value:
// Handle passers / candidate passers
//
_EvaluateCandidatePasser(pos, pHash, c);
- }
}
}
@@ -2412,13 +2450,6 @@ Return value:
ASSERT(uIsolated[WHITE] <= 8);
ASSERT(uIsolated[BLACK] >= 0);
ASSERT(uIsolated[BLACK] <= 8);
- //
- // 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
@@ -4572,22 +4603,17 @@ Return value:
BITBOARD bbFile = BBFILE[FILE(c)];
BITBOARD bbFriendPawns = pos->bbPawns[uColor] & bbFile;
BITBOARD bbEnemyPawns = pos->bbPawns[xColor] & bbFile;
- // Collapsed 2026-09-06: friend-passer bonus used to distinguish
- // rook-behind-passer (good, rank-scaled +1..+25) from rook-in-
- // front-of-passer (bad, -3..-22) -- now a single flat bonus
- // regardless of which side of the pawn the rook is on, by
- // direct instruction.
SCORE bonus = ROOK_FULL_HALF_OPEN_BONUS[bbFriendPawns != 0][bbEnemyPawns != 0];
bonus += (SCORE)((bbEnemyPawns & pHash->bbPasserLocations[xColor]) != 0) *
ROOK_WITH_ENEMY_PASSER;
bonus += (SCORE)((bbFriendPawns & pHash->bbPasserLocations[uColor]) != 0) *
ROOK_WITH_FRIEND_PASSER;
-
+ bonus += (SCORE)((COOR_TO_BB(c) & BBROOK_7TH_RANK[uColor]) != 0) *
+ ROOK_ON_SEVENTH_BONUS;
EVAL_TERM(uColor, ROOK, c, pos->iScore[uColor], bonus,
"file/passer bonus");
-
//
// Removed 2026-08-30 ("rook trapping enemy king"): a rook on the
// 7th/8th rank aligned with the enemy king is exactly the
diff --git a/src/recogn.c b/src/recogn.c
index c573f8f..7b7ba84 100644
--- a/src/recogn.c
+++ b/src/recogn.c
@@ -88,10 +88,7 @@ Return value:
return(x);
}
-
#ifdef DEBUG
-
-
static FLAG
_TablebasesSaySideWins(IN SEARCHER_THREAD_CONTEXT *ctx,
IN ULONG uSide)
@@ -136,7 +133,6 @@ _TablebasesSayDrawOrWin(IN SEARCHER_THREAD_CONTEXT *ctx,
return TRUE;
}
-
static FLAG
_SanityCheckRecognizers(IN SEARCHER_THREAD_CONTEXT *ctx,
IN SCORE iScore,
@@ -210,7 +206,6 @@ Parameters:
Return value:
- #ifdef DEBUG
static FLAG
**/
@@ -254,7 +249,7 @@ Return value:
**/
{
- *piScore = 0;
+ *piScore = g_iDrawScore[ctx->sPosition.uToMove];
return(RECOGN_EXACT);
}
@@ -300,7 +295,7 @@ Return value:
if ((CORNER_DISTANCE(KingPosition(pos, WHITE)) != 0) &&
(CORNER_DISTANCE(KingPosition(pos, BLACK)) != 0))
{
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
return(RECOGN_EXACT);
}
}
@@ -335,7 +330,7 @@ Return value:
(WhiteSqBishopCount(pos, uStrong) == 0) ||
(WhiteSqBishopCount(pos, uStrong) == uBishops))
{
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
return(RECOGN_EXACT);
}
@@ -441,7 +436,7 @@ Return value:
if ((CORNER_DISTANCE(KingPosition(pos, WHITE)) != 0) &&
(CORNER_DISTANCE(KingPosition(pos, BLACK)) != 0))
{
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
return(RECOGN_EXACT);
}
return(UNRECOGNIZED);
@@ -478,7 +473,7 @@ Return value:
{
return(UNRECOGNIZED);
}
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
return(RECOGN_EXACT);
}
@@ -546,7 +541,7 @@ Return value:
{
return(UNRECOGNIZED);
}
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
return(RECOGN_EXACT);
}
@@ -715,7 +710,7 @@ Return value:
// This is at least a draw for the side with the pawn and at best a
// draw for the side with the knight
//
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
if (pos->uToMove == uStrong)
{
return(RECOGN_UPPER);
@@ -862,7 +857,7 @@ Return value:
#endif
at_best_draw_for_strong:
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
if (pos->uToMove == uStrong)
{
return(RECOGN_UPPER);
@@ -870,7 +865,7 @@ Return value:
return(RECOGN_LOWER);
draw:
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
return(RECOGN_EXACT);
}
@@ -1086,7 +1081,7 @@ Return value:
if (FastCountBits(pos->bbPawns[uStrong]) > 1)
{
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
if (pos->uToMove == uStrong)
{
return(RECOGN_LOWER);
@@ -1193,7 +1188,7 @@ Return value:
// strong side but we don't know -- the logic is ugly. This is
// still at best drawn for the weak side though.
//
- *piScore = 0;
+ *piScore = g_iDrawScore[pos->uToMove];
if (pos->uToMove == uStrong)
{
return(RECOGN_LOWER);
diff --git a/tests/ECM.md b/tests/ECM.md
index 64b4fa5..62c5bd4 100644
--- a/tests/ECM.md
+++ b/tests/ECM.md
@@ -82,6 +82,8 @@ session).
| ECM.875 | hard_quick | `2kr1br1/ppp2p1p/2n1p1q1/2Nn4/3P4/1Q2PB2/PP1B1P1P/2R1K2R b K - 0 0` | Qg1+ | Score exactly `0.00` on a repetition line (`<=>`) across 6 straight iterations (depths 8-13) -- textbook flaky signature. Contradicts the "Gains seen" table's loose note calling this Crafty-confirmed legit in an earlier review; that note appears to have been wrong, or about a different search reaching a different (non-drawing) conclusion. Gained by the candidate LMR-graded-curve build (2026-09-11) |
| ECM.879 | hard_quick | `r2r2k1/pp3ppp/2q5/2p5/2P1P1n1/P4P1b/1BQ1BP1P/R3R2K b - - 0 0` | Rd2 | Score exactly `0.00` on a repetition line (`<=>`) -- same signature as ECM.875. Gained by the candidate LMR-graded-curve build (2026-09-11) |
| ECM.866 | hard_quick | `7R/1p1r4/2b2p2/2Pp1qk1/3P1bp1/3N4/2Q2PB1/6K1 w - -` | Rg8+ | HEAD's own score is negative (-0.12 to -0.15); Crafty's independent top choice (Qd1) is also near-zero. **Note**: has shown up as a *gain* (candidate solves it, HEAD doesn't) in at least one later run too -- consistent with a genuinely coin-flip position, not a stable read either direction |
+| ECM.360 | hard_quick | `2r3k1/p1r1ppb1/2bp1np1/q5N1/1p1BP2Q/3P4/PP2N1PP/2R2R1K w - -` | Bxf6 | Wobbles Ra1 -> Bxf6 (depth 10, matching "solution") -> Ra1 (depth 11) -> Ra1 `++`-confirmed (depth 12) -> Nf4 (depth 12, second entry) -> Nf4 stable through depth 15. A separate `go` run at a 30s time control (depths 17-22) settles firmly and stably on Nf4, never touching Bxf6 -- the sd10 gate just sampled the one brief window sitting on the "solution" move mid-wobble. Gained by the pawn-eval isolation/backward-pawn session (2026-09-19) but not a real solve |
+| ECM.420 | hard_quick | `3rr1k1/1p1qb2p/pP4p1/2p2p2/4RQ2/3P4/1PPB2PP/4R1K1 w - -` | Bc3 | Wobbles Re6 (depths 8-10 first entry) -> Bc3 (depth 10 second entry, matching "solution", through depth 12 first entry) -> Re6 (depth 12 second entry) -> Re6 stable through depth 15. A deeper run (depths 16-18) confirms Re6 firmly, with `Bc3+` only ever appearing a few plies into the Re6 line (as a follow-up check: `...Rxe8+ Qxe8 Qxc5 Bxc3...`), not as the intended root move. Gained by the pawn-eval isolation/backward-pawn session (2026-09-19) but not a real solve |
| ECM.357 | confident_quick | `r3r1k1/4pp1p/b5pB/q1pP4/3b4/2N4P/PP4P1/R2RQ2K w - - 0 0` | Be3 | Abandons a `++`-confirmed Be3 (depth 10, +0.70) for an unconfirmed Rxd4 (+0.80) at the very last iteration -- textbook flaky signature. Lost by the f698c02 eval-margin-redesign build (2026-09-18) |
| ECM.717 | ringers | `r1qb1r1k/2p3pp/p1n1bp2/1p1Np2Q/P3P3/1BP3R1/1P3PPP/R1B3K1 w - - 0 0` | axb5 | Abandons a `++`-confirmed axb5 (depth 10, +3.16) for an unconfirmed Rxg7 (+3.17) at the very last iteration -- same shape as ECM.357. Lost by the f698c02 eval-margin-redesign build (2026-09-18) |
| ECM.601 | hard_quick | `8/p5k1/2p2p2/4p1p1/1p2P3/1P4PP/1PP5/5K2 b - - 0 0` | g4 | No iteration lines printed at all before the depth limit; final score a dead-equal +0.07 -- no confirmation of any kind. Lost by the f698c02 eval-margin-redesign build (2026-09-18) |