planetsim/test_ecoregions.cpp
Jonas Reith 2351fb79a5 Ecoregions atlas + Civilizations Step 2 (habitability & settlements, save v20)
Two features (the ecoregions layer was authored locally and was still
uncommitted; civilization Step 2 builds on top and is intermingled in shared
files, so they land together):

Ecoregions (v19, PlanetEcoregions.*, key E):
- generateEcoregions() flood-fills cells sharing biome + land/water context +
  productivity band into named ecological provinces (dominant flora/fauna/funga
  + per-kind productivity), a separate sEcoRng. ColorMode::Ecoregion + an Eco
  tab + cell-info dominants. Saved v19 (v18 geography reshuffle salt already in).

Civilizations Step 2 (v20, PlanetCiv.*, keys U/I):
- computeHabitability(): derived per-cell food/livability (climate comfort +
  water access (rivers/lakes/coast) + food (flora/fauna + ecoregion
  productivity), gated by freezing winters / high terrain). ColorMode::
  Habitability (key I).
- placeSettlements() (key U, "the dawn"): one-time greedy placement on the best
  well-spaced fertile cells (separate sCivRng; named from the continent's
  NameGen bank). The set is fixed, so the only mutable per-step state is each
  settlement's population.
- stepCivilization(): logistic growth toward K = civMaxPopulation*habitability,
  cut where an active volcano ashes the area, so settlements grow / decline /
  are abandoned (floored at 1 so a site can revive). Tiers village->town->city.
  Runs in liveAdvance; detectLiveEvents logs kind=3 events.
- Step-back snapshots only the population vector (WeatherSnapshot.settlementPop).
  3D + 2D tier-sized markers + city/town labels, a Civ tab, cell-info line.
  buildGeometry() clears settlements on reseed. Save v20; sCellSettlement
  rebuilt on load. civ* config knobs.

New test_ecoregions.cpp + test_civ.cpp; all 10 headless suites pass; GUI build
clean. CLAUDE.md / design-notes / BUILD.md updated (roadmap Step 2 done).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:01:43 +02:00

132 lines
5.7 KiB
C++

// Headless test for the ecoregion atlas. No display needed.
#include "Planet.hpp"
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <set>
#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 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();
p.computeBiotaDensity();
}
static bool sameBiota(const std::vector<CellBiota>& a, const std::vector<CellBiota>& b) {
if (a.size() != b.size()) return false;
auto eq = [](const std::vector<Organism>& x, const std::vector<Organism>& y) {
if (x.size() != y.size()) return false;
for (size_t k = 0; k < x.size(); ++k)
if (x[k].archetype != y[k].archetype || x[k].biome != y[k].biome) return false;
return true;
};
for (size_t i = 0; i < a.size(); ++i)
if (!eq(a[i].flora, b[i].flora) || !eq(a[i].fauna, b[i].fauna) || !eq(a[i].funga, b[i].funga))
return false;
return true;
}
int main() {
PlanetConfig cfg; cfg.seed = 9191; cfg.subdivisions = 5;
Planet p; p.generate(cfg); settle(p);
const int n = (int)p.cells.size();
std::printf("Ecoregions: extraction\n");
p.generateEcoregions();
const auto& E = p.ecoregions();
const auto& ce = p.cellEcoregion();
check(!E.empty(), "generateEcoregions produces regions");
check((int)ce.size() == n, "per-cell ecoregion array sized n");
bool mapped = true, indexOk = true, anchorsOk = true, namesOk = true, domOk = true;
std::set<std::string> names;
for (int i = 0; i < n; ++i) {
if (p.cells[i].biome != Biome::Ice && ce[i] < 0) mapped = false;
if (ce[i] >= (int)E.size()) indexOk = false;
}
for (const Ecoregion& e : E) {
if (e.name.empty() || !names.insert(e.name).second) namesOk = false;
if (e.anchorCell < 0 || e.anchorCell >= n || p.cells[e.anchorCell].biome != e.biome
|| e.biome == Biome::Ice) anchorsOk = false;
auto validArch = [](int a) { return a == -1 || (a >= 0 && a < (int)biotaArchetypes().size()); };
if (!validArch(e.dominantFlora) || !validArch(e.dominantFauna) || !validArch(e.dominantFunga)) domOk = false;
if (!std::isfinite(e.floraProductivity) || !std::isfinite(e.faunaProductivity)
|| !std::isfinite(e.fungaProductivity)) domOk = false;
}
check(mapped, "every non-ice cell maps to an ecoregion");
check(indexOk, "per-cell ecoregion indices are valid");
check(anchorsOk, "ecoregion anchors match their biome");
check(namesOk, "ecoregion names are unique and non-empty");
check(domOk, "dominants/productivity are valid");
std::printf("Ecoregions: determinism\n");
Planet q; q.generate(cfg); settle(q); q.generateEcoregions();
bool same = (q.ecoregions().size() == E.size()) && (q.cellEcoregion() == ce);
if (same)
for (size_t i = 0; i < E.size(); ++i) {
const Ecoregion& a = E[i]; const Ecoregion& b = q.ecoregions()[i];
if (a.name != b.name || a.biome != b.biome || a.anchorCell != b.anchorCell
|| a.size != b.size || a.dominantFlora != b.dominantFlora
|| a.dominantFauna != b.dominantFauna || a.dominantFunga != b.dominantFunga) {
same = false; break;
}
}
check(same, "same seed produces identical ecoregions");
std::printf("Ecoregions: RNG isolation\n");
{
Planet a; a.generate(cfg); settle(a); a.drifting = true;
Planet b; b.generate(cfg); settle(b); b.drifting = true; b.generateEcoregions();
double dt = a.cflDtMy();
for (int k = 0; k < 5; ++k) { a.advect(dt); a.step(); a.erode(dt);
b.advect(dt); b.step(); b.erode(dt); }
bool terrainSame = a.cells.size() == b.cells.size();
for (size_t i = 0; terrainSame && i < a.cells.size(); ++i)
if (a.cells[i].elevation != b.cells[i].elevation || a.cells[i].plateId != b.cells[i].plateId)
terrainSame = false;
check(terrainSame, "ecoregions do not perturb tectonic evolution");
}
{
Planet a; a.generate(cfg); settle(a); a.generateBiota();
Planet b; b.generate(cfg); settle(b); b.generateEcoregions(); b.generateBiota();
check(a.biotaPopulated() && b.biotaPopulated() && sameBiota(a.biota(), b.biota()),
"ecoregions do not perturb biota generation");
}
std::printf("Ecoregions: save v19 round-trip\n");
{
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
p.writeState(ss);
Planet r;
bool ok = r.readState(ss);
check(ok, "readState accepts a v19 stream");
bool match = (r.ecoregions().size() == E.size()) && (r.cellEcoregion() == ce);
if (match)
for (size_t i = 0; i < E.size(); ++i)
if (r.ecoregions()[i].name != E[i].name || r.ecoregions()[i].biome != E[i].biome
|| r.ecoregions()[i].anchorCell != E[i].anchorCell) { match = false; break; }
check(match, "ecoregions round-trip through save");
ss.clear(); ss.seekg(0);
Planet old;
bool okOld = old.readState(ss, true, true, true, true, true, true, true, true, true, false);
check(okOld && old.ecoregions().empty(), "pre-v19 read leaves ecoregions empty");
}
std::printf("\n%s (%d failure%s)\n", failures ? "FAILURES" : "ALL ECOREGION CHECKS PASSED",
failures, failures == 1 ? "" : "s");
return failures ? 1 : 0;
}