diff --git a/CLAUDE.md b/CLAUDE.md index 5ba0a6f..81772ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -584,6 +584,33 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived `planet.cfg`+`F2` or the Main Menu's Settings tab, only take effect through a regenerate — there is currently no way to push a single global field like `seaLevel` into an already-running world without starting over); the practical fix for an affected world is a New World. +- **Two more culture-schism triggers: world dominance + war between kin (was: only a distant-cluster + schism)** *(done — see `Planet::stepCulture()`, `src/sim/PlanetCulture.cpp`)* — reported after a + long run left the world under a single "world-dominant" culture (the existing schism only fires for + a geographically distant cluster, e.g. overseas colonies — a single unbroken landmass never + qualified, so one culture could quietly absorb the whole world via border conversion with nothing + to check it). Two new independent triggers, both sharing the existing schism machinery (now + factored into a `doSchism()` lambda so all three read identically): **dominance schism** — once a + culture exceeds `cultDominanceShare` (80%) of the world's living population, a per-year chance + rising from `cultDominanceProbMin` (1%) at the threshold to `cultDominanceProbMax` (99%) at 100% + share that it fractures on its own, **no geography required** — a coin flip picks **half or a + fifth** of its own members, the ones farthest from its own population-weighted cultural core, to + found a new people; **war schism** — a war between two realms that happen to share a culture (a + "civil war") has `cultWarSchismChance` (50%) odds, rolled once right at declaration, that one whole + side (a coin flip between attacker/defender) renounces the shared identity, its entire realm's + settlements becoming a new people — checked against wars with `startYear` equal to the current + year, so it only ever looks at a war on the exact year it was declared. Both are pure hashes of + (stable id, year, seed) like the rest of Step 8, so step-back replays them exactly. New config + fields `cultDominanceShare`/`cultDominanceProbMin`/`cultDominanceProbMax`/`cultWarSchismChance` + (self-describing config, **no save-version bump**). One design pitfall found while adding these: + an initial `validateConfig()` cross-rule requiring `cultDominanceProbMin <= cultDominanceProbMax` + was removed again — the interpolation tolerates the reverse fine (the whole mechanic is already + gated on `cultDominanceProbMax > 0`), and the rule would have rejected the very natural "disable by + setting Max to 0" pattern (e.g. from the Main Menu's Settings tab) whenever `Min` was left at its + nonzero default, silently reverting a player's *entire* config to defaults on New World — a bad + surprise for an edit that looks perfectly reasonable. `test_cultevo.cpp` gained two new scenarios + (force every settlement onto one culture and watch it split near-deterministically; inject a + same-culture war and check the split) alongside the existing distant-cluster one. ## Current state @@ -1459,7 +1486,11 @@ triangles (plates are fixed in phase 1). cultural weight — rich hubs radiate culture); schism — `cultSchismMinMembers` (6, min living settlements to schism), `cultSchismRange` (0.55 rad from the population centroid past which members are "distant" — lower = easier colonial breakaways), `cultSchismMinCluster` (2, distant settlements needed to break away - together), `cultSchismRate` (0.08, per-year chance per qualifying culture). All rates 0 = the static + together), `cultSchismRate` (0.08, per-year chance per qualifying culture); dominance schism — + `cultDominanceShare` (0.80, world population share above which it can trigger), `cultDominanceProbMin` + (0.01, per-year chance right at the threshold) / `cultDominanceProbMax` (0.99, at 100% share — the + chance ramps linearly between the two); war schism — `cultWarSchismChance` (0.50, chance a war + between two same-culture realms splits one side, rolled once at declaration). All rates 0 = the static pre-Step-8 world. Runs in the yearly `stepCulture` tick (pure hashes, no RNG); the culture list is capped at 64 peoples. Dawn seeding (`Planet::seedCultures()`, before Step 8 evolution runs at all): `cultDawnSettlementsPerCulture` (5, target dawn settlements per initial culture on a continent — diff --git a/src/sim/PlanetCulture.cpp b/src/sim/PlanetCulture.cpp index bb0a1e1..9472aa7 100644 --- a/src/sim/PlanetCulture.cpp +++ b/src/sim/PlanetCulture.cpp @@ -14,9 +14,11 @@ // - computeCultures() the DERIVED refresh (tallies, governments, per-cell view). Runs on every // territory rebuild / load / step-back, so it must never mutate identity state. // - stepCulture() the once-per-sim-year mutation pass: assimilation (conquered settlements adopt -// their ruler's culture), border conversion (dominant foreign cultural pressure) -// and schism (a far-flung cluster becomes a new people). Pure hashes of -// (stable id, year, seed) -- no RNG touched, so step-back replays it exactly. +// their ruler's culture), border conversion (dominant foreign cultural pressure), +// and three independent schism triggers -- a far-flung cluster (colonies), a +// culture that has grown to dominate most of the world's population, and a war +// between two realms of the same culture. Pure hashes of (stable id, year, seed) +// -- no RNG touched, so step-back replays it exactly. // The culture list is append-only and records are immutable after creation (indices/colors stay // stable; extinct cultures keep their slot); the list + sSettleCulture are saved (v23) + snapshotted. @@ -350,14 +352,21 @@ void Planet::computeCultures() { } // --- Civilization Step 8: the yearly cultural-evolution pass ---------------------------------------- -// Three passes, all pure hashes of (stable id, year, seed) so a step-back replays them exactly: +// All passes are pure hashes of (stable id, year, seed) so a step-back replays them exactly: // A) assimilation -- a settlement held by a foreign-culture overlord adopts the ruler's culture // (the Step-5 revolt cultBonus then drops on its own: assimilation pacifies provinces); // B) border conversion -- a settlement dwarfed by a nearby foreign culture's weight (population + // trade prestige) converts toward it (realm capitals are exempt: they anchor identity); -// C) schism -- a large culture's far-flung coherent cluster (overseas colonies) breaks away as a -// NEW people: a fresh name from the local language bank, ethos/faith re-derived from its own -// lands (appended to `cultures`; step-back truncates the list and replay re-creates it). +// C) schism -- a large culture splits off a NEW people (fresh name, ethos/faith re-derived from its +// own lands, appended to `cultures`; step-back truncates the list and replay re-creates it), via +// any of three independent triggers, all sharing one doSchism() helper: +// C1) a far-flung coherent cluster (overseas colonies) breaks away past cultSchismRange; +// C2) once a culture exceeds cultDominanceShare of the world's population, a rising per-year +// chance (cultDominanceProbMin..Max) that half or a fifth of it (whichever the coin flip +// picks), the members farthest from its own cultural core, secede on their own -- no +// geography required, so one people can't quietly conquer the whole world by attrition; +// C3) a war between two realms that share a culture has cultWarSchismChance odds, checked right +// at declaration, that one whole side renounces the shared identity -- kin stop being kin. std::vector Planet::stepCulture(long year) { std::vector ev; const int n = (int)cells.size(); @@ -436,45 +445,29 @@ std::vector Planet::stepCulture(long year) { } } - // C) Schism: a distant coherent cluster of a large culture becomes a new people. - const size_t ncult = cultures.size(); // iterate the pre-pass list (children append) - if (cfg.cultSchismRate > 0.0 && (int)ncult < 64) { + // C) Schism: a large culture splits into a new people, via three independent triggers -- a + // coherent distant cluster (colonies), overwhelming world dominance, or a war between kin. + const size_t ncult = cultures.size(); // iterate the pre-pass list (children append, never re-split) + if ((int)ncult < 64 && (cfg.cultSchismRate > 0.0 || cfg.cultDominanceProbMax > 0.0 || cfg.cultWarSchismChance > 0.0)) { + // Shared setup: per-culture living member count + population-weighted centroid, used by both + // C1 (distance from the core) and C2 (farthest members when splitting off by dominance). std::vector memCount(ncult, 0); std::vector cSum(ncult); + double totalLivingPop = 0.0; for (size_t s = 0; s < ns; ++s) { if (!living(s)) continue; int ci = sSettleCulture[s]; if (ci < 0 || ci >= (int)ncult) continue; memCount[ci]++; cSum[ci] = cSum[ci] + unitOf(s) * std::max(1.0, settlements[s].population); + totalLivingPop += settlements[s].population; } const double range = std::max(1e-6, cfg.cultSpreadRange); - for (size_t ci = 0; ci < ncult && (int)cultures.size() < 64; ++ci) { - if (memCount[ci] < std::max(1, cfg.cultSchismMinMembers)) continue; - if (cSum[ci].length() <= 1e-12) continue; - Vec3 centroid = cSum[ci].normalized(); // population-weighted cultural core - // One deterministic roll per culture-year (cheap early exit; pure hash, no state consumed). - if (cultHashf(cultures[ci].id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0x5C1531u) - >= cfg.cultSchismRate) continue; - // Members far from the core; the breakaway cluster = the farthest one + distant members - // near it (a coherent region / overseas colony group, not scattered strays). - std::vector distant; int far = -1; double farD = 0.0; - for (size_t s = 0; s < ns; ++s) { - if (!living(s) || sSettleCulture[s] != (int)ci) continue; - int sc = settlements[s].cell; - if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; - double d = ang(unitOf(s), centroid); - if (d > cfg.cultSchismRange) { - distant.push_back((int)s); - if (d > farD) { farD = d; far = (int)s; } - } - } - if (far < 0 || (int)distant.size() < std::max(1, cfg.cultSchismMinCluster)) continue; - std::vector cluster; - for (int s : distant) - if (ang(unitOf((size_t)s), unitOf((size_t)far)) <= range) cluster.push_back(s); - if ((int)cluster.size() < std::max(1, cfg.cultSchismMinCluster)) continue; - if ((int)cluster.size() >= memCount[ci]) continue; // a schism splits, it never renames all + // Creates a new culture from `cluster` (settlement indices, all currently culture `ci`), + // re-deriving ethos/faith from its own lands and a fresh name (keeping the parent's faith name + // if the faith is unchanged); logs a kind=7 event. Shared by all three triggers below. + auto doSchism = [&](int ci, const std::vector& cluster, const std::string& verb, + const std::string& detail, int eventCell) { // Copy the parent fields we need BEFORE the push_back (it invalidates references). const uint32_t parId = cultures[ci].id; const std::string parName = cultures[ci].name; @@ -496,8 +489,6 @@ std::vector Planet::stepCulture(long year) { child.regionId = regBest; child.bank = (regBest >= 0) ? namegen::bankForRegion(cfg.seed, regBest) : parBank; - // Ethos/faith re-derived from the cluster's own lands (hash-salted by the child id so the - // faith can mutate); keep the parent's faith name if the faith itself is unchanged. CultEnv env; for (int s : cluster) envAdd(env, cells, sMoist, cfg.seaLevel, settlements[s].cell); uint32_t rk = child.id * 0x9E3779B9u + 0xC41Du; @@ -521,11 +512,109 @@ std::vector Planet::stepCulture(long year) { if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; // edit mode: pinned sSettleCulture[s] = childIdx; converted[s] = 1; } - std::string title = child.name + " break away from " + parName; + std::string title = child.name + " " + verb + " " + parName; title[0] = (char)std::toupper((unsigned char)title[0]); - ev.push_back(WarEvent{ 2, settlements[far].cell, title, - "A distant people drifts apart and names itself anew.", 7 }); + ev.push_back(WarEvent{ 2, eventCell, title, detail, 7 }); cultures.push_back(std::move(child)); + }; + + // C1) A distant coherent cluster of a large culture (typically overseas colonies) breaks away. + if (cfg.cultSchismRate > 0.0) { + for (size_t ci = 0; ci < ncult && (int)cultures.size() < 64; ++ci) { + if (memCount[ci] < std::max(1, cfg.cultSchismMinMembers)) continue; + if (cSum[ci].length() <= 1e-12) continue; + Vec3 centroid = cSum[ci].normalized(); // population-weighted cultural core + // One deterministic roll per culture-year (cheap early exit; pure hash, no state consumed). + if (cultHashf(cultures[ci].id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0x5C1531u) + >= cfg.cultSchismRate) continue; + // Members far from the core; the breakaway cluster = the farthest one + distant members + // near it (a coherent region / overseas colony group, not scattered strays). + std::vector distant; int far = -1; double farD = 0.0; + for (size_t s = 0; s < ns; ++s) { + if (!living(s) || sSettleCulture[s] != (int)ci) continue; + int sc = settlements[s].cell; + if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; + double d = ang(unitOf(s), centroid); + if (d > cfg.cultSchismRange) { + distant.push_back((int)s); + if (d > farD) { farD = d; far = (int)s; } + } + } + if (far < 0 || (int)distant.size() < std::max(1, cfg.cultSchismMinCluster)) continue; + std::vector cluster; + for (int s : distant) + if (ang(unitOf((size_t)s), unitOf((size_t)far)) <= range) cluster.push_back(s); + if ((int)cluster.size() < std::max(1, cfg.cultSchismMinCluster)) continue; + if ((int)cluster.size() >= memCount[ci]) continue; // a schism splits, it never renames all + doSchism((int)ci, cluster, "break away from", + "A distant people drifts apart and names itself anew.", settlements[far].cell); + } + } + + // C2) Overwhelming world dominance: past cultDominanceShare of the world's living population, a + // rising per-year chance (cultDominanceProbMin at the threshold, cultDominanceProbMax at 100%) + // that the culture fractures on its own, no geography required -- half or a fifth of its own + // members (an even coin flip), the farthest-flung from its own population-weighted core, found + // a new people. Curbs one culture quietly conquering the whole world by attrition. + if (cfg.cultDominanceProbMax > 0.0 && totalLivingPop > 0.0) { + for (size_t ci = 0; ci < ncult && (int)cultures.size() < 64; ++ci) { + if (memCount[ci] < 2 || cSum[ci].length() <= 1e-12) continue; + double share = cultures[ci].totalPop / totalLivingPop; + if (share < cfg.cultDominanceShare) continue; + double t = std::clamp((share - cfg.cultDominanceShare) + / std::max(1e-6, 1.0 - cfg.cultDominanceShare), 0.0, 1.0); + double prob = cfg.cultDominanceProbMin + (cfg.cultDominanceProbMax - cfg.cultDominanceProbMin) * t; + if (cultHashf(cultures[ci].id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0xD0111Au) + >= prob) continue; + double fraction = (cultHashf(cultures[ci].id * 2654435761u ^ (uint32_t)year * 19349663u + ^ seed ^ 0xF4AC7u) < 0.5) ? 0.5 : 0.2; + Vec3 centroid = cSum[ci].normalized(); + std::vector> byDist; + for (size_t s = 0; s < ns; ++s) { + if (!living(s) || sSettleCulture[s] != (int)ci) continue; + int sc = settlements[s].cell; + if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; + byDist.push_back({ ang(unitOf(s), centroid), (int)s }); + } + if ((int)byDist.size() < 2) continue; + std::stable_sort(byDist.begin(), byDist.end(), + [](const auto& a, const auto& b) { return a.first > b.first; }); // farthest first + int want = std::clamp((int)std::llround(byDist.size() * fraction), 1, (int)byDist.size() - 1); + std::vector cluster; + for (int k = 0; k < want; ++k) cluster.push_back(byDist[(size_t)k].second); + doSchism((int)ci, cluster, "splits away from", + "Grown too vast to hold together, the people drift into rival identities.", + settlements[cluster.front()].cell); + } + } + + // C3) A war between two realms of the same culture: cultWarSchismChance odds that one side's + // whole realm renounces the shared identity right as the war begins -- a war between kin only + // makes sense once they're no longer kin. Checked against wars declared THIS year only. + if (cfg.cultWarSchismChance > 0.0 && sSettleAllegiance.size() == ns) { + for (const War& w : wars) { + if (w.startYear != year || (int)cultures.size() >= 64) continue; + int attCap = w.attacker, defCap = w.defender; + if (attCap < 0 || attCap >= (int)ns || defCap < 0 || defCap >= (int)ns) continue; + int ci = sSettleCulture[attCap]; + if (ci < 0 || ci >= (int)cultures.size() || sSettleCulture[defCap] != ci) continue; + if (cultHashf(w.id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0x5A57A0u) + >= cfg.cultWarSchismChance) continue; + int chosenCap = (cultHashf(w.id * 2654435761u ^ seed ^ 0x51DE5u) < 0.5) ? attCap : defCap; + std::vector cluster; int total = 0; + for (size_t s = 0; s < ns; ++s) { + if (!living(s) || sSettleCulture[s] != ci) continue; + ++total; + if ((int)s != chosenCap && sSettleAllegiance[s] != chosenCap) continue; + int sc = settlements[s].cell; + if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; + cluster.push_back((int)s); + } + if (cluster.empty() || (int)cluster.size() >= total) continue; + doSchism(ci, cluster, "renounces kinship with", + "War between kin cracks their shared identity beyond repair.", + settlements[chosenCap].cell); + } } } return ev; diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 0aed2d5..57baa2e 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -71,6 +71,7 @@ D(civColonySpacing) D(civColonySupply) \ D(cultAssimRate) D(cultConvertRate) D(cultConvertDominance) D(cultSpreadRange) \ D(cultPrestigeWeight) D(cultSchismRange) D(cultSchismRate) \ + D(cultDominanceShare) D(cultDominanceProbMin) D(cultDominanceProbMax) D(cultWarSchismChance) \ I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \ I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \ I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \ @@ -268,7 +269,9 @@ static const struct { const char* name; const char* category; } kConfigFieldCate {"cultConvertDominance", "Cultural evolution"}, {"cultSpreadRange", "Cultural evolution"}, {"cultPrestigeWeight", "Cultural evolution"}, {"cultSchismMinMembers", "Cultural evolution"}, {"cultSchismRange", "Cultural evolution"}, {"cultSchismMinCluster", "Cultural evolution"}, - {"cultSchismRate", "Cultural evolution"}, {"cultDawnSettlementsPerCulture", "Cultural evolution"}, + {"cultSchismRate", "Cultural evolution"}, {"cultDominanceShare", "Cultural evolution"}, + {"cultDominanceProbMin", "Cultural evolution"}, {"cultDominanceProbMax", "Cultural evolution"}, + {"cultWarSchismChance", "Cultural evolution"}, {"cultDawnSettlementsPerCulture", "Cultural evolution"}, {"cultDawnMaxPerContinent", "Cultural evolution"}, }; @@ -560,6 +563,10 @@ std::string validateConfig(const PlanetConfig& cfg) { E(rng(cfg.cultSchismRange, 0.0, 3.14159, "cultSchismRange")); E(irng(cfg.cultSchismMinCluster, 1, 1000000, "cultSchismMinCluster")); E(rng(cfg.cultSchismRate, 0.0, 1.0, "cultSchismRate")); + E(rng(cfg.cultDominanceShare, 0.5, 1.0, "cultDominanceShare")); + E(rng(cfg.cultDominanceProbMin, 0.0, 1.0, "cultDominanceProbMin")); + E(rng(cfg.cultDominanceProbMax, 0.0, 1.0, "cultDominanceProbMax")); + E(rng(cfg.cultWarSchismChance, 0.0, 1.0, "cultWarSchismChance")); E(irng(cfg.cultDawnSettlementsPerCulture, 1, 1000000, "cultDawnSettlementsPerCulture")); E(irng(cfg.cultDawnMaxPerContinent, 1, 1000, "cultDawnMaxPerContinent")); E(irng(cfg.subdivisions, 0, 7, "subdivisions")); diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index e983613..a251a6b 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -568,6 +568,14 @@ struct PlanetConfig { double cultSchismRange = 0.55; // rad from the culture's population centroid past which members are "distant" int cultSchismMinCluster = 2; // distant settlements needed to break away together double cultSchismRate = 0.08; // per-year chance a qualifying distant cluster becomes a new people + // Two more schism triggers, independent of the distant-cluster one above: a culture that swallows + // most of the world fractures on its own (no geography needed), and a war between same-culture + // realms can crack their shared identity right at the outbreak. Both curb one people quietly + // conquering the whole world by attrition. + double cultDominanceShare = 0.80; // world population share above which dominance-schism can trigger + double cultDominanceProbMin = 0.01; // per-year schism chance right at cultDominanceShare + double cultDominanceProbMax = 0.99; // per-year schism chance at 100% world population share + double cultWarSchismChance = 0.50; // chance a war between same-culture realms splits one side away // Dawn seeding (seedCultures()): a populous continent starts as SEVERAL distinct peoples instead // of one continent-spanning culture -- without this, decades of border conversion/backfill tend // to erode a single per-continent culture into one super-dominant people across the whole world. diff --git a/test_cultevo.cpp b/test_cultevo.cpp index bf43d5a..452cc37 100644 --- a/test_cultevo.cpp +++ b/test_cultevo.cpp @@ -12,8 +12,10 @@ // // Verifies: seeding parity (one culture per continent, frozen identities); colonies inherit the // founder's culture; assimilation under foreign allegiance; border conversion (capitals exempt); -// schism (a distant cluster becomes a new people); determinism + RNG isolation; snapshot -// truncate-and-replay; save-v23 round-trip + corrupt-stream rejection; pre-v23 compatibility. +// 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 @@ -40,6 +42,10 @@ static void drift(Planet& p, int iters) { } 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& a, const std::vector& b) { if (a.size() != b.size()) return false; @@ -214,6 +220,80 @@ int main() { 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 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);