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