Reported after a long run left the world under one dominant culture -- the existing schism only fires for a geographically distant cluster (overseas colonies), so a single unbroken landmass had nothing to check one culture quietly absorbing the whole world via border conversion. Two new independent triggers, both refactored to share the existing schism machinery via a doSchism() helper: - Dominance schism: past cultDominanceShare (80%) of the world's living population, a per-year chance rising from cultDominanceProbMin (1%) to cultDominanceProbMax (99%) at 100% share that the culture fractures on its own -- a coin flip splits off half or a fifth of its own members, the ones farthest from its own population-weighted core. - War schism: a war between two realms sharing a culture has cultWarSchismChance (50%) odds, rolled once at declaration, that one whole side renounces the shared identity and becomes a new people. Both are pure hashes of (stable id, year, seed), so step-back replays them exactly, matching the rest of Step 8. Config is the self-describing text block, so no save-version bump. Dropped an initial validateConfig() cross-rule requiring cultDominanceProbMin <= cultDominanceProbMax after finding it would reject a very natural "disable via Max=0" edit (e.g. from the Main Menu) whenever Min was left at its default -- silently reverting a player's entire config to defaults on New World. The interpolation tolerates the reverse fine since the mechanic is already gated on Max > 0. test_cultevo.cpp gains two new scenarios (dominance split, war split) alongside the existing distant-cluster one; quietCulture() now also zeros the two new rates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
417 lines
23 KiB
C++
417 lines
23 KiB
C++
// Headless test for civilization Step 8 (cultural evolution). No display needed.
|
|
//
|
|
// g++ -std=c++17 -O2 -Isrc/sim test_cultevo.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/PlanetConflict.cpp
|
|
// src/sim/PlanetTrade.cpp src/sim/PlanetIO.cpp -o /tmp/tcev && /tmp/tcev
|
|
//
|
|
// Verifies: seeding parity (one culture per continent, frozen identities); colonies inherit the
|
|
// founder's culture; assimilation under foreign allegiance; border conversion (capitals exempt);
|
|
// three independent schism triggers -- a distant cluster becomes a new people, an overwhelmingly
|
|
// dominant culture splits on its own (half or a fifth of its members), a war between same-culture
|
|
// realms splits one side; determinism + RNG isolation; snapshot truncate-and-replay; save-v23
|
|
// round-trip + corrupt-stream rejection; pre-v23 compatibility.
|
|
|
|
#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();
|
|
}
|
|
static void quietCulture(Planet& p) { // no evolution: state should then never change
|
|
p.cfg.cultAssimRate = 0.0; p.cfg.cultConvertRate = 0.0; p.cfg.cultSchismRate = 0.0;
|
|
// Both, not just Max: the gate check only needs Max == 0, but leaving Min at its default (0.01)
|
|
// violates validateConfig()'s cultDominanceProbMin <= cultDominanceProbMax cross-rule.
|
|
p.cfg.cultDominanceProbMin = 0.0; p.cfg.cultDominanceProbMax = 0.0;
|
|
p.cfg.cultWarSchismChance = 0.0;
|
|
}
|
|
static bool culturesEqual(const std::vector<Culture>& a, const std::vector<Culture>& b) {
|
|
if (a.size() != b.size()) return false;
|
|
for (size_t i = 0; i < a.size(); ++i)
|
|
if (a[i].id != b[i].id || a[i].regionId != b[i].regionId || a[i].bank != b[i].bank
|
|
|| a[i].ethos != b[i].ethos || a[i].faith != b[i].faith
|
|
|| a[i].parentId != b[i].parentId || a[i].foundedYear != b[i].foundedYear
|
|
|| a[i].name != b[i].name || a[i].faithName != b[i].faithName) return false;
|
|
return true;
|
|
}
|
|
// The shared yearly civ sequence (mirrors the viewer's year tick, minus weather).
|
|
static std::vector<WarEvent> civYear(Planet& p, long yr, bool wars = false, bool colonize = false) {
|
|
p.computeTerritory(); p.computeCultures();
|
|
if (wars) p.stepConflict(yr);
|
|
std::vector<WarEvent> ev = p.stepCulture(yr);
|
|
if (colonize) for (WarEvent& e : p.stepColonization(yr)) ev.push_back(e);
|
|
return ev;
|
|
}
|
|
|
|
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 yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
|
|
|
std::printf("Cultural evolution: seeding parity (frozen dawn identities)\n");
|
|
p.placeSettlements();
|
|
double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
|
|
p.computeTerritory(); p.computeCultures();
|
|
{
|
|
check(!p.cultureList().empty(), "cultures seeded at the dawn");
|
|
// Each continent (regionId / -1-bank fallback) seeds 1..cultDawnMaxPerContinent cultures now,
|
|
// not always exactly one -- check the count stays within that bound instead.
|
|
std::map<int, std::set<int>> keyToCults;
|
|
for (size_t k = 0; k < p.settlements.size(); ++k) {
|
|
const Settlement& s = p.settlements[k];
|
|
if (s.population < p.cfg.civAbandonPop) continue;
|
|
int ci = p.settleCulture()[k]; if (ci < 0) continue;
|
|
keyToCults[s.regionId >= 0 ? s.regionId : (-1 - s.bank)].insert(ci);
|
|
}
|
|
bool boundedDiversity = true;
|
|
for (auto& kv : keyToCults)
|
|
if ((int)kv.second.size() < 1 || (int)kv.second.size() > p.cfg.cultDawnMaxPerContinent) boundedDiversity = false;
|
|
check(boundedDiversity, "each continent seeds 1..cultDawnMaxPerContinent cultures");
|
|
bool rootsOk = true, idsOk = true;
|
|
for (size_t i = 0; i < p.cultureList().size(); ++i) {
|
|
const Culture& cu = p.cultureList()[i];
|
|
if (cu.parentId != -1 || cu.foundedYear != -1 || cu.name.empty() || cu.faithName.empty()) rootsOk = false;
|
|
if (cu.id != (uint32_t)i + 1) idsOk = false;
|
|
}
|
|
check(rootsOk, "all seeded cultures are roots (no parent, present since the dawn)");
|
|
check(idsOk, "culture ids are dense from 1");
|
|
// A quiet world (all rates 0) never mutates culture state.
|
|
Planet q0 = p; quietCulture(q0);
|
|
std::vector<int> sc0 = q0.settleCulture();
|
|
for (long yr = 0; yr < 30; ++yr) civYear(q0, yr);
|
|
check(q0.settleCulture() == sc0 && q0.cultureList().size() == p.cultureList().size(),
|
|
"no evolution with all cult* rates 0 (Steps 3-7 behaviour preserved)");
|
|
}
|
|
|
|
std::printf("Cultural evolution: colonies inherit the founder's culture\n");
|
|
{
|
|
Planet c = p; quietCulture(c);
|
|
c.computeHabitability();
|
|
c.cfg.civColonizeRate = 1.0; c.cfg.civColonyMinPop = 1000.0; c.cfg.civColonyMinHab = 0.1;
|
|
size_t before = c.settlements.size();
|
|
for (long yr = 0; yr < 40 && c.settlements.size() < before + 6; ++yr) civYear(c, yr, false, true);
|
|
std::printf(" %zu colonies founded\n", c.settlements.size() - before);
|
|
check(c.settlements.size() > before, "colonies are founded");
|
|
bool inherit = c.settlements.size() > before;
|
|
for (size_t k = before; k < c.settlements.size(); ++k) {
|
|
int ov = c.settleAllegiance()[k];
|
|
if (ov < 0 || c.settleCulture()[k] != c.settleCulture()[ov]) inherit = false;
|
|
}
|
|
check(inherit, "every colony carries its founder capital's culture");
|
|
}
|
|
|
|
std::printf("Cultural evolution: assimilation under foreign rule\n");
|
|
{
|
|
Planet a = p; quietCulture(a); a.cfg.cultAssimRate = 1.0;
|
|
// Force a conquest scenario: a living settlement held by a living foreign-culture capital
|
|
// (allegiance + culture are snapshot fields, so a hacked snapshot injects the state cleanly).
|
|
int s = -1, ov = -1;
|
|
const auto& sc = a.settleCulture();
|
|
for (size_t i = 0; i < a.settlements.size() && s < 0; ++i)
|
|
for (size_t j = 0; j < a.settlements.size(); ++j)
|
|
if (i != j && a.settlements[i].population >= a.cfg.civAbandonPop
|
|
&& a.settlements[j].population >= a.cfg.civAbandonPop
|
|
&& sc[i] >= 0 && sc[j] >= 0 && sc[i] != sc[j]) { s = (int)i; ov = (int)j; break; }
|
|
if (s < 0) { std::printf(" (single-culture world: skipping)\n"); }
|
|
else {
|
|
WeatherSnapshot snap = a.captureWeather();
|
|
if (snap.settlementAllegiance.size() != a.settlements.size()) // no war has sized it yet
|
|
snap.settlementAllegiance.assign(a.settlements.size(), -1);
|
|
snap.settlementAllegiance[s] = ov;
|
|
a.restoreWeather(snap);
|
|
int want = a.settleCulture()[ov];
|
|
std::vector<WarEvent> ev = a.stepCulture(1);
|
|
bool flipped = a.settleCulture()[s] == want;
|
|
bool logged = false;
|
|
for (const WarEvent& e : ev)
|
|
if (e.kind == 7 && e.title.find("adopts the culture of") != std::string::npos) logged = true;
|
|
check(flipped, "a conquered settlement assimilates into its ruler's culture");
|
|
check(logged, "assimilation emits a kind-7 event");
|
|
}
|
|
}
|
|
|
|
std::printf("Cultural evolution: border conversion (capitals exempt)\n");
|
|
{
|
|
Planet b = p; quietCulture(b);
|
|
b.cfg.cultConvertRate = 1.0; b.cfg.cultConvertDominance = 0.01; b.cfg.cultSpreadRange = 3.0;
|
|
b.computeTerritory(); b.computeCultures();
|
|
std::vector<int> caps;
|
|
for (const Nation& nat : b.nationList()) caps.push_back(nat.capital);
|
|
std::vector<int> capCultBefore;
|
|
for (int c : caps) capCultBefore.push_back(b.settleCulture()[c]);
|
|
int conversions = 0;
|
|
for (long yr = 0; yr < 5; ++yr)
|
|
for (const WarEvent& e : civYear(b, yr))
|
|
if (e.kind == 7 && e.title.find("embraces the ways of") != std::string::npos) ++conversions;
|
|
std::printf(" %d conversions in 5 years\n", conversions);
|
|
check(conversions > 0, "settlements convert under dominant foreign cultural pressure");
|
|
bool capsStable = true; // capitals anchor identity: only assimilation/schism may move them (both off/limited here)
|
|
for (size_t i = 0; i < caps.size(); ++i)
|
|
if (b.settleCulture()[caps[i]] != capCultBefore[i]) capsStable = false;
|
|
check(capsStable, "realm capitals never convert via border pressure");
|
|
}
|
|
|
|
std::printf("Cultural evolution: locked clusters cannot create empty schisms\n");
|
|
{
|
|
Planet locked = p; quietCulture(locked);
|
|
locked.cfg.cultSchismRate = 1.0; locked.cfg.cultSchismRange = 0.10;
|
|
locked.cfg.cultSchismMinMembers = 3; locked.cfg.cultSchismMinCluster = 1;
|
|
for (int s = 0; s < (int)locked.settlements.size(); ++s) locked.setCultureLock(s, true);
|
|
size_t before = locked.cultureList().size();
|
|
int breakEvents = 0;
|
|
for (long yr = 0; yr < 20; ++yr)
|
|
for (const WarEvent& e : civYear(locked, yr))
|
|
if (e.kind == 7 && e.title.find("break away from") != std::string::npos) ++breakEvents;
|
|
check(locked.cultureList().size() == before && breakEvents == 0,
|
|
"an entirely locked cluster creates neither a child culture nor a false event");
|
|
}
|
|
|
|
std::printf("Cultural evolution: schism (a distant cluster becomes a new people)\n");
|
|
long schismYear = -1;
|
|
{
|
|
quietCulture(p);
|
|
p.cfg.cultSchismRate = 1.0; p.cfg.cultSchismRange = 0.10; p.cfg.cultSchismMinMembers = 3;
|
|
p.cfg.cultSchismMinCluster = 1;
|
|
size_t before = p.cultureList().size();
|
|
for (long yr = 0; yr < 20 && p.cultureList().size() == before; ++yr) {
|
|
for (const WarEvent& e : civYear(p, yr))
|
|
if (e.kind == 7 && e.title.find("break away from") != std::string::npos) schismYear = yr;
|
|
}
|
|
check(p.cultureList().size() > before, "a schism appends a new culture");
|
|
if (p.cultureList().size() > before) {
|
|
const Culture& child = p.cultureList().back();
|
|
bool parentOk = false;
|
|
for (size_t i = 0; i < before; ++i) if ((int)p.cultureList()[i].id == child.parentId) parentOk = true;
|
|
check(parentOk, "the child records its parent culture");
|
|
check(child.foundedYear >= 0 && child.foundedYear == schismYear, "the child records its founding year");
|
|
bool nameUnique = !child.name.empty();
|
|
for (size_t i = 0; i + 1 < p.cultureList().size(); ++i)
|
|
if (p.cultureList()[i].name == child.name) nameUnique = false;
|
|
check(nameUnique, "the child's people name is unique");
|
|
int adopted = 0;
|
|
for (int c : p.settleCulture()) if (c == (int)p.cultureList().size() - 1) ++adopted;
|
|
check(adopted >= 1, "the breakaway cluster adopted the new culture");
|
|
p.computeTerritory(); p.computeCultures(); // the child cluster may found its own realm
|
|
check((int)p.cultureList().back().members == adopted, "derived tallies pick the child up");
|
|
}
|
|
check(p.cultureList().size() <= 64, "the culture list stays bounded");
|
|
}
|
|
|
|
std::printf("Cultural evolution: dominance schism (a culture holding most of the world splits)\n");
|
|
{
|
|
Planet dom = p;
|
|
quietCulture(dom);
|
|
// Always trigger once eligible (min==max==1) so the test doesn't depend on the probability ramp.
|
|
dom.cfg.cultDominanceShare = 0.80; dom.cfg.cultDominanceProbMin = 1.0; dom.cfg.cultDominanceProbMax = 1.0;
|
|
dom.computeTerritory(); dom.computeCultures();
|
|
// Force every living settlement onto one culture so it starts at ~100% world population share --
|
|
// dominance-schism has no distance/cluster requirement, so this alone should trigger it.
|
|
int targetCi = -1;
|
|
for (size_t k = 0; k < dom.settlements.size(); ++k) {
|
|
if (dom.settlements[k].population < dom.cfg.civAbandonPop) continue;
|
|
if (targetCi < 0) targetCi = dom.settleCulture()[k];
|
|
if (targetCi >= 0) break;
|
|
}
|
|
check(targetCi >= 0, "a living settlement with a seeded culture exists to consolidate onto");
|
|
if (targetCi >= 0) {
|
|
for (size_t k = 0; k < dom.settlements.size(); ++k)
|
|
if (dom.settlements[k].population >= dom.cfg.civAbandonPop) dom.setSettlementCulture((int)k, targetCi);
|
|
dom.computeTerritory(); dom.computeCultures(); // retally members/totalPop after consolidation
|
|
int beforeMembers = dom.cultureList()[(size_t)targetCi].members;
|
|
size_t beforeCultCount = dom.cultureList().size();
|
|
bool splitHappened = false;
|
|
for (long yr = 0; yr < 10 && !splitHappened; ++yr) {
|
|
for (const WarEvent& e : civYear(dom, yr))
|
|
if (e.kind == 7 && e.title.find("splits away from") != std::string::npos) splitHappened = true;
|
|
}
|
|
check(splitHappened, "an overwhelmingly dominant culture eventually splits on its own");
|
|
check(dom.cultureList().size() > beforeCultCount, "the split appends a new culture");
|
|
if (splitHappened && dom.cultureList().size() > beforeCultCount) {
|
|
const Culture& child = dom.cultureList().back();
|
|
check(child.parentId == (int)dom.cultureList()[(size_t)targetCi].id,
|
|
"the child records the dominant culture as parent");
|
|
int adopted = 0;
|
|
for (int c : dom.settleCulture()) if (c == (int)dom.cultureList().size() - 1) ++adopted;
|
|
double frac = (double)adopted / std::max(1, beforeMembers);
|
|
check(std::fabs(frac - 0.5) < 0.05 || std::fabs(frac - 0.2) < 0.05,
|
|
"the breakaway is close to half or a fifth of the dominant culture's prior members");
|
|
}
|
|
}
|
|
}
|
|
|
|
std::printf("Cultural evolution: war schism (a war between same-culture realms can split one side)\n");
|
|
{
|
|
Planet warp = p;
|
|
quietCulture(warp);
|
|
warp.cfg.cultWarSchismChance = 1.0; // always trigger once a same-culture war is seen
|
|
warp.computeTerritory(); warp.computeCultures();
|
|
int capA = -1, capB = -1;
|
|
for (size_t k = 0; k < warp.settlements.size() && capB < 0; ++k) {
|
|
if (warp.settlements[k].population < warp.cfg.civAbandonPop) continue;
|
|
if (capA < 0) capA = (int)k; else capB = (int)k;
|
|
}
|
|
check(capA >= 0 && capB >= 0, "at least two living settlements exist to test a war schism");
|
|
if (capA >= 0 && capB >= 0) {
|
|
int ci = warp.settleCulture()[(size_t)capA];
|
|
if (ci < 0) ci = 0;
|
|
warp.setSettlementCulture(capA, ci);
|
|
warp.setSettlementCulture(capB, ci);
|
|
warp.computeTerritory(); warp.computeCultures();
|
|
long yr = 0;
|
|
warp.stepConflict(yr); // sizes/refreshes sSettleAllegiance; any real wars it declares are harmless extras
|
|
War w; w.id = 999999; w.attacker = capA; w.defender = capB; w.startYear = yr;
|
|
warp.wars.push_back(w);
|
|
size_t before = warp.cultureList().size();
|
|
std::vector<WarEvent> ev = warp.stepCulture(yr);
|
|
bool splitHappened = false;
|
|
for (const WarEvent& e : ev)
|
|
if (e.kind == 7 && e.title.find("renounces kinship with") != std::string::npos) splitHappened = true;
|
|
check(splitHappened, "a war between same-culture realms splits one side's culture");
|
|
check(warp.cultureList().size() > before, "the war-triggered split appends a new culture");
|
|
}
|
|
}
|
|
|
|
std::printf("Cultural evolution: 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();
|
|
quietCulture(q);
|
|
q.cfg.cultSchismRate = 1.0; q.cfg.cultSchismRange = 0.10; q.cfg.cultSchismMinMembers = 3;
|
|
q.cfg.cultSchismMinCluster = 1;
|
|
size_t before = q.cultureList().size();
|
|
for (long yr = 0; yr < 20 && q.cultureList().size() == before; ++yr) civYear(q, yr);
|
|
check(culturesEqual(q.cultureList(), p.cultureList()) && q.settleCulture() == p.settleCulture(),
|
|
"two identical worlds evolve identical cultures");
|
|
}
|
|
|
|
std::printf("Cultural evolution: 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.cfg.cultConvertRate = 1.0; y.cfg.cultConvertDominance = 0.01; y.cfg.cultSpreadRange = 3.0;
|
|
for (long yr = 0; yr < 3; ++yr) civYear(y, yr);
|
|
}
|
|
}
|
|
bool same = true;
|
|
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) same = false;
|
|
check(same, "stepCulture never perturbs tectonic evolution");
|
|
}
|
|
{
|
|
// Interleaved no-op stepCulture calls must not disturb the war RNG stream.
|
|
Planet w1 = p, w2 = p; quietCulture(w1); quietCulture(w2);
|
|
w1.cfg.warDeclareRate = 2.0; w1.cfg.warMinRealmPop = 1.0;
|
|
w2.cfg.warDeclareRate = 2.0; w2.cfg.warMinRealmPop = 1.0;
|
|
for (long yr = 100; yr < 130; ++yr) {
|
|
w1.computeTerritory(); w1.computeCultures(); w1.stepConflict(yr);
|
|
w2.computeTerritory(); w2.computeCultures(); w2.stepConflict(yr); w2.stepCulture(yr);
|
|
}
|
|
bool warsSame = w1.warList().size() == w2.warList().size()
|
|
&& w1.settleAllegiance() == w2.settleAllegiance();
|
|
check(warsSame, "stepCulture uses no RNG (interleaving leaves wars identical)");
|
|
}
|
|
|
|
std::printf("Cultural evolution: snapshot truncate-and-replay\n");
|
|
{
|
|
WeatherSnapshot snap = p.captureWeather();
|
|
std::vector<Culture> cBefore = p.cultureList();
|
|
std::vector<int> scBefore = p.settleCulture();
|
|
// Mutate: aggressive conversion + schism for 15 years.
|
|
p.cfg.cultConvertRate = 1.0; p.cfg.cultConvertDominance = 0.01; p.cfg.cultSpreadRange = 3.0;
|
|
p.cfg.cultSchismRate = 1.0; p.cfg.cultSchismRange = 0.10;
|
|
for (long yr = 50; yr < 65; ++yr) civYear(p, yr);
|
|
std::vector<Culture> cAfter = p.cultureList();
|
|
std::vector<int> scAfter = p.settleCulture();
|
|
bool changed = !culturesEqual(cAfter, cBefore) || scAfter != scBefore;
|
|
p.restoreWeather(snap);
|
|
p.computeTerritory(); p.computeCultures(); // the derived refresh after a step-back
|
|
check(changed, "the mutation actually changed the culture state");
|
|
check(p.cultureList().size() == cBefore.size() && p.settleCulture() == scBefore,
|
|
"restoreWeather truncates schism children + rewinds settlement cultures");
|
|
// Deterministic replay re-creates the same children + conversions.
|
|
for (long yr = 50; yr < 65; ++yr) civYear(p, yr);
|
|
check(culturesEqual(p.cultureList(), cAfter) && p.settleCulture() == scAfter,
|
|
"replaying the same years re-creates identical schisms/conversions");
|
|
// Out-of-range snapshot entries are clamped on restore.
|
|
WeatherSnapshot odd = p.captureWeather();
|
|
if (!odd.settlementCulture.empty()) {
|
|
odd.settlementCulture[0] = 9999;
|
|
p.restoreWeather(odd);
|
|
check(p.settleCulture()[0] == -1, "restoreWeather clamps out-of-range culture entries");
|
|
odd.settlementCulture[0] = -1; // leave p in a sane (clamped) state for the save tests
|
|
}
|
|
}
|
|
|
|
std::printf("Cultural evolution: save v23 round-trip\n");
|
|
p.computeTerritory(); p.computeCultures();
|
|
{
|
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
|
p.writeState(ss);
|
|
Planet r;
|
|
bool ok = r.readState(ss);
|
|
check(ok, "readState accepts the v23 stream");
|
|
check(culturesEqual(r.cultureList(), p.cultureList()), "culture identities survive save/load");
|
|
check(r.settleCulture() == p.settleCulture(), "per-settlement culture survives save/load");
|
|
}
|
|
{
|
|
Planet bad = p;
|
|
if (!bad.cultures.empty()) {
|
|
bad.cultures[0].ethos = (CultureEthos)200;
|
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
|
bad.writeState(ss);
|
|
Planet r;
|
|
check(!r.readState(ss), "readState rejects an out-of-range culture ethos");
|
|
}
|
|
}
|
|
|
|
std::printf("Cultural evolution: pre-v23 compatibility\n");
|
|
{
|
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
|
p.writeState(ss);
|
|
Planet r;
|
|
// The culture block is the last one; reading with hasCultures=false ignores the trailing bytes.
|
|
bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true,
|
|
true, true, false);
|
|
check(ok, "readState accepts the stream as pre-v23");
|
|
check(r.cultureList().empty(), "pre-v23 load has no culture state");
|
|
r.computeTerritory(); r.computeCultures();
|
|
check(!r.cultureList().empty(), "cultures re-seed on the next refresh");
|
|
bool allRoots = true;
|
|
for (const Culture& cu : r.cultureList())
|
|
if (cu.parentId != -1 || cu.foundedYear != -1) allRoots = false;
|
|
check(allRoots, "re-seeded cultures are roots (dawn peoples, no parent)");
|
|
}
|
|
|
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CULTURAL-EVOLUTION CHECKS PASSED\n", failures);
|
|
return failures ? 1 : 0;
|
|
}
|