diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index c55309a..19eb605 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -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(); + planet.computeTerritory(); // refreshes per-settlement culture itself before grouping into realms planet.computeCultures(); planet.computeTrade(); // civ Step 7: trade links + prosperity (derived) buildNationBorders(planet, borderR, nationBorders); diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index de55e88..65638ba 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -212,12 +212,22 @@ public: const std::vector& 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 (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). + // 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(); void computeCultures(); std::vector stepCulture(long year); bool culturesBuilt() const { return !cultures.empty(); } diff --git a/src/sim/PlanetCulture.cpp b/src/sim/PlanetCulture.cpp index f3a4c7e..bb0a1e1 100644 --- a/src/sim/PlanetCulture.cpp +++ b/src/sim/PlanetCulture.cpp @@ -145,26 +145,73 @@ void Planet::seedCultures() { const double sea = cfg.seaLevel, abP = cfg.civAbandonPop; const uint32_t seed = cfg.seed ? cfg.seed : 1u; - // 1) Group living settlements into cultures. - std::unordered_map keyToCulture; + // 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. auto living = [&](const Settlement& s) { return s.cell >= 0 && s.cell < n && s.population >= abP; }; + std::unordered_map> 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); - auto it = keyToCulture.find(key); - int ci; - if (it == keyToCulture.end()) { - ci = (int)cultures.size(); - Culture cu; cu.id = sCultureNextId++; cu.regionId = s.regionId; cu.bank = s.bank; - cultures.push_back(cu); - keyToCulture[key] = ci; - } else ci = it->second; - sSettleCulture[k] = ci; - cultures[ci].members++; - cultures[ci].totalPop += s.population; + 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 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& 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 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 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; @@ -175,10 +222,13 @@ 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). + // 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) { 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); uint32_t nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u); 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 -// 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() { +// 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() { 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; @@ -228,6 +278,19 @@ void Planet::computeCultures() { 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 diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 711fdd6..5b018d7 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -77,6 +77,7 @@ 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) @@ -355,6 +356,8 @@ 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")); @@ -385,6 +388,13 @@ 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) diff --git a/src/sim/PlanetNation.cpp b/src/sim/PlanetNation.cpp index c340c74..b00ef9d 100644 --- a/src/sim/PlanetNation.cpp +++ b/src/sim/PlanetNation.cpp @@ -19,6 +19,15 @@ 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); diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index ce34fae..11a5e15 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -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 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. + // 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. 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,4 +557,9 @@ 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 }; diff --git a/test_cultevo.cpp b/test_cultevo.cpp index 69e0704..bf43d5a 100644 --- a/test_cultevo.cpp +++ b/test_cultevo.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include static int failures = 0; @@ -64,20 +65,25 @@ 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 one-per-continent identities)\n"); + std::printf("Cultural evolution: seeding parity (frozen dawn 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"); - // One culture per distinct key (continent regionId / -1-bank fallback) among living settlements. - std::set keys; + // 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> keyToCults; for (size_t k = 0; k < p.settlements.size(); ++k) { const Settlement& s = p.settlements[k]; 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; for (size_t i = 0; i < p.cultureList().size(); ++i) { const Culture& cu = p.cultureList()[i]; @@ -162,6 +168,21 @@ 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; { @@ -307,7 +328,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 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); diff --git a/test_culture.cpp b/test_culture.cpp index 32408e9..c94b6c0 100644 --- a/test_culture.cpp +++ b/test_culture.cpp @@ -9,7 +9,8 @@ // 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: 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; // determinism + RNG isolation; save->load->recompute parity. @@ -55,33 +56,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: 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 - // every living settlement's key maps to exactly one culture (and vice versa). - std::set keys; - std::map keyToCult; // grouping key -> culture index (must be consistent) - bool consistent = true, allLiving = true; + // 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> keyToCults; // continent key -> set of culture indices seen there + std::map 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) { 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); - 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 + 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 } - // No two distinct keys share a culture. - std::set 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()); + 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()); check(allLiving, "every living settlement has a valid culture"); - 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"); + check(singleContinent, "every culture's members all belong to one continent"); + check(boundedDiversity, "each continent seeds 1..cultDawnMaxPerContinent cultures"); } std::printf("Culture: valid ethos / faith / members\n");