summaryrefslogtreecommitdiff
path: root/src/chess.h
AgeCommit message (Collapse)Author
5 daysAdd bitboard-backed GetAttacks (section 2/3), verified faster than asmScott Gasch
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
6 daysAdd bbPieces incremental piece-location bitboards (migration plan section 1)Scott Gasch
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
6 daysCherry-pick non-LMR fixes and tooling from the "LMR" stashScott Gasch
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
6 daysFix draw-score bug in hash-hit path; centralize as g_iDrawScore[2]Scott Gasch
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
6 daysFix all build warnings across release/DEBUG/TEST profilesScott Gasch
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
10 daysPlumb iImprovement into LMR and futiltiy and parallel search.Scott Gasch
Fix a bug (iEval) in HelpSearch This commit changes the heuristics for: nullmove pruning, LMR, and EFP slightly.
10 daysImproving for nullmove, eval speedup, remove old cruft, bugfix in split / MP.Scott Gasch
11 daysEval.c diet: kill exact-duplicate terms, turn down overlapping ones, fix a ↵Scott Gasch
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
11 daysDynamic move ordering overhaul: continuation-history, evidence-gatedScott Gasch
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
12 daysBake the short git commit hash (plus -dirty suffix) into the binary and ↵Scott Gasch
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
12 daysSwitch to Crafty-style killer ordering; fix mvNullmoveRefutations ↵Scott Gasch
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
12 daysReplace SEARCH_SORT_LIMIT's ply-indexed leftover-selection budget withScott Gasch
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.
12 daysRewire GetMoveFailHighPercentage: index by (cFrom, cTo, pMoved) insteadScott Gasch
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%).
12 daysFix IID's killer-blind gate and its ordering-flag/history contaminationScott Gasch
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
12 daysFix _ShouldWeConsiderThisMove's SEE-purity bug and ComputeMoveScore'sScott Gasch
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
13 daysRemove ctx->uPositional and the EVAL_HASH subsystem; fix GetRoughEvalScore.Scott Gasch
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
13 daysBaseline: uPositional data-calibrated fix, enprise/trapped hints, ↵Scott Gasch
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.
2026-08-26Replace ctx->uPositional with a data-calibrated Eval() return value.Scott Gasch
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]>
2026-08-26Don't [re-]consult the SEE when generator already just did so.Scott Gasch
2026-08-26Make EVAL_HASH optional.Scott Gasch
2026-08-25Tame check-extension compounding; verified neutral in self-play (0.4955)Scott Gasch
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]>
2026-08-25Add LMR with PV-adjacency guard (v7), verified against saved binaryScott Gasch
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]>
2026-08-24Started doing texel eval tuning.Scott Gasch
2026-08-23Integrate new syzygy egtb code, lose the old Nalimov code.Scott Gasch
2026-08-23Clean X64 build and ported GetAttacks to x64.Scott Gasch
2018-10-03Update codebase to remove clang warnings (and a couple of legit errorsScott Gasch
it found)
2016-06-01Initial checkin for typhoon chess engine.Scott Gasch