Files
2026-09-25 17:02:24 +03:00

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