package region import ( "strings" "testing" "salty/terrain/internal/template" "salty/terrain/internal/world" ) const legendJSON = `{"classes":[ {"name":"sea","rgb":[0,0,255],"sea":true,"depth_m":100}, {"name":"land","rgb":[0,255,0],"uplift_mm_yr":0.5} ]}` // testMap builds a planet from a picture of its painted rows. '#' is land, '.' is sea; the polar pad of // synthetic ocean is added above and below. func testMap(t *testing.T, rows []string, pad int) *template.Map { t.Helper() l, err := template.Parse([]byte(legendJSON)) if err != nil { t.Fatal(err) } w := len(rows[0]) p := world.Planet{CellM: 1, W: w, H: len(rows) + 2*pad, PadY: pad, NoisePeriodM: float64(w)} if err := p.Validate(); err != nil { t.Fatal(err) } m := &template.Map{P: p, L: l, Class: make([]uint8, p.W*p.H), Sea: make([]bool, p.W*p.H)} for i := range m.Class { m.Class[i], m.Sea[i] = 0, true } for y, row := range rows { if len(row) != w { t.Fatalf("row %d is %d wide, want %d", y, len(row), w) } for x, r := range row { if r == '#' { i := (y+pad)*p.W + x m.Class[i], m.Sea[i] = 1, false } } } return m } // assertBordersAreWater is the invariant the whole solve depends on, and the direct analogue of // TestBorderIsAlwaysOcean: a grid edge is an outlet, so land on one would freeze at its initial relief while // the interior eroded out from under it. func assertBordersAreWater(t *testing.T, part *Partition, m *template.Map) { t.Helper() for _, r := range part.Regions { _, land := part.Cut(m, r) for x := 0; x < r.Frame.W; x++ { if land[x] { t.Errorf("region %d: land on the top edge at column %d", r.ID, x) } if land[(r.Frame.H-1)*r.Frame.W+x] { t.Errorf("region %d: land on the bottom edge at column %d", r.ID, x) } } for y := 0; y < r.Frame.H; y++ { if land[y*r.Frame.W] { t.Errorf("region %d: land on the left edge at row %d", r.ID, y) } if land[y*r.Frame.W+r.Frame.W-1] { t.Errorf("region %d: land on the right edge at row %d", r.ID, y) } } } } // The crater island in the template this was written for straddles x = 0. If the partitioner split it in // two, half of it would be solved against a shore that does not exist. func TestSeamStraddlingLandmassIsOneRegion(t *testing.T) { m := testMap(t, []string{ "#.........#", "#.........#", "...........", }, 2) part, err := Build(m, 2, 1) if err != nil { t.Fatal(err) } if len(part.Regions) != 1 { t.Fatalf("got %d regions, want 1: the landmass wraps", len(part.Regions)) } r := part.Regions[0] if !r.Seam { t.Error("the region does not report that it straddles the seam") } if r.LandCells != 4 { t.Errorf("LandCells = %d, want 4", r.LandCells) } // Land occupies columns 10 and 0, which are neighbours on an 11-column cylinder; a 2-cell margin on // each side makes the frame six columns wide starting at column 8. if r.Frame.W != 6 { t.Errorf("frame width = %d, want 6", r.Frame.W) } if r.Frame.X0 != 8 { t.Errorf("frame X0 = %d, want 8", r.Frame.X0) } assertBordersAreWater(t, part, m) } // Landmasses close enough to shelter each other are solved together; further apart they are not. The // alternative that was rejected - overlapping dilated bounding boxes - is transitively closed and one long // landmass has an enormous box, so on a real template it collapses most of the map into a single region. func TestClusteringFollowsDistanceNotBoundingBoxes(t *testing.T) { near := testMap(t, []string{ "..............................", ".####....#....................", ".####....#....................", "..............................", }, 3) part, err := Build(near, 3, 1) if err != nil { t.Fatal(err) } if len(part.Regions) != 1 { t.Fatalf("got %d regions, want 1: a four-cell gap closes under a three-cell margin on each side", len(part.Regions)) } far := testMap(t, []string{ "..............................", ".####.........#...............", ".####.........#...............", "..............................", }, 3) part, err = Build(far, 3, 1) if err != nil { t.Fatal(err) } if len(part.Regions) != 2 { t.Fatalf("got %d regions, want 2: a nine-cell gap does not", len(part.Regions)) } assertBordersAreWater(t, part, far) } func TestEveryRegionBorderIsWater(t *testing.T) { m := testMap(t, []string{ "..###...........................................................", "..###.........####.............####.............##..............", "..............####.............####.............##..............", "..............####.............####.............................", "................................................................", "................................................................", }, 3) part, err := Build(m, 3, 1) if err != nil { t.Fatal(err) } if len(part.Regions) < 2 { t.Fatalf("got %d regions; the picture has several separate landmasses", len(part.Regions)) } assertBordersAreWater(t, part, m) } // A polar cap touches the top row of the painted map. The synthetic ocean pad is what gives it a shore, so // that fluvial.isOutlet - which treats every top-row cell as an outlet - is answering about water. func TestAPolarCapGetsAMarginOfOcean(t *testing.T) { m := testMap(t, []string{ "###############...............", "########......................", "..............................", "..............................", }, 3) part, err := Build(m, 3, 1) if err != nil { t.Fatal(err) } if len(part.Regions) != 1 { t.Fatalf("got %d regions, want 1", len(part.Regions)) } r := part.Regions[0] if r.Frame.Y0 != 0 { t.Errorf("frame Y0 = %d, want 0: the cap reaches into the pad", r.Frame.Y0) } assertBordersAreWater(t, part, m) } // Every painted land cell belongs to exactly one region, and a round trip through Cut and Composite // reproduces it. If two regions owned the same cell, one would silently overwrite the other. func TestCutAndCompositeCoverEveryLandCellOnce(t *testing.T) { m := testMap(t, []string{ "#..####...................#", "#..####...................#", "...........................", "..........##.....##........", "..........##.....##........", "...........................", }, 3) part, err := Build(m, 2, 1) if err != nil { t.Fatal(err) } if len(part.Regions) < 2 { t.Fatalf("got %d regions; the picture has several separate landmasses", len(part.Regions)) } dst := make([]float32, m.P.W*m.P.H) hits := make([]int, len(dst)) total := 0 for _, r := range part.Regions { _, land := part.Cut(m, r) src := make([]float32, r.Frame.Cells()) for i := range src { if land[i] { src[i] = float32(r.ID + 1) } } total += part.Composite(dst, m, r, src) for y := 0; y < r.Frame.H; y++ { for x := 0; x < r.Frame.W; x++ { pi := r.Frame.PlanetIdx(x, y) if part.Owner[pi] == int32(r.ID) && !m.Sea[pi] { hits[pi]++ } } } } painted := 0 for i := range m.Sea { if !m.Sea[i] { painted++ } } if total != painted { t.Errorf("composited %d land cells, but %d are painted", total, painted) } for i, n := range hits { if m.Sea[i] { if n != 0 { t.Fatalf("water cell %d was written %d times", i, n) } continue } if n != 1 { t.Fatalf("land cell %d was written %d times, want exactly 1", i, n) } if dst[i] == 0 { t.Fatalf("land cell %d came back zero", i) } } } // A landmass that rings the planet cannot be flattened into a rectangle with water on both sides, and the // solve needs that. Better a clear refusal than a silently frozen coastline. func TestALandmassRingingThePlanetIsRefused(t *testing.T) { m := testMap(t, []string{ "##########", "..........", "..........", "..........", }, 2) _, err := Build(m, 2, 1) if err == nil { t.Fatal("accepted a landmass that goes all the way round") } if !strings.Contains(err.Error(), "all the way round") { t.Errorf("error %q does not say why", err) } } // A stray paint pixel should not cost a whole region. func TestSpecksAreDroppedAndCounted(t *testing.T) { m := testMap(t, []string{ "####......................", "####...............#......", "####......................", "..........................", }, 2) part, err := Build(m, 2, 4) if err != nil { t.Fatal(err) } if len(part.Regions) != 1 { t.Fatalf("got %d regions, want 1", len(part.Regions)) } if part.DroppedRegions != 1 || part.DroppedCells != 1 { t.Errorf("dropped %d regions / %d cells, want 1 and 1", part.DroppedRegions, part.DroppedCells) } // And the speck is not owned by anything, so nothing solves it. for i := range m.Sea { if !m.Sea[i] && part.Owner[i] < 0 { return } } t.Error("the speck is still owned by a region") } func TestTheMarginMustFitInsideThePad(t *testing.T) { m := testMap(t, []string{"####......", ".........."}, 1) if _, err := Build(m, 4, 1); err == nil { t.Fatal("accepted a margin wider than the polar pad") } } func TestSpanWrapsTheShortWay(t *testing.T) { mark := func(w int, on ...int) []bool { c := make([]bool, w) for _, x := range on { c[x] = true } return c } cases := []struct { cols []bool w int x0, wanted int }{ {mark(10, 0, 1, 2), 10, 0, 3}, {mark(10, 8, 9, 0, 1), 10, 8, 4}, {mark(10, 5), 10, 5, 1}, {mark(10, 0, 5), 10, 5, 6}, } for _, c := range cases { x0, w := span(c.cols, c.w) if x0 != c.x0 || w != c.wanted { t.Errorf("span = %d+%d, want %d+%d", x0, w, c.x0, c.wanted) } } }