GammonView
The backgammon analysis backend behind
gammonview.com: read and write the site's
.gvab/.gva match files, import .mat/.xg/.bgf, analyze a match or a
single position with the bgsage engine, and
compute per-decision error rate (PR), luck and cube statistics.
Those files are based on OGXM — the chunked match format from HedgeHog, not ours — extended to carry what bgsage's analysis produces. This repo reads OGXM; what it writes is that extended form. See The format.
It ships as two importable Python packages, plus a JavaScript port of the codec for the browser:
gvformat— the pure-stdlib codec: read/write the.gvabbinary,.gvaJSON conversion,.xg/.bgfimport, compute-on-read stats, OGID. Zero third-party deps —pip install gammonviewandimport gvformatanywhere, no engine required. This is the format layer: everything else here, Python and JavaScript alike, reads and writes through it.gvanalysis— the bgsage-powered analysis:analyze_match(a match file in, analyzed.gvabbytes out),analyze_file(the same work stopping at the OGXM document), and single-position analysis. Depends ongvformat+ bgsage (the[engine]extra). Dependencies flow one way:gvanalysis→gvformat, never back.@gammonview/gvformat— the same codec in JavaScript, on npm. What reads the analysis in the browser. See In the browser.
Install
To use the packages, install from PyPI — no clone needed:
pip install gammonview # the codec alone (gvformat), no engine
pip install "gammonview[engine]" # adds bgsage, for gvanalysis
To work on them, clone the repo and let uv build the environment — it installs both packages in editable mode, plus bgsage and the test dependencies:
git clone https://github.com/ngvlamis/gammonview-analysis.git
cd gammonview-analysis
uv sync
uv run python tests/run_all.py --fast # ~3s, no engine needed
Every command below is written uv run … for that checkout; from a plain
pip install, drop the prefix.
Quickstart
Server one-liner — analyze a dropped match and get the analyzed .gvab back,
ready to store and ship to the client:
from gvanalysis import analyze_match
data = analyze_match("match.xg", preset="world_class") # analyzed .gvab bytes
It runs the engine over every decision, and the bytes carry the match and the
analysis. Any input the codec reads works — .xg, .bgf, .mat, .gva,
.gvab — and one that already carries analysis keeps it, with ours appended, so
an .xg comes back with XG's judgement and ours side by side.
It returns .gvab (binary), always — that is the artifact to store and ship.
For the JSON, read_gvab(data) is the .gva; to stop a step earlier and work
with the document itself, analyze_file returns it directly. (The CLI is the
other way round: gvan-match writes .gva unless asked for --gvab, because
a file you open by hand is likelier to want to be readable.)
The format
The files this repo reads and writes — .gvab (binary) and .gva (JSON) — are
based directly on OGXM (OpenGammon eXtensible Match), the chunked match
format from HedgeHog, and
extend it with what GammonView's viewer shows and OGXM has no field for: luck,
per-decision PR and illegal-move flags, and per-alternative eval levels — all in
one GVAN chunk — plus match metadata like event, site and cube_limit.
We read OGXM. read_gvab takes a file this repo never wrote, including one
carrying another engine's analysis, and gvan-match appends its own analysis
block rather than replacing what is already there.
We make no claim in the other direction, and that is about us, not about OGXM.
One of our choices is a genuine departure: in match play we store normalized
equity in the three cube equity fields, where the base spec specifies raw
MWC. We do it for quantization — these fields are an int16 at 1e-4, which
gives normalized equity about 20,000 usable steps at any score, while raw MWC
has as few as 16 at a lopsided one — and we recover MWC on read from the
shipped MET. It is the right trade for us and it is still a difference a reader
cannot see: same fields, same layout, different numbers.
So: treat these as GammonView files that an OGXM reader may well be able to make
partial sense of, not as OGXM files with extras. The two spec documents describe
what this repo writes — the base spec with every addition marked
[GammonView extension] and every divergence marked, including where a base
reader would go wrong. The upstream specs they track are in HedgeHog's docs/
and remain the authority on the base format.
In the browser
The codec has a JavaScript twin, published to npm as
@gammonview/gvformat:
npm install @gammonview/gvformat
import { read_gvab, convert_mat, parse_ogid } from '@gammonview/gvformat';
const match = read_gvab(bytes); // .gvab -> .gva JSON
const dropped = convert_mat(matText); // .mat -> .gva JSON, no analysis
const pos = parse_ogid(match.games[0].plies[0].ogid_before);
An ESM port of gvformat — the same reads, writes, source converters and OGID
handling, with no engine, no WebAssembly and no build step. It is how
gammonview.com parses a dropped .xg/.bgf/.mat client-side and renders the
.gvab the Python side produced.
The port lives in gvformat-js/ and is maintained here rather
than downstream. Python and JavaScript must agree byte-for-byte — the same
.gvab in gives the same .gva JSON out — and the two test suites check that
from either side.
There is no JavaScript port of gvanalysis: analysis needs the engine, and the
engine is Python-side only.
Command-line tools
Entry points are defined in pyproject.toml and run with uv run:
| CLI | What it does |
|---|---|
gvan-match |
Analyze a match (.xg, .bgf, .mat, .gva/.ogxm, .gvab), compute per-move PR → .gva/.gvab |
gvan-batch |
The same over many files or a directory, one output each |
gvan-position |
Analyze a single position from an XGID or OGID |
xg2gva |
Convert an eXtreme Gammon .xg file to .gva JSON (no engine) |
bgf2gva |
Convert a BGBlitz .bgf file to .gva JSON (no engine) |
uv run gvan-match match.xg --preset world_class --gvab # an .xg straight in, binary out
uv run gvan-match match.gvab --preset fast # re-analyze a binary, append
uv run gvan-match match.mat --link # print a shareable URL
uv run gvan-batch matches/ --preset fast # a dir -> its match files
uv run gvan-position "XGID=-b----E-C---eE---c-e----B-:0:-1:1:31:0:0:0:0"
uv run gvan-position "11ccccchhhjjjjj:66666888dddddoo:N0N:13:B:R:0:0:0:0"
An input keeps any analysis it already carries — ours is appended as a new
block rather than replacing it. (xg2gva/bgf2gva exist for conversion on its
own; analysis does not need them.) --link encodes the whole match into a
gammonview.com URL fragment, so it needs no hosted file; --browser opens it.
Analysis presets
--preset selects an eXtreme Gammon–style scheme: a cheap first pass screens
every decision, and a stronger pass runs only where it is needed. The five
built-ins:
| Preset (aliases) | 1st pass | Middle tier | 2nd pass | XG equivalent |
|---|---|---|---|---|
very_quick (vq) |
2ply |
— | — | Very quick |
fast (f) |
2ply |
— | 3ply |
Fast |
deep (d) |
3ply |
— | — | Deep |
world_class (wc) |
3ply |
4ply (both kinds) |
truncated2 |
World Class (XG Roller+) |
world_class_fast (wcf) |
3ply |
truncated2 (cube only) |
truncated2 |
World Class (3-tier) |
The second pass runs on an error; the middle tier runs on a borderline
decision the screen would otherwise have settled. fast is the default.
Which one to use
493 matches were analyzed by XG at World Class and by each of the strong presets, then compared decision by decision:
| mean gap to XG's match PR | same checker play as XG | PR within 1 of XG | cost, ms/ply (24c → 4c) | |
|---|---|---|---|---|
world_class |
0.423 | 90.3% | 91.7% | 291 → 2048 |
world_class_fast |
0.431 | 90.0% | 91.3% | 166 → 1016 |
Re-measured 2026-09-20 over the same 493 matches after the retune. The retune moved both presets about 0.01 PR closer to XG, which is not a significant improvement — the paired confidence intervals span zero — but does establish that it cost nothing. The checker-play column is carried over from the original study; a like-for-like check says the retune moves it by at most 0.6 points, but its exact figure could not be reproduced (see the write-up).
Mean gap is the average distance between the two ratings of the same
player-match, in either direction: rate a match with world_class and XG rates
it 0.434 PR away, typically. It is not what following the preset costs you —
priced by XG's own equities, a player who made world_class's recommended
checker play every time would post a PR of 0.333 and world_class_fast
0.353. Both are inside what XG itself calls world-class play. (Those two
figures predate the retune and were not re-measured; everything else in this
section was.)
Cost is milliseconds per ply over six matches, on a 24-core M2 Ultra and a
4-core i5-7600 — absolutes rather than a multiple of fast, because that
multiple is not constant across machines. Individual matches spread about 2×
around these means. See the timings.
world_class_fastis the recommended analysis. It lands 0.008 PR behind the deepest preset for a little under half the time — a difference the 493-match corpus cannot resolve from zero (95% CI [−0.020, +0.004]). The two sit three times closer to each other (0.136) than either does to XG, because what separates them from XG is the engine, not the search depth — 95% ofworld_class_fast's squared PR error against XG is that engine floor, which no preset can spend its way past.world_classis for reproducing XG specifically, not for being more accurate. Its only real edge is on borderline cubes, and it gets there by rolling them out less often thanworld_class_fastdoes — which is XG-like, not demonstrably better. It costs roughly twice as much for that.- If you want cheap, drop to
deep, which costs an order of magnitude less than either. There is nothing between them: abalancedpreset used to sit in that gap, but after the September 2026 retune it cost about whatworld_class_fastcosts while agreeing with XG less often, so it was retired.
Every number above measures similarity to XG, which is not the same as being
right — where the two engines differ there is no arbiter here to say which is
correct. docs/PRESET_ACCURACY.md is the full
experiment: how the presets split on checker play versus the cube, why rolling
out costs agreement with XG, and the evidence that the residual disagreement
is systematic rather than noise.
Presets live in gvanalysis/presets.py and are overridable via presets.yaml.
See docs/CLI.md for the full reference — every flag, how the
tiers fire, parallelism, and preset overrides.
Documentation
| Doc | What it covers |
|---|---|
docs/CLI.md |
Full command-line reference — every flag, presets, parallelism |
gvformat-js/README.md |
The JavaScript package — API, layout, parity rules |
CONTRIBUTING.md |
How to run the tests, the conventions, what a good change looks like |
CHANGELOG.md |
Release history, including the measurements behind several design decisions |
CLAUDE.md |
Maintainer and coding-agent notes: design rationale, measurements, and the rituals around an engine upgrade |
docs/OGXM_JSON_SPEC_GAMMONVIEW.md |
The JSON shape this repo writes (match → game → ply → analysis) — base spec plus every GammonView addition, marked |
docs/OGXM_FORMAT_SPEC_GAMMONVIEW.md |
The binary .gvab chunk layout, including the GVAN extension chunk |
docs/OGXM_COMPUTED_FIELDS.md |
Fields the reader computes rather than stores (PR, luck-MWC, MWC, classification) |
docs/METHODOLOGY.md |
PR / luck / cube-decision methodology and filters |
docs/PRESET_ACCURACY.md |
What each preset costs in accuracy, measured against XG over 493 matches |
Licence
MIT. Every source file carries an SPDX header; LICENSE holds the
full text.
MIT because what this repo mainly offers is a format, and a format wants to be copied in pieces — one routine lifted into another project, or the codec ported to another language. It also stays compatible in both directions at once, which matters in this ecosystem: GNU Backgammon is GPL, eXtreme Gammon and BGBlitz are proprietary, and all of them are welcome to interoperate. HedgeHog, whose format this implements, is MIT too.
bgsage, the engine behind the optional
[engine] extra, is MPL-2.0, which constrains nothing here. MPL is file-level
copyleft with no linking trigger: it attaches to its own files, and §3.3
expressly permits distributing a larger work under other terms. bgsage resolves
from PyPI at install time and is never modified or redistributed here, and
gvformat does not touch the engine at all.
The JavaScript mirror (gvformat-js/, published as
@gammonview/gvformat) is under the same licence and ships its own copy.
The GammonView logo (assets/logo.png) is excluded: it identifies the site,
so it is not part of the MIT grant. Fork the code freely; use your own mark.
Third-party material
Two pieces of third-party material are incorporated, both permissively licensed. Their notices must be preserved — see THIRD-PARTY-NOTICES.md, which reproduces them in full:
- HedgeHog (MIT, © 2026 Eran Lambooij) — OGXM is HedgeHog's format. Most of
gvformatis an independent implementation, butbinary.pyandexport.py(and their JS mirrors) are genuine ports of its C++ sources: the chunk struct layouts andparse_ply_analysis_jsonfromogxm_io.cpp/ogxm_json.cpp, and_TurnStatefromogxm_replay.cpp'sreplay_game(). Those four files say so in their headers. - Kazaross-XG2 MET (© 2011 Neil Kazaross; transcribed for GNUbg by Michael
Petch) — the match-equity table embedded in
gvformat/met.pyandgvformat-js/src/met.js. It ships with GNU Backgammon but is not under its GPL; its own terms require only that the notice be preserved, and it is reproduced at the top of both files.
Metadata
Release files for gammonview 1.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gammonview-1.4.0.tar.gz | 3.4 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gammonview-1.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.6 MB
Release files / gammonview-1.4.0.tar.gz
| Download URL | gammonview-1.4.0.tar.gz |
|---|---|
| Size | 3.4 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
74a930367749cf3f0758dd0241b20acf43eacd6709db6beab9d0e5eb78e11007
|
|
BLAKE2b-256 checksum How to use checksums |
5aaa79092fbd49d1026646a8f34b777046d505317698a1fdeb7c983ea1175f1a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency logRelease files / gammonview-1.4.0-py3-none-any.whl
| Download URL | gammonview-1.4.0-py3-none-any.whl |
|---|---|
| Size | 229.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2b729b55ef69e4497836e039098e5edfa2bc429018082358247f851d56f1d8a7
|
|
BLAKE2b-256 checksum How to use checksums |
83e66ad8a023ff2c015e6d0cb43728fd000d5a57549202b26851a5f44026774d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.
Transparency log