gammonview 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,319 @@
1
+ Metadata-Version: 2.5
2
+ Name: gammonview
3
+ Version: 1.0.0
4
+ Summary: Backgammon match analysis — the pure-Python OGXM/GVA codec (`gvformat`) plus bgsage-powered match and position analysis (`gvanalysis`).
5
+ Project-URL: Homepage, https://github.com/ngvlamis/gammonview-analysis
6
+ Project-URL: Repository, https://github.com/ngvlamis/gammonview-analysis
7
+ Project-URL: Issues, https://github.com/ngvlamis/gammonview-analysis/issues
8
+ Author: Nicholas Vlamis
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ License-File: THIRD-PARTY-NOTICES.md
12
+ Keywords: analysis,backgammon,bgsage,gvab,match-equity,ogxm,xg
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Games/Entertainment :: Board Games
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.10
25
+ Provides-Extra: engine
26
+ Requires-Dist: bgsage>=2.0.20260907; extra == 'engine'
27
+ Requires-Dist: pyyaml>=6.0.3; extra == 'engine'
28
+ Description-Content-Type: text/markdown
29
+
30
+ <p align="center">
31
+ <img src="https://raw.githubusercontent.com/ngvlamis/gammonview-analysis/main/assets/logo.png"
32
+ alt="" width="104" height="104">
33
+ </p>
34
+
35
+ <h1 align="center">GammonView</h1>
36
+
37
+ <p align="center">
38
+ <a href="https://github.com/ngvlamis/gammonview-analysis/actions/workflows/ci.yml"><img src="https://github.com/ngvlamis/gammonview-analysis/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
39
+ <a href="https://github.com/ngvlamis/gammonview-analysis/actions/workflows/engine.yml"><img src="https://github.com/ngvlamis/gammonview-analysis/actions/workflows/engine.yml/badge.svg" alt="engine"></a>
40
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+"></a>
41
+ <a href="LICENSE"><img src="https://img.shields.io/badge/licence-MIT-green.svg" alt="Licence: MIT"></a>
42
+ </p>
43
+
44
+ The backgammon analysis backend behind
45
+ **[gammonview.com](https://gammonview.com)**: read and write the site's
46
+ `.gvab`/`.gva` match files, import `.mat`/`.xg`/`.bgf`, analyze a match or a
47
+ single position with the [bgsage](https://pypi.org/project/bgsage/) engine, and
48
+ compute per-decision error rate (PR), luck and cube statistics.
49
+
50
+ Those files are based on **OGXM** — the chunked match format from
51
+ [HedgeHog](https://gitlab.com/eranlambooij/hedgehog-public), not ours — extended
52
+ to carry what bgsage's analysis produces. This repo reads OGXM; what it writes is
53
+ that extended form. See [The format](#the-format).
54
+
55
+ It ships as two importable Python packages, plus a JavaScript port of the codec
56
+ for the browser:
57
+
58
+ - **`gvformat`** — the pure-stdlib codec: read/write the `.gvab` binary, `.gva`
59
+ JSON conversion, `.xg`/`.bgf` import, compute-on-read stats, OGID. **Zero
60
+ third-party deps** — `pip install gammonview` and `import gvformat` anywhere,
61
+ no engine required. This is the format layer: everything else here, Python and
62
+ JavaScript alike, reads and writes through it.
63
+ - **`gvanalysis`** — the [bgsage](https://pypi.org/project/bgsage/)-powered
64
+ analysis: `analyze_match` (a match file in, analyzed `.gvab` bytes out),
65
+ `analyze_file` (the same work stopping at the OGXM document), and single-position
66
+ analysis. Depends on `gvformat` + bgsage (the `[engine]` extra). Dependencies
67
+ flow one way: `gvanalysis` → `gvformat`, never back.
68
+ - **`@gammonview/gvformat`** — the same codec in JavaScript, on npm. What reads
69
+ the analysis in the browser. See [In the browser](#in-the-browser).
70
+
71
+ ## Install
72
+
73
+ To use the packages, install from PyPI — no clone needed:
74
+
75
+ ```bash
76
+ pip install gammonview # the codec alone (gvformat), no engine
77
+ pip install "gammonview[engine]" # adds bgsage, for gvanalysis
78
+ ```
79
+
80
+ To work on them, clone the repo and let [uv](https://docs.astral.sh/uv/) build
81
+ the environment — it installs both packages in editable mode, plus bgsage and
82
+ the test dependencies:
83
+
84
+ ```bash
85
+ git clone https://github.com/ngvlamis/gammonview-analysis.git
86
+ cd gammonview-analysis
87
+ uv sync
88
+ uv run python tests/run_all.py --fast # ~3s, no engine needed
89
+ ```
90
+
91
+ Every command below is written `uv run …` for that checkout; from a plain
92
+ `pip install`, drop the prefix.
93
+
94
+ ## Quickstart
95
+
96
+ Server one-liner — analyze a dropped `.mat` and get the analyzed `.gvab` back,
97
+ ready to store and ship to the client:
98
+
99
+ ```python
100
+ from gvanalysis import analyze_match
101
+ data = analyze_match("match.mat", preset="world_class") # analyzed .gvab bytes
102
+ ```
103
+
104
+ It runs the engine over every decision, and the bytes carry the match *and* the
105
+ analysis. Any input the codec reads works — `.mat`, `.gva`, `.gvab` — and one
106
+ that already carries analysis keeps it, with ours appended.
107
+
108
+ It returns `.gvab` (binary), always — that is the artifact to store and ship.
109
+ For the JSON, `read_gvab(data)` is the `.gva`; to stop a step earlier and work
110
+ with the document itself, `analyze_file` returns it directly. (The CLI is the
111
+ other way round: `gvan-match` writes `.gva` unless asked for `--gvab`, because
112
+ a file you open by hand is likelier to want to be readable.)
113
+
114
+ ## The format
115
+
116
+ The files this repo reads and writes — `.gvab` (binary) and `.gva` (JSON) — are
117
+ based directly on **OGXM** (OpenGammon eXtensible Match), the chunked match
118
+ format from [HedgeHog](https://gitlab.com/eranlambooij/hedgehog-public), and
119
+ extend it with what GammonView's viewer shows and OGXM has no field for: luck,
120
+ per-decision PR and illegal-move flags, and per-alternative eval levels — all in
121
+ one `GVAN` chunk — plus match metadata like `event`, `site` and `cube_limit`.
122
+
123
+ **We read OGXM.** `read_gvab` takes a file this repo never wrote, including one
124
+ carrying another engine's analysis, and `gvan-match` appends its own analysis
125
+ block rather than replacing what is already there.
126
+
127
+ We make no claim in the other direction, and that is about us, not about OGXM.
128
+ One of our choices is a genuine departure: in match play we store **normalized
129
+ equity** in the three cube equity fields, where the base spec specifies **raw
130
+ MWC**. We do it for quantization — these fields are an `int16` at 1e-4, which
131
+ gives normalized equity about 20,000 usable steps at any score, while raw MWC
132
+ has as few as **16** at a lopsided one — and we recover MWC on read from the
133
+ shipped MET. It is the right trade for us and it is still a difference a reader
134
+ cannot see: same fields, same layout, different numbers.
135
+
136
+ So: treat these as GammonView files that an OGXM reader may well be able to make
137
+ partial sense of, not as OGXM files with extras. The two spec documents describe
138
+ **what this repo writes** — the base spec with every addition marked
139
+ `[GammonView extension]` and every divergence marked, including where a base
140
+ reader would go wrong. The upstream specs they track are in HedgeHog's `docs/`
141
+ and remain the authority on the base format.
142
+
143
+ ## In the browser
144
+
145
+ The codec has a JavaScript twin, published to npm as
146
+ **[`@gammonview/gvformat`](https://www.npmjs.com/package/@gammonview/gvformat)**:
147
+
148
+ ```bash
149
+ npm install @gammonview/gvformat
150
+ ```
151
+
152
+ ```js
153
+ import { read_gvab, convert_mat, parse_ogid } from '@gammonview/gvformat';
154
+
155
+ const match = read_gvab(bytes); // .gvab -> .gva JSON
156
+ const dropped = convert_mat(matText); // .mat -> .gva JSON, no analysis
157
+ const pos = parse_ogid(match.games[0].plies[0].ogid_before);
158
+ ```
159
+
160
+ An ESM port of `gvformat` — the same reads, writes, source converters and OGID
161
+ handling, with **no engine, no WebAssembly and no build step**. It is how
162
+ gammonview.com parses a dropped `.xg`/`.bgf`/`.mat` client-side and renders the
163
+ `.gvab` the Python side produced.
164
+
165
+ The port lives in [`gvformat-js/`](gvformat-js/) and is maintained here rather
166
+ than downstream. Python and JavaScript must agree byte-for-byte — the same
167
+ `.gvab` in gives the same `.gva` JSON out — and the two test suites check that
168
+ from either side.
169
+
170
+ There is no JavaScript port of `gvanalysis`: analysis needs the engine, and the
171
+ engine is Python-side only.
172
+
173
+ ## Command-line tools
174
+
175
+ Entry points are defined in `pyproject.toml` and run with `uv run`:
176
+
177
+ | CLI | What it does |
178
+ |---|---|
179
+ | `gvan-match` | Analyze a match (`.mat`, `.gva`/`.ogxm`, `.gvab`), compute per-move PR → `.gva`/`.gvab` |
180
+ | `gvan-batch` | The same over many files or a directory, one output each |
181
+ | `gvan-position` | Analyze a single position from an **XGID or OGID** |
182
+ | `xg2gva` | Convert an eXtreme Gammon `.xg` file to `.gva` JSON (no engine) |
183
+ | `bgf2gva` | Convert a BGBlitz `.bgf` file to `.gva` JSON (no engine) |
184
+
185
+ ```bash
186
+ uv run gvan-match match.mat --preset world_class --gvab # binary out, not .gva
187
+ uv run gvan-match match.gvab --preset fast # re-analyze a binary, append
188
+ uv run gvan-match match.mat --link # print a shareable URL
189
+ uv run gvan-batch matches/ --preset fast # a dir -> its match files
190
+ uv run gvan-position "XGID=-b----E-C---eE---c-e----B-:0:-1:1:31:0:0:0:0"
191
+ uv run gvan-position "11ccccchhhjjjjj:66666888dddddoo:N0N:13:B:R:0:0:0:0"
192
+ ```
193
+
194
+ An OGXM input keeps any analysis it already carries — ours is appended as a new
195
+ block rather than replacing it. `--link` encodes the whole match into a
196
+ `gammonview.com` URL fragment, so it needs no hosted file; `--browser` opens it.
197
+
198
+ ### Analysis presets
199
+
200
+ `--preset` selects an eXtreme Gammon–style scheme: a cheap first pass screens
201
+ every decision, and a stronger pass runs only where it is needed. The six
202
+ built-ins:
203
+
204
+ | Preset (aliases) | 1st pass | Middle tier | 2nd pass | XG equivalent |
205
+ |---|---|---|---|---|
206
+ | `very_quick` (`vq`) | `2ply` | — | — | Very quick |
207
+ | `fast` (`f`) | `2ply` | — | `3ply` | Fast |
208
+ | `deep` (`d`) | `3ply` | — | — | Deep |
209
+ | `balanced` (`b`) | `2ply` | `3ply` (both kinds) | `truncated2` | — (quality/speed) |
210
+ | `world_class` (`wc`) | `4ply` | — | `truncated2` | World Class (XG Roller+) |
211
+ | `world_class_fast` (`wcf`) | `3ply` | `truncated2` (**cube only**) | `truncated2` | World Class (3-tier) |
212
+
213
+ The second pass runs on an **error**; the middle tier runs on a **borderline**
214
+ decision the screen would otherwise have settled. `fast` is the default.
215
+
216
+ #### Which one to use
217
+
218
+ 493 matches were analyzed by XG at World Class and by each of the three strong
219
+ presets, then compared decision by decision:
220
+
221
+ | | mean gap to XG's match PR | same checker play as XG | PR within 1 of XG | relative cost |
222
+ |---|---|---|---|---|
223
+ | `world_class` | 0.434 | 90.3% | 91.8% | 45× |
224
+ | `world_class_fast` | 0.441 | 90.0% | 90.7% | 24× |
225
+ | `balanced` | 0.540 | 89.2% | 86.7% | 6× |
226
+
227
+ **Mean gap** is the average distance between the two *ratings* of the same
228
+ player-match, in either direction: rate a match with `world_class` and XG rates
229
+ it 0.434 PR away, typically. It is not what following the preset costs you —
230
+ priced by XG's own equities, a player who made `world_class`'s recommended
231
+ checker play every time would post a PR of 0.333, `world_class_fast` 0.353,
232
+ `balanced` 0.412. All three are inside what XG itself calls world-class play.
233
+
234
+ Cost is mean wall clock relative to `fast` over four matches, and varies with
235
+ the match — see [the timings](docs/CLI.md#what-they-cost).
236
+
237
+ - **`world_class_fast` is the recommended analysis.** It lands 0.007 PR behind
238
+ the deepest preset for a little under half the time. The two sit three times
239
+ closer to each other (0.136) than either does to XG, because what separates
240
+ them from XG is the *engine*, not the search depth — 95% of
241
+ `world_class_fast`'s squared PR error against XG is that engine floor, which
242
+ no preset can spend its way past.
243
+ - **`world_class` is for reproducing XG specifically**, not for being more
244
+ accurate. Its only real edge is on borderline cubes, and it gets there by
245
+ rolling them out *less* often than `world_class_fast` does — which is
246
+ XG-like, not demonstrably better. It costs roughly twice as much for that.
247
+ - **`balanced` is the quality/speed option**, and the only one of the three with
248
+ real headroom: upgrading it recovers eight times as much error as upgrading
249
+ `world_class_fast` does.
250
+
251
+ Every number above measures *similarity to XG*, which is not the same as being
252
+ right — where the two engines differ there is no arbiter here to say which is
253
+ correct. **[`docs/PRESET_ACCURACY.md`](docs/PRESET_ACCURACY.md)** is the full
254
+ experiment: how the presets split on checker play versus the cube, why rolling
255
+ out *costs* agreement with XG, and the evidence that the residual disagreement
256
+ is systematic rather than noise.
257
+
258
+ Presets live in `gvanalysis/presets.py` and are overridable via `presets.yaml`.
259
+ See **[`docs/CLI.md`](docs/CLI.md)** for the full reference — every flag, how the
260
+ tiers fire, parallelism, and preset overrides.
261
+
262
+ ## Documentation
263
+
264
+ | Doc | What it covers |
265
+ |---|---|
266
+ | [`docs/CLI.md`](docs/CLI.md) | **Full command-line reference** — every flag, presets, parallelism |
267
+ | [`gvformat-js/README.md`](gvformat-js/README.md) | The JavaScript package — API, layout, parity rules |
268
+ | [`CONTRIBUTING.md`](CONTRIBUTING.md) | How to run the tests, the conventions, what a good change looks like |
269
+ | [`CHANGELOG.md`](CHANGELOG.md) | Release history, including the measurements behind several design decisions |
270
+ | [`CLAUDE.md`](CLAUDE.md) | Maintainer and coding-agent notes: design rationale, measurements, and the rituals around an engine upgrade |
271
+ | [`docs/OGXM_JSON_SPEC_GAMMONVIEW.md`](docs/OGXM_JSON_SPEC_GAMMONVIEW.md) | The JSON shape this repo writes (match → game → ply → analysis) — base spec plus every GammonView addition, marked |
272
+ | [`docs/OGXM_FORMAT_SPEC_GAMMONVIEW.md`](docs/OGXM_FORMAT_SPEC_GAMMONVIEW.md) | The binary `.gvab` chunk layout, including the `GVAN` extension chunk |
273
+ | [`docs/OGXM_COMPUTED_FIELDS.md`](docs/OGXM_COMPUTED_FIELDS.md) | Fields the reader computes rather than stores (PR, luck-MWC, MWC, classification) |
274
+ | [`docs/METHODOLOGY.md`](docs/METHODOLOGY.md) | PR / luck / cube-decision methodology and filters |
275
+ | [`docs/PRESET_ACCURACY.md`](docs/PRESET_ACCURACY.md) | What each preset costs in accuracy, measured against XG over 493 matches |
276
+
277
+ ## Licence
278
+
279
+ [MIT](LICENSE). Every source file carries an SPDX header; `LICENSE` holds the
280
+ full text.
281
+
282
+ MIT because what this repo mainly offers is a **format**, and a format wants to
283
+ be copied in pieces — one routine lifted into another project, or the codec
284
+ ported to another language. It also stays compatible in both directions at once,
285
+ which matters in this ecosystem: GNU Backgammon is GPL, eXtreme Gammon and
286
+ BGBlitz are proprietary, and all of them are welcome to interoperate.
287
+ [HedgeHog](https://gitlab.com/eranlambooij/hedgehog-public), whose format this
288
+ implements, is MIT too.
289
+
290
+ [bgsage](https://pypi.org/project/bgsage/), the engine behind the optional
291
+ `[engine]` extra, is MPL-2.0, which constrains nothing here. MPL is *file-level*
292
+ copyleft with no linking trigger: it attaches to its own files, and §3.3
293
+ expressly permits distributing a larger work under other terms. bgsage resolves
294
+ from PyPI at install time and is never modified or redistributed here, and
295
+ `gvformat` does not touch the engine at all.
296
+
297
+ The JavaScript mirror ([`gvformat-js/`](gvformat-js/), published as
298
+ `@gammonview/gvformat`) is under the same licence and ships its own copy.
299
+
300
+ The **GammonView logo** (`assets/logo.png`) is excluded: it identifies the site,
301
+ so it is not part of the MIT grant. Fork the code freely; use your own mark.
302
+
303
+ ### Third-party material
304
+
305
+ Two pieces of third-party material are incorporated, both permissively licensed.
306
+ Their notices must be preserved — see **[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)**,
307
+ which reproduces them in full:
308
+
309
+ - **HedgeHog** (MIT, © 2026 Eran Lambooij) — OGXM is HedgeHog's format. Most of
310
+ `gvformat` is an independent implementation, but `binary.py` and `export.py`
311
+ (and their JS mirrors) are genuine ports of its C++ sources: the chunk struct
312
+ layouts and `parse_ply_analysis_json` from `ogxm_io.cpp`/`ogxm_json.cpp`, and
313
+ `_TurnState` from `ogxm_replay.cpp`'s `replay_game()`. Those four files say so
314
+ in their headers.
315
+ - **Kazaross-XG2 MET** (© 2011 Neil Kazaross; transcribed for GNUbg by Michael
316
+ Petch) — the match-equity table embedded in `gvformat/met.py` and
317
+ `gvformat-js/src/met.js`. It ships with GNU Backgammon but is *not* under its
318
+ GPL; its own terms require only that the notice be preserved, and it is
319
+ reproduced at the top of both files.
@@ -0,0 +1,38 @@
1
+ gvanalysis/__init__.py,sha256=S9O17fPn54wZrTT2qA_JLFUPzAywkDVNFwRNqJ8EAkU,5185
2
+ gvanalysis/batch.py,sha256=ZqFGyimIL2Vf8lvkLnzbwHkomgqj51pTmWl_ZIaJoBg,8205
3
+ gvanalysis/checker_eval.py,sha256=y38kmFk8MqDZj6h1Rn-m4GLhYPdQVvoY3mobldg3TSQ,17138
4
+ gvanalysis/compare_gva.py,sha256=Os-NA_Jjy-dBU61Bl7EXVprurO2y1sw8iSyMw1nhFqo,12358
5
+ gvanalysis/game_eval.py,sha256=AMhctfXq0gxnddSxxYUpl4K2pqqPJxUbBLyzMBf_Bcw,46745
6
+ gvanalysis/loader.py,sha256=Uh7wklmW9hYSIFFUv98Z5zRsblaoPP566ispILB0PDE,2448
7
+ gvanalysis/luck.py,sha256=CCafBrb58jYTap8wx2rCck47p8sCe4tOV_5A8TMQ_QQ,6260
8
+ gvanalysis/match.py,sha256=EoC0jhCAf_hkdl8qctllJD48FKAy86JcOQhJan3OEnU,36579
9
+ gvanalysis/ogxm_reconstructor.py,sha256=IWuP6BmwJ3gVh_5AGu5KIagg7TX1Eh5I-COFg_tMxIk,9717
10
+ gvanalysis/position.py,sha256=_Rzrq8Hjh1hYSD6IP-msnA4oOEdmBhjHVdIb0trjQ-4,19103
11
+ gvanalysis/presets.py,sha256=-y0DwAfdCt0z9eNwrPlXn4iIeyQtDC7inni6zBccLCA,17124
12
+ gvanalysis/progress.py,sha256=SlNBJwFI3EyAvo1_CcwiOfpuxtA2pu7WyvTxhDL1jvk,3341
13
+ gvanalysis/reconstruct_mat.py,sha256=IA3IaZ4EfTf0vbSksXZcx3EntIlbkvjckixQrhPrpTo,10358
14
+ gvanalysis/share.py,sha256=60hjWWSBZWx1lduBgyx2T3_--zKTiAGXL3GdQoG55J4,1312
15
+ gvformat/__init__.py,sha256=0b1FwWAAfzv-_Ja8iCVB88nwqx5PW6ZrqwDKj7KggEo,2186
16
+ gvformat/basefill.py,sha256=nQp7xYC_co0iO1uPZ32lHYAmk_v9PdO-VuJvJk5YNwM,12477
17
+ gvformat/bgf.py,sha256=TDy97AD5zzcQvTB0afVDj44MDxsl41DG5eY_M6wdocM,61346
18
+ gvformat/binary.py,sha256=OoVVJEwTxf_JVOLv0IeHsjJUtTRhRYP2B3PKDpbgejM,40839
19
+ gvformat/export.py,sha256=kGwi6h23lb9KQ5X3QQlnZHA8gM_sgo7Mtn6mRnP-6RI,59291
20
+ gvformat/game_reconstructor.py,sha256=rs6qD8pJjFKVC2w3Kl14guXOcYYGn1Gp-y8pIY6ydbw,12133
21
+ gvformat/legality.py,sha256=eRU3MyNx_J2U_seS6KB7aILebaoK_yJW35XympnscyI,9198
22
+ gvformat/mat.py,sha256=JRtJFWsTnV3eNG6LGUWIFqTRn5cOE7pSXSaic8G-J2I,6140
23
+ gvformat/mat_parser.py,sha256=v1QgSnBqdwWXcmWhwHrAe0xWdzGwVGMCZbWK3hC8fjM,11794
24
+ gvformat/merge.py,sha256=B-M-MIyzsPFQyJSh56Nb3YMsbVJjnue8WLpoLtzUEoU,5800
25
+ gvformat/met.py,sha256=QYjycKZatVWo1F3M8IxQngE8qCbcmhXy-qrrFkXn9T4,13769
26
+ gvformat/notation.py,sha256=s69yhh-5IlXVAXK1OKMNiZDrrABPoElzAdFK_skIHGI,8294
27
+ gvformat/ogid.py,sha256=rn5rOPxXjqa9wLu9sRcE3k7aZhkczmd9S0Niy0Vq67g,35673
28
+ gvformat/place.py,sha256=CfoiYndZg08NGqVK2r1HuYbgBYIT2gKVSpzSFDVXasE,2185
29
+ gvformat/reader.py,sha256=jkQolPbQp4bcltrdl0EZ7D8777kn7ygRoN6FVHgFUKQ,46464
30
+ gvformat/share.py,sha256=ZjHmbhWRbnZhH-MDqbC8zPWXuktzmMFwhjhLbOdghn0,1944
31
+ gvformat/stats.py,sha256=5qLR2okawClhcMZ96b06sOq8jO36YfWUTgU8iS0K9TM,19811
32
+ gvformat/xg.py,sha256=QV2qUNWQ5PXUKo7akS40bf2nINDuywaz0S_RjmRXkms,62957
33
+ gammonview-1.0.0.dist-info/METADATA,sha256=5zX8d1dABVXNwZB_nN44fhlpukmCIPweHSRiwVCRUoA,16692
34
+ gammonview-1.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
35
+ gammonview-1.0.0.dist-info/entry_points.txt,sha256=6jzfIHRiRLHQOe6tryjxj_JOkBrlkVW7kXJnLxfxfOw,275
36
+ gammonview-1.0.0.dist-info/licenses/LICENSE,sha256=alJKWw6YN2SqgNVH3WHiBoE610YzS716wiR8qQSIcBc,1072
37
+ gammonview-1.0.0.dist-info/licenses/THIRD-PARTY-NOTICES.md,sha256=79PZed48nGcRAWkYUWQ2IVDgKmXCIz0eFmR-5Nya6uw,3472
38
+ gammonview-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,8 @@
1
+ [console_scripts]
2
+ bgf2gva = gvformat.bgf:main
3
+ compare-gva = gvanalysis.compare_gva:main
4
+ gvan-batch = gvanalysis.batch:main
5
+ gvan-match = gvanalysis.match:main
6
+ gvan-position = gvanalysis.position:main
7
+ reconstruct-mat = gvanalysis.reconstruct_mat:main
8
+ xg2gva = gvformat.xg:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nicholas Vlamis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,80 @@
1
+ # Third-party notices
2
+
3
+ This project is MIT-licensed (see `LICENSE`). It incorporates the third-party
4
+ material below, each under its own terms. **These notices must be preserved in
5
+ any copy or substantial portion of this software.**
6
+
7
+ ---
8
+
9
+ ## HedgeHog — the OGXM format and reference codec
10
+
11
+ OGXM is HedgeHog's format. `gvformat` is an independent implementation of it,
12
+ but several modules are direct ports of HedgeHog's C++ sources rather than
13
+ clean-room implementations of the published spec:
14
+
15
+ | This repo | Ported from |
16
+ |---|---|
17
+ | `gvformat/binary.py`, `gvformat-js/src/binary.js` | `src/match/ogxm_io.cpp`, `ogxm_format.hpp`, `ogxm_json.cpp` — chunk struct layouts, `encode_ply`, fixed-point probability encoding, and `parse_ply_analysis_json` field-for-field |
18
+ | `gvformat/export.py`, `gvformat-js/src/export.js` | `src/match/ogxm_replay.cpp` — `_TurnState` is a port of `replay_game()`; the OGID state/action string constants are taken byte-for-byte from `src/ogid.h` |
19
+
20
+ Source: <https://gitlab.com/eranlambooij/hedgehog-public>
21
+
22
+ ```
23
+ MIT License
24
+
25
+ Copyright (c) 2026 Eran Lambooij
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Kazaross-XG2 match-equity table
49
+
50
+ The MET data shipped in `gvformat/met.py` and `gvformat-js/src/met.js` (the
51
+ 25x25 pre-Crawford table and the post-Crawford vector) is transcribed from GNU
52
+ Backgammon's `met/Kazaross-XG2.xml`. The table is separately licensed under the
53
+ permissive notice below — it is **not** covered by GNU Backgammon's GPL.
54
+
55
+ ```
56
+ Copyright (C) 2011 Neil Kazaross
57
+
58
+ Table rolled up to 9 point match by eXtreme Gammon. Then uses R/K MET
59
+ which was rolled up to 15 and extrapolated to 25 points.
60
+
61
+ Transcribed for use by GNUbg by Michael Petch <mpetch@capp-sysware.com>.
62
+
63
+ This file is distributed as a part of the GNU Backgammon program.
64
+ Copying and distribution of this file, with or without modification, are
65
+ permitted in any medium without royalty provided the copyright notice and
66
+ this notice are preserved. This file is offered as-is, without any warranty.
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Not bundled
72
+
73
+ For completeness, since both are commonly assumed to be vendored here:
74
+
75
+ - **bgsage** (MPL-2.0) — the analysis engine. An optional `[engine]` extra
76
+ resolved from PyPI at install time; no bgsage source is included in this
77
+ repository, and none of its files are modified.
78
+ - **pako** (MIT) — used by the JS mirror for zlib deflate. A declared npm
79
+ dependency, installed by `npm install`; not vendored, and excluded from the
80
+ Python sdist.
gvanalysis/__init__.py ADDED
@@ -0,0 +1,118 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (C) 2026 Nicholas Vlamis
3
+
4
+ """GammonView Analysis — bgsage-powered backgammon analysis.
5
+
6
+ This is the *analysis* layer: it drives the bgsage engine to evaluate a match
7
+ or position, and hands the result to the pure-stdlib ``gvformat`` codec for
8
+ serialization. It depends on ``gvformat`` + ``bgsage`` (the ``[engine]`` extra);
9
+ ``gvformat`` on its own has no such dependency.
10
+
11
+ Typical server use — analyze a dropped match file and get the analyzed
12
+ ``.gvab`` bytes back, in one call::
13
+
14
+ from gvanalysis import analyze_match
15
+ data = analyze_match("match.mat", preset="world_class") # bytes to send to the client
16
+
17
+ Or the pieces:
18
+
19
+ from gvanalysis import analyze_mat
20
+ from gvformat import to_ogxm_json, write_gvab, read_gvab
21
+ result = analyze_mat("match.mat", preset="fast")
22
+ data = write_gvab(to_ogxm_json(result))
23
+
24
+ A single position is a different scale of problem -- fast enough to answer in
25
+ a request, so it returns a plain OGXM-shaped dict rather than a file::
26
+
27
+ from gvanalysis import analyze_position
28
+ result = analyze_position("XGID=...", level="3ply") # ~0.07s at 3ply
29
+
30
+ CLIs: ``gvan-match`` / ``gvan-position`` (or ``python -m gvanalysis.match``).
31
+
32
+ Note: ``analyze_mat`` / ``analyze_match`` (and the ``match`` / ``position`` /
33
+ ``game_eval`` submodules) require the bgsage engine — the ``[engine]`` extra.
34
+ Importing ``gvanalysis`` itself, and the engine-free tool submodules
35
+ (``reconstruct_mat``, ``compare_gva``), works without it; the engine
36
+ requirement only surfaces — with a clear message — when you actually reach for
37
+ the analyzer.
38
+
39
+ The ``.mat`` parser/converter is *not* here: it needs no engine, so it lives in
40
+ the codec layer beside the other source-format converters — ``gvformat``'s
41
+ ``convert_mat``/``mat_to_ogxm`` (and ``mat_parser`` / ``game_reconstructor``),
42
+ mirroring ``convert_xg`` and ``convert_bgf``.
43
+ """
44
+
45
+ _ENGINE_HINT = (
46
+ "the bgsage engine is required for this, but isn't installed. Add the "
47
+ "analysis extra:\n"
48
+ " pip install 'gammonview[engine]' (or, in this repo: uv sync)\n"
49
+ "The pure-codec layer `gvformat` needs no engine."
50
+ )
51
+
52
+
53
+ def _load_match():
54
+ """Import the engine-backed ``match`` module, or raise a clear, actionable
55
+ error when the bgsage engine (the ``[engine]`` extra) isn't installed."""
56
+ try:
57
+ from . import match
58
+ except ModuleNotFoundError as e:
59
+ if (e.name or "") in ("bgsage", "bgbot_cpp") or (e.name or "").startswith("bgsage."):
60
+ raise ModuleNotFoundError(f"gvanalysis: {_ENGINE_HINT}") from e
61
+ raise
62
+ return match
63
+
64
+
65
+ def analyze_match(input_path, *, preset: str | None = None, quiet: bool = True, **kwargs) -> bytes:
66
+ """Analyze a match file and return the analyzed ``.gvab`` bytes.
67
+
68
+ Returns **bytes**, not a dict: this is the one call that hands back the
69
+ storable artifact. ``analyze_file`` is the same work stopping one step
70
+ earlier, at the OGXM document -- ``analyze_match(p)`` is exactly
71
+ ``write_gvab(analyze_file(p))``, and ``read_gvab`` of these bytes is that
72
+ document back.
73
+
74
+ One call for the common server path: a match in (``.mat``, ``.gva``/
75
+ ``.ogxm``, or ``.gvab`` — OGXM is the internal representation, so ``.mat``
76
+ is converted first), canonical ``.gvab`` bytes out, carrying the match
77
+ *and* the analysis this call just ran (ready to store and ship to the
78
+ client). An OGXM input's existing analysis blocks are preserved and ours is
79
+ appended. Equivalent to ``write_gvab(analyze_file(...))``. Requires the
80
+ ``[engine]`` extra; without it, raises a ``ModuleNotFoundError`` explaining
81
+ how to install.
82
+
83
+ Extra keyword arguments go to ``analyze_ogxm``; notably
84
+ ``on_progress=lambda done, total: ...`` reports decision-level progress, for
85
+ a server driving a progress bar in a client.
86
+ """
87
+ analyze_file = _load_match().analyze_file
88
+ from gvformat import write_gvab
89
+ return write_gvab(analyze_file(input_path, preset=preset, quiet=quiet, **kwargs))
90
+
91
+
92
+ #: The former name. It read as a format conversion and named an input format
93
+ #: this call never required, so the analysis it runs was invisible in it; kept
94
+ #: working because callers exist.
95
+ mat_to_gvab = analyze_match
96
+
97
+
98
+ def __getattr__(name):
99
+ # PEP 562: expose the engine-backed entry points lazily so `import
100
+ # gvanalysis` (and the engine-free tool submodules) load without bgsage; the
101
+ # engine requirement only bites — with a clear message — when the analyzer
102
+ # is actually used.
103
+ if name in ("analyze_mat", "analyze_ogxm", "analyze_file"):
104
+ return getattr(_load_match(), name)
105
+ # `position` imports no engine at module scope (it reaches for bgsage only
106
+ # inside the call), so it needs none of _load_match's guarding — but it is
107
+ # still lazy, to keep `import gvanalysis` as cheap as it has always been.
108
+ if name == "analyze_position":
109
+ from . import position
110
+ return position.analyze_position
111
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
112
+
113
+
114
+ __all__ = [
115
+ "analyze_mat", "analyze_ogxm", "analyze_file",
116
+ "analyze_match", "mat_to_gvab",
117
+ "analyze_position",
118
+ ]