<feed xmlns='http://www.w3.org/2005/Atom'>
<title>typhoon.git, branch master</title>
<subtitle>The typhoon chess playing engine.
</subtitle>
<id>https://git.acknak.org/cgit/typhoon.git/atom?h=master</id>
<link rel='self' href='https://git.acknak.org/cgit/typhoon.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/'/>
<updated>2026-10-25T18:20:41Z</updated>
<entry>
<title>Remove the x86/x64 assembly backend; use compiler intrinsics instead</title>
<updated>2026-10-25T18:20:41Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-25T18:20:24Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=c1e8cefc7bf0c4017dda24b0486806ce3304a7ae'/>
<id>urn:sha1:c1e8cefc7bf0c4017dda24b0486806ce3304a7ae</id>
<content type='text'>
x86.asm/x64.asm were already mostly dead weight -- GetAttacks's real
asm body was retired 2026-09-06 (section 7 of
board_representation/MIGRATION.md), and LockDecrement/
CanUseParallelOpcodes/ParallelCompareUlong/ParallelCompareVector had
zero remaining callers (confirmed by grep before deleting anything).
What was still real and load-bearing:

- CountBits/FirstBit/LastBit: popcount/bsf/bsr CDECL calls. Already
  had faster, compiler-intrinsic replacements sitting right next to
  them in chess.h (FastCountBits/FastFirstBit/FastLastBit, via
  __builtin_popcountll/__builtin_ctzll/__builtin_clzll with -mpopcnt)
  that per-file #ifndef CROUTINES blocks in board.c/eval.c/generate.c/
  probe.c/recogn.c were already redirecting individual call sites to.
  Promoted that redirect to the plain names unconditionally
  (#define CountBits FastCountBits etc. in chess.h) and dropped the
  now-always-true CROUTINES guards in those five files -- CountBits'
  real asm body was a Kernighan bit-clearing loop (~23 cycles/op per
  gmake TEST=1's own benchmark), not even O(1), so this is strictly
  faster as well as removing the toolchain dependency.
- LockCompareExchange/LockIncrement: real atomics backing
  AcquireSpinLock/TryAcquireSpinLock/ReleaseSpinLock (util.c) and the
  move-pool allocator (gamelist.c). Replaced with static inline
  wrappers over __sync_val_compare_and_swap/__sync_add_and_fetch --
  same CAS/atomic-add semantics as the retired `lock cmpxchg`/
  `lock xadd` instructions.

Removes the yasm/nasm build dependency entirely: GNUmakefile's
NASM/NASMFLAGS variables, the CROUTINES flag, the .asm-&gt;.o build rule,
and x86.o/x64.o from every OBJS list. x86.asm/x64.asm deleted outright
(git history keeps them if ever needed again). The ASM=1 debug mode
(-S -fverbose-asm, a C-compiler assembly-listing dump, unrelated to
the deleted files despite the similar name) is untouched.

Verified: clean build with no yasm/nasm invocation anywhere in the
log, a clean DEBUG=1 TEST=1 self-test run, and node-count parity
against the previous commit on all three curated suites at sd10
(4,003,856 / 22,308,015 / 26,868,016 on both sides) -- confirms this
is a pure substitution with zero behavioral change, not just "doesn't
crash."

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Fix SIGSEGV in GetNthOfficialMoveRecord on an exact-length index</title>
<updated>2026-10-25T18:05:10Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-25T18:05:10Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=455accb4cbb1777cb553b6d75136521c6b8d1c01'/>
<id>urn:sha1:455accb4cbb1777cb553b6d75136521c6b8d1c01</id>
<content type='text'>
GetNthOfficialMoveRecord's end-of-list check only tested whether its
decrementing counter n reached 0, not whether the walk pointer p had
also reached the move list's own sentinel head at the same moment.
Requesting index == the list's real length (exactly what every
caller's "walk until NULL" loop does as its final probe) hit both
conditions simultaneously, so the function treated the sentinel's own
DLIST_ENTRY as a GAME_MOVE via CONTAINING_STRUCT and handed back a
pointer to g_GameData.sMoveList's own links field reinterpreted as a
move record -- q-&gt;mv.uMove read as nonzero garbage (never actually 0,
so the "is this real" check downstream never caught it), and
q-&gt;szMoveInSan was effectively an arbitrary pointer.

Reported as: play a few moves (e.g. e4 e5 Nc3), run "dump moves" -&gt;
crash. Confirmed via lldb: SIGSEGV in vsnprintf/strlen, called from
Trace() in DumpGameList (gamelist.c:575) handed that garbage
szMoveInSan pointer. book.c's two GetNthOfficialMoveRecord loops
(opening-book move matching) have the identical exposure and are fixed
by the same change at the source.

Fix: also require p != &amp;g_GameData.sMoveList before treating it as a
real node.

Verified: "dump moves"/"dump pgn" after several moves (both odd- and
even-length move lists, so the off-by-one's exact trigger condition is
covered on both sides) no longer crash, and a clean DEBUG=1 TEST=1
self-test run shows no regressions.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Enable readline by default for command-line history/editing/completion</title>
<updated>2026-10-25T17:54:23Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-25T17:54:23Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=fc873933e21d9188f2b5a9d13764f00b6ce68ec9'/>
<id>urn:sha1:fc873933e21d9188f2b5a9d13764f00b6ce68ec9</id>
<content type='text'>
USE_READLINE was a complete, working, opt-in-only feature -- input.c's
readline()/add_history() integration had no known reason to still be
gated behind a flag nobody was passing. Flips it to default-on
(renamed to a DISABLE_READLINE opt-out, matching the project's own
ifndef-DISABLE_* convention) and fixes the build: readline/history
live under the FreeBSD ports prefix (/usr/local), which this box's
clang/ld don't search by default, so linking bare -lreadline failed
until -L/usr/local/lib (LIBRARIES) and -I/usr/local/include (PROFILE)
were added alongside it.

Unlike the GENERATE_*_BITBOARD cleanup, kept DISABLE_READLINE as a
real escape hatch rather than removing the toggle outright -- this
introduces an actual external library dependency (readline-devel),
not just dead source-level conditionality, so a box without it
installed still needs a way to build.

Verified: both the new default build and `DISABLE_READLINE=1` build
clean (confirmed via ldd that only the latter has no libreadline.so
dependency), and a clean DEBUG=1 TEST=1 self-test run under the new
default with no assertion failures. Batch mode (used by all of this
project's scripted testing) never touches the input thread/readline
path at all, so this has no effect there.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Make NNUE eval the default, same convention as the EGTB path default</title>
<updated>2026-10-25T17:47:41Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-25T17:47:41Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=93ddedd408cfebb162068d458ea42484cd3f98d2'/>
<id>urn:sha1:93ddedd408cfebb162068d458ea42484cd3f98d2</id>
<content type='text'>
main.c's InitializeOptions now tries to load "eval.nnue" (a file next
to the binary, matching typhoon_nnue's own NNUE_WEIGHTS default --
typically a symlink into head_reference/nnue/current/weights.nnue)
after argument parsing, if the user hasn't already enabled NNUE
explicitly via --nnueweights or an interactive "nnue load"/"nnue on".
Same hardcoded-default-path convention main.c already uses for
--egtbpath (/zscratch/egtb).

Fails soft: a checkout without eval.nnue present, or one where it
fails to load, just falls back to HCE eval exactly as before -- this
is a default, not a requirement, so there's no UtilPanic the way
--nnueweights's own explicit-request failure path has. The existing
interactive "nnue off" command remains the way to explicitly opt back
out of NNUE for a given session; a script that wants guaranteed HCE
(e.g. an apples-to-apples eval comparison) should add "nnue off;" to
its own --command string rather than relying on the old no-flag
default.

Verified: clean release build, clean DEBUG=1 TEST=1 self-test run (no
assertion failures), and manually confirmed both the default-load path
(eval.nnue present in cwd) and the silent-fallback path (run from a
directory without it) behave as expected; --nnueweights still works
unchanged.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Make bitboard move generation unconditional; delete the mailbox fallback</title>
<updated>2026-10-25T17:39:58Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-25T17:39:58Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=f97b2710981e936cfb500ad0cdf1ceab233efad8'/>
<id>urn:sha1:f97b2710981e936cfb500ad0cdf1ceab233efad8</id>
<content type='text'>
All GENERATE_*_BITBOARD/EXPOSESCHECK_BITBOARD/ISATTACKED_BITBOARD/
UNBLOCKED_BITBOARD flags were already default-on (ifndef
DISABLE_BITBOARD_MOVEGEN); this removes that opt-out entirely (GNUmakefile)
and deletes the mailbox-era code the flags used to toggle between, since
there's no longer a build configuration that still needs it:

- generate.c: the mailbox per-piece generators and escape-generators
  (GenerateKnight/GenerateWhiteKnight/GenerateBishop/GenerateRook/
  GenerateQueen/GenerateBlackKing/GenerateWhiteKing/GenerateWhitePawn/
  GenerateBlackPawn and their SaveMe* escape-generator counterparts),
  _FindUnblockedSquares and its DEBUG-build cross-check inside
  _FindUnblockedSquaresBB, and the dispatch-table #if/#else pairs that
  chose between mailbox and bitboard functions (now unconditionally
  bitboard). WouldGiveCheck simplified to its UNBLOCKED_BITBOARD-only
  body. g_iBDeltas/g_iRDeltas (orphaned once their only consumers were
  deleted) removed; g_iNDeltas (still used by data.c's InitMagic) moved
  there, next to its real consumer.
- movesup.c: mailbox FasterExposesCheck/ExposesCheck/ExposesCheckEp/
  IsAttacked/InCheck deleted; chess.h's three-way macro choice
  simplified to a direct #define onto the BB versions (ExposesCheckBB/
  IsAttackedBB/etc. are simply "the" implementation now).
- testsee.c/testgenerate.c: deleted the mailbox-vs-BB comparison/speed
  harnesses that only existed to validate code just removed
  (TestIsAttackedBB, TestGenerateKnightSpeed/KingSpeed/RookSpeed/
  BishopSpeed/QueenSpeed, TestFindUnblockedSquaresSpeed's mailbox arm).
  TestGetAttacks and the _GenerateAllMoves-vs-_GenerateAllMovesBB
  dispatch-overhead benchmark are unaffected -- GetAttacks' mailbox/asm
  predecessors were already retired independently, and that pair
  compares two still-live BB-only dispatch strategies, not mailbox vs
  BB.

Verified: clean release build and a clean DEBUG=1 TEST=1 self-test run
(move generation, perft-style legal-move checks, SAN/ICS parsing,
search smoke test) with no assertion failures.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Make the FH% regression model the default; remove data.c's retired tables</title>
<updated>2026-10-25T17:16:16Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-25T17:16:16Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=3927a4cac6ec254a8e34a5e864300f75c3efc9f3'/>
<id>urn:sha1:3927a4cac6ec254a8e34a5e864300f75c3efc9f3</id>
<content type='text'>
Flips FH_USE_REGRESSION_MODEL from an opt-in safety toggle to the only
behavior: fhmodel.c's quantized logistic regressions are now always
used, with no build-time choice left between them and the old
histogram tables. Removes g_uJointFHThousandths/g_uBetaFHThousandths
and their _NNUE counterparts from data.c (4085 lines), their extern
declarations from chess.h, and the now-dead
_JointTablePredictedFHThousandths/_BetaTablePredictedFHThousandths
lookup functions and their supporting bucket helpers
(HangCpBucket/GapCpBucket/MoveNumBucket/DepthBucket/ImproveBucket/
GapBetaBucket/_ShipDepthBucket/_ShipLateBucket) from searchsup.c, along
with the uRawDepthB/uRawImproveB plumbing that only existed to feed
them (search.c, split.c, SearchOneMove's parameter list).

Also drops the beta-side static-nullmove skip threshold's provisional-
96000 #else branch -- 99000 is now simply the default, confirmed (not
provisional) by docs/REGRESSION.md's trust-audit sweep (+291 Elo over
96000 at 200 games, LOWER95=0.79).

Verified: clean release build, clean DEBUG=1 TEST=1 self-test run (no
asserts), and a fresh head_reference/ checkpoint
(head-reference-20261025 -&gt; 874ef04's regression-model binary) with
sd10/sn5000000 solve counts on the three curated suites essentially
matching the previous table-based checkpoint (70/191 vs 71/191 sd10,
93/191 vs 96/191 sn5M).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Learned FH% logistic-regression model (alpha full, beta whole-node-only)</title>
<updated>2026-10-25T16:55:30Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-25T16:55:30Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=874ef0478078ef4eb436af7d2b68a353f61f4d70'/>
<id>urn:sha1:874ef0478078ef4eb436af7d2b68a353f61f4d70</id>
<content type='text'>
Replaces the data.c histogram tables (g_uJointFHThousandths/
g_uBetaFHThousandths and their _NNUE counterparts) with two quantized
logistic regressions as the default FH%-prediction mechanism under
FH_USE_REGRESSION_MODEL=1, trained against real collected search data
(fhcollect.c) per docs/REGRESSION.md.

Alpha side is a full per-move regression, validated against the old
table (0.119pp weighted mean deviation across 10,684 matched cells).
Beta side is whole-node-only (matching the old table's own scope --
no functionality lost), refit on the NO_MOVE population after an
earlier mixed-tier fit saturated and over-pruned; its skip threshold
(FH_BETA_SKIP_THRESHOLD=99000) was confirmed by direct self-play
sweeps, not just ECM solve-count intuition (see docs/REGRESSION.md's
"Category A vs B" discussion).

Gated by an overnight SPRT match (regression vs HEAD tables, same
NNUE eval, sd8): at 4120 games, llr=+2.53 (never dipped negative,
peaked +2.8), score=0.517, candidate reaching comparable depth at
higher nps/ebf than head. Did not cross the H1=+2.94 SPRT bound, but
the trend across the full run is unambiguous and the marginal value of
waiting out the remaining games was judged not worth it.

The data.c tables are left in place as the inactive #else branch of
FH_USE_REGRESSION_MODEL for now; a follow-up commit will flip the
build default and remove them once this commit's head_reference
baseline and ECM numbers are recorded.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>More aggressive move reductions; Wilson bound overrides static signal when available</title>
<updated>2026-10-24T16:07:03Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-24T16:07:03Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=5f901d737b17c5f99b6e456d6c0c7d5e3c093dc4'/>
<id>urn:sha1:5f901d737b17c5f99b6e456d6c0c7d5e3c093dc4</id>
<content type='text'>
SPRT playtest (2000 games, sd/depth matched): -535 =875 +590, score=0.514,
llr=+1.04 (floated around +1.0 the whole run, never went negative) toward
H1=+2.94. Not a confirmed win, but searches meaningfully deeper (avg depth
14.35 vs 13.50, EBF 3.131 vs 3.305) at no measurable strength cost -- worth
banking and continuing to accumulate games against.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>NNUE search integration: FH% tables, NODE_TYPE refinement, TREEDUMP rewrite</title>
<updated>2026-10-23T20:19:17Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-23T20:19:17Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=69525eb2f42ea9cca8255f6cc3b85e6b1312fd05'/>
<id>urn:sha1:69525eb2f42ea9cca8255f6cc3b85e6b1312fd05</id>
<content type='text'>
- data.c/chess.h: widen the joint FH% table's late axis 16-&gt;17, dedicating
  the new bucket 0 to a whole-node ("will something here fail high at
  all") sample rather than sharing it with the first-leftover-move curve.
  Add an NNUE-eval counterpart table (g_uJointFHThousandths_NNUE) and an
  NNUE counterpart of the beta-side table, selected at runtime based on
  whether NNUE eval is active.
- search.c/searchsup.c: add _RefineNodeTypeGuess, which uses the FH%
  tables (via g_NodeTypeGuessTable) to refine a node's NODE_TYPE guess
  instead of the old heuristic; thread uParentMoveFHEstimate through
  PLY_INFO so a node can see its parent's prediction for the move that
  led to it.
- generate.c: initialize sKillers[2]/[3].uBonus unconditionally (an
  uninitialized-stack read caught by valgrind under LTO; harmless today
  but a real bug).
- dumptree.c/chess.h/GNUmakefile: replace the long-dead DUMP_TREE XML
  tree dump (written for a GUI this project no longer has) with TREEDUMP,
  a flat, path-keyed move/result trace meant to diff against an
  identically-instrumented Stockfish build -- see docs/TREEDUMP_FORMAT.md.
- docs/DEEPER_PRUNING.md: notes on closing the search-efficiency gap vs
  Stockfish.
- crafty_egtb/stockfish_xboard/typhoon_nnue: wrapper scripts so
  match_play.py's Engine class can drive Crafty, Stockfish (via
  PolyGlot), and an NNUE-enabled typhoon the same way it drives HCE
  typhoon.
- tests/lop: rename lop.txt -&gt; lop.ep_; update compare_lop.cpp.
- annotate_pgns: add aggregate_marginsafety.py, dedup_pgn.py,
  fit_lazy_probe.py; update annotate.py/run_nnue_pipeline.py for the NNUE
  training pipeline.
- .gitignore: ignore compile_commands.json (clangd build DB) and
  eval.nnue (a symlink into the untracked head_reference/ comparison
  baseline), neither of which belongs in the repo.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>Restore 5D-alpha + 4D-beta FH% collection (improve axis + whole-node bucket), fix two bugs found via validation</title>
<updated>2026-10-21T02:46:51Z</updated>
<author>
<name>Scott Gasch</name>
<email>scott@gasch.org</email>
</author>
<published>2026-10-21T02:46:51Z</published>
<link rel='alternate' type='text/html' href='https://git.acknak.org/cgit/typhoon.git/commit/?id=6b883e83935f5e8110788565cfcb77cd6fbf8950'/>
<id>urn:sha1:6b883e83935f5e8110788565cfcb77cd6fbf8950</id>
<content type='text'>
Restores the full dual-table FH% collection instrumentation under
FH_TABLE_COLLECTION, which had regressed to a 4D-only alpha joint with
no beta side at all (see the prior commit's note): adds back the
`improve` axis to the alpha joint and a new 4D beta joint (depth,
gap-to-beta, own-improve, enemy-improve), both with real INC() call
sites and ALPHARAW/BETARAW/ESTRAW dump lines matching dual_table_run/'s
existing (unmodified) aggregate.py/dashboard.py/marginal_crosscheck.py
parsers exactly.

Also adds a genuine NODE-level sample into the alpha table's previously
unused late==0 bucket: "will something at this node fail high" when
iEval &lt;= iAlpha, ground truth = the real move loop's outcome. This is
the alpha-side analog of the beta table (which only covers iEval &gt;
iBeta) -- together they cover every null-window child's self-estimate
with no gap, motivating a follow-up (not part of this change) on
combining a parent's per-move FH% prediction with a child's own
node-level self-estimate for a sharper NODE_CUT/NODE_ALL signal than
CommonSearchInit's current purely-structural PVS alternation.

Collecting real ground truth for both new node-level samples requires
not taking the beta-side static-null-move shortcut's early `goto end`
during collection builds, same reasoning as the existing LMP disable.

Two bugs found and fixed via validation before trusting this for a
multi-day run:

1. goto-skips-initializer: Search()'s early QSearch-depth `goto end`
   jumped over the new collection variables' declarations, leaving
   them uninitialized (indeterminate stack garbage) on that path. Only
   manifested in the release (-O3) build -- a DEBUG (-O0) build worked
   by accident (the skipped stack slots happened to read as zero).
   Caught via lldb on a real segfault: garbage "eligible" flags feeding
   wildly out-of-range array indices. Fixed by moving the declarations
   before the early goto.

2. MAX_SPLITS amplification: the new arrays were nested inside
   COUNTERS.tree, which is embedded both in SEARCHER_THREAD_CONTEXT and
   in split.c's g_SplitInfo[MAX_SPLITS] array -- so the ~19MB growth
   got replicated 64x, inflating the binary's BSS from ~220MB to
   ~1.47GB and breaking valgrind's fixed-address mmap outright (harmless
   at runtime for a --cpus 1 collection shard, since g_SplitInfo's
   copies are never touched, but it also meant any real MP split would
   have silently lost a helper thread's collected data, since no
   merge-back code exists for these fields). Fixed by moving them into
   a new standalone FH_COLLECTION_COUNTERS struct referenced only from
   SEARCHER_THREAD_CONTEXT, not from SPLIT_INFO. BSS dropped to ~281MB;
   valgrind (--track-origins=yes) now runs clean: 0 errors, 0 bytes
   definitely/indirectly/possibly lost.

Validated end-to-end against real NNUE weights (--nnueweights
eval.nnue), release build, full tests/ecm_ringers.ep_ suite at sd 10:
ALPHARAW/BETARAW/ESTRAW all populated (including real late==0 "whole
node" rows), dual_table_run/'s existing aggregate.py and
marginal_crosscheck.py parse it unmodified -- depth/gap/improve axes
match exactly; the only two "mismatches" (dumb and late bucket 0) are
exactly and fully explained by the new whole-node samples, which
deliberately don't feed those two per-move marginals (hang=0/late=0
are placeholders there, not real measurements) -- not a real bug, and
reproduced byte-for-byte identical after the struct relocation above.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
