package uplift import ( "math" "testing" "salty/terrain/internal/noise" "salty/terrain/internal/world" ) var testMult = []float64{0.6, 1.0, 1.8} func rockIn(p world.Planet, f world.Frame, cells int) []float32 { u, v := noise.WorldUV(f.W, f.H, p.CellM, f.OriginXM(), f.OriginYM(), p.NoisePeriodM) return RockK(p, 7, cells, testMult, u, v).Data } // The rule the whole painted path is built on, applied to the rock field: a threshold on a decomposed planet // has to be a quantile of the *planet*. Two regions taking percentiles of their own extents would put the // same physical hillside in different rock, and the boundary between them would be a wall the solver carves. // // It is the same negative control TestTwoFramesAgreeAboutTheSameGround is for the upland fabric, and it is // here rather than assumed because `uplift.Build`'s lithology does take a percentile of its own grid - so the // obvious port of it would fail this and nothing else would have noticed. func TestTwoFramesAgreeAboutTheSameRock(t *testing.T) { const w, h, cellM = 2048, 512, 64.0 p := testPlanet(t, w, h, cellM) whole := rockIn(p, world.Whole(p), 8) a := world.Frame{P: p, X0: 400, Y0: 80, W: 240, H: 160} b := world.Frame{P: p, X0: 520, Y0: 120, W: 240, H: 160} ra, rb := rockIn(p, a, 8), rockIn(p, b, 8) checked := 0 for y := 0; y < a.H; y++ { for x := 0; x < a.W; x++ { px, py := a.PlanetXY(x, y) if px < b.X0 || px >= b.X0+b.W || py < b.Y0 || py >= b.Y0+b.H { continue } got := ra[y*a.W+x] if want := rb[(py-b.Y0)*b.W+(px-b.X0)]; got != want { t.Fatalf("at planet (%d,%d) frame A says %v and frame B says %v", px, py, got, want) } if wh := whole[py*p.W+px]; wh != got { t.Fatalf("at planet (%d,%d) a frame says %v and the whole planet says %v", px, py, got, wh) } checked++ } } if checked == 0 { t.Fatal("the two frames do not overlap; this test measured nothing") } } // Every rock type has to appear, whatever the seed did to the noise. Equal-area bands are what the procedural // path got out of a percentile and the reason it is worth keeping: a seed that happened to produce no hard // rock anywhere would be a seed that quietly removed a process. func TestRockTypesComeOutInEqualShares(t *testing.T) { const w, h, cellM = 2048, 512, 64.0 p := testPlanet(t, w, h, cellM) data := rockIn(p, world.Whole(p), 8) count := map[float64]int{} for _, v := range data { // Only the flat interior of a band counts: the edges are deliberately blended, so a cell there is // between two types and belongs to neither. for _, m := range testMult { if math.Abs(float64(v)-m) < 1e-4 { count[m]++ } } } total := 0 for _, n := range count { total += n } if total < len(data)/2 { t.Fatalf("only %d of %d cells are in the flat middle of a band; the blend is eating the field", total, len(data)) } for _, m := range testMult { share := float64(count[m]) / float64(total) if share < 0.2 || share > 0.47 { t.Errorf("rock type %v is %.1f%% of the land; three equal bands should each be about a third", m, 100*share) } } } // The softening is pointwise, in rank space, and it has to be: a blur is a neighbourhood operation and a // neighbourhood operation near a region's edge reads cells a different decomposition would not have given it. // What the test asserts is the consequence - the field is continuous, so a rock boundary is a transition and // not a wall - measured as the largest step between neighbouring cells. // // The geometry has to be the real one to mean anything. What decides how wide a boundary comes out *in cells* // is the wavelength divided by the cell size: the real planet is a 9 km province on an 8 m cell, about eleven // hundred cells across one, so a blend of a twentieth of the rank falls over tens of cells. A coarse test grid // compresses the same blend into three or four and would fail a threshold the real run passes comfortably, // which is a test measuring its own resolution rather than the code. func TestRockBoundariesAreGradedRatherThanWalls(t *testing.T) { const w, h, cellM, cells = 4096, 64, 8.0, 4 p := testPlanet(t, w, h, cellM) d := rockIn(p, world.Whole(p), cells) if perWave := w / cells; perWave < 512 { t.Fatalf("%d cells across a province; too coarse to say anything about the real grid", perWave) } // The largest gap between neighbouring rock types, which is what a wall would look like. gap := 0.0 for i := 1; i < len(testMult); i++ { gap = math.Max(gap, math.Abs(testMult[i]-testMult[i-1])) } worst := 0.0 for y := 0; y < h; y++ { for x := 0; x+1 < w; x++ { worst = math.Max(worst, math.Abs(float64(d[y*w+x+1]-d[y*w+x]))) } } if worst > gap/8 { t.Errorf("the largest step between neighbouring cells is %.4f against a %.2f gap between types; "+ "the bands are walls, not transitions", worst, gap) } if worst == 0 { t.Fatal("the field is flat; this test measured nothing") } } // bandValue is the pointwise part on its own: continuous, and exactly half-way at a boundary from either side. func TestBandValueIsContinuousAcrossABoundary(t *testing.T) { n := len(testMult) below := bandValue(1/float64(n)-1e-9, testMult, n) above := bandValue(1/float64(n)+1e-9, testMult, n) want := (testMult[0] + testMult[1]) / 2 if math.Abs(below-want) > 1e-6 || math.Abs(above-want) > 1e-6 { t.Errorf("at the first boundary: below %v, above %v, want %v from both sides", below, above, want) } if got := bandValue(0.5/float64(n), testMult, n); got != testMult[0] { t.Errorf("the middle of the first band should be the type itself: got %v want %v", got, testMult[0]) } // The ends clamp rather than running off. if got := bandValue(0, testMult, n); got != testMult[0] { t.Errorf("rank 0 is the first type, got %v", got) } if got := bandValue(1, testMult, n); got != testMult[n-1] { t.Errorf("rank 1 is the last type, got %v", got) } } // Nothing is built when nothing asks for it, which is what a planet with no lithology_wavelength_km gets. func TestNoRockFieldWhenNoneIsAskedFor(t *testing.T) { const w, h, cellM = 256, 128, 64.0 p := testPlanet(t, w, h, cellM) f := world.Whole(p) u, v := noise.WorldUV(f.W, f.H, cellM, 0, 0, p.NoisePeriodM) if RockK(p, 7, 0, testMult, u, v) != nil { t.Error("zero cells should build no field") } if RockK(p, 7, 8, []float64{1.0}, u, v) != nil { t.Error("one rock type is no lithology at all") } }