Backward stepping previously only rewound the deterministic sky (day/night, tides, seasons, moons) and held the weather, because the cloud/rain/storm state is integrated forward and not analytically reversible. Now a forward step snapshots the full weather state via new Planet::captureWeather()/restoreWeather() (humidity/cloud/rain/storms + the storm RNG/next-id) into a bounded wxUndo ring, and the backward step restores the previous snapshot -- so '.' then ',' reverses EVERYTHING, including clouds, rain and moving storms. Both steps auto-pause (video frame-step feel). A continuous run (unpause) clears the undo history, after which ',' falls back to the sky-only rewind. Restoring also reseeds the storm RNG so re-stepping forward replays deterministically. test_weather.cpp adds a capture/restore round-trip check; all five suites pass; GUI build clean. Docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
6.6 KiB
C++
139 lines
6.6 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;
|
|
}
|
|
|
|
// 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 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;
|
|
}
|