// Headless test for civilization Step 4 (cultures, beliefs & governments). No display needed. // // g++ -std=c++17 -O2 -Isrc/sim test_culture.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/PlanetCulture.cpp src/sim/PlanetIO.cpp -o /tmp/tc && /tmp/tc // // Verifies: one culture per inhabited continent; every living settlement has a culture; valid // ethos/faith; governments plausible per tier + reflected in the realm name; realms are mono-cultural; // determinism + RNG isolation; save->load->recompute parity. #include "Planet.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); if (k >= iters/2) p.hydrology(dt*0.2); } p.computeClimate(); p.classifyBiomes(); } // Grouping key matching computeCultures: continent if geography is present, else a per-bank fallback. static int cultKey(const Settlement& s) { return (s.regionId >= 0) ? s.regionId : (-1 - s.bank); } 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 abP = p.cfg.civAbandonPop, yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays; p.placeSettlements(); double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); } std::printf("Culture: extraction\n"); p.computeTerritory(); p.computeCultures(); check(!p.cultureList().empty(), "computeCultures produces cultures"); check((int)p.cellCulture().size() == n && (int)p.settleCulture().size() == (int)p.settlements.size(), "index arrays sized"); std::printf("Culture: one culture per inhabited continent\n"); { // The set of distinct grouping keys among living settlements == the number of cultures, and // every living settlement's key maps to exactly one culture (and vice versa). std::set keys; std::map keyToCult; // grouping key -> culture index (must be consistent) bool consistent = true, allLiving = true; for (size_t k = 0; k < p.settlements.size(); ++k) { const Settlement& s = p.settlements[k]; if (s.population < abP) continue; int ci = p.settleCulture()[k]; if (ci < 0 || ci >= (int)p.cultureList().size()) { allLiving = false; continue; } int key = cultKey(s); keys.insert(key); auto it = keyToCult.find(key); if (it == keyToCult.end()) keyToCult[key] = ci; else if (it->second != ci) consistent = false; // same continent -> must be same culture } // No two distinct keys share a culture. std::set usedCults; bool distinct = true; for (auto& kv : keyToCult) { if (usedCults.count(kv.second)) distinct = false; usedCults.insert(kv.second); } std::printf(" %d cultures, %d inhabited continents/keys\n", (int)p.cultureList().size(), (int)keys.size()); check(allLiving, "every living settlement has a valid culture"); check(consistent, "settlements on the same continent share one culture"); check(distinct, "no two continents share a culture"); check((int)p.cultureList().size() == (int)keys.size(), "exactly one culture per inhabited continent"); } std::printf("Culture: valid ethos / faith / members\n"); { bool ethosOk = true, faithOk = true, membersOk = true, namesOk = true; for (const Culture& c : p.cultureList()) { if ((int)c.ethos < 0 || (int)c.ethos > 5) ethosOk = false; if ((int)c.faith < 0 || (int)c.faith > 7) faithOk = false; if (c.members < 1 || c.totalPop <= 0.0) membersOk = false; if (c.name.empty() || c.faithName.empty()) namesOk = false; } check(ethosOk, "every culture ethos is a valid enum"); check(faithOk, "every culture faith is a valid enum"); check(membersOk, "every culture has >=1 member and positive population"); check(namesOk, "every culture has a people name + a faith name"); } std::printf("Culture: governments plausible per tier + reflected in realm name\n"); { bool govOk = true, nameOk = true, cultOk = true; for (const Nation& nat : p.nationList()) { bool tierOk = (nat.tier == NationTier::CityState && (nat.gov == GovType::CityRepublic || nat.gov == GovType::Tribe || nat.gov == GovType::Theocracy)) || (nat.tier == NationTier::Kingdom && (nat.gov == GovType::Kingdom || nat.gov == GovType::Duchy || nat.gov == GovType::Theocracy)) || (nat.tier == NationTier::Empire && (nat.gov == GovType::Empire || nat.gov == GovType::Autocracy || nat.gov == GovType::Confederation)); if (!tierOk) govOk = false; // Rebuild the expected government-aware name and compare. const std::string& cap = p.settlements[nat.capital].name; std::string exp; switch (nat.gov) { case GovType::Tribe: exp = "Chiefdom of " + cap; break; case GovType::CityRepublic: exp = "Republic of " + cap; break; case GovType::Duchy: exp = "Duchy of " + cap; break; case GovType::Kingdom: exp = "Kingdom of " + cap; break; case GovType::Theocracy: exp = cap + " Theocracy"; break; case GovType::Confederation: exp = cap + " Confederation"; break; case GovType::Empire: exp = cap + " Empire"; break; case GovType::Autocracy: exp = cap + " Dominion"; break; } if (nat.name != exp) nameOk = false; if (nat.cultureId < 0 || nat.cultureId >= (int)p.cultureList().size()) cultOk = false; } check(govOk, "each realm's government fits its tier"); check(nameOk, "each realm name reflects its government"); check(cultOk, "each realm is tagged with a valid culture"); } std::printf("Culture: realms are mono-cultural (no realm spans two continents)\n"); { std::map nationCulture; // nation index -> the single culture of its members bool mono = true; for (size_t k = 0; k < p.settlements.size(); ++k) { int ni = p.settleNation()[k]; if (ni < 0) continue; int ci = p.settleCulture()[k]; auto it = nationCulture.find(ni); if (it == nationCulture.end()) nationCulture[ni] = ci; else if (it->second != ci) mono = false; } check(mono, "every settlement in a realm shares one culture"); } std::printf("Culture: 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(); q.computeCultures(); { bool same = (q.cellCulture() == p.cellCulture()) && (q.cultureList().size() == p.cultureList().size()); if (same) for (size_t i = 0; i < q.cultureList().size(); ++i) if (q.cultureList()[i].name != p.cultureList()[i].name || q.cultureList()[i].ethos != p.cultureList()[i].ethos || q.cultureList()[i].faith != p.cultureList()[i].faith) same = false; check(same, "computeCultures is deterministic"); } std::printf("Culture: 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(); y.computeCultures(); } } { 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, "computeCultures never perturbs tectonic evolution"); } std::printf("Culture: 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(); r.computeCultures(); // cultures are derived, not saved check(r.cellCulture() == p.cellCulture(), "culture recomputed after load matches (derived, not saved)"); } std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CULTURE CHECKS PASSED\n", failures); return failures ? 1 : 0; }