package overlay import ( "math" "testing" ) // A small world with a sea on the left, a flat coastal plain, and a steep ridge on the right, so every // generated kind has somewhere it should go and somewhere it should not. func testWorld(w, h int) GenInputs { height := make([]float32, w*h) sea := make([]bool, w*h) flow := make([]float32, w*h) for y := 0; y < h; y++ { for x := 0; x < w; x++ { i := y*w + x switch { case x < w/5: sea[i] = true height[i] = -50 case x < 3*w/5: height[i] = float32(x-w/5) * 0.2 // a gentle plain default: height[i] = float32(w/5)*0.2 + float32(x-3*w/5)*12 // a wall } // One river down the middle row of the plain. if y == h/2 && !sea[i] { flow[i] = 5e7 } } } return GenInputs{W: w, H: h, CellM: 100, HeightM: height, Sea: sea, FlowM2: flow, Seed: 11} } func genLegend(specs map[string]*GenSpec) *Legend { l := &Legend{ MatchDistance: DefaultMatchDistance, MinAreaPx: DefaultMinAreaPx, Marks: []Mark{ {Name: "forest", RGB: [3]int{0, 128, 0}}, {Name: "town", RGB: [3]int{220, 30, 30}}, {Name: "road", RGB: [3]int{90, 60, 30}, Kind: KindPath, WidthM: 8}, {Name: "wild_coast", RGB: [3]int{255, 128, 0}}, {Name: "hand", RGB: [3]int{10, 10, 200}}, }, } for i := range l.Marks { if g, ok := specs[l.Marks[i].Name]; ok { l.Marks[i].Generate = g } } if err := l.resolve(); err != nil { panic(err) } return l } // The property the whole feature rests on: generation never touches a pixel somebody painted. Without it, // re-running the generator would quietly destroy an author's work, and the round trip would be unusable. func TestGenerationNeverOverwritesPaintedPixels(t *testing.T) { in := testWorld(200, 120) l := genLegend(map[string]*GenSpec{ "forest": {Kind: GenForest, Cover: 0.9}, "town": {Kind: GenSettlement, Count: 6, MinSpacingKm: 2}, "road": {Kind: GenRoad}, "wild_coast": {Kind: GenCoast, CoastKm: 3}, }) // A hand-painted stripe right across the plain, where the generator badly wants to put things. handIdx := uint8(l.Index("hand")) existing := &Raster{W: in.W, H: in.H, Mark: make([]uint8, in.W*in.H)} handAt := map[int]bool{} for y := 0; y < in.H; y++ { for x := in.W / 5; x < in.W/2; x += 3 { i := y*in.W + x existing.Mark[i] = handIdx handAt[i] = true } } in.Existing = existing out, rep, err := l.Generate(in) if err != nil { t.Fatal(err) } for i := range handAt { if out.Mark[i] != handIdx { t.Fatalf("cell %d was painted %d by hand and the generator changed it to %d", i, handIdx, out.Mark[i]) } } if rep.Kept != len(handAt) { t.Errorf("kept %d painted pixels, want %d", rep.Kept, len(handAt)) } if rep.Painted == 0 { t.Error("the generator filled nothing at all; the test world should have room for every kind") } } // A mark with no generate block is only ever painted by hand. This is what makes the feature opt-in and what // keeps every legend written before it producing exactly the blank sheet it always did. func TestMarksWithoutAGenerateBlockAreNeverGenerated(t *testing.T) { in := testWorld(160, 100) l := genLegend(map[string]*GenSpec{"forest": {Kind: GenForest, Cover: 0.8}}) out, _, err := l.Generate(in) if err != nil { t.Fatal(err) } forest := uint8(l.Index("forest")) for i, m := range out.Mark { if m != Blank && m != forest { t.Fatalf("cell %d got mark %d, but only %q asked to be generated", i, m, "forest") } } } // Nothing is ever put in the sea. A forest, a town or a road on open water is the one output that is simply // wrong rather than merely a matter of taste. func TestNothingIsGeneratedAtSea(t *testing.T) { in := testWorld(200, 120) l := genLegend(map[string]*GenSpec{ "forest": {Kind: GenForest, Cover: 1}, "town": {Kind: GenSettlement, Count: 8, MinSpacingKm: 2}, "road": {Kind: GenRoad}, "wild_coast": {Kind: GenCoast, CoastKm: 4}, }) out, _, err := l.Generate(in) if err != nil { t.Fatal(err) } for i, m := range out.Mark { if m != Blank && in.Sea[i] { t.Fatalf("cell %d is sea and was marked %d", i, m) } } } // Settlements keep their spacing, across tiers as well as within one. A village inside a city is two marks // for one place. func TestSettlementsKeepTheirSpacing(t *testing.T) { in := testWorld(300, 160) l := &Legend{MatchDistance: DefaultMatchDistance, MinAreaPx: DefaultMinAreaPx, Marks: []Mark{ {Name: "city", RGB: [3]int{220, 30, 30}, Generate: &GenSpec{Kind: GenSettlement, Count: 3, MinSpacingKm: 5}}, {Name: "village", RGB: [3]int{150, 90, 200}, Generate: &GenSpec{Kind: GenSettlement, Count: 12}}, }} if err := l.resolve(); err != nil { t.Fatal(err) } _, rep, err := l.Generate(in) if err != nil { t.Fatal(err) } if len(rep.Settlement) < 2 { t.Fatalf("only %d settlements placed; the test world should hold more", len(rep.Settlement)) } minPx := 5 * 1000 / in.CellM for a := range rep.Settlement { for b := a + 1; b < len(rep.Settlement); b++ { p, q := rep.Settlement[a], rep.Settlement[b] dx := float64(wrapDelta(p.X-q.X, in.W)) dy := float64(p.Y - q.Y) if d := math.Hypot(dx, dy); d < minPx-1e-9 { t.Fatalf("settlements %d and %d are %.1f px apart, closer than the %.1f px spacing", a, b, d, minPx) } } } } // Roads connect only what is on the same landmass. Water is impassable, so a two-island world gets no road // between the islands however close they are. func TestRoadsNeverCrossWater(t *testing.T) { w, h := 240, 120 height := make([]float32, w*h) sea := make([]bool, w*h) for y := 0; y < h; y++ { for x := 0; x < w; x++ { i := y*w + x // Two flat islands with a channel between them. island := (x > 20 && x < 100) || (x > 140 && x < 220) if !island { sea[i] = true height[i] = -30 } else { height[i] = 10 } } } in := GenInputs{W: w, H: h, CellM: 100, HeightM: height, Sea: sea, Seed: 3} l := &Legend{MatchDistance: DefaultMatchDistance, MinAreaPx: DefaultMinAreaPx, Marks: []Mark{ {Name: "town", RGB: [3]int{220, 30, 30}, Generate: &GenSpec{Kind: GenSettlement, Count: 8, MinSpacingKm: 3}}, {Name: "road", RGB: [3]int{90, 60, 30}, Kind: KindPath, WidthM: 8, Generate: &GenSpec{Kind: GenRoad}}, }} if err := l.resolve(); err != nil { t.Fatal(err) } out, rep, err := l.Generate(in) if err != nil { t.Fatal(err) } // Towns landed on both islands, so a network that ignored water would have had a reason to cross. regions := map[int]bool{} for _, p := range rep.Settlement { regions[p.Region] = true } if len(regions) < 2 { t.Fatalf("settlements only landed on %d landmass(es); the test cannot show anything", len(regions)) } road := uint8(l.Index("road")) for i, m := range out.Mark { if m == road && sea[i] { t.Fatalf("a road was painted at sea, cell %d", i) } } } // The same world and seed generate the same sheet. Determinism is cross-cutting rule 12 and it is what makes // a regenerated overlay reviewable in a diff. func TestGenerationIsDeterministic(t *testing.T) { l := genLegend(map[string]*GenSpec{ "forest": {Kind: GenForest, Cover: 0.5}, "town": {Kind: GenSettlement, Count: 5, MinSpacingKm: 2}, "road": {Kind: GenRoad}, "wild_coast": {Kind: GenCoast, CoastKm: 2}, }) a, repA, err := l.Generate(testWorld(200, 120)) if err != nil { t.Fatal(err) } b, repB, err := l.Generate(testWorld(200, 120)) if err != nil { t.Fatal(err) } for i := range a.Mark { if a.Mark[i] != b.Mark[i] { t.Fatalf("two runs disagree at cell %d: %d against %d", i, a.Mark[i], b.Mark[i]) } } if repA.Painted != repB.Painted || len(repA.Settlement) != len(repB.Settlement) { t.Errorf("reports differ: %d/%d painted, %d/%d settlements", repA.Painted, repB.Painted, len(repA.Settlement), len(repB.Settlement)) } } // A generated sheet has to survive the round trip: encoded to RGBA and classified back, it must be the same // raster. If it did not, what the studio opened would not be what the generator wrote. func TestGeneratedSheetSurvivesClassifyingItBack(t *testing.T) { in := testWorld(200, 120) l := genLegend(map[string]*GenSpec{ "forest": {Kind: GenForest, Cover: 0.5}, "town": {Kind: GenSettlement, Count: 5, MinSpacingKm: 2}, "road": {Kind: GenRoad}, "wild_coast": {Kind: GenCoast, CoastKm: 2}, }) out, _, err := l.Generate(in) if err != nil { t.Fatal(err) } px, alpha := l.Encode(out) back, match := l.Classify(px, alpha, out.W, out.H) if match.Far != 0 { t.Errorf("%d pixels of a sheet this legend wrote matched no mark", match.Far) } for i := range out.Mark { if out.Mark[i] != back.Mark[i] { t.Fatalf("round trip changed cell %d from %d to %d", i, out.Mark[i], back.Mark[i]) } } } func TestGenerateRejectsAnUnknownKind(t *testing.T) { l := genLegend(map[string]*GenSpec{"forest": {Kind: "woods"}}) if _, _, err := l.Generate(testWorld(80, 40)); err == nil { t.Fatal("a kind the generator does not know should be an error, not a silent no-op") } } func TestGenerateRejectsAMismatchedExistingSheet(t *testing.T) { in := testWorld(80, 40) in.Existing = &Raster{W: 40, H: 20, Mark: make([]uint8, 800)} l := genLegend(map[string]*GenSpec{"forest": {Kind: GenForest}}) if _, _, err := l.Generate(in); err == nil { t.Fatal("an existing sheet of the wrong size should be an error; it is registered to the template") } } // A re-roll must actually re-roll. The studio's Generate button hands a fresh seed every press, and if the // placement does not move, the button does nothing an author can see: the forest count is a quantile and so // is invariant by construction, which makes the settlements the only visible difference between two drafts. func TestASecondSeedMovesTheSettlements(t *testing.T) { l := &Legend{MatchDistance: DefaultMatchDistance, MinAreaPx: DefaultMinAreaPx, Marks: []Mark{ {Name: "town", RGB: [3]int{220, 30, 30}, Generate: &GenSpec{Kind: GenSettlement, Count: 8, MinSpacingKm: 3}}, }} if err := l.resolve(); err != nil { t.Fatal(err) } place := func(seed int64) []Placed { in := testWorld(300, 160) in.Seed = seed _, rep, err := l.Generate(in) if err != nil { t.Fatal(err) } return rep.Settlement } a, b := place(11), place(20260920) if len(a) == 0 || len(b) == 0 { t.Fatalf("no settlements placed (%d, %d); the test world should hold some", len(a), len(b)) } same := 0 for i := range a { if i < len(b) && a[i].X == b[i].X && a[i].Y == b[i].Y { same++ } } if same == len(a) && len(a) == len(b) { t.Fatalf("both seeds placed the same %d settlements in the same places; the seed is not reaching "+ "the placement", len(a)) } // And the same seed twice is still the same world, or nothing is reproducible. c := place(11) for i := range a { if a[i].X != c[i].X || a[i].Y != c[i].Y { t.Fatalf("the same seed placed settlement %d differently on two runs", i) } } }