Compare commits
No commits in common. "78e58171e53b5c7a6af9f4e7c488bc0ef6743212" and "a9ea113f975da310bb8653b6cd7674cd06e15f53" have entirely different histories.
78e58171e5
...
a9ea113f97
29
CLAUDE.md
29
CLAUDE.md
@ -479,28 +479,6 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived
|
|||||||
differs from the capital's own name. Touches `src/sim` (`PlanetCulture.cpp`, `PlanetCiv.cpp`) as
|
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
|
well as `src/render` — `Nation.name`/`cultureId` are derived/not saved (per the Territory & Culture
|
||||||
entries above), so no save-format version bump.
|
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
|
## 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
|
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). 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.
|
||||||
`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.
|
|
||||||
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||||
knob for how fast/high relief builds.
|
knob for how fast/high relief builds.
|
||||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||||
|
|||||||
@ -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(); // refreshes per-settlement culture itself before grouping into realms
|
planet.computeTerritory();
|
||||||
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,22 +212,12 @@ 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 (a continent starts as 1..cultDawnMaxPerContinent peoples, split via
|
// it seeds the cultures once (one per inhabited continent) when none exist, then only recomputes
|
||||||
// deterministic farthest-point seeding) when none exist, then only recomputes tallies, governments
|
// tallies, governments (folded into nation.name) and the per-cell culture view. Runs AFTER
|
||||||
// (folded into nation.name) and the per-cell culture view. Runs AFTER computeTerritory() (reads
|
// computeTerritory() (reads nations/sCellNation/sCellSettleOwner). Culture identities + the
|
||||||
// nations/sCellNation/sCellSettleOwner). Culture identities + the per-settlement culture are
|
// per-settlement culture are STATEFUL since Step 8 (saved v23 + snapshotted); stepCulture() is
|
||||||
// STATEFUL since Step 8 (saved v23 + snapshotted); stepCulture() is the once-per-sim-year mutation
|
// the once-per-sim-year mutation pass (border conversion / assimilation / schism, pure hashes --
|
||||||
// pass (border conversion / assimilation / schism, pure hashes -- no RNG touched, so step-back
|
// no RNG touched, so step-back replays it exactly).
|
||||||
// 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,73 +145,26 @@ 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 by continent, then split each continent into 1..cultDawnMaxPerContinent
|
// 1) Group living settlements into cultures.
|
||||||
// initial peoples via deterministic farthest-point seeding (geographically coherent, like a
|
std::unordered_map<int, int> keyToCulture;
|
||||||
// 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);
|
||||||
continentMembers[key].push_back(k);
|
auto it = keyToCulture.find(key);
|
||||||
}
|
int ci;
|
||||||
if (continentMembers.empty()) return;
|
if (it == keyToCulture.end()) {
|
||||||
// Process continents in a deterministic (sorted-key) order -- unordered_map iteration order is
|
ci = (int)cultures.size();
|
||||||
// not guaranteed, and culture creation order feeds sCultureNextId/cultures indices, which must
|
Culture cu; cu.id = sCultureNextId++; cu.regionId = s.regionId; cu.bank = s.bank;
|
||||||
// stay reproducible for a given world/seed.
|
cultures.push_back(cu);
|
||||||
std::vector<int> contKeys; contKeys.reserve(continentMembers.size());
|
keyToCulture[key] = ci;
|
||||||
for (const auto& kv : continentMembers) contKeys.push_back(kv.first);
|
} else ci = it->second;
|
||||||
std::sort(contKeys.begin(), contKeys.end());
|
sSettleCulture[k] = ci;
|
||||||
auto angDist = [&](int ca, int cb) {
|
cultures[ci].members++;
|
||||||
return std::acos(std::clamp(cells[ca].unit.dot(cells[cb].unit), -1.0, 1.0));
|
cultures[ci].totalPop += s.population;
|
||||||
};
|
|
||||||
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;
|
||||||
|
|
||||||
@ -222,13 +175,10 @@ 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). `cu.id`
|
// 3) Per-culture ethos, faith and names (all deterministic hashes of the region + seed).
|
||||||
// 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 = cultHash(((uint32_t)(cu.regionId >= 0 ? cu.regionId : (1000 - cu.bank)) + 1u)
|
uint32_t rk = (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);
|
||||||
@ -237,19 +187,19 @@ void Planet::seedCultures() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The settlement-level slice of the derived refresh: seeds once when no cultures exist, then only
|
// The DERIVED refresh: seeds once when no cultures exist, then only recomputes tallies, governments
|
||||||
// sanitizes/backfills sSettleCulture and retallies member counts -- no dependency on `nations`, so
|
// and the per-cell culture view from the (stateful) per-settlement culture. Safe to call on every
|
||||||
// it's safe to call before computeTerritory() (see the Planet.hpp comment for why that ordering
|
// territory rebuild / load / step-back -- it never reassigns a settlement's culture (only sanitizes
|
||||||
// matters now that a continent can seed several cultures instead of always exactly one). Never
|
// out-of-range entries and backfills unset living ones from the nearest cultured neighbour).
|
||||||
// reassigns a settlement's existing culture (only sanitizes out-of-range entries and backfills
|
void Planet::computeCultures() {
|
||||||
// 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;
|
||||||
@ -278,19 +228,6 @@ void Planet::refreshSettlementCultures() {
|
|||||||
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,7 +77,6 @@
|
|||||||
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)
|
||||||
@ -356,8 +355,6 @@ 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"));
|
||||||
@ -388,13 +385,6 @@ 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,15 +19,6 @@ 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 evolve statefully on the live
|
// Placed once on entering Live World by tectonic context, then erupt on the live clock
|
||||||
// clock (step-back snapshots preserve/reverse that state). Submarine volcanoes build up to
|
// (build height + eruption intensity are pure functions of liveTime, so step-back reverses
|
||||||
// breach sea level into new volcanic islands.
|
// 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 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,9 +557,4 @@ 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,7 +20,6 @@
|
|||||||
#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;
|
||||||
@ -65,25 +64,20 @@ 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 dawn identities)\n");
|
std::printf("Cultural evolution: seeding parity (frozen one-per-continent 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");
|
||||||
// Each continent (regionId / -1-bank fallback) seeds 1..cultDawnMaxPerContinent cultures now,
|
// One culture per distinct key (continent regionId / -1-bank fallback) among living settlements.
|
||||||
// not always exactly one -- check the count stays within that bound instead.
|
std::set<int> keys;
|
||||||
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;
|
||||||
int ci = p.settleCulture()[k]; if (ci < 0) continue;
|
keys.insert(s.regionId >= 0 ? s.regionId : (-1 - s.bank));
|
||||||
keyToCults[s.regionId >= 0 ? s.regionId : (-1 - s.bank)].insert(ci);
|
|
||||||
}
|
}
|
||||||
bool boundedDiversity = true;
|
check(p.cultureList().size() == keys.size(), "one culture per inhabited continent");
|
||||||
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];
|
||||||
@ -168,21 +162,6 @@ 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;
|
||||||
{
|
{
|
||||||
@ -328,7 +307,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 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);
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CULTURAL-EVOLUTION CHECKS PASSED\n", failures);
|
||||||
|
|||||||
@ -9,8 +9,7 @@
|
|||||||
// 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: each continent seeds 1..cultDawnMaxPerContinent distinct cultures (never spanning two
|
// Verifies: one culture per inhabited continent; every living settlement has a culture; valid
|
||||||
// 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.
|
||||||
|
|
||||||
@ -56,33 +55,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: 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,
|
// The set of distinct grouping keys among living settlements == the number of cultures, and
|
||||||
// bounded by cultDawnMaxPerContinent) instead of always exactly one -- but a single culture
|
// every living settlement's key maps to exactly one culture (and vice versa).
|
||||||
// must never itself span two continents.
|
std::set<int> keys;
|
||||||
std::map<int, std::set<int>> keyToCults; // continent key -> set of culture indices seen there
|
std::map<int,int> keyToCult; // grouping key -> culture index (must be consistent)
|
||||||
std::map<int, int> cultToKey; // culture index -> the one continent key it belongs to
|
bool consistent = true, allLiving = true;
|
||||||
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);
|
||||||
keyToCults[key].insert(ci);
|
keys.insert(key);
|
||||||
auto it = cultToKey.find(ci);
|
auto it = keyToCult.find(key);
|
||||||
if (it == cultToKey.end()) cultToKey[ci] = key;
|
if (it == keyToCult.end()) keyToCult[key] = ci;
|
||||||
else if (it->second != key) singleContinent = false; // a culture must stay on one continent
|
else if (it->second != ci) consistent = false; // same continent -> must be same culture
|
||||||
}
|
}
|
||||||
bool boundedDiversity = true;
|
// No two distinct keys share a culture.
|
||||||
for (auto& kv : keyToCults)
|
std::set<int> usedCults;
|
||||||
if ((int)kv.second.size() < 1 || (int)kv.second.size() > p.cfg.cultDawnMaxPerContinent) boundedDiversity = false;
|
bool distinct = true;
|
||||||
std::printf(" %d cultures across %d inhabited continents/keys\n",
|
for (auto& kv : keyToCult) { if (usedCults.count(kv.second)) distinct = false; usedCults.insert(kv.second); }
|
||||||
(int)p.cultureList().size(), (int)keyToCults.size());
|
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(allLiving, "every living settlement has a valid culture");
|
||||||
check(singleContinent, "every culture's members all belong to one continent");
|
check(consistent, "settlements on the same continent share one culture");
|
||||||
check(boundedDiversity, "each continent seeds 1..cultDawnMaxPerContinent cultures");
|
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");
|
std::printf("Culture: valid ethos / faith / members\n");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user