Add two more culture-schism triggers: world dominance and war between kin
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
This commit is contained in:
parent
69f3aa2d22
commit
b1175c6e54
33
CLAUDE.md
33
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 —
|
||||
|
||||
@ -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<WarEvent> Planet::stepCulture(long year) {
|
||||
std::vector<WarEvent> ev;
|
||||
const int n = (int)cells.size();
|
||||
@ -436,45 +445,29 @@ std::vector<WarEvent> 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<int> memCount(ncult, 0);
|
||||
std::vector<Vec3> 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<int> 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<int> 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<int>& 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<WarEvent> 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<WarEvent> 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<int> 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<int> 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<std::pair<double, int>> 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<int> 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<int> 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;
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 <cstdio>
|
||||
@ -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<Culture>& a, const std::vector<Culture>& 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<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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user