planetsim/test_weather.cpp

176 lines
8.5 KiB
C++

// 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 <cstdio>
#include <cmath>
#include <algorithm>
#include <sstream>
static int failures = 0;
static void check(bool cond, const char* what) {
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
if (!cond) ++failures;
}
static bool sameStorms(const std::vector<WeatherSystem>& a, const std::vector<WeatherSystem>& b) {
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); ++i) {
if (a[i].id != b[i].id
|| a[i].pos.x != b[i].pos.x || a[i].pos.y != b[i].pos.y || a[i].pos.z != b[i].pos.z
|| a[i].strength != b[i].strength || a[i].radius != b[i].radius
|| a[i].age != b[i].age || a[i].life != b[i].life || a[i].spin != b[i].spin
|| a[i].tropical != b[i].tropical) return false;
}
return true;
}
static bool sameWeatherSnapshot(const WeatherSnapshot& a, const WeatherSnapshot& b) {
return a.humidity == b.humidity && a.cloud == b.cloud && a.rain == b.rain
&& sameStorms(a.storms, b.storms)
&& a.rng == b.rng && a.nextId == b.nextId;
}
// 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<double>& rn = p.rain();
const std::vector<double>& 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<double> 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 v11 (fields + storms)\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, 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 round-trips the weather fields");
bool srt = ((int)q.storms().size() == (int)p.storms().size());
for (size_t i = 0; i < q.storms().size() && srt; ++i)
if (q.storms()[i].id != p.storms()[i].id
|| q.storms()[i].pos.dot(p.storms()[i].pos) < 1.0 - 1e-9
|| q.storms()[i].strength != p.storms()[i].strength) srt = false;
check(srt, "save v11 round-trips the active weather systems");
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("Weather: snapshot deterministic replay\n");
{
Planet wc; wc.generate(cfg); wc.initWeather();
for (int k = 0; k < 90; ++k) { wc.computeInsolation(0.25, std::fmod(0.2 + 0.01 * k, 1.0)); wc.stepWeather(1.0); }
WeatherSnapshot snap = wc.captureWeather();
for (int k = 0; k < 180; ++k) { wc.computeInsolation(0.25, std::fmod(0.4 + 0.01 * k, 1.0)); wc.stepWeather(1.0); }
WeatherSnapshot first = wc.captureWeather();
wc.restoreWeather(snap);
for (int k = 0; k < 180; ++k) { wc.computeInsolation(0.25, std::fmod(0.4 + 0.01 * k, 1.0)); wc.stepWeather(1.0); }
WeatherSnapshot second = wc.captureWeather();
check(sameWeatherSnapshot(first, second), "restored weather snapshot replays storms and RNG exactly");
}
std::printf(failures ? "\nSOME WEATHER CHECKS FAILED (%d)\n" : "\nALL WEATHER CHECKS PASSED\n", failures);
return failures ? 1 : 0;
}