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>
132 lines
5.7 KiB
C++
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;
|
|
}
|