// 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 #include #include #include #include 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& a, const std::vector& 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& 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 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 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 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; }