272 lines
14 KiB
C++
272 lines
14 KiB
C++
// Headless test for the geography / atlas stage (named feature extraction + naming). No display.
|
|
//
|
|
// g++ -std=c++17 -O2 -Isrc/sim test_geography.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/NameGen.cpp \
|
|
// src/sim/PlanetGeography.cpp src/sim/PlanetIO.cpp -o /tmp/tg && /tmp/tg
|
|
//
|
|
// Verifies: extraction (continents/oceans/ranges/rivers/lakes), per-cell membership consistency,
|
|
// names non-empty/unique/deterministic, RNG isolation from tectonics, and a v18 save round-trip.
|
|
|
|
#include "Planet.hpp"
|
|
#include "NameGen.hpp"
|
|
#include <cstdio>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
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();
|
|
}
|
|
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); }
|
|
}
|
|
|
|
static int countKind(const Planet& p, FeatureKind k) {
|
|
int c = 0; for (const GeoFeature& f : p.geography()) if (f.kind == k) ++c; return c;
|
|
}
|
|
|
|
int main() {
|
|
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 7777;
|
|
Planet p; p.generate(cfg); settle(p); drift(p, 120);
|
|
const int n = (int)p.cells.size();
|
|
const double sea = p.cfg.seaLevel;
|
|
|
|
std::printf("Geography: NameGen\n");
|
|
std::string a1 = namegen::makeName(123, 0), a2 = namegen::makeName(123, 0);
|
|
check(!a1.empty() && a1 == a2, "makeName deterministic + non-empty");
|
|
check(namegen::makeName(124, 0) != a1, "different seeds give different names (usually)");
|
|
check(namegen::bankForRegion(cfg.seed, 5) == namegen::bankForRegion(cfg.seed, 5), "bankForRegion deterministic");
|
|
|
|
std::printf("Geography: extraction\n");
|
|
p.generateGeography();
|
|
const auto& F = p.geography();
|
|
check(!F.empty(), "generateGeography produces features");
|
|
int continents = countKind(p, FeatureKind::Continent), islands = countKind(p, FeatureKind::Island);
|
|
int oceans = countKind(p, FeatureKind::Ocean), seas = countKind(p, FeatureKind::Sea);
|
|
int ranges = countKind(p, FeatureKind::MountainRange), peaks = countKind(p, FeatureKind::Peak);
|
|
int rivers = countKind(p, FeatureKind::River), lakes = countKind(p, FeatureKind::Lake);
|
|
std::printf(" continents %d islands %d | oceans %d seas %d | ranges %d peaks %d | rivers %d lakes %d\n",
|
|
continents, islands, oceans, seas, ranges, peaks, rivers, lakes);
|
|
check(continents + islands >= 1, "at least one land mass");
|
|
check(oceans >= 1, "at least one ocean");
|
|
|
|
std::printf("Geography: reshuffle rechecks atlas with alternate names\n");
|
|
{
|
|
Planet r; r.generate(cfg); settle(r); drift(r, 40); r.generateGeography();
|
|
std::vector<FeatureKind> kinds;
|
|
std::vector<int> anchors;
|
|
std::vector<std::string> names;
|
|
for (const GeoFeature& f : r.geography()) {
|
|
kinds.push_back(f.kind);
|
|
anchors.push_back(f.anchorCell);
|
|
names.push_back(f.name);
|
|
}
|
|
r.reshuffleGeography();
|
|
bool sameFeatures = r.geography().size() == names.size();
|
|
bool renamed = false;
|
|
if (sameFeatures) {
|
|
for (size_t i = 0; i < names.size(); ++i) {
|
|
const GeoFeature& f = r.geography()[i];
|
|
if (f.kind != kinds[i] || f.anchorCell != anchors[i]) sameFeatures = false;
|
|
if (f.name != names[i]) renamed = true;
|
|
}
|
|
}
|
|
check(sameFeatures, "reshuffle rebuilds the same current-terrain features");
|
|
check(renamed, "reshuffle uses alternate place names");
|
|
check(countKind(r, FeatureKind::Ocean) >= 1, "reshuffle rechecks oceans");
|
|
|
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
|
r.writeState(ss);
|
|
Planet loaded;
|
|
check(loaded.readState(ss), "reshuffled atlas save/load preserves v18 salt");
|
|
std::vector<std::string> loadedNames;
|
|
for (const GeoFeature& f : loaded.geography()) loadedNames.push_back(f.name);
|
|
loaded.reshuffleGeography();
|
|
bool advancedAfterLoad = loaded.geography().size() == loadedNames.size();
|
|
if (advancedAfterLoad) {
|
|
bool anyChanged = false;
|
|
for (size_t i = 0; i < loadedNames.size(); ++i)
|
|
if (loaded.geography()[i].name != loadedNames[i]) anyChanged = true;
|
|
advancedAfterLoad = anyChanged;
|
|
}
|
|
check(advancedAfterLoad, "reshuffle continues with new names after load");
|
|
}
|
|
|
|
std::printf("Geography: rebuild clears stale atlas state\n");
|
|
{
|
|
Planet r; r.generate(cfg); settle(r); drift(r, 40); r.generateGeography();
|
|
check(!r.geography().empty(), "regression setup builds an atlas");
|
|
Volcano stale{}; stale.cell = 0; r.volcanoes.push_back(stale);
|
|
PlanetConfig next = cfg; next.seed = cfg.seed + 1;
|
|
r.generate(next);
|
|
bool landEmpty = !r.cellLand().empty()
|
|
&& std::all_of(r.cellLand().begin(), r.cellLand().end(), [](int v) { return v == -1; });
|
|
bool waterEmpty = !r.cellWater().empty()
|
|
&& std::all_of(r.cellWater().begin(), r.cellWater().end(), [](int v) { return v == -1; });
|
|
bool rangeEmpty = !r.cellRange().empty()
|
|
&& std::all_of(r.cellRange().begin(), r.cellRange().end(), [](int v) { return v == -1; });
|
|
bool riverEmpty = !r.cellRiver().empty()
|
|
&& std::all_of(r.cellRiver().begin(), r.cellRiver().end(), [](int v) { return v == -1; });
|
|
check(!r.geographyBuilt() && r.geography().empty(), "generate() clears the old atlas");
|
|
check(landEmpty && waterEmpty && rangeEmpty && riverEmpty, "generate() resets per-cell feature indices");
|
|
check(r.volcanoes.empty(), "generate() clears stale volcanoes");
|
|
}
|
|
|
|
std::printf("Geography: per-cell membership consistency\n");
|
|
const auto& land = p.cellLand(); const auto& water = p.cellWater();
|
|
const auto& range = p.cellRange();
|
|
check((int)land.size() == n && (int)water.size() == n, "index arrays sized n");
|
|
bool landOk = true, waterOk = true, rangeOk = true, idxOk = true;
|
|
for (int i = 0; i < n; ++i) {
|
|
bool isLand = p.cells[i].elevation > sea;
|
|
if (isLand && land[i] < 0) landOk = false; // every land cell has a continent/island
|
|
if (!isLand && land[i] >= 0) landOk = false; // ocean cells aren't a land feature
|
|
if (!isLand && water[i] < 0) waterOk = false; // every ocean cell has a water feature
|
|
if (range[i] >= 0 && !(p.cells[i].elevation > p.cfg.geoMountainElev)) rangeOk = false; // range cells are high
|
|
for (int v : {land[i], water[i], range[i]})
|
|
if (v >= (int)F.size()) idxOk = false;
|
|
}
|
|
check(landOk, "land cells map to a continent/island; ocean cells don't");
|
|
check(waterOk, "ocean cells map to an ocean/sea feature");
|
|
check(rangeOk, "mountain-range cells are all above geoMountainElev");
|
|
check(idxOk, "per-cell feature indices are in range");
|
|
|
|
std::printf("Geography: features anchor on the right terrain\n");
|
|
bool anchorsOk = true;
|
|
for (const GeoFeature& f : F) {
|
|
const Cell& c = p.cells[f.anchorCell];
|
|
bool landKind = (f.kind == FeatureKind::Continent || f.kind == FeatureKind::Island ||
|
|
f.kind == FeatureKind::MountainRange || f.kind == FeatureKind::Peak ||
|
|
f.kind == FeatureKind::River || f.kind == FeatureKind::Lake);
|
|
bool oceanKind = (f.kind == FeatureKind::Ocean || f.kind == FeatureKind::Sea);
|
|
if (landKind && c.elevation <= sea) anchorsOk = false;
|
|
if (oceanKind && c.elevation > sea) anchorsOk = false;
|
|
}
|
|
check(anchorsOk, "land features anchor on land, ocean features on water");
|
|
|
|
if (rivers > 0) {
|
|
std::printf("Geography: a named river traces downstream to a sink/ocean\n");
|
|
const auto& fl = p.flowTo(); int riverCell = -1;
|
|
for (int i = 0; i < n; ++i) if (p.cellRiver()[i] >= 0) { riverCell = i; break; }
|
|
bool reaches = false;
|
|
for (int cur = riverCell, guard = 0; cur >= 0 && guard < n; ++guard) {
|
|
int d = fl[cur];
|
|
if (d < 0 || p.cells[d].elevation <= sea) { reaches = true; break; }
|
|
cur = d;
|
|
}
|
|
check(riverCell >= 0 && reaches, "a river cell flows down to an ocean/sink");
|
|
}
|
|
|
|
std::printf("Geography: names non-empty + unique\n");
|
|
bool namesOk = true; std::set<std::string> seen;
|
|
for (const GeoFeature& f : F) {
|
|
if (f.name.empty()) namesOk = false;
|
|
if (!seen.insert(f.name).second) namesOk = false; // no duplicates
|
|
}
|
|
check(namesOk, "every feature has a unique non-empty name");
|
|
|
|
// Proper-noun uniqueness: strip the kind suffix/prefix and assert no two features share a root
|
|
// (so a continent "Karn", a "Karn River" and "Karn Mountains" can't coexist).
|
|
{
|
|
auto strip = [](const GeoFeature& f) -> std::string {
|
|
const std::string& s = f.name;
|
|
auto cut = [&](const std::string& suf) { return s.size() > suf.size() && s.compare(s.size() - suf.size(), suf.size(), suf) == 0 ? s.substr(0, s.size() - suf.size()) : s; };
|
|
switch (f.kind) {
|
|
case FeatureKind::Ocean: return cut(" Ocean");
|
|
case FeatureKind::Sea: return cut(" Sea");
|
|
case FeatureKind::MountainRange: return cut(" Mountains");
|
|
case FeatureKind::River: return cut(" River");
|
|
case FeatureKind::Lake: return s.rfind("Lake ", 0) == 0 ? s.substr(5) : s;
|
|
case FeatureKind::Peak: return s.rfind("Mount ", 0) == 0 ? s.substr(6) : s;
|
|
default: return s;
|
|
}
|
|
};
|
|
std::set<std::string> roots; bool rootsUnique = true;
|
|
for (const GeoFeature& f : F) if (!roots.insert(strip(f)).second) rootsUnique = false;
|
|
check(rootsUnique, "no two features share a proper-noun root");
|
|
}
|
|
|
|
std::printf("Geography: determinism (same seed -> identical atlas)\n");
|
|
Planet q; q.generate(cfg); settle(q); drift(q, 120); q.generateGeography();
|
|
bool sameAtlas = (q.geography().size() == F.size());
|
|
if (sameAtlas)
|
|
for (size_t i = 0; i < F.size(); ++i)
|
|
if (q.geography()[i].kind != F[i].kind || q.geography()[i].anchorCell != F[i].anchorCell
|
|
|| q.geography()[i].name != F[i].name) { sameAtlas = false; break; }
|
|
check(sameAtlas, "generateGeography is deterministic");
|
|
|
|
std::printf("Geography: RNG isolation from tectonics\n");
|
|
Planet x; x.generate(cfg); settle(x);
|
|
Planet y; y.generate(cfg); settle(y);
|
|
for (int k = 0; k < 40; ++k) {
|
|
double dtx = x.cflDtMy(); x.advect(dtx); x.step(); x.erode(dtx);
|
|
double dty = y.cflDtMy(); y.advect(dty); y.step(); y.erode(dty);
|
|
if (k == 20) y.generateGeography(); // must not touch the tectonic RNG stream
|
|
}
|
|
bool terrainSame = true;
|
|
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false;
|
|
check(terrainSame, "generateGeography never perturbs tectonic evolution");
|
|
|
|
std::printf("Geography: save v18 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, true, true, true, true, true, true, true, true);
|
|
check(ok, "readState accepts a v18 stream");
|
|
bool match = (r.geography().size() == F.size());
|
|
if (match)
|
|
for (size_t i = 0; i < F.size(); ++i)
|
|
if (r.geography()[i].name != F[i].name || r.geography()[i].kind != F[i].kind
|
|
|| r.geography()[i].anchorCell != F[i].anchorCell) { match = false; break; }
|
|
check(match, "geography round-trips through save");
|
|
check(r.cellLand() == p.cellLand() && r.cellRiver() == p.cellRiver(), "per-cell region arrays round-trip");
|
|
}
|
|
|
|
// Naming new land last -- it mutates p's atlas, so it must run after the determinism/save checks.
|
|
std::printf("Geography: naming a new (volcanic) island\n");
|
|
{
|
|
int oceanAdjLand = -1, oceanIsolated = -1;
|
|
for (int i = 0; i < n && (oceanAdjLand < 0 || oceanIsolated < 0); ++i) {
|
|
if (p.cells[i].elevation > sea) continue;
|
|
bool nearLand = false; for (int j : p.cells[i].neighbors) if (p.cells[j].elevation > sea) nearLand = true;
|
|
if (nearLand && oceanAdjLand < 0) oceanAdjLand = i;
|
|
if (!nearLand && oceanIsolated < 0) oceanIsolated = i;
|
|
}
|
|
if (oceanAdjLand >= 0) {
|
|
size_t before = p.geography().size();
|
|
std::string nm = p.nameNewLand(oceanAdjLand);
|
|
check(!nm.empty() && p.cellLand()[oceanAdjLand] >= 0, "coastal new land joins an adjacent named landmass");
|
|
check(p.geography().size() == before, "joining an existing landmass adds no new feature");
|
|
}
|
|
if (oceanIsolated >= 0) {
|
|
size_t b2 = p.geography().size();
|
|
std::string nm = p.nameNewLand(oceanIsolated);
|
|
check(!nm.empty() && p.geography().size() == b2 + 1, "an isolated new island mints a fresh feature");
|
|
check(p.geography().back().kind == FeatureKind::Island && p.cellLand()[oceanIsolated] >= 0, "the new island is a named Island feature");
|
|
}
|
|
}
|
|
|
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL GEOGRAPHY CHECKS PASSED\n", failures);
|
|
return failures ? 1 : 0;
|
|
}
|