// Headless test for civilization Step 3 (territory & nations / realms). No display needed. // // g++ -std=c++17 -O2 -Isrc/sim test_nation.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/PlanetNation.cpp src/sim/PlanetIO.cpp -o /tmp/tn && /tmp/tn // // Verifies: territory ownership + wilderness; bigger cities own more; realm grouping (kingdom vs // city-state); tiers; ocean/ice unowned; determinism + RNG isolation; save->load->recompute parity. #include "Planet.hpp" #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(); } static int ownedCells(const Planet& p, int nationIdx) { int c = 0; for (int v : p.cellNation()) if (v == nationIdx) ++c; return c; } 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, yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays; p.placeSettlements(); // run civilization a while so populations diverge (capitals, towns) double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); } std::printf("Territory: extraction\n"); p.computeTerritory(); check(!p.nationList().empty(), "computeTerritory produces nations"); check((int)p.cellNation().size() == n && (int)p.settleNation().size() == (int)p.settlements.size(), "index arrays sized"); bool seaUnowned = true, ownedIsLand = true, idxOk = true, settleMatch = true; int ownedLand = 0; for (int i = 0; i < n; ++i) { int ni = p.cellNation()[i]; if (ni >= (int)p.nationList().size()) idxOk = false; if (ni >= 0) { ++ownedLand; if (p.cells[i].elevation <= sea || p.cells[i].biome == Biome::Ice) ownedIsLand = false; } if ((p.cells[i].elevation <= sea) && ni >= 0) seaUnowned = false; } // every living settlement's home cell belongs to its own nation for (size_t k = 0; k < p.settlements.size(); ++k) { if (p.settlements[k].population < p.cfg.civAbandonPop) continue; int ni = p.settleNation()[k]; if (ni < 0 || p.cellNation()[p.settlements[k].cell] != ni) settleMatch = false; } std::printf(" %d nations, %d owned land cells of %d\n", (int)p.nationList().size(), ownedLand, n); check(idxOk, "per-cell nation indices in range"); check(seaUnowned, "ocean cells are unowned (wilderness)"); check(ownedIsLand, "owned cells are land + non-ice"); check(ownedLand > 0 && ownedLand < n, "some land is owned, some is wilderness (influence-limited)"); check(settleMatch, "a living settlement's home cell belongs to its own nation"); std::printf("Territory: bigger realms control more land\n"); { double empCells = 0, csCells = 0; int empN = 0, csN = 0; for (size_t ni = 0; ni < p.nationList().size(); ++ni) { int cells = ownedCells(p, (int)ni); if (p.nationList()[ni].tier == NationTier::Empire) { empCells += cells; ++empN; } else if (p.nationList()[ni].tier == NationTier::CityState) { csCells += cells; ++csN; } } double empAvg = empN ? empCells / empN : 0.0, csAvg = csN ? csCells / csN : 0.0; std::printf(" empire avg %.1f cells (%d) ; city-state avg %.1f cells (%d)\n", empAvg, empN, csAvg, csN); check(empN == 0 || csN == 0 || empAvg > csAvg, "empires control more territory on average than city-states"); } std::printf("Territory: realms (kingdoms / city-states / empires)\n"); { int kingdoms = 0, cityStates = 0, empires = 0, multiMember = 0; for (const Nation& nat : p.nationList()) { if (nat.tier == NationTier::Empire) ++empires; else if (nat.tier == NationTier::Kingdom) ++kingdoms; else ++cityStates; if (nat.members > 1) ++multiMember; // a kingdom/empire name references its capital; city-states are bare bool ok = !nat.name.empty(); if (nat.tier == NationTier::Kingdom && nat.name.rfind("Kingdom of ", 0) != 0) ok = false; check(ok || nat.tier == NationTier::CityState || nat.tier == NationTier::Empire, "nation has a sensible name"); } std::printf(" %d empires, %d kingdoms, %d city-states (%d multi-settlement realms)\n", empires, kingdoms, cityStates, multiMember); check(multiMember >= 1, "at least one realm groups multiple settlements (a kingdom forms)"); } std::printf("Territory: determinism\n"); Planet q; q.generate(cfg); settle(q); drift(q, 400); q.placeSettlements(); double lt2 = 0.0; for (int yr = 0; yr < 600; ++yr) { lt2 += 2.0 * yearH; q.stepCivilization(2.0 * yearH, lt2); } q.computeTerritory(); bool same = (q.cellNation() == p.cellNation()) && (q.nationList().size() == p.nationList().size()); check(same, "computeTerritory is deterministic"); std::printf("Territory: 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, yearH); y.computeTerritory(); } } 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, "computeTerritory never perturbs tectonic evolution"); std::printf("Territory: save -> load -> recompute parity\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 the v20 stream"); r.computeTerritory(); // territory is derived, not saved -- recompute on both sides must match check(r.cellNation() == p.cellNation(), "territory recomputed after load matches (derived, not saved)"); } std::printf(failures ? "\nFAILURES: %d\n" : "\nALL NATION CHECKS PASSED\n", failures); return failures ? 1 : 0; }