Files
UnrealPrototyping/Tools/Terrain/internal/overlay/generate_test.go
T
2026-09-25 17:02:24 +03:00

331 lines
11 KiB
Go

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)
}
}
}