Added: Initial world generation tool

This commit is contained in:
Rainer Leit
2026-09-17 17:55:48 +03:00
parent d64748f76f
commit cc43ed8dc8
2065 changed files with 23664 additions and 1011 deletions
@@ -0,0 +1,86 @@
package check
import (
"math"
"os"
"strconv"
"testing"
"salty/terrain/internal/fluvial"
"salty/terrain/internal/manifest"
"salty/terrain/internal/thermal"
"salty/terrain/internal/uplift"
)
// TestTalusHoldsInThePipeline is the gap between a unit test and a system. thermal.Apply demonstrably cuts a
// cone to the angle of repose, and yet a full run with the angle set to 22 degrees reports a median land slope
// of 37. One of those two things is wrong and the difference matters: the angle of repose is the only direct
// lever on how steep hill country looks.
func TestTalusHoldsInThePipeline(t *testing.T) {
const (
size = 300
deg = 22.0
steps = 300
)
passesUnderTest := 3
if v := os.Getenv("TALUS_PASSES"); v != "" {
passesUnderTest, _ = strconv.Atoi(v)
}
m := manifest.Defaults()
m.Source.Seed = 7
cellM := m.SideM() / float64(size-1)
up := uplift.Build(size, cellM, m)
h := up.Height.Clone()
g := fluvial.NewGrid(size, size, cellM, up.Base)
g.SetElevationRange(-2000, 4000)
talus := thermal.TalusFromDegrees(deg)
g.Run(h.Data, up.Rate.Data, up.K.Data, fluvial.Params{
K: m.Pipeline.Fluvial.K, M: 0.5, N: 1, DtYr: 1500, Steps: steps,
Diffusion: 0.02, FillEvery: 1,
TalusSlope: talus, ThermalEvery: 1, ThermalPasses: passesUnderTest,
}, nil)
if os.Getenv("CLAMP_AFTER") != "" {
t.Logf("removed %.2f m mean by a final clamp", g.ClampToRepose(h.Data, talus))
}
// The steepest neighbour drop anywhere on land, in the same units the angle of repose is written in.
worst, count := 0.0, 0
wx, wy := 0, 0
worstOther := float32(0)
worstOtherBase := false
for y := 1; y < size-1; y++ {
for x := 1; x < size-1; x++ {
i := y*size + x
if up.Base[i] {
continue
}
for _, d := range [][2]int{{1, 0}, {0, 1}, {1, 1}, {1, -1}} {
ni := (y+d[1])*size + (x + d[0])
if up.Base[ni] {
continue
}
dist := cellM
if d[0] != 0 && d[1] != 0 {
dist = cellM * math.Sqrt2
}
if s := math.Abs(float64(h.Data[i]-h.Data[ni])) / dist; s > worst {
worst = s
wx, wy = x, y
worstOther = h.Data[ni]
worstOtherBase = up.Base[ni]
}
}
if count++; false {
_ = count
}
}
}
t.Logf("worst pair at (%d,%d): h=%.1f vs h=%.1f, base=%v/%v", wx, wy, h.Data[wy*size+wx], worstOther, up.Base[wy*size+wx], worstOtherBase)
gotDeg := math.Atan(worst) * 180 / math.Pi
t.Logf("steepest land slope after %d steps: %.1f deg (repose %.1f, cell %.0f m)", steps, gotDeg, deg, cellM)
if gotDeg > deg+5 {
t.Errorf("steepest land slope is %.1f deg with the angle of repose at %.1f: landsliding is not binding", gotDeg, deg)
}
}