ui: plan 01-27 done #1
@@ -18,6 +18,7 @@ use (
|
|||||||
./pkg/storage
|
./pkg/storage
|
||||||
./pkg/transcoder
|
./pkg/transcoder
|
||||||
./pkg/util
|
./pkg/util
|
||||||
|
./tools/local-dev/legacy-report
|
||||||
./ui/core
|
./ui/core
|
||||||
./ui/wasm
|
./ui/wasm
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# 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` |
|
||||||
|
|
||||||
|
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 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 `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.
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Command legacy-report-to-json converts a legacy text-format Galaxy
|
||||||
|
// turn report (the "dg" / "gplus" engines) into the JSON shape of
|
||||||
|
// pkg/model/report.Report. The resulting file is what the UI client's
|
||||||
|
// DEV-only synthetic-report loader on the lobby consumes.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
legacyreport "galaxy/legacy-report"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
in := flag.String("in", "", "path to legacy .REP file (use - for stdin)")
|
||||||
|
out := flag.String("out", "", "path to write JSON to (use - or empty for stdout)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *in == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: legacy-report-to-json --in <path|-> [--out <path|->]")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, closeIn, err := openInput(*in)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "open input: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer closeIn()
|
||||||
|
|
||||||
|
rep, err := legacyreport.Parse(r)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "parse: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
w, closeOut, err := openOutput(*out)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "open output: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer closeOut()
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err := enc.Encode(rep); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "encode: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func openInput(path string) (io.Reader, func(), error) {
|
||||||
|
if path == "-" {
|
||||||
|
return os.Stdin, func() {}, nil
|
||||||
|
}
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return f, func() { _ = f.Close() }, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openOutput(path string) (io.Writer, func(), error) {
|
||||||
|
if path == "" || path == "-" {
|
||||||
|
return os.Stdout, func() {}, nil
|
||||||
|
}
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return f, func() { _ = f.Close() }, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
module galaxy/legacy-report
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require galaxy/model v0.0.0
|
||||||
|
|
||||||
|
require github.com/google/uuid v1.6.0 // indirect
|
||||||
|
|
||||||
|
replace galaxy/model => ../../../pkg/model
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
// Package legacyreport parses legacy text-format Galaxy turn reports
|
||||||
|
// (the "dg" / "gplus" engines) into [report.Report] values.
|
||||||
|
//
|
||||||
|
// Scope is intentionally narrow: only the fields the UI client decodes
|
||||||
|
// from server reports today (planets, players, own ship classes,
|
||||||
|
// header data). Everything else in the legacy file is silently
|
||||||
|
// skipped. The synthetic-report parity rule in ui/PLAN.md is the
|
||||||
|
// source of truth for when to extend this parser; the package's
|
||||||
|
// README.md tracks every legacy section that could be wired up later
|
||||||
|
// when the corresponding UI decoder lands.
|
||||||
|
package legacyreport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"galaxy/model/report"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse reads a legacy text report and returns a [report.Report]
|
||||||
|
// carrying the in-scope subset of fields. The Width and Height of the
|
||||||
|
// returned report are both set to the legacy "Size" value (galaxies
|
||||||
|
// are square in the legacy engines).
|
||||||
|
func Parse(r io.Reader) (report.Report, error) {
|
||||||
|
p := newParser()
|
||||||
|
sc := bufio.NewScanner(r)
|
||||||
|
sc.Buffer(make([]byte, 1024*1024), 4*1024*1024)
|
||||||
|
for sc.Scan() {
|
||||||
|
if err := p.handle(sc.Text()); err != nil {
|
||||||
|
return report.Report{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := sc.Err(); err != nil {
|
||||||
|
return report.Report{}, fmt.Errorf("legacyreport: scan: %w", err)
|
||||||
|
}
|
||||||
|
return p.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
type section int
|
||||||
|
|
||||||
|
const (
|
||||||
|
sectionNone section = iota
|
||||||
|
sectionStatusOfPlayers
|
||||||
|
sectionYourVote
|
||||||
|
sectionYourPlanets
|
||||||
|
sectionOtherPlanets
|
||||||
|
sectionUninhabitedPlanets
|
||||||
|
sectionUnidentifiedPlanets
|
||||||
|
sectionYourShipTypes
|
||||||
|
)
|
||||||
|
|
||||||
|
type parser struct {
|
||||||
|
rep report.Report
|
||||||
|
sec section
|
||||||
|
otherOwner string
|
||||||
|
skipHeader bool
|
||||||
|
sawHeader bool
|
||||||
|
sawSize bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParser() *parser {
|
||||||
|
return &parser{sec: sectionNone}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) handle(line string) error {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if !p.sawHeader && trimmed != "" {
|
||||||
|
if race, turn, ok := parseHeader(trimmed); ok {
|
||||||
|
p.rep.Race = race
|
||||||
|
p.rep.Turn = turn
|
||||||
|
p.sawHeader = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !p.sawSize && strings.HasPrefix(trimmed, "Size:") {
|
||||||
|
if w, planets, ok := parseSize(trimmed); ok {
|
||||||
|
p.rep.Width = w
|
||||||
|
p.rep.Height = w
|
||||||
|
p.rep.PlanetCount = planets
|
||||||
|
p.sawSize = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if newSec, owner, isHeader := classifySection(trimmed); isHeader {
|
||||||
|
p.sec = newSec
|
||||||
|
p.otherOwner = owner
|
||||||
|
p.skipHeader = newSec != sectionNone
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if p.sec == sectionNone {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if p.skipHeader {
|
||||||
|
p.skipHeader = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Fields(trimmed)
|
||||||
|
switch p.sec {
|
||||||
|
case sectionStatusOfPlayers:
|
||||||
|
p.parsePlayer(fields)
|
||||||
|
case sectionYourVote:
|
||||||
|
p.parseYourVote(fields)
|
||||||
|
case sectionYourPlanets:
|
||||||
|
p.parseLocalPlanet(fields)
|
||||||
|
case sectionOtherPlanets:
|
||||||
|
p.parseOtherPlanet(fields)
|
||||||
|
case sectionUninhabitedPlanets:
|
||||||
|
p.parseUninhabitedPlanet(fields)
|
||||||
|
case sectionUnidentifiedPlanets:
|
||||||
|
p.parseUnidentifiedPlanet(fields)
|
||||||
|
case sectionYourShipTypes:
|
||||||
|
p.parseShipClass(fields)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) finish() (report.Report, error) {
|
||||||
|
if !p.sawHeader {
|
||||||
|
return report.Report{}, errors.New("legacyreport: missing report header line")
|
||||||
|
}
|
||||||
|
return p.rep, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHeader extracts (race, turn) from
|
||||||
|
// "<Race> Report for Galaxy ... Turn N ...".
|
||||||
|
func parseHeader(line string) (string, uint, bool) {
|
||||||
|
race, rest, ok := strings.Cut(line, " Report for Galaxy ")
|
||||||
|
if !ok {
|
||||||
|
return "", 0, false
|
||||||
|
}
|
||||||
|
_, afterTurn, ok := strings.Cut(rest, " Turn ")
|
||||||
|
if !ok {
|
||||||
|
return "", 0, false
|
||||||
|
}
|
||||||
|
after := strings.Fields(afterTurn)
|
||||||
|
if len(after) == 0 {
|
||||||
|
return "", 0, false
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseUint(after[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, false
|
||||||
|
}
|
||||||
|
return race, uint(n), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSize extracts (size, planets) from
|
||||||
|
// "Size: W Planets: P Players: N". Players is intentionally
|
||||||
|
// dropped: report.Report has no field for it.
|
||||||
|
func parseSize(line string) (uint32, uint32, bool) {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
var size, planets uint32
|
||||||
|
var ok bool
|
||||||
|
for i := 0; i+1 < len(fields); i++ {
|
||||||
|
key := strings.TrimRight(fields[i], ":")
|
||||||
|
switch key {
|
||||||
|
case "Size":
|
||||||
|
if n, err := strconv.ParseUint(fields[i+1], 10, 32); err == nil {
|
||||||
|
size = uint32(n)
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
case "Planets":
|
||||||
|
if n, err := strconv.ParseUint(fields[i+1], 10, 32); err == nil {
|
||||||
|
planets = uint32(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size, planets, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// classifySection returns the section the trimmed line opens. When
|
||||||
|
// isHeader is true the caller transitions state — sectionNone there
|
||||||
|
// means "switch into skip mode" (an uninteresting section like
|
||||||
|
// Bombings, Battle at ..., or a foreign Ship Types block).
|
||||||
|
func classifySection(line string) (sec section, owner string, isHeader bool) {
|
||||||
|
switch line {
|
||||||
|
case "":
|
||||||
|
return sectionNone, "", false
|
||||||
|
case "Your Planets":
|
||||||
|
return sectionYourPlanets, "", true
|
||||||
|
case "Your Ship Types":
|
||||||
|
return sectionYourShipTypes, "", true
|
||||||
|
case "Uninhabited Planets":
|
||||||
|
return sectionUninhabitedPlanets, "", true
|
||||||
|
case "Unidentified Planets":
|
||||||
|
return sectionUnidentifiedPlanets, "", true
|
||||||
|
case "Your vote:":
|
||||||
|
return sectionYourVote, "", true
|
||||||
|
case "Your Sciences",
|
||||||
|
"Bombings",
|
||||||
|
"Ships In Production",
|
||||||
|
"Approaching Groups",
|
||||||
|
"Broadcast Message",
|
||||||
|
"Battle Protocol":
|
||||||
|
return sectionNone, "", true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "Status of Players") {
|
||||||
|
return sectionStatusOfPlayers, "", true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "Battle at ") {
|
||||||
|
return sectionNone, "", true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "=== ATTENTION") {
|
||||||
|
return sectionNone, "", true
|
||||||
|
}
|
||||||
|
if owner, ok := singleTokenPrefix(line, " Planets"); ok {
|
||||||
|
return sectionOtherPlanets, owner, true
|
||||||
|
}
|
||||||
|
if _, ok := singleTokenPrefix(line, " Ship Types"); ok {
|
||||||
|
return sectionNone, "", true
|
||||||
|
}
|
||||||
|
if _, ok := singleTokenPrefix(line, " Sciences"); ok {
|
||||||
|
return sectionNone, "", true
|
||||||
|
}
|
||||||
|
if _, ok := singleTokenPrefix(line, " Groups"); ok {
|
||||||
|
return sectionNone, "", true
|
||||||
|
}
|
||||||
|
return sectionNone, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleTokenPrefix(line, suffix string) (string, bool) {
|
||||||
|
if !strings.HasSuffix(line, suffix) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
prefix := strings.TrimSuffix(line, suffix)
|
||||||
|
if prefix == "" || strings.ContainsAny(prefix, " \t") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return prefix, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePlayer expects 10 columns:
|
||||||
|
//
|
||||||
|
// N D W S C P I # R V
|
||||||
|
func (p *parser) parsePlayer(fields []string) {
|
||||||
|
if len(fields) < 10 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := fields[0]
|
||||||
|
drive, err := parseFloat(fields[1])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
weapons, _ := parseFloat(fields[2])
|
||||||
|
shields, _ := parseFloat(fields[3])
|
||||||
|
cargo, _ := parseFloat(fields[4])
|
||||||
|
population, _ := parseFloat(fields[5])
|
||||||
|
industry, _ := parseFloat(fields[6])
|
||||||
|
plCount, err := strconv.ParseUint(fields[7], 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
relation := fields[8]
|
||||||
|
votes, _ := parseFloat(fields[9])
|
||||||
|
|
||||||
|
extinct := strings.HasSuffix(name, "_RIP")
|
||||||
|
if extinct {
|
||||||
|
name = strings.TrimSuffix(name, "_RIP")
|
||||||
|
}
|
||||||
|
p.rep.Player = append(p.rep.Player, report.Player{
|
||||||
|
Name: name,
|
||||||
|
Drive: report.F(drive),
|
||||||
|
Weapons: report.F(weapons),
|
||||||
|
Shields: report.F(shields),
|
||||||
|
Cargo: report.F(cargo),
|
||||||
|
Population: report.F(population),
|
||||||
|
Industry: report.F(industry),
|
||||||
|
Planets: uint16(plCount),
|
||||||
|
Relation: relation,
|
||||||
|
Votes: report.F(votes),
|
||||||
|
Extinct: extinct,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) parseYourVote(fields []string) {
|
||||||
|
if len(fields) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.rep.VoteFor = fields[0]
|
||||||
|
if v, err := parseFloat(fields[1]); err == nil {
|
||||||
|
p.rep.Votes = report.F(v)
|
||||||
|
}
|
||||||
|
p.sec = sectionNone
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLocalPlanet expects 13 columns:
|
||||||
|
//
|
||||||
|
// # X Y N S P I R Production $ M C L
|
||||||
|
func (p *parser) parseLocalPlanet(fields []string) {
|
||||||
|
lp, ok := decodeLocalPlanetRow(fields)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.rep.LocalPlanet = append(p.rep.LocalPlanet, lp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) parseOtherPlanet(fields []string) {
|
||||||
|
lp, ok := decodeLocalPlanetRow(fields)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.rep.OtherPlanet = append(p.rep.OtherPlanet, report.OtherPlanet{
|
||||||
|
Owner: p.otherOwner,
|
||||||
|
LocalPlanet: lp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeLocalPlanetRow(fields []string) (report.LocalPlanet, bool) {
|
||||||
|
var lp report.LocalPlanet
|
||||||
|
if len(fields) < 13 {
|
||||||
|
return lp, false
|
||||||
|
}
|
||||||
|
number, err := strconv.ParseUint(fields[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return lp, false
|
||||||
|
}
|
||||||
|
x, _ := parseFloat(fields[1])
|
||||||
|
y, _ := parseFloat(fields[2])
|
||||||
|
size, _ := parseFloat(fields[4])
|
||||||
|
population, _ := parseFloat(fields[5])
|
||||||
|
industry, _ := parseFloat(fields[6])
|
||||||
|
resources, _ := parseFloat(fields[7])
|
||||||
|
capital, _ := parseFloat(fields[9])
|
||||||
|
material, _ := parseFloat(fields[10])
|
||||||
|
colonists, _ := parseFloat(fields[11])
|
||||||
|
free, _ := parseFloat(fields[12])
|
||||||
|
|
||||||
|
lp.Number = uint(number)
|
||||||
|
lp.X = report.F(x)
|
||||||
|
lp.Y = report.F(y)
|
||||||
|
lp.Name = fields[3]
|
||||||
|
lp.Size = report.F(size)
|
||||||
|
lp.Resources = report.F(resources)
|
||||||
|
lp.Capital = report.F(capital)
|
||||||
|
lp.Material = report.F(material)
|
||||||
|
lp.Industry = report.F(industry)
|
||||||
|
lp.Population = report.F(population)
|
||||||
|
lp.Colonists = report.F(colonists)
|
||||||
|
lp.Production = fields[8]
|
||||||
|
lp.FreeIndustry = report.F(free)
|
||||||
|
return lp, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseUninhabitedPlanet expects 8 columns:
|
||||||
|
//
|
||||||
|
// # X Y N S R $ M
|
||||||
|
func (p *parser) parseUninhabitedPlanet(fields []string) {
|
||||||
|
if len(fields) < 8 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
number, err := strconv.ParseUint(fields[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, _ := parseFloat(fields[1])
|
||||||
|
y, _ := parseFloat(fields[2])
|
||||||
|
size, _ := parseFloat(fields[4])
|
||||||
|
resources, _ := parseFloat(fields[5])
|
||||||
|
capital, _ := parseFloat(fields[6])
|
||||||
|
material, _ := parseFloat(fields[7])
|
||||||
|
|
||||||
|
var u report.UninhabitedPlanet
|
||||||
|
u.Number = uint(number)
|
||||||
|
u.X = report.F(x)
|
||||||
|
u.Y = report.F(y)
|
||||||
|
u.Name = fields[3]
|
||||||
|
u.Size = report.F(size)
|
||||||
|
u.Resources = report.F(resources)
|
||||||
|
u.Capital = report.F(capital)
|
||||||
|
u.Material = report.F(material)
|
||||||
|
p.rep.UninhabitedPlanet = append(p.rep.UninhabitedPlanet, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseUnidentifiedPlanet expects 3 columns:
|
||||||
|
//
|
||||||
|
// # X Y
|
||||||
|
func (p *parser) parseUnidentifiedPlanet(fields []string) {
|
||||||
|
if len(fields) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
number, err := strconv.ParseUint(fields[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x, _ := parseFloat(fields[1])
|
||||||
|
y, _ := parseFloat(fields[2])
|
||||||
|
p.rep.UnidentifiedPlanet = append(p.rep.UnidentifiedPlanet, report.UnidentifiedPlanet{
|
||||||
|
Number: uint(number),
|
||||||
|
X: report.F(x),
|
||||||
|
Y: report.F(y),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseShipClass expects 7 columns:
|
||||||
|
//
|
||||||
|
// N D A W S C M
|
||||||
|
func (p *parser) parseShipClass(fields []string) {
|
||||||
|
if len(fields) < 7 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
drive, err := parseFloat(fields[1])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
armament, err := strconv.ParseUint(fields[2], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
weapons, _ := parseFloat(fields[3])
|
||||||
|
shields, _ := parseFloat(fields[4])
|
||||||
|
cargo, _ := parseFloat(fields[5])
|
||||||
|
mass, _ := parseFloat(fields[6])
|
||||||
|
|
||||||
|
p.rep.LocalShipClass = append(p.rep.LocalShipClass, report.ShipClass{
|
||||||
|
Name: fields[0],
|
||||||
|
Drive: report.F(drive),
|
||||||
|
Armament: uint(armament),
|
||||||
|
Weapons: report.F(weapons),
|
||||||
|
Shields: report.F(shields),
|
||||||
|
Cargo: report.F(cargo),
|
||||||
|
Mass: report.F(mass),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFloat(s string) (float64, error) {
|
||||||
|
return strconv.ParseFloat(s, 64)
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
package legacyreport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"galaxy/model/report"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestParseHeaderAndSize covers the standalone single-line preamble.
|
||||||
|
func TestParseHeaderAndSize(t *testing.T) {
|
||||||
|
in := strings.Join([]string{
|
||||||
|
" KnightErrants Report for Galaxy PLUS dg283 Turn 39 Thu Jul 06 09:01:16 2000",
|
||||||
|
"",
|
||||||
|
" Size: 800 Planets: 700 Players: 91",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := rep.Race, "KnightErrants"; got != want {
|
||||||
|
t.Errorf("Race = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := rep.Turn, uint(39); got != want {
|
||||||
|
t.Errorf("Turn = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := rep.Width, uint32(800); got != want {
|
||||||
|
t.Errorf("Width = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := rep.Height, uint32(800); got != want {
|
||||||
|
t.Errorf("Height = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := rep.PlanetCount, uint32(700); got != want {
|
||||||
|
t.Errorf("PlanetCount = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseStatusOfPlayers exercises the alive / extinct distinction
|
||||||
|
// that drives the races view.
|
||||||
|
func TestParseStatusOfPlayers(t *testing.T) {
|
||||||
|
in := strings.Join([]string{
|
||||||
|
"Race Report for Galaxy PLUS Turn 1",
|
||||||
|
"",
|
||||||
|
"Status of Players (total 10.00 votes)",
|
||||||
|
"",
|
||||||
|
"N D W S C P I # R V",
|
||||||
|
"Alpha 4.51 2.24 1.80 1.00 100.00 80.00 3 War 3.00",
|
||||||
|
"Bravo 9.03 5.62 2.16 1.53 200.00 150.00 5 Peace 5.00",
|
||||||
|
"Gone_RIP 1.00 1.00 1.00 1.00 0.00 0.00 0 War 0.00",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.Player), 3; got != want {
|
||||||
|
t.Fatalf("len(Player) = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
alpha := rep.Player[0]
|
||||||
|
if alpha.Name != "Alpha" || alpha.Extinct {
|
||||||
|
t.Errorf("Alpha = %+v, want Name=Alpha extinct=false", alpha)
|
||||||
|
}
|
||||||
|
if alpha.Planets != 3 || alpha.Relation != "War" {
|
||||||
|
t.Errorf("Alpha planets/relation = %d/%q, want 3/War", alpha.Planets, alpha.Relation)
|
||||||
|
}
|
||||||
|
if got, want := float64(alpha.Drive), 4.51; got != want {
|
||||||
|
t.Errorf("Alpha.Drive = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
gone := rep.Player[2]
|
||||||
|
if gone.Name != "Gone" || !gone.Extinct {
|
||||||
|
t.Errorf("Gone = %+v, want Name=Gone extinct=true (suffix _RIP stripped)", gone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseYourVote(t *testing.T) {
|
||||||
|
in := strings.Join([]string{
|
||||||
|
"Race Report for Galaxy PLUS Turn 1",
|
||||||
|
"",
|
||||||
|
"Your vote:",
|
||||||
|
"",
|
||||||
|
"R V",
|
||||||
|
"KnightErrants 16.02",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if rep.VoteFor != "KnightErrants" {
|
||||||
|
t.Errorf("VoteFor = %q, want KnightErrants", rep.VoteFor)
|
||||||
|
}
|
||||||
|
if got, want := float64(rep.Votes), 16.02; got != want {
|
||||||
|
t.Errorf("Votes = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseLocalAndOtherPlanets(t *testing.T) {
|
||||||
|
in := strings.Join([]string{
|
||||||
|
"Race Report for Galaxy PLUS Turn 1",
|
||||||
|
"",
|
||||||
|
"Your Planets",
|
||||||
|
"",
|
||||||
|
" # X Y N S P I R P $ M C L",
|
||||||
|
" 17 171.05 700.24 Castle 1000.00 1000.00 1000.00 10.00 Drive_Research 0.00 0.68 88.78 1000.00",
|
||||||
|
" 87 169.59 694.49 North 500.00 500.00 500.00 10.00 Capital 0.00 0.52 35.76 500.00",
|
||||||
|
"",
|
||||||
|
"Monstrai Planets",
|
||||||
|
"",
|
||||||
|
" # X Y N S P I R P $ M C L",
|
||||||
|
" 12 303.84 579.23 Skarabei 500.00 500.00 500.00 10.00 Capital 0.00 70.99 20.03 341.78",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.LocalPlanet), 2; got != want {
|
||||||
|
t.Fatalf("len(LocalPlanet) = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
castle := rep.LocalPlanet[0]
|
||||||
|
if castle.Number != 17 || castle.Name != "Castle" {
|
||||||
|
t.Errorf("Castle = (%d, %q), want (17, Castle)", castle.Number, castle.Name)
|
||||||
|
}
|
||||||
|
if castle.Production != "Drive_Research" {
|
||||||
|
t.Errorf("Castle.Production = %q, want Drive_Research", castle.Production)
|
||||||
|
}
|
||||||
|
if got, want := float64(castle.Size), 1000.0; got != want {
|
||||||
|
t.Errorf("Castle.Size = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.OtherPlanet), 1; got != want {
|
||||||
|
t.Fatalf("len(OtherPlanet) = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
skarabei := rep.OtherPlanet[0]
|
||||||
|
if skarabei.Owner != "Monstrai" {
|
||||||
|
t.Errorf("Skarabei.Owner = %q, want Monstrai", skarabei.Owner)
|
||||||
|
}
|
||||||
|
if skarabei.Number != 12 || skarabei.Name != "Skarabei" {
|
||||||
|
t.Errorf("Skarabei = (%d, %q), want (12, Skarabei)", skarabei.Number, skarabei.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseUninhabitedAndUnidentified(t *testing.T) {
|
||||||
|
in := strings.Join([]string{
|
||||||
|
"Race Report for Galaxy PLUS Turn 1",
|
||||||
|
"",
|
||||||
|
"Uninhabited Planets",
|
||||||
|
"",
|
||||||
|
" # X Y N S R $ M",
|
||||||
|
" 9 117.87 795.21 Dw2 500.00 10.00 0.00 500.00",
|
||||||
|
"",
|
||||||
|
"Unidentified Planets",
|
||||||
|
"",
|
||||||
|
" # X Y",
|
||||||
|
" 0 738.08 600.26",
|
||||||
|
" 1 579.12 489.37",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.UninhabitedPlanet), 1; got != want {
|
||||||
|
t.Fatalf("len(UninhabitedPlanet) = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
dw2 := rep.UninhabitedPlanet[0]
|
||||||
|
if dw2.Number != 9 || dw2.Name != "Dw2" {
|
||||||
|
t.Errorf("Dw2 = (%d, %q), want (9, Dw2)", dw2.Number, dw2.Name)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.UnidentifiedPlanet), 2; got != want {
|
||||||
|
t.Fatalf("len(UnidentifiedPlanet) = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
first := rep.UnidentifiedPlanet[0]
|
||||||
|
if first.Number != 0 || float64(first.X) != 738.08 || float64(first.Y) != 600.26 {
|
||||||
|
t.Errorf("Unidentified[0] = %+v, want (0, 738.08, 600.26)", first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseShipClasses(t *testing.T) {
|
||||||
|
in := strings.Join([]string{
|
||||||
|
"Race Report for Galaxy PLUS Turn 1",
|
||||||
|
"",
|
||||||
|
"Your Ship Types",
|
||||||
|
"",
|
||||||
|
"N D A W S C M",
|
||||||
|
"Frontier 11.37 0 0.00 0.00 1.00 12.37",
|
||||||
|
"Bow105 74.77 105 1.00 19.72 1.00 148.49",
|
||||||
|
"",
|
||||||
|
"Monstrai Ship Types",
|
||||||
|
"",
|
||||||
|
"N D A W S C M",
|
||||||
|
"Dragon 16.70 1 1.10 1.00 1 19.80",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.LocalShipClass), 2; got != want {
|
||||||
|
t.Fatalf("len(LocalShipClass) = %d, want %d (foreign types must be ignored)", got, want)
|
||||||
|
}
|
||||||
|
bow := rep.LocalShipClass[1]
|
||||||
|
if bow.Name != "Bow105" || bow.Armament != 105 {
|
||||||
|
t.Errorf("Bow105 name/armament = %q/%d, want Bow105/105", bow.Name, bow.Armament)
|
||||||
|
}
|
||||||
|
if got, want := float64(bow.Drive), 74.77; got != want {
|
||||||
|
t.Errorf("Bow105.Drive = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSkipsBattlesAndBombings(t *testing.T) {
|
||||||
|
in := strings.Join([]string{
|
||||||
|
"Race Report for Galaxy PLUS Turn 1",
|
||||||
|
"",
|
||||||
|
"Battle at (#7) B-007",
|
||||||
|
"",
|
||||||
|
"# T D W S C T Q L",
|
||||||
|
"1 PeaceShip 4 0 0 0 - 0 1 Out_Battle",
|
||||||
|
"",
|
||||||
|
"Battle Protocol",
|
||||||
|
"",
|
||||||
|
"Foo fires on Bar : Destroyed",
|
||||||
|
"",
|
||||||
|
"Bombings",
|
||||||
|
"",
|
||||||
|
"# data line",
|
||||||
|
"",
|
||||||
|
"Your Planets",
|
||||||
|
"",
|
||||||
|
" # X Y N S P I R P $ M C L",
|
||||||
|
" 17 171.05 700.24 Castle 1000.00 1000.00 1000.00 10.00 Capital 0.00 0.68 88.78 1000.00",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.LocalPlanet), 1; got != want {
|
||||||
|
t.Fatalf("len(LocalPlanet) = %d, want %d (battle/bombing rows must not leak in)", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseDgKNNTS039 is a smoke test: the parser must produce
|
||||||
|
// stable top-line counts from the real dg/KNNTS039.REP fixture.
|
||||||
|
// Field-level fidelity is asserted in the unit tests above; this
|
||||||
|
// test catches regressions where a section-classifier change
|
||||||
|
// silently drops half the data.
|
||||||
|
func TestParseDgKNNTS039(t *testing.T) {
|
||||||
|
const path = "../reports/dg/KNNTS039.REP"
|
||||||
|
rep, err := parseFile(t, path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
t.Skipf("legacy report fixture missing: %s", path)
|
||||||
|
}
|
||||||
|
t.Fatalf("Parse %s: %v", path, err)
|
||||||
|
}
|
||||||
|
want := struct {
|
||||||
|
race string
|
||||||
|
turn uint
|
||||||
|
mapW, mapH, planetCount uint32
|
||||||
|
voteFor string
|
||||||
|
votes float64
|
||||||
|
players, extinct, local, other, uninhabited, unidentified, shipClasses int
|
||||||
|
}{
|
||||||
|
race: "KnightErrants", turn: 39,
|
||||||
|
mapW: 800, mapH: 800, planetCount: 700,
|
||||||
|
voteFor: "KnightErrants", votes: 16.02,
|
||||||
|
players: 91, extinct: 49,
|
||||||
|
local: 22, other: 89, uninhabited: 17, unidentified: 572,
|
||||||
|
shipClasses: 24,
|
||||||
|
}
|
||||||
|
if rep.Race != want.race || rep.Turn != want.turn {
|
||||||
|
t.Errorf("(race, turn) = (%q, %d), want (%q, %d)", rep.Race, rep.Turn, want.race, want.turn)
|
||||||
|
}
|
||||||
|
if rep.Width != want.mapW || rep.Height != want.mapH || rep.PlanetCount != want.planetCount {
|
||||||
|
t.Errorf("(W, H, planets) = (%d, %d, %d), want (%d, %d, %d)",
|
||||||
|
rep.Width, rep.Height, rep.PlanetCount, want.mapW, want.mapH, want.planetCount)
|
||||||
|
}
|
||||||
|
if rep.VoteFor != want.voteFor || float64(rep.Votes) != want.votes {
|
||||||
|
t.Errorf("(voteFor, votes) = (%q, %v), want (%q, %v)",
|
||||||
|
rep.VoteFor, float64(rep.Votes), want.voteFor, want.votes)
|
||||||
|
}
|
||||||
|
extinct := 0
|
||||||
|
for _, pl := range rep.Player {
|
||||||
|
if pl.Extinct {
|
||||||
|
extinct++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.Player), want.players; got != exp {
|
||||||
|
t.Errorf("len(Player) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if extinct != want.extinct {
|
||||||
|
t.Errorf("extinct = %d, want %d", extinct, want.extinct)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.LocalPlanet), want.local; got != exp {
|
||||||
|
t.Errorf("len(LocalPlanet) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.OtherPlanet), want.other; got != exp {
|
||||||
|
t.Errorf("len(OtherPlanet) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.UninhabitedPlanet), want.uninhabited; got != exp {
|
||||||
|
t.Errorf("len(UninhabitedPlanet) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.UnidentifiedPlanet), want.unidentified; got != exp {
|
||||||
|
t.Errorf("len(UnidentifiedPlanet) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.LocalShipClass), want.shipClasses; got != exp {
|
||||||
|
t.Errorf("len(LocalShipClass) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseGplus40 mirrors TestParseDgKNNTS039 for the gplus engine
|
||||||
|
// fixture so the variant difference (tabs vs spaces in headers) is
|
||||||
|
// exercised on a real file.
|
||||||
|
func TestParseGplus40(t *testing.T) {
|
||||||
|
const path = "../reports/gplus/40.REP"
|
||||||
|
rep, err := parseFile(t, path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
t.Skipf("legacy report fixture missing: %s", path)
|
||||||
|
}
|
||||||
|
t.Fatalf("Parse %s: %v", path, err)
|
||||||
|
}
|
||||||
|
want := struct {
|
||||||
|
race string
|
||||||
|
turn uint
|
||||||
|
mapW, mapH, planetCount uint32
|
||||||
|
players, extinct, local, other, uninhabited, unidentified, shipClasses int
|
||||||
|
}{
|
||||||
|
race: "MbI", turn: 40,
|
||||||
|
mapW: 350, mapH: 350, planetCount: 300,
|
||||||
|
players: 26, extinct: 0,
|
||||||
|
local: 26, other: 116, uninhabited: 7, unidentified: 152,
|
||||||
|
shipClasses: 56,
|
||||||
|
}
|
||||||
|
if rep.Race != want.race || rep.Turn != want.turn {
|
||||||
|
t.Errorf("(race, turn) = (%q, %d), want (%q, %d)", rep.Race, rep.Turn, want.race, want.turn)
|
||||||
|
}
|
||||||
|
if rep.Width != want.mapW || rep.Height != want.mapH || rep.PlanetCount != want.planetCount {
|
||||||
|
t.Errorf("(W, H, planets) = (%d, %d, %d), want (%d, %d, %d)",
|
||||||
|
rep.Width, rep.Height, rep.PlanetCount, want.mapW, want.mapH, want.planetCount)
|
||||||
|
}
|
||||||
|
extinct := 0
|
||||||
|
for _, pl := range rep.Player {
|
||||||
|
if pl.Extinct {
|
||||||
|
extinct++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.Player), want.players; got != exp {
|
||||||
|
t.Errorf("len(Player) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if extinct != want.extinct {
|
||||||
|
t.Errorf("extinct = %d, want %d", extinct, want.extinct)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.LocalPlanet), want.local; got != exp {
|
||||||
|
t.Errorf("len(LocalPlanet) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.OtherPlanet), want.other; got != exp {
|
||||||
|
t.Errorf("len(OtherPlanet) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.UninhabitedPlanet), want.uninhabited; got != exp {
|
||||||
|
t.Errorf("len(UninhabitedPlanet) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.UnidentifiedPlanet), want.unidentified; got != exp {
|
||||||
|
t.Errorf("len(UnidentifiedPlanet) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
if got, exp := len(rep.LocalShipClass), want.shipClasses; got != exp {
|
||||||
|
t.Errorf("len(LocalShipClass) = %d, want %d", got, exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFile(t *testing.T, rel string) (report.Report, error) {
|
||||||
|
t.Helper()
|
||||||
|
abs, err := filepath.Abs(rel)
|
||||||
|
if err != nil {
|
||||||
|
return report.Report{}, err
|
||||||
|
}
|
||||||
|
f, err := os.Open(abs)
|
||||||
|
if err != nil {
|
||||||
|
return report.Report{}, err
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
return Parse(f)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user