Tooling
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user