planetsim/test_volcano.cpp

229 lines
11 KiB
C++

// Headless test for Live World volcanoes (stateful growth, dormancy, explosions, ash,
// rewind snapshots and save/load). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_volcano.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/PlanetVolcano.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tv && /tmp/tv
#include "Planet.hpp"
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <sstream>
static int failures = 0;
static constexpr double YEAR_HOURS = 24.0 * 365.25;
static void check(bool cond, const char* what) {
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
if (!cond) ++failures;
}
static void settle(Planet& p, int maxSteps = 800) {
int run = 0;
for (int s = 0; s < maxSteps; ++s) {
double mc = p.step();
if (mc < 2.0) { if (++run >= 3) break; } else run = 0;
}
p.computeClimate();
p.classifyBiomes();
}
static void drift(Planet& p, int iters) {
p.drifting = true;
for (int k = 0; k < iters; ++k) { double dt = p.cflDtMy(); p.advect(dt); p.step(); p.erode(dt); }
}
// Replicate placeVolcanoes()'s context classification: 0 = ridge (baby plate / neighbour baby),
// 1 = plate border (a differing-plate neighbour), 2 = interior.
static int classify(const Planet& p, int i) {
auto isBaby = [&](int pid) { return pid >= 0 && pid < (int)p.plates.size() && p.plates[pid].baby; };
int pid = p.cells[i].plateId;
bool ridge = isBaby(pid), border = false;
for (int j : p.cells[i].neighbors) { int pj = p.cells[j].plateId; if (pj != pid) border = true; if (isBaby(pj)) ridge = true; }
return ridge ? 0 : (border ? 1 : 2);
}
static bool sameVolcanoes(const std::vector<Volcano>& a, const std::vector<Volcano>& 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].cell != b[i].cell || a[i].kind != b[i].kind
|| a[i].submarine != b[i].submarine || a[i].phase != b[i].phase
|| a[i].activity != b[i].activity || a[i].baseElev != b[i].baseElev
|| a[i].built != b[i].built || a[i].timer != b[i].timer
|| a[i].ashTimer != b[i].ashTimer || a[i].ashCarry != b[i].ashCarry) return false;
return true;
}
static int cloudRaisedCells(const Planet& p, const std::vector<double>& before, double eps = 1e-9) {
int raised = 0;
const auto& cloud = p.cloud();
for (size_t i = 0; i < cloud.size() && i < before.size(); ++i)
if (cloud[i] > before[i] + eps) ++raised;
return raised;
}
int main() {
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 9090;
Planet p; p.generate(cfg); settle(p); drift(p, 120);
const int n = (int)p.cells.size();
std::printf("Volcanoes: deterministic placement\n");
p.placeVolcanoes(0.0); std::vector<Volcano> first = p.volcanoes;
p.placeVolcanoes(0.0);
check(!first.empty(), "placeVolcanoes places a non-empty set");
check(sameVolcanoes(first, p.volcanoes), "placeVolcanoes is deterministic (re-run identical)");
std::printf("Volcanoes: RNG isolation from tectonics\n");
Planet a; a.generate(cfg); settle(a);
Planet b; b.generate(cfg); settle(b);
b.cfg.volcanoMaxCount = 0; // isolate RNG stream without intentionally changing terrain
for (int k = 0; k < 40; ++k) {
double dta = a.cflDtMy(); a.advect(dta); a.step(); a.erode(dta);
double dtb = b.cflDtMy(); b.advect(dtb); b.step(); b.erode(dtb);
if (k == 20) b.placeVolcanoes(0.0); // must not touch the tectonic RNG stream
}
bool terrainSame = true;
for (int i = 0; i < n; ++i) if (std::fabs(a.cells[i].elevation - b.cells[i].elevation) > 1e-9) terrainSame = false;
check(terrainSame, "placeVolcanoes never perturbs tectonic evolution");
std::printf("Volcanoes: context classification\n");
int eligRidge = 0, eligBorder = 0, eligInterior = 0;
for (int i = 0; i < n; ++i) { int k = classify(p, i); if (k == 0) ++eligRidge; else if (k == 1) ++eligBorder; else ++eligInterior; }
std::printf(" eligible cells: ridge %d, border %d, interior %d\n", eligRidge, eligBorder, eligInterior);
p.cfg.volcanoProbRidge = 1.0; p.cfg.volcanoProbBorder = 1.0; p.cfg.volcanoProbInterior = 0.0;
p.cfg.volcanoMaxCount = 1000000; p.cfg.volcanoInitialBuildMax = 0.0;
p.placeVolcanoes(0.0);
bool noInterior = true; for (const Volcano& v : p.volcanoes) if (v.kind == 2) noInterior = false;
check(noInterior, "interior prob 0 places no interior vents");
check((int)p.volcanoes.size() == eligRidge + eligBorder, "prob 1 fills exactly the ridge+border cells");
std::printf("Volcanoes: probability ordering (border > interior)\n");
p.cfg.volcanoProbRidge = 1.0; p.cfg.volcanoProbBorder = 0.30; p.cfg.volcanoProbInterior = 0.05;
p.placeVolcanoes(0.0);
int gotRidge = 0, gotBorder = 0, gotInterior = 0;
for (const Volcano& v : p.volcanoes) { if (v.kind == 0) ++gotRidge; else if (v.kind == 1) ++gotBorder; else ++gotInterior; }
double rB = eligBorder ? (double)gotBorder / eligBorder : 0.0;
double rI = eligInterior ? (double)gotInterior / eligInterior : 0.0;
std::printf(" placement rate: border %.3f, interior %.3f\n", rB, rI);
check(rB > rI, "border cells are far likelier to host a volcano than interior cells");
if (eligRidge > 0) {
double rR = (double)gotRidge / eligRidge;
std::printf(" placement rate: ridge %.3f\n", rR);
check(rR >= rB, "young-ridge cells are the likeliest of all");
} else std::printf(" (no young-ridge cells this seed -- ridge rate not asserted)\n");
std::printf("Volcanoes: initial built height can make islands immediately\n");
Planet pre; pre.generate(cfg); settle(pre); drift(pre, 120);
pre.cfg.volcanoProbRidge = pre.cfg.volcanoProbBorder = pre.cfg.volcanoProbInterior = 1.0;
pre.cfg.volcanoMaxCount = 1000000;
pre.cfg.volcanoInitialBuildMax = 10000.0;
pre.placeVolcanoes(0.0);
bool someBuilt = false, instantIsland = false;
for (const Volcano& v : pre.volcanoes) {
if (v.built > 0.0) someBuilt = true;
if (v.submarine && pre.cells[v.cell].elevation > pre.cfg.seaLevel && !pre.cells[v.cell].oceanic)
instantIsland = true;
}
check(someBuilt, "placement assigns nonzero pre-built height");
check(instantIsland, "a pre-built submarine vent can breach into an island on entry");
std::printf("Volcanoes: forward stepping grows statefully\n");
Planet g; g.generate(cfg); settle(g); drift(g, 80);
g.cfg.volcanoProbRidge = g.cfg.volcanoProbBorder = g.cfg.volcanoProbInterior = 1.0;
g.cfg.volcanoMaxCount = 1; g.cfg.volcanoInitialBuildMax = 0.0;
g.cfg.volcanoBuildRate = 10.0; g.cfg.volcanoFreeHeight = 1e9; g.cfg.volcanoDeadActivity = 0.0;
g.placeVolcanoes(0.0);
check(!g.volcanoes.empty(), "one growth-test volcano placed");
if (!g.volcanoes.empty()) {
g.volcanoes[0].activity = 1.0;
double b0 = g.volcanoes[0].built;
g.stepVolcanoes(2.0);
check(g.volcanoes[0].built > b0 + 19.9, "growing vent integrates built height forward");
check(std::fabs(g.cells[g.volcanoes[0].cell].elevation - (g.volcanoes[0].baseElev + g.volcanoes[0].built)) < 1e-6,
"vent elevation is reasserted from baseElev + built");
}
std::printf("Volcanoes: forced dormancy, explosion, ash blast and activity decay\n");
Planet x; x.generate(cfg); settle(x); drift(x, 80);
x.initWeather();
x.computeInsolation(0.25, 0.3);
x.computeLiveSeason(0.25);
x.cfg.volcanoProbRidge = x.cfg.volcanoProbBorder = x.cfg.volcanoProbInterior = 1.0;
x.cfg.volcanoMaxCount = 1; x.cfg.volcanoInitialBuildMax = 0.0;
x.cfg.volcanoFreeHeight = -1e9; x.cfg.volcanoMaxHeight = 1.0;
x.cfg.volcanoDormancyRate = YEAR_HOURS * 1000.0;
x.cfg.volcanoDormantMinYears = x.cfg.volcanoDormantMaxYears = 0.0;
x.cfg.volcanoExplodeDropFrac = 0.20; x.cfg.volcanoActivityDecay = 0.70;
x.cfg.volcanoAshMinYears = x.cfg.volcanoAshMaxYears = 0.01;
x.cfg.volcanoBlastRadius = 0.09; x.cfg.volcanoBlastCloud = 1.5;
x.cfg.volcanoAshPuffCellsPerWeek = 100.0;
x.placeVolcanoes(0.0);
check(!x.volcanoes.empty(), "one lifecycle-test volcano placed");
if (!x.volcanoes.empty()) {
x.volcanoes[0].built = 2000.0;
x.volcanoes[0].activity = 1.0;
x.volcanoes[0].phase = 0;
x.stepVolcanoes(0.0);
x.stepVolcanoes(1.0);
check(x.volcanoes[0].phase == 1, "tall growing vent can go dormant");
double beforeBuilt = x.volcanoes[0].built;
double beforeActivity = x.volcanoes[0].activity;
std::vector<double> beforeCloud = x.cloud();
x.stepVolcanoes(1.0);
check(x.volcanoes[0].phase == 0, "dormant vent explodes and returns to growing");
check(x.volcanoes[0].built < beforeBuilt * 0.81, "explosion shaves the peak");
check(x.volcanoes[0].ashTimer > 0.0, "explosion starts sustained ash emission");
check(x.volcanoes[0].activity < beforeActivity, "explosion decays activity");
check(cloudRaisedCells(x, beforeCloud) >= 20, "explosion blasts ash over a wide cell radius");
beforeCloud = x.cloud();
x.stepVolcanoes(24.0 * 7.0);
check(cloudRaisedCells(x, beforeCloud) > 0, "post-explosion ashTimer keeps puffing ash");
}
std::printf("Volcanoes: snapshot restore reverses lifecycle state\n");
if (!x.volcanoes.empty()) {
WeatherSnapshot snap = x.captureWeather();
std::vector<Volcano> saved = x.volcanoes;
x.volcanoes[0].built += 500.0;
x.volcanoes[0].phase = 1;
x.volcanoes[0].timer = 123.0;
x.stepVolcanoes(0.0);
x.restoreWeather(snap);
x.stepVolcanoes(0.0);
check(sameVolcanoes(saved, x.volcanoes), "captureWeather/restoreWeather round-trips volcano state");
check(std::fabs(x.cells[x.volcanoes[0].cell].elevation - (x.volcanoes[0].baseElev + x.volcanoes[0].built)) < 1e-6,
"restored volcano state reasserts terrain");
}
std::printf("Volcanoes: save v15 round-trip and v14 discard path\n");
{
x.cfg.volcanoDormancyRate = 1.0; // keep saved config inside normal validation bounds
x.cfg.volcanoFreeHeight = 1000.0;
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
x.writeState(ss);
ss.seekg(0);
Planet r;
bool ok = r.readState(ss, true, true, true, true, true, true, true);
check(ok, "readState accepts a v15 stream");
check(sameVolcanoes(x.volcanoes, r.volcanoes), "stateful volcanoes round-trip through save");
}
{
Planet old; old.generate(cfg); settle(old);
old.volcanoes.clear(); // empty old block is layout-compatible and still exercises discard.
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
old.writeState(ss);
ss.seekg(0);
Planet r;
bool ok = r.readState(ss, true, true, true, true, true, true, false);
check(ok, "readState consumes a v14 volcano block");
check(r.volcanoes.empty(), "v14 volcanoes are discarded for lifecycle reseeding");
}
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL VOLCANO CHECKS PASSED\n", failures);
return failures ? 1 : 0;
}