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>
This commit is contained in:
@@ -60,22 +60,47 @@ already decodes from server responses
|
|||||||
| `UninhabitedPlanet[]` | `Uninhabited Planets` |
|
| `UninhabitedPlanet[]` | `Uninhabited Planets` |
|
||||||
| `UnidentifiedPlanet[]`| `Unidentified Planets` |
|
| `UnidentifiedPlanet[]`| `Unidentified Planets` |
|
||||||
| `LocalShipClass[]` | `Your Ship Types` |
|
| `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
|
Players whose name in the legacy file ends with `_RIP` are emitted with
|
||||||
the suffix stripped and `Extinct: true`.
|
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)
|
## Skipped sections (today)
|
||||||
|
|
||||||
These exist in legacy reports but have no UI decoder yet, so the
|
These exist in legacy reports but either have no UI decoder yet or
|
||||||
parser ignores them. Each becomes in-scope as soon as its UI phase
|
cannot be derived from the legacy text format at all. Each becomes
|
||||||
lands (see "Adding a new field" below).
|
in-scope as soon as its UI phase lands (see "Adding a new field"
|
||||||
|
below).
|
||||||
|
|
||||||
- Foreign / other ship types (`<Race> Ship Types`)
|
- Foreign / other ship types (`<Race> Ship Types`)
|
||||||
- Sciences, both local (`Your Sciences`) and foreign (`<Race> Sciences`)
|
- Sciences, both local (`Your Sciences`) and foreign (`<Race> Sciences`)
|
||||||
- Battles (`Battle at (#N) Name`, `Battle Protocol`)
|
- 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`)
|
- Bombings (`Bombings`)
|
||||||
- Approaching / foreign groups (`Approaching Groups`, `<Race> Groups`)
|
|
||||||
- Ships in production (`Ships In Production`)
|
- 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
|
- Cargo routes — no dedicated section in the legacy text format; the
|
||||||
synthetic JSON emits `route: []`. The UI's overlay path
|
synthetic JSON emits `route: []`. The UI's overlay path
|
||||||
(`applyOrderOverlay`) supports running on top of an empty `routes`.
|
(`applyOrderOverlay`) supports running on top of an empty `routes`.
|
||||||
@@ -114,8 +139,20 @@ go test ./tools/local-dev/legacy-report/...
|
|||||||
```
|
```
|
||||||
|
|
||||||
Inline fixtures exercise the per-section row parsers; smoke tests
|
Inline fixtures exercise the per-section row parsers; smoke tests
|
||||||
parse the real `dg/KNNTS039.REP` and `gplus/40.REP` and assert
|
parse the real fixtures under `tools/local-dev/reports/dg/` and
|
||||||
top-level counts (number of planets, players, extinct races, ship
|
`tools/local-dev/reports/gplus/` and assert top-level counts. The
|
||||||
classes). Field-level fidelity is the inline tests' responsibility;
|
current smoke set spans:
|
||||||
the smoke tests catch regressions where a refactor of the section
|
|
||||||
classifier silently drops a whole table.
|
- **dg/KNNTS039–041** — 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.
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ go 1.26.0
|
|||||||
|
|
||||||
require galaxy/model v0.0.0
|
require galaxy/model v0.0.0
|
||||||
|
|
||||||
require github.com/google/uuid v1.6.0 // indirect
|
require github.com/google/uuid v1.6.0
|
||||||
|
|
||||||
replace galaxy/model => ../../../pkg/model
|
replace galaxy/model => ../../../pkg/model
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
//
|
//
|
||||||
// Scope is intentionally narrow: only the fields the UI client decodes
|
// Scope is intentionally narrow: only the fields the UI client decodes
|
||||||
// from server reports today (planets, players, own ship classes,
|
// from server reports today (planets, players, own ship classes,
|
||||||
// header data). Everything else in the legacy file is silently
|
// header data, plus — added in Phase 19 — own ship groups, own fleets
|
||||||
|
// and incoming groups). Everything else in the legacy file is silently
|
||||||
// skipped. The synthetic-report parity rule in ui/PLAN.md is the
|
// skipped. The synthetic-report parity rule in ui/PLAN.md is the
|
||||||
// source of truth for when to extend this parser; the package's
|
// source of truth for when to extend this parser; the package's
|
||||||
// README.md tracks every legacy section that could be wired up later
|
// README.md tracks every legacy section that could be wired up later
|
||||||
@@ -18,6 +19,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"galaxy/model/report"
|
"galaxy/model/report"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +54,9 @@ const (
|
|||||||
sectionUninhabitedPlanets
|
sectionUninhabitedPlanets
|
||||||
sectionUnidentifiedPlanets
|
sectionUnidentifiedPlanets
|
||||||
sectionYourShipTypes
|
sectionYourShipTypes
|
||||||
|
sectionYourGroups
|
||||||
|
sectionYourFleets
|
||||||
|
sectionIncomingGroups
|
||||||
)
|
)
|
||||||
|
|
||||||
type parser struct {
|
type parser struct {
|
||||||
@@ -60,6 +66,50 @@ type parser struct {
|
|||||||
skipHeader bool
|
skipHeader bool
|
||||||
sawHeader bool
|
sawHeader bool
|
||||||
sawSize bool
|
sawSize bool
|
||||||
|
|
||||||
|
// Group/fleet/incoming rows are buffered during the scan because
|
||||||
|
// they carry destination/origin planet names that may resolve
|
||||||
|
// against the planet tables only after the whole file has been
|
||||||
|
// read — "Incoming Groups" can appear before "Your Planets" in
|
||||||
|
// some engine variants.
|
||||||
|
pendingGroups []pendingGroup
|
||||||
|
pendingFleets []pendingFleet
|
||||||
|
pendingIncomings []pendingIncoming
|
||||||
|
}
|
||||||
|
|
||||||
|
type pendingGroup struct {
|
||||||
|
g uint
|
||||||
|
number uint
|
||||||
|
class string
|
||||||
|
drive float64
|
||||||
|
weapons float64
|
||||||
|
shields float64
|
||||||
|
cargoTech float64
|
||||||
|
cargoType string
|
||||||
|
load float64
|
||||||
|
destinationName string
|
||||||
|
originName string // empty when "-"
|
||||||
|
rangeStr string // empty when "-"
|
||||||
|
mass float64
|
||||||
|
fleet string // empty when "-"
|
||||||
|
state string
|
||||||
|
}
|
||||||
|
|
||||||
|
type pendingFleet struct {
|
||||||
|
name string
|
||||||
|
groups uint
|
||||||
|
destinationName string
|
||||||
|
originName string // empty when "-"
|
||||||
|
rangeStr string // empty when "-"
|
||||||
|
state string
|
||||||
|
}
|
||||||
|
|
||||||
|
type pendingIncoming struct {
|
||||||
|
originName string
|
||||||
|
destinationName string
|
||||||
|
distance float64
|
||||||
|
speed float64
|
||||||
|
mass float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func newParser() *parser {
|
func newParser() *parser {
|
||||||
@@ -121,6 +171,12 @@ func (p *parser) handle(line string) error {
|
|||||||
p.parseUnidentifiedPlanet(fields)
|
p.parseUnidentifiedPlanet(fields)
|
||||||
case sectionYourShipTypes:
|
case sectionYourShipTypes:
|
||||||
p.parseShipClass(fields)
|
p.parseShipClass(fields)
|
||||||
|
case sectionYourGroups:
|
||||||
|
p.parseYourGroup(fields)
|
||||||
|
case sectionYourFleets:
|
||||||
|
p.parseYourFleet(fields)
|
||||||
|
case sectionIncomingGroups:
|
||||||
|
p.parseIncomingGroup(fields)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -129,6 +185,7 @@ func (p *parser) finish() (report.Report, error) {
|
|||||||
if !p.sawHeader {
|
if !p.sawHeader {
|
||||||
return report.Report{}, errors.New("legacyreport: missing report header line")
|
return report.Report{}, errors.New("legacyreport: missing report header line")
|
||||||
}
|
}
|
||||||
|
p.resolvePending()
|
||||||
return p.rep, nil
|
return p.rep, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +247,12 @@ func classifySection(line string) (sec section, owner string, isHeader bool) {
|
|||||||
return sectionYourPlanets, "", true
|
return sectionYourPlanets, "", true
|
||||||
case "Your Ship Types":
|
case "Your Ship Types":
|
||||||
return sectionYourShipTypes, "", true
|
return sectionYourShipTypes, "", true
|
||||||
|
case "Your Groups":
|
||||||
|
return sectionYourGroups, "", true
|
||||||
|
case "Your Fleets":
|
||||||
|
return sectionYourFleets, "", true
|
||||||
|
case "Incoming Groups":
|
||||||
|
return sectionIncomingGroups, "", true
|
||||||
case "Uninhabited Planets":
|
case "Uninhabited Planets":
|
||||||
return sectionUninhabitedPlanets, "", true
|
return sectionUninhabitedPlanets, "", true
|
||||||
case "Unidentified Planets":
|
case "Unidentified Planets":
|
||||||
@@ -432,6 +495,256 @@ func (p *parser) parseShipClass(fields []string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseYourGroup buffers a "Your Groups" row for post-processing in
|
||||||
|
// [parser.finish]. Columns (16 fields, last is state):
|
||||||
|
//
|
||||||
|
// G # T D W S C T Q D F R P M L state
|
||||||
|
//
|
||||||
|
// where the second D is the destination planet name, F is the origin
|
||||||
|
// planet name (or "-" for on-planet groups), R is the remaining
|
||||||
|
// distance, and L is the fleet membership (or "-").
|
||||||
|
func (p *parser) parseYourGroup(fields []string) {
|
||||||
|
if len(fields) < 16 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g, err := strconv.ParseUint(fields[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
number, err := strconv.ParseUint(fields[1], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
drive, _ := parseFloat(fields[3])
|
||||||
|
weapons, _ := parseFloat(fields[4])
|
||||||
|
shields, _ := parseFloat(fields[5])
|
||||||
|
cargoTech, _ := parseFloat(fields[6])
|
||||||
|
load, _ := parseFloat(fields[8])
|
||||||
|
mass, _ := parseFloat(fields[13])
|
||||||
|
|
||||||
|
p.pendingGroups = append(p.pendingGroups, pendingGroup{
|
||||||
|
g: uint(g),
|
||||||
|
number: uint(number),
|
||||||
|
class: fields[2],
|
||||||
|
drive: drive,
|
||||||
|
weapons: weapons,
|
||||||
|
shields: shields,
|
||||||
|
cargoTech: cargoTech,
|
||||||
|
cargoType: fields[7],
|
||||||
|
load: load,
|
||||||
|
destinationName: fields[9],
|
||||||
|
originName: dashOrEmpty(fields[10]),
|
||||||
|
rangeStr: dashOrEmpty(fields[11]),
|
||||||
|
mass: mass,
|
||||||
|
fleet: dashOrEmpty(fields[14]),
|
||||||
|
state: fields[15],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseYourFleet buffers a "Your Fleets" row. Columns vary by engine
|
||||||
|
// — Killer/Tancordia ship 8 fields including a trailing state token,
|
||||||
|
// gplus emits 7 (no state). Layout:
|
||||||
|
//
|
||||||
|
// # N G D F R P [state]
|
||||||
|
//
|
||||||
|
// where D is the destination planet name, F is the origin planet
|
||||||
|
// name (or "-"), and R is the remaining distance.
|
||||||
|
func (p *parser) parseYourFleet(fields []string) {
|
||||||
|
if len(fields) < 7 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groups, err := strconv.ParseUint(fields[2], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state := ""
|
||||||
|
if len(fields) >= 8 {
|
||||||
|
state = fields[7]
|
||||||
|
}
|
||||||
|
p.pendingFleets = append(p.pendingFleets, pendingFleet{
|
||||||
|
name: fields[1],
|
||||||
|
groups: uint(groups),
|
||||||
|
destinationName: fields[3],
|
||||||
|
originName: dashOrEmpty(fields[4]),
|
||||||
|
rangeStr: dashOrEmpty(fields[5]),
|
||||||
|
state: state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIncomingGroup buffers an "Incoming Groups" row. Columns:
|
||||||
|
//
|
||||||
|
// O D R S M
|
||||||
|
func (p *parser) parseIncomingGroup(fields []string) {
|
||||||
|
if len(fields) < 5 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
distance, err := parseFloat(fields[2])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
speed, _ := parseFloat(fields[3])
|
||||||
|
mass, _ := parseFloat(fields[4])
|
||||||
|
p.pendingIncomings = append(p.pendingIncomings, pendingIncoming{
|
||||||
|
originName: fields[0],
|
||||||
|
destinationName: fields[1],
|
||||||
|
distance: distance,
|
||||||
|
speed: speed,
|
||||||
|
mass: mass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvePending walks the buffered group/fleet/incoming rows and
|
||||||
|
// emits the typed entries on the report. Names that resolve neither
|
||||||
|
// against the parsed planet tables nor the "#NN" id syntax are
|
||||||
|
// skipped silently — they typically point at planets not visible to
|
||||||
|
// the local player. Stable LocalGroup IDs are derived from the
|
||||||
|
// per-report group index so repeated conversions of the same file
|
||||||
|
// produce byte-identical JSON.
|
||||||
|
func (p *parser) resolvePending() {
|
||||||
|
for _, pg := range p.pendingGroups {
|
||||||
|
dest, ok := p.lookupPlanetNumber(pg.destinationName)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var origin *uint
|
||||||
|
if pg.originName != "" {
|
||||||
|
if n, ok := p.lookupPlanetNumber(pg.originName); ok {
|
||||||
|
v := n
|
||||||
|
origin = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var rng *report.Float
|
||||||
|
if pg.rangeStr != "" {
|
||||||
|
if r, err := parseFloat(pg.rangeStr); err == nil {
|
||||||
|
v := report.F(r)
|
||||||
|
rng = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var fleet *string
|
||||||
|
if pg.fleet != "" {
|
||||||
|
f := pg.fleet
|
||||||
|
fleet = &f
|
||||||
|
}
|
||||||
|
tech := map[string]report.Float{
|
||||||
|
"drive": report.F(pg.drive),
|
||||||
|
"weapons": report.F(pg.weapons),
|
||||||
|
"shields": report.F(pg.shields),
|
||||||
|
"cargo": report.F(pg.cargoTech),
|
||||||
|
}
|
||||||
|
p.rep.LocalGroup = append(p.rep.LocalGroup, report.LocalGroup{
|
||||||
|
OtherGroup: report.OtherGroup{
|
||||||
|
Number: pg.number,
|
||||||
|
Class: pg.class,
|
||||||
|
Tech: tech,
|
||||||
|
Cargo: pg.cargoType,
|
||||||
|
Load: report.F(pg.load),
|
||||||
|
Destination: dest,
|
||||||
|
Origin: origin,
|
||||||
|
Range: rng,
|
||||||
|
Mass: report.F(pg.mass),
|
||||||
|
},
|
||||||
|
ID: syntheticGroupID(pg.g),
|
||||||
|
State: pg.state,
|
||||||
|
Fleet: fleet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pf := range p.pendingFleets {
|
||||||
|
dest, ok := p.lookupPlanetNumber(pf.destinationName)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var origin *uint
|
||||||
|
if pf.originName != "" {
|
||||||
|
if n, ok := p.lookupPlanetNumber(pf.originName); ok {
|
||||||
|
v := n
|
||||||
|
origin = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var rng *report.Float
|
||||||
|
if pf.rangeStr != "" {
|
||||||
|
if r, err := parseFloat(pf.rangeStr); err == nil {
|
||||||
|
v := report.F(r)
|
||||||
|
rng = &v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.rep.LocalFleet = append(p.rep.LocalFleet, report.LocalFleet{
|
||||||
|
Name: pf.name,
|
||||||
|
Groups: pf.groups,
|
||||||
|
Destination: dest,
|
||||||
|
Origin: origin,
|
||||||
|
Range: rng,
|
||||||
|
State: pf.state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pi := range p.pendingIncomings {
|
||||||
|
origin, ok := p.lookupPlanetNumber(pi.originName)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dest, ok := p.lookupPlanetNumber(pi.destinationName)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.rep.IncomingGroup = append(p.rep.IncomingGroup, report.IncomingGroup{
|
||||||
|
Origin: origin,
|
||||||
|
Destination: dest,
|
||||||
|
Distance: report.F(pi.distance),
|
||||||
|
Speed: report.F(pi.speed),
|
||||||
|
Mass: report.F(pi.mass),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupPlanetNumber resolves a legacy planet reference — either a
|
||||||
|
// "#NN" by-id form or a planet name from one of the parsed planet
|
||||||
|
// tables. Returns false when the planet is not visible to the local
|
||||||
|
// player (the caller drops the row).
|
||||||
|
func (p *parser) lookupPlanetNumber(s string) (uint, bool) {
|
||||||
|
if strings.HasPrefix(s, "#") {
|
||||||
|
n, err := strconv.ParseUint(s[1:], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return uint(n), true
|
||||||
|
}
|
||||||
|
for _, lp := range p.rep.LocalPlanet {
|
||||||
|
if lp.Name == s {
|
||||||
|
return lp.Number, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, op := range p.rep.OtherPlanet {
|
||||||
|
if op.Name == s {
|
||||||
|
return op.Number, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, up := range p.rep.UninhabitedPlanet {
|
||||||
|
if up.Name == s {
|
||||||
|
return up.Number, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// syntheticGroupNamespace seeds [uuid.NewSHA1] for the per-report
|
||||||
|
// group-index → UUID derivation. The constant value is arbitrary;
|
||||||
|
// any UUID would work as long as it stays stable across releases so
|
||||||
|
// re-running the converter on the same input file yields the same
|
||||||
|
// LocalGroup IDs.
|
||||||
|
var syntheticGroupNamespace = uuid.MustParse("be01a000-0000-0000-0000-000000000001")
|
||||||
|
|
||||||
|
func syntheticGroupID(g uint) uuid.UUID {
|
||||||
|
return uuid.NewSHA1(syntheticGroupNamespace, fmt.Appendf(nil, "legacy-local-group-%d", g))
|
||||||
|
}
|
||||||
|
|
||||||
|
func dashOrEmpty(s string) string {
|
||||||
|
if s == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func parseFloat(s string) (float64, error) {
|
func parseFloat(s string) (float64, error) {
|
||||||
return strconv.ParseFloat(s, 64)
|
return strconv.ParseFloat(s, 64)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,133 +244,322 @@ func TestParseSkipsBattlesAndBombings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseYourGroups exercises the local-group section. Two rows
|
||||||
|
// cover the on-planet ("In_Orbit", origin "-") and in-space ("In_Space",
|
||||||
|
// origin name + range) variants, plus a cargo-loaded row to assert the
|
||||||
|
// load-type / load-quantity columns are wired through. A planet
|
||||||
|
// table is mounted upfront so destination/origin name resolution
|
||||||
|
// has something to bind against.
|
||||||
|
func TestParseYourGroups(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 Capital 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",
|
||||||
|
"",
|
||||||
|
"Your Groups",
|
||||||
|
"",
|
||||||
|
" G # T D W S C T Q D F R P M L",
|
||||||
|
" 0 2 Frontier 5.05 0.00 0.00 1.0 - 0 Castle - - 92.84 12.37 - In_Orbit",
|
||||||
|
" 1 1 Bow105 11.19 4.76 7.09 1.0 COL 1 Castle - - 111.9 149.54 - In_Orbit",
|
||||||
|
" 2 1 Tormoz 11.19 0.00 0.00 1.0 CAP 4 North Castle 7.5 60.66 49.50 - In_Space",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.LocalGroup), 3; got != want {
|
||||||
|
t.Fatalf("len(LocalGroup) = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
first := rep.LocalGroup[0]
|
||||||
|
if first.Number != 2 || first.Class != "Frontier" {
|
||||||
|
t.Errorf("first group = (%d, %q), want (2, Frontier)", first.Number, first.Class)
|
||||||
|
}
|
||||||
|
if got, want := float64(first.Tech["drive"]), 5.05; got != want {
|
||||||
|
t.Errorf("first.Tech[drive] = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
if first.Destination != 17 {
|
||||||
|
t.Errorf("first.Destination = %d, want 17 (Castle resolved)", first.Destination)
|
||||||
|
}
|
||||||
|
if first.Origin != nil || first.Range != nil {
|
||||||
|
t.Errorf("first.{Origin,Range} = (%v, %v), want both nil for In_Orbit", first.Origin, first.Range)
|
||||||
|
}
|
||||||
|
if first.State != "In_Orbit" {
|
||||||
|
t.Errorf("first.State = %q, want In_Orbit", first.State)
|
||||||
|
}
|
||||||
|
if first.Fleet != nil {
|
||||||
|
t.Errorf("first.Fleet = %v, want nil for `-`", first.Fleet)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded := rep.LocalGroup[1]
|
||||||
|
if loaded.Cargo != "COL" || float64(loaded.Load) != 1.0 {
|
||||||
|
t.Errorf("loaded cargo/load = (%q, %v), want (COL, 1.0)", loaded.Cargo, float64(loaded.Load))
|
||||||
|
}
|
||||||
|
|
||||||
|
flying := rep.LocalGroup[2]
|
||||||
|
if flying.State != "In_Space" {
|
||||||
|
t.Errorf("flying.State = %q, want In_Space", flying.State)
|
||||||
|
}
|
||||||
|
if flying.Origin == nil || *flying.Origin != 17 {
|
||||||
|
t.Errorf("flying.Origin = %v, want 17 (Castle)", flying.Origin)
|
||||||
|
}
|
||||||
|
if flying.Range == nil || float64(*flying.Range) != 7.5 {
|
||||||
|
t.Errorf("flying.Range = %v, want 7.5", flying.Range)
|
||||||
|
}
|
||||||
|
if flying.Destination != 87 {
|
||||||
|
t.Errorf("flying.Destination = %d, want 87 (North)", flying.Destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseYourFleets(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 Capital 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",
|
||||||
|
"",
|
||||||
|
"Your Fleets",
|
||||||
|
"",
|
||||||
|
" # N G D F R P",
|
||||||
|
" 0 Fast 3 Castle - - 45 In_Orbit",
|
||||||
|
" 1 Far 2 North Castle 4.50 20 In_Space",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.LocalFleet), 2; got != want {
|
||||||
|
t.Fatalf("len(LocalFleet) = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
fast := rep.LocalFleet[0]
|
||||||
|
if fast.Name != "Fast" || fast.Groups != 3 || fast.Destination != 17 {
|
||||||
|
t.Errorf("fast = %+v, want Name=Fast Groups=3 Destination=17", fast)
|
||||||
|
}
|
||||||
|
if fast.State != "In_Orbit" {
|
||||||
|
t.Errorf("fast.State = %q, want In_Orbit", fast.State)
|
||||||
|
}
|
||||||
|
if fast.Origin != nil || fast.Range != nil {
|
||||||
|
t.Errorf("fast.{Origin,Range} = (%v, %v), want both nil", fast.Origin, fast.Range)
|
||||||
|
}
|
||||||
|
far := rep.LocalFleet[1]
|
||||||
|
if far.Origin == nil || *far.Origin != 17 {
|
||||||
|
t.Errorf("far.Origin = %v, want 17 (Castle)", far.Origin)
|
||||||
|
}
|
||||||
|
if far.Range == nil || float64(*far.Range) != 4.5 {
|
||||||
|
t.Errorf("far.Range = %v, want 4.5", far.Range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseIncomingGroups(t *testing.T) {
|
||||||
|
// Origin is a `#NN` by-id reference; destination resolves
|
||||||
|
// against the local planet table that was parsed earlier in
|
||||||
|
// this synthetic file. The order is intentionally swapped — in
|
||||||
|
// real legacy reports "Incoming Groups" can land before
|
||||||
|
// "Your Planets", which is why the parser buffers rows for
|
||||||
|
// post-processing.
|
||||||
|
in := strings.Join([]string{
|
||||||
|
"Race Report for Galaxy PLUS Turn 1",
|
||||||
|
"",
|
||||||
|
"Incoming Groups",
|
||||||
|
"",
|
||||||
|
"O D R S M",
|
||||||
|
"#98 Castle 136.16 190 1",
|
||||||
|
"North Castle 42.12 99 2",
|
||||||
|
"",
|
||||||
|
"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",
|
||||||
|
" 87 169.59 694.49 North 500.00 500.00 500.00 10.00 Capital 0.00 0.52 35.76 500.00",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
rep, err := Parse(strings.NewReader(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := len(rep.IncomingGroup), 2; got != want {
|
||||||
|
t.Fatalf("len(IncomingGroup) = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
a := rep.IncomingGroup[0]
|
||||||
|
if a.Origin != 98 || a.Destination != 17 {
|
||||||
|
t.Errorf("a (Origin, Destination) = (%d, %d), want (98, 17)", a.Origin, a.Destination)
|
||||||
|
}
|
||||||
|
if float64(a.Distance) != 136.16 || float64(a.Speed) != 190 {
|
||||||
|
t.Errorf("a (Distance, Speed) = (%v, %v), want (136.16, 190)", float64(a.Distance), float64(a.Speed))
|
||||||
|
}
|
||||||
|
b := rep.IncomingGroup[1]
|
||||||
|
if b.Origin != 87 || b.Destination != 17 {
|
||||||
|
t.Errorf("b (Origin, Destination) = (%d, %d), want (87, 17)", b.Origin, b.Destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- smoke tests -----------------------------------------------------
|
||||||
|
|
||||||
|
type smokeWant struct {
|
||||||
|
race string
|
||||||
|
turn uint
|
||||||
|
mapW, mapH, planetCount uint32
|
||||||
|
voteFor string
|
||||||
|
votes float64
|
||||||
|
players, extinct, local, other int
|
||||||
|
uninhabited, unidentified, shipClasses int
|
||||||
|
localGroups, localFleets, incomingGroups int
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSmoke(t *testing.T, path string, want smokeWant) {
|
||||||
|
t.Helper()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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 want.voteFor != "" {
|
||||||
|
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++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checks := []struct {
|
||||||
|
name string
|
||||||
|
got int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"Player", len(rep.Player), want.players},
|
||||||
|
{"extinct", extinct, want.extinct},
|
||||||
|
{"LocalPlanet", len(rep.LocalPlanet), want.local},
|
||||||
|
{"OtherPlanet", len(rep.OtherPlanet), want.other},
|
||||||
|
{"UninhabitedPlanet", len(rep.UninhabitedPlanet), want.uninhabited},
|
||||||
|
{"UnidentifiedPlanet", len(rep.UnidentifiedPlanet), want.unidentified},
|
||||||
|
{"LocalShipClass", len(rep.LocalShipClass), want.shipClasses},
|
||||||
|
{"LocalGroup", len(rep.LocalGroup), want.localGroups},
|
||||||
|
{"LocalFleet", len(rep.LocalFleet), want.localFleets},
|
||||||
|
{"IncomingGroup", len(rep.IncomingGroup), want.incomingGroups},
|
||||||
|
}
|
||||||
|
for _, c := range checks {
|
||||||
|
if c.got != c.want {
|
||||||
|
t.Errorf("%s = %d, want %d", c.name, c.got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestParseDgKNNTS039 is a smoke test: the parser must produce
|
// TestParseDgKNNTS039 is a smoke test: the parser must produce
|
||||||
// stable top-line counts from the real dg/KNNTS039.REP fixture.
|
// stable top-line counts from the real dg/KNNTS039.REP fixture.
|
||||||
// Field-level fidelity is asserted in the unit tests above; this
|
// Field-level fidelity is asserted in the unit tests above; this
|
||||||
// test catches regressions where a section-classifier change
|
// test catches regressions where a section-classifier change
|
||||||
// silently drops half the data.
|
// silently drops half the data.
|
||||||
func TestParseDgKNNTS039(t *testing.T) {
|
func TestParseDgKNNTS039(t *testing.T) {
|
||||||
const path = "../reports/dg/KNNTS039.REP"
|
runSmoke(t, "../reports/dg/KNNTS039.REP", smokeWant{
|
||||||
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,
|
race: "KnightErrants", turn: 39,
|
||||||
mapW: 800, mapH: 800, planetCount: 700,
|
mapW: 800, mapH: 800, planetCount: 700,
|
||||||
voteFor: "KnightErrants", votes: 16.02,
|
voteFor: "KnightErrants", votes: 16.02,
|
||||||
players: 91, extinct: 49,
|
players: 91, extinct: 49,
|
||||||
local: 22, other: 89, uninhabited: 17, unidentified: 572,
|
local: 22, other: 89, uninhabited: 17, unidentified: 572,
|
||||||
shipClasses: 24,
|
shipClasses: 24,
|
||||||
}
|
localGroups: 171,
|
||||||
if rep.Race != want.race || rep.Turn != want.turn {
|
localFleets: 0,
|
||||||
t.Errorf("(race, turn) = (%q, %d), want (%q, %d)", rep.Race, rep.Turn, want.race, want.turn)
|
incomingGroups: 0,
|
||||||
}
|
})
|
||||||
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
|
func TestParseDgKNNTS040(t *testing.T) {
|
||||||
// fixture so the variant difference (tabs vs spaces in headers) is
|
runSmoke(t, "../reports/dg/KNNTS040.REP", smokeWant{
|
||||||
// exercised on a real file.
|
race: "KnightErrants", turn: 40,
|
||||||
|
mapW: 800, mapH: 800, planetCount: 700,
|
||||||
|
players: 91, extinct: 49,
|
||||||
|
local: 22, other: 93, uninhabited: 27, unidentified: 558,
|
||||||
|
shipClasses: 34,
|
||||||
|
localGroups: 207,
|
||||||
|
localFleets: 0,
|
||||||
|
incomingGroups: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseDgKNNTS041 covers a turn with active "Incoming Groups"
|
||||||
|
// entries (12 rows) appearing before the "Your Planets" table —
|
||||||
|
// exercises the deferred name-resolution path in [parser.finish].
|
||||||
|
func TestParseDgKNNTS041(t *testing.T) {
|
||||||
|
runSmoke(t, "../reports/dg/KNNTS041.REP", smokeWant{
|
||||||
|
race: "KnightErrants", turn: 41,
|
||||||
|
mapW: 800, mapH: 800, planetCount: 700,
|
||||||
|
players: 91, extinct: 50,
|
||||||
|
local: 29, other: 103, uninhabited: 23, unidentified: 545,
|
||||||
|
shipClasses: 36,
|
||||||
|
localGroups: 285,
|
||||||
|
localFleets: 0,
|
||||||
|
incomingGroups: 12,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseGplus40 exercises the gplus engine variant (tabs in
|
||||||
|
// section headers, pseudo-cyrillic ASCII names) and a single fleet.
|
||||||
|
// gplus also sneaks "Incoming Groups" between sections.
|
||||||
func TestParseGplus40(t *testing.T) {
|
func TestParseGplus40(t *testing.T) {
|
||||||
const path = "../reports/gplus/40.REP"
|
runSmoke(t, "../reports/gplus/40.REP", smokeWant{
|
||||||
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,
|
race: "MbI", turn: 40,
|
||||||
mapW: 350, mapH: 350, planetCount: 300,
|
mapW: 350, mapH: 350, planetCount: 300,
|
||||||
players: 26, extinct: 0,
|
players: 26, extinct: 0,
|
||||||
local: 26, other: 116, uninhabited: 7, unidentified: 152,
|
local: 26, other: 116, uninhabited: 7, unidentified: 151,
|
||||||
shipClasses: 56,
|
shipClasses: 56,
|
||||||
|
localGroups: 255,
|
||||||
|
localFleets: 1,
|
||||||
|
incomingGroups: 10,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
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)
|
// TestParseDgKiller031 exercises the Killer engine variant which
|
||||||
}
|
// ships "Your Fleets" + "Your Groups" with the Fl1/F2 fleet
|
||||||
if rep.Width != want.mapW || rep.Height != want.mapH || rep.PlanetCount != want.planetCount {
|
// membership shape (no "Incoming Groups" this turn).
|
||||||
t.Errorf("(W, H, planets) = (%d, %d, %d), want (%d, %d, %d)",
|
func TestParseDgKiller031(t *testing.T) {
|
||||||
rep.Width, rep.Height, rep.PlanetCount, want.mapW, want.mapH, want.planetCount)
|
runSmoke(t, "../reports/dg/Killer031.rep", smokeWant{
|
||||||
}
|
race: "Killer", turn: 31,
|
||||||
extinct := 0
|
mapW: 250, mapH: 250, planetCount: 175,
|
||||||
for _, pl := range rep.Player {
|
players: 25, extinct: 12,
|
||||||
if pl.Extinct {
|
local: 18, other: 127, uninhabited: 20, unidentified: 10,
|
||||||
extinct++
|
shipClasses: 11,
|
||||||
}
|
localGroups: 175,
|
||||||
}
|
localFleets: 2,
|
||||||
if got, exp := len(rep.Player), want.players; got != exp {
|
incomingGroups: 0,
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseDgTancordia037 is the richest smoke fixture: it carries
|
||||||
|
// 311 local groups across 30 fleets, two incoming groups, and the
|
||||||
|
// "Incoming Groups" section appears before "Your Planets" (so the
|
||||||
|
// deferred name resolution is exercised in production conditions).
|
||||||
|
func TestParseDgTancordia037(t *testing.T) {
|
||||||
|
runSmoke(t, "../reports/dg/Tancordia037.rep", smokeWant{
|
||||||
|
race: "Tancordia", turn: 37,
|
||||||
|
mapW: 210, mapH: 210, planetCount: 140,
|
||||||
|
players: 18, extinct: 7,
|
||||||
|
local: 23, other: 62, uninhabited: 26, unidentified: 29,
|
||||||
|
shipClasses: 40,
|
||||||
|
localGroups: 311,
|
||||||
|
localFleets: 30,
|
||||||
|
incomingGroups: 2,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFile(t *testing.T, rel string) (report.Report, error) {
|
func parseFile(t *testing.T, rel string) (report.Report, error) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Executable
+5904
File diff suppressed because it is too large
Load Diff
Executable
+3866
File diff suppressed because it is too large
Load Diff
Executable
+4061
File diff suppressed because it is too large
Load Diff
Executable
+1936
File diff suppressed because it is too large
Load Diff
Executable
+2717
File diff suppressed because it is too large
Load Diff
Executable
+6324
File diff suppressed because it is too large
Load Diff
Executable
+4882
File diff suppressed because it is too large
Load Diff
Executable
+7341
File diff suppressed because it is too large
Load Diff
Executable
+4724
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user