Files
galaxy-game/tools/local-dev/legacy-report/README.md
T
Ilia Denisov 8839f46c25 ui/phase-19: legacy parser learns Your Groups / Your Fleets / Incoming Groups
The parity rule from ui/PLAN.md says every UI phase that decodes a
new Report field must extend the legacy converter in lockstep.
Phase 19 brings ship groups (LocalGroup / OtherGroup /
UnidentifiedGroup / IncomingGroup) and LocalFleet onto the wire-
compatible UI surface; this commit teaches
tools/local-dev/legacy-report to populate the three sections that
exist in the legacy text format:

  - "Your Groups" → []LocalGroup. Cargo type, load, fleet name,
    state, on-planet vs hyperspace position (origin / range) all
    decoded; LocalGroup.ID is synthesised deterministically from
    the per-report group index so re-running the converter
    produces byte-identical JSON. Speed is left zero — the legacy
    table doesn't expose it.
  - "Your Fleets" → []LocalFleet. Origin / range / state mirror
    the row layout used by Killer / Tancordia variants; gplus's
    state-less rows still resolve.
  - "Incoming Groups" → []IncomingGroup. Origin / destination
    names — and `#NN` by-id references — resolve against the
    parsed planet tables. Because the section can land before
    "Your Planets" in some engines, group / fleet / incoming rows
    are buffered and resolved in `parser.finish` after every
    planet is known.

Battles, OtherGroup (only ever in battle rosters), and
UnidentifiedGroup stay out of scope — README.md spells out what
remains not-derivable.

Adds Killer031–033 / TSERCON_Z032–033 / Tancordia036–039 fixtures
to the dg directory and exercises three of them through new
TestParseDg{Killer031,Tancordia037,KNNTS041} smoke tests, plus
inline tests for each new section parser. Drops the stale
KNNTS039.json artefact left over from Phase 18 development.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 13:23:17 +02:00

159 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](../../../pkg/model/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`](../../../ui/PLAN.md).
## Build / run
```sh
# 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` |
| `LocalGroup[]` | `Your Groups` (Phase 19) |
| `LocalFleet[]` | `Your Fleets` (Phase 19) |
| `IncomingGroup[]` | `Incoming Groups` (Phase 19) |
Players whose name in the legacy file ends with `_RIP` are emitted with
the suffix stripped and `Extinct: true`.
`LocalGroup.ID` is synthesised deterministically from the per-report
group index via `uuid.NewSHA1`, so re-running the converter on the same
input file yields byte-identical JSON.
`LocalGroup.Speed` is left at zero — the legacy "Your Groups" table does
not expose ship speed; the UI can derive it from `pkg/calc.Speed` if
ever required.
Origin / Range names that don't resolve against the parsed planet
tables (foreign-only knowledge the local player lacks) cause the entire
group / fleet / incoming row to be dropped — preferable to fabricating
a destination.
## Skipped sections (today)
These exist in legacy reports but either have no UI decoder yet or
cannot be derived from the legacy text format at all. 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`) — battle rosters
inside these blocks carry minimal columns (no origin / range /
destination) and are intentionally skipped: parsing them would
produce mostly-empty `OtherGroup` records that drift away from the
typed contract.
- Bombings (`Bombings`)
- Ships in production (`Ships In Production`)
- `OtherGroup[]` — no top-level legacy section. Foreign groups appear
only inside battle rosters (see above), with stripped columns; the
synthetic JSON emits `otherGroup: []`.
- `UnidentifiedGroup[]` — no legacy section at all; synthetic JSON
emits `unidentifiedGroup: []`.
- `OtherShipClass[]` — present in legacy as `<Race> Ship Types`, but
no UI decoder yet; synthetic JSON emits `otherShipClass: []`.
- 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 empty `routes`.
## 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:
1. Identify the legacy section in `tools/local-dev/reports/dg/*.REP`
(and `gplus/*.REP`) that carries the field, using `game/rules.txt`
section "Отчет о результатах хода" as the column-layout reference.
2. Add a section to the state machine in `parser.go`
(`classifySection`, the `section` constants, the `parse*` methods).
3. Cover the new section with a unit test in `parser_test.go` (inline
minimal fixture) and update the smoke counts in
`TestParseDgKNNTS039` / `TestParseGplus40` so a future regression
that drops the section is caught.
4. Run `go test ./tools/local-dev/legacy-report/...`, then re-run the
CLI on `dg/KNNTS039.REP` and `gplus/40.REP` and visually skim the
JSON — the field should appear with sensible values.
## Tests
```sh
go test ./tools/local-dev/legacy-report/...
```
Inline fixtures exercise the per-section row parsers; smoke tests
parse the real fixtures under `tools/local-dev/reports/dg/` and
`tools/local-dev/reports/gplus/` and assert top-level counts. The
current smoke set spans:
- **dg/KNNTS039041** — KnightErrants saga; `041` is the only one
with `Incoming Groups`, exercising deferred name resolution.
- **dg/Killer031** — Killer engine variant with two `Your Fleets`
entries (`Fl1`, `F2`).
- **dg/Tancordia037** — the richest fixture: 311 local groups in
30 fleets, two incoming groups, "Incoming Groups" landing before
"Your Planets".
- **gplus/40.REP** — gplus variant; tabs in headers, pseudo-cyrillic
ship class names, single fleet, ten incoming groups.
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.