| Age | Commit message (Collapse) | Author |
|
Brings in the last remaining piece from stash@{0}: the super-lazy exit
point (material-only pre-check before the regular lazy gate), a
material-bucket floor under the regular lazy exit's margin
(iSwingFloorByArmy), and search.c's qsearch futility rework
(FUTILITY_BASE_MARGIN_BY_SOURCE, indexed by which Eval() exit tier
produced the score). Required Eval()'s signature change from a single
SCORE* to SCORE(*)[2] (positional estimate per side instead of one
munged magnitude) -- search.c's futility margin folds in
rgiPositional[pos->uToMove], which the earlier bad-trades investigation
found to be a meaningfully predictive signal.
search.c and chess.h brought in wholesale from the stash (both were
either completely untouched by prior commits or contained no
divergence worth preserving). eval.c required hand-merging on top of
this session's already-applied bad-trades fix, B-over-N removal, and
xColor/reorder cleanups -- ported the super-lazy exit block, the
regular-lazy material floor, the per-color piPositional writes (all
three exit sites: super-lazy, regular-lazy x2, full-eval), the
super-lazy calibration harness (RecordSuperLazyMarginSafetySwing,
dual-regime DumpMarginSafetyCalibration), and moved uArmyScaler/
uNumTrapped initialization to match the new ordering the super-lazy
exit depends on.
Verified: clean release + DEBUG build (only the previously-flagged
_EvalTrappedPieces warning), DEBUG smoke test pass, and a 40-game
st1 match against clean 434fa04 (score 0.487, llr -0.04) -- landing
this margin machinery as-is from the stash, before any retuning, does
not regress strength on its own. This confirms the original
regression (0.15-0.225 score seen early in this investigation) was
fully explained by the bad-trades unsigned-underflow bug, not by these
margins being unsound.
Values are the original, as-derived-from-calibration ones (see
inline comments: SUPER_LAZY_MARGIN_BY_ARMY from 100 positions/sd8
with ~15-20% headroom, iSwingFloorByArmy from 1500 positions/sd8
with ~25% headroom, FUTILITY_BASE_MARGIN_BY_SOURCE from a separate
1500-position/sd6 surprise-rate calibration). Not yet retuned --
suspected to carry more headroom than necessary, which costs real
search speed (speed=depth). Next step: re-run the margin-safety
calibration fresh against the current build and trim the super-lazy
and regular-lazy floor headroom down from the built-in database,
leaving the qsearch futility margins alone (different calibration
method, already risk-tolerant by construction).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ka9o3S2eKqh4jNfmxVZ6fH
|
|
reorder
Continues re-applying the eval.c overhaul from stash@{0} (see fbb138c),
each piece verified individually against clean 434fa04 (fast st1 matches,
~30-40 games each) before landing:
- _EvalBadTrades: replaced the TRADE_PIECES/DONT_TRADE_PAWNS lookup
tables with direct arithmetic (part of a broader "eval.c is too slow,
cut lookup tables where possible" effort). The stash's first attempt
at this had a real bug: uInverseBehindPieceCount was computed by
subtracting a raw material sum from 9 in unsigned arithmetic, which
underflows to ~4 billion in any non-bare-endgame position with
unequal material -- confirmed via match_play (score 0.15-0.225 over
20 games vs clean 434fa04, a near-total wipeout). Fixed to use the
existing uNonPawnCount piece-count field directly instead of
reinventing it via material math, and restored a cheap zero-pawn
guard (old DONT_TRADE_PAWNS punished "material lead with zero pawns
left" specifically -- the classic KNB-vs-KRP false positive -- which
the arithmetic replacement had dropped entirely). Verified back at
parity (0.475/40 games) after the fix.
- _EvalRook: ROOK_FULL_HALF_OPEN_BONUS is now a startup-computed cache
(InitEval(), refreshed on every DNA reload) instead of a per-call
local array rebuild.
- xColor = FLIP(uColor) cached once instead of recomputed inline:
_EvalBishop, _EvalKnight, CountKingSafetyDefects (also renamed its
uSide/xSide params to uColor/xColor to match), and Eval()'s own
per-piece-type loop.
- _EvalBishopPairs collapsed to one FOREACH_COLOR loop instead of
duplicated per-side code.
- eval.c "diet" cleanup: removed pos->iTempScore (a scalar handoff
between _EvalKing and Eval()'s per-color copy, now redundant --
_EvalKing writes directly into ctx->sPlyInfo[ply].iKingScore[uColor]);
removed several stale comments documenting already-historical
removals; _EvalPassers renamed to _ReEvalPassers; re-enabled a
previously-disabled ASSERT in _EvaluateCandidatePasser (confirmed
live via DEBUG smoke test, does not fire under current DNA).
- Added _SideHasWinningChances (Crafty-style material-only "can this
side force a win at all" classifier) plus the fifty-move-rule
dampening in Eval() -- both scale the score toward g_iDrawScore when
material alone rules out real winning chances, without touching
Eval()'s signature (kept the existing scalar piPositional convention
rather than pulling in the super-lazy exit's per-color rework).
- Removed the "B over N in the endgame with 2 pawn wings" term
entirely, by direct instruction -- BISHOP_OVER_KNIGHT_IN_ENDGAME is
now an orphaned DNA constant (matches the stash's own choice, which
left it similarly unused).
- Eval()'s main per-piece-type loop reordered from "all of our minors,
then all of the enemy's minors, then rooks both colors, then queens
both colors" to "ours then enemy's, interleaved per type" (knights,
then bishops, then rooks, then queens). Verified the real dependency
this ordering has to preserve -- _EvalRook/_EvalQueen read the
enemy's bbMinorAttacks, _EvalKing reads both colors' bbMinorAttacks/
bbQueenAttacks, _ReEvalPassers needs the king scores -- and confirmed
the new interleaving still satisfies it (all minors both colors done
before any rook/queen; both colors done before king).
Verification methodology throughout: gmake clean release build, DEBUG
smoke test (10 random ECM positions at sd 5, asserts compiled in), then
a fast (st 1, 40-game) match_play.py run against a clean 434fa04
reference binary before landing each piece. A same-binary-vs-itself
control match (score 0.500/20 games) confirms the harness itself has no
structural bias, so per-checkpoint scores in the 0.44-0.58 band across
this session reflect real (if noisy) parity, not measurement artifacts.
Still not re-applied (remains in stash@{0}): the super-lazy exit, the
material-based normal-lazy floor, and search.c's qsearch futility
rework -- these three are one coupled unit (the futility margins index
by which Eval() exit tier fired) and go in as the next, more carefully
scrutinized step, since the original regression that started this
whole investigation traced to exactly this area.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ka9o3S2eKqh4jNfmxVZ6fH
|
|
Confirmed self-play regression traced to a stale test_vs_head.sh reference
binary (typhoon_allbitboards/550ea81, deleted): every "vs head" comparison
since 92fc412 (Sep 4) was checking new work against that fixed Sep-4
snapshot, never against real HEAD or the working tree. Rebuilt clean
reference binaries directly from git and re-verified everything from
scratch.
This commit lands only the pieces confirmed safe against clean 434fa04
(fast st1 match, ~30-40 games, score ~0.44-0.55, consistent with parity;
plus a DEBUG-build smoke test pass):
- recogn.c, fen.c: real bugfixes
- data.c, draw.c, ics.c: whitespace only
- x64.asm: retires the asm GetAttacks implementation now that chess.h's
GetAttacks macro unconditionally selects the already-verified-faster
_GetAttacksBB bitboard version instead of a three-way build-flag
toggle (GETATTACKS_BITBOARD/CROUTINES/asm default)
- see.c, testsee.c: SEE/test-harness updates supporting that default
- root.c: per-tier eval-exit reporting (super-lazy counters currently
always read 0 -- accurate, since no super-lazy exit exists yet)
- main.c: startup banner update, InitEval() call, TestRecogn() added to
the #ifdef TEST self-test sequence
- command.c: InitEval() DNA-reload hook, new qsearchfutility diagnostic
- dynamic.c: minor changes
- chess.h: the GetAttacks default change above, three
FUTILITY_BASE_MARGIN_* compatibility aliases (all still equal to the
original flat FUTILITY_BASE_MARGIN -- search.c has not been split into
per-tier margins here), placeholder super-lazy counters, and an
EvalPasserRaces -> _EvalPasserRacesAgainstLoneKings rename (confirmed
byte-identical body) to match recogn.c's call site
- eval.c: the same rename, plus a no-op InitEval() stub (nothing to
initialize until the ROOK_FULL_HALF_OPEN_BONUS cache below exists)
Deliberately NOT included: the full eval.c overhaul (~1770 lines) and
search.c's qsearch-futility rework (~650 lines), including yesterday's
loosened SUPER_LAZY_MARGIN_BY_ARMY/FUTILITY_BASE_MARGIN_BY_SOURCE tables.
Reverting just those two tables while keeping the rest of the eval.c
overhaul still lost badly to 434fa04 (0.20 over 10 games), so the
regression isn't fully explained by the margins alone -- the eval.c
overhaul needs careful, incremental re-verification against this commit
as the new baseline, not a bulk re-apply. Full original work preserved in
git stash (stash@{0} as of this commit) for that follow-up.
Note: two pre-existing, position/state-dependent assertion crashes were
found during this verification (util.c:1093 WalkPV, recogn.c:1359
_SanityCheckRecognizers), both reproducing on unmodified 434fa04 -- not
introduced by anything here, not yet root-caused.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ka9o3S2eKqh4jNfmxVZ6fH
|
|
Recalibrate iKingSwingP90 against the bitboard-rewritten
CountKingSafetyDefects (~1.28B samples via new CALIBRATE_POSITIONAL/
CALIBRATE_BASE_MARGIN/CALIBRATE_MARGIN_SAFETY diagnostic build flags,
board_representation/EVAL.md section 9). Add LAZY_EVAL_MIN_MATERIAL:
measured the regular lazy exit's real swing exceeding its own assumed
margin 20.6% of the time in near-bare-king endgames (vs <=0.36%
elsewhere) -- skip lazy eval entirely below that material floor.
Double the stale search.c/searchsup.c CountKingSafetyDefects extension
thresholds as a stopgap pending their own recalibration.
Eval hot-path trimming (measured via EVAL_TIME, ~1759 -> ~1386 avg
cycles/eval on a representative middlegame position):
- Pull _GetFileStormDefects out of EstimatePositionalScore's hot path
(cost more than the "cheap cached lookup" it was assumed to be,
running on ~90% of all Eval() calls).
- Add pos->bbOccupiedSide[2], incrementally maintained alongside
bbOccupied, so _BuildFriendlySideBB is a field read instead of a
6-term OR.
- Switch CoorFromBitBoardRank8ToRank1/Rank1ToRank8 to the existing
static-inline FastFirstBit/FastLastBit (same bsf/bsr instruction,
no call/ret overhead).
- Defer EvalPasserRaces' uRacerDist/fDontCountMeOut past its
no-passer early return.
- Remove the mailbox-era "max mobility in a row" term from
_EvalBishop/_EvalRook (no bitboard-mobility equivalent need for it).
- Simplify _EvalBishopPairs and rook file-openness/passer bonuses to
flat DNA-tunable constants instead of distance/pawn-count-scaled
tables, rook file-openness now a branchless bitboard-indexed lookup.
- Remove pos->cPiece (write-only, no reader anywhere).
- Collapse WHITE/BLACK mirror-branches (castle-rights block,
rook-trapped-in-corner) to color-indexed constants.
- Close the PAWN_BIT..KING_BIT gap (bits 7-3 -> bits 4-0), removing
the bvPattern >>= 3 before its KING_COUNTER_BY_ATTACK_PATTERN
lookup. This also fixes a real bug introduced earlier this session
when _WhoControlsSquareFast was converted to read these constants
directly: g_SwapTable is only [32][32], but the old bit values
(up to 0xF8) indexed far out of bounds on any attacked square --
data.c's InitializeSwapTable was always built assuming the bits
0-4 range this change now actually produces.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XxmVi2sTMwpPp4i6WYFjan
|
|
Now that king (the last piece writing it) has converted to a bitboard
accumulator, nothing writes rgSquare[c|8].bvAttacks any more -- deletes
the whole mechanism rather than leaving a known-dead struct around:
- chess.h: ATTACK_BITV union gone. SQUARE collapses from a
union-with-ATTACK_BITV to a plain {pPiece, uIndex} struct (the #pragma
pack(1) that only existed for ATTACK_BITV's bitfield layout goes too).
UNSAFE_FOR_ROOK/UNSAFE_FOR_QUEEN and the whole-word MINOR_XRAY_BIT/
ROOK_XRAY_BIT/QUEEN_XRAY_BIT constants deleted -- confirmed unused
(only ever referenced in stale comments, not code) now that every
consumer reads bbXAttacks bitboards directly. PAWN_BIT/MINOR_BIT/
ROOK_BIT/QUEEN_BIT/KING_BIT stay: they're a separate, still-live
local bit-packing scheme _EvalKing/_WhoControlsSquareFast use to
build a per-square attack-pattern index into KING_COUNTER_BY_
ATTACK_PATTERN/g_SwapTable, unrelated to the retired storage struct.
- eval.c: _ClearAttackTables drops its entire macro-unrolled,
128-square clearing loop (CLEAR_A_SQ/CLEAR_A_RANK/CLEAR_SHORT_RANK,
all deleted) -- it only ever existed to zero the old per-square
struct; clearing the 7 bbXAttacks accumulators is the whole function
now. The transitional _IsSquareAttackedByX/_IsSquareXrayedByX helpers
(minor/rook/queen/king, 8 functions total) are deleted outright, not
just simplified -- their only remaining purpose was bridging to the
now-gone struct, and their DEBUG cross-checks were explicitly
migration-only scaffolding, not a permanent invariant. Call sites
(_WhoControlsSquareFast, _EvalKing's bvAttack/bvXray/bvDefend) read
the bbXAttacks bitboards directly instead. _WhoControlsSquareFast
simplifies to a flat OR of 8 bitboard membership tests per color,
down from raw struct reads plus 7 helper calls each.
Found and fixed one real, pre-existing bug while doing this (flagged
and confirmed with the user before touching it, kept as its own
documented change rather than silently folded into the mechanical
rename): _EvaluateCandidatePasser's helper-pawn-safety gate read
rgSquare[c1+8].bvAttacks[...].uWholeThing, but this function runs from
_EvalPawns -- the first piece type Eval() evaluates each call, before
any non-pawn piece (or, since commit 57502d6 retired pawns' own
bvAttacks write, even pawns) has written anything there. That word has
therefore been unconditionally zero, and the gate unconditionally true
(a silent no-op), since 57502d6 landed -- not something today's cleanup
introduced. Left exactly as dead/unconditional (deleted the
now-meaningless condition, kept the body it always ran anyway) rather
than fixed, since a real fix changes eval scoring and deserves its own
before/after check, documented inline for a future session.
Verification: precommit_check.sh (self-test + DEBUG smoke test) passes.
tests/ecm_ringers.ep_ at sd10 vs. the immediately preceding commit:
solve parity holds exactly (10/11 both), and final (depth-10) node
counts are byte-identical for all 11 positions -- the bar for a change
meant to be purely mechanical, unlike king's own conversion. One
harmless artifact noted: ECM.750 has a different depth-6 *intermediate*
best move (a shallow tie-break flip) that already resolves to the
identical PV and node count by depth 7 and holds through depth 10 --
not chased further since the actual (depth-10) result matches exactly
and search is deterministic at --cpus 1.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XxmVi2sTMwpPp4i6WYFjan
|
|
asymmetry
_EvalKing was the last piece type contributing to the old bvAttacks/
ATTACK_BITV mechanism -- both write sites (the low-enemy-material
early-exit path and the main king-safety loop) replaced with a single
pos->bbKingAttacks[uColor] |= g_KingAttacksBB[c], plus a new
_IsSquareAttackedByKing transitional helper (DEBUG-cross-checked, no
x-ray companion needed since a king can't move through a blocker) for
_WhoControlsSquareFast and _EvalKing's own bvDefend to read.
Two real, intentional behavior changes land with this conversion,
both discussed and confirmed before coding rather than assumed:
1. The main king-safety loop's old per-square write used
KingSafetyDeltas (11 entries: the 8 real king-move squares plus -2/
+2, two squares away on the same rank, present only for that loop's
own file-distance bookkeeping) instead of the real 8-square king
move pattern -- a bug, confirmed by direct instruction, not a
design choice worth preserving. Fixed by writing the real
g_KingAttacksBB[c] pattern once, before the loop, instead of
whatever KingSafetyDeltas happened to visit per-square.
2. _EvalKing's own bvAttack computation (does the *enemy* king attack
a square near this king?) is now symmetric where it used to be an
accidental artifact of evaluation order: kings are evaluated
black-then-white, so the old mailbox code let white's computation
see black's already-written king bit while black's could never see
white's (white hadn't run yet). Rather than preserve or upgrade
that asymmetry now that both colors go through an explicit helper
either way, neither side sees the enemy king as a threat here,
matching the side that already couldn't -- by direct instruction.
This is narrow in practice (two kings can never be legally
adjacent, so it only ever fires at king-vs-king distance 2) but
real.
_WhoControlsSquareFast's own king contribution is unaffected by either
change and stays fully symmetric: it's only ever called after *both*
kings finish evaluating (the passed-pawn re-check and trapped-piece/
danger passes all run after Eval()'s king-eval block), so
pos->bbKingAttacks is always fully populated for both colors by the
time it runs -- confirmed by checking call-site ordering directly, not
assumed from bvAttack's own (different) situation.
CountKingSafetyDefects is untouched by this change (confirmed by
reading it again): it's pure CHECK_VECTOR geometry over piece
locations, computed before any bbXAttacks accumulator exists this
Eval() call, and reads none of them. Its correlation with _EvalKing's
score is therefore preserved by construction, not something requiring
separate re-tuning.
Verification: since this bundles three attributable behavior changes
(the bitboard rewrite itself, the KingSafetyDeltas bug fix, and the
dropped bvAttack asymmetry), used the ringers-suite-plus-bounded-delta
bar from bishop's conversion rather than expecting exact node-count
equality: precommit_check.sh (self-test + DEBUG smoke test, plus
several hand-built king-adjacency/king-proximity positions run
directly against a DEBUG binary to exercise the new cross-check) all
pass; whole-engine tests/ecm_ringers.ep_ at sd10 vs. the immediately
preceding commit holds exact solve parity (10/11 both), with
per-position node-count deltas (-50% to +76%) all attributable to the
three documented changes above, no unexplained outliers.
ATTACK_BITV/bvAttacks itself is intentionally left in place -- nothing
writes it any more (king was the last writer), but the struct deletion
and the resulting simplification of the transitional
_IsSquareAttackedByX/_IsSquareXrayedByX helpers (which collapse to
plain bitboard reads once nothing can ever populate the old structure)
is staged as a deliberate follow-up commit, not bundled here.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XxmVi2sTMwpPp4i6WYFjan
|
|
rook/bishop mobility to credit squares beyond a battery partner
_EvalQueen's mobility ray-walk and QMobCaseTable switch replaced with
two passes of the same unified chain-walk technique rook/bishop use --
_RookAttacksBB for the orthogonal-direction pass, _BishopAttacksBB for
the diagonal-direction pass (MOVEGEN_MIGRATION.md/EVAL.md already
established a combined 8-ray table measures slower than reusing the
rook/bishop tables separately, per the stashed first bitboard-eval
attempt's _EvalQueenOccupancyBB PoC -- reused that structure instead of
rediscovering the regression). Continue-set per pass: friendly queen in
both, friendly rook only in the rook-direction pass, friendly bishop
only in the bishop-direction pass -- QMOB_FRIEND_ROOK/_BISHOP's old
fOrthogonalRay flag is entirely subsumed by which lookup a blocker
shows up in. Queen never x-rays through any enemy piece (unlike
rook/bishop), so each pass's continue-set has no enemy side. Adds
pos->bbQueenAttacks[2]/bbQueenXrayAttacks[2] and the corresponding
_IsSquareAttackedByQueen/_IsSquareXrayedByQueen transitional helpers
(DEBUG-cross-checked against an independent mailbox walk, same pattern
as the rook/minor helpers), with _WhoControlsSquareFast and _EvalKing's
bvAttack/bvXray/bvDefend/uQueenNearKing computations updated to read
them instead of the old per-square bvAttacks bits queen no longer
writes.
Also fixes a real, previously-unnoticed fidelity gap in the
already-committed rook and bishop conversions: the old mailbox walk
doesn't just x-ray past a friendly battery partner (or, for rook/
bishop specifically, an x-rayable enemy queen/king) for attack-bit
purposes -- it keeps walking and keeps crediting *mobility* for
whatever safe/empty squares and further captures lie beyond, however
many such blockers are stacked on one ray. The originally-committed
bitboard versions only ever computed mobility from the near side (up
to the first blocker via the magic lookup) and treated the chain
purely as an attack-bit population exercise. Caught by direct
comparison against the pre-conversion mailbox binary on battery
positions (rook: two same-color rooks on an open file, old code
credited 13 mobility to one rook via squares beyond its companion vs.
9 for the near-side-only bitboard version; bishop: two same-color
bishops on one diagonal, 12/8 old vs. 11/5 near-side-only). Fixed by
merging each piece's mobility and x-ray-population computation into a
single chain-walk that credits mobility at every layer, not just the
first -- queen's own conversion was written with this fix in from the
start and verified against the same kind of battery position (two
queens on a file, queen+rook, queen+bishop) before landing.
Verified via precommit_check.sh (self-test + DEBUG smoke test, several
hand-built multi-piece-battery positions run directly against a DEBUG
binary to exercise the new cross-checks) after each step.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XxmVi2sTMwpPp4i6WYFjan
|
|
bishop
_EvalRook's mobility ray-walk and RMobCaseTable switch replaced with
_RookAttacksBB(c, pos->bbOccupied) plus bitboard masks, same technique
as knight/bishop. Adds pos->bbRookAttacks[2]/bbRookXrayAttacks[2],
first contributors alongside bbMinorAttacks/bbMinorXrayAttacks.
Connected-rook bonus and x-ray population derived from the attack
bitboard instead of a per-square dispatch.
Unlike bishop's single-hop x-ray simplification, rook's x-ray
population uses a bounded chain-following loop (recompute with the
newly-found blocker excluded, repeat until no new x-ray-worthy
terminal appears): checked frequency first (board_representation/
EVAL.md), and 14% of the curated-suite positions have a genuine
2+-deep rook/queen battery on some ray, far more common than bishop's
~1% -- a one-hop approximation here would be a real fidelity loss, not
a negligible one. The stashed first bitboard-eval attempt (git
stash@{1}) had already solved this correctly by walking blocker-to-
blocker via bit-scan; this reproduces the same unbounded behavior via
repeated magic-lookup recomputation, cheap because the loop only
iterates again when an actual chained battery exists.
Backported the same bounded-chain fix to bishop's x-ray population
(previously single-hop only) for consistency, now that it's known
cheap and mechanically identical -- bishop's own battery rate is much
lower (~1%) so this mostly just removes an intentional divergence
rather than fixing an active problem.
Also fixes two real correctness gaps this conversion would otherwise
have introduced silently (same failure mode as pawn's earlier
conversion, EVAL.md's progress log): rook no longer writes ROOK_BIT/
ROOK_XRAY_BIT into the old per-square bvAttacks structure, but
_WhoControlsSquareFast and queen's mobility unsafe-check both still
read those bits directly. Added _IsSquareAttackedByRook/
_IsSquareXrayedByRook (same transitional-helper, DEBUG-cross-checked
pattern as the minor-piece helpers) and updated both call sites.
Verified via precommit_check.sh (self-test + DEBUG smoke test) after
each step.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XxmVi2sTMwpPp4i6WYFjan
|
|
_EvalBishop's mobility ray-walk (per-square g_iBDeltas delta-walk +
BMobCaseTable switch dispatch) replaced with _BishopAttacksBB(c,
pos->bbOccupied) -- generate.c's magic-bitboard slider lookup,
already used by move generation -- plus bitboard masks for the
mobility count, matching knight's reduction pattern:
enemy-non-pawn-terminal counts unconditionally, empty-or-enemy-pawn-
terminal counts unless pawn-unsafe (via bbPawnAttacks), and the one
case that isn't a pure occupancy mask -- a terminal friendly,
non-stationary pawn on this bishop's own color complex -- keeps its
credit via bbPc (already computed for the existing good/bad
transient-pawn scoring, untouched this change).
Adds POSITION::bbMinorAttacks contributions from bishop (direct
attack bits) and POSITION::bbMinorXrayAttacks (new field) for squares
seen through a friendly bishop/queen battery partner or an
x-rayable enemy rook/queen/king.
Deliberate behavior change from the old ray-walk, made for speed per
direct instruction: the old walk's fStop=FALSE for the x-ray cases
meant it kept going -- and kept counting mobility -- through however
many x-ray-worthy blockers were stacked consecutively on one ray
(e.g. x-raying an enemy rook, then continuing to x-ray *through* an
enemy king sitting right behind it too). The bitboard version only
extends one hop past the first x-ray-worthy blocker; it does not
re-check whether the newly-revealed terminal square is itself
x-ray-worthy and extend again. This was found and deliberately kept
(not fixed) after a DEBUG assert caught the exact case on
8/1R1B4/2B1r3/5k2/2P2P2/1p6/1Kb5/7n w - - during precommit's random-
sample smoke test -- judged an acceptable trade given how rare a ray
with >=2 consecutive x-ray-worthy pieces is.
Two new transitional helpers (_IsSquareAttackedByMinor's bishop
contribution, and new _IsSquareXrayedByMinor) carry bishop's combined
state to every remaining consumer (rook/queen mobility-safety checks,
king's danger computation, _WhoControlsSquareFast) -- both DEBUG-
asserted against an independent from-scratch mailbox ray-walk (using
the still-live rgSquare representation, not any shared code with the
production bitboard technique) implementing this same single-hop
rule, so a real regression fails loudly rather than drifting into a
wrong score.
Verified via precommit_check.sh and the full tests/ecm_ringers.ep_
suite at sd10 against the prior commit (7e3e6b6): same 10/11 solved
(same single miss, ECM.335, in both), node counts now legitimately
differ per position (expected given the semantic change) but stay in
a bounded, reasonable range (-17% to +20%), nothing resembling the
60%+ blowups a real bug produced earlier in this same session before
being caught, isolated, and traced to this exact x-ray-chain gap.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P6g6iF6mD1Hau6nCZCzwYj
|
|
_EvalKnight's mobility loop (per-square g_iNDeltas delta-walk +
NMobCaseTable switch dispatch) replaced entirely with
g_KnightAttacksBB[c] (generate.c's precomputed table, already used by
move generation) plus two bitboard masks -- knights never x-ray or
have a battery partner, so the old four-case table collapses to
"enemy non-pawn: count unconditionally" and "empty-or-enemy-pawn:
count unless pawn-unsafe."
Adds POSITION::bbMinorAttacks[2] (chess.h), the first of the
bbMinorAttacks/bbRookAttacks/bbQueenAttacks accumulators from
board_representation/EVAL.md section 2 -- knight ORs its full attack
set in directly, no per-square bit-scan needed. Bishop is not
converted yet and still writes its own minor-bit contribution into
the old per-square rgSquare[c|8].bvAttacks structure.
Since knight stopped writing that old structure, every consumer that
needs "does any minor attack this square" (rook/queen's mobility-
safety check, king's danger computation, _WhoControlsSquareFast) now
goes through a new transitional helper, _IsSquareAttackedByMinor,
which ORs the new bitboard (knight) with the old bvAttacks bit
(bishop) in exactly one place rather than each call site hand-rolling
its own combination -- this collapses to a plain bbMinorAttacks read
once bishop converts too, and the helper goes away entirely.
_IsSquareAttackedByMinor is DEBUG-asserted against an independent
recomputation (g_KnightAttacksBB / _BishopAttacksBB, both already-
trusted primitives from move generation, unrelated to either the old
ray-walk's bit bookkeeping or the new accumulator) so a bug in either
mechanism fails loudly in any DEBUG build/smoke-test run rather than
silently drifting into a wrong score deep in search.
Verified via precommit_check.sh and a direct before/after node-count
comparison (sd10, r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/
R1BQ1RK1 w - - 0 1): byte-identical, 3125335 nodes both builds.
This landed as a deliberately small, single-piece-type step after an
earlier attempt to convert knight+bishop+rook+queen+king in one
combined change produced a real bug (a byte-scale mismatch in xray
bit reconstruction) that was hard to isolate with five things changed
at once, and was reverted back to 6e86450 rather than debugged
further. Bishop, rook, queen, and _EvalKing's own conversion are
follow-up steps, each to be landed and verified the same way, one at
a time.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P6g6iF6mD1Hau6nCZCzwYj
|
|
First piece type converted per board_representation/EVAL.md section 2:
pawns no longer write into rgSquare[c|8].bvAttacks at all. Every
consumer of "does a pawn attack this square" now reads
pos->bbPawnAttacks[2] directly -- a plain bitboard, computed fresh
each Eval() call from pos->bbPawns[] via the same shift-and-mask
technique generate.c's _GenerateAllPawnMovesBB already uses (zero
per-pawn mailbox iteration, vs. up to 16 delta+IS_ON_BOARD checks
before). Knight/bishop/rook/queen/king are unchanged -- still
populate/read their own bvAttacks bits (uMinor/uRook/uQueen/uKing)
the old way until their own conversions land.
Consumer changes, all in eval.c:
- UNSAFE_FOR_MINOR retired as a macro (it only ever tested the pawn
bit) -- its 3 call sites (knight, bishop x2) now test
bbPawnAttacks directly.
- UNSAFE_FOR_ROOK/_QUEEN masks narrowed to drop the now-dead pawn
bit; call sites OR in an explicit bbPawnAttacks test alongside the
narrowed bvAttacks read.
- Two direct .small.uPawn reads (bishop's transient-pawn mobility
credit, bishop's defended-pawn bonus) switched to bbPawnAttacks
tests.
- _EvalKing's bvAttack/bvDefend (the real king-danger computation)
OR the bitboard bit back in at both read points, careful to
preserve the original ordering where bvDefend must reflect the
king's own just-set defend bit.
- _WhoControlsSquareFast (used by passer-race/trapped-piece/danger
code) ORs the bitboard bit back into its g_SwapTable index at the
same bit position PAWN_BIT always occupied.
- Two bugs caught by manually auditing every remaining |8 site after
the fact (not by any test failing): _EvalPawns' own pawn-duo and
backward-pawn detection read bvAttacks.uWholeThing at a point in
Eval()'s sequence where only pawns could have written it -- once
pawns stopped writing there, both checks went permanently dead
silently. Fixed to read bbPawnAttacks directly. No self-test caught
this; it's exactly the gap EVAL.md section 5's planned exact-score
harness is meant to close.
EVAL_TIME instrumentation: split _EvalPawns' cycle counter into
pawn-hash hit/miss buckets (chess.h/eval.c/root.c), answering whether
the hash is still worth it now that attack-bit population is nearly
free. Measured on one sd12 benchmark position: hits average 90.4
cycles, misses average 1741.3 cycles (~19x), 97.15% hit rate -- the
hash stays a clear win; the miss cost was never mostly attack-bit
population (that's a separate ~1% bucket now, down from ~3.6%), it's
the isolated/doubled/duo/backward-pawn scoring loops, which still do
real per-pawn work on a miss.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P6g6iF6mD1Hau6nCZCzwYj
|
|
chess.h: add POSITION::bbOccupied (full-board occupancy, both colors,
every piece including kings), maintained incrementally alongside
bbPieces/bbPawns rather than rebuilt on demand -- resolves the open
question in EVAL.md section 0 about whether this is worth doing given
both generate.c and the planned eval.c mobility rewrite need it.
move.c: SlidePiece/SlidePawn/LiftPiece/PlacePiece and their
WithoutSigs variants now maintain bbOccupied at the same choke points
that already maintain bbPieces/bbPawns -- unconditionally, since
occupancy doesn't care about piece type or color. Kings only ever
move through SlidePiece/SlidePieceWithoutSigs (never Lift/Place), so
no separate king-specific update site was needed.
fen.c: populate bbOccupied when parsing a FEN.
board.c: VerifyPositionConsistency cross-checks pos->bbOccupied
against a from-scratch rebuild, same pattern already used for
bbPieces/bbPawns.
generate.c/movesup.c/see.c: replace call sites that rebuilt full
occupancy via _BuildFullOccupiedBB/_BuildOccupiedBB with direct reads
of pos->bbOccupied; delete see.c's _BuildOccupiedBB, which was a
byte-for-byte duplicate of generate.c's _BuildFullOccupiedBB (kept
only as the from-scratch ground truth for the new consistency check
and testgenerate.c's benchmark harness).
testsup.c: GenerateRandomLegalPosition builds POSITIONs by poking
rgSquare/bbPieces/bbPawns directly, bypassing both move.c and fen.c --
a third construction path the above missed. It never set bbOccupied,
so the new VerifyPositionConsistency check failed on every generated
position, and since generation retries until a position verifies, the
self-test suite spun forever (100% CPU, no progress) instead of
crashing outright. Fixed by setting bbOccupied at all four placement
sites (both kings, pawn, non-pawn piece). Full self-test suite and
precommit_check.sh verified clean afterward.
board_representation/EVAL.md: record the bbOccupied decision and
rationale, resolving section 0's open question.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P6g6iF6mD1Hau6nCZCzwYj
|
|
redundant pawn-location bitboard
board_representation/EVAL.md: rewritten migration plan for a
bitboard-backed Eval() (mobility ray-walks + bvAttacks replacement),
plus a performance-philosophy section recording the profiling-first,
cut-aggressively-except-mobility/safety-awareness approach agreed on
this session, and findings on CountKingSafetyDefects' structural
inability to share bvAttacks-derived state with _EvalKing.
EVAL_TIME per-term instrumentation (chess.h/eval.c/root.c): breaks
the existing whole-Eval() cycle counter down by pawns/knight/bishop/
rook/queen/king, the always-paid pre-lazy-exit segment, and the
full-eval-only post-lazy segment, printed alongside the existing
"Avg. cpu cycles in eval" line. Diagnostic only (EVAL_TIME-gated),
no effect on the normal release profile.
Drop PAWN_HASH_ENTRY's bbPawnLocations[2]: it duplicated
POSITION's own incrementally-maintained bbPawns[2], rebuilt bit-by-bit
on every pawn-hash miss for no reason. eval.c now reads pos->bbPawns[]
directly; removed a stale per-iteration invariant assert in _EvalPawns
that only made sense when the bitboard was being built bit-by-bit in
that same loop.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P6g6iF6mD1Hau6nCZCzwYj
|
|
default on
Implements the full board_representation/MOVEGEN_MIGRATION.md scope:
bitboard-backed generators for all six not-in-check piece types plus
the JumpTable-avoiding whole-node dispatch fork (_GenerateAllMovesBB),
the in-check escape path (king flight + block/capture), and
movesup.c's ExposesCheck/FasterExposesCheck/ExposesCheckEp/IsAttacked/
InCheck bitboard equivalents. Nine toggles total
(GENERATE_{KNIGHT,KING,ROOK,BISHOP,QUEEN,PAWN}_BITBOARD,
GENERATE_ESCAPES_{KING,BLOCK}_BITBOARD, EXPOSESCHECK_BITBOARD,
ISATTACKED_BITBOARD), all now on by default in GNUmakefile --
DISABLE_BITBOARD_MOVEGEN=1 opts back into the mailbox path, which
remains fully present and compiled either way.
Correctness verified via perft (Kiwipete, Position 4), the move-set
comparison harness across 20,000 random positions, all nine toggles
combined cleanly (15/15 runs, after fixing a GenerateRandomLegalPosition
en-passant-sentinel bug in the test harness), and sd10 on all three
curated suites showing zero solve-count regression vs head_reference
(the ecm_hard_quick delta traced to unrelated intervening commits).
Speed: most individual generators land near parity by design (mailbox's
per-square walk was already close to O(destination count)); the real,
consistent wins are the dispatch-layer fork (up to 23% in dense
positions) and IsAttackedBB (0.73x-0.93x of mailbox).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AbHkVrm5KUyzLwWd3GHmo6
|
|
Board-representation migration section 6: a GNUmakefile build flag
(-DGETATTACKS_BITBOARD) makes chess.h's GetAttacks macro resolve to
_GetAttacksBB instead of the real asm implementation (or SlowGetAttacks
under CROUTINES) -- a three-way choice at the same spot the existing
CROUTINES switch already lived. _GetAttacksBB is now reachable from
every real call site (generate.c's check-detection call, see.c's
SEE(), searchsup.c), not just the test/bench harness.
Found and fixed while verifying this: testsee.c's TestGetAttacks and
its benchmark call the identifier GetAttacks meaning "the real
asm/CROUTINES baseline" -- once the macro could resolve to
_GetAttacksBB, those calls would silently compare the new
implementation against itself, turning both the correctness sweep and
the benchmark into false-positive no-ops. Fixed with a local #undef
GetAttacks right after #include "chess.h" in testsee.c, so the harness
always validates against the true baseline regardless of which
implementation is live in production.
Verified: gmake TEST=1 GETATTACKS_BITBOARD=1 passes (self-test suite,
corrected benchmark still reporting real asm vs. _GetAttacksBB
correctly, and a real Search() call exercising _GetAttacksBB live).
precommit_check.sh GETATTACKS_BITBOARD=1 clean for both the TEST=1
self-test and DEBUG=1 smoke test. Default (no flag) build confirmed
unaffected -- GetAttacks still resolves to the real asm function.
See board_representation/MIGRATION.md section 6 for the full writeup.
Sections 4/5/7's remaining items (curated-suite sd10 comparison,
match_play.py gate) are now unblocked but not yet run.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jntky4yGUTyQVaGCXms4F2
|
|
Board-representation migration, sections 2-3 (GetAttacks half):
- board.c: VerifyPositionConsistency's bbPieces consistency check
(migration section 2), verified clean via gmake TEST=1 with the
assert live.
- POSITION.bbPawns[2]: new incrementally-maintained per-color pawn
location bitboard (chess.h), maintained at the same 6 move.c sites
as bbPieces, populated from scratch in fen.c. Distinct from the
pawn-hash-keyed bbPawnLocations; this one needs no
SEARCHER_THREAD_CONTEXT, so it's reachable from GetAttacks's actual
call sites (which only ever have a POSITION*).
- data.c/chess.h/main.c: g_RookRayAll/g_BishopRayAll (all 4 per-square
ray directions pre-ORed) and g_PawnAttackOriginBB[2][128] startup
tables, plus FastFirstBit/FastLastBit (static inline bsf/bsr
wrappers, chess.h) -- supporting tables/helpers for the primitive
below.
- see.c: _WhoAttacksSquareBB (bitboard "who attacks square X" query)
and _GetAttacksBB (SEE_LIST-populating PoC wrapping it), side by
side with the existing SlowGetAttacks/asm GetAttacks -- not wired
into the GetAttacks macro yet (section 6), pure addition.
- testsee.c: SeeListsAreEqual made order-independent (SEE() sorts the
list right after GetAttacks returns, so order was never semantically
significant); TestGetAttacks extended to run _GetAttacksBB as a
third comparison across the existing 20,000-random-position sweep;
added an interleaved asm/Slow/BB cycles-per-call benchmark across
opening/middlegame/endgame positions.
- testsup.c: fixed GenerateRandomLegalPosition (used by the sweep
above) to maintain bbPieces/bbPawns at its two hand-placement sites
-- a latent gap since section 1 that made its own
VerifyPositionConsistency legality gate almost always reject
generated positions, causing large, variable retry-loop slowdowns.
Verified: 20,000-position x every-square x both-colors correctness
sweep passes (gmake TEST=1), precommit_check.sh clean (self-test +
DEBUG smoke test). Benchmark: _GetAttacksBB is ~0.53-0.55x asm
GetAttacks's cycles/call (opening/middlegame) and ~0.89x (endgame) --
faster, not just equivalent, primarily from replacing bbOccupied's
up-to-16-iteration pawn loop with two bbPawns ORs, plus a
g_PawnAttackOriginBB table lookup replacing per-call pawn-delta
arithmetic and per-direction/per-side-group early-outs in the slider
walk. See board_representation/MIGRATION.md section 3 for the full
writeup, including a reverted approach that measured slower and why,
and the CountKingSafetyDefects half's re-scoped (not yet implemented)
design.
Also confirmed (not caused by this work, not fixed here): a
pre-existing non-deterministic MP-race assertion in util.c:1093's PV
printing, reproduced independently on a clean HEAD checkout.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jntky4yGUTyQVaGCXms4F2
|
|
Board-representation migration, section 1: add POSITION.bbPieces[2][8]
(per-color, per-piece-type location bitboards, indexed like the
existing uNonPawnCount) as incrementally-maintained state, not a
per-Eval()-call rebuild -- the structural fix for why the earlier
attack-presence-bitboard work measured slower, not faster.
- chess.h: bbPieces[2][8] field; extern decls for data.c's
g_RookRayToEdge/g_BishopRayToEdge/g_KnightAttacksBB ray tables and
their Initialize* functions (needed by the planned bitboard-backed
GetAttacks/CountKingSafetyDefects primitive, section 3).
- fen.c: populate bbPieces during piece placement; zeroing is free via
the existing memset(p, 0, sizeof(POSITION)).
- move.c: maintain bbPieces at all 6 non-pawn piece-movement functions
(SlidePiece/LiftPiece/PlacePiece and their WithoutSigs siblings used
by UnmakeMove) -- covers every move type: normal moves, captures,
both-side castling, promotion with/without capture, en passant, and
every undo.
- board.c: extend VerifyPositionConsistency's existing non-pawn
piece-list walk with a parallel bbPieces reconstruction-and-compare,
rather than a separate bespoke check.
- data.c/main.c: pulled ray-to-edge/knight-attack tables from stash
(needed by section 3, not section 1 itself, but zero-risk to land
now).
Also, while verifying: COOR_TO_BB was a table lookup (BBSQUARE[idx])
measured ~5-7% slower than the pure-ALU shift already sitting unused in
SLOWCOOR_TO_BB (whose "SLOW" name reflects a stale assumption about
variable shifts never actually tested on this hardware). Switched
COOR_TO_BB to the shift; fixed testbitboard.c's existing but broken
(dead-code-eliminated, silently reporting "0 cycles/op") comparison
benchmark for both while at it.
Verified via gmake TEST=1 (including TestMakeUnmakeMove's explicit
en-passant/promotion-with-capture/both-castling coverage) and
debug_smoke_test.sh, both clean; release build clean and runs normally.
Nothing reads bbPieces yet -- pure addition, zero behavioral risk.
See board_representation/MIGRATION.md for the full plan.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jntky4yGUTyQVaGCXms4F2
|
|
Pulled the parts of the stashed LMR work that are genuinely independent
of the reduction logic itself, leaving the actual LMR redesign for
separate review:
- Fix extension-taper table overflow: remove the flat MAX_EXTEND_PER_LINE
cap and instead clamp the depth used to build g_uExtensionReduction[]
so a deep `sd` request can't leave the whole taper table stuck at "0
penalty" (every index unreachable).
- Remove a spuriously-firing ASSERT(fMovesRescoredByIID) in Search():
RescoreMovesViaSearch's own fail-high branch deliberately leaves that
flag FALSE by contract, so the assert could fire on any DEBUG build
given an unlucky rescore, making the DEBUG/TEST harness unreliable.
- Misc correctness/portability fixes: unix.c pointer-truncation casts,
chess.h's CONTAINING_STRUCT/IS_ENPASSANT/ABS_DIFF macro hardening
(plus gating the branchless bit-tricks on _X64_ too, not just _X86_),
removal of dead Slide*WithoutSigs prototypes, main.c's hash default
bumped to 256m and its CPP self-test's arch gate widened to _X64_.
- eval_tune/match_play.py: cosmetic SPRT progress-bar/output rework.
- Delete eval_tune/run_ecm.sh (superseded, unreferenced elsewhere).
- run_tests.sh: parameterize suites/SD/SN via args/env vars instead of
hardcoding the three curated suites and sd10/sn5M (defaults kept
pointing at the existing curated suites, since the stash's own
lmr_sensitive_30/lmr_control_30 default suites aren't present in the
repo).
Deliberately left out of this commit: the stash's actual LMR reduction
logic, the M-SIGNAL-SHADOW diagnostic subsystem, the large PERF_COUNTERS
instrumentation buildout, the history-table gravity rework, and the
FindEnprisePiece pre-move staleness fix (skipped per request pending a
decision on whether to also change EFP's pruning behavior).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MjdDfHry3i2jfJzyDXaG8A
|
|
Search()'s Dieter-Brusser hash-hit-leads-to-draw check only verified
that a score of 0 would clear the same alpha/beta bound as the stored
iScore -- it didn't establish that iScore itself was accurate. Since
playing the hash move actually produces a draw, propagate the draw
score upward instead of the stale score computed along a different,
non-repeating path.
While fixing this, centralized every other place that returned a
literal 0 for a draw (search.c's stalemate leaf, searchsup.c's
QSearch draw leaf, probe.c's EGTB draw case, which had a dead
`// g_iDrawValue[...]` comment suggesting this was intended all
along) into a single g_iDrawScore[2] global in draw.c, declared in
chess.h. It's indexed by side to move rather than a scalar so a
future contempt-factor tweak can bias the draw score per color
without touching every call site again; both entries are currently 0,
so behavior is unchanged except for the hash-hit bugfix above.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012e2SaEaQ27JJq1D3wCqryr
|
|
Clean gmake GENETIC=1 PERF_COUNTERS=1 MP=1 SIXTYFOUR=1 build had 100
warnings; DEBUG=1 and TEST=1 builds had more once actually exercised.
- OFFSET_OF/CONTAINING_STRUCT (chess.h) and PTR_TO_ALLOC_HASH (unix.c)
truncated pointers through 32-bit ULONG before use in offset/hash
arithmetic on this 64-bit build -- routed through size_t instead.
- Diagnostic int<->void* round-trips (command.c, root.c, split.c,
sig.c, data.c, unix.c, util.c) widened/narrowed via size_t to avoid
implicit truncation.
- ABS_DIFF on unsigned COOR now casts to int before abs().
- Dropped -fexpensive-optimizations (GCC-only, clang silently ignores
it) from GNUmakefile.
- Removed genuinely dead variables (book.c, gamelist.c, split.c,
testgenerate.c, testhash.c).
- Guarded DEBUG/PERF_COUNTERS/_X86_-only variables and the
_CMEvidenceBucket helper under the #ifdef that actually reads them,
since ASSERT/EVAL_TERM/KEEP_TRACK_OF_FIRST_MOVE_FHs compile away
outside those builds.
- Added missing prototypes for SlidePawn, SlidePawnWithoutSigs,
SlidePieceWithoutSigs (move.c), previously undeclared in chess.h.
- Removed dead _SystemIsRoot (unix.c).
Verified via precommit_check.sh: TEST=1 self-test suite passes,
DEBUG=1 smoke test (10 random ECM positions, sd 4) passes with no
crashes/assertions, release build restored -- all three profiles now
build with zero warnings.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014Cmv11sJZqVfanrPh6UnWE
|
|
Fix a bug (iEval) in HelpSearch
This commit changes the heuristics for: nullmove pruning, LMR, and EFP slightly.
|
|
|
|
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
|
|
countermove promotion, retired hung-piece-escape and NumLeftoverMovesToSelect.
Full session was built on a "measure the pick, not the game" methodology:
aggregate solve counts on curated suites are too noisy to tune move-ordering
knobs against, so most decisions here came from per-move fail-high/alpha-raise
rates at much larger sample sizes (leftover FH% instrumentation, a zero-
selection-budget diagnostic that isolates a single best-of-remaining pick,
and evidence-bucket calibration), not solve-count deltas alone. See
CLAUDE.md's "Dynamic move ordering experiments" section for the reusable
methodology and generate.c's _ScoreAllMoves comment for the resulting
ordering hierarchy.
Changes:
- Added g_ContinuationHistory: same growth/decay math as the existing
g_HistoryCounters butterfly table, additionally keyed by the previous
move, so its magnitude is self-calibrated rather than a hand-picked
constant. Flat, sufficient response across a 256x scale sweep.
- Countermove-table matches now get a real GOOD_MOVE-tier promotion
(previously the table was write-only, tracked for stats but never read
for ordering), but only when the match's own accumulated
history+continuation evidence clears COUNTERMOVE_EVIDENCE_THRESHOLD
(10,000) -- a raw match with no track record was shown to perform
identically to an ordinary leftover (~0.6-0.85% FH), so promoting on
match alone would have repeated hung-piece-escape's mistake below.
- Retired hung-piece-escape's unconditional GOOD_MOVE-tier promotion.
Evidence-calibration showed the overwhelming majority of triggers (a
zero-evidence population 250-1000x larger than countermove's) performed
at the plain-leftover baseline -- the promotion was mostly free tier-
escape treatment for moves that hadn't earned it. Replaced with
FLEE_BONUS, a flat same-tier nudge inside SelectBestWithHistory (never
escapes GOOD_MOVE/leftover classification, unlike a generation-time
promotion) at the magnitude found to plateau a same-tier-nudge sweep.
- Retired NumLeftoverMovesToSelect (the depth-indexed budget on how many
leftover moves got a full selection scan before falling back to
unsorted order). search.c's main move loop now always fully selects --
the leftover pool was shown to contain real, findable signal a bailout
budget was discarding for a node-count savings that didn't hold up net-
net once measured by solve counts and fail-high rates rather than raw
node counts (noisy on small suites independent of this change).
- Collapsed leftover-move instrumentation from sorted/raw pairs down to a
single set now that "raw" (unsorted fallback) is structurally
impossible; kept the countermove evidence-bucket calibration counters
(ongoing check that COUNTERMOVE_EVIDENCE_THRESHOLD stays well-
calibrated); removed the contested-node A/B harness and hung-piece
evidence calibration now that the decisions they were built to inform
are made.
Net effect on the three curated suites (sd 10): solve counts wash (tied,
+1, -1 across ringers/confident/hard), leftover fail-high rate improved
consistently on all three (the intended, directly-measured target of this
work). Not yet validated beyond sd 10 -- an sn-based run or
eval_tune/match_play.py head-to-head gate is the natural next check before
leaning on this as a proven strength gain rather than a directionally-
sound, sd-10-clean change.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014XePz6Sk4qQsTaP2jVJWJu
|
|
trace it at startup alongside the build timestamp.
A --logfile trace could previously only be tied back to a build
timestamp, not the exact source state -- distinguishing same-day
rebuilds during A/B testing required diffing binaries. GIT_COMMIT is
injected via GNUmakefile (git rev-parse --short HEAD, kept out of the
PROFILE variable itself since PROFILE gets separately stringified
whole for the "Make profile used" trace line, and this value's
embedded quotes broke that outer string literal when first tried
folded in there).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EortUUkDVpsfrbqshBJYJg
|
|
type-mixing bug and add a quiet-refutation killer backfill.
Killer tiers now try both of this ply's own killers before either
ply-2-back one, matching Crafty's ordering. Two earlier attempts at
this same swap were reverted for regressing; this pass lands on top of
NumLeftoverMovesToSelect (more SelectBestWithHistory budget to reach
these lower-tier slots) and a real bug fix below, and beats interleaved
order head-to-head on solves, node count, and first-move beta cutoff
across the three curated suites.
The bug: mvNullmoveRefutations's empty-killer-slot backfill could only
ever contain a capturing move (TryNullmovePruning only wrote it inside
the capture-refutation branch), but IS_SAME_MOVE's mask includes the
pCaptured bits, so that backfilled value could never match a real
quiet candidate -- the backfill was silently dead code. Fixed by
recording genuinely quiet null-move refutations into a new, separate
mvNullmoveQuietRefutations array (kept separate so it can't clobber the
capture history mvNullmoveRefutations still needs for the
Botvinnik-Markoff same-piece-two-squares extension check) and
backfilling the regular killer table from that instead. The
check-evasion killer table intentionally does *not* get this backfill:
a null-move refutation can never legitimately be an escaping-check
move (null moves can't deliver check), so backfilling there risks
IS_SAME_MOVE cross-context false positives instead of the old
guaranteed-inert no-op.
Measured at sd10 across ecm_ringers/ecm_confident_quick/ecm_hard_quick
against head_reference (commit d11e973): 115/191 solves (vs. 116
baseline), 924.36M total nodes (vs. 933.23M), first-move beta cutoff
within 0.1-0.9 points of baseline on all three suites -- and clearly
better than the same fix under interleaved order (113/191 solves,
963.10M nodes), which loses to head_reference on every metric.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EortUUkDVpsfrbqshBJYJg
|
|
NumLeftoverMovesToSelect, indexed by remaining depth; make EFP's
leftover-only scope explicit.
SEARCH_SORT_LIMIT[ply] was a poor proxy for what actually matters here
-- how large the remaining subtree below this node is. Distance from
root only correlates with that when total search depth is roughly
fixed; it says nothing once extensions/reductions/iterative-deepening
are in play. NumLeftoverMovesToSelect(ctx, uDepth) uses remaining depth
instead, only ever consulted once every high-performer move (winning/
even capture, killer, killer-mate -- anything >= GOOD_MOVE) has already
been exhausted; this never limits how many of *those* get selected,
only how much further care to spend on the ordinary/leftover tail.
Table values carried over verbatim from the old one as an untuned
starting point, just reindexed.
Also adds an explicit (TRUE == fInLeftovers) gate to EFP's per-move
checklist (landed last commit) -- every high-performer move was already
excluded as a side effect of the capture/check/killer exemptions, but
this makes "EFP only ever touches leftovers" a real, direct condition
rather than an emergent property of unrelated checks.
Verified against HEAD (commit bb07fbd) at sd10:
ecm_ringers: 9/11 -> 10/11 (+1 solve), ~flat nodes (-0.04%)
ecm_confident_quick: 88/90 -> 88/90 (even), +0.43% nodes
ecm_hard_quick: 15/90 -> 18/90 (+3 solves), +4.4% nodes
Net +4 solves across 269 positions for a negligible node-count cost.
|
|
of (cFrom, cTo, color), and expose sample size.
MOVE_TO_INDEX (used elsewhere for the counter-move table) folds any
two moves with the same from/to/color into one fail-high bucket --
e.g. a king shuffle and a queen sac to the same square/color shared
a slot. New MOVE_TO_FH_INDEX uses the low 20 bits of mv.uMove
(cFrom+cTo+pMoved), which already encodes color in pMoved's low bit,
so this is strictly more granular for free. Table grown 0x20000 ->
0x100000 entries to match. Also adds an optional ULONG *puAttempts
out-param so future callers can weight by sample confidence instead
of trusting a percentage computed from as few as one observation.
GetLMRReduction (proto-LMR, live at HEAD) is the only real consumer
right now; verified against head_reference at sd10 across
ecm_ringers/ecm_confident_quick/ecm_hard_quick: net +2 solves (88 vs
87 on confident_quick, 18 vs 17 on hard_quick), node counts flat
within noise (-1.5%/+1.4%/+0.5%).
|
|
of iValue; harden against a latent ComputeMoveExtension bug.
DO_IID's "is the top move crappy" gate compared raw iValue against
SORT_THESE_FIRST only, missing that ordinary killer moves (FIRST_KILLER
through FOURTH_KILLER) sit below that threshold too -- a killer that
already proved itself elsewhere in the tree was being treated as
"crappy" and triggering an unnecessary shallow rescore. Fixed by also
excluding killer-flagged moves from the gate.
RescoreMovesViaSearch corrupted the winning move's real search score by
OR-ing in SORT_THESE_FIRST to force it to sort first (`mvf[uBest].iValue
|= SORT_THESE_FIRST`) -- unnecessary (SelectBest{With,No}History already
find the true max by plain magnitude comparison, no flag needed) and
actively dangerous: a later ComputeMoveScore() call on that same move,
if it's a capture, would see the corrupted value, mistake it for
generate.c's biased-capture-ordering format, and subtract the wrong
bias entirely. Removed the OR; added an explicit
PLY_INFO.fMovesRescoredByIID flag so ComputeMoveScore and the main
search-loop's move-selection call can both recognize "this ply's
iValue holds a real eval-axis score" without relying on bit-pattern
inference.
Consequently, ComputeMoveScore now trusts an IID-rescored move's score
outright instead of running it through the capture-bias-subtraction or
quiet-move-collapse-to-0 logic (both of which assume generate.c's
ordering encoding, which a rescored ply no longer holds). Separately
hardened it against quiet killer-mate moves, which can reach
SORT_THESE_FIRST via a different, capture-unrelated path and were
incorrectly getting the capture bias subtracted from them; they now
correctly collapse to 0 like other quiet moves.
Two follow-on ideas -- blending history into the real IID score (scaled
or capped) and a exact-tie-only history tiebreak -- were implemented,
measured, and rejected: blending invents a new, leak-prone move-scoring
axis for no measured benefit, and the tiebreak-only compromise still
cost solves relative to just trusting the real score outright. Main
search's move-selection call now branches once per selection (not once
per candidate move) between SelectBestNoHistory (IID-rescored plies)
and SelectBestWithHistory (everyone else), keeping the overwhelmingly
common non-rescored path at zero added cost.
Net measured effect (ecm_ringers.ep_/ecm_confident_quick.ep_/
ecm_hard_quick.ep_, sn=5M): 10/90/9, down from a pre-existing 11/88/10
on ringers and hard specifically -- see lmr_testing/RESULTS.md for the
full sweep of rejected alternatives and why the regression was accepted
as the cost of removing a latent, leak-prone bug class rather than
chasing the exact prior numbers.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2
|
|
killer-mate edge case; fix PV-display cycle hang.
_ShouldWeConsiderThisMove (QSearch's move-consider gate) read the raw,
move-ordering-biased mvf[].iValue directly instead of going through
ComputeMoveScore, so it inherited the same +120-ish flat bias (plus
small MVV-LVA nudges) on winning/even captures that ComputeMoveScore
was already fixed to strip out. Fixed via the same MOVE_SCORE_ORDERING_BIAS
subtraction, now factored into a shared chess.h macro. Restoring the old
effective leniency required an explicit QSEARCH_CONSIDER_MARGIN (120,
A/B'd against 0/60/120 on ecm_ringers/confident_quick/hard_quick) rather
than assuming the bug's magnitude was itself a meaningful margin -- net
effect vs the pre-fix baseline is -2 solves on hard_quick, accepted as
the cost of correctness (see lmr_testing/RESULTS.md for the full sweep).
ComputeMoveScore separately mishandled quiet killer-mate moves: they can
reach SORT_THESE_FIRST via generate.c's killer-mate bonus (unrelated to
the capture-bias path), so the bias-subtraction was wrongly applied to a
move that never had that bias. Gated the subtraction on
IS_CAPTURE_OR_PROMOTION(mv); quiet moves (including killer-mate ones) now
correctly collapse to 0, per the function's contract of estimating a
move's value on the 100=1-pawn axis. Measured as a no-op on all three
suites -- rare in practice, but a real correctness fix. Left a comment
documenting two candidate refinements for scoring quiet moves as
non-uniform future work, deliberately not implemented (each needs its
own isolated test).
FinishPVTailFromHash (cosmetic PV-display hash-walk, used only for
printing) had no cycle detection, so a drawish/repeating position could
spin until the output buffer filled instead of terminating naturally.
Added visited-position-signature tracking and a <REP> marker.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2
|
|
Finishes work left half-done in 7857096 ("Replace ctx->uPositional with
a data-calibrated Eval() return value"): that commit added Eval()'s new
piPositional out-param but never migrated GetRoughEvalScore onto it, so
GetRoughEvalScore's mid/deep-tree fallback kept reading the old
ctx->uPositional field -- a per-thread EWMA written only on full-eval
calls and never touched by the (far more common) lazy-eval path, so it
carried a stale value from whatever unrelated position last triggered a
full eval, potentially many nodes/plies away. Combined with EVAL_HASH
being long since disabled (its probe branch already dead), every
GetRoughEvalScore call past ply 4 was effectively "material + garbage."
Fixed by having GetRoughEvalScore just call Eval() directly -- its own
lazy-exit machinery already is the cheap, calibrated estimate this
function exists to provide, so there's no separate estimator to
maintain. Removed ctx->uPositional entirely (struct field, its EWMA
update in eval.c, both root.c init sites, split.c's cross-split
propagation, testeval.c's reset) along with the entire EVAL_HASH
subsystem (struct, table, Probe/StoreEvalHash, main.c's now-dead
reporting branch, the GNUmakefile flag) -- confirmed unused elsewhere
and explicitly being cut for good, not coming back in this form.
Also fixed GetRoughEvalScore's prototype being wrongly declared inside
#ifdef EVAL_HASH in chess.h even though the function itself is defined
and called unconditionally -- this was the source of the recurring
"call to undeclared function 'GetRoughEvalScore'" implicit-declaration
warning seen throughout this session's builds.
Separately, fixed QSearch to match its own documented intent: the
en-prise/trapped-piece "don't let this side stand pat" check now only
fires if the side hasn't already been allowed to stand pat earlier in
this qsearch line (matching the comment above it, which already said
this but the code never implemented it).
Verified against baseline/typhoon_baseline (pristine, pre-session) on
ecm_ringers.ep_ (4), ecm_hard_quick.ep_ (50-sample), and
ecm_confident_quick.ep_ (40) at sn=5M, --cpus 1, book disabled:
pristine baseline solves 3/50 on the hard sample; this commit solves
6/50, with the stand-pat fix and GetRoughEvalScore fix each
contributing +1 independently confirmed. No regressions on the other
two suites (4/4 and 40/40 unchanged throughout).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2
|
|
EBF/beta-cutoff/counter-move stats, script.c FPE fix.
No LMR, no counter-move-driven move ordering (both explored separately,
kept out for now -- counter-move measured worse, ~655->647 solved on
ecm879 @ sn=4M with a leaner tree beforehand). Futility pruning restored.
Verified: 647/879 solved, EBF 4.609 @ sn=4M; 684/879 solved, EBF 3.995
@ 20s/move, 1cpu, 256m hash (typhoon_baseline.log).
The counter-move table is still written and its stats still tracked
(dynamic.c) for diagnostic purposes, but generate.c no longer reads it
for move ordering, so it has no effect on search behavior in this
commit.
lmr_testing/ holds the in-flight graded-LMR + counter-move code (not
applied here) with notes on what was already tried and measured, so a
future session can resume without re-deriving it.
|
|
uPositional was a per-thread EWMA of abs(material - true score) used to
size lazy-eval and futility margins. It was history-derived (reflecting
whatever recent, unrelated positions looked like) rather than derived
from the position actually being margined, and its update/consumption
was tangled with EVAL_HASH (now disabled).
Eval() now takes an optional SCORE *piPositional out-param and fills it
in on every return path: exact (abs(material-delta)) on a full eval,
or an estimate from a new EstimatePositionalScore() on a lazy exit.
EstimatePositionalScore()'s two terms (king-safety-defect-bucketed, and
a flat residual for mobility/passers/everything else) are calibrated
from ~1.6M measured full-eval samples (p90 of the actual swing), not
guessed -- an initial guessed version measurably regressed ECM solve
rate (630 vs a 650 baseline at sn=4M); the recalibrated version is back
at parity (649/879).
search.c's qsearch futility now reads the value Eval() just computed
instead of the stale/shared ctx field. Also removes QSearchInDangerNoStandPat
and SideCanStandPat, dead since the danger-hash check that fed them was
already commented out (e08387a) -- they depended on the same enprise/
trapped-piece data this conversation is about to move off of
g_PositionHash entirely.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
|
|
|
|
|
Fixes a real pathology: checks along a long unbroken forcing line could
extend for free (net zero cost against the qsearch boundary), letting
tree size blow up multiple orders of magnitude on positions like a
near-all-check forced mate (ECM.089: 4.5B nodes / 31min at depth 12
before this change).
- Main-search check extension: gate on SEE soundness (a losing
sacrifice check gets a small consolation QUARTER_PLY instead of the
full bonus a sound check gets), and flatten the sound-check bonus to
a flat THREE_QUARTERS_PLY instead of a near-free ONE_PLY.
- Lower the qsearch entry threshold to match (THREE_QUARTERS_PLY
instead of ONE_PLY) so a lone check still buys one extra full-width
ply as before; root.c trims QUARTER_PLY off the per-iteration depth
budget so this doesn't add a blanket 1/4 ply to every search.
- Qsearch's own check-widening (QSearchFromCheckNoStandPat) now relies
on fCouldStandPat history plus a g_uIterateDepth/4 ceiling instead of
an unconditional per-check grant, and QPLIES_OF_NON_CAPTURE_CHECKS
moved from 1 to 2 to cover both "enter qsearch already in check" and
"opponent's reply is the first real check" cases with one baseline
window instead of ad hoc attacker-color tracking.
Net effect on ECM.089 (sn 4M canary): ~7.5x fewer nodes and ~4x less
time at depth 12 versus the original, unbounded behavior. Costs solve
count on the full ECM suite (879 pos, sn 4M): 650 baseline -> 636 here
-- expected and accepted, since ECM is unusually check-extension-heavy
tactics and not representative of real games. Self-play vs baseline
(1000 games, st 1) came back at B_SCORE=0.4955, ELO=-3.1+/-21.5 --
statistically neutral, confirming the fix costs nothing in real play.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
|
Late Move Reductions using a depth x movecount table (Ethereal-style
formula), gated off PV nodes and the ply directly below a PV node
(PLY_INFO.fIsPVNode), with magnitude-aware re-search on fail-high.
Verified node-for-node identical to the previously tested-good v7
binary on a canary position (sd 10) after reconstructing from a ZFS
snapshot of search.c/root.c/split.c taken just before that binary was
built.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
|
|
|
|
|
|
|
it found)
|
|
|