// 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 geography save round-trip. #include "Planet.hpp" #include "NameGen.hpp" #include #include #include #include #include #include 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 kinds; std::vector anchors; std::vector 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 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 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 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 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 geography 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; }