// Headless test for the Live World weather cycle (humidity / cloud / rain). No display needed. // // g++ -std=c++17 -O2 -Isrc/sim test_weather.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/PlanetWeather.cpp \ // src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \ // src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tw && /tmp/tw // // Verifies: fields stay in range; oceans (the evaporation source) end up moister than land; // clouds form and rain falls somewhere; the cycle is deterministic; and save v10 round-trips it. #include "Planet.hpp" #include #include #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; } // Run a fixed weather sequence on a planet (returns whether rain ever fell, max cloud + storms). static void runWeather(Planet& p, bool& everRained, double& maxCloud, int& maxStorms) { p.initWeather(); everRained = false; maxCloud = 0.0; maxStorms = 0; for (int k = 0; k < 300; ++k) { p.computeInsolation(0.25, std::fmod(0.3 + 0.01 * k, 1.0)); // sun advances p.stepWeather(1.0); // 1-hour steps const std::vector& rn = p.rain(); const std::vector& cl = p.cloud(); for (size_t i = 0; i < rn.size(); ++i) { if (rn[i] > 0.0) everRained = true; maxCloud = std::max(maxCloud, cl[i]); } maxStorms = std::max(maxStorms, (int)p.storms().size()); } } int main() { PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 1337; Planet p; p.generate(cfg); const int n = (int)p.cells.size(); std::printf("Weather: cycle\n"); bool rained = false; double maxCloud = 0.0; int maxStorms = 0; runWeather(p, rained, maxCloud, maxStorms); bool inRange = true; for (int i = 0; i < n; ++i) { if (p.humidity()[i] < -1e-9) inRange = false; if (p.cloud()[i] < -1e-9 || p.cloud()[i] > 1.5 + 1e-9) inRange = false; if (p.rain()[i] < -1e-9) inRange = false; } check(inRange, "humidity/cloud/rain stay in range"); check(maxCloud > 0.05, "clouds form"); check(rained, "rain falls somewhere"); // Oceans are the moisture source -> moister than land on average. double oh = 0, lh = 0; int oc = 0, lc = 0; for (int i = 0; i < n; ++i) { if (p.cells[i].elevation <= cfg.seaLevel) { oh += p.humidity()[i]; ++oc; } else { lh += p.humidity()[i]; ++lc; } } oh /= std::max(1, oc); lh /= std::max(1, lc); std::printf(" mean humidity: ocean %.3f, land %.3f\n", oh, lh); check(oh > lh, "oceans end up moister than land"); std::printf("Weather: moving systems\n"); std::printf(" max concurrent systems: %d\n", maxStorms); check(maxStorms > 0, "weather systems spawn over a run"); if (!p.storms().empty()) { // cloud shield (no mutation of p) const auto& ws = p.storms()[0]; double inSum = 0, allSum = 0; int inN = 0; for (int i = 0; i < n; ++i) { allSum += p.cloud()[i]; double d = std::acos(std::clamp(p.cells[i].unit.dot(ws.pos), -1.0, 1.0)); if (d < ws.radius) { inSum += p.cloud()[i]; ++inN; } } check(inN > 0 && inSum / inN > allSum / n, "cloud is thicker inside a weather system"); } std::printf("Weather: RNG isolation\n"); Planet z; z.generate(cfg); std::vector elev0(n); for (int i = 0; i < n; ++i) elev0[i] = z.cells[i].elevation; z.initWeather(); for (int k = 0; k < 60; ++k) { z.computeInsolation(0.25, std::fmod(0.3 + 0.01 * k, 1.0)); z.stepWeather(1.0); } bool terrainSame = true; for (int i = 0; i < n; ++i) if (z.cells[i].elevation != elev0[i]) terrainSame = false; check(terrainSame, "weather + storm RNG never perturb the terrain"); std::printf("Weather: determinism\n"); Planet p2; p2.generate(cfg); bool r2; double mc2; int ms2; runWeather(p2, r2, mc2, ms2); // p and p2 both at 300 steps bool same = ((int)p2.storms().size() == (int)p.storms().size()); for (int i = 0; i < n; ++i) if (p2.humidity()[i] != p.humidity()[i] || p2.cloud()[i] != p.cloud()[i] || p2.rain()[i] != p.rain()[i]) same = false; check(same, "same seed + sequence -> identical weather + systems"); std::printf("Weather: systems move\n"); if (!p.storms().empty()) { // one more step -> a system shifts position Vec3 before = p.storms()[0].pos; p.computeInsolation(0.25, 0.61); p.stepWeather(1.0); double best = -2.0; for (const auto& ws : p.storms()) best = std::max(best, before.dot(ws.pos)); double ang = std::acos(std::clamp(best, -1.0, 1.0)); check(ang > 1e-4 && ang < 0.3, "a weather system moves between steps"); } std::printf("Weather: save v10\n"); std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary); p.writeState(ss); Planet q; bool ok = q.readState(ss, true, true, true, true); bool rt = ok && (int)q.cloud().size() == n; for (int i = 0; i < n && rt; ++i) if (q.humidity()[i] != p.humidity()[i] || q.cloud()[i] != p.cloud()[i] || q.rain()[i] != p.rain()[i]) rt = false; check(rt, "save v10 round-trips the weather state"); std::printf("Weather: snapshot round-trip (step-back undo)\n"); { Planet wc; wc.generate(cfg); wc.initWeather(); for (int k = 0; k < 60; ++k) { wc.computeInsolation(0.25, std::fmod(0.3 + 0.01 * k, 1.0)); wc.stepWeather(1.0); } WeatherSnapshot snap = wc.captureWeather(); int s0 = (int)wc.storms().size(); for (int k = 0; k < 30; ++k) { wc.computeInsolation(0.25, std::fmod(0.9 + 0.01 * k, 1.0)); wc.stepWeather(1.0); } wc.restoreWeather(snap); // step back to the saved frame bool rt = ((int)wc.storms().size() == s0); for (int i = 0; i < n && rt; ++i) if (wc.cloud()[i] != snap.cloud[i] || wc.humidity()[i] != snap.humidity[i] || wc.rain()[i] != snap.rain[i]) rt = false; check(rt, "captureWeather/restoreWeather round-trips the full weather state"); } std::printf(failures ? "\nSOME WEATHER CHECKS FAILED (%d)\n" : "\nALL WEATHER CHECKS PASSED\n", failures); return failures ? 1 : 0; }