gammonview 2.0.0__tar.gz → 2.0.2__tar.gz

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.
Files changed (302) hide show
  1. {gammonview-2.0.0 → gammonview-2.0.2}/CHANGELOG.md +65 -0
  2. {gammonview-2.0.0 → gammonview-2.0.2}/PKG-INFO +1 -1
  3. {gammonview-2.0.0 → gammonview-2.0.2}/docs/OGXM_JSON_SPEC_GAMMONVIEW.md +11 -6
  4. {gammonview-2.0.0 → gammonview-2.0.2}/docs/OGXM_V2_PROFILE.md +17 -4
  5. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/match.py +7 -0
  6. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/binary.py +2 -0
  7. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/export.py +1 -0
  8. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/ogxm2.py +54 -6
  9. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/ogxm2_passthrough.py +7 -4
  10. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/ogxm2_writer.py +59 -17
  11. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/package.json +1 -1
  12. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/constants.js +2 -0
  13. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/legality.js +5 -3
  14. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/ogxm2.js +69 -22
  15. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/ogxm2_passthrough.js +7 -4
  16. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/ogxm2_writer.js +67 -21
  17. gammonview-2.0.2/gvformat-js/test/fixtures/ogxm2/annos-docs.json +1 -0
  18. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/annos-edits-sha256.json +6 -6
  19. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/annos-sha256.json +4 -4
  20. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/annos.expected.json +1 -1
  21. gammonview-2.0.2/gvformat-js/test/fixtures/ogxm2/bgf-writer-sha256.json +46 -0
  22. gammonview-2.0.2/gvformat-js/test/fixtures/ogxm2/blocks-docs.json +1 -0
  23. gammonview-2.0.2/gvformat-js/test/fixtures/ogxm2/blocks-sha256.json +42 -0
  24. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/blocks.expected.json +1 -1
  25. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/blocks.ogxm +0 -0
  26. gammonview-2.0.2/gvformat-js/test/fixtures/ogxm2/fields-docs.json +1 -0
  27. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/fields.expected.json +1 -1
  28. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/passthrough-sha256.json +12 -12
  29. gammonview-2.0.2/gvformat-js/test/fixtures/ogxm2/writer-sha256.json +46 -0
  30. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-legality.js +8 -0
  31. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-ogxm2-blocks.js +3 -1
  32. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-ogxm2-fields.js +8 -8
  33. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-ogxm2-limits.js +16 -1
  34. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-ogxm2-writer.js +24 -0
  35. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-ogxm2.js +5 -4
  36. gammonview-2.0.2/tests/golden/3WNK_g1Z-PLsh_HyvQ5j4a.fast.gva +1 -0
  37. {gammonview-2.0.0 → gammonview-2.0.2}/tests/golden/3WNK_g1Z-PLsh_HyvQ5j4a.fast.gvab +0 -0
  38. gammonview-2.0.2/tests/golden/5nqfGw9bWG3deTaU.fast.gva +1 -0
  39. {gammonview-2.0.0 → gammonview-2.0.2}/tests/golden/5nqfGw9bWG3deTaU.fast.gvab +0 -0
  40. gammonview-2.0.2/tests/golden/B4_SrGcsKAQmoTyHlgJCbM.fast.gva +1 -0
  41. {gammonview-2.0.0 → gammonview-2.0.2}/tests/golden/B4_SrGcsKAQmoTyHlgJCbM.fast.gvab +0 -0
  42. gammonview-2.0.2/tests/golden/MYdvw1qGuyaRUH__.fast.gva +1 -0
  43. {gammonview-2.0.0 → gammonview-2.0.2}/tests/golden/MYdvw1qGuyaRUH__.fast.gvab +0 -0
  44. {gammonview-2.0.0 → gammonview-2.0.2}/tests/golden/PROVENANCE.json +1 -1
  45. gammonview-2.0.2/tests/golden/baR-U643iUDpvzTC.fast.gva +1 -0
  46. {gammonview-2.0.0 → gammonview-2.0.2}/tests/golden/baR-U643iUDpvzTC.fast.gvab +0 -0
  47. {gammonview-2.0.0 → gammonview-2.0.2}/tests/ogxm2_blocks_cases.py +7 -5
  48. {gammonview-2.0.0 → gammonview-2.0.2}/tests/ogxm2_fields_cases.py +2 -2
  49. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2.py +5 -4
  50. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2_blocks.py +7 -4
  51. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2_fields.py +8 -8
  52. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2_limits.py +15 -0
  53. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2_writer.py +12 -3
  54. gammonview-2.0.0/docs/GAMMONVIEW_V2_HANDOFF.md +0 -197
  55. gammonview-2.0.0/gvformat-js/test/fixtures/ogxm2/annos-docs.json +0 -1
  56. gammonview-2.0.0/gvformat-js/test/fixtures/ogxm2/bgf-writer-sha256.json +0 -46
  57. gammonview-2.0.0/gvformat-js/test/fixtures/ogxm2/blocks-docs.json +0 -1
  58. gammonview-2.0.0/gvformat-js/test/fixtures/ogxm2/blocks-sha256.json +0 -42
  59. gammonview-2.0.0/gvformat-js/test/fixtures/ogxm2/fields-docs.json +0 -1
  60. gammonview-2.0.0/gvformat-js/test/fixtures/ogxm2/writer-sha256.json +0 -46
  61. gammonview-2.0.0/tests/golden/3WNK_g1Z-PLsh_HyvQ5j4a.fast.gva +0 -1
  62. gammonview-2.0.0/tests/golden/5nqfGw9bWG3deTaU.fast.gva +0 -1
  63. gammonview-2.0.0/tests/golden/B4_SrGcsKAQmoTyHlgJCbM.fast.gva +0 -1
  64. gammonview-2.0.0/tests/golden/MYdvw1qGuyaRUH__.fast.gva +0 -1
  65. gammonview-2.0.0/tests/golden/baR-U643iUDpvzTC.fast.gva +0 -1
  66. {gammonview-2.0.0 → gammonview-2.0.2}/.gitattributes +0 -0
  67. {gammonview-2.0.0 → gammonview-2.0.2}/.github/workflows/ci.yml +0 -0
  68. {gammonview-2.0.0 → gammonview-2.0.2}/.github/workflows/engine.yml +0 -0
  69. {gammonview-2.0.0 → gammonview-2.0.2}/.github/workflows/publish.yml +0 -0
  70. {gammonview-2.0.0 → gammonview-2.0.2}/.gitignore +0 -0
  71. {gammonview-2.0.0 → gammonview-2.0.2}/.python-version +0 -0
  72. {gammonview-2.0.0 → gammonview-2.0.2}/CLAUDE.md +0 -0
  73. {gammonview-2.0.0 → gammonview-2.0.2}/CONTRIBUTING.md +0 -0
  74. {gammonview-2.0.0 → gammonview-2.0.2}/LICENSE +0 -0
  75. {gammonview-2.0.0 → gammonview-2.0.2}/README.md +0 -0
  76. {gammonview-2.0.0 → gammonview-2.0.2}/THIRD-PARTY-NOTICES.md +0 -0
  77. {gammonview-2.0.0 → gammonview-2.0.2}/assets/logo.png +0 -0
  78. {gammonview-2.0.0 → gammonview-2.0.2}/docs/CLI.md +0 -0
  79. {gammonview-2.0.0 → gammonview-2.0.2}/docs/METHODOLOGY.md +0 -0
  80. {gammonview-2.0.0 → gammonview-2.0.2}/docs/OGXM_COMPUTED_FIELDS.md +0 -0
  81. {gammonview-2.0.0 → gammonview-2.0.2}/docs/PRESET_ACCURACY.md +0 -0
  82. {gammonview-2.0.0 → gammonview-2.0.2}/docs/upstream-bgsage.md +0 -0
  83. {gammonview-2.0.0 → gammonview-2.0.2}/docs/v1/OGXM_FORMAT_SPEC_GAMMONVIEW.md +0 -0
  84. {gammonview-2.0.0 → gammonview-2.0.2}/experiments/2026-09-16-checker-mid-tier.md +0 -0
  85. {gammonview-2.0.0 → gammonview-2.0.2}/experiments/2026-09-17-checker-mid-tier-followup.md +0 -0
  86. {gammonview-2.0.0 → gammonview-2.0.2}/experiments/2026-09-19-cross-machine-costs.md +0 -0
  87. {gammonview-2.0.0 → gammonview-2.0.2}/experiments/2026-09-20-corpus-rerun.md +0 -0
  88. {gammonview-2.0.0 → gammonview-2.0.2}/experiments/bench.py +0 -0
  89. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/__init__.py +0 -0
  90. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/batch.py +0 -0
  91. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/checker_eval.py +0 -0
  92. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/compare_gva.py +0 -0
  93. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/game_eval.py +0 -0
  94. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/loader.py +0 -0
  95. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/luck.py +0 -0
  96. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/ogxm_reconstructor.py +0 -0
  97. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/parentwatch.py +0 -0
  98. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/position.py +0 -0
  99. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/presets.py +0 -0
  100. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/progress.py +0 -0
  101. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/reconstruct_mat.py +0 -0
  102. {gammonview-2.0.0 → gammonview-2.0.2}/gvanalysis/share.py +0 -0
  103. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/__init__.py +0 -0
  104. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/basefill.py +0 -0
  105. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/bgf.py +0 -0
  106. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/game_reconstructor.py +0 -0
  107. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/legality.py +0 -0
  108. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/mat.py +0 -0
  109. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/mat_parser.py +0 -0
  110. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/merge.py +0 -0
  111. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/met.py +0 -0
  112. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/notation.py +0 -0
  113. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/ogid.py +0 -0
  114. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/place.py +0 -0
  115. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/reader.py +0 -0
  116. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/share.py +0 -0
  117. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/stats.py +0 -0
  118. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat/xg.py +0 -0
  119. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/.gitignore +0 -0
  120. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/LICENSE +0 -0
  121. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/README.md +0 -0
  122. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/THIRD-PARTY-NOTICES.md +0 -0
  123. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/cli/README.md +0 -0
  124. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/cli/bgf2gva.js +0 -0
  125. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/cli/mat2gva.js +0 -0
  126. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/cli/xg2gva.js +0 -0
  127. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/package-lock.json +0 -0
  128. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/basefill.js +0 -0
  129. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/bgf2gva.js +0 -0
  130. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/binary.js +0 -0
  131. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/export.js +0 -0
  132. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/index.js +0 -0
  133. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/mat2gva.js +0 -0
  134. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/merge.js +0 -0
  135. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/met.js +0 -0
  136. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/notation.js +0 -0
  137. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/og2gva.js +0 -0
  138. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/ogid.js +0 -0
  139. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/place.js +0 -0
  140. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/reader.js +0 -0
  141. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/sha256.js +0 -0
  142. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/share.js +0 -0
  143. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/stats.js +0 -0
  144. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/src/xg2gva.js +0 -0
  145. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/3WNK_g1Z-PLsh_HyvQ5j4a.json +0 -0
  146. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/5nqfGw9bWG3deTaU.json +0 -0
  147. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/B4_SrGcsKAQmoTyHlgJCbM.json +0 -0
  148. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/C07OQCKyw4Pe-CnjFiAMYO.json +0 -0
  149. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/MYdvw1qGuyaRUH__.json +0 -0
  150. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/Sz-2PgKnvb6aFls69-qFXg.json +0 -0
  151. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/UL-CV6F9jMM-E1Pu.json +0 -0
  152. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/XCu3RJ0UvDg_Tldl.json +0 -0
  153. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/baR-U643iUDpvzTC.json +0 -0
  154. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/eXBNG5wZHS5Alu3P.json +0 -0
  155. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/hQ8sVn2LbTdF4wRm.json +0 -0
  156. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/rK7pXm4TqLb9NzWd.json +0 -0
  157. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/mat/uwvEbd63UvdX7SDin5o_Iw.json +0 -0
  158. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/annos.ogxm +0 -0
  159. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/cube-on-two.expected.json +0 -0
  160. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/cube-on-two.ogxm +0 -0
  161. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/fields-sha256.json +0 -0
  162. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/fields.ogxm +0 -0
  163. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/foreign.ogxm +0 -0
  164. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/gen.py +0 -0
  165. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/match.expected.json +0 -0
  166. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/match.ogxm +0 -0
  167. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/resign.expected.json +0 -0
  168. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/resign.ogxm +0 -0
  169. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/set-up.expected.json +0 -0
  170. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/set-up.ogxm +0 -0
  171. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/two-blocks.expected.json +0 -0
  172. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/two-blocks.ogxm +0 -0
  173. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/fixtures/ogxm2/v1-chunks.gvab +0 -0
  174. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-alternative-move-steps.js +0 -0
  175. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-basefill.js +0 -0
  176. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-bgf-alternatives.js +0 -0
  177. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-bgf-embedded-cube.js +0 -0
  178. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-bgf-move-steps.js +0 -0
  179. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-chunk-passthrough.js +0 -0
  180. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-equity-loss.js +0 -0
  181. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-illegal-move.js +0 -0
  182. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-illegal-play-steps.js +0 -0
  183. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-mat.js +0 -0
  184. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-merge.js +0 -0
  185. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-met.js +0 -0
  186. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-og.js +0 -0
  187. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-ogid.js +0 -0
  188. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-ogxm2-annos.js +0 -0
  189. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-ogxm2-passthrough.js +0 -0
  190. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-place.js +0 -0
  191. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-robustness.js +0 -0
  192. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-roundtrip.js +0 -0
  193. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-share.js +0 -0
  194. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-stats.js +0 -0
  195. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-xg-cube.js +0 -0
  196. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-xg-eval-levels.js +0 -0
  197. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-xg-move-steps.js +0 -0
  198. {gammonview-2.0.0 → gammonview-2.0.2}/gvformat-js/test/test-xg-zero-win-eval.js +0 -0
  199. {gammonview-2.0.0 → gammonview-2.0.2}/pyproject.toml +0 -0
  200. {gammonview-2.0.0 → gammonview-2.0.2}/samples/README.md +0 -0
  201. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/3WNK_g1Z-PLsh_HyvQ5j4a.bgf +0 -0
  202. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/5nqfGw9bWG3deTaU.bgf +0 -0
  203. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/B4_SrGcsKAQmoTyHlgJCbM.bgf +0 -0
  204. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/C07OQCKyw4Pe-CnjFiAMYO.bgf +0 -0
  205. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/MYdvw1qGuyaRUH__.bgf +0 -0
  206. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/Sz-2PgKnvb6aFls69-qFXg.bgf +0 -0
  207. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/UL-CV6F9jMM-E1Pu.bgf +0 -0
  208. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/XCu3RJ0UvDg_Tldl.bgf +0 -0
  209. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/baR-U643iUDpvzTC.bgf +0 -0
  210. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/eXBNG5wZHS5Alu3P.bgf +0 -0
  211. {gammonview-2.0.0 → gammonview-2.0.2}/samples/bgf/uwvEbd63UvdX7SDin5o_Iw.bgf +0 -0
  212. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/3WNK_g1Z-PLsh_HyvQ5j4a.gvab +0 -0
  213. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/5nqfGw9bWG3deTaU.gvab +0 -0
  214. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/B4_SrGcsKAQmoTyHlgJCbM.gva +0 -0
  215. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/B4_SrGcsKAQmoTyHlgJCbM.gvab +0 -0
  216. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/C07OQCKyw4Pe-CnjFiAMYO.gvab +0 -0
  217. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/MYdvw1qGuyaRUH__.gvab +0 -0
  218. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/Sz-2PgKnvb6aFls69-qFXg.gvab +0 -0
  219. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/UL-CV6F9jMM-E1Pu.gvab +0 -0
  220. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/XCu3RJ0UvDg_Tldl.gvab +0 -0
  221. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/baR-U643iUDpvzTC.gvab +0 -0
  222. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/eXBNG5wZHS5Alu3P.gvab +0 -0
  223. {gammonview-2.0.0 → gammonview-2.0.2}/samples/gv/uwvEbd63UvdX7SDin5o_Iw.gvab +0 -0
  224. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/3WNK_g1Z-PLsh_HyvQ5j4a.mat +0 -0
  225. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/5nqfGw9bWG3deTaU.mat +0 -0
  226. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/B4_SrGcsKAQmoTyHlgJCbM.mat +0 -0
  227. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/C07OQCKyw4Pe-CnjFiAMYO.mat +0 -0
  228. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/MYdvw1qGuyaRUH__.mat +0 -0
  229. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/Sz-2PgKnvb6aFls69-qFXg.mat +0 -0
  230. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/UL-CV6F9jMM-E1Pu.mat +0 -0
  231. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/XCu3RJ0UvDg_Tldl.mat +0 -0
  232. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/baR-U643iUDpvzTC.mat +0 -0
  233. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/eXBNG5wZHS5Alu3P.mat +0 -0
  234. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/hQ8sVn2LbTdF4wRm.mat +0 -0
  235. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/rK7pXm4TqLb9NzWd.mat +0 -0
  236. {gammonview-2.0.0 → gammonview-2.0.2}/samples/mat/uwvEbd63UvdX7SDin5o_Iw.mat +0 -0
  237. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/3WNK_g1Z-PLsh_HyvQ5j4a.xg +0 -0
  238. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/5nqfGw9bWG3deTaU.xg +0 -0
  239. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/B4_SrGcsKAQmoTyHlgJCbM.xg +0 -0
  240. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/C07OQCKyw4Pe-CnjFiAMYO.xg +0 -0
  241. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/MYdvw1qGuyaRUH__.xg +0 -0
  242. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/Sz-2PgKnvb6aFls69-qFXg.xg +0 -0
  243. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/UL-CV6F9jMM-E1Pu.xg +0 -0
  244. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/XCu3RJ0UvDg_Tldl.xg +0 -0
  245. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/baR-U643iUDpvzTC.xg +0 -0
  246. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/eXBNG5wZHS5Alu3P.xg +0 -0
  247. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/hQ8sVn2LbTdF4wRm.xg +0 -0
  248. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/rK7pXm4TqLb9NzWd.xg +0 -0
  249. {gammonview-2.0.0 → gammonview-2.0.2}/samples/xg/uwvEbd63UvdX7SDin5o_Iw.xg +0 -0
  250. {gammonview-2.0.0 → gammonview-2.0.2}/tests/audit_corpus.py +0 -0
  251. {gammonview-2.0.0 → gammonview-2.0.2}/tests/fixtures.py +0 -0
  252. {gammonview-2.0.0 → gammonview-2.0.2}/tests/gen_js_v2_fixtures.py +0 -0
  253. {gammonview-2.0.0 → gammonview-2.0.2}/tests/gen_ogxm2_annos_fixtures.py +0 -0
  254. {gammonview-2.0.0 → gammonview-2.0.2}/tests/gen_ogxm2_blocks_fixtures.py +0 -0
  255. {gammonview-2.0.0 → gammonview-2.0.2}/tests/gen_ogxm2_fields_fixtures.py +0 -0
  256. {gammonview-2.0.0 → gammonview-2.0.2}/tests/gen_ogxm2_passthrough_fixtures.py +0 -0
  257. {gammonview-2.0.0 → gammonview-2.0.2}/tests/ogxm2_annos_cases.py +0 -0
  258. {gammonview-2.0.0 → gammonview-2.0.2}/tests/ogxm2_oracle.py +0 -0
  259. {gammonview-2.0.0 → gammonview-2.0.2}/tests/passthrough_cases.py +0 -0
  260. {gammonview-2.0.0 → gammonview-2.0.2}/tests/regen_golden.py +0 -0
  261. {gammonview-2.0.0 → gammonview-2.0.2}/tests/run_all.py +0 -0
  262. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_alternative_move_steps.py +0 -0
  263. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_analysis_orientation.py +0 -0
  264. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_analyze_position.py +0 -0
  265. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_basefill.py +0 -0
  266. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_bgf_alternatives.py +0 -0
  267. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_bgf_cube_response.py +0 -0
  268. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_bgf_move_steps.py +0 -0
  269. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_chunk_passthrough.py +0 -0
  270. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_count_illegal.py +0 -0
  271. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_cube_tiers.py +0 -0
  272. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_docs_presets.py +0 -0
  273. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_equity_loss.py +0 -0
  274. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_gvab_writer.py +0 -0
  275. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_illegal_move.py +0 -0
  276. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_illegal_play_steps.py +0 -0
  277. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_legality.py +0 -0
  278. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_loader_formats.py +0 -0
  279. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_luck_fill.py +0 -0
  280. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_met.py +0 -0
  281. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2_annos.py +0 -0
  282. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2_blocks_analysis.py +0 -0
  283. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2_fields_analysis.py +0 -0
  284. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2_passthrough.py +0 -0
  285. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm2_reference.py +0 -0
  286. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm_conformance.py +0 -0
  287. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm_export.py +0 -0
  288. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm_pipeline.py +0 -0
  289. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_ogxm_stats.py +0 -0
  290. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_parent_watch.py +0 -0
  291. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_position_id.py +0 -0
  292. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_read_gvab.py +0 -0
  293. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_reconstruct_mat.py +0 -0
  294. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_resign_actor.py +0 -0
  295. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_restated_play_analysis.py +0 -0
  296. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_screened_checker.py +0 -0
  297. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_share_link.py +0 -0
  298. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_xg_cube.py +0 -0
  299. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_xg_eval_levels.py +0 -0
  300. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_xg_move_steps.py +0 -0
  301. {gammonview-2.0.0 → gammonview-2.0.2}/tests/test_xg_zero_win_eval.py +0 -0
  302. {gammonview-2.0.0 → gammonview-2.0.2}/uv.lock +0 -0
@@ -13,6 +13,71 @@ when the repository was opened; they are kept because they record why things
13
13
  are the way they are — particularly the breaking changes and the measurements
14
14
  behind several design decisions. Dates are the tag dates.
15
15
 
16
+ ## 2.0.2 — 2026-10-12
17
+
18
+ **A set-up start is written once** *(fix)*
19
+
20
+ 2.0.1's fix for v1 set-up starts went too far. A game whose first ply sets up
21
+ the board *with* dice keeps that ply as a ply, and the writers then also took
22
+ the game's `initial_board` as a second start, so a rewritten file read back
23
+ with an extra set-up ply in front and every later ply one place out. Both
24
+ writers now take `initial_board` as the start only when the game has no
25
+ leading set-up ply of either kind, which is the v1 case 2.0.1 was for.
26
+
27
+ ## 2.0.1 — 2026-10-10
28
+
29
+ **Plies are counted the way v2 counts them** *(fix)*
30
+
31
+ OGXM counts plies from the alternative, as gnubg does, so a 3-ply search is `checker_ply` 2 in
32
+ the file; HedgeHog's author confirmed this as the convention. We wrote our own
33
+ count, so other readers took our 3ply for 4-ply, and we read HedgeHog's 3-ply
34
+ as 2ply. The writer now subtracts one from `checker_ply`, `cube_ply` and a
35
+ rollout's `move_ply`, and the reader adds it back. Documents keep the count
36
+ players use. A file of ours written by 2.0.0 still reads as it did: blocks
37
+ written since carry `plies=gnubg` in their `x-gammonview-analysis` annotation,
38
+ and a block of ours without it is read unshifted.
39
+
40
+ **`site` is never derived** *(change)*
41
+
42
+ The reader filled a missing `site` in from `city`, else the platform, and the
43
+ writer left out a `site` that matched them. Choosing a place to show is the
44
+ viewer's call, not the codec's, so `site` now reads back exactly as written,
45
+ and none when none was: a document with a city and no site keeps no site. The
46
+ writer stores any `site` in `x-gammonview-site`. A 2.0.0 file that left out a
47
+ site equal to its city now reads with no site.
48
+
49
+ **Our analysis is named in other readers** *(change)*
50
+
51
+ An analysis block now carries `model_name` "GammonView bgsage" beside its
52
+ `model_id`. HedgeHog's viewer names an analysis by `model_name` alone, so a
53
+ match analysed here and opened there showed no engine. The identifier is
54
+ unchanged, so nothing that keys on it moves.
55
+
56
+ **A v1 set-up start survives a rewrite** *(fix)*
57
+
58
+ A v1 file states a game that starts from a set-up board in its `GAME` header,
59
+ so a v1 read gives that game an `initial_board` and no leading set-position
60
+ ply. The v2 writers took the start only from such a ply, so rewriting the file
61
+ dropped the board, replayed the game from the opening, and flagged its plays
62
+ illegal. Both writers now take a bare `initial_board` as the game's start; a
63
+ leading set-up ply, where there is one, still wins.
64
+
65
+ **Long text values are split, not refused** *(fix)*
66
+
67
+ An event or a `site` too long for one v2 string is carried in our annotation,
68
+ which the profile says is split over `key~1`, `key~2`, … — but only the
69
+ analysis and annotation records were split, so the writer raised instead. Every
70
+ `x-gammonview-` record is split now, at character boundaries. A player name
71
+ past v2's 4 096 bytes, the one text field with no annotation to fall back on,
72
+ is still refused, and the profile now says so.
73
+
74
+ **`isPlayLegal` on a roll with no legal play** *(fix, JavaScript only)*
75
+
76
+ `legalPlays` returned no `best` for a dance, so `isPlayLegal` — and with it
77
+ `write_gvab` — threw a `TypeError` on a dice ply that recorded checker steps on
78
+ a roll that could not be played, where the Python writer flagged the play
79
+ illegal. The two writers now agree on such a file.
80
+
16
81
  ## 2.0.0 — 2026-10-08
17
82
 
18
83
  **`.gvab` is OGXM v2** *(format change)*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gammonview
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Backgammon match analysis — the pure-Python OGXM/GVA codec (`gvformat`) plus bgsage-powered match and position analysis (`gvanalysis`).
5
5
  Project-URL: Homepage, https://github.com/ngvlamis/gammonview-analysis
6
6
  Project-URL: Repository, https://github.com/ngvlamis/gammonview-analysis
@@ -66,8 +66,10 @@ unknown sections, presence-bit tails a later spec adds), it rides along in
66
66
  - **Output only** keys are produced by a reader (or a converter) for display and
67
67
  ignored by the writer, which derives them again: `ogid_before`, `ogid_after`,
68
68
  `notation`, `diff`, `played_equity`, `eval.equity`, `game_index`,
69
- `is_crawford`, `first_to_move`, `initial_board`, `action` (on a
70
- `cube_decision`) and `analysis_info.preset`.
69
+ `is_crawford`, `first_to_move`, `action` (on a `cube_decision`) and
70
+ `analysis_info.preset`. `initial_board` is output only *unless* the game has
71
+ no leading set-up ply, which is how a v1 read states a set-up start; the
72
+ writer then takes the board from it.
71
73
 
72
74
  ---
73
75
 
@@ -342,7 +344,7 @@ analysis to a document that already has one promotes it to this form
342
344
  | `initial_cube_value` | uint | The cube's value when the game opens (a power of two, within `cube_limit`). Default-omitted at 1 | `GAME.initial_cube_value` |
343
345
  | `initial_cube_owner` | uint | 0 White, 1 Black, 2 centred. Default-omitted at 2 | `GAME.initial_cube_owner` |
344
346
  | `auto_doubles` | uint | Automatic doubles this game opened with. The cube's real starting value is `initial_cube_value · 2^auto_doubles`. Default-omitted at 0 | `GAME.auto_doubles` |
345
- | `initial_board` | int[26] | **Output only.** The board of a game that starts from a set-up position, as a read also states it as the game's first ply | `GAME.initial_board` |
347
+ | `initial_board` | int[26] | The board of a game that starts from a set-up position, as a v2 read also states it as the game's first ply. **Output only** when that ply is there; a v1 read gives this key alone, and the writer then takes the start from it | `GAME.initial_board` |
346
348
  | `annotations` | Annotation[] | Notes on the game | `ANNO`, game scope |
347
349
  | `plies` | [Ply](#ply)[] | The game's actions in order | |
348
350
 
@@ -623,7 +625,7 @@ A hold with the cube dead has none.
623
625
  | `action` | string | **Output only.** `"no_double"` when holding is correct; else `"double_take"` (when `double_pass_equity ≥ double_take_equity`) or `"double_pass"` |
624
626
  | `equity_loss` | float | Fresh converter output only (0.0 for a correct hold); not stored, absent in a read. The error belongs to `missed_double` |
625
627
  | `eval` | Eval | The pre-roll probabilities |
626
- | `decision` | bool | Whether this cube decision counted as a PR decision (not trivial). **Absent when a `missed_double` sits beside it** in a block of ours or in a v1 read — that record carries the count, so one cube is never counted twice. (A block of another producer completed by `basefill` currently gets one; see [`OGXM_COMPUTED_FIELDS.md`](OGXM_COMPUTED_FIELDS.md)) |
628
+ | `decision` | bool | Whether this cube decision counted as a PR decision (not trivial). **Absent when a `missed_double` sits beside it** in a block of ours or in a v1 read — that record carries the count, so one cube is never counted twice. A block of another producer completed by `basefill` leaves it absent too; see [`OGXM_COMPUTED_FIELDS.md`](OGXM_COMPUTED_FIELDS.md) |
627
629
  | `eval_level` | string | The label of the cube result used. Our blocks only |
628
630
  | `level`, `take_point`, `window_searched`, `is_optional`, `is_free_cube`, `cubeful_take_value`, `currency`, `producer_ref`, `annotations` | | See [Per-decision v2 fields](#per-decision-v2-fields) |
629
631
 
@@ -921,8 +923,11 @@ The v1 byte layout is frozen in
921
923
  Those of v2 (spec §10): 1 000 games, 1 500 plies per game, 100 000 plies in a
922
924
  file, 1 024 alternatives per decision, 64 analysis blocks, 4 096 bytes per
923
925
  string, 4 096 annotations (256 KiB) and 256 drawings per annotation, a video URL
924
- of 512 bytes. The writer raises `ValueError` past a string or annotation limit and cuts an
925
- alternatives list at 1 024; the game and ply counts are not checked here.
926
+ of 512 bytes. The writer cuts an alternatives list at 1 024, and carries a
927
+ string past a field's own cap in an `x-gammonview-` annotation, split over as
928
+ many records as it needs. It raises `ValueError` past the game, ply, block and
929
+ annotation limits, and for a player name past 4 096 bytes, which has no
930
+ annotation to fall back on.
926
931
 
927
932
  ---
928
933
 
@@ -37,7 +37,7 @@ below over the whole sample corpus and against the reference codec.
37
37
  | `stage`, `round`, `table`, `city`, `country`, `event_url`, `match_ref` | the same fields |
38
38
  | `platform` | v2's `site`: the platform's host name. Renamed because our `site` predates it |
39
39
  | `white_profile`, `black_profile` | the `player` records (4.1): `user_id`, `rating` (a float, to 0.01), `rating_system`, `country`, `kind` |
40
- | `site` | `x-gammonview-site`, unless it is what the reader would derive anyway: `city`, else `platform`. Ours is whatever the source called the place, sometimes a city and sometimes a platform, and nothing tells which |
40
+ | `site` | `x-gammonview-site`, always. Ours is whatever the source or the user called the place, sometimes a city and sometimes a platform, and nothing tells which; it is never derived from `city` or `platform`, and a reader showing a place may fall back to them itself |
41
41
  | `games[].winner`, `points_won`, `is_lastgame` | `GAME` bits 0-2; `points_won` is written exactly when `winner` is |
42
42
  | `games[].termination`, `initial_cube_value`, `initial_cube_owner`, `auto_doubles` | the same `GAME` fields. The game's cube starts at `initial_cube_value · 2^auto_doubles`, owned by `initial_cube_owner` (5.3), in the OGIDs and in `gvanalysis` |
43
43
  | `games[].first_to_move`, `is_crawford`, `game_index` | derived, not stored |
@@ -71,6 +71,8 @@ has not assigned is kept whole, as `extras_raw`.
71
71
  A game that starts from a set-up board (a leading set-position ply with no
72
72
  dice) stores it as `GAME.initial_board` — unless the board is the opening
73
73
  position, which v2 never writes as an initial board, and then it stays a ply.
74
+ A game read from a v1 file states its set-up board only as `initial_board`,
75
+ with no such ply; the writer stores that as `GAME.initial_board` too.
74
76
 
75
77
  **Illegal plays.** A play that is not legal (M4) for its roll — or that any
76
78
  analysis block flags `illegal_move` — is written the way v2 records one: a
@@ -130,6 +132,16 @@ alternative overrides it only where its own differs, and a decision's or cube's
130
132
  `ply` is `checker_ply` / `cube_ply`. A luck record states its depth as
131
133
  `checker_ply`; the luck label travels once, in the block's annotation.
132
134
 
135
+ **Counting plies.** v2 counts plies gnubg's way, from the alternative rather
136
+ than from the position, and our documents count them the way players do: a
137
+ search our document calls `3ply` is `checker_ply` 2 in the file, and a rollout
138
+ whose moves are played at 2-ply has `move_ply` 1. The spec does not say this;
139
+ HedgeHog's files count so and its author confirmed it as the convention. So the
140
+ writer subtracts one from `checker_ply`, `cube_ply` and `move_ply`, and the
141
+ reader adds it back. A block of ours written before gvformat 2.0.1 counted as
142
+ its document does; it is told apart by its annotation, which since then carries
143
+ `plies=gnubg`, and is read without the shift.
144
+
133
145
  **Checker decisions** carry the alternatives in their order, each with its
134
146
  steps, equity, probabilities when recorded, and `is_played`. v2 then derives
135
147
  the best equity (the first alternative's) and, when the played move is listed,
@@ -190,7 +202,7 @@ holds is split over `key`, `key~1`, `key~2`, ... and joined in that order.
190
202
 
191
203
  | Key | Scope | Value after `1:` |
192
204
  |---|---|---|
193
- | `x-gammonview-analysis/<analysis_id>` | match | `;`-separated items: `level=<label>` (the block's own level, when it is not the block preset; empty for none), `luck=<label>` (when the block has luck), `pr=<tokens>` (always present; it marks the block as ours), `frame=<entries>` (when plies carry a source frame) |
205
+ | `x-gammonview-analysis/<analysis_id>` | match | `;`-separated items: `level=<label>` (the block's own level, when it is not the block preset; empty for none), `luck=<label>` (when the block has luck), `pr=<tokens>` (always present; it marks the block as ours), `plies=gnubg` (since gvformat 2.0.1: the block's levels count plies v2's way; see *Counting plies*), `frame=<entries>` (when plies carry a source frame) |
194
206
  | `x-gammonview-decisions/<analysis_id>` | ply | base64 of `DECS` records for this ply, in kind order. They stand in for `DECS`'s records of the same kind at that ply |
195
207
  | `x-gammonview-illegal-ply` | ply | the steps of the illegal play, `from/pips` joined by `,` (empty for a dance) |
196
208
  | `x-gammonview-site` | match | `site` |
@@ -234,7 +246,7 @@ decisions where the document says otherwise, each as its `ply_ref` and a letter:
234
246
  | met-id | a match block names its table: `kazaross-xg2` when the document named none |
235
247
  | mwc-frame | a source's frame (`mwc_frame`, §2) is carried by v2 only; v1 loses it |
236
248
  | default-omitted | a value equal to v2's default is not written, so it reads back absent: `variant` 0, a 0-0 `score_start`, `initial_cube_value` 1, `initial_cube_owner` 2 (centred), `auto_doubles` 0 or false, `rules_other` 0, a false flag, an empty string |
237
- | site-follows-place | a document with a `city` (else a `platform`) and no `site` reads back with that place as its `site` |
249
+ | site-is-stated | `site` reads back exactly as written, and a document with none reads back with none, whatever its `city` or `platform` |
238
250
  | resign-derived | a `resign_value` equal to what the game's points and the cube give is not kept; only a different one is |
239
251
  | settle-millionths | a `settle_value` is held to a millionth of a point |
240
252
  | block-provenance | what v2's block record states (producer, completeness, coverage, model name, identifier and digest, engine build, currency, cube efficiency, tables, dials, completion time, sources, a level beyond the labels) is in the v2 read-back only; v1 has no field for it |
@@ -256,7 +268,8 @@ decisions where the document says otherwise, each as its `ply_ref` and a letter:
256
268
  The writer refuses, with a `ValueError`, what v2's limits cannot hold rather than
257
269
  writing an invalid file: more than 1000 games, more than 1500 plies in a game,
258
270
  more than 100000 plies, more than 64 analysis blocks (`write_gvab_v1`: more than
259
- 16). `tests/test_ogxm2_limits.py` pins these and the cut above.
271
+ 16), and a player name longer than v2's 4096-byte string (the one text field
272
+ with no annotation to fall back on). `tests/test_ogxm2_limits.py` pins these and the cut above.
260
273
 
261
274
  Each rule above is in `tests/test_ogxm2_writer.py`, which fails on any difference
262
275
  none of them explains.
@@ -95,6 +95,12 @@ from .progress import ProgressBar
95
95
  # engine upgrade invalidates the goldens anyway.
96
96
  MODEL_NAME = "gv-bgsage"
97
97
 
98
+ #: What a reader shows for this analysis: v2's ``model_name``, beside the
99
+ #: opaque ``model_id``. HedgeHog's viewer names an analysis by this field alone,
100
+ #: so it is worded for a reader there, saying whose analysis it is; GammonView
101
+ #: labels a ``gv-bgsage`` block from the identifier and never shows it.
102
+ DISPLAY_NAME = "GammonView bgsage"
103
+
98
104
 
99
105
  def model_id() -> str:
100
106
  """``gv-bgsage/<bgsage version>`` -- the producer of this analysis.
@@ -631,6 +637,7 @@ def analyze_ogxm(
631
637
  return {
632
638
  "summary": {
633
639
  "engine": model_id(),
640
+ "model_name": DISPLAY_NAME,
634
641
  "engine_build": engine_build(),
635
642
  # Every decision of the match was looked at: v2's `complete`, as
636
643
  # against an incremental run that covered some of them.
@@ -135,6 +135,8 @@ GVAN_CUBE_REC = 2
135
135
  GVAN_CHECKER_REC_V2 = 7
136
136
  GVAN_CUBE_REC_V2 = 6
137
137
 
138
+ # v1's cap on a decision's alternatives, used by ``write_gvab_v1`` only. v2
139
+ # holds 1024 (``MAX_ALTS`` in ogxm2_writer).
138
140
  MAX_ALTS_PER_DECISION = 50
139
141
 
140
142
  # The largest hop a ply record can hold. A move step is one byte -- the start
@@ -1552,6 +1552,7 @@ def to_ogxm_json(result: dict, *, keep_orientation: bool = False) -> dict:
1552
1552
  # bgsage its version, so the label arrives in the summary;
1553
1553
  # the bare fallback keeps an older caller's output valid.
1554
1554
  "model_id": summary.get("engine") or "bgsage",
1555
+ **({"model_name": summary["model_name"]} if summary.get("model_name") else {}),
1555
1556
  "timestamp": ts,
1556
1557
  # What ran it, and that it ran the whole match (v2's `engine_build`
1557
1558
  # and `complete`); supplied for the same reason as `model_id`.
@@ -119,6 +119,11 @@ CURRENCY_CUBEFUL_MATCH = 2
119
119
  # holds; every value starts with GV_FORMAT.
120
120
  GV_FORMAT = "1:"
121
121
  GV_KEY_ANALYSIS = "x-gammonview-analysis/"
122
+ #: The ``x-gammonview-analysis`` item saying the block counts plies v2's way
123
+ #: (``PLY_SHIFT``). Written since gvformat 2.0.1; a block of ours without it
124
+ #: counts them as its document does.
125
+ GV_ITEM_PLIES = "plies"
126
+ GV_PLIES_GNUBG = "gnubg"
122
127
  GV_KEY_DECISIONS = "x-gammonview-decisions/"
123
128
  GV_KEY_ILLEGAL_PLY = "x-gammonview-illegal-ply"
124
129
  GV_KEY_SITE = "x-gammonview-site"
@@ -354,6 +359,37 @@ def _level(cur: _Cursor) -> dict:
354
359
  return lv
355
360
 
356
361
 
362
+ #: How v2 counts plies: gnubg's way, from the alternative rather than from the
363
+ #: position, so a search a document calls ``3ply`` is ``checker_ply`` 2 on the
364
+ #: wire. HedgeHog's own files count so, and its author confirmed it as the
365
+ #: convention (the spec does not say). A document keeps the count players use;
366
+ #: the codec moves by this much at the boundary, both ways.
367
+ PLY_SHIFT = 1
368
+
369
+
370
+ def shift_level(lv: dict | None, by: int) -> None:
371
+ """Move a level's depths -- ``checker_ply``, ``cube_ply`` and a rollout's
372
+ ``move_ply`` -- by ``by``, in place, never below 0."""
373
+ if not lv:
374
+ return
375
+ for k in ("checker_ply", "cube_ply"):
376
+ if k in lv:
377
+ lv[k] = max(lv[k] + by, 0)
378
+ r = lv.get("rollout")
379
+ if r and "move_ply" in r:
380
+ r["move_ply"] = max(r["move_ply"] + by, 0)
381
+
382
+
383
+ def shift_levels(anal: dict, decisions, by: int) -> None:
384
+ """``shift_level`` over a block's level and every level its decision
385
+ records (``_decode_decs``'s) and their alternatives state."""
386
+ shift_level(anal.get("level"), by)
387
+ for d in decisions:
388
+ shift_level(d.get("level"), by)
389
+ for alt in d.get("alternatives") or []:
390
+ shift_level(alt.get("level"), by)
391
+
392
+
357
393
  def _resolve(parent: dict, override: dict | None) -> dict:
358
394
  """The effective level: each field from the innermost tier carrying it."""
359
395
  if not override:
@@ -1842,10 +1878,10 @@ def read_ogxm2(data: bytes, *, verify_crc: bool = True, derive_ogids: bool = Tru
1842
1878
  ogxm[name] = {**ogxm.get(name, {}), **restored}
1843
1879
  for gi, game in enumerate(games):
1844
1880
  _restore_fields(game, gv_game, gi, GV_GAME_FIELDS)
1845
- # `site` is where the match was played: our annotation, else v2's `city`,
1846
- # else the platform's host name.
1847
- ogxm["site"] = (gv_match[(0, GV_KEY_SITE)] if (0, GV_KEY_SITE) in gv_match
1848
- else ogxm.get("city") or ogxm.get("platform") or None)
1881
+ # `site` is ours, free text, and v2 has no field for it: it is what our
1882
+ # annotation says, or nothing. It is never derived from `city` or the
1883
+ # platform -- that is a display choice, and the codec makes none.
1884
+ ogxm["site"] = gv_match.get((0, GV_KEY_SITE))
1849
1885
  if (0, GV_KEY_SCORE) in gv_match:
1850
1886
  try:
1851
1887
  ogxm["white_score"], ogxm["black_score"] = (
@@ -1897,14 +1933,24 @@ def read_ogxm2(data: bytes, *, verify_crc: bool = True, derive_ogids: bool = Tru
1897
1933
  decoded = []
1898
1934
  base_blocks = []
1899
1935
  ours_ids: set = set()
1936
+ recounted: set = set()
1900
1937
  alt_maps: dict = {}
1901
1938
  for blk in blocks:
1902
1939
  aid = blk["anal"]["analysis_id"]
1903
1940
  ours = None
1941
+ items = None
1904
1942
  if (0, GV_KEY_ANALYSIS + aid) in gv_match:
1905
- ours_ids.add(aid)
1906
1943
  items = dict(item.partition("=")[::2]
1907
1944
  for item in gv_match[(0, GV_KEY_ANALYSIS + aid)].split(";") if item)
1945
+ # Every producer counts plies v2's way except our writer before
1946
+ # gvformat 2.0.1, whose blocks say so by not naming the count.
1947
+ shift = items is None or items.get(GV_ITEM_PLIES) == GV_PLIES_GNUBG
1948
+ if shift:
1949
+ shift_levels(blk["anal"], blk["decisions"], PLY_SHIFT)
1950
+ elif items is not None:
1951
+ recounted.add(aid)
1952
+ if items is not None:
1953
+ ours_ids.add(aid)
1908
1954
  extra = {}
1909
1955
  for (ref, key), value in gv_ply.items():
1910
1956
  if key == GV_KEY_DECISIONS + aid:
@@ -1913,6 +1959,8 @@ def read_ogxm2(data: bytes, *, verify_crc: bool = True, derive_ogids: bool = Tru
1913
1959
  except ValueError as exc:
1914
1960
  raise GvabError(f"annotation on ply {ref} is not base64") from exc
1915
1961
  recs = _decode_decs(raw)
1962
+ if shift:
1963
+ shift_levels({}, recs, PLY_SHIFT)
1916
1964
  if any(d["ply_ref"] != ref for d in recs):
1917
1965
  raise GvabError(f"annotation on ply {ref} holds another ply's decision")
1918
1966
  extra[ref] = recs
@@ -1958,5 +2006,5 @@ def read_ogxm2(data: bytes, *, verify_crc: bool = True, derive_ogids: bool = Tru
1958
2006
  # What the file holds that this document cannot: kept for the writer.
1959
2007
  from .ogxm2_passthrough import attach
1960
2008
  header = (struct.unpack_from("<H", data, 6)[0], struct.unpack_from("<H", data, 10)[0])
1961
- attach(ogxm, data, header, sections, annos, ours_ids)
2009
+ attach(ogxm, data, header, sections, annos, ours_ids, recounted)
1962
2010
  return ogxm
@@ -461,11 +461,14 @@ def v1_sign_to_v2(body: bytes) -> bytes | None:
461
461
  # ---------------------------------------------------------------------------
462
462
 
463
463
  def attach(ogxm: dict, data: bytes, header: tuple, sections: list, annos: list,
464
- ours_ids: set) -> None:
464
+ ours_ids: set, recounted: set = frozenset()) -> None:
465
465
  """Attach ``_ogxm2_passthrough`` to ``ogxm`` when the file holds anything
466
466
  the document cannot. ``sections`` is ``[(type, offset, payload)]`` in file
467
- order, ``annos`` the decoded ``ANNO`` records (each with its ``raw`` bytes)
468
- and ``ours_ids`` the ``analysis_id`` of every block our writer made.
467
+ order, ``annos`` the decoded ``ANNO`` records (each with its ``raw`` bytes),
468
+ ``ours_ids`` the ``analysis_id`` of every block our writer made, and
469
+ ``recounted`` those of them that count plies as their document does (before
470
+ gvformat 2.0.1): their bytes are never kept, since the writer now counts
471
+ otherwise and would pair them with an annotation saying so.
469
472
 
470
473
  A file the writer reproduces byte for byte gets nothing, since there is
471
474
  nothing to keep: that is our own files, the common case, and it is decided
@@ -546,7 +549,7 @@ def attach(ogxm: dict, data: bytes, header: tuple, sections: list, annos: list,
546
549
  if mine is None or "decs" not in blk:
547
550
  return
548
551
  blk["fp"] = fingerprint(mine.anal, mine.decs, mine.anno_bytes())
549
- pt["blocks"] = pt_blocks
552
+ pt["blocks"] = {aid: blk for aid, blk in pt_blocks.items() if aid not in recounted}
550
553
  # The clock and the video are in the document; the source's bytes stand for
551
554
  # them while the document still encodes to what they decoded to.
552
555
  for name, canon in (("clck", parts.clck), ("vido", parts.vido)):
@@ -28,8 +28,8 @@ reader keeps and never interprets:
28
28
  ``x-gammonview-illegal-ply`` (ply scope)
29
29
  The steps of an illegal play that our document holds as a dice ply.
30
30
  ``x-gammonview-site``, ``x-gammonview-event`` (match scope)
31
- A ``site`` that is neither the city nor the platform (v2's own ``site`` is a
32
- host name), and an over-long ``event``.
31
+ Our free-text ``site``, which v2 has no field for (v2's own ``site`` is a
32
+ platform's host name), and an over-long ``event``.
33
33
  ``x-gammonview-<field>`` (match scope), ``x-gammonview-<side>_profile.<field>``, and
34
34
  ``x-gammonview-<field>`` at game scope
35
35
  A value v2 cannot hold (a string past its cap or outside its byte set, a
@@ -192,7 +192,10 @@ _ROLLOUT_UINTS = ("trials", "truncation_depth", "move_ply", "variance_reduction"
192
192
 
193
193
 
194
194
  def _level_bytes(lv: dict) -> bytes:
195
- """A ``level`` record from an override (``_level_diff``'s shape)."""
195
+ """A ``level`` record from an override (``_level_diff``'s shape), its depths
196
+ counted v2's way (``R.PLY_SHIFT``)."""
197
+ lv = copy.deepcopy(lv)
198
+ R.shift_level(lv, -R.PLY_SHIFT)
196
199
  fields: dict[int, bytes] = {}
197
200
  if "preset" in lv:
198
201
  fields[0] = _str(lv["preset"])
@@ -456,6 +459,26 @@ def _game_start(g: dict, opening_abs: list) -> int:
456
459
  and [int(v) for v in first.get("set_position") or [0] * 26] != opening_abs)
457
460
 
458
461
 
462
+ def _bare_initial_board(g: dict, opening_abs: list, start: int):
463
+ """The set-up board a game states only as ``initial_board``, or None.
464
+
465
+ A v1 read states a set-up start that way, with no leading ply (v1 kept the
466
+ board in the GAME header), so a v1 file rewritten as v2 would otherwise
467
+ lose its start -- and its plies, replayed from the opening, would be
468
+ flagged illegal. A leading set-up ply, where there is one, wins: a v2 read
469
+ states the board both ways. So does one *with* dice, which `_game_start`
470
+ keeps as a ply: the document already states its start, and taking
471
+ ``initial_board`` too would write that start twice."""
472
+ ib = g.get("initial_board")
473
+ if start or not isinstance(ib, list) or len(ib) != 26:
474
+ return None
475
+ plies = g.get("plies") or []
476
+ if plies and plies[0].get("action_id") == R.ACTION_SET_POSITION:
477
+ return None
478
+ board_abs = [int(v) for v in ib]
479
+ return None if board_abs == opening_abs else board_abs
480
+
481
+
459
482
  def ply_layout(doc: dict):
460
483
  """``(keys, starts)``: the ``(game, ply)`` of every ply v2 holds, in
461
484
  ``ply_ref`` order, and each game's leading set-up position (0 or 1) that it
@@ -543,8 +566,10 @@ class _Match:
543
566
  fields: dict[int, bytes] = {}
544
567
  start = _game_start(g, opening_abs)
545
568
  if start:
546
- first = plies[0]
547
- board_abs = [int(v) for v in first["set_position"]]
569
+ board_abs = [int(v) for v in plies[0]["set_position"]]
570
+ else:
571
+ board_abs = _bare_initial_board(g, opening_abs, start)
572
+ if board_abs is not None:
548
573
  board = _absolute_to_p1(board_abs)
549
574
  fields[3] = _board(board_abs)
550
575
 
@@ -832,11 +857,11 @@ class _Match:
832
857
  fields[11] = b""
833
858
  if doc.get("rated"):
834
859
  fields[25] = b""
835
- # Our `site` is where the match was played, free text; v2 states the
836
- # same in `city` (or names the platform). Only a `site` that is
837
- # neither needs its own record.
860
+ # Our `site` is free text with no v2 field, so it always rides in its
861
+ # own record -- even when it repeats the city, since the reader no
862
+ # longer derives one from the other.
838
863
  site = doc.get("site")
839
- if site and site != (doc.get("city") or doc.get("platform")):
864
+ if site:
840
865
  self.annos.append((SCOPE_MATCH, 0, R.GV_KEY_SITE, R.GV_FORMAT + site))
841
866
  variant = doc.get("variant") or 0
842
867
  if not _is_uint(variant):
@@ -1444,12 +1469,21 @@ def _flag_exceptions(doc_obj_by_key: dict, decoded_by_key: dict) -> list[str]:
1444
1469
 
1445
1470
 
1446
1471
  def _chunked(key: str, value: str) -> list[tuple[str, str]]:
1447
- """Split an ASCII value over ``key``, ``key~1``, ... to stay under
1448
- ``MAX_STRING``; the reader concatenates them in suffix order."""
1472
+ """Split a value over ``key``, ``key~1``, ... to stay under ``MAX_STRING``
1473
+ bytes; the reader concatenates them in suffix order. Cuts fall between
1474
+ characters, never inside one's UTF-8 bytes."""
1449
1475
  room = MAX_STRING - 16
1450
- if len(value) <= room:
1476
+ if len(value.encode("utf-8")) <= room:
1451
1477
  return [(key, value)]
1452
- parts = [value[i:i + room] for i in range(0, len(value), room)]
1478
+ parts, cur, size = [], [], 0
1479
+ for ch in value:
1480
+ n = len(ch.encode("utf-8"))
1481
+ if size + n > room:
1482
+ parts.append("".join(cur))
1483
+ cur, size = [], 0
1484
+ cur.append(ch)
1485
+ size += n
1486
+ parts.append("".join(cur))
1453
1487
  return [(key if i == 0 else f"{key}~{i}", part) for i, part in enumerate(parts)]
1454
1488
 
1455
1489
 
@@ -1844,10 +1878,14 @@ def _encode(ogxm: dict) -> _Parts:
1844
1878
  # from the records just written, and keep only where the document
1845
1879
  # disagrees.
1846
1880
  anal_dec = R._decode_anal(anal)
1881
+ decs_dec = R._decode_decs(decs_payload)
1882
+ R.shift_levels(anal_dec, decs_dec, R.PLY_SHIFT)
1847
1883
  extra_dec = {ref: R._decode_decs(b"".join(rec for _k, rec in recs))
1848
1884
  for ref, recs in extra.items()}
1885
+ for recs in extra_dec.values():
1886
+ R.shift_levels({}, recs, R.PLY_SHIFT)
1849
1887
  _info, decoded, _luck = R._v1_block(
1850
- anal_dec, R._decode_decs(decs_payload), match.ply_at, match.match_length,
1888
+ anal_dec, decs_dec, match.ply_at, match.match_length,
1851
1889
  ours={"extra": extra_dec, "exceptions": set(), "illegal": match.illegal,
1852
1890
  "frames": R._parse_frames(",".join(frames))})
1853
1891
  want = {(match.ref_of[key], key): select(p) for key, p in match.ply_at
@@ -1860,6 +1898,7 @@ def _encode(ogxm: dict) -> _Parts:
1860
1898
  k == R.KIND_ROLL for recs in extra.values() for k, _b in recs):
1861
1899
  items.append("luck=" + quote(luck_label, safe=_SAFE))
1862
1900
  items.append("pr=" + ",".join(tokens))
1901
+ items.append(f"{R.GV_ITEM_PLIES}={R.GV_PLIES_GNUBG}")
1863
1902
  if frames:
1864
1903
  items.append("frame=" + ",".join(frames))
1865
1904
  gv_annos: list[tuple[int, int, str, str]] = []
@@ -1929,9 +1968,12 @@ def _assemble(parts: _Parts, plan: P.Plan) -> bytes:
1929
1968
  ensure_ascii=False).encode("utf-8")).decode("ascii")
1930
1969
  ours += [(SCOPE_MATCH, 0, key, part)
1931
1970
  for key, part in _chunked(R.GV_KEY_ANNOTATIONS, R.GV_FORMAT + text)]
1932
- for scope, ref, key, value in ours:
1933
- entries.append((P.anno_sort_key(scope, ref, None, None, None, key, None, len(entries)),
1934
- _anno_record(scope, ref, key, value)))
1971
+ # A value of ours past a string's length goes over ``key~1``, ``key~2``, ...
1972
+ # (profile 3), whichever field it carries.
1973
+ for scope, ref, key0, value0 in ours:
1974
+ for key, value in _chunked(key0, value0):
1975
+ entries.append((P.anno_sort_key(scope, ref, None, None, None, key, None, len(entries)),
1976
+ _anno_record(scope, ref, key, value)))
1935
1977
  if entries:
1936
1978
  entries.sort(key=lambda e: e[0])
1937
1979
  payload = b"".join(rec for _k, rec in entries)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gammonview/gvformat",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.js",
@@ -32,6 +32,8 @@ export const HEADER_FLAG_HAS_ANALYSIS = 0x0001;
32
32
  // v3: the blanked bytes dropped -- checker record 7->3 B, cube record 6->2 B
33
33
  export const GVAN_VERSION = 3;
34
34
 
35
+ // v1's cap on a decision's alternatives, used by `write_gvab_v1` only. v2
36
+ // holds 1024 (`MAX_ALTS` in ogxm2_writer.js).
35
37
  export const MAX_ALTS_PER_DECISION = 50;
36
38
 
37
39
  // The largest hop a ply record can hold. A move step is one byte -- the start
@@ -146,17 +146,19 @@ function extend(mine, opp, dice, acc, results) {
146
146
  * possible, and when a non-double allows only one die, the larger one must be
147
147
  * played if either alone is playable.
148
148
  *
149
- * @returns {{plays: Set<string>, diceUsed: number}} plays are normalizePlay keys.
149
+ * @returns {{plays: Set<string>, best: Array, diceUsed: number}} plays are
150
+ * normalizePlay keys; best the maximal sequences they came from (empty in a
151
+ * dance, which `isPlayLegal` relies on).
150
152
  */
151
153
  export function legalPlays(mine, opp, d1, d2) {
152
154
  const results = [];
153
155
  const dice = d1 === d2 ? [d1, d1, d1, d1] : [d1, d2];
154
156
  extend(mine, opp, dice, [], results);
155
- if (!results.length) return { plays: new Set(), diceUsed: 0 };
157
+ if (!results.length) return { plays: new Set(), best: [], diceUsed: 0 };
156
158
 
157
159
  let longest = 0;
158
160
  for (const s of results) if (s.length > longest) longest = s.length;
159
- if (longest === 0) return { plays: new Set(), diceUsed: 0 };
161
+ if (longest === 0) return { plays: new Set(), best: [], diceUsed: 0 };
160
162
 
161
163
  let best = results.filter((s) => s.length === longest);
162
164