Compare commits

..

No commits in common. "78e58171e53b5c7a6af9f4e7c488bc0ef6743212" and "a9ea113f975da310bb8653b6cd7674cd06e15f53" have entirely different histories.

9 changed files with 58 additions and 204 deletions

View File

@ -479,28 +479,6 @@ on the Live World clock). **Steps 17 of the roadmap are done (plus a derived
differs from the capital's own name. Touches `src/sim` (`PlanetCulture.cpp`, `PlanetCiv.cpp`) as
well as `src/render``Nation.name`/`cultureId` are derived/not saved (per the Territory & Culture
entries above), so no save-format version bump.
- **Multiple cultures per continent (was always exactly one)** *(done — see `Planet::seedCultures()`,
`src/sim/PlanetCulture.cpp`)* — a continent seeding only ever one culture at the dawn, combined with
the existing border-conversion/backfill dynamics, eroded into one super-dominant culture covering
the whole world over a long run. `seedCultures()` now splits each continent into
**1..`cultDawnMaxPerContinent`** initial peoples via deterministic farthest-point seeding (each
settlement joins its nearest seed point — geographically coherent, Voronoi-like regions, not a
checkerboard); the target count scales with the continent's settlement population via
`cultDawnSettlementsPerCulture` (both new `planet.cfg` knobs). Per-culture ethos/faith/name hashes
now fold in `Culture.id` so same-continent cultures (sharing `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 — harmless when one culture ≡ one continent,
but would leave temporarily multi-cultural realms now. Fixed at the root: `computeCultures()` split
into `refreshSettlementCultures()` (the settlement-level seed/backfill/tally slice, no dependency on
`nations`) which `computeTerritory()` now calls on itself before grouping — every caller (the viewer
and the dozens of existing test call sites) gets correct behaviour automatically. `test_culture.cpp`/
`test_cultevo.cpp` updated to assert the new bounded invariant (a continent has 1..N cultures; a
single culture never itself spans two continents — still enforced) instead of exact equality.
Verified live: one continent now shows six distinct peoples instead of a single dominant culture,
with realms still correctly mono-cultural. `src/sim` + `src/render`; cultures are derived/not saved,
so no save-format version bump.
## Current state
@ -1363,12 +1341,7 @@ triangles (plates are fixed in phase 1).
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
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 —
lower = more initial peoples) and `cultDawnMaxPerContinent` (6, hard cap on how many initial peoples
one continent can seed) — raise `cultDawnSettlementsPerCulture` or lower `cultDawnMaxPerContinent`
for a world that starts more culturally uniform (closer to the old always-one-per-continent
behaviour), lower/raise them the other way for more initial diversity.
capped at 64 peoples.
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
knob for how fast/high relief builds.
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks

View File

@ -892,7 +892,7 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
// 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.
void Viewer::rebuildTerritory() {
planet.computeTerritory(); // refreshes per-settlement culture itself before grouping into realms
planet.computeTerritory();
planet.computeCultures();
planet.computeTrade(); // civ Step 7: trade links + prosperity (derived)
buildNationBorders(planet, borderR, nationBorders);

View File

@ -212,22 +212,12 @@ public:
const std::vector<int>& settleNation() const { return sSettleNation; } // nation index per settlement (-1 = dead)
// Cultures, beliefs & governments (PlanetCulture.cpp). computeCultures() is the DERIVED refresh:
// it seeds the cultures once (a continent starts as 1..cultDawnMaxPerContinent peoples, split via
// deterministic farthest-point seeding) when none exist, then only recomputes tallies, governments
// (folded into nation.name) and the per-cell culture view. Runs AFTER computeTerritory() (reads
// nations/sCellNation/sCellSettleOwner). Culture identities + the per-settlement culture are
// STATEFUL since Step 8 (saved v23 + snapshotted); stepCulture() is the once-per-sim-year mutation
// 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();
// it seeds the cultures once (one per inhabited continent) when none exist, then only recomputes
// tallies, governments (folded into nation.name) and the per-cell culture view. Runs AFTER
// computeTerritory() (reads nations/sCellNation/sCellSettleOwner). Culture identities + the
// per-settlement culture are STATEFUL since Step 8 (saved v23 + snapshotted); stepCulture() is
// the once-per-sim-year mutation pass (border conversion / assimilation / schism, pure hashes --
// no RNG touched, so step-back replays it exactly).
void computeCultures();
std::vector<WarEvent> stepCulture(long year);
bool culturesBuilt() const { return !cultures.empty(); }

View File

@ -145,73 +145,26 @@ void Planet::seedCultures() {
const double sea = cfg.seaLevel, abP = cfg.civAbandonPop;
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
// 1) Group living settlements by continent, then split each continent into 1..cultDawnMaxPerContinent
// 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.
// 1) Group living settlements into cultures.
std::unordered_map<int, int> keyToCulture;
auto living = [&](const Settlement& s) {
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) {
const Settlement& s = settlements[k];
if (!living(s)) continue;
int key = (s.regionId >= 0) ? s.regionId : (-1 - s.bank);
continentMembers[key].push_back(k);
}
if (continentMembers.empty()) return;
// Process continents in a deterministic (sorted-key) order -- unordered_map iteration order is
// not guaranteed, and culture creation order feeds sCultureNextId/cultures indices, which must
// stay reproducible for a given world/seed.
std::vector<int> contKeys; contKeys.reserve(continentMembers.size());
for (const auto& kv : continentMembers) contKeys.push_back(kv.first);
std::sort(contKeys.begin(), contKeys.end());
auto angDist = [&](int ca, int cb) {
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);
auto it = keyToCulture.find(key);
int ci;
if (it == seedToCulture.end()) {
if (it == keyToCulture.end()) {
ci = (int)cultures.size();
Culture cu; cu.id = sCultureNextId++; cu.regionId = (key >= 0) ? key : -1;
cu.bank = settlements[bestSeed].bank;
Culture cu; cu.id = sCultureNextId++; cu.regionId = s.regionId; cu.bank = s.bank;
cultures.push_back(cu);
seedToCulture[bestSeed] = ci;
keyToCulture[key] = ci;
} else ci = it->second;
sSettleCulture[m] = ci;
sSettleCulture[k] = ci;
cultures[ci].members++;
cultures[ci].totalPop += settlements[m].population;
}
cultures[ci].totalPop += s.population;
}
if (cultures.empty()) return;
@ -222,13 +175,10 @@ void Planet::seedCultures() {
envAdd(acc[ci], cells, sMoist, sea, settlements[k].cell);
}
// 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.
// 3) Per-culture ethos, faith and names (all deterministic hashes of the region + seed).
for (size_t ci = 0; ci < cultures.size(); ++ci) {
Culture& cu = cultures[ci];
uint32_t rk = cultHash(((uint32_t)(cu.regionId >= 0 ? cu.regionId : (1000 - cu.bank)) + 1u)
^ (cu.id * 2654435761u));
uint32_t rk = (uint32_t)(cu.regionId >= 0 ? cu.regionId : (1000 - cu.bank)) + 1u;
envPick(acc[ci], seed, rk, cu.members, cu.ethos, cu.faith);
uint32_t nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u);
uint32_t faithSeed = seed ^ cultHash(rk * 40503u + 0xFA17Fu);
@ -237,19 +187,19 @@ void Planet::seedCultures() {
}
}
// The settlement-level slice of the derived refresh: seeds once when no cultures exist, then only
// sanitizes/backfills sSettleCulture and retallies member counts -- no dependency on `nations`, so
// it's safe to call before computeTerritory() (see the Planet.hpp comment for why that ordering
// matters now that a continent can seed several cultures instead of always exactly one). Never
// 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() {
// The DERIVED refresh: seeds once when no cultures exist, then only recomputes tallies, governments
// and the per-cell culture view from the (stateful) per-settlement culture. Safe to call on every
// territory rebuild / load / step-back -- it never reassigns a settlement's culture (only sanitizes
// out-of-range entries and backfills unset living ones from the nearest cultured neighbour).
void Planet::computeCultures() {
const int n = (int)cells.size();
sCellCulture.assign(n, -1);
if (settlements.empty()) { sSettleCulture.clear(); return; }
if (cultures.empty()) seedCultures();
if (cultures.empty()) return; // no living settlement yet -> seeded on a later refresh
const double abP = cfg.civAbandonPop;
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
auto living = [&](size_t k) {
const Settlement& s = settlements[k];
return s.cell >= 0 && s.cell < n && s.population >= abP;
@ -278,19 +228,6 @@ void Planet::refreshSettlementCultures() {
cultures[ci].members++;
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
// tag the realm with its capital's culture. The realm's "place name" is independently generated

View File

@ -77,7 +77,6 @@
I(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
I(geoOceanDeep) I(civMaxSettlements) I(civEmpireMinMembers) I(warMaxConcurrent) I(civMaxColonies) \
I(cultSchismMinMembers) I(cultSchismMinCluster) \
I(cultDawnSettlementsPerCulture) I(cultDawnMaxPerContinent) \
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
U(seed)
@ -356,8 +355,6 @@ 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(irng(cfg.cultDawnSettlementsPerCulture, 1, 1000000, "cultDawnSettlementsPerCulture"));
E(irng(cfg.cultDawnMaxPerContinent, 1, 1000, "cultDawnMaxPerContinent"));
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
E(irng(cfg.plateCount, 1, 100, "plateCount"));
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
@ -388,13 +385,6 @@ std::string validateConfig(const PlanetConfig& cfg) {
E(irng(cfg.bioFungaPoints, 1, 100000, "bioFungaPoints"));
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)
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
if (cfg.peakSoftCapStart >= cfg.peakSoftCapEnd)

View File

@ -19,15 +19,6 @@ const char* nationTierName(NationTier t) {
}
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();
nations.clear();
sCellNation.assign(n, -1);

View File

@ -402,9 +402,9 @@ struct PlanetConfig {
double weatherHurricaneStr= 0.6; // strength above which a tropical system is a hurricane/typhoon
// --- Volcanoes (Live World) -- see PlanetVolcano.cpp ------------------------
// Placed once on entering Live World by tectonic context, then evolve statefully on the live
// clock (step-back snapshots preserve/reverse that state). Submarine volcanoes build up to
// breach sea level into new volcanic islands.
// Placed once on entering Live World by tectonic context, then erupt on the live clock
// (build height + eruption intensity are pure functions of liveTime, so step-back reverses
// them). Submarine volcanoes build up to breach sea level into new volcanic islands.
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 volcanoProbInterior = 0.003; // per-cell placement prob elsewhere (intraplate hotspots)
@ -557,9 +557,4 @@ 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
// 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
};

View File

@ -20,7 +20,6 @@
#include <cmath>
#include <algorithm>
#include <set>
#include <map>
#include <sstream>
static int failures = 0;
@ -65,25 +64,20 @@ int main() {
const int n = (int)p.cells.size();
const double yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
std::printf("Cultural evolution: seeding parity (frozen dawn identities)\n");
std::printf("Cultural evolution: seeding parity (frozen one-per-continent identities)\n");
p.placeSettlements();
double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
p.computeTerritory(); p.computeCultures();
{
check(!p.cultureList().empty(), "cultures seeded at the dawn");
// Each continent (regionId / -1-bank fallback) seeds 1..cultDawnMaxPerContinent cultures now,
// not always exactly one -- check the count stays within that bound instead.
std::map<int, std::set<int>> keyToCults;
// One culture per distinct key (continent regionId / -1-bank fallback) among living settlements.
std::set<int> keys;
for (size_t k = 0; k < p.settlements.size(); ++k) {
const Settlement& s = p.settlements[k];
if (s.population < p.cfg.civAbandonPop) continue;
int ci = p.settleCulture()[k]; if (ci < 0) continue;
keyToCults[s.regionId >= 0 ? s.regionId : (-1 - s.bank)].insert(ci);
keys.insert(s.regionId >= 0 ? s.regionId : (-1 - s.bank));
}
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");
check(p.cultureList().size() == keys.size(), "one culture per inhabited continent");
bool rootsOk = true, idsOk = true;
for (size_t i = 0; i < p.cultureList().size(); ++i) {
const Culture& cu = p.cultureList()[i];
@ -168,21 +162,6 @@ int main() {
check(capsStable, "realm capitals never convert via border pressure");
}
std::printf("Cultural evolution: locked clusters cannot create empty schisms\n");
{
Planet locked = p; quietCulture(locked);
locked.cfg.cultSchismRate = 1.0; locked.cfg.cultSchismRange = 0.10;
locked.cfg.cultSchismMinMembers = 3; locked.cfg.cultSchismMinCluster = 1;
for (int s = 0; s < (int)locked.settlements.size(); ++s) locked.setCultureLock(s, true);
size_t before = locked.cultureList().size();
int breakEvents = 0;
for (long yr = 0; yr < 20; ++yr)
for (const WarEvent& e : civYear(locked, yr))
if (e.kind == 7 && e.title.find("break away from") != std::string::npos) ++breakEvents;
check(locked.cultureList().size() == before && breakEvents == 0,
"an entirely locked cluster creates neither a child culture nor a false event");
}
std::printf("Cultural evolution: schism (a distant cluster becomes a new people)\n");
long schismYear = -1;
{
@ -328,7 +307,7 @@ int main() {
bool allRoots = true;
for (const Culture& cu : r.cultureList())
if (cu.parentId != -1 || cu.foundedYear != -1) allRoots = false;
check(allRoots, "re-seeded cultures are roots (dawn peoples, no parent)");
check(allRoots, "re-seeded cultures are one-per-continent roots");
}
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CULTURAL-EVOLUTION CHECKS PASSED\n", failures);

View File

@ -9,8 +9,7 @@
// 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
//
// Verifies: each continent seeds 1..cultDawnMaxPerContinent distinct cultures (never spanning two
// continents); every living settlement has a culture; valid
// Verifies: one culture per inhabited continent; every living settlement has a culture; valid
// ethos/faith; governments plausible per tier + reflected in the realm name; realms are mono-cultural;
// determinism + RNG isolation; save->load->recompute parity.
@ -56,33 +55,33 @@ int main() {
check(!p.cultureList().empty(), "computeCultures produces cultures");
check((int)p.cellCulture().size() == n && (int)p.settleCulture().size() == (int)p.settlements.size(), "index arrays sized");
std::printf("Culture: continents seed 1..cultDawnMaxPerContinent cultures, never crossing continents\n");
std::printf("Culture: one culture per inhabited continent\n");
{
// A continent (grouping key) may now seed several distinct cultures (farthest-point seeding,
// bounded by cultDawnMaxPerContinent) instead of always exactly one -- but a single culture
// must never itself span two continents.
std::map<int, std::set<int>> keyToCults; // continent key -> set of culture indices seen there
std::map<int, int> cultToKey; // culture index -> the one continent key it belongs to
bool singleContinent = true, allLiving = true;
// The set of distinct grouping keys among living settlements == the number of cultures, and
// every living settlement's key maps to exactly one culture (and vice versa).
std::set<int> keys;
std::map<int,int> keyToCult; // grouping key -> culture index (must be consistent)
bool consistent = true, allLiving = true;
for (size_t k = 0; k < p.settlements.size(); ++k) {
const Settlement& s = p.settlements[k];
if (s.population < abP) continue;
int ci = p.settleCulture()[k];
if (ci < 0 || ci >= (int)p.cultureList().size()) { allLiving = false; continue; }
int key = cultKey(s);
keyToCults[key].insert(ci);
auto it = cultToKey.find(ci);
if (it == cultToKey.end()) cultToKey[ci] = key;
else if (it->second != key) singleContinent = false; // a culture must stay on one continent
keys.insert(key);
auto it = keyToCult.find(key);
if (it == keyToCult.end()) keyToCult[key] = ci;
else if (it->second != ci) consistent = false; // same continent -> must be same culture
}
bool boundedDiversity = true;
for (auto& kv : keyToCults)
if ((int)kv.second.size() < 1 || (int)kv.second.size() > p.cfg.cultDawnMaxPerContinent) boundedDiversity = false;
std::printf(" %d cultures across %d inhabited continents/keys\n",
(int)p.cultureList().size(), (int)keyToCults.size());
// No two distinct keys share a culture.
std::set<int> usedCults;
bool distinct = true;
for (auto& kv : keyToCult) { if (usedCults.count(kv.second)) distinct = false; usedCults.insert(kv.second); }
std::printf(" %d cultures, %d inhabited continents/keys\n", (int)p.cultureList().size(), (int)keys.size());
check(allLiving, "every living settlement has a valid culture");
check(singleContinent, "every culture's members all belong to one continent");
check(boundedDiversity, "each continent seeds 1..cultDawnMaxPerContinent cultures");
check(consistent, "settlements on the same continent share one culture");
check(distinct, "no two continents share a culture");
check((int)p.cultureList().size() == (int)keys.size(), "exactly one culture per inhabited continent");
}
std::printf("Culture: valid ethos / faith / members\n");