Skip to content

XGID <-> GNU position id converter - #147

Merged
nodots merged 3 commits into
developmentfrom
feat/xgid-converter
Jul 30, 2026
Merged

nodots merged 3 commits into
developmentfrom
feat/xgid-converter

Conversation

@nodots

@nodots nodots commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Adds src/XGID — conversion between XGID strings and GNU Backgammon position ids. This is what the BGBlitz plugin bridge in gnubg-protocol-adapter needs; that repo currently depends on file:../backgammon-core because the converter is not in a published core.

What is here

  • d6201af the converter
  • 17bac4f a gnubg CLI oracle closing the scalar-semantics gap
  • 9aff752 renames the nine-field dialect from berger to bgblitz, matching the adapter's /bgblitz endpoint

Zero imports — the module depends on nothing else in core. Format details come from docs/spec-xgid-gnu-position-id.md in 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

dialect is a required argument everywhere, deliberately. canonical encodes the cube as a base-2 logarithm; bgblitz encodes 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. encodeGnuPositionId here is on-roll-first, the ordering gnubg itself produces, established empirically against the compiled addon. Board/gnuPositionId.ts's exportToGnuPositionId is opponent-first, the engine protocol's default positionIdConvention. Neither is "the" encoder; an id is only meaningful alongside its convention. The header comment on src/XGID/index.ts says so, and they are not interchangeable.

Testing

npx jest src/XGID — 55 pass, 6 skipped. The skipped suite is crossValidate, gated on a binary that is not present locally; gnubgCli does 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 at EvalInitialise because gnubg.wd and gnubg.weights are absent from node_modules. Verified pre-existing: the identical failure reproduces on development at the same commit range, and the abort happens at addon load before any code on this branch runs.

nodots added 3 commits July 30, 2026 16:25
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
nodots force-pushed the feat/xgid-converter branch from 9aff752 to 80dec0d Compare July 30, 2026 22:25
@nodots
nodots merged commit 3440b5a into development Jul 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant