bd11cd80da
Legacy reports list the same `(race, className)` pair across several roster rows; the engine likewise creates one ShipGroup per arrival. Both the legacy parser and `TransformBattle` were keyed on shipClass without summing — only the last row / group's counts survived, so a protocol's destroy count appeared to exceed the recorded initial roster. The UI worked around this with phantom-frame logic. Both parser and engine now SUM `Number`/`NumberLeft` across rows / groups sharing the same class; the phantom-frame workaround is gone. KNNTS041 turn 41 planet #7 reconciles: `Nails:pup` 1168 initial − 86 survivors = 1082 destroys. The engine's previously latent nil-map write on `bg.Tech` (would have paniced on any group with non-empty Tech) is fixed in the same patch — it blocked the aggregation regression test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1030 lines
35 KiB
Go
1030 lines
35 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", 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)
|
|
}
|
|
if got, want := len(rep.OtherShipClass), 1; got != want {
|
|
t.Fatalf("len(OtherShipClass) = %d, want %d", got, want)
|
|
}
|
|
dragon := rep.OtherShipClass[0]
|
|
if dragon.Race != "Monstrai" || dragon.Name != "Dragon" || dragon.Armament != 1 {
|
|
t.Errorf("Dragon = (%q, %q, %d), want (Monstrai, Dragon, 1)",
|
|
dragon.Race, dragon.Name, dragon.Armament)
|
|
}
|
|
if got, want := float64(dragon.Mass), 19.80; got != want {
|
|
t.Errorf("Dragon.Mass = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
// TestParseSciences covers both "Your Sciences" and "<Race> Sciences"
|
|
// in one fixture. The five-column layout (N D W S C) is shared.
|
|
func TestParseSciences(t *testing.T) {
|
|
in := strings.Join([]string{
|
|
"Race Report for Galaxy PLUS Turn 1",
|
|
"",
|
|
"Your Sciences",
|
|
"",
|
|
"N D W S C",
|
|
"_TerraForm 1 0 0 0",
|
|
"BalancedMix 0.5 0.2 0.2 0.1",
|
|
"",
|
|
"Pahanchiks Sciences",
|
|
"",
|
|
"N D W S C",
|
|
"_Drift 1 0 0 0",
|
|
"",
|
|
}, "\n")
|
|
rep, _, err := Parse(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if got, want := len(rep.LocalScience), 2; got != want {
|
|
t.Fatalf("len(LocalScience) = %d, want %d", got, want)
|
|
}
|
|
mix := rep.LocalScience[1]
|
|
if mix.Name != "BalancedMix" {
|
|
t.Errorf("LocalScience[1].Name = %q, want BalancedMix", mix.Name)
|
|
}
|
|
if float64(mix.Drive) != 0.5 || float64(mix.Cargo) != 0.1 {
|
|
t.Errorf("BalancedMix (Drive, Cargo) = (%v, %v), want (0.5, 0.1)",
|
|
float64(mix.Drive), float64(mix.Cargo))
|
|
}
|
|
if got, want := len(rep.OtherScience), 1; got != want {
|
|
t.Fatalf("len(OtherScience) = %d, want %d", got, want)
|
|
}
|
|
drift := rep.OtherScience[0]
|
|
if drift.Race != "Pahanchiks" || drift.Name != "_Drift" {
|
|
t.Errorf("OtherScience[0] = (%q, %q), want (Pahanchiks, _Drift)",
|
|
drift.Race, drift.Name)
|
|
}
|
|
}
|
|
|
|
// TestParseBombings covers a wiped row + a damaged row + the duplicate
|
|
// `P` column header (population vs production string) — assertions
|
|
// hit every wire field so a positional-index slip is caught.
|
|
func TestParseBombings(t *testing.T) {
|
|
in := strings.Join([]string{
|
|
"Race Report for Galaxy PLUS Turn 1",
|
|
"",
|
|
"Bombings",
|
|
"",
|
|
"W O # N P I P $ M C A",
|
|
"Knights Ricksha 20 DW-1207 1.56 0.00 Dron 0.00 0.00 0.00 7.62 Wiped",
|
|
"Knights Ricksha 332 PEHKE 500.00 258.64 Dron 184.39 0.00 6.42 331.93 Damaged",
|
|
"",
|
|
}, "\n")
|
|
rep, _, err := Parse(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if got, want := len(rep.Bombing), 2; got != want {
|
|
t.Fatalf("len(Bombing) = %d, want %d", got, want)
|
|
}
|
|
wiped := rep.Bombing[0]
|
|
if !wiped.Wiped {
|
|
t.Errorf("Bombing[0].Wiped = false, want true")
|
|
}
|
|
if wiped.Attacker != "Knights" || wiped.Owner != "Ricksha" || wiped.Number != 20 {
|
|
t.Errorf("Bombing[0] head = (%q, %q, %d), want (Knights, Ricksha, 20)",
|
|
wiped.Attacker, wiped.Owner, wiped.Number)
|
|
}
|
|
if wiped.Planet != "DW-1207" || wiped.Production != "Dron" {
|
|
t.Errorf("Bombing[0] (planet, production) = (%q, %q), want (DW-1207, Dron)",
|
|
wiped.Planet, wiped.Production)
|
|
}
|
|
if float64(wiped.AttackPower) != 7.62 {
|
|
t.Errorf("Bombing[0].AttackPower = %v, want 7.62", float64(wiped.AttackPower))
|
|
}
|
|
damaged := rep.Bombing[1]
|
|
if damaged.Wiped {
|
|
t.Errorf("Bombing[1].Wiped = true, want false (Damaged)")
|
|
}
|
|
if float64(damaged.Capital) != 184.39 || float64(damaged.Colonists) != 6.42 {
|
|
t.Errorf("Bombing[1] (capital, colonists) = (%v, %v), want (184.39, 6.42)",
|
|
float64(damaged.Capital), float64(damaged.Colonists))
|
|
}
|
|
}
|
|
|
|
// TestParseShipsInProduction covers the prod_used derivation through
|
|
// [calc.ShipBuildCost]. The producing planet is mounted first with a
|
|
// non-zero material stockpile so the farming term contributes a
|
|
// non-trivial slice of totalCost; the expected prod_used number is
|
|
// derived from totalCost * percent with the same formula the parser
|
|
// uses.
|
|
func TestParseShipsInProduction(t *testing.T) {
|
|
// Planet: Material=0.68, Resources=10.00.
|
|
// Ship: cost=990.10 -> shipMass=99.01.
|
|
// totalCost = ShipProductionCost(99.01) + max(0, 99.01-0.68)/10
|
|
// = 990.10 + 9.833
|
|
// = 999.933
|
|
// prod_used = 999.933 * 0.07 (percent) ≈ 69.99531
|
|
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 CombatFlame 0.00 0.68 88.78 1000.00",
|
|
"",
|
|
"Ships In Production",
|
|
"",
|
|
" # N S C P L",
|
|
" 17 Castle CombatFlame 990.10 0.07 1000.00",
|
|
"",
|
|
}, "\n")
|
|
rep, _, err := Parse(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if got, want := len(rep.ShipProduction), 1; got != want {
|
|
t.Fatalf("len(ShipProduction) = %d, want %d", got, want)
|
|
}
|
|
sp := rep.ShipProduction[0]
|
|
if sp.Planet != 17 || sp.Class != "CombatFlame" {
|
|
t.Errorf("ShipProduction[0] = (planet=%d, class=%q), want (17, CombatFlame)",
|
|
sp.Planet, sp.Class)
|
|
}
|
|
if got := float64(sp.Cost); got != 990.10 {
|
|
t.Errorf("ShipProduction[0].Cost = %v, want 990.10", got)
|
|
}
|
|
if got := float64(sp.Percent); got != 0.07 {
|
|
t.Errorf("ShipProduction[0].Percent = %v, want 0.07", got)
|
|
}
|
|
if got := float64(sp.Free); got != 1000.0 {
|
|
t.Errorf("ShipProduction[0].Free = %v, want 1000", got)
|
|
}
|
|
wantProdUsed := 69.995
|
|
if got := float64(sp.ProdUsed); got < wantProdUsed-0.01 || got > wantProdUsed+0.01 {
|
|
t.Errorf("ShipProduction[0].ProdUsed = %v, want ~%v (totalCost * percent)",
|
|
got, wantProdUsed)
|
|
}
|
|
}
|
|
|
|
// TestParseShipsInProductionDropsUnknownPlanet exercises the safety
|
|
// net: a ships-in-production row referencing a planet not seen in
|
|
// "Your Planets" is dropped, because the prod_used derivation needs
|
|
// the planet's material and resources.
|
|
func TestParseShipsInProductionDropsUnknownPlanet(t *testing.T) {
|
|
in := strings.Join([]string{
|
|
"Race Report for Galaxy PLUS Turn 1",
|
|
"",
|
|
"Ships In Production",
|
|
"",
|
|
" # N S C P L",
|
|
" 99 Lost Frigate 100.00 0.05 500.00",
|
|
"",
|
|
}, "\n")
|
|
rep, _, err := Parse(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if got, want := len(rep.ShipProduction), 0; got != want {
|
|
t.Errorf("len(ShipProduction) = %d, want %d (planet #99 missing → drop)",
|
|
got, want)
|
|
}
|
|
}
|
|
|
|
// TestParseBattles exercises the battle-block parser end-to-end:
|
|
// two battles with two races each, full rosters, and protocols. The
|
|
// inline fixture mirrors the KNNTS-style layout (race-named roster
|
|
// sub-headers, 10-column roster rows, 8-token shot lines) so any
|
|
// drift from the real engine format breaks this test before a smoke
|
|
// regression. Asserts:
|
|
// - report.Battle carries one BattleSummary per "Battle at"
|
|
// - BattleReport slice mirrors that with full Races/Ships/Protocol
|
|
// - Battle Protocol "Foo fires on Bar : <Destroyed|Shields>" lines
|
|
// map to BattleActionReport entries with the correct destroyed flag
|
|
// - Roster column 8 (the "L" column) populates NumberLeft
|
|
// - Top-level sections after a battle (Your Planets) still parse
|
|
// — battle state must close cleanly without leaking rows.
|
|
func TestParseBattles(t *testing.T) {
|
|
in := strings.Join([]string{
|
|
"Race Report for Galaxy PLUS Turn 1",
|
|
"",
|
|
"Battle at (#7) B-007",
|
|
"",
|
|
"Foo Groups",
|
|
"",
|
|
"# T D W S C T Q L",
|
|
"1 PeaceShip 4.0 0 0 0 - 0 1 In_Battle",
|
|
"2 Drone 0.0 1 1 0 - 0 0 In_Battle",
|
|
"",
|
|
"Bar Groups",
|
|
"",
|
|
"# T D W S C T Q L",
|
|
"1 Pistolet 1.0 1.0 0 0 - 0 1 In_Battle",
|
|
"",
|
|
"Battle Protocol",
|
|
"",
|
|
"Foo PeaceShip fires on Bar Pistolet : Shields",
|
|
"Bar Pistolet fires on Foo Drone : Destroyed",
|
|
"Bar Pistolet fires on Foo Drone : Destroyed",
|
|
"",
|
|
"Battle at (#11) X-011",
|
|
"",
|
|
"Foo Groups",
|
|
"",
|
|
"# T D W S C T Q L",
|
|
"1 Scout 1.0 0 0 0 - 0 1 In_Battle",
|
|
"",
|
|
"Bar Groups",
|
|
"",
|
|
"# T D W S C T Q L",
|
|
"1 Sniper 2.0 1 0 0 - 0 0 In_Battle",
|
|
"",
|
|
"Battle Protocol",
|
|
"",
|
|
"Foo Scout fires on Bar Sniper : Destroyed",
|
|
"",
|
|
"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, battles, err := Parse(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
// The trailing Your Planets section must still parse — battle
|
|
// state must close before the next top-level header.
|
|
if got, want := len(rep.LocalPlanet), 1; got != want {
|
|
t.Fatalf("len(LocalPlanet) = %d, want %d (battle state did not close)", got, want)
|
|
}
|
|
|
|
if got, want := len(rep.Battle), 2; got != want {
|
|
t.Fatalf("len(rep.Battle) = %d, want %d", got, want)
|
|
}
|
|
if got, want := len(battles), 2; got != want {
|
|
t.Fatalf("len(battles) = %d, want %d", got, want)
|
|
}
|
|
|
|
// First battle: planet 7, 3 shots; protocol shape with one
|
|
// shielded shot and two destroyed shots.
|
|
b0 := battles[0]
|
|
if b0.Planet != 7 || b0.PlanetName != "B-007" {
|
|
t.Errorf("battle[0] = (planet=%d, name=%q), want (7, %q)",
|
|
b0.Planet, b0.PlanetName, "B-007")
|
|
}
|
|
if got, want := len(b0.Protocol), 3; got != want {
|
|
t.Fatalf("battle[0].Protocol = %d shots, want %d", got, want)
|
|
}
|
|
if b0.Protocol[0].Destroyed {
|
|
t.Errorf("battle[0].Protocol[0].Destroyed = true (Shields hit), want false")
|
|
}
|
|
if !b0.Protocol[1].Destroyed || !b0.Protocol[2].Destroyed {
|
|
t.Errorf("battle[0].Protocol[1..2].Destroyed must be true (Destroyed hits)")
|
|
}
|
|
|
|
// First battle: roster size and NumberLeft mapping.
|
|
if got, want := len(b0.Ships), 3; got != want {
|
|
t.Fatalf("battle[0].Ships = %d groups, want %d", got, want)
|
|
}
|
|
// 'Drone' has NumberLeft=0 in the roster (column 8 = 0). The
|
|
// protocol corroborates: Pistolet destroyed Drone twice.
|
|
dronePresent := false
|
|
for _, ship := range b0.Ships {
|
|
if ship.ClassName == "Drone" {
|
|
dronePresent = true
|
|
if ship.NumberLeft != 0 {
|
|
t.Errorf("Drone.NumberLeft = %d, want 0", ship.NumberLeft)
|
|
}
|
|
if ship.Number != 2 {
|
|
t.Errorf("Drone.Number = %d, want 2", ship.Number)
|
|
}
|
|
}
|
|
}
|
|
if !dronePresent {
|
|
t.Errorf("Drone roster row not parsed into battle[0].Ships")
|
|
}
|
|
|
|
// Summary mirrors the BattleReport ID and shot count.
|
|
if rep.Battle[0].ID != b0.ID {
|
|
t.Errorf("rep.Battle[0].ID = %s, want %s", rep.Battle[0].ID, b0.ID)
|
|
}
|
|
if rep.Battle[0].Shots != 3 {
|
|
t.Errorf("rep.Battle[0].Shots = %d, want 3", rep.Battle[0].Shots)
|
|
}
|
|
if rep.Battle[0].Planet != 7 {
|
|
t.Errorf("rep.Battle[0].Planet = %d, want 7", rep.Battle[0].Planet)
|
|
}
|
|
|
|
// Second battle: planet 11, 1 shot.
|
|
if rep.Battle[1].Planet != 11 || rep.Battle[1].Shots != 1 {
|
|
t.Errorf("rep.Battle[1] = (planet=%d, shots=%d), want (11, 1)",
|
|
rep.Battle[1].Planet, rep.Battle[1].Shots)
|
|
}
|
|
|
|
// Battle IDs are stable across re-parses.
|
|
rep2, battles2, err := Parse(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatalf("Parse (second pass): %v", err)
|
|
}
|
|
if rep.Battle[0].ID != rep2.Battle[0].ID || battles[0].ID != battles2[0].ID {
|
|
t.Errorf("battle id must be deterministic across re-parses")
|
|
}
|
|
}
|
|
|
|
// TestParseBattleAggregatesDuplicateClasses guards against the
|
|
// regression that produced the original "phantom destroys" symptom:
|
|
// the same `(race, className)` pair appearing on multiple roster
|
|
// rows must collapse into a single BattleReportGroup whose `Number`
|
|
// (the "#" column, initial ship count) and `NumberLeft` (the "L"
|
|
// column, survivors) are the sums across rows. Without the
|
|
// aggregation only the last row's counts survived and the protocol's
|
|
// destroy count dwarfed the recorded initial count (e.g. KNNTS041
|
|
// turn-41 planet #7 lists `pup` seven separate times: 99 + 105 + 291 +
|
|
// 287 + 166 + 132 + 88 = 1168 ships, 86 survivors, 1082 destroys).
|
|
func TestParseBattleAggregatesDuplicateClasses(t *testing.T) {
|
|
in := strings.Join([]string{
|
|
"Race Report for Galaxy PLUS Turn 1",
|
|
"",
|
|
"Battle at (#7) B-007",
|
|
"",
|
|
"Foo Groups",
|
|
"",
|
|
" # T D W S C T Q L",
|
|
" 3 Drone 1.0 1.0 0 0 - 0 1 In_Battle",
|
|
" 4 Drone 1.2 1.0 0 0 - 0 2 In_Battle",
|
|
"10 Cruiser 3.0 2.0 0 0 - 0 9 In_Battle",
|
|
"",
|
|
"Bar Groups",
|
|
"",
|
|
"# T D W S C T Q L",
|
|
"5 Pistolet 1.0 1.0 0 0 - 0 3 In_Battle",
|
|
"",
|
|
"Battle Protocol",
|
|
"",
|
|
"Bar Pistolet fires on Foo Drone : Destroyed",
|
|
"Bar Pistolet fires on Foo Drone : Destroyed",
|
|
"Bar Pistolet fires on Foo Drone : Destroyed",
|
|
"Bar Pistolet fires on Foo Drone : Destroyed",
|
|
"",
|
|
}, "\n")
|
|
|
|
rep, battles, err := Parse(strings.NewReader(in))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if got, want := len(battles), 1; got != want {
|
|
t.Fatalf("len(battles) = %d, want %d", got, want)
|
|
}
|
|
b := battles[0]
|
|
|
|
// The three Foo roster rows collapse into two BattleReportGroup
|
|
// entries: one Foo:Drone (rows 1+2) and one Foo:Cruiser (row 3),
|
|
// plus one Bar:Pistolet. Total 3 groups, NOT 4.
|
|
if got, want := len(b.Ships), 3; got != want {
|
|
t.Fatalf("battle.Ships = %d groups, want %d (duplicate class rows must merge)", got, want)
|
|
}
|
|
|
|
var drone, cruiser, pistolet *report.BattleReportGroup
|
|
for i := range b.Ships {
|
|
g := b.Ships[i]
|
|
switch g.ClassName {
|
|
case "Drone":
|
|
drone = &g
|
|
case "Cruiser":
|
|
cruiser = &g
|
|
case "Pistolet":
|
|
pistolet = &g
|
|
}
|
|
}
|
|
if drone == nil || cruiser == nil || pistolet == nil {
|
|
t.Fatalf("missing class: drone=%v cruiser=%v pistolet=%v", drone, cruiser, pistolet)
|
|
}
|
|
|
|
// Drone rows sum to Number = 3 + 4 = 7 and NumberLeft = 1 + 2 = 3.
|
|
// Protocol corroborates: four Destroyed shots against Drone, so
|
|
// 7 - 3 = 4 — the protocol's destroy count reconciles with the
|
|
// recorded delta only when both rows are summed.
|
|
if drone.Number != 7 {
|
|
t.Errorf("Drone.Number = %d, want 7 (3+4)", drone.Number)
|
|
}
|
|
if drone.NumberLeft != 3 {
|
|
t.Errorf("Drone.NumberLeft = %d, want 3 (1+2)", drone.NumberLeft)
|
|
}
|
|
// Cruiser and Pistolet are single-row classes — counts must match
|
|
// the file verbatim with no spurious merging across classes.
|
|
if cruiser.Number != 10 || cruiser.NumberLeft != 9 {
|
|
t.Errorf("Cruiser = (Number=%d, NumberLeft=%d), want (10, 9)",
|
|
cruiser.Number, cruiser.NumberLeft)
|
|
}
|
|
if pistolet.Number != 5 || pistolet.NumberLeft != 3 {
|
|
t.Errorf("Pistolet = (Number=%d, NumberLeft=%d), want (5, 3)",
|
|
pistolet.Number, pistolet.NumberLeft)
|
|
}
|
|
|
|
// rep-level summary must reflect the merged shape: 4 shots, one
|
|
// battle, no crash or spurious extra battles.
|
|
if got, want := len(rep.Battle), 1; got != want {
|
|
t.Fatalf("len(rep.Battle) = %d, want %d", got, want)
|
|
}
|
|
if rep.Battle[0].Shots != 4 {
|
|
t.Errorf("rep.Battle[0].Shots = %d, want 4", rep.Battle[0].Shots)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
localScience, otherScience, otherShipClass int
|
|
bombings, shipProductions int
|
|
battles int
|
|
}
|
|
|
|
func runSmoke(t *testing.T, path string, want smokeWant) {
|
|
t.Helper()
|
|
rep, battles, 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},
|
|
{"LocalScience", len(rep.LocalScience), want.localScience},
|
|
{"OtherScience", len(rep.OtherScience), want.otherScience},
|
|
{"OtherShipClass", len(rep.OtherShipClass), want.otherShipClass},
|
|
{"Bombing", len(rep.Bombing), want.bombings},
|
|
{"ShipProduction", len(rep.ShipProduction), want.shipProductions},
|
|
{"Battle (summary)", len(rep.Battle), want.battles},
|
|
{"BattleReport", len(battles), want.battles},
|
|
}
|
|
for _, c := range checks {
|
|
if c.got != c.want {
|
|
t.Errorf("%s = %d, want %d", c.name, c.got, c.want)
|
|
}
|
|
}
|
|
for i, summary := range rep.Battle {
|
|
if i >= len(battles) {
|
|
break
|
|
}
|
|
if summary.ID != battles[i].ID {
|
|
t.Errorf("battle[%d].ID summary=%s vs report=%s",
|
|
i, summary.ID, battles[i].ID)
|
|
}
|
|
if summary.Shots != uint(len(battles[i].Protocol)) {
|
|
t.Errorf("battle[%d].Shots = %d, want %d (len(Protocol))",
|
|
i, summary.Shots, len(battles[i].Protocol))
|
|
}
|
|
if summary.Planet != battles[i].Planet {
|
|
t.Errorf("battle[%d].Planet summary=%d vs report=%d",
|
|
i, summary.Planet, battles[i].Planet)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
runSmoke(t, "../reports/dg/KNNTS039.REP", smokeWant{
|
|
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,
|
|
localGroups: 171,
|
|
localFleets: 0,
|
|
incomingGroups: 0,
|
|
localScience: 1,
|
|
otherScience: 1,
|
|
otherShipClass: 170,
|
|
bombings: 16,
|
|
shipProductions: 6,
|
|
battles: 28,
|
|
})
|
|
}
|
|
|
|
func TestParseDgKNNTS040(t *testing.T) {
|
|
runSmoke(t, "../reports/dg/KNNTS040.REP", smokeWant{
|
|
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,
|
|
localScience: 1,
|
|
otherScience: 1,
|
|
otherShipClass: 160,
|
|
bombings: 24,
|
|
shipProductions: 16,
|
|
battles: 79,
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
localScience: 1,
|
|
otherScience: 1,
|
|
otherShipClass: 218,
|
|
bombings: 12,
|
|
shipProductions: 22,
|
|
battles: 56,
|
|
})
|
|
}
|
|
|
|
// 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) {
|
|
runSmoke(t, "../reports/gplus/40.REP", smokeWant{
|
|
race: "MbI", turn: 40,
|
|
mapW: 350, mapH: 350, planetCount: 300,
|
|
players: 26, extinct: 0,
|
|
local: 26, other: 116, uninhabited: 7, unidentified: 151,
|
|
shipClasses: 56,
|
|
localGroups: 255,
|
|
localFleets: 1,
|
|
incomingGroups: 10,
|
|
localScience: 0,
|
|
otherScience: 0,
|
|
otherShipClass: 183,
|
|
bombings: 4,
|
|
shipProductions: 8,
|
|
battles: 30,
|
|
})
|
|
}
|
|
|
|
// TestParseDgKiller031 exercises the Killer engine variant which
|
|
// ships "Your Fleets" + "Your Groups" with the Fl1/F2 fleet
|
|
// membership shape (no "Incoming Groups" this turn).
|
|
func TestParseDgKiller031(t *testing.T) {
|
|
runSmoke(t, "../reports/dg/Killer031.rep", smokeWant{
|
|
race: "Killer", turn: 31,
|
|
mapW: 250, mapH: 250, planetCount: 175,
|
|
players: 25, extinct: 12,
|
|
local: 18, other: 127, uninhabited: 20, unidentified: 10,
|
|
shipClasses: 11,
|
|
localGroups: 175,
|
|
localFleets: 2,
|
|
incomingGroups: 0,
|
|
localScience: 0,
|
|
otherScience: 0,
|
|
otherShipClass: 161,
|
|
bombings: 18,
|
|
shipProductions: 0,
|
|
battles: 83,
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
localScience: 1,
|
|
otherScience: 1,
|
|
otherShipClass: 123,
|
|
bombings: 22,
|
|
shipProductions: 20,
|
|
battles: 57,
|
|
})
|
|
}
|
|
|
|
func parseFile(t *testing.T, rel string) (report.Report, []report.BattleReport, error) {
|
|
t.Helper()
|
|
abs, err := filepath.Abs(rel)
|
|
if err != nil {
|
|
return report.Report{}, nil, err
|
|
}
|
|
f, err := os.Open(abs)
|
|
if err != nil {
|
|
return report.Report{}, nil, err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
return Parse(f)
|
|
}
|