// Headless test for the sea-level controller's runaway guard (no display needed). // // g++ -std=c++17 -O2 -Isrc/sim test_sealevel.cpp src/sim/IcoSphere.cpp // src/sim/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp // src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp // src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp // src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp // src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp // -o /tmp/ts && /tmp/ts // // Reproduces the bug reported after a long real run: on a world whose buoyant // (continental) crust area permanently falls short of landFractionTarget (crust // generation is independent of that geographic target -- nothing guarantees they // match), the old unbounded adjustSeaLevel() had no way to reach the target using // real land, so it kept sinking seaLevel to "poach" ageing oceanic crust as fake // land -- and since that crust keeps deepening with age, the target kept slipping // away, sinking seaLevel indefinitely (observed: -2900 m after ~1340 My, still // falling). Verifies the seaLevelMin/seaLevelMax clamp stops the runaway in both // directions, that it actually engages (proving the mechanism still adjusts, not // just trivially idle), and that a normal, non-pathological world never comes // close to the clamp. #include "Planet.hpp" #include #include static int failures = 0; static void check(bool cond, const char* what) { std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what); if (!cond) ++failures; } int main() { std::printf("Sea level: runaway guard\n"); // --- Scenario 1: continental crust structurally short of the target ------- // 25% of cells continental near continentBase, 75% oceanic spread smoothly // across a wide, continuous depth range (mimicking a real mix of young and // long-ageing seafloor, not a cliff) -- deliberately short of the 30% default // landFractionTarget. With a continuous distribution, each single seaLevelStep // nudge always captures more cells and strictly reduces the error, so the // pre-fix controller would keep taking that nudge forever, sinking without end. { PlanetConfig cfg; cfg.subdivisions = 3; cfg.seed = 99; Planet planet; planet.generate(cfg); const int n = (int)planet.cells.size(); for (int i = 0; i < n; ++i) { bool continental = (i % 4) == 0; // 25% < landFractionTarget (30%) planet.setCrust(i, continental); if (continental) planet.setElevation(i, planet.cfg.continentBase + (i % 7) * 10.0); else planet.setElevation(i, -10.0 - 8000.0 * (double)i / n); // continuous -10..-8010 m } double startSeaLevel = planet.cfg.seaLevel; // erode()'s transport is dtMy-scaled; dt=0 isolates the periodic // adjustSeaLevel() call (still triggered by the iteration counter) from // any elevation change, so the crafted distribution above stays intact. for (int i = 0; i < planet.cfg.seaLevelEvery * 400; ++i) planet.erode(0.0); check(planet.cfg.seaLevel < startSeaLevel - 100.0, "short-of-target land engages the controller (seaLevel moves down)"); check(planet.cfg.seaLevel >= planet.cfg.seaLevelMin, "seaLevel never sinks past seaLevelMin (the pre-fix bug: it had no floor)"); check(planet.cfg.seaLevel <= planet.cfg.seaLevelMax, "seaLevel stays within the upper bound too"); } // --- Scenario 2: mirror case, land far in excess of the target ------------ { PlanetConfig cfg; cfg.subdivisions = 3; cfg.seed = 100; Planet planet; planet.generate(cfg); const int n = (int)planet.cells.size(); for (int i = 0; i < n; ++i) { bool continental = (i % 4) != 0; // 75% >> landFractionTarget (30%) planet.setCrust(i, continental); if (continental) planet.setElevation(i, 10.0 + 8000.0 * (double)i / n); // continuous 10..8010 m else planet.setElevation(i, planet.cfg.oceanBase + (i % 7) * 10.0); } double startSeaLevel = planet.cfg.seaLevel; for (int i = 0; i < planet.cfg.seaLevelEvery * 400; ++i) planet.erode(0.0); check(planet.cfg.seaLevel > startSeaLevel + 100.0, "excess land engages the controller (seaLevel moves up)"); check(planet.cfg.seaLevel <= planet.cfg.seaLevelMax, "seaLevel never climbs past seaLevelMax (mirror of the sinking bug)"); check(planet.cfg.seaLevel >= planet.cfg.seaLevelMin, "seaLevel stays within the lower bound too"); } // --- Scenario 3: a normal, freshly-generated world never approaches the --- // clamp -- the guard is a safety rail for the pathological case, not // something that interferes with ordinary sea-level settling. { PlanetConfig cfg; cfg.subdivisions = 4; cfg.seed = 42; Planet planet; planet.generate(cfg); for (int i = 0; i < 40; ++i) planet.step(); // settle Phase-1 relief first planet.drifting = true; for (int i = 0; i < planet.cfg.seaLevelEvery * 20; ++i) planet.erode(planet.cflDtMy()); check(planet.cfg.seaLevel > planet.cfg.seaLevelMin * 0.5 && planet.cfg.seaLevel < planet.cfg.seaLevelMax * 0.5, "a normal world's sea level settles well clear of the clamp bounds"); } std::printf(failures ? "\nFAILURES: %d\n" : "\nALL SEA LEVEL CHECKS PASSED\n", failures); return failures ? 1 : 0; }