#include "Planet.hpp" #include "NameGen.hpp" #include #include #include #include // --- Civilization Step 4 + Step 8: cultures, beliefs, governments & cultural evolution -------------- // Settlements share a CULTURE (a people/language family with an environment-driven ethos + a religion); // each realm gets a GOVERNMENT type folded into its name. Since Step 8 the culture layer is split: // - seedCultures() one-time seeding at the dawn (one culture per inhabited continent) -- also runs // for pre-v23 loads. Identities (name/ethos/faith) are FROZEN at creation. // - computeCultures() the DERIVED refresh (tallies, governments, per-cell view). Runs on every // territory rebuild / load / step-back, so it must never mutate identity state. // - stepCulture() the once-per-sim-year mutation pass: assimilation (conquered settlements adopt // their ruler's culture), border conversion (dominant foreign cultural pressure) // and schism (a far-flung cluster becomes a new people). Pure hashes of // (stable id, year, seed) -- no RNG touched, so step-back replays it exactly. // The culture list is append-only and records are immutable after creation (indices/colors stay // stable; extinct cultures keep their slot); the list + sSettleCulture are saved (v23) + snapshotted. namespace { // A pure hash for deterministic picks -- never touches rngState (mirrors civHash in PlanetCiv.cpp). inline uint32_t cultHash(uint32_t a) { a ^= a << 13; a ^= a >> 17; a ^= a << 5; return a ? a : 1u; } inline double cultHashf(uint32_t a) { return (cultHash(a) & 0xFFFFFFu) / double(0x1000000); } // [0,1) // Environmental tally over a set of settlement cells -> the signals that pick an ethos + faith. // Factored out of the old computeCultures() so schism children re-derive theirs the same way. struct CultEnv { int cnt = 0, coast = 0, high = 0, arid = 0, fert = 0; int biome[13] = {0}; }; void envAdd(CultEnv& a, const std::vector& cells, const std::vector& moist, double sea, int c) { if (c < 0 || c >= (int)cells.size()) return; a.cnt++; bool coast = false; for (int j : cells[c].neighbors) if (cells[j].elevation <= sea) { coast = true; break; } if (coast) a.coast++; Biome b = cells[c].biome; if ((int)b >= 0 && (int)b < 13) a.biome[(int)b]++; if (b == Biome::Mountains || b == Biome::Hills) a.high++; if (b == Biome::Desert || (moist.size() == cells.size() && moist[c] < 0.25)) a.arid++; if (b == Biome::Grassland || b == Biome::Forest || b == Biome::Wetland || b == Biome::Savanna) a.fert++; } // Ethos: the dominant environmental signal; if none is strong, a deterministic social pick. // Faith: coastal peoples worship the Sea, else the dominant biome sets it, with a rare variation. void envPick(const CultEnv& a, uint32_t seed, uint32_t rk, int members, CultureEthos& ethos, Faith& faith) { double cnt = std::max(1, a.cnt); double fCoast = a.coast / cnt, fHigh = a.high / cnt, fArid = a.arid / cnt, fFert = a.fert / cnt; CultureEthos best = CultureEthos::Agrarian; double bestF = fFert; if (fCoast > bestF) { bestF = fCoast; best = CultureEthos::Seafaring; } if (fHigh > bestF) { bestF = fHigh; best = CultureEthos::Highland; } if (fArid > bestF) { bestF = fArid; best = CultureEthos::Nomadic; } if (bestF >= 0.34) ethos = best; else { uint32_t h = cultHash(seed ^ (rk * 2654435761u) ^ 0xC0FFEEu); ethos = (members >= 3 && h % 3u == 0u) ? CultureEthos::Mercantile : (h % 2u ? CultureEthos::Warlike : CultureEthos::Agrarian); } Faith f; if (fCoast >= 0.5) f = Faith::Sea; else { int dom = 0; for (int bi = 1; bi < 13; ++bi) if (a.biome[bi] > a.biome[dom]) dom = bi; switch ((Biome)dom) { case Biome::Mountains: case Biome::Hills: f = Faith::Sky; break; case Biome::Desert: case Biome::Savanna: f = Faith::Sun; break; case Biome::Tundra: case Biome::Taiga: case Biome::Ice: f = Faith::Ancestors; break; case Biome::Forest: case Biome::Wetland: f = Faith::Harvest; break; case Biome::Beach: case Biome::Lake: case Biome::Ocean:f = Faith::Sea; break; default: f = Faith::Earth; break; } uint32_t h = cultHash(seed ^ (rk * 40503u) ^ 0xFA17u); if (h % 7u == 0u) f = Faith::Moon; else if (h % 13u == 0u) f = Faith::War; } faith = f; } // The faith's proper name from a seed + language bank ("the X Faith" / "Cult of X" / "the X Path"). std::string makeFaithName(uint32_t faithSeed, int bank) { std::string root = namegen::makeName(faithSeed, bank); switch (cultHash(faithSeed) % 3u) { case 0: return "the " + root + " Faith"; case 1: return "Cult of " + root; default: return "the " + root + " Path"; } } } const char* cultureEthosName(CultureEthos e) { switch (e) { case CultureEthos::Agrarian: return "Agrarian"; case CultureEthos::Seafaring: return "Seafaring"; case CultureEthos::Nomadic: return "Nomadic"; case CultureEthos::Highland: return "Highland"; case CultureEthos::Mercantile: return "Mercantile"; case CultureEthos::Warlike: return "Warlike"; } return "Agrarian"; } const char* faithFocusName(Faith f) { switch (f) { case Faith::Sun: return "Sun"; case Faith::Moon: return "Moon"; case Faith::Sea: return "Sea"; case Faith::Sky: return "Sky"; case Faith::Earth: return "Earth"; case Faith::Ancestors: return "Ancestors"; case Faith::War: return "War"; case Faith::Harvest: return "Harvest"; } return "Earth"; } const char* govTypeName(GovType g) { switch (g) { case GovType::Tribe: return "Chiefdom"; case GovType::CityRepublic: return "Republic"; case GovType::Duchy: return "Duchy"; case GovType::Kingdom: return "Kingdom"; case GovType::Theocracy: return "Theocracy"; case GovType::Confederation: return "Confederation"; case GovType::Empire: return "Empire"; case GovType::Autocracy: return "Autocracy"; } return "Kingdom"; } // One-time seeding: group living settlements into cultures, one per inhabited continent (regionId; // settlements with no geography fall back to a per-language-bank culture), then freeze each culture's // ethos/faith/names. Byte-identical to the pre-Step-8 derivation, so a pre-v23 load or the dawn // produces the same peoples as before. void Planet::seedCultures() { cultures.clear(); sSettleCulture.assign(settlements.size(), -1); sCultureNextId = 1; if (settlements.empty()) return; const int n = (int)cells.size(); 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; auto living = [&](const Settlement& s) { return s.cell >= 0 && s.cell < n && s.population >= abP; }; 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; } if (cultures.empty()) return; // 2) Environmental tally over each culture's settlement cells. std::vector acc(cultures.size()); for (size_t k = 0; k < settlements.size(); ++k) { int ci = sSettleCulture[k]; if (ci < 0) continue; envAdd(acc[ci], cells, sMoist, sea, settlements[k].cell); } // 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 = (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); cu.name = "the " + namegen::makeName(nameSeed, cu.bank); cu.faithName = makeFaithName(faithSeed, cu.bank); } } // 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; }; // Per-settlement culture is STATE (Step 8): resize/sanitize + backfill only, never reassign. if (sSettleCulture.size() != settlements.size()) sSettleCulture.resize(settlements.size(), -1); for (int& c : sSettleCulture) if (c < -1 || c >= (int)cultures.size()) c = -1; for (size_t k = 0; k < settlements.size(); ++k) { if (!living(k) || sSettleCulture[k] >= 0) continue; int bestS = -1; double bestD = 1e9; // nearest living cultured settlement (deterministic) for (size_t j = 0; j < settlements.size(); ++j) { if (j == k || !living(j) || sSettleCulture[j] < 0) continue; double d = std::acos(std::clamp( cells[settlements[k].cell].unit.dot(cells[settlements[j].cell].unit), -1.0, 1.0)); if (d < bestD) { bestD = d; bestS = (int)j; } } if (bestS >= 0) sSettleCulture[k] = sSettleCulture[bestS]; } // Derived tallies (living settlements only; extinct cultures keep their slot with members = 0). for (Culture& cu : cultures) { cu.members = 0; cu.totalPop = 0.0; } for (size_t k = 0; k < settlements.size(); ++k) { int ci = sSettleCulture[k]; if (ci < 0 || !living(k)) continue; cultures[ci].members++; cultures[ci].totalPop += settlements[k].population; } // Government per realm (from tier + a deterministic pick) + fold it into the realm's name, and // tag the realm with its capital's culture. for (Nation& nat : nations) { nat.cultureId = (nat.capital >= 0 && nat.capital < (int)sSettleCulture.size()) ? sSettleCulture[nat.capital] : -1; uint32_t gh = cultHash((nat.id * 2654435761u) ^ seed ^ 0x604Fu) % 3u; switch (nat.tier) { case NationTier::CityState: nat.gov = (gh == 0) ? GovType::CityRepublic : (gh == 1) ? GovType::Tribe : GovType::Theocracy; break; case NationTier::Kingdom: nat.gov = (gh == 0) ? GovType::Kingdom : (gh == 1) ? GovType::Duchy : GovType::Theocracy; break; case NationTier::Empire: nat.gov = (gh == 0) ? GovType::Empire : (gh == 1) ? GovType::Autocracy: GovType::Confederation; break; } const std::string& capName = settlements[nat.capital].name; switch (nat.gov) { case GovType::Tribe: nat.name = "Chiefdom of " + capName; break; case GovType::CityRepublic: nat.name = "Republic of " + capName; break; case GovType::Duchy: nat.name = "Duchy of " + capName; break; case GovType::Kingdom: nat.name = "Kingdom of " + capName; break; case GovType::Theocracy: nat.name = capName + " Theocracy"; break; case GovType::Confederation: nat.name = capName + " Confederation"; break; case GovType::Empire: nat.name = capName + " Empire"; break; case GovType::Autocracy: nat.name = capName + " Dominion"; break; } } // Per-cell culture: prefer the owning SETTLEMENT's culture (Step 8 -- a conquered city keeps its // people's colour inside the conqueror's realm until it assimilates); fall back to the realm's. const bool haveOwner = (int)sCellSettleOwner.size() == n; const bool haveNation = (int)sCellNation.size() == n; if (!haveNation) return; for (int i = 0; i < n; ++i) { int ni = sCellNation[i]; if (ni < 0 || ni >= (int)nations.size()) continue; int cu = -1; if (haveOwner) { int ow = sCellSettleOwner[i]; if (ow >= 0 && ow < (int)sSettleCulture.size()) cu = sSettleCulture[ow]; } sCellCulture[i] = (cu >= 0) ? cu : nations[ni].cultureId; } } // --- Civilization Step 8: the yearly cultural-evolution pass ---------------------------------------- // Three passes, all pure hashes of (stable id, year, seed) so a step-back replays them exactly: // A) assimilation -- a settlement held by a foreign-culture overlord adopts the ruler's culture // (the Step-5 revolt cultBonus then drops on its own: assimilation pacifies provinces); // B) border conversion -- a settlement dwarfed by a nearby foreign culture's weight (population + // trade prestige) converts toward it (realm capitals are exempt: they anchor identity); // C) schism -- a large culture's far-flung coherent cluster (overseas colonies) breaks away as a // NEW people: a fresh name from the local language bank, ethos/faith re-derived from its own // lands (appended to `cultures`; step-back truncates the list and replay re-creates it). std::vector Planet::stepCulture(long year) { std::vector ev; const int n = (int)cells.size(); if (settlements.empty() || cultures.empty()) return ev; if (sSettleCulture.size() != settlements.size()) return ev; // refresh hasn't run yet const double abP = cfg.civAbandonPop; const uint32_t seed = cfg.seed ? cfg.seed : 1u; const size_t ns = settlements.size(); auto living = [&](size_t k) { const Settlement& s = settlements[k]; return s.cell >= 0 && s.cell < n && s.population >= abP; }; auto ang = [&](const Vec3& a, const Vec3& b) { return std::acos(std::clamp(a.dot(b), -1.0, 1.0)); }; auto unitOf = [&](size_t k) -> const Vec3& { return cells[settlements[k].cell].unit; }; std::vector converted(ns, 0); // at most one culture change per settlement per year // A) Assimilation under foreign rule. if (sSettleAllegiance.size() == ns && cfg.cultAssimRate > 0.0) { for (size_t s = 0; s < ns; ++s) { if (!living(s)) continue; int ov = sSettleAllegiance[s]; if (ov < 0 || ov >= (int)ns || ov == (int)s || !living((size_t)ov)) continue; int myCult = sSettleCulture[s], ovCult = sSettleCulture[ov]; if (ovCult < 0 || ovCult >= (int)cultures.size() || myCult == ovCult) continue; if (cultHashf(settlements[s].id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0xA5513Au) >= cfg.cultAssimRate) continue; sSettleCulture[s] = ovCult; converted[s] = 1; ev.push_back(WarEvent{ 1, settlements[s].cell, settlements[s].name + " adopts the culture of " + cultures[ovCult].name, "Generations under foreign rule erode the old ways.", 7 }); } } // B) Border conversion under dominant foreign cultural pressure. if (cfg.cultConvertRate > 0.0) { std::vector isCapital(ns, 0); for (const Nation& nat : nations) if (nat.capital >= 0 && nat.capital < (int)ns) isCapital[nat.capital] = 1; const bool haveProsp = sProsperity.size() == ns; std::vector w(ns, 0.0); // cultural weight: population x (1 + prestige from trade) for (size_t k = 0; k < ns; ++k) if (living(k)) w[k] = settlements[k].population * (1.0 + cfg.cultPrestigeWeight * (haveProsp ? sProsperity[k] : 0.0)); const double range = std::max(1e-6, cfg.cultSpreadRange); std::vector pressure(cultures.size(), 0.0); for (size_t s = 0; s < ns; ++s) { if (!living(s) || converted[s] || isCapital[s]) continue; int myCult = sSettleCulture[s]; if (myCult < 0 || myCult >= (int)cultures.size()) continue; std::fill(pressure.begin(), pressure.end(), 0.0); for (size_t j = 0; j < ns; ++j) { if (j == s || !living(j)) continue; int cj = sSettleCulture[j]; if (cj < 0 || cj >= (int)cultures.size()) continue; double d = ang(unitOf(s), unitOf(j)); if (d < range) pressure[cj] += w[j] * (1.0 - d / range); } double own = pressure[myCult] + w[s]; int cBest = -1; double pBest = 0.0; for (size_t c = 0; c < cultures.size(); ++c) if ((int)c != myCult && pressure[c] > pBest) { pBest = pressure[c]; cBest = (int)c; } if (cBest < 0 || own <= 0.0 || pBest <= cfg.cultConvertDominance * own) continue; double rate = cfg.cultConvertRate * std::min(3.0, pBest / (cfg.cultConvertDominance * own)); if (cultHashf(settlements[s].id * 2654435761u ^ (uint32_t)year * 19349663u ^ seed ^ 0xB07DE4u) >= rate) continue; sSettleCulture[s] = cBest; converted[s] = 1; ev.push_back(WarEvent{ 1, settlements[s].cell, settlements[s].name + " embraces the ways of " + cultures[cBest].name, "Kinship and trade draw the town into a foreign sphere.", 7 }); } } // C) Schism: a distant coherent cluster of a large culture becomes a new people. const size_t ncult = cultures.size(); // iterate the pre-pass list (children append) if (cfg.cultSchismRate > 0.0 && (int)ncult < 64) { std::vector memCount(ncult, 0); std::vector cSum(ncult); for (size_t s = 0; s < ns; ++s) { if (!living(s)) continue; int ci = sSettleCulture[s]; if (ci < 0 || ci >= (int)ncult) continue; memCount[ci]++; cSum[ci] = cSum[ci] + unitOf(s) * std::max(1.0, settlements[s].population); } const double range = std::max(1e-6, cfg.cultSpreadRange); for (size_t ci = 0; ci < ncult && (int)cultures.size() < 64; ++ci) { if (memCount[ci] < std::max(1, cfg.cultSchismMinMembers)) continue; if (cSum[ci].length() <= 1e-12) continue; Vec3 centroid = cSum[ci].normalized(); // population-weighted cultural core // One deterministic roll per culture-year (cheap early exit; pure hash, no state consumed). if (cultHashf(cultures[ci].id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0x5C1531u) >= cfg.cultSchismRate) continue; // Members far from the core; the breakaway cluster = the farthest one + distant members // near it (a coherent region / overseas colony group, not scattered strays). std::vector distant; int far = -1; double farD = 0.0; for (size_t s = 0; s < ns; ++s) { if (!living(s) || sSettleCulture[s] != (int)ci) continue; double d = ang(unitOf(s), centroid); if (d > cfg.cultSchismRange) { distant.push_back((int)s); if (d > farD) { farD = d; far = (int)s; } } } if (far < 0 || (int)distant.size() < std::max(1, cfg.cultSchismMinCluster)) continue; std::vector cluster; for (int s : distant) if (ang(unitOf((size_t)s), unitOf((size_t)far)) <= range) cluster.push_back(s); if ((int)cluster.size() < std::max(1, cfg.cultSchismMinCluster)) continue; if ((int)cluster.size() >= memCount[ci]) continue; // a schism splits, it never renames all // Copy the parent fields we need BEFORE the push_back (it invalidates references). const uint32_t parId = cultures[ci].id; const std::string parName = cultures[ci].name; const Faith parFaith = cultures[ci].faith; const std::string parFaithName = cultures[ci].faithName; const int parBank = cultures[ci].bank; // Majority regionId of the cluster (ties resolve by cluster order -- deterministic). std::unordered_map regCount; int regBest = -1, regBestN = 0; for (int s : cluster) { int c = ++regCount[settlements[s].regionId]; if (c > regBestN) { regBestN = c; regBest = settlements[s].regionId; } } Culture child; child.id = sCultureNextId++; child.parentId = (int)parId; child.foundedYear = year; child.regionId = regBest; child.bank = (regBest >= 0) ? namegen::bankForRegion(cfg.seed, regBest) : parBank; // Ethos/faith re-derived from the cluster's own lands (hash-salted by the child id so the // faith can mutate); keep the parent's faith name if the faith itself is unchanged. CultEnv env; for (int s : cluster) envAdd(env, cells, sMoist, cfg.seaLevel, settlements[s].cell); uint32_t rk = child.id * 0x9E3779B9u + 0xC41Du; envPick(env, seed, rk, (int)cluster.size(), child.ethos, child.faith); uint32_t nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u); std::string root = namegen::makeName(nameSeed, child.bank); auto usedName = [&](const std::string& r) { for (const Culture& c : cultures) if (c.name == "the " + r) return true; return false; }; for (int g = 0; usedName(root) && g < 128; ++g) root = namegen::makeName(nameSeed += 0x9E3779B9u, child.bank); child.name = "the " + root; child.faithName = (child.faith == parFaith) ? parFaithName : makeFaithName(seed ^ cultHash(rk * 40503u + 0xFA17Fu), child.bank); int childIdx = (int)cultures.size(); for (int s : cluster) { sSettleCulture[s] = childIdx; converted[s] = 1; } std::string title = child.name + " break away from " + parName; title[0] = (char)std::toupper((unsigned char)title[0]); ev.push_back(WarEvent{ 2, settlements[far].cell, title, "A distant people drifts apart and names itself anew.", 7 }); cultures.push_back(std::move(child)); } } return ev; }