Seed multiple cultures per continent instead of always exactly one
A continent used to always seed exactly one culture at the dawn
(seedCultures(), grouped by regionId), which -- combined with the existing
border-conversion/backfill dynamics -- tended to erode into one
super-dominant culture covering the entire world over a long simulated run.
seedCultures() now splits each continent into 1..cultDawnMaxPerContinent
initial peoples via deterministic farthest-point seeding (a settlement is
assigned to its nearest seed point, giving geographically coherent,
Voronoi-like cultural regions instead of a checkerboard). The target count
scales with the continent's settlement population via the new
cultDawnSettlementsPerCulture config knob, capped by cultDawnMaxPerContinent.
Per-culture ethos/faith/name hashes now fold in Culture.id so multiple
cultures sharing a continent (and therefore the same regionId/bank) get
distinct identities instead of colliding.
This surfaced a real, previously-latent ordering bug: computeTerritory()
(realm/vassalage grouping, which prefers matching by actual culture and only
falls back to same-continent when culture data doesn't exist yet) ran BEFORE
computeCultures() populated per-settlement culture, so on every fresh
recompute realms formed by continent for one full pass before the next
recompute split them correctly. Harmless before this change (one culture per
continent made the two equivalent), but would have left temporarily
multi-cultural realms now. Fixed at the root: split computeCultures() into
refreshSettlementCultures() (the settlement-level seed/backfill/tally slice,
no dependency on `nations`) and had computeTerritory() call it internally
before grouping -- every caller (production and the dozens of existing test
call sites) gets correct behaviour automatically, no call-site changes needed
beyond the two tests whose assertions encoded the old exact one-per-continent
invariant.
test_culture.cpp and test_cultevo.cpp updated to assert the new bounded
(1..cultDawnMaxPerContinent) invariant instead of exact equality, plus that a
single culture never itself spans two continents (still enforced).
Verified live: a single continent now shows six distinct peoples ("the
Thisur", "the Bomebro", "the Draordaes", etc.) instead of one dominant
culture, while realms remain correctly mono-cultural.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
This commit is contained in:
parent
a9ea113f97
commit
cd268167b0
@ -892,7 +892,7 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
|
|||||||
// Recompute realms/territory + cultures from the (derived) settlement set and rebuild the border
|
// Recompute realms/territory + cultures from the (derived) settlement set and rebuild the border
|
||||||
// segments (political + cultural). Cultures depend on territory, so compute them right after.
|
// segments (political + cultural). Cultures depend on territory, so compute them right after.
|
||||||
void Viewer::rebuildTerritory() {
|
void Viewer::rebuildTerritory() {
|
||||||
planet.computeTerritory();
|
planet.computeTerritory(); // refreshes per-settlement culture itself before grouping into realms
|
||||||
planet.computeCultures();
|
planet.computeCultures();
|
||||||
planet.computeTrade(); // civ Step 7: trade links + prosperity (derived)
|
planet.computeTrade(); // civ Step 7: trade links + prosperity (derived)
|
||||||
buildNationBorders(planet, borderR, nationBorders);
|
buildNationBorders(planet, borderR, nationBorders);
|
||||||
|
|||||||
@ -212,12 +212,22 @@ public:
|
|||||||
const std::vector<int>& settleNation() const { return sSettleNation; } // nation index per settlement (-1 = dead)
|
const std::vector<int>& settleNation() const { return sSettleNation; } // nation index per settlement (-1 = dead)
|
||||||
|
|
||||||
// Cultures, beliefs & governments (PlanetCulture.cpp). computeCultures() is the DERIVED refresh:
|
// Cultures, beliefs & governments (PlanetCulture.cpp). computeCultures() is the DERIVED refresh:
|
||||||
// it seeds the cultures once (one per inhabited continent) when none exist, then only recomputes
|
// it seeds the cultures once (a continent starts as 1..cultDawnMaxPerContinent peoples, split via
|
||||||
// tallies, governments (folded into nation.name) and the per-cell culture view. Runs AFTER
|
// deterministic farthest-point seeding) when none exist, then only recomputes tallies, governments
|
||||||
// computeTerritory() (reads nations/sCellNation/sCellSettleOwner). Culture identities + the
|
// (folded into nation.name) and the per-cell culture view. Runs AFTER computeTerritory() (reads
|
||||||
// per-settlement culture are STATEFUL since Step 8 (saved v23 + snapshotted); stepCulture() is
|
// nations/sCellNation/sCellSettleOwner). Culture identities + the per-settlement culture are
|
||||||
// the once-per-sim-year mutation pass (border conversion / assimilation / schism, pure hashes --
|
// STATEFUL since Step 8 (saved v23 + snapshotted); stepCulture() is the once-per-sim-year mutation
|
||||||
// no RNG touched, so step-back replays it exactly).
|
// pass (border conversion / assimilation / schism, pure hashes -- no RNG touched, so step-back
|
||||||
|
// replays it exactly).
|
||||||
|
// refreshSettlementCultures() is just the settlement-level slice of computeCultures() (seed once
|
||||||
|
// + sanitize/backfill sSettleCulture + tally) -- no dependency on `nations`. Call it BEFORE
|
||||||
|
// computeTerritory() so realm/vassalage grouping (which prefers matching by actual culture, only
|
||||||
|
// falling back to same-continent when culture data doesn't exist yet) sees up-to-date per-
|
||||||
|
// settlement culture on every call, not just from the second recompute onward -- otherwise a
|
||||||
|
// continent seeded with several peoples would form one realm per continent for one full pass
|
||||||
|
// before the next recompute split it correctly. computeCultures() calls this itself too (so it
|
||||||
|
// stays correct when called on its own), making a second call from here a harmless no-op.
|
||||||
|
void refreshSettlementCultures();
|
||||||
void computeCultures();
|
void computeCultures();
|
||||||
std::vector<WarEvent> stepCulture(long year);
|
std::vector<WarEvent> stepCulture(long year);
|
||||||
bool culturesBuilt() const { return !cultures.empty(); }
|
bool culturesBuilt() const { return !cultures.empty(); }
|
||||||
|
|||||||
@ -145,26 +145,73 @@ void Planet::seedCultures() {
|
|||||||
const double sea = cfg.seaLevel, abP = cfg.civAbandonPop;
|
const double sea = cfg.seaLevel, abP = cfg.civAbandonPop;
|
||||||
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
||||||
|
|
||||||
// 1) Group living settlements into cultures.
|
// 1) Group living settlements by continent, then split each continent into 1..cultDawnMaxPerContinent
|
||||||
std::unordered_map<int, int> keyToCulture;
|
// initial peoples via deterministic farthest-point seeding (geographically coherent, like a
|
||||||
|
// Voronoi partition) instead of always one culture per continent -- without this, a populous
|
||||||
|
// continent starts as a single culture that years of border conversion + the settlement-culture
|
||||||
|
// backfill (both pre-existing mechanics) tend to erode into one super-dominant people covering
|
||||||
|
// the whole world, leaving little cultural diversity.
|
||||||
auto living = [&](const Settlement& s) {
|
auto living = [&](const Settlement& s) {
|
||||||
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
||||||
};
|
};
|
||||||
|
std::unordered_map<int, std::vector<size_t>> continentMembers;
|
||||||
for (size_t k = 0; k < settlements.size(); ++k) {
|
for (size_t k = 0; k < settlements.size(); ++k) {
|
||||||
const Settlement& s = settlements[k];
|
const Settlement& s = settlements[k];
|
||||||
if (!living(s)) continue;
|
if (!living(s)) continue;
|
||||||
int key = (s.regionId >= 0) ? s.regionId : (-1 - s.bank);
|
int key = (s.regionId >= 0) ? s.regionId : (-1 - s.bank);
|
||||||
auto it = keyToCulture.find(key);
|
continentMembers[key].push_back(k);
|
||||||
int ci;
|
}
|
||||||
if (it == keyToCulture.end()) {
|
if (continentMembers.empty()) return;
|
||||||
ci = (int)cultures.size();
|
// Process continents in a deterministic (sorted-key) order -- unordered_map iteration order is
|
||||||
Culture cu; cu.id = sCultureNextId++; cu.regionId = s.regionId; cu.bank = s.bank;
|
// not guaranteed, and culture creation order feeds sCultureNextId/cultures indices, which must
|
||||||
cultures.push_back(cu);
|
// stay reproducible for a given world/seed.
|
||||||
keyToCulture[key] = ci;
|
std::vector<int> contKeys; contKeys.reserve(continentMembers.size());
|
||||||
} else ci = it->second;
|
for (const auto& kv : continentMembers) contKeys.push_back(kv.first);
|
||||||
sSettleCulture[k] = ci;
|
std::sort(contKeys.begin(), contKeys.end());
|
||||||
cultures[ci].members++;
|
auto angDist = [&](int ca, int cb) {
|
||||||
cultures[ci].totalPop += s.population;
|
return std::acos(std::clamp(cells[ca].unit.dot(cells[cb].unit), -1.0, 1.0));
|
||||||
|
};
|
||||||
|
const int perCulture = std::max(1, cfg.cultDawnSettlementsPerCulture);
|
||||||
|
const int maxPerCont = std::max(1, cfg.cultDawnMaxPerContinent);
|
||||||
|
for (int key : contKeys) {
|
||||||
|
const std::vector<size_t>& members = continentMembers[key];
|
||||||
|
int target = std::clamp((int)std::llround((double)members.size() / perCulture), 1, maxPerCont);
|
||||||
|
// Farthest-point seeding: first seed is a deterministic hash pick, then each further seed is
|
||||||
|
// whichever remaining member maximises its distance to the nearest already-chosen seed --
|
||||||
|
// spreads the initial peoples out across the continent instead of clustering them together.
|
||||||
|
std::vector<size_t> seeds;
|
||||||
|
seeds.push_back(members[cultHash((uint32_t)key * 2654435761u ^ seed ^ 0x5EED0u) % members.size()]);
|
||||||
|
while ((int)seeds.size() < target && (int)seeds.size() < (int)members.size()) {
|
||||||
|
size_t best = members[0]; double bestD = -1.0;
|
||||||
|
for (size_t m : members) {
|
||||||
|
if (std::find(seeds.begin(), seeds.end(), m) != seeds.end()) continue;
|
||||||
|
double dMin = 1e18;
|
||||||
|
for (size_t sd : seeds) dMin = std::min(dMin, angDist(settlements[m].cell, settlements[sd].cell));
|
||||||
|
if (dMin > bestD) { bestD = dMin; best = m; }
|
||||||
|
}
|
||||||
|
seeds.push_back(best);
|
||||||
|
}
|
||||||
|
// Assign every member to its nearest seed -> one geographically coherent culture per seed.
|
||||||
|
std::unordered_map<size_t, int> seedToCulture;
|
||||||
|
for (size_t m : members) {
|
||||||
|
size_t bestSeed = seeds[0]; double bestD = 1e18;
|
||||||
|
for (size_t sd : seeds) {
|
||||||
|
double d = angDist(settlements[m].cell, settlements[sd].cell);
|
||||||
|
if (d < bestD) { bestD = d; bestSeed = sd; }
|
||||||
|
}
|
||||||
|
auto it = seedToCulture.find(bestSeed);
|
||||||
|
int ci;
|
||||||
|
if (it == seedToCulture.end()) {
|
||||||
|
ci = (int)cultures.size();
|
||||||
|
Culture cu; cu.id = sCultureNextId++; cu.regionId = (key >= 0) ? key : -1;
|
||||||
|
cu.bank = settlements[bestSeed].bank;
|
||||||
|
cultures.push_back(cu);
|
||||||
|
seedToCulture[bestSeed] = ci;
|
||||||
|
} else ci = it->second;
|
||||||
|
sSettleCulture[m] = ci;
|
||||||
|
cultures[ci].members++;
|
||||||
|
cultures[ci].totalPop += settlements[m].population;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (cultures.empty()) return;
|
if (cultures.empty()) return;
|
||||||
|
|
||||||
@ -175,10 +222,13 @@ void Planet::seedCultures() {
|
|||||||
envAdd(acc[ci], cells, sMoist, sea, settlements[k].cell);
|
envAdd(acc[ci], cells, sMoist, sea, settlements[k].cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Per-culture ethos, faith and names (all deterministic hashes of the region + seed).
|
// 3) Per-culture ethos, faith and names (all deterministic hashes of the region + seed). `cu.id`
|
||||||
|
// is folded into the region key so multiple cultures sharing the same continent (and therefore
|
||||||
|
// the same regionId/bank) still get distinct ethos/faith/name hashes instead of identical ones.
|
||||||
for (size_t ci = 0; ci < cultures.size(); ++ci) {
|
for (size_t ci = 0; ci < cultures.size(); ++ci) {
|
||||||
Culture& cu = cultures[ci];
|
Culture& cu = cultures[ci];
|
||||||
uint32_t rk = (uint32_t)(cu.regionId >= 0 ? cu.regionId : (1000 - cu.bank)) + 1u;
|
uint32_t rk = cultHash(((uint32_t)(cu.regionId >= 0 ? cu.regionId : (1000 - cu.bank)) + 1u)
|
||||||
|
^ (cu.id * 2654435761u));
|
||||||
envPick(acc[ci], seed, rk, cu.members, cu.ethos, cu.faith);
|
envPick(acc[ci], seed, rk, cu.members, cu.ethos, cu.faith);
|
||||||
uint32_t nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u);
|
uint32_t nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u);
|
||||||
uint32_t faithSeed = seed ^ cultHash(rk * 40503u + 0xFA17Fu);
|
uint32_t faithSeed = seed ^ cultHash(rk * 40503u + 0xFA17Fu);
|
||||||
@ -187,19 +237,19 @@ void Planet::seedCultures() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The DERIVED refresh: seeds once when no cultures exist, then only recomputes tallies, governments
|
// The settlement-level slice of the derived refresh: seeds once when no cultures exist, then only
|
||||||
// and the per-cell culture view from the (stateful) per-settlement culture. Safe to call on every
|
// sanitizes/backfills sSettleCulture and retallies member counts -- no dependency on `nations`, so
|
||||||
// territory rebuild / load / step-back -- it never reassigns a settlement's culture (only sanitizes
|
// it's safe to call before computeTerritory() (see the Planet.hpp comment for why that ordering
|
||||||
// out-of-range entries and backfills unset living ones from the nearest cultured neighbour).
|
// matters now that a continent can seed several cultures instead of always exactly one). Never
|
||||||
void Planet::computeCultures() {
|
// reassigns a settlement's existing culture (only sanitizes out-of-range entries and backfills
|
||||||
|
// unset living ones from the nearest cultured neighbour).
|
||||||
|
void Planet::refreshSettlementCultures() {
|
||||||
const int n = (int)cells.size();
|
const int n = (int)cells.size();
|
||||||
sCellCulture.assign(n, -1);
|
|
||||||
if (settlements.empty()) { sSettleCulture.clear(); return; }
|
if (settlements.empty()) { sSettleCulture.clear(); return; }
|
||||||
if (cultures.empty()) seedCultures();
|
if (cultures.empty()) seedCultures();
|
||||||
if (cultures.empty()) return; // no living settlement yet -> seeded on a later refresh
|
if (cultures.empty()) return; // no living settlement yet -> seeded on a later refresh
|
||||||
|
|
||||||
const double abP = cfg.civAbandonPop;
|
const double abP = cfg.civAbandonPop;
|
||||||
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
|
||||||
auto living = [&](size_t k) {
|
auto living = [&](size_t k) {
|
||||||
const Settlement& s = settlements[k];
|
const Settlement& s = settlements[k];
|
||||||
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
||||||
@ -228,6 +278,19 @@ void Planet::computeCultures() {
|
|||||||
cultures[ci].members++;
|
cultures[ci].members++;
|
||||||
cultures[ci].totalPop += settlements[k].population;
|
cultures[ci].totalPop += settlements[k].population;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The DERIVED refresh: recomputes governments and the per-cell culture view from the (stateful)
|
||||||
|
// per-settlement culture (refreshed via refreshSettlementCultures() -- called here too, so this
|
||||||
|
// stays correct when called on its own, e.g. from tests). Safe to call on every territory rebuild /
|
||||||
|
// load / step-back.
|
||||||
|
void Planet::computeCultures() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
sCellCulture.assign(n, -1);
|
||||||
|
refreshSettlementCultures();
|
||||||
|
if (settlements.empty() || cultures.empty()) return;
|
||||||
|
|
||||||
|
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
||||||
|
|
||||||
// Government per realm (from tier + a deterministic pick) + fold it into the realm's name, and
|
// Government per realm (from tier + a deterministic pick) + fold it into the realm's name, and
|
||||||
// tag the realm with its capital's culture. The realm's "place name" is independently generated
|
// tag the realm with its capital's culture. The realm's "place name" is independently generated
|
||||||
|
|||||||
@ -77,6 +77,7 @@
|
|||||||
I(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
|
I(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
|
||||||
I(geoOceanDeep) I(civMaxSettlements) I(civEmpireMinMembers) I(warMaxConcurrent) I(civMaxColonies) \
|
I(geoOceanDeep) I(civMaxSettlements) I(civEmpireMinMembers) I(warMaxConcurrent) I(civMaxColonies) \
|
||||||
I(cultSchismMinMembers) I(cultSchismMinCluster) \
|
I(cultSchismMinMembers) I(cultSchismMinCluster) \
|
||||||
|
I(cultDawnSettlementsPerCulture) I(cultDawnMaxPerContinent) \
|
||||||
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
||||||
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
|
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
|
||||||
U(seed)
|
U(seed)
|
||||||
@ -355,6 +356,8 @@ 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(irng(cfg.cultDawnSettlementsPerCulture, 1, 1000000, "cultDawnSettlementsPerCulture"));
|
||||||
|
E(irng(cfg.cultDawnMaxPerContinent, 1, 1000, "cultDawnMaxPerContinent"));
|
||||||
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
||||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||||
@ -385,6 +388,13 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(irng(cfg.bioFungaPoints, 1, 100000, "bioFungaPoints"));
|
E(irng(cfg.bioFungaPoints, 1, 100000, "bioFungaPoints"));
|
||||||
E(irng(cfg.bioMarineCoastRings, 1, 100, "bioMarineCoastRings"));
|
E(irng(cfg.bioMarineCoastRings, 1, 100, "bioMarineCoastRings"));
|
||||||
|
|
||||||
|
if (cfg.subdivisions >= 0 && cfg.subdivisions <= 7) {
|
||||||
|
size_t cellCount = 10;
|
||||||
|
for (int i = 0; i < cfg.subdivisions; ++i) cellCount *= 4;
|
||||||
|
cellCount += 2;
|
||||||
|
if (cfg.plateCount > (int)cellCount)
|
||||||
|
bad.push_back("plateCount exceeds the number of cells at this subdivision level");
|
||||||
|
}
|
||||||
if (cfg.oceanBase >= cfg.continentBase)
|
if (cfg.oceanBase >= cfg.continentBase)
|
||||||
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
||||||
if (cfg.peakSoftCapStart >= cfg.peakSoftCapEnd)
|
if (cfg.peakSoftCapStart >= cfg.peakSoftCapEnd)
|
||||||
|
|||||||
@ -19,6 +19,15 @@ const char* nationTierName(NationTier t) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Planet::computeTerritory() {
|
void Planet::computeTerritory() {
|
||||||
|
// Refresh per-settlement culture BEFORE grouping into realms below -- vassalage grouping prefers
|
||||||
|
// matching by actual culture (only falling back to same-continent when culture data doesn't
|
||||||
|
// exist yet), and since a continent can seed several distinct cultures (PlanetCulture.cpp), this
|
||||||
|
// needs up-to-date sSettleCulture on every call, not just from the second recompute onward, or a
|
||||||
|
// freshly-seeded multi-culture continent would form one realm per continent for a full pass
|
||||||
|
// before the next recompute split it correctly. No dependency the other way: this only touches
|
||||||
|
// settlements/cultures, never `nations`, so it's safe to run before the rest of this function
|
||||||
|
// rebuilds `nations` from scratch.
|
||||||
|
refreshSettlementCultures();
|
||||||
const int n = (int)cells.size();
|
const int n = (int)cells.size();
|
||||||
nations.clear();
|
nations.clear();
|
||||||
sCellNation.assign(n, -1);
|
sCellNation.assign(n, -1);
|
||||||
|
|||||||
@ -402,9 +402,9 @@ struct PlanetConfig {
|
|||||||
double weatherHurricaneStr= 0.6; // strength above which a tropical system is a hurricane/typhoon
|
double weatherHurricaneStr= 0.6; // strength above which a tropical system is a hurricane/typhoon
|
||||||
|
|
||||||
// --- Volcanoes (Live World) -- see PlanetVolcano.cpp ------------------------
|
// --- Volcanoes (Live World) -- see PlanetVolcano.cpp ------------------------
|
||||||
// Placed once on entering Live World by tectonic context, then erupt on the live clock
|
// Placed once on entering Live World by tectonic context, then evolve statefully on the live
|
||||||
// (build height + eruption intensity are pure functions of liveTime, so step-back reverses
|
// clock (step-back snapshots preserve/reverse that state). Submarine volcanoes build up to
|
||||||
// them). Submarine volcanoes build up to breach sea level into new volcanic islands.
|
// breach sea level into new volcanic islands.
|
||||||
double volcanoProbRidge = 0.55; // per-cell placement prob on a young spreading-ridge cell
|
double volcanoProbRidge = 0.55; // per-cell placement prob on a young spreading-ridge cell
|
||||||
double volcanoProbBorder = 0.06; // per-cell placement prob on a normal plate-border cell
|
double volcanoProbBorder = 0.06; // per-cell placement prob on a normal plate-border cell
|
||||||
double volcanoProbInterior = 0.003; // per-cell placement prob elsewhere (intraplate hotspots)
|
double volcanoProbInterior = 0.003; // per-cell placement prob elsewhere (intraplate hotspots)
|
||||||
@ -557,4 +557,9 @@ 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
|
||||||
|
// 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.
|
||||||
|
int cultDawnSettlementsPerCulture = 5; // roughly this many dawn settlements per initial culture on a continent
|
||||||
|
int cultDawnMaxPerContinent = 6; // hard cap on how many initial peoples one continent can seed
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <map>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
static int failures = 0;
|
static int failures = 0;
|
||||||
@ -64,20 +65,25 @@ int main() {
|
|||||||
const int n = (int)p.cells.size();
|
const int n = (int)p.cells.size();
|
||||||
const double yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
const double yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
||||||
|
|
||||||
std::printf("Cultural evolution: seeding parity (frozen one-per-continent identities)\n");
|
std::printf("Cultural evolution: seeding parity (frozen dawn identities)\n");
|
||||||
p.placeSettlements();
|
p.placeSettlements();
|
||||||
double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
|
double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
|
||||||
p.computeTerritory(); p.computeCultures();
|
p.computeTerritory(); p.computeCultures();
|
||||||
{
|
{
|
||||||
check(!p.cultureList().empty(), "cultures seeded at the dawn");
|
check(!p.cultureList().empty(), "cultures seeded at the dawn");
|
||||||
// One culture per distinct key (continent regionId / -1-bank fallback) among living settlements.
|
// Each continent (regionId / -1-bank fallback) seeds 1..cultDawnMaxPerContinent cultures now,
|
||||||
std::set<int> keys;
|
// not always exactly one -- check the count stays within that bound instead.
|
||||||
|
std::map<int, std::set<int>> keyToCults;
|
||||||
for (size_t k = 0; k < p.settlements.size(); ++k) {
|
for (size_t k = 0; k < p.settlements.size(); ++k) {
|
||||||
const Settlement& s = p.settlements[k];
|
const Settlement& s = p.settlements[k];
|
||||||
if (s.population < p.cfg.civAbandonPop) continue;
|
if (s.population < p.cfg.civAbandonPop) continue;
|
||||||
keys.insert(s.regionId >= 0 ? s.regionId : (-1 - s.bank));
|
int ci = p.settleCulture()[k]; if (ci < 0) continue;
|
||||||
|
keyToCults[s.regionId >= 0 ? s.regionId : (-1 - s.bank)].insert(ci);
|
||||||
}
|
}
|
||||||
check(p.cultureList().size() == keys.size(), "one culture per inhabited continent");
|
bool boundedDiversity = true;
|
||||||
|
for (auto& kv : keyToCults)
|
||||||
|
if ((int)kv.second.size() < 1 || (int)kv.second.size() > p.cfg.cultDawnMaxPerContinent) boundedDiversity = false;
|
||||||
|
check(boundedDiversity, "each continent seeds 1..cultDawnMaxPerContinent cultures");
|
||||||
bool rootsOk = true, idsOk = true;
|
bool rootsOk = true, idsOk = true;
|
||||||
for (size_t i = 0; i < p.cultureList().size(); ++i) {
|
for (size_t i = 0; i < p.cultureList().size(); ++i) {
|
||||||
const Culture& cu = p.cultureList()[i];
|
const Culture& cu = p.cultureList()[i];
|
||||||
@ -162,6 +168,21 @@ int main() {
|
|||||||
check(capsStable, "realm capitals never convert via border pressure");
|
check(capsStable, "realm capitals never convert via border pressure");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: locked clusters cannot create empty schisms\n");
|
||||||
|
{
|
||||||
|
Planet locked = p; quietCulture(locked);
|
||||||
|
locked.cfg.cultSchismRate = 1.0; locked.cfg.cultSchismRange = 0.10;
|
||||||
|
locked.cfg.cultSchismMinMembers = 3; locked.cfg.cultSchismMinCluster = 1;
|
||||||
|
for (int s = 0; s < (int)locked.settlements.size(); ++s) locked.setCultureLock(s, true);
|
||||||
|
size_t before = locked.cultureList().size();
|
||||||
|
int breakEvents = 0;
|
||||||
|
for (long yr = 0; yr < 20; ++yr)
|
||||||
|
for (const WarEvent& e : civYear(locked, yr))
|
||||||
|
if (e.kind == 7 && e.title.find("break away from") != std::string::npos) ++breakEvents;
|
||||||
|
check(locked.cultureList().size() == before && breakEvents == 0,
|
||||||
|
"an entirely locked cluster creates neither a child culture nor a false event");
|
||||||
|
}
|
||||||
|
|
||||||
std::printf("Cultural evolution: schism (a distant cluster becomes a new people)\n");
|
std::printf("Cultural evolution: schism (a distant cluster becomes a new people)\n");
|
||||||
long schismYear = -1;
|
long schismYear = -1;
|
||||||
{
|
{
|
||||||
@ -307,7 +328,7 @@ int main() {
|
|||||||
bool allRoots = true;
|
bool allRoots = true;
|
||||||
for (const Culture& cu : r.cultureList())
|
for (const Culture& cu : r.cultureList())
|
||||||
if (cu.parentId != -1 || cu.foundedYear != -1) allRoots = false;
|
if (cu.parentId != -1 || cu.foundedYear != -1) allRoots = false;
|
||||||
check(allRoots, "re-seeded cultures are one-per-continent roots");
|
check(allRoots, "re-seeded cultures are roots (dawn peoples, no parent)");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CULTURAL-EVOLUTION CHECKS PASSED\n", failures);
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CULTURAL-EVOLUTION CHECKS PASSED\n", failures);
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
// src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp
|
// src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp
|
||||||
// src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetIO.cpp -o /tmp/tc && /tmp/tc
|
// src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetIO.cpp -o /tmp/tc && /tmp/tc
|
||||||
//
|
//
|
||||||
// Verifies: one culture per inhabited continent; every living settlement has a culture; valid
|
// Verifies: each continent seeds 1..cultDawnMaxPerContinent distinct cultures (never spanning two
|
||||||
|
// continents); every living settlement has a culture; valid
|
||||||
// ethos/faith; governments plausible per tier + reflected in the realm name; realms are mono-cultural;
|
// ethos/faith; governments plausible per tier + reflected in the realm name; realms are mono-cultural;
|
||||||
// determinism + RNG isolation; save->load->recompute parity.
|
// determinism + RNG isolation; save->load->recompute parity.
|
||||||
|
|
||||||
@ -55,33 +56,33 @@ int main() {
|
|||||||
check(!p.cultureList().empty(), "computeCultures produces cultures");
|
check(!p.cultureList().empty(), "computeCultures produces cultures");
|
||||||
check((int)p.cellCulture().size() == n && (int)p.settleCulture().size() == (int)p.settlements.size(), "index arrays sized");
|
check((int)p.cellCulture().size() == n && (int)p.settleCulture().size() == (int)p.settlements.size(), "index arrays sized");
|
||||||
|
|
||||||
std::printf("Culture: one culture per inhabited continent\n");
|
std::printf("Culture: continents seed 1..cultDawnMaxPerContinent cultures, never crossing continents\n");
|
||||||
{
|
{
|
||||||
// The set of distinct grouping keys among living settlements == the number of cultures, and
|
// A continent (grouping key) may now seed several distinct cultures (farthest-point seeding,
|
||||||
// every living settlement's key maps to exactly one culture (and vice versa).
|
// bounded by cultDawnMaxPerContinent) instead of always exactly one -- but a single culture
|
||||||
std::set<int> keys;
|
// must never itself span two continents.
|
||||||
std::map<int,int> keyToCult; // grouping key -> culture index (must be consistent)
|
std::map<int, std::set<int>> keyToCults; // continent key -> set of culture indices seen there
|
||||||
bool consistent = true, allLiving = true;
|
std::map<int, int> cultToKey; // culture index -> the one continent key it belongs to
|
||||||
|
bool singleContinent = true, allLiving = true;
|
||||||
for (size_t k = 0; k < p.settlements.size(); ++k) {
|
for (size_t k = 0; k < p.settlements.size(); ++k) {
|
||||||
const Settlement& s = p.settlements[k];
|
const Settlement& s = p.settlements[k];
|
||||||
if (s.population < abP) continue;
|
if (s.population < abP) continue;
|
||||||
int ci = p.settleCulture()[k];
|
int ci = p.settleCulture()[k];
|
||||||
if (ci < 0 || ci >= (int)p.cultureList().size()) { allLiving = false; continue; }
|
if (ci < 0 || ci >= (int)p.cultureList().size()) { allLiving = false; continue; }
|
||||||
int key = cultKey(s);
|
int key = cultKey(s);
|
||||||
keys.insert(key);
|
keyToCults[key].insert(ci);
|
||||||
auto it = keyToCult.find(key);
|
auto it = cultToKey.find(ci);
|
||||||
if (it == keyToCult.end()) keyToCult[key] = ci;
|
if (it == cultToKey.end()) cultToKey[ci] = key;
|
||||||
else if (it->second != ci) consistent = false; // same continent -> must be same culture
|
else if (it->second != key) singleContinent = false; // a culture must stay on one continent
|
||||||
}
|
}
|
||||||
// No two distinct keys share a culture.
|
bool boundedDiversity = true;
|
||||||
std::set<int> usedCults;
|
for (auto& kv : keyToCults)
|
||||||
bool distinct = true;
|
if ((int)kv.second.size() < 1 || (int)kv.second.size() > p.cfg.cultDawnMaxPerContinent) boundedDiversity = false;
|
||||||
for (auto& kv : keyToCult) { if (usedCults.count(kv.second)) distinct = false; usedCults.insert(kv.second); }
|
std::printf(" %d cultures across %d inhabited continents/keys\n",
|
||||||
std::printf(" %d cultures, %d inhabited continents/keys\n", (int)p.cultureList().size(), (int)keys.size());
|
(int)p.cultureList().size(), (int)keyToCults.size());
|
||||||
check(allLiving, "every living settlement has a valid culture");
|
check(allLiving, "every living settlement has a valid culture");
|
||||||
check(consistent, "settlements on the same continent share one culture");
|
check(singleContinent, "every culture's members all belong to one continent");
|
||||||
check(distinct, "no two continents share a culture");
|
check(boundedDiversity, "each continent seeds 1..cultDawnMaxPerContinent cultures");
|
||||||
check((int)p.cultureList().size() == (int)keys.size(), "exactly one culture per inhabited continent");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::printf("Culture: valid ethos / faith / members\n");
|
std::printf("Culture: valid ethos / faith / members\n");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user