// 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 v17 save round-trip. #include "Planet.hpp" #include "NameGen.hpp" #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: 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"); 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 v17 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 v17 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"); } std::printf(failures ? "\nFAILURES: %d\n" : "\nALL GEOGRAPHY CHECKS PASSED\n", failures); return failures ? 1 : 0; }