Civ growth rebalance: urban crowding plateau, plagues, good-year cap
Cities used to climb a hugely stacked carrying capacity (K up to 20-40M for the best trade hubs -- civMaxPopulation x habitability x siteQuality x conditions x trade) at ~2%/yr for millennia: bounded in principle, endless in practice. Three fixes, all population-only + derived vectors (pure hashes, no RNG, no save-version bump, step-back exact): - Urban crowding: mortality rises with the square of city size (civCrowdingLoss x (P/civMetropolisPop)^2 per year), so the best hubs PLATEAU at a historical metropolis scale (~1-1.5M) instead of chasing K; negligible below ~50k, not a clamp. - Good-year cap (civCondBoomCap): a lucky harvest no longer inflates the K target by 70% (the logistic chased booms at full rate while famine corrected busts slowly -- an upward ratchet); droughts stay uncapped. - Plagues (civPlague*): rare deterministic epidemics (1-3-year waves, 20-40% deaths at full exposure) strike cities (exposure 0 below ~30k), harder when trade-connected -- contagion travels the routes, the historical check on big hubs. Derived sCivPlague + cell-info PLAGUE line + "Plague ravages X" / "Plague shrinks X" kind-3 events. Retuned: civMaxPopulation 2e6 -> 1e6 (it is a capacity SCALE, not a cap -- comment fixed), civEmpirePop 5e6 -> 2.5e6 for the new sizes. test_civ gains a plateau/plague section: with the new model the largest city settles ~1.1M vs 3.7M-and-climbing without it; villages never plague; waves are deterministic, twin-identical and rewind exactly. All 16 suites pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4cb0ef9328
commit
ab2c4024f8
33
CLAUDE.md
33
CLAUDE.md
@ -146,7 +146,16 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived
|
|||||||
year-to-year **harvests** (bigger swings in continental interiors), rare **cold years** and river
|
year-to-year **harvests** (bigger swings in continental interiors), rare **cold years** and river
|
||||||
**floods**, plus **volcano ash**. **Storms** over a town kill people directly (hurricanes worst,
|
**floods**, plus **volcano ash**. **Storms** over a town kill people directly (hurricanes worst,
|
||||||
`civStormDeathRate`/`civHurricaneDeathMult`); sustained famine / acute disasters can **collapse** a
|
`civStormDeathRate`/`civHurricaneDeathMult`); sustained famine / acute disasters can **collapse** a
|
||||||
settlement to ruins (kept in the set, can revive). Tiers village→town→city; markers (3D + 2D) sized by
|
settlement to ruins (kept in the set, can revive). **Big-city demography** (the growth rebalance —
|
||||||
|
cities used to climb a huge stacked K exponentially for millennia, "endless growth"): an **urban
|
||||||
|
crowding** mortality rises with the square of city size (`civCrowdingLoss·(P/civMetropolisPop)²`), so
|
||||||
|
the best trade hubs **plateau at a historical metropolis scale (~1–2M)** instead of chasing K; the
|
||||||
|
good-year `cond` upside in the K *target* is capped (`civCondBoomCap` — no boom-year ratchet; droughts
|
||||||
|
still bite uncapped); and rare deterministic **plagues** (`civPlague*`, pure hash of (id, year, seed) —
|
||||||
|
a 1–3-year wave killing 20–40% at full exposure) strike **cities** (exposure 0 below ~30k) harder when
|
||||||
|
**trade-connected** (contagion travels the routes — the cost of being a hub; derived `sCivPlague`, a
|
||||||
|
cell-info `PLAGUE` line + "Plague ravages X" / "Plague shrinks X" kind=3 events). Tiers
|
||||||
|
village→town→city; markers (3D + 2D) sized by
|
||||||
tier and **withered-tinted** by hardship + a **Civ** tab + cell-info "conditions/drought" line + kind=3
|
tier and **withered-tinted** by hardship + a **Civ** tab + cell-info "conditions/drought" line + kind=3
|
||||||
`WorldEvent`s ("X grew into a city", "Hurricane <name> devastates X", "Famine shrinks X to a Town", "X
|
`WorldEvent`s ("X grew into a city", "Hurricane <name> devastates X", "Famine shrinks X to a Town", "X
|
||||||
was abandoned"). The set is fixed, so the step-back snapshot only restores the per-settlement
|
was abandoned"). The set is fixed, so the step-back snapshot only restores the per-settlement
|
||||||
@ -1022,22 +1031,32 @@ triangles (plates are fixed in phase 1).
|
|||||||
tighter clusters), `civMinHabitability` (0.22, don't place below this). Habitability blend — `civHabWaterWeight` (0.45), `civHabFoodWeight`
|
tighter clusters), `civMinHabitability` (0.22, don't place below this). Habitability blend — `civHabWaterWeight` (0.45), `civHabFoodWeight`
|
||||||
(0.40, the rest is temperature comfort), `civHabTempOpt` (18 °C, most comfortable mean), `civHabElevPenalty`
|
(0.40, the rest is temperature comfort), `civHabTempOpt` (18 °C, most comfortable mean), `civHabElevPenalty`
|
||||||
(2500 m, high terrain steeply penalised above this). Population — `civSeedPopulation` (250, initial
|
(2500 m, high terrain steeply penalised above this). Population — `civSeedPopulation` (250, initial
|
||||||
village), `civGrowthRate` (0.02/yr logistic rate), `civMaxPopulation` (2e6, the carrying capacity at
|
village), `civGrowthRate` (0.02/yr logistic rate), `civMaxPopulation` (1e6, the carrying-capacity
|
||||||
habitability 1), tier thresholds `civTownPop` (5000) / `civCityPop` (100000), `civAbandonPop` (50,
|
**scale** — multiplied by habitability/siteQuality/trade, a top hub's K is several × this; the real
|
||||||
|
metropolis ceiling is the crowding below), tier thresholds `civTownPop` (5000) / `civCityPop` (100000),
|
||||||
|
`civAbandonPop` (50,
|
||||||
below = abandoned/ruins but can revive). **Dynamics** — `civSiteVariety` (1.0; 0 = flat capacities,
|
below = abandoned/ruins but can revive). **Dynamics** — `civSiteVariety` (1.0; 0 = flat capacities,
|
||||||
higher = big rivers/coasts host far larger cities → wide size spread), `civGrowthMin` (0.25, growth-rate
|
higher = big rivers/coasts host far larger cities → wide size spread), `civGrowthMin` (0.25, growth-rate
|
||||||
floor at habitability 0), `civHarvestVar` (0.25, year-to-year harvest swing, scaled by continentality),
|
floor at habitability 0), `civHarvestVar` (0.25, year-to-year harvest swing, scaled by continentality),
|
||||||
`civDroughtStrength` (0.70) / `civDroughtPeriod` (8 yr) / `civDroughtThresh` (−0.15) / `civDroughtArid`
|
`civDroughtStrength` (0.70) / `civDroughtPeriod` (8 yr) / `civDroughtThresh` (−0.15) / `civDroughtArid`
|
||||||
(0.50, drought-proneness in arid regions), `civColdYearStrength` (0.50), `civFloodBonus` (0.25, river
|
(0.50, drought-proneness in arid regions), `civColdYearStrength` (0.50), `civFloodBonus` (0.25, river
|
||||||
silt), `civFamineRate` (0.15, accelerated loss when food < population), `civStormDeathRate` (0.50) /
|
silt), `civFamineRate` (0.15, accelerated loss when food < population), `civStormDeathRate` (0.50) /
|
||||||
`civHurricaneDeathMult` (3.0, deaths from a storm/hurricane over a town). Droughts/harvests are
|
`civHurricaneDeathMult` (3.0, deaths from a storm/hurricane over a town). **Big-city plateau** —
|
||||||
deterministic per (~20° region, year, seed); an active volcano's ash within ~1.5× its blast radius also
|
`civMetropolisPop` (1.5e6, crowding scale: mortality = `civCrowdingLoss·(P/this)²`/yr; the best hubs
|
||||||
cuts capacity. Marker sizes/colours + hardship tint are render constants (ViewerRender.cpp).
|
plateau near `this·sqrt(growth/civCrowdingLoss)` ≈ 1–1.5M), `civCrowdingLoss` (0.02/yr at P =
|
||||||
|
civMetropolisPop; **0 = the old unbounded behaviour**), `civCondBoomCap` (1.25, cap on the good-year
|
||||||
|
condition upside in the K target — droughts uncapped). **Plagues** — `civPlagueRate` (0.01/yr outbreak
|
||||||
|
chance per settlement), `civPlagueDeathMin`/`Max` (0.20/0.40, total wave kill at full exposure over 1–3
|
||||||
|
years), `civPlagueTradeWeight` (0.5, exposure share from trade connectivity vs pure size; exposure is 0
|
||||||
|
below ~30k → villages never plague). Droughts/harvests/plagues are
|
||||||
|
deterministic per (region/settlement, year, seed); an active volcano's ash within ~1.5× its blast radius
|
||||||
|
also cuts capacity. Marker sizes/colours + hardship tint are render constants (ViewerRender.cpp).
|
||||||
- **Territory & nations (`civTerritory*`/`civVassal*`/`civEmpire*`, `planet.cfg`; key `P`):**
|
- **Territory & nations (`civTerritory*`/`civVassal*`/`civEmpire*`, `planet.cfg`; key `P`):**
|
||||||
`civTerritoryBase` (0.035 rad, a village's reach), `civTerritoryScale` (0.05 rad per log10 of
|
`civTerritoryBase` (0.035 rad, a village's reach), `civTerritoryScale` (0.05 rad per log10 of
|
||||||
population/seed — big cities reach far), `civTerritoryMax` (0.35 rad cap); `civVassalRange` (1.5 ×
|
population/seed — big cities reach far), `civTerritoryMax` (0.35 rad cap); `civVassalRange` (1.5 ×
|
||||||
a capital's range = its annexation reach for vassal towns → bigger = larger kingdoms); empire
|
a capital's range = its annexation reach for vassal towns → bigger = larger kingdoms); empire
|
||||||
threshold `civEmpireMinMembers` (5 settlements) / `civEmpirePop` (5e6 total). Territory + realms are
|
threshold `civEmpireMinMembers` (5 settlements) / `civEmpirePop` (2.5e6 total, retuned for the
|
||||||
|
plateaued city sizes). Territory + realms are
|
||||||
**derived** (recomputed each sim year, not saved). Realm colours/border colour/labels are render
|
**derived** (recomputed each sim year, not saved). Realm colours/border colour/labels are render
|
||||||
constants (Colors.cpp / ViewerRender.cpp).
|
constants (Colors.cpp / ViewerRender.cpp).
|
||||||
- **Culture, beliefs & governments (civ Step 4, key `X`):** **no config knobs** — the ethos/faith/
|
- **Culture, beliefs & governments (civ Step 4, key `X`):** **no config knobs** — the ethos/faith/
|
||||||
|
|||||||
@ -96,13 +96,16 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
|
|||||||
: TextFormat("%.0f", s.population);
|
: TextFormat("%.0f", s.population);
|
||||||
L.push_back(std::string(alive ? settleTierName(t) : "Ruins of") + " " + s.name
|
L.push_back(std::string(alive ? settleTierName(t) : "Ruins of") + " " + s.name
|
||||||
+ " (pop " + pop + ")");
|
+ " (pop " + pop + ")");
|
||||||
// Live conditions: drought / hardship / boom (derived each civ step).
|
// Live conditions: drought / hardship / boom / plague (derived each civ step).
|
||||||
const auto& cond = p.settlementCondition(); const auto& dro = p.settlementDrought();
|
const auto& cond = p.settlementCondition(); const auto& dro = p.settlementDrought();
|
||||||
if (si < (int)cond.size()) {
|
if (si < (int)cond.size()) {
|
||||||
double cd = cond[si], dr = (si < (int)dro.size()) ? dro[si] : 0.0;
|
double cd = cond[si], dr = (si < (int)dro.size()) ? dro[si] : 0.0;
|
||||||
if (dr > 0.15) L.push_back(std::string(TextFormat(" drought %.0f%% conditions %.0f%%", dr * 100.0, cd * 100.0)));
|
if (dr > 0.15) L.push_back(std::string(TextFormat(" drought %.0f%% conditions %.0f%%", dr * 100.0, cd * 100.0)));
|
||||||
else L.push_back(std::string(TextFormat(" conditions %.0f%% (%s)", cd * 100.0,
|
else L.push_back(std::string(TextFormat(" conditions %.0f%% (%s)", cd * 100.0,
|
||||||
cd > 1.05 ? "good harvest" : cd < 0.8 ? "hardship" : "normal")));
|
cd > 1.05 ? "good harvest" : cd < 0.8 ? "hardship" : "normal")));
|
||||||
|
const auto& plv = p.settlementPlague();
|
||||||
|
double pl = (si < (int)plv.size()) ? plv[si] : 0.0;
|
||||||
|
if (pl > 0.01) L.push_back(std::string(TextFormat(" PLAGUE -%.0f%%/yr", pl * 100.0)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -379,7 +379,8 @@ void Viewer::appendEvent(uint8_t kind, uint8_t severity, double timeHours, int c
|
|||||||
|
|
||||||
void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
||||||
const std::vector<Volcano>& beforeVolcanoes,
|
const std::vector<Volcano>& beforeVolcanoes,
|
||||||
const std::vector<Settlement>& beforeSettlements) {
|
const std::vector<Settlement>& beforeSettlements,
|
||||||
|
const std::vector<double>& beforePlague) {
|
||||||
auto beforeStorm = [&](uint32_t id) -> const WeatherSystem* {
|
auto beforeStorm = [&](uint32_t id) -> const WeatherSystem* {
|
||||||
for (const WeatherSystem& ws : beforeStorms) if (ws.id == id) return &ws;
|
for (const WeatherSystem& ws : beforeStorms) if (ws.id == id) return &ws;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@ -445,6 +446,11 @@ void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
|||||||
const Settlement& o = beforeSettlements[k];
|
const Settlement& o = beforeSettlements[k];
|
||||||
SettleTier tb = settleTierOf(o.population, townP, cityP), ta = settleTierOf(s.population, townP, cityP);
|
SettleTier tb = settleTierOf(o.population, townP, cityP), ta = settleTierOf(s.population, townP, cityP);
|
||||||
bool aliveB = o.population >= abP, aliveA = s.population >= abP;
|
bool aliveB = o.population >= abP, aliveA = s.population >= abP;
|
||||||
|
double pl = (k < planet.settlementPlague().size()) ? planet.settlementPlague()[k] : 0.0;
|
||||||
|
// Plague onset: an epidemic wave started in this settlement this frame.
|
||||||
|
if (aliveA && pl > 0.02 && (k >= beforePlague.size() || beforePlague[k] <= 1e-9))
|
||||||
|
appendEvent(3, 2, liveTime, s.cell, s.id, "Plague ravages " + s.name,
|
||||||
|
std::string(TextFormat("%.0f%%/yr dying, ", pl * 100.0)) + popLine(s));
|
||||||
if (aliveB && !aliveA)
|
if (aliveB && !aliveA)
|
||||||
appendEvent(3, 2, liveTime, s.cell, s.id, s.name + " was abandoned", popLine(s));
|
appendEvent(3, 2, liveTime, s.cell, s.id, s.name + " was abandoned", popLine(s));
|
||||||
else if (!aliveB && aliveA)
|
else if (!aliveB && aliveA)
|
||||||
@ -461,6 +467,8 @@ void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
|||||||
double dr = (k < planet.settlementDrought().size()) ? planet.settlementDrought()[k] : 0.0;
|
double dr = (k < planet.settlementDrought().size()) ? planet.settlementDrought()[k] : 0.0;
|
||||||
std::string title, detail = popLine(s);
|
std::string title, detail = popLine(s);
|
||||||
if (stormName) title = std::string(stormName) + " devastates " + s.name;
|
if (stormName) title = std::string(stormName) + " devastates " + s.name;
|
||||||
|
else if (pl > 0.02) { title = std::string("Plague shrinks ") + s.name + " to a " + settleTierName(ta);
|
||||||
|
detail = std::string(TextFormat("%.0f%%/yr dying, ", pl * 100.0)) + popLine(s); }
|
||||||
else if (dr > 0.25) { title = std::string("Famine shrinks ") + s.name + " to a " + settleTierName(ta);
|
else if (dr > 0.25) { title = std::string("Famine shrinks ") + s.name + " to a " + settleTierName(ta);
|
||||||
detail = std::string(TextFormat("drought %.0f%%, ", dr * 100.0)) + popLine(s); }
|
detail = std::string(TextFormat("drought %.0f%%, ", dr * 100.0)) + popLine(s); }
|
||||||
else title = s.name + " declined to a " + settleTierName(ta);
|
else title = s.name + " declined to a " + settleTierName(ta);
|
||||||
@ -816,17 +824,19 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
|
|||||||
std::vector<WeatherSystem> beforeStorms;
|
std::vector<WeatherSystem> beforeStorms;
|
||||||
std::vector<Volcano> beforeVolcanoes;
|
std::vector<Volcano> beforeVolcanoes;
|
||||||
std::vector<Settlement> beforeSettlements;
|
std::vector<Settlement> beforeSettlements;
|
||||||
|
std::vector<double> beforePlague;
|
||||||
if (dtWeather > 0.0) {
|
if (dtWeather > 0.0) {
|
||||||
beforeStorms = planet.storms();
|
beforeStorms = planet.storms();
|
||||||
beforeVolcanoes = planet.volcanoes;
|
beforeVolcanoes = planet.volcanoes;
|
||||||
beforeSettlements = planet.settlements;
|
beforeSettlements = planet.settlements;
|
||||||
|
beforePlague = planet.settlementPlague();
|
||||||
}
|
}
|
||||||
planet.stepWeather(dtWeather);
|
planet.stepWeather(dtWeather);
|
||||||
// Volcanoes are stateful lifecycle agents; step-back restores their snapshot, then dt=0 here
|
// Volcanoes are stateful lifecycle agents; step-back restores their snapshot, then dt=0 here
|
||||||
// reasserts restored terrain/biome state without advancing the lifecycle.
|
// reasserts restored terrain/biome state without advancing the lifecycle.
|
||||||
VolcanoUpdate vu = planet.stepVolcanoes(dtWeather);
|
VolcanoUpdate vu = planet.stepVolcanoes(dtWeather);
|
||||||
CivUpdate cu = planet.stepCivilization(dtWeather, liveTime); // env-driven growth/decline on the clock
|
CivUpdate cu = planet.stepCivilization(dtWeather, liveTime); // env-driven growth/decline on the clock
|
||||||
if (dtWeather > 0.0) detectLiveEvents(beforeStorms, beforeVolcanoes, beforeSettlements);
|
if (dtWeather > 0.0) detectLiveEvents(beforeStorms, beforeVolcanoes, beforeSettlements, beforePlague);
|
||||||
// Territory & nations shift slowly -> recompute once per sim year (and rebuild the border lines).
|
// Territory & nations shift slowly -> recompute once per sim year (and rebuild the border lines).
|
||||||
// Wars (civ Step 5) run on the same yearly tick: stepConflict mutates allegiance/populations first,
|
// Wars (civ Step 5) run on the same yearly tick: stepConflict mutates allegiance/populations first,
|
||||||
// then territory recomputes so borders move as cities change hands.
|
// then territory recomputes so borders move as cities change hands.
|
||||||
|
|||||||
@ -187,7 +187,8 @@ struct Viewer {
|
|||||||
const std::string& title, const std::string& detail);
|
const std::string& title, const std::string& detail);
|
||||||
void detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
void detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
||||||
const std::vector<Volcano>& beforeVolcanoes,
|
const std::vector<Volcano>& beforeVolcanoes,
|
||||||
const std::vector<Settlement>& beforeSettlements);
|
const std::vector<Settlement>& beforeSettlements,
|
||||||
|
const std::vector<double>& beforePlague);
|
||||||
void detectNationEvents(const std::vector<Nation>& beforeNations);
|
void detectNationEvents(const std::vector<Nation>& beforeNations);
|
||||||
void focusCell(int idx, const std::string& status = "");
|
void focusCell(int idx, const std::string& status = "");
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ void Planet::buildGeometry() {
|
|||||||
volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
volcanoes.clear(); sVolRng = cfg.seed ? (cfg.seed ^ 0x70C4F12Au) : 0x70C4F12Au;
|
||||||
settlements.clear(); sCivRng = cfg.seed ? (cfg.seed ^ 0x017B1A2Eu) : 0x017B1A2Eu;
|
settlements.clear(); sCivRng = cfg.seed ? (cfg.seed ^ 0x017B1A2Eu) : 0x017B1A2Eu;
|
||||||
sCellSettlement.assign(cells.size(), -1); sHabitability.clear();
|
sCellSettlement.assign(cells.size(), -1); sHabitability.clear();
|
||||||
sCivCond.clear(); sCivDrought.clear();
|
sCivCond.clear(); sCivDrought.clear(); sCivPlague.clear();
|
||||||
nations.clear(); sCellNation.assign(cells.size(), -1); sSettleNation.clear();
|
nations.clear(); sCellNation.assign(cells.size(), -1); sSettleNation.clear();
|
||||||
cultures.clear(); sCellCulture.assign(cells.size(), -1); sSettleCulture.clear(); sCultureNextId = 1;
|
cultures.clear(); sCellCulture.assign(cells.size(), -1); sSettleCulture.clear(); sCultureNextId = 1;
|
||||||
sCellSettleOwner.assign(cells.size(), -1);
|
sCellSettleOwner.assign(cells.size(), -1);
|
||||||
|
|||||||
@ -251,6 +251,7 @@ public:
|
|||||||
// current drought severity 0..1. Parallel to `settlements`. Used by the viewer for tint + events.
|
// current drought severity 0..1. Parallel to `settlements`. Used by the viewer for tint + events.
|
||||||
const std::vector<double>& settlementCondition() const { return sCivCond; }
|
const std::vector<double>& settlementCondition() const { return sCivCond; }
|
||||||
const std::vector<double>& settlementDrought() const { return sCivDrought; }
|
const std::vector<double>& settlementDrought() const { return sCivDrought; }
|
||||||
|
const std::vector<double>& settlementPlague() const { return sCivPlague; } // active epidemic loss (/yr, 0 = none)
|
||||||
|
|
||||||
// Build a fine-resolution subgrid patch for one macro cell (phase 4/5 hook).
|
// Build a fine-resolution subgrid patch for one macro cell (phase 4/5 hook).
|
||||||
std::shared_ptr<SubGrid> makeSubGrid(int cellIndex, int res) const;
|
std::shared_ptr<SubGrid> makeSubGrid(int cellIndex, int res) const;
|
||||||
@ -383,7 +384,7 @@ private:
|
|||||||
// a derived habitability field, and a separate RNG so placement never perturbs tectonics.
|
// a derived habitability field, and a separate RNG so placement never perturbs tectonics.
|
||||||
std::vector<int> sCellSettlement;
|
std::vector<int> sCellSettlement;
|
||||||
std::vector<double> sHabitability;
|
std::vector<double> sHabitability;
|
||||||
std::vector<double> sCivCond, sCivDrought; // per-settlement live conditions (derived)
|
std::vector<double> sCivCond, sCivDrought, sCivPlague; // per-settlement live conditions (derived)
|
||||||
uint32_t sCivRng = 1;
|
uint32_t sCivRng = 1;
|
||||||
// Territory & nations (derived from settlements; not saved). sCellNation: nation index per cell
|
// Territory & nations (derived from settlements; not saved). sCellNation: nation index per cell
|
||||||
// (-1 = wilderness/ocean); sSettleNation: nation index per settlement.
|
// (-1 = wilderness/ocean); sSettleNation: nation index per settlement.
|
||||||
|
|||||||
@ -130,6 +130,7 @@ void Planet::placeSettlements() {
|
|||||||
}
|
}
|
||||||
sCivCond.assign(settlements.size(), 1.0);
|
sCivCond.assign(settlements.size(), 1.0);
|
||||||
sCivDrought.assign(settlements.size(), 0.0);
|
sCivDrought.assign(settlements.size(), 0.0);
|
||||||
|
sCivPlague.assign(settlements.size(), 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One live-frame civilization step. Each settlement's population moves logistically toward a food-driven
|
// One live-frame civilization step. Each settlement's population moves logistically toward a food-driven
|
||||||
@ -145,6 +146,7 @@ CivUpdate Planet::stepCivilization(double dtHours, double liveTime) {
|
|||||||
const int n = (int)cells.size();
|
const int n = (int)cells.size();
|
||||||
if ((int)sHabitability.size() != n) computeHabitability();
|
if ((int)sHabitability.size() != n) computeHabitability();
|
||||||
if ((int)sCivCond.size() != (int)settlements.size()) { sCivCond.assign(settlements.size(), 1.0); sCivDrought.assign(settlements.size(), 0.0); }
|
if ((int)sCivCond.size() != (int)settlements.size()) { sCivCond.assign(settlements.size(), 1.0); sCivDrought.assign(settlements.size(), 0.0); }
|
||||||
|
if (sCivPlague.size() != settlements.size()) sCivPlague.assign(settlements.size(), 0.0);
|
||||||
|
|
||||||
const double yearHours = std::max(1.0, cfg.dayLengthHours * cfg.yearLengthDays);
|
const double yearHours = std::max(1.0, cfg.dayLengthHours * cfg.yearLengthDays);
|
||||||
const double dtYears = std::max(0.0, dtHours) / yearHours;
|
const double dtYears = std::max(0.0, dtHours) / yearHours;
|
||||||
@ -218,9 +220,34 @@ CivUpdate Planet::stepCivilization(double dtHours, double liveTime) {
|
|||||||
|
|
||||||
double cond = harvest * droughtFactor * coldFactor * floodFactor * ashFactor;
|
double cond = harvest * droughtFactor * coldFactor * floodFactor * ashFactor;
|
||||||
double prosp = ((size_t)k < sProsperity.size()) ? sProsperity[(size_t)k] : 0.0; // civ Step 7: trade wealth boosts capacity
|
double prosp = ((size_t)k < sProsperity.size()) ? sProsperity[(size_t)k] : 0.0; // civ Step 7: trade wealth boosts capacity
|
||||||
double K = cfg.civMaxPopulation * hab * siteQ * cond * (1.0 + cfg.tradeProsperityWeight * prosp);
|
// Good-year cap: a lucky harvest must not inflate the K TARGET by 70% (the logistic would chase
|
||||||
|
// it at full rate while busts correct slowly -- an upward ratchet). The downside stays uncapped
|
||||||
|
// (droughts/ash bite hard); famine, tint and the conditions display keep the raw cond.
|
||||||
|
double condK = std::min(cond, cfg.civCondBoomCap);
|
||||||
|
double K = cfg.civMaxPopulation * hab * siteQ * condK * (1.0 + cfg.tradeProsperityWeight * prosp);
|
||||||
sCivCond[k] = cond; sCivDrought[k] = drought;
|
sCivCond[k] = cond; sCivDrought[k] = drought;
|
||||||
|
|
||||||
|
// Plague: rare epidemics keep big cities in check (pre-industrial metropolises were population
|
||||||
|
// sinks). A wave is a pure function of (settlement id, onset year, seed); to know whether one is
|
||||||
|
// active at `year`, re-derive the onset test for the last 3 years -- stateless, so a step-back
|
||||||
|
// replays it exactly. Exposure needs a real city (population) and grows with trade connectivity
|
||||||
|
// (contagion travels the routes -- the cost of being a hub); villages are untouched.
|
||||||
|
double plague = 0.0;
|
||||||
|
for (int back = 0; back < 3; ++back) {
|
||||||
|
uint32_t y0 = (uint32_t)(year - back);
|
||||||
|
uint32_t h = civHash(base ^ civHash(st.id * 0x85EBCA6Bu ^ y0 * 0xC2B2AE35u ^ 0x9A19D5Cu));
|
||||||
|
if (civHashf(h) >= cfg.civPlagueRate) continue; // no outbreak begun that year
|
||||||
|
int L = 1 + (int)((h >> 8) % 3u); // the wave lasts 1..3 years
|
||||||
|
if (back >= L) continue; // already burned out
|
||||||
|
double sev = cfg.civPlagueDeathMin
|
||||||
|
+ (cfg.civPlagueDeathMax - cfg.civPlagueDeathMin) * civHashf(h ^ 0xA341316Cu);
|
||||||
|
plague += sev / (double)L; // annual loss rate of this wave
|
||||||
|
}
|
||||||
|
double popF = std::clamp((st.population / std::max(1.0, cfg.civCityPop) - 0.3) / 2.7, 0.0, 1.0);
|
||||||
|
double connF = std::clamp(prosp / 3.0, 0.0, 1.0); // sProsperity is clamped <= 3
|
||||||
|
double expo = popF * (1.0 - cfg.civPlagueTradeWeight + cfg.civPlagueTradeWeight * connF);
|
||||||
|
sCivPlague[k] = std::min(plague, cfg.civPlagueDeathMax) * expo;
|
||||||
|
|
||||||
// Storms over the town kill people directly (acute), hurricanes worst. Storms are snapshotted, so
|
// Storms over the town kill people directly (acute), hurricanes worst. Storms are snapshotted, so
|
||||||
// this stays consistent on a step-back.
|
// this stays consistent on a step-back.
|
||||||
double stormLoss = 0.0;
|
double stormLoss = 0.0;
|
||||||
@ -237,6 +264,12 @@ CivUpdate Planet::stepCivilization(double dtHours, double liveTime) {
|
|||||||
double r = cfg.civGrowthRate * (cfg.civGrowthMin + (1.0 - cfg.civGrowthMin) * hab); // env-driven rate
|
double r = cfg.civGrowthRate * (cfg.civGrowthMin + (1.0 - cfg.civGrowthMin) * hab); // env-driven rate
|
||||||
P += r * P * (1.0 - P / std::max(1.0, K)) * dtYears; // logistic toward K
|
P += r * P * (1.0 - P / std::max(1.0, K)) * dtYears; // logistic toward K
|
||||||
if (K < P) P -= cfg.civFamineRate * (1.0 - K / P) * P * dtYears; // accelerated famine
|
if (K < P) P -= cfg.civFamineRate * (1.0 - K / P) * P * dtYears; // accelerated famine
|
||||||
|
// Urban crowding: mortality rises with the square of city size (disease/logistics), so the
|
||||||
|
// best sites PLATEAU near civMetropolisPop x sqrt(r/civCrowdingLoss) instead of chasing a
|
||||||
|
// huge K for millennia. Negligible below ~50k; not a clamp -- everything still moves P.
|
||||||
|
double crowd = P / std::max(1.0, cfg.civMetropolisPop);
|
||||||
|
P -= std::min(cfg.civCrowdingLoss * crowd * crowd, 0.25) * P * dtYears;
|
||||||
|
P -= sCivPlague[k] * P * dtYears; // epidemic deaths (big hubs)
|
||||||
P -= stormLoss * P * dtYears; // acute storm deaths
|
P -= stormLoss * P * dtYears; // acute storm deaths
|
||||||
P = std::max(1.0, P);
|
P = std::max(1.0, P);
|
||||||
if (std::fabs(P - st.population) > std::max(1.0, st.population * 0.0005)) up.recolor = true;
|
if (std::fabs(P - st.population) > std::max(1.0, st.population * 0.0005)) up.recolor = true;
|
||||||
@ -324,6 +357,7 @@ std::vector<WarEvent> Planet::stepColonization(long year) {
|
|||||||
if (sSettleCulture.size() != settlements.size()) sSettleCulture.resize(settlements.size(), -1);
|
if (sSettleCulture.size() != settlements.size()) sSettleCulture.resize(settlements.size(), -1);
|
||||||
sSettleCulture[newIdx] = (cap < (int)sSettleCulture.size()) ? sSettleCulture[cap] : -1;
|
sSettleCulture[newIdx] = (cap < (int)sSettleCulture.size()) ? sSettleCulture[cap] : -1;
|
||||||
if (sCivCond.size() != settlements.size()) { sCivCond.resize(settlements.size(), 1.0); sCivDrought.resize(settlements.size(), 0.0); }
|
if (sCivCond.size() != settlements.size()) { sCivCond.resize(settlements.size(), 1.0); sCivDrought.resize(settlements.size(), 0.0); }
|
||||||
|
if (sCivPlague.size() != settlements.size()) sCivPlague.resize(settlements.size(), 0.0);
|
||||||
ev.push_back(WarEvent{ 1, best, "The " + nat.name + " founds the colony of " + settlements[newIdx].name,
|
ev.push_back(WarEvent{ 1, best, "The " + nat.name + " founds the colony of " + settlements[newIdx].name,
|
||||||
"A new settlement rises on distant land.", 3 });
|
"A new settlement rises on distant land.", 3 });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,8 @@
|
|||||||
D(civSiteVariety) D(civGrowthMin) D(civHarvestVar) D(civDroughtStrength) D(civDroughtPeriod) D(civDroughtThresh) \
|
D(civSiteVariety) D(civGrowthMin) D(civHarvestVar) D(civDroughtStrength) D(civDroughtPeriod) D(civDroughtThresh) \
|
||||||
D(civDroughtArid) D(civColdYearStrength) D(civFloodBonus) D(civFamineRate) \
|
D(civDroughtArid) D(civColdYearStrength) D(civFloodBonus) D(civFamineRate) \
|
||||||
D(civStormDeathRate) D(civHurricaneDeathMult) \
|
D(civStormDeathRate) D(civHurricaneDeathMult) \
|
||||||
|
D(civMetropolisPop) D(civCrowdingLoss) D(civCondBoomCap) \
|
||||||
|
D(civPlagueRate) D(civPlagueDeathMin) D(civPlagueDeathMax) D(civPlagueTradeWeight) \
|
||||||
D(civTerritoryBase) D(civTerritoryScale) D(civTerritoryMax) D(civVassalRange) D(civEmpirePop) \
|
D(civTerritoryBase) D(civTerritoryScale) D(civTerritoryMax) D(civVassalRange) D(civEmpirePop) \
|
||||||
D(warDeclareRate) D(warAmbition) D(warIdeology) D(warBorder) D(warWarlikeMult) D(warCasualtyRate) \
|
D(warDeclareRate) D(warAmbition) D(warIdeology) D(warBorder) D(warWarlikeMult) D(warCasualtyRate) \
|
||||||
D(warConquerScore) D(warSackChance) D(warExhaustion) D(warRevoltRate) D(warMinRealmPop) \
|
D(warConquerScore) D(warSackChance) D(warExhaustion) D(warRevoltRate) D(warMinRealmPop) \
|
||||||
@ -293,6 +295,15 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.civFamineRate, 0.0, 10.0, "civFamineRate"));
|
E(rng(cfg.civFamineRate, 0.0, 10.0, "civFamineRate"));
|
||||||
E(rng(cfg.civStormDeathRate, 0.0, 10.0, "civStormDeathRate"));
|
E(rng(cfg.civStormDeathRate, 0.0, 10.0, "civStormDeathRate"));
|
||||||
E(rng(cfg.civHurricaneDeathMult, 1.0, 50.0, "civHurricaneDeathMult"));
|
E(rng(cfg.civHurricaneDeathMult, 1.0, 50.0, "civHurricaneDeathMult"));
|
||||||
|
E(rng(cfg.civMetropolisPop, 1.0e3, 1.0e12, "civMetropolisPop"));
|
||||||
|
E(rng(cfg.civCrowdingLoss, 0.0, 10.0, "civCrowdingLoss"));
|
||||||
|
E(rng(cfg.civCondBoomCap, 1.0, 10.0, "civCondBoomCap"));
|
||||||
|
E(rng(cfg.civPlagueRate, 0.0, 1.0, "civPlagueRate"));
|
||||||
|
E(rng(cfg.civPlagueDeathMin, 0.0, 1.0, "civPlagueDeathMin"));
|
||||||
|
E(rng(cfg.civPlagueDeathMax, 0.0, 1.0, "civPlagueDeathMax"));
|
||||||
|
E(rng(cfg.civPlagueTradeWeight, 0.0, 1.0, "civPlagueTradeWeight"));
|
||||||
|
if (cfg.civPlagueDeathMin > cfg.civPlagueDeathMax)
|
||||||
|
E("civPlagueDeathMin > civPlagueDeathMax");
|
||||||
E(rng(cfg.civTerritoryBase, 0.0, 3.14159, "civTerritoryBase"));
|
E(rng(cfg.civTerritoryBase, 0.0, 3.14159, "civTerritoryBase"));
|
||||||
E(rng(cfg.civTerritoryScale, 0.0, 3.14159, "civTerritoryScale"));
|
E(rng(cfg.civTerritoryScale, 0.0, 3.14159, "civTerritoryScale"));
|
||||||
E(rng(cfg.civTerritoryMax, 0.01, 3.14159, "civTerritoryMax"));
|
E(rng(cfg.civTerritoryMax, 0.01, 3.14159, "civTerritoryMax"));
|
||||||
|
|||||||
@ -429,7 +429,9 @@ struct PlanetConfig {
|
|||||||
double civMinHabitability = 0.22; // don't place a settlement below this habitability
|
double civMinHabitability = 0.22; // don't place a settlement below this habitability
|
||||||
double civSeedPopulation = 250.0; // initial village population at placement
|
double civSeedPopulation = 250.0; // initial village population at placement
|
||||||
double civGrowthRate = 0.02; // logistic growth rate per year (toward carrying capacity)
|
double civGrowthRate = 0.02; // logistic growth rate per year (toward carrying capacity)
|
||||||
double civMaxPopulation = 2.0e6; // population at habitability 1 (carrying-capacity scale)
|
double civMaxPopulation = 1.0e6; // carrying-capacity SCALE (multiplied by habitability, site
|
||||||
|
// quality and trade -- a top hub's K is several x this; the
|
||||||
|
// real metropolis ceiling is the civMetropolisPop crowding)
|
||||||
double civTownPop = 5000.0; // population at/above which a settlement is a Town
|
double civTownPop = 5000.0; // population at/above which a settlement is a Town
|
||||||
double civCityPop = 100000.0;// population at/above which a settlement is a City
|
double civCityPop = 100000.0;// population at/above which a settlement is a City
|
||||||
double civAbandonPop = 50.0; // below this a settlement is abandoned (dormant; can revive)
|
double civAbandonPop = 50.0; // below this a settlement is abandoned (dormant; can revive)
|
||||||
@ -452,6 +454,18 @@ struct PlanetConfig {
|
|||||||
double civFamineRate = 0.15; // /year accelerated population loss when food < population
|
double civFamineRate = 0.15; // /year accelerated population loss when food < population
|
||||||
double civStormDeathRate = 0.50; // /year population loss for a full-strength storm over a settlement
|
double civStormDeathRate = 0.50; // /year population loss for a full-strength storm over a settlement
|
||||||
double civHurricaneDeathMult= 3.0; // extra storm death multiplier for a hurricane/typhoon
|
double civHurricaneDeathMult= 3.0; // extra storm death multiplier for a hurricane/typhoon
|
||||||
|
// Big-city demography: pre-industrial metropolises were population sinks (disease, crowding, food
|
||||||
|
// logistics), so growth meets a headwind that rises with city size -- cities PLATEAU (~1-2M for the
|
||||||
|
// best trade hubs) instead of exponentially chasing a huge carrying capacity for millennia. Plus
|
||||||
|
// rare deterministic PLAGUES: probability rises with population and trade connectivity (contagion
|
||||||
|
// is the cost of being a hub). All pure functions of (id, year, seed) -> reversible, no save change.
|
||||||
|
double civMetropolisPop = 1.5e6; // crowding scale: mortality = civCrowdingLoss x (P/this)^2 per year
|
||||||
|
double civCrowdingLoss = 0.02; // /yr crowding mortality at P = civMetropolisPop (0 = no plateau)
|
||||||
|
double civCondBoomCap = 1.25; // cap on the good-year condition upside in the K target (droughts uncapped)
|
||||||
|
double civPlagueRate = 0.01; // /yr per-settlement plague-outbreak chance (hash gate)
|
||||||
|
double civPlagueDeathMin = 0.20; // min total wave kill fraction at full exposure
|
||||||
|
double civPlagueDeathMax = 0.40; // max total wave kill fraction (also caps the per-year loss)
|
||||||
|
double civPlagueTradeWeight = 0.5; // exposure share driven by trade connectivity vs pure size
|
||||||
// Territory & nations (PlanetNation.cpp): influence range each settlement projects (size-scaled),
|
// Territory & nations (PlanetNation.cpp): influence range each settlement projects (size-scaled),
|
||||||
// realm grouping (vassals/kingdoms), and the empire threshold. Derived -> recomputed, not saved.
|
// realm grouping (vassals/kingdoms), and the empire threshold. Derived -> recomputed, not saved.
|
||||||
double civTerritoryBase = 0.035; // rad: base influence range of a seed-size village (~220 km)
|
double civTerritoryBase = 0.035; // rad: base influence range of a seed-size village (~220 km)
|
||||||
@ -459,7 +473,7 @@ struct PlanetConfig {
|
|||||||
double civTerritoryMax = 0.35; // rad: cap on a single settlement's reach (~2200 km)
|
double civTerritoryMax = 0.35; // rad: cap on a single settlement's reach (~2200 km)
|
||||||
double civVassalRange = 1.5; // a capital annexes smaller settlements within this x its range
|
double civVassalRange = 1.5; // a capital annexes smaller settlements within this x its range
|
||||||
int civEmpireMinMembers = 5; // realm of >= this many settlements counts as an Empire
|
int civEmpireMinMembers = 5; // realm of >= this many settlements counts as an Empire
|
||||||
double civEmpirePop = 5.0e6; // ...or total population >= this counts as an Empire
|
double civEmpirePop = 2.5e6; // ...or total population >= this counts as an Empire
|
||||||
// Civilization Step 5: conflict & war (stateful, saved v21). Neighbouring realms grow hostile and
|
// Civilization Step 5: conflict & war (stateful, saved v21). Neighbouring realms grow hostile and
|
||||||
// fight; casualties shrink frontier cities, winners conquer (flip) or sack (raze) them, empires
|
// fight; casualties shrink frontier cities, winners conquer (flip) or sack (raze) them, empires
|
||||||
// fracture as provinces revolt. All rolls come from a separate war RNG (tectonic stream intact).
|
// fracture as provinces revolt. All rolls come from a separate war RNG (tectonic stream intact).
|
||||||
|
|||||||
@ -53,7 +53,7 @@ void Planet::restoreWeather(const WeatherSnapshot& s) {
|
|||||||
if (cnt <= settlements.size()) {
|
if (cnt <= settlements.size()) {
|
||||||
settlements.resize(cnt);
|
settlements.resize(cnt);
|
||||||
for (size_t k = 0; k < cnt; ++k) settlements[k].population = s.settlementPop[k];
|
for (size_t k = 0; k < cnt; ++k) settlements[k].population = s.settlementPop[k];
|
||||||
sCivCond.assign(cnt, 1.0); sCivDrought.assign(cnt, 0.0);
|
sCivCond.assign(cnt, 1.0); sCivDrought.assign(cnt, 0.0); sCivPlague.assign(cnt, 0.0);
|
||||||
sCellSettlement.assign(cells.size(), -1);
|
sCellSettlement.assign(cells.size(), -1);
|
||||||
for (size_t k = 0; k < cnt; ++k)
|
for (size_t k = 0; k < cnt; ++k)
|
||||||
if (settlements[k].cell >= 0 && settlements[k].cell < (int)cells.size()) sCellSettlement[settlements[k].cell] = (int)k;
|
if (settlements[k].cell >= 0 && settlements[k].cell < (int)cells.size()) sCellSettlement[settlements[k].cell] = (int)k;
|
||||||
|
|||||||
91
test_civ.cpp
91
test_civ.cpp
@ -10,7 +10,9 @@
|
|||||||
// src/sim/PlanetIO.cpp -o /tmp/tc && /tmp/tc
|
// src/sim/PlanetIO.cpp -o /tmp/tc && /tmp/tc
|
||||||
//
|
//
|
||||||
// Verifies: habitability range/zeros; placement spacing/cap/land + unique names; food-driven growth
|
// Verifies: habitability range/zeros; placement spacing/cap/land + unique names; food-driven growth
|
||||||
// and decline; tiers; determinism + RNG isolation; population snapshot round-trip; v20 save; reseed clear.
|
// and decline; tiers; determinism + RNG isolation; population snapshot round-trip; v20 save; reseed clear;
|
||||||
|
// growth plateau (crowding + plague bound city size at a historical metropolis scale; villages untouched;
|
||||||
|
// plague waves are deterministic + step-back exact).
|
||||||
|
|
||||||
#include "Planet.hpp"
|
#include "Planet.hpp"
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@ -195,6 +197,93 @@ int main() {
|
|||||||
check(r.cellSettlement() == p.cellSettlement(), "cellSettlement index rebuilt on load");
|
check(r.cellSettlement() == p.cellSettlement(), "cellSettlement index rebuilt on load");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::printf("Civ: growth plateau (crowding + plague keep cities at a historical scale)\n");
|
||||||
|
{
|
||||||
|
Planet base; base.generate(cfg); settle(base); drift(base, 400);
|
||||||
|
base.placeSettlements();
|
||||||
|
const double stepH = 2.0 * yearH;
|
||||||
|
// One era: fixed cadence (territory/culture/trade every 10 steps so prosperity feeds K, like
|
||||||
|
// the viewer's yearly rebuild but cheaper); returns the end clock so eras chain year-correctly.
|
||||||
|
auto era = [&](Planet& x, int iters, double lt) {
|
||||||
|
for (int it = 0; it < iters; ++it) {
|
||||||
|
if (it % 10 == 0) { x.computeTerritory(); x.computeCultures(); x.computeTrade(); }
|
||||||
|
lt += stepH;
|
||||||
|
x.stepCivilization(stepH, lt);
|
||||||
|
}
|
||||||
|
return lt;
|
||||||
|
};
|
||||||
|
Planet A = base; // defaults: crowding + plague + good-year cap
|
||||||
|
Planet B = base; // the old unbounded behaviour
|
||||||
|
B.cfg.civCrowdingLoss = 0.0; B.cfg.civPlagueRate = 0.0; B.cfg.civCondBoomCap = 100.0;
|
||||||
|
// Run A inline so we can watch every step: villages (< 30k) must never see plague.
|
||||||
|
bool villagesClean = true;
|
||||||
|
double ltA = 0.0;
|
||||||
|
for (int it = 0; it < 2000; ++it) { // 4000 years
|
||||||
|
if (it % 10 == 0) { A.computeTerritory(); A.computeCultures(); A.computeTrade(); }
|
||||||
|
ltA += stepH;
|
||||||
|
A.stepCivilization(stepH, ltA);
|
||||||
|
for (size_t k = 0; k < A.settlements.size(); ++k)
|
||||||
|
if (A.settlements[k].population < 0.3 * A.cfg.civCityPop
|
||||||
|
&& k < A.settlementPlague().size() && A.settlementPlague()[k] > 0.0) villagesClean = false;
|
||||||
|
}
|
||||||
|
era(B, 2000, 0.0);
|
||||||
|
double mxA = 0.0, mxB = 0.0;
|
||||||
|
for (const auto& s : A.settlements) mxA = std::max(mxA, s.population);
|
||||||
|
for (const auto& s : B.settlements) mxB = std::max(mxB, s.population);
|
||||||
|
std::printf(" max pop with plateau %.0f without %.0f (x%.1f)\n", mxA, mxB, mxA > 0 ? mxB / mxA : 0.0);
|
||||||
|
check(mxA > 4.0e5 && mxA < 3.0e6, "the largest city plateaus at a historical metropolis scale (~1-2M)");
|
||||||
|
check(mxB > mxA * 2.0, "without crowding/plague/boom-cap cities grow far larger (the old runaway)");
|
||||||
|
check(villagesClean, "small settlements never suffer plague");
|
||||||
|
|
||||||
|
// A plague wave hits the biggest hub within a few centuries, is deterministic and rewindable.
|
||||||
|
int big = 0;
|
||||||
|
for (size_t k = 0; k < A.settlements.size(); ++k)
|
||||||
|
if (A.settlements[k].population > A.settlements[big].population) big = (int)k;
|
||||||
|
bool sawPlague = false, dropInWave = false;
|
||||||
|
double ltP = ltA;
|
||||||
|
for (int yr = 0; yr < 400; ++yr) {
|
||||||
|
double before = A.settlements[big].population;
|
||||||
|
if (yr % 10 == 0) { A.computeTerritory(); A.computeCultures(); A.computeTrade(); }
|
||||||
|
ltP += yearH;
|
||||||
|
A.stepCivilization(yearH, ltP);
|
||||||
|
if (A.settlementPlague()[big] > 0.02) {
|
||||||
|
sawPlague = true;
|
||||||
|
if (A.settlements[big].population < before * 0.98) dropInWave = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::printf(" plague hit the largest hub: %s\n", sawPlague ? "yes" : "no");
|
||||||
|
check(sawPlague, "a plague wave strikes a large trade hub within a few centuries");
|
||||||
|
check(dropInWave, "an active plague year visibly shrinks the city");
|
||||||
|
|
||||||
|
// Determinism: two identical copies evolve identical populations through plague years.
|
||||||
|
{
|
||||||
|
Planet d1 = base, d2 = base;
|
||||||
|
double l1 = era(d1, 200, 0.0), l2 = era(d2, 200, 0.0);
|
||||||
|
bool same = (l1 == l2) && d1.settlements.size() == d2.settlements.size();
|
||||||
|
for (size_t k = 0; same && k < d1.settlements.size(); ++k)
|
||||||
|
if (d1.settlements[k].population != d2.settlements[k].population) same = false;
|
||||||
|
check(same, "growth incl. plague/crowding is deterministic (bit-identical twins)");
|
||||||
|
}
|
||||||
|
// Step-back: capture -> 50 years (across plague waves) -> restore -> replay -> identical.
|
||||||
|
{
|
||||||
|
WeatherSnapshot snap = A.captureWeather();
|
||||||
|
double lt0 = ltP;
|
||||||
|
std::vector<double> endPops;
|
||||||
|
double lt1 = era(A, 25, lt0); // 50 years
|
||||||
|
for (const auto& s : A.settlements) endPops.push_back(s.population);
|
||||||
|
A.restoreWeather(snap);
|
||||||
|
bool back = true;
|
||||||
|
for (size_t k = 0; k < A.settlements.size(); ++k)
|
||||||
|
if (A.settlements[k].population != snap.settlementPop[k]) back = false;
|
||||||
|
check(back, "restoreWeather rewinds populations across plague years");
|
||||||
|
double lt2 = era(A, 25, lt0); // deterministic replay
|
||||||
|
bool same = (lt1 == lt2);
|
||||||
|
for (size_t k = 0; same && k < A.settlements.size(); ++k)
|
||||||
|
if (A.settlements[k].population != endPops[k]) same = false;
|
||||||
|
check(same, "replaying the same years reproduces the same populations (plague is stateless)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::printf("Civ: reseed clears settlements\n");
|
std::printf("Civ: reseed clears settlements\n");
|
||||||
p.generate(cfg);
|
p.generate(cfg);
|
||||||
check(p.settlements.empty() && (p.cellSettlement().empty() || p.cellSettlement()[0] == -1), "reseed clears the settlement set");
|
check(p.settlements.empty() && (p.cellSettlement().empty() || p.cellSettlement()[0] == -1), "reseed clears the settlement set");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user