99962b295f
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>
389 lines
13 KiB
Go
389 lines
13 KiB
Go
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)
|
|
}
|