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
|
`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
|
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.
|
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
|
## 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
|
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" —
|
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
|
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
|
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):
|
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 —
|
`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
|
// - computeCultures() the DERIVED refresh (tallies, governments, per-cell view). Runs on every
|
||||||
// territory rebuild / load / step-back, so it must never mutate identity state.
|
// territory rebuild / load / step-back, so it must never mutate identity state.
|
||||||
// - stepCulture() the once-per-sim-year mutation pass: assimilation (conquered settlements adopt
|
// - stepCulture() the once-per-sim-year mutation pass: assimilation (conquered settlements adopt
|
||||||
// their ruler's culture), border conversion (dominant foreign cultural pressure)
|
// their ruler's culture), border conversion (dominant foreign cultural pressure),
|
||||||
// and schism (a far-flung cluster becomes a new people). Pure hashes of
|
// and three independent schism triggers -- a far-flung cluster (colonies), a
|
||||||
// (stable id, year, seed) -- no RNG touched, so step-back replays it exactly.
|
// 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
|
// 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.
|
// 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 ----------------------------------------
|
// --- 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
|
// 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);
|
// (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 +
|
// 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);
|
// 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
|
// C) schism -- a large culture splits off a NEW people (fresh name, ethos/faith re-derived from its
|
||||||
// NEW people: a fresh name from the local language bank, ethos/faith re-derived from its own
|
// own lands, appended to `cultures`; step-back truncates the list and replay re-creates it), via
|
||||||
// lands (appended to `cultures`; step-back truncates the list and replay re-creates it).
|
// 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> Planet::stepCulture(long year) {
|
||||||
std::vector<WarEvent> ev;
|
std::vector<WarEvent> ev;
|
||||||
const int n = (int)cells.size();
|
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.
|
// C) Schism: a large culture splits into a new people, via three independent triggers -- a
|
||||||
const size_t ncult = cultures.size(); // iterate the pre-pass list (children append)
|
// coherent distant cluster (colonies), overwhelming world dominance, or a war between kin.
|
||||||
if (cfg.cultSchismRate > 0.0 && (int)ncult < 64) {
|
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<int> memCount(ncult, 0);
|
||||||
std::vector<Vec3> cSum(ncult);
|
std::vector<Vec3> cSum(ncult);
|
||||||
|
double totalLivingPop = 0.0;
|
||||||
for (size_t s = 0; s < ns; ++s) {
|
for (size_t s = 0; s < ns; ++s) {
|
||||||
if (!living(s)) continue;
|
if (!living(s)) continue;
|
||||||
int ci = sSettleCulture[s]; if (ci < 0 || ci >= (int)ncult) continue;
|
int ci = sSettleCulture[s]; if (ci < 0 || ci >= (int)ncult) continue;
|
||||||
memCount[ci]++;
|
memCount[ci]++;
|
||||||
cSum[ci] = cSum[ci] + unitOf(s) * std::max(1.0, settlements[s].population);
|
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);
|
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).
|
// Copy the parent fields we need BEFORE the push_back (it invalidates references).
|
||||||
const uint32_t parId = cultures[ci].id;
|
const uint32_t parId = cultures[ci].id;
|
||||||
const std::string parName = cultures[ci].name;
|
const std::string parName = cultures[ci].name;
|
||||||
@ -496,8 +489,6 @@ std::vector<WarEvent> Planet::stepCulture(long year) {
|
|||||||
child.regionId = regBest;
|
child.regionId = regBest;
|
||||||
child.bank = (regBest >= 0) ? namegen::bankForRegion(cfg.seed, regBest) : parBank;
|
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;
|
CultEnv env;
|
||||||
for (int s : cluster) envAdd(env, cells, sMoist, cfg.seaLevel, settlements[s].cell);
|
for (int s : cluster) envAdd(env, cells, sMoist, cfg.seaLevel, settlements[s].cell);
|
||||||
uint32_t rk = child.id * 0x9E3779B9u + 0xC41Du;
|
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
|
if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue; // edit mode: pinned
|
||||||
sSettleCulture[s] = childIdx; converted[s] = 1;
|
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]);
|
title[0] = (char)std::toupper((unsigned char)title[0]);
|
||||||
ev.push_back(WarEvent{ 2, settlements[far].cell, title,
|
ev.push_back(WarEvent{ 2, eventCell, title, detail, 7 });
|
||||||
"A distant people drifts apart and names itself anew.", 7 });
|
|
||||||
cultures.push_back(std::move(child));
|
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;
|
return ev;
|
||||||
|
|||||||
@ -71,6 +71,7 @@
|
|||||||
D(civColonySpacing) D(civColonySupply) \
|
D(civColonySpacing) D(civColonySupply) \
|
||||||
D(cultAssimRate) D(cultConvertRate) D(cultConvertDominance) D(cultSpreadRange) \
|
D(cultAssimRate) D(cultConvertRate) D(cultConvertDominance) D(cultSpreadRange) \
|
||||||
D(cultPrestigeWeight) D(cultSchismRange) D(cultSchismRate) \
|
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(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
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"},
|
{"cultConvertDominance", "Cultural evolution"}, {"cultSpreadRange", "Cultural evolution"},
|
||||||
{"cultPrestigeWeight", "Cultural evolution"}, {"cultSchismMinMembers", "Cultural evolution"},
|
{"cultPrestigeWeight", "Cultural evolution"}, {"cultSchismMinMembers", "Cultural evolution"},
|
||||||
{"cultSchismRange", "Cultural evolution"}, {"cultSchismMinCluster", "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"},
|
{"cultDawnMaxPerContinent", "Cultural evolution"},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -560,6 +563,10 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.cultSchismRange, 0.0, 3.14159, "cultSchismRange"));
|
E(rng(cfg.cultSchismRange, 0.0, 3.14159, "cultSchismRange"));
|
||||||
E(irng(cfg.cultSchismMinCluster, 1, 1000000, "cultSchismMinCluster"));
|
E(irng(cfg.cultSchismMinCluster, 1, 1000000, "cultSchismMinCluster"));
|
||||||
E(rng(cfg.cultSchismRate, 0.0, 1.0, "cultSchismRate"));
|
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.cultDawnSettlementsPerCulture, 1, 1000000, "cultDawnSettlementsPerCulture"));
|
||||||
E(irng(cfg.cultDawnMaxPerContinent, 1, 1000, "cultDawnMaxPerContinent"));
|
E(irng(cfg.cultDawnMaxPerContinent, 1, 1000, "cultDawnMaxPerContinent"));
|
||||||
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
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"
|
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
|
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
|
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
|
// 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
|
// 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.
|
// 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
|
// Verifies: seeding parity (one culture per continent, frozen identities); colonies inherit the
|
||||||
// founder's culture; assimilation under foreign allegiance; border conversion (capitals exempt);
|
// founder's culture; assimilation under foreign allegiance; border conversion (capitals exempt);
|
||||||
// schism (a distant cluster becomes a new people); determinism + RNG isolation; snapshot
|
// three independent schism triggers -- a distant cluster becomes a new people, an overwhelmingly
|
||||||
// truncate-and-replay; save-v23 round-trip + corrupt-stream rejection; pre-v23 compatibility.
|
// 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 "Planet.hpp"
|
||||||
#include <cstdio>
|
#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
|
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;
|
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) {
|
static bool culturesEqual(const std::vector<Culture>& a, const std::vector<Culture>& b) {
|
||||||
if (a.size() != b.size()) return false;
|
if (a.size() != b.size()) return false;
|
||||||
@ -214,6 +220,80 @@ int main() {
|
|||||||
check(p.cultureList().size() <= 64, "the culture list stays bounded");
|
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");
|
std::printf("Cultural evolution: determinism\n");
|
||||||
{
|
{
|
||||||
Planet q; q.generate(cfg); settle(q); drift(q, 400);
|
Planet q; q.generate(cfg); settle(q); drift(q, 400);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user