// Headless test for civilization Step 2 (habitability + settlements). No display needed. // // g++ -std=c++17 -O2 -Isrc/sim test_civ.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/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp \ // src/sim/PlanetIO.cpp -o /tmp/tc && /tmp/tc // // Verifies: habitability range/zeros; placement spacing/cap/land + unique names; food-driven growth // and decline; tiers; determinism + RNG isolation; population snapshot round-trip; v20 save; reseed clear. #include "Planet.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); if (k >= iters/2) p.hydrology(dt*0.2); } p.computeClimate(); p.classifyBiomes(); } int main() { PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 4242; Planet p; p.generate(cfg); settle(p); drift(p, 400); const int n = (int)p.cells.size(); const double sea = p.cfg.seaLevel; const double yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays; std::printf("Civ: habitability field\n"); p.computeHabitability(); const auto& H = p.habitability(); check((int)H.size() == n, "habitability sized n"); bool ranged = true, zeroWaterIce = true, anyHabitable = false; for (int i = 0; i < n; ++i) { if (!(std::isfinite(H[i]) && H[i] >= 0.0 && H[i] <= 1.0)) ranged = false; if ((p.cells[i].elevation <= sea || p.cells[i].biome == Biome::Ice) && H[i] != 0.0) zeroWaterIce = false; if (H[i] > 0.3) anyHabitable = true; } check(ranged, "habitability in [0,1]"); check(zeroWaterIce, "habitability 0 on ocean/ice"); check(anyHabitable, "some land is habitable"); std::printf("Civ: placement\n"); p.placeSettlements(); const auto& S = p.settlements; check(!S.empty(), "settlements placed"); bool onLand = true, aboveMin = true, capOk = (int)S.size() <= p.cfg.civMaxSettlements; std::set names; bool uniqueNames = true; const double sepCos = std::cos(p.cfg.civMinSpacingRadians); bool spaced = true; for (size_t a = 0; a < S.size(); ++a) { if (p.cells[S[a].cell].elevation <= sea) onLand = false; if (p.habitability()[S[a].cell] < p.cfg.civMinHabitability - 1e-9) aboveMin = false; if (!names.insert(S[a].name).second || S[a].name.empty()) uniqueNames = false; for (size_t b = a + 1; b < S.size(); ++b) if (p.cells[S[a].cell].unit.dot(p.cells[S[b].cell].unit) > sepCos + 1e-9) spaced = false; if (p.cellSettlement()[S[a].cell] != (int)a) onLand = false; // index consistency } std::printf(" %d settlements\n", (int)S.size()); check(onLand, "settlements sit on land + cellSettlement index is consistent"); check(aboveMin, "settlements only on cells >= civMinHabitability"); check(spaced, "settlements respect the minimum spacing"); check(capOk, "settlement count within the cap"); check(uniqueNames, "settlement names are unique + non-empty"); std::printf("Civ: food-driven growth + decline\n"); { int gi = 0; for (size_t k = 0; k < S.size(); ++k) if (p.habitability()[S[k].cell] > p.habitability()[S[gi].cell]) gi = (int)k; double p0 = p.settlements[gi].population; for (int k = 0; k < 400; ++k) p.stepCivilization(5.0 * yearH); // ~2000 yr of small steps check(p.settlements[gi].population > p0 * 2.0, "a high-habitability settlement grows"); // Decline: push one well over its carrying capacity, then step -> it shrinks. p.settlements[gi].population = 5.0e7; double over = p.settlements[gi].population; for (int k = 0; k < 400; ++k) p.stepCivilization(5.0 * yearH); check(p.settlements[gi].population < over, "an over-capacity settlement declines toward its food limit"); } std::printf("Civ: tiers\n"); check(settleTierOf(100.0, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::Village && settleTierOf(p.cfg.civTownPop, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::Town && settleTierOf(p.cfg.civCityPop, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::City, "tier thresholds (village/town/city)"); std::printf("Civ: determinism\n"); Planet q; q.generate(cfg); settle(q); drift(q, 400); q.placeSettlements(); bool same = (q.settlements.size() == S.size()); if (same) for (size_t k = 0; k < S.size(); ++k) if (q.settlements[k].cell != p.settlements[k].cell || q.settlements[k].name != p.settlements[k].name) { same = false; break; } check(same, "placeSettlements is deterministic"); std::printf("Civ: 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 dx = x.cflDtMy(); x.advect(dx); x.step(); x.erode(dx); double dy = y.cflDtMy(); y.advect(dy); y.step(); y.erode(dy); if (k == 20) { y.placeSettlements(); y.stepCivilization(yearH); } } 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, "placeSettlements/stepCivilization never perturb tectonic evolution"); std::printf("Civ: population snapshot round-trip\n"); { WeatherSnapshot snap = p.captureWeather(); for (auto& st : p.settlements) st.population = 12345.0; p.restoreWeather(snap); bool restored = true; for (size_t k = 0; k < p.settlements.size(); ++k) if (std::fabs(p.settlements[k].population - snap.settlementPop[k]) > 1e-9) restored = false; check(snap.settlementPop.size() == p.settlements.size() && restored, "captureWeather/restoreWeather round-trips populations"); } std::printf("Civ: save v20 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, true, true, true); check(ok, "readState accepts a v20 stream"); bool match = (r.settlements.size() == p.settlements.size()); if (match) for (size_t k = 0; k < p.settlements.size(); ++k) if (r.settlements[k].cell != p.settlements[k].cell || r.settlements[k].name != p.settlements[k].name || std::fabs(r.settlements[k].population - p.settlements[k].population) > 1e-6) { match = false; break; } check(match, "settlements round-trip through save"); check(r.cellSettlement() == p.cellSettlement(), "cellSettlement index rebuilt on load"); } std::printf("Civ: reseed clears settlements\n"); p.generate(cfg); check(p.settlements.empty() && (p.cellSettlement().empty() || p.cellSettlement()[0] == -1), "reseed clears the settlement set"); std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CIV CHECKS PASSED\n", failures); return failures ? 1 : 0; }