summaryrefslogtreecommitdiff
path: root/src/searchsup.c
AgeCommit message (Collapse)Author
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
11 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 daysReplace SEARCH_SORT_LIMIT's pure move-count cutoff with a quality-basedScott Gasch
one: always fully select "high performer" moves regardless of count, only apply the per-ply budget to leftover ordinary moves. The old gate (uLegalMoves < SEARCH_SORT_LIMIT(ply)) stopped selecting carefully after a fixed count, counting the hash move too -- so at ply 6+ (limit 5), a position with a hash move already used one of only 5 total slots before the cutoff hit. It had no way to tell "a handful of mediocre quiet moves" from "a hash move plus three winning captures and two killers" -- in the latter case, a real high-performer beyond the 4th/5th slot would get treated identically to a random leftover quiet move, even though generate.c had already tagged it as excellent. Checked what three real engines do here: Crafty always fully sorts the hash move, then MVV/LVA-ordered captures, then up to 4 killers -- its own cheap fallback (a move-count cutoff, gated by remaining depth) only ever applies to what's left after all of that, i.e. plain untested quiet moves. Stockfish uses a value threshold, not a position/count threshold, so a good move is never orphaned by where it happens to sit in the list, only by its own assessed quality. Berserk never gates at all -- full selection sort unconditionally, every node. New design: keep fully selecting for as long as every move found so far is >= GOOD_MOVE (a generate.c ordering-encoding constant that already sits, by construction, below every killer tier and SORT_THESE_FIRST's winning/even-capture range, and above ordinary quiet moves and losing captures -- a real quality floor already baked into the existing encoding, not a new one). The first selection that reveals a move below that floor marks the transition to "the rest of the team"; from there, SEARCH_SORT_LIMIT's existing table is reused (as an explicitly untuned starting point -- its old numbers were calibrated, if at all, against a different question: total selection budget from move 1, not a leftover-only budget) to decide how many more full selections are worth the cost before taking the remainder in place. On an IID-rescored ply, GOOD_MOVE is meaningless (iValue is a real eval-axis score there, not generate.c's encoding), so that ply type keeps its existing unconditional full-select behavior unchanged. Measured (ecm_ringers.ep_/ecm_confident_quick.ep_/ecm_hard_quick.ep_, sn=5M) against the prior baseline (10/88/9): 11/87/12, net +3 solves. EBF: unchanged on ringers, worse on confident_quick (the one suite that also lost a solve -- consistent single-suite regression, not a systemic pattern), better on hard_quick (paired with its solve gain). Not yet a fully validated result -- SEARCH_SORT_LIMIT's numbers (17/12/9/7/6/5) now need their own recalibration pass under this new "leftover budget" meaning, since whatever they were tuned against before doesn't apply to this role. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2
12 daysFix RescoreMovesViaSearch's fail-high handling: stop destroying theScott Gasch
winning move's own score, and stop discarding generate.c's ordering information for moves it never got to search. Two bugs, found while reading this function to understand it: 1. On fail-high, `goto end` jumped past the for loop's own x++, so x at the `end:` label still pointed at the move that just failed high -- whose iValue had just been correctly set to its real score two lines earlier. The clearing loop then started at that same x, immediately overwriting the winning move's own just-computed score with -INFINITY: exactly backwards, marking the one move IID found good enough to fail high on as worst-possible, while the inferior moves it beat kept their real scores and would be preferred instead. 2. Even with that fixed, every move after the winner was still set to -INFINITY -- total, deliberate amnesia about generate.c's original ordering estimate for moves we simply didn't get to (a fail-high means we stop early on purpose, to avoid burning nodes confirming what we've already decided to play). If the winner's fail-high doesn't hold up at full depth, the caller falls back to a list where every remaining move is a tied -INFINITY -- worse than never having run IID at all for that tail, and inconsistent with -INFINITY's use elsewhere in this function for genuinely-known-illegal moves. Restructured to defer committing to mvf[].iValue until it's known whether every move got an honest, fully-searched score (scores go into a local scratch array during the loop instead of directly into the move stack). On full completion, commit all of them and set fMovesRescoredByIID as before. On fail-high, commit nothing -- leave every move's original generate.c ordering value untouched, and bump just the winning move into killer-tier territory (same trick generate.c uses for a real killer move) so the normal, non-rescored selection path still tries it first. fMovesRescoredByIID stays FALSE in this case, since the ply's iValue is back to being generate.c's ordering encoding, not real scores. Measured (ecm_ringers.ep_/ecm_confident_quick.ep_/ecm_hard_quick.ep_, sn=5M): 10/88/9, recovering the confident_quick point lost by the previous IID-trust commit (was 10/87/9) with no cost elsewhere. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2
12 daysRemove RescoreMovesViaSearch's dead same-ply recursive pre-call.Scott Gasch
Before searching its own move list, RescoreMovesViaSearch called itself recursively at an even shallower depth, at the *same* ctx->uPly, on the theory that the extra rescore's side effects (hash/killer/history table population) would help the real loop's own -Search() calls find cutoffs faster. But the recursive call's own iValue writes were always fully overwritten by this same call's loop immediately after it (same ply, same move-stack range), so the only way it could possibly help was via those side effects. Measured directly: disabling it produced a bit-identical result across all three test suites (ecm_ringers.ep_, ecm_confident_quick.ep_, ecm_hard_quick.ep_ at sn=5M) -- no change whatsoever, not even a single position. It was pure wasted search effort. Removed. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YGSMkwjqiCk4XhbfN7ugD2
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 daysFix QSearch stand-pat gating bug and ComputeMoveScore's SEE contamination; ↵Scott Gasch
tune singular-reply-to-check margin. QSearch stand-pat: the "deny stand pat when material is genuinely in trouble" check was gated on fCouldStandPat (has this side had a chance to stand pat earlier in this qsearch line). That's wrong -- whether an ancestor node had a moment of safety says nothing about whether *this* node's material danger is real; a hanging piece doesn't stop hanging because the position was quiet three plies ago. fCouldStandPat's legitimate uses (search.c:950, 1190/1193) are about deciding whether a *whole line* looks forcing enough to justify extra qsearch depth/ breadth, a different question from per-node stand-pat correctness. Removed the gate; the material-in-trouble check now always denies stand-pat, regardless of history. ComputeMoveScore: for winning/even captures and promotions, the value extracted from the move-ordering sort key (generate.c's _ScoreAllMoves) included a flat +120 ordering bias plus small MVV-LVA tie-break nudges (PIECE_VALUE_OVER_100 terms) baked in on top of the real SEE/ material-diff value. Harmless for its original sorting purpose (every capture gets the same treatment), but this function's callers (futility pruning, the singular-reply-to-check extension) use the result as an eval-axis quantity compared against material-scale margins -- the contamination doesn't belong there. Subtracted the ordering-only bias back out to recover pure SEE/material-diff, same axis as the raw PIECE_VALUE() fallback used when no move-stack index is available. Quiet-move and losing-capture handling were already correct (both collapse to a clean, uncontaminated value). Also bumped the singular-reply-to-check margin (225 -> 400): confirmed via direct A/B on the quick suites that this is a real, independent improvement on top of the SEE fix, not just compensating for it -- reverting to 225 measurably regressed both ecm_confident_quick.ep_ (89->88/90) and ecm_hard_quick.ep_ (10->6/90) versus keeping 400. Verified against pristine baseline (no LMR in this binary) on the three-suite protocol (ecm_ringers.ep_, ecm_confident_quick.ep_, ecm_hard_quick.ep_, sn=5M, book disabled): 11/11 ringers (matches baseline exactly), 89/90 confident (vs baseline's 90/90), 10/90 hard (vs baseline's 4/90) -- a real net improvement over baseline with zero LMR involved, considerably stronger than any state reached earlier in this session's LMR-only experimentation. 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.
14 daysReplace 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-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]>
2016-06-01Initial checkin for typhoon chess engine.Scott Gasch