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

243 lines
8.8 KiB
Go

package uplift
import (
"math"
"testing"
"salty/terrain/internal/manifest"
"salty/terrain/internal/noise"
"salty/terrain/internal/world"
)
// testPlanet is a small cylinder with a period that divides its circumference, which world.Planet.Validate
// requires and which every noise field here depends on.
func testPlanet(t *testing.T, w, h int, cellM float64) world.Planet {
t.Helper()
p := world.Planet{CellM: cellM, W: w, H: h, PadY: 0, NoisePeriodM: float64(w) * cellM}
if err := p.Validate(); err != nil {
t.Fatal(err)
}
return p
}
// The whole contract of the fraction key: it is a share of the planet's surface, and it is the share standing
// above the midpoint between the class floor and the class rate.
//
// This is the test that makes the number worth writing in a legend. Cutting the fabric at a fixed *value*
// instead would make the realised share depend on the shape of the noise's distribution, which nobody can
// predict from a JSON file, and it would drift every time an octave count changed.
func TestTheMassifFractionIsTheShareOfThePlanetThatStandsUp(t *testing.T) {
// Wider than massifProbeW, deliberately. At 512 the probe clamps to the planet's own width and samples
// the identical grid, so every number below comes out exact and the test measures nothing - which is
// what the first version of it did. A real planet is 12500 columns against a 1024-column probe, so the
// distribution being applied is always a coarser measurement of the field than the field it is applied
// to, and that gap is the thing worth bounding.
const w, h, cellM = 2048, 512, 64.0
p := testPlanet(t, w, h, cellM)
f := world.Whole(p)
for _, fraction := range []float64{0.05, 0.15, 0.30, 0.50} {
u, v := noise.WorldUV(w, h, cellM, f.OriginXM(), f.OriginYM(), p.NoisePeriodM)
rank := MassifRank(p, 7, 8, u, v)
above, full, off := 0, 0, 0
for _, r := range rank.Data {
s := massifShape(float64(r), fraction)
if s > 0.5 {
above++
}
if s >= 1 {
full++
}
if s > 0 {
off++
}
}
got := float64(above) / float64(len(rank.Data))
// Two per cent of the planet. The probe is 1024 x 256 against this 2048 x 512 grid, so the two are
// sampling the same field at different steps and cannot agree to the cell.
if math.Abs(got-fraction) > 0.02 {
t.Errorf("fraction %.2f: %.1f%% of the planet stands above the midpoint, want %.0f%%",
fraction, 100*got, 100*fraction)
}
// Half the fraction again reaches the class rate outright and half again above that is off the plain
// at all. Both follow from the ramp and both are what the legend documents.
if g, want := float64(full)/float64(len(rank.Data)), fraction*0.5; math.Abs(g-want) > 0.02 {
t.Errorf("fraction %.2f: %.1f%% is at the full rate, want %.0f%%", fraction, 100*g, 100*want)
}
if g, want := float64(off)/float64(len(rank.Data)), fraction*1.5; math.Abs(g-want) > 0.03 {
t.Errorf("fraction %.2f: %.1f%% is off the plain, want %.0f%%", fraction, 100*g, 100*want)
}
}
}
// Rule 1 of the tiling plan, for the fabric: two regions covering the same physical place must agree to the
// bit. This is the one that would fail if the threshold were ever taken as a percentile of the region, which
// is the obvious implementation and the wrong one - see the note on massifCDF.
func TestTwoFramesAgreeAboutTheSameGround(t *testing.T) {
const w, h, cellM = 2048, 512, 64.0
p := testPlanet(t, w, h, cellM)
rankIn := func(f world.Frame) []float32 {
u, v := noise.WorldUV(f.W, f.H, cellM, f.OriginXM(), f.OriginYM(), p.NoisePeriodM)
return MassifRank(p, 7, 8, u, v).Data
}
whole := rankIn(world.Whole(p))
// A window well inside the planet, and a second one overlapping it from a different origin.
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 := rankIn(a), rankIn(b)
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]
want := rb[(py-b.Y0)*b.W+(px-b.X0)]
if 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")
}
}
// A frame that straddles the seam is ordinary, not special: column W-1 and column 0 are neighbours, so the
// fabric has to run continuously across them. A wrong noise period is the way this breaks, and it breaks
// invisibly on a map whose two edges are as far apart on screen as they can be.
func TestTheFabricCrossesTheSeam(t *testing.T) {
const w, h, cellM = 2048, 512, 64.0
p := testPlanet(t, w, h, cellM)
u, v := noise.WorldUV(w, h, cellM, 0, 0, p.NoisePeriodM)
whole := MassifRank(p, 7, 8, u, v)
// The step across the seam must be no bigger than a typical step inside the map.
worstSeam, worstInside := 0.0, 0.0
for y := 0; y < h; y++ {
d := math.Abs(float64(whole.Data[y*w] - whole.Data[y*w+w-1]))
if d > worstSeam {
worstSeam = d
}
for x := 1; x < w; x++ {
if e := math.Abs(float64(whole.Data[y*w+x] - whole.Data[y*w+x-1])); e > worstInside {
worstInside = e
}
}
}
if worstSeam > worstInside {
t.Errorf("the biggest step across the seam is %.4f against %.4f anywhere inside the map; "+
"the fabric does not wrap", worstSeam, worstInside)
}
}
// What the feature is for, measured on the thing an author actually gets: a class with a massif has to come
// out mostly plain, and the plain has to be the floor rather than some average of the two.
func TestAPaintedClassWithAMassifIsMostlyPlain(t *testing.T) {
const w, h, cellM = 2048, 512, 64.0
p := testPlanet(t, w, h, cellM)
f := world.Whole(p)
class := make([]uint8, w*h)
land := make([]bool, w*h)
for i := range class {
class[i], land[i] = 1, true
}
const rate = 0.00008 // 0.08 mm/yr, the rate a massif reaches
const floor = 0.00001 // 0.01 mm/yr, the plain
const fraction = 0.15
m := manifest.Defaults()
m.Source.Seed = 7
up := FromTemplate(Paint{
Frame: f, Class: class, Land: land,
Rates: []float32{0, rate},
Ks: []float32{1, 1},
PlainM: []float64{0, 0},
PlainFloor: []float32{0, 0},
MassifFloor: []float32{0, floor},
MassifFraction: []float64{0, fraction},
MassifCells: 8,
Variation: 0, // the swell off, so the fabric is the only thing being measured
}, m)
// Under twice the floor is "plain" for this purpose: the ramp is smooth, so a cell just off the plain is
// still plain, and the question being asked is whether most of the class is down there at all.
plain, high := 0, 0
for _, r := range up.Rate.Data {
if float64(r) < 2*floor {
plain++
}
if float64(r) > 0.5*(rate+floor) {
high++
}
}
if share := float64(plain) / float64(len(up.Rate.Data)); share < 0.6 {
t.Errorf("only %.0f%% of the class is plain; the point of a massif is that most of it is", 100*share)
}
if share := float64(high) / float64(len(up.Rate.Data)); math.Abs(share-fraction) > 0.02 {
t.Errorf("%.1f%% of the class is above the midpoint, want %.0f%%", 100*share, 100*fraction)
}
// And the floor has to be the floor. Before this existed the lowest rate on a uniformly painted class was
// the class rate itself, which is exactly the defect: 0.08 mm/yr is an 11 degree hillslope everywhere.
lo := math.Inf(1)
for _, r := range up.Rate.Data {
if float64(r) < lo {
lo = float64(r)
}
}
if math.Abs(lo-floor) > 0.02*floor {
t.Errorf("the lowest rate on the class is %.5f mm/yr, want the floor %.3f", lo*1000, floor*1000)
}
}
// A legend that asks for no massif has to produce exactly what it did before the fabric existed. The fabric
// is opt-in and it must not be a silent change to every template already written against the old contract.
func TestAClassWithoutAMassifIsUnchanged(t *testing.T) {
const w, h, cellM = 256, 128, 64.0
p := testPlanet(t, w, h, cellM)
f := world.Whole(p)
class := make([]uint8, w*h)
land := make([]bool, w*h)
for i := range class {
class[i], land[i] = 1, true
}
const rate = 0.00008
m := manifest.Defaults()
m.Source.Seed = 7
base := Paint{
Frame: f, Class: class, Land: land,
Rates: []float32{0, rate}, Ks: []float32{1, 1},
PlainM: []float64{0, 0}, PlainFloor: []float32{0, 0},
Variation: 0.3,
}
without := FromTemplate(base, m)
withTables := base
withTables.MassifFloor = []float32{0, 0}
withTables.MassifFraction = []float64{0, 0} // the tables present, the feature not asked for
withTables.MassifCells = 8
same := FromTemplate(withTables, m)
for i := range without.Rate.Data {
if without.Rate.Data[i] != same.Rate.Data[i] {
t.Fatalf("cell %d: %v without the massif tables, %v with them at fraction 0",
i, without.Rate.Data[i], same.Rate.Data[i])
}
}
}