163 lines
6.3 KiB
Go
163 lines
6.3 KiB
Go
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")
|
|
}
|
|
}
|