planetsim/test_culture.cpp

185 lines
9.7 KiB
C++

// 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 <cstdio>
#include <cmath>
#include <algorithm>
#include <set>
#include <map>
#include <sstream>
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<int> keys;
std::map<int,int> 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<int> 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<int,int> 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;
}