planetsim/test_live.cpp
Jonas Reith aa32e38dad Live World stage: clock, day/night, seasons, moons, tides & ocean currents
Adds the slow real-time "Live World" mode (key W on a settled world) that runs the
finished planet on an hours->weeks/months clock (liveTime/liveRate, [ / ] ramp the rate)
with geology frozen. Everything new is a derived per-cell field flowed over the fixed grid.

Day/night & seasons (PlanetLive.cpp): a raylib-free per-cell insolation field
(computeInsolation) drives a moving day/night terminator (N) from time-of-day rotation +
seasonal declination (axialTilt); a live seasonal temperature cycles the static summer/winter
fields over the year (computeLiveSeason); a moving snow/sea-ice line tracks it. Day/night +
snow are render overlays over any colour mode (3D + 2D). Save v8 stores the live clock.

Sky & tides (PlanetOcean.cpp): 1-3 random moons (separate RNG, saved v9) orbit on the
clock and, with the now small/distant sun, raise an equilibrium tide (computeTides -> sTide),
shown as a tide-coloured coastline (T, buildCoastline + tideColor). Moons render with sun-lit
phases, orbit rings and eclipses (solar shadow spot in the day/night overlay, lunar dimming).
The 2D map is left-aligned; the freed space holds a Live-World "Sky & tides" panel (per-moon
phase + a selected coastal tile's tidal phase).

Ocean currents + climate feedback (PlanetOcean.cpp): computeOceanCurrents builds a
per-ocean-cell tangent velocity from wind stress + Coriolis deflection + coast-following
(gyres); computeClimate feeds warm (poleward) / cold (equatorward) currents back into sTemp as
a bounded coastal anomaly (climateCurrentFactor) before seasons, so biomes shift. Rendered as
warm/cold current arrows (O).

Config: dayLengthHours/yearLengthDays/snowTemp/seaIceTemp/tideAmplitude/tideSunFactor/
climateCurrentFactor. Save header v7->v9 (version-gated; older saves load fine). Headless
test_live.cpp + test_ocean.cpp; test_logic/test_biota still pass; GUI build clean. Docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 10:04:45 +02:00

138 lines
6.5 KiB
C++

// Headless test for the Live World stage (insolation + live seasonal temperature).
// No display / raylib needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_live.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
// src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tl && /tmp/tl
//
// Verifies: insolation range; the sub-solar hemisphere is lit and the night side dark;
// the solar declination tracks axialTilt (poles lit/dark at solstice, neutral at equinox);
// live temperature stays within the summer/winter band and is anti-phased across the
// hemispheres; the snow line advances in the winter hemisphere; and determinism.
#include "Planet.hpp"
#include "Projection.hpp"
#include <cstdio>
#include <cmath>
#include <algorithm>
static int failures = 0;
static void check(bool cond, const char* what) {
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
if (!cond) ++failures;
}
// Index of the cell whose unit direction is closest to dir.
static int nearestCell(const Planet& p, const Vec3& dir) {
int best = 0; double bd = -2.0;
for (int i = 0; i < (int)p.cells.size(); ++i) {
double d = p.cells[i].unit.dot(dir);
if (d > bd) { bd = d; best = i; }
}
return best;
}
int main() {
Planet planet;
PlanetConfig cfg;
cfg.subdivisions = 5;
cfg.seed = 1337;
planet.generate(cfg);
planet.computeClimate(); // live season needs the climate fields
const int n = (int)planet.cells.size();
// North/south pole cells (extreme latitude) for declination checks.
int north = 0, south = 0;
for (int i = 0; i < n; ++i) {
if (planet.cells[i].unit.y > planet.cells[north].unit.y) north = i;
if (planet.cells[i].unit.y < planet.cells[south].unit.y) south = i;
}
const double tilt = cfg.axialTilt * M_PI / 180.0;
std::printf("Live World: insolation\n");
// --- Range + lit/dark hemispheres (arbitrary time, summer solstice) -------
planet.computeInsolation(0.25, 0.30);
const std::vector<double>& sun = planet.insolation();
bool inRange = true; int lit = 0; double mx = 0.0;
for (int i = 0; i < n; ++i) {
if (sun[i] < 0.0 || sun[i] > 1.0) inRange = false;
if (sun[i] > 0.0) ++lit;
mx = std::max(mx, sun[i]);
}
check(inRange, "insolation in [0,1]");
check(lit > (int)(0.40 * n) && lit < (int)(0.60 * n), "about half the planet is in daylight");
// Sub-solar cell is fully lit; its antipode is dark.
double decl = tilt * std::sin(2.0 * M_PI * 0.25);
double lon = M_PI * (1.0 - 2.0 * 0.30);
Vec3 sd = lonLatToDir(lon, decl);
int subsolar = nearestCell(planet, sd);
int antipode = nearestCell(planet, sd * -1.0);
check(sun[subsolar] > 0.99, "sub-solar cell is fully lit");
check(sun[antipode] == 0.0, "antipodal (midnight) cell is dark");
std::printf("Live World: declination tracks axial tilt\n");
// Equinox (decl=0): both poles near the terminator (~0). Solstice: summer pole lit,
// winter pole in polar night.
planet.computeInsolation(0.0, 0.0); // equinox
double npEq = planet.insolation()[north], spEq = planet.insolation()[south];
planet.computeInsolation(0.25, 0.0); // northern summer solstice
double npSol = planet.insolation()[north], spSol = planet.insolation()[south];
check(npEq < 0.1 && spEq < 0.1, "equinox: both poles near the terminator");
check(npSol > 0.3, "summer solstice: summer pole sees the midnight sun");
check(spSol == 0.0, "summer solstice: winter pole is in polar night");
check(std::fabs(npSol - std::sin(tilt)) < 0.05, "polar insolation ~ sin(axialTilt)");
std::printf("Live World: seasonal temperature\n");
// At northern summer (doy=0.25) live temp = summer in the north, winter in the south;
// everywhere it stays within the [winter, summer] band.
planet.computeLiveSeason(0.25);
const std::vector<double>& lt = planet.liveTemp();
const std::vector<double>& summ = planet.summerTemp();
const std::vector<double>& wint = planet.winterTemp();
bool inBand = true; double nLiveSummer = 0.0; int nN = 0;
for (int i = 0; i < n; ++i) {
if (lt[i] < wint[i] - 1e-6 || lt[i] > summ[i] + 1e-6) inBand = false;
if (planet.cells[i].unit.y > 0.3) { nLiveSummer += lt[i]; ++nN; }
}
nLiveSummer /= std::max(1, nN);
check(inBand, "live temp within [winter, summer] for every cell");
check(std::fabs(lt[north] - summ[north]) < 0.5, "northern summer solstice -> north at its summer temp");
check(std::fabs(lt[south] - wint[south]) < 0.5, "northern summer solstice -> south at its winter temp");
// Half a year later the northern hemisphere is colder (anti-phase).
planet.computeLiveSeason(0.75);
double nLiveWinter = 0.0; nN = 0;
for (int i = 0; i < n; ++i)
if (planet.cells[i].unit.y > 0.3) { nLiveWinter += planet.liveTemp()[i]; ++nN; }
nLiveWinter /= std::max(1, nN);
check(nLiveWinter < nLiveSummer - 1.0, "northern hemisphere colder in its winter than its summer");
std::printf("Live World: snow line advances in winter\n");
auto snowCountNorth = [&](double doy) {
planet.computeLiveSeason(doy); const std::vector<double>& t = planet.liveTemp();
int c = 0;
for (int i = 0; i < n; ++i)
if (planet.cells[i].unit.y > 0.0 && planet.cells[i].elevation > cfg.seaLevel
&& t[i] < cfg.snowTemp) ++c;
return c;
};
int snowSummer = snowCountNorth(0.25), snowWinter = snowCountNorth(0.75);
std::printf(" north land snow cells: summer %d, winter %d\n", snowSummer, snowWinter);
check(snowWinter > snowSummer, "more northern land under snow in winter than summer");
std::printf("Live World: determinism\n");
Planet p2; p2.generate(cfg); p2.computeClimate();
p2.computeInsolation(0.37, 0.61); p2.computeLiveSeason(0.37);
planet.computeInsolation(0.37, 0.61); planet.computeLiveSeason(0.37);
bool same = true;
for (int i = 0; i < n; ++i)
if (p2.insolation()[i] != planet.insolation()[i] || p2.liveTemp()[i] != planet.liveTemp()[i])
same = false;
check(same, "same seed + time -> identical insolation & live temp");
std::printf(failures ? "\nSOME LIVE CHECKS FAILED (%d)\n" : "\nALL LIVE CHECKS PASSED\n", failures);
return failures ? 1 : 0;
}