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