XGID <-> GNU position id converter - #147
Merged
Merged
Conversation
Adds src/XGID: bidirectional conversion between XGID strings and GNU Backgammon position ids, for the BGBlitz plugin-protocol bridge (nodots/backgammon#426). Both XGID dialects are supported and the dialect is a required argument everywhere. There is no in-band marker distinguishing them, and guessing corrupts the cube on every position: a field value v means cube v in the Berger dialect and 2**v in the canonical one, and v === 2**v has no solution. The corruption is also invisible in checker-play tests, since cube value barely affects best checker play. The overloaded rules field is decoded against matchLength — it means Crawford in a match and Jacoby/beavers in money play — and Draft 0.02's inability to express a pending double is surfaced rather than papered over: parseXgid rejects 'D' in that dialect and formatXgid refuses to emit it. Note that encodeGnuPositionId here is on-roll-first, the ordering gnubg itself produces, while Board/gnuPositionId.ts is opponent-first. Both now live in core; neither is changed by this commit. See the header comment in src/XGID for why. Verified: 43/43 pass, including six differential checks against gnubg over 10,000-board corpora (encoder, decoder, mutual inverses, and the XGID bridge in both turn directions). The differential suite skips when GNUBG_ADDON/GNUBG_WEIGHTS are unset, and was mutation-tested — flipping the bit order fails 9 of 43 rather than passing.
The converter's board field was already validated end-to-end against gnubg via the compiled addon. Its scalar semantics were not: field order, the cube logarithm, the overloaded rules field and `D` rested on the spec plus Draft 0.02's single worked example, because SetXGID is not among the addon's compiled sources. Adds three things: gnubgCli.test.ts drives the GNU Backgammon 1.07.001 CLI as an oracle. `set xgid` runs the reference parser and one `show` command per field reports how each was understood, so every scalar is now checked against an independently built binary rather than against our vendored copy. One process per position: gnubg carries state across `set xgid` within a session and reports a stale turn/owner for at least one combination, which `new session` does not clear. A process costs ~90ms. corpus.test.ts covers the scalar cross-product — 12,768 canonical and 12,432 berger combinations over cube value, cube owner, turn, all 38 dice values and eight match/money configurations, rotating through five boards spanning loaded bars and borne-off checkers. Plus negative fuzzing: 4,000 mutated strings must raise XgidError specifically, never a stray TypeError from inside the parser and never a plausible-but-wrong parse. Position fields in the corpus are derived from boards via formatXgid rather than written by hand. Two hand-written ones were illegal under the mirroring rule, which is exactly the mistake the helper prevents. Findings worth recording: - The cube-as-logarithm reading is confirmed against gnubg for every value 1..64, not just Frank's one example. - The overloaded rules field matches gnubg exactly: 0/1/2/3 -> none, Jacoby, beavers, both in money play, and Crawford in match play. - gnubg cannot hold a pending double either. `set xgid` with `D` says "SetMatchID cannot handle positions where a double has been offered" and steps back to the pre-double state. So /v1/take is unreachable through XGID on the canonical path too, and the spec's suggestion that adopting `D` would close that gap does not survive. Corrected in docs/spec-xgid-gnu-position-id.md. - gnubg reports the cube as disabled during the Crawford game rather than giving a value, which independently confirms the flag was read. Verified: 61/61 across four suites, typecheck clean. Mutation-tested — reading the canonical cube field as a raw value instead of a logarithm fails 10 tests across all three non-skipped suites rather than passing. The CLI suite skips when gnubg is absent.
The dialect names the format, not its author. BGBlitz is what the nine-field variant in Draft 0.02 belongs to, and the adapter's endpoint has been /bgblitz since it was written; this closes the naming gap between the two. Identifiers, error messages and prose only. No behaviour change: the field count still tells the dialects apart, cube encoding is unchanged, and 'D' is still rejected in the nine-field dialect.
nodots
force-pushed
the
feat/xgid-converter
branch
from
July 30, 2026 22:25
9aff752 to
80dec0d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
src/XGID— conversion between XGID strings and GNU Backgammon position ids. This is what the BGBlitz plugin bridge ingnubg-protocol-adapterneeds; that repo currently depends onfile:../backgammon-corebecause the converter is not in a published core.What is here
d6201afthe converter17bac4fa gnubg CLI oracle closing the scalar-semantics gap9aff752renames the nine-field dialect frombergertobgblitz, matching the adapter's/bgblitzendpointZero imports — the module depends on nothing else in core. Format details come from
docs/spec-xgid-gnu-position-id.mdin the workspace, written from observed behaviour rather than from gnubg sources, so no GPL-derived content enters via this route.Two things worth knowing before review
dialectis a required argument everywhere, deliberately.canonicalencodes the cube as a base-2 logarithm;bgblitzencodes it literally. Draft 0.02 provides no in-band marker, so a wrong default silently corrupts the cube — and that corruption is invisible in checker-play tests, because cube value barely affects best play. The dialects are told apart by field count (10 vs 9).There are now two GNU position id encoders in core, with opposite side orderings.
encodeGnuPositionIdhere is on-roll-first, the ordering gnubg itself produces, established empirically against the compiled addon.Board/gnuPositionId.ts'sexportToGnuPositionIdis opponent-first, the engine protocol's defaultpositionIdConvention. Neither is "the" encoder; an id is only meaningful alongside its convention. The header comment onsrc/XGID/index.tssays so, and they are not interchangeable.Testing
npx jest src/XGID— 55 pass, 6 skipped. The skipped suite iscrossValidate, gated on a binary that is not present locally;gnubgClidoes run and passes.Full suite: 579 pass, 18 skipped, and 3 suites fail to run —
gnuPositionId,gnuPositionIdBar, and one sibling. Those crash with SIGABRT inside the native gnubg addon, which aborts atEvalInitialisebecausegnubg.wdandgnubg.weightsare absent fromnode_modules. Verified pre-existing: the identical failure reproduces ondevelopmentat the same commit range, and the abort happens at addon load before any code on this branch runs.