A Go module under tools/local-dev/legacy-report that converts the "dg" / "gplus" engine .REP files in tools/local-dev/reports/ into the JSON shape of pkg/model/report.Report. The output drives a DEV-only synthetic-mode loader on the UI lobby so the map, inspectors, and order-overlay can be exercised against rich game states without playing many turns end-to-end. Scope is intentionally narrow: only the fields the UI client decodes today (planets, players, own ship classes, header). Importing pkg/model/report keeps the parser and the typed contract in lockstep — any backwards-incompatible schema change breaks the tool's compilation before it ships. The README spells out the parity rule for extending the parser alongside future UI decoders. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
legacy-report-to-json
Converts legacy text-format Galaxy turn reports (the dg and gplus
engines that lived under tools/local-dev/reports/) into the JSON
shape of pkg/model/report.Report.
The output is consumed by the DEV-only synthetic-report loader on
the UI client's lobby (import.meta.env.DEV). With it, the map view,
inspectors, and order-overlay can be exercised against rich game
states without playing many turns end-to-end against a real backend.
The tool is part of the synthetic-report parity rule documented in
ui/PLAN.md.
Build / run
# from the repo root, with the Go workspace active
go run ./tools/local-dev/legacy-report/cmd/legacy-report-to-json \
--in tools/local-dev/reports/dg/KNNTS039.REP \
--out tools/local-dev/reports/dg/KNNTS039.json
--in reads - as stdin; --out defaults to stdout when empty or
-. The tool exits non-zero on any I/O or parse failure.
Supported input variants
| Variant | Sample dir | Status |
|---|---|---|
| dg | tools/local-dev/reports/dg/*.REP |
First-class |
| gplus | tools/local-dev/reports/gplus/*.REP |
First-class |
| ng | tools/local-dev/reports/ng/*.rep |
Not supported |
| lucky | tools/local-dev/reports/lucky/*.rep |
Not supported |
dg uses CRLF line endings, gplus uses LF and tabs in section indentation;
both are space-aligned tabular inside data blocks. The parser splits on
runs of whitespace (strings.Fields) so the same code handles both.
Pseudo-Cyrillic glyphs (MbI, KAMA3, 9IMA) appear in some races
and ship class names but are stored as plain ASCII letter substitutions
— no encoding conversion is needed.
In-scope fields (current)
The parser only fills the subset of report.Report that the UI client
already decodes from server responses
(ui/frontend/src/api/game-state.ts → decodeReport):
report.Report field |
Source section in legacy file |
|---|---|
Race |
<Race> Report for Galaxy ... line |
Turn |
same |
Width, Height |
Size: N (square galaxies) |
PlanetCount |
Planets: N |
VoteFor, Votes |
Your vote: block |
Player[] |
Status of Players (total ...) |
LocalPlanet[] |
Your Planets |
OtherPlanet[] |
<Race> Planets (one per race) |
UninhabitedPlanet[] |
Uninhabited Planets |
UnidentifiedPlanet[] |
Unidentified Planets |
LocalShipClass[] |
Your Ship Types |
Players whose name in the legacy file ends with _RIP are emitted with
the suffix stripped and Extinct: true.
Skipped sections (today)
These exist in legacy reports but have no UI decoder yet, so the parser ignores them. Each becomes in-scope as soon as its UI phase lands (see "Adding a new field" below).
- Foreign / other ship types (
<Race> Ship Types) - Sciences, both local (
Your Sciences) and foreign (<Race> Sciences) - Battles (
Battle at (#N) Name,Battle Protocol) - Bombings (
Bombings) - Approaching / foreign groups (
Approaching Groups,<Race> Groups) - Ships in production (
Ships In Production) - Cargo routes — no dedicated section in the legacy text format; the
synthetic JSON emits
route: []. The UI's overlay path (applyOrderOverlay) supports running on top of an emptyroutes.
Adding a new field
ui/PLAN.md carries a global rule: every UI phase that extends
decodeReport to read a new report.Report field also extends this
parser, in the same PR, to populate it from legacy text — or, if the
field cannot be derived, adds an entry to the Skipped sections
list above with a one-line explanation.
The Go side of the rule is enforced mechanically: this tool imports
galaxy/model/report, so any backwards-incompatible change to the
schema breaks the tool's compilation before the change ships.
When extending:
- Identify the legacy section in
tools/local-dev/reports/dg/*.REP(andgplus/*.REP) that carries the field, usinggame/rules.txtsection "Отчет о результатах хода" as the column-layout reference. - Add a section to the state machine in
parser.go(classifySection, thesectionconstants, theparse*methods). - Cover the new section with a unit test in
parser_test.go(inline minimal fixture) and update the smoke counts inTestParseDgKNNTS039/TestParseGplus40so a future regression that drops the section is caught. - Run
go test ./tools/local-dev/legacy-report/..., then re-run the CLI ondg/KNNTS039.REPandgplus/40.REPand visually skim the JSON — the field should appear with sensible values.
Tests
go test ./tools/local-dev/legacy-report/...
Inline fixtures exercise the per-section row parsers; smoke tests
parse the real dg/KNNTS039.REP and gplus/40.REP and assert
top-level counts (number of planets, players, extinct races, ship
classes). Field-level fidelity is the inline tests' responsibility;
the smoke tests catch regressions where a refactor of the section
classifier silently drops a whole table.