Civ settlements: environment-driven dynamic growth (droughts, harvests, storms)
Settlements were static -- every town grew with one global rate to the same
habitability cap (frozen in Live World), so they grew "the same amount
everywhere". Now stepCivilization(dtHours, liveTime) is environment-driven and
time-varying. All drivers are deterministic functions of (region, year, seed),
so step-back replays them with no new saved/snapshot state (population is
already snapshotted).
- Differentiated growth: rate scales with habitability (fertile boom, marginal
crawl); capacity K = maxPop * habitability * siteQuality * conditions.
- Site quality: max city size varies by an order of magnitude with location --
a great river (log-scaled discharge) or coast hosts a metropolis, a dry inland
cell a town (civSiteVariety). This spreads final sizes ~8-9x instead of all
saturating equally.
- Conditions (per ~20deg region, per year): multi-year droughts (worse in arid
regions), year-to-year harvests (bigger swings inland), rare cold years, river
floods, and the existing volcano ash. Regional, so different parts of a
continent have different fortunes.
- Storms kill directly: a system within its radius of a town deals deaths scaled
by strength (hurricanes x civHurricaneDeathMult) -- a parked hurricane can gut
a coastal city. Reads the already-snapshotted storms().
- Famine: accelerated loss when food < population; sustained drought / acute
disaster can collapse a settlement to ruins (revives when conditions return).
- Viewer: markers withered-tinted by hardship (3D + 2D), cell-info "drought/
conditions" line, and cause-attributed kind=3 events ("Hurricane X devastates
Y", "Famine shrinks Y to a Town", tier up/down/abandon).
New civ* knobs (self-describing config -> no save bump). test_civ extended:
sizes vary widely (not uniform), bad-year declines, a hurricane over a town
kills its population. All 10 suites pass; GUI build clean. Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2351fb79a5
commit
3655dc0661
12
BUILD.md
12
BUILD.md
@ -296,6 +296,18 @@ population grows/declines on the Live World clock toward a food-driven carrying
|
|||||||
civHabFoodWeight 0.40 habitability weight of food (flora/fauna + ecoregion productivity)
|
civHabFoodWeight 0.40 habitability weight of food (flora/fauna + ecoregion productivity)
|
||||||
civHabTempOpt 18 C most comfortable annual-mean temperature
|
civHabTempOpt 18 C most comfortable annual-mean temperature
|
||||||
civHabElevPenalty 2500 m high terrain steeply penalised above this
|
civHabElevPenalty 2500 m high terrain steeply penalised above this
|
||||||
|
civSiteVariety 1.0 0 = flat city sizes; higher = big rivers/coasts host far larger cities
|
||||||
|
civGrowthMin 0.25 growth-rate floor at habitability 0 (rate scales with habitability)
|
||||||
|
civHarvestVar 0.25 year-to-year harvest swing (scaled by continentality)
|
||||||
|
civDroughtStrength 0.70 how hard a full drought cuts a region's carrying capacity
|
||||||
|
civDroughtPeriod 8 yr drought-noise epoch length (drought duration scale)
|
||||||
|
civDroughtThresh -0.15 drought-onset threshold (lower = rarer)
|
||||||
|
civDroughtArid 0.50 extra drought-proneness in arid regions
|
||||||
|
civColdYearStrength 0.50 crop loss in a rare cold year, x the cell's cold exposure
|
||||||
|
civFloodBonus 0.25 fertile-silt bonus on river cells (rare flood disaster)
|
||||||
|
civFamineRate 0.15 /yr accelerated population loss when food < population
|
||||||
|
civStormDeathRate 0.50 /yr deaths from a full-strength storm over a settlement
|
||||||
|
civHurricaneDeathMult 3.0 extra storm-death multiplier for a hurricane/typhoon
|
||||||
|
|
||||||
## Headless logic test (no display)
|
## Headless logic test (no display)
|
||||||
|
|
||||||
|
|||||||
35
CLAUDE.md
35
CLAUDE.md
@ -133,13 +133,22 @@ on the Live World clock). **Steps 1–2 of the roadmap are done (plus a derived
|
|||||||
coast) + food (flora/fauna density + ecoregion productivity), gated by freezing winters / high
|
coast) + food (flora/fauna density + ecoregion productivity), gated by freezing winters / high
|
||||||
terrain; colour mode `Habitability`, key `I`). On key **`U`** ("the dawn") `placeSettlements()` seeds
|
terrain; colour mode `Habitability`, key `I`). On key **`U`** ("the dawn") `placeSettlements()` seeds
|
||||||
a fixed set **once** on the best, well-spaced (`civMinSpacingRadians`) fertile cells (separate
|
a fixed set **once** on the best, well-spaced (`civMinSpacingRadians`) fertile cells (separate
|
||||||
`sCivRng`; named from the continent's `NameGen` bank). `stepCivilization()` runs each live frame:
|
`sCivRng`; named from the continent's `NameGen` bank). `stepCivilization(dtHours, liveTime)` runs each
|
||||||
each settlement's population moves **logistically toward a food-driven carrying capacity** `K =
|
live frame: population moves **logistically toward a food-driven carrying capacity**, but everything is
|
||||||
civMaxPopulation·habitability` (cut transiently where an active volcano ashes the area), so it
|
**environment-driven and dynamic** (not the old "grow the same everywhere"): the growth **rate** scales
|
||||||
**grows / declines / is abandoned** (kept in the set, can revive). Tiers village→town→city by
|
with habitability (fertile cells boom, marginal crawl); the capacity `K = civMaxPopulation·habitability·
|
||||||
population; markers (3D + 2D) sized by tier + a **Civ** tab + cell-info line + kind=3 `WorldEvent`s
|
**siteQuality**·conditions` where **siteQuality** spreads max size by orders of magnitude (a great river
|
||||||
("X grew into a city", "X was abandoned"). Since the set is fixed, the step-back snapshot only
|
— `discharge` is log-scaled — or a coast hosts a metropolis, a dry inland cell a town); and
|
||||||
restores the per-settlement **population** vector. Saved (**v20**). Knobs `civ*`.
|
**conditions** are deterministic time-varying drivers (pure functions of (region, year, seed) → constant
|
||||||
|
within a year, reversible on step-back): **regional droughts** (multi-year, worse in arid regions),
|
||||||
|
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,
|
||||||
|
`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
|
||||||
|
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
|
||||||
|
was abandoned"). The set is fixed, so the step-back snapshot only restores the per-settlement
|
||||||
|
**population** vector; conditions recompute. Saved (**v20**). Knobs `civ*`.
|
||||||
*Next steps (not yet built): territory + borders, kingdoms/empires, culture + beliefs, conflict +
|
*Next steps (not yet built): territory + borders, kingdoms/empires, culture + beliefs, conflict +
|
||||||
diplomacy.*
|
diplomacy.*
|
||||||
|
|
||||||
@ -858,9 +867,15 @@ triangles (plates are fixed in phase 1).
|
|||||||
(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` (2e6, the carrying capacity at
|
||||||
habitability 1), tier thresholds `civTownPop` (5000) / `civCityPop` (100000), `civAbandonPop` (50,
|
habitability 1), tier thresholds `civTownPop` (5000) / `civCityPop` (100000), `civAbandonPop` (50,
|
||||||
below = abandoned/ruins but can revive). An active volcano's ash within ~1.5× its blast radius cuts a
|
below = abandoned/ruins but can revive). **Dynamics** — `civSiteVariety` (1.0; 0 = flat capacities,
|
||||||
settlement's carrying capacity (the "ashed-out region" decline). Marker sizes/colours are render
|
higher = big rivers/coasts host far larger cities → wide size spread), `civGrowthMin` (0.25, growth-rate
|
||||||
constants (ViewerRender.cpp).
|
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`
|
||||||
|
(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) /
|
||||||
|
`civHurricaneDeathMult` (3.0, deaths from a storm/hurricane over a town). Droughts/harvests are
|
||||||
|
deterministic per (~20° region, 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).
|
||||||
- `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
|
||||||
|
|||||||
@ -366,16 +366,35 @@ seeds a **fixed** set once — greedily the highest-habitability cells with a mi
|
|||||||
|
|
||||||
Because placement is one-time, the settlement *set* never changes, so the only mutable per-step state
|
Because placement is one-time, the settlement *set* never changes, so the only mutable per-step state
|
||||||
is each settlement's **population** — which is all the step-back snapshot stores (a `vector<double>` in
|
is each settlement's **population** — which is all the step-back snapshot stores (a `vector<double>` in
|
||||||
`WeatherSnapshot`, restored in `restoreWeather`; no per-frame string churn). `stepCivilization(dtHours)`
|
`WeatherSnapshot`, restored in `restoreWeather`; no per-frame string churn). `stepCivilization(dtHours,
|
||||||
runs in `liveAdvance` after `stepVolcanoes`: each population moves logistically toward a carrying
|
liveTime)` runs in `liveAdvance` after `stepVolcanoes`. It is **environment-driven and dynamic** (the
|
||||||
capacity `K = civMaxPopulation·habitability(cell)`, cut transiently where an active volcano's ash plume
|
first cut grew every town uniformly to the same cap):
|
||||||
(`ashTimer>0`, within ~1.5× blast radius) overlaps — so towns **grow, decline, and are abandoned**
|
- growth **rate** `r = civGrowthRate·(civGrowthMin + (1−civGrowthMin)·habitability)` so fertile cells
|
||||||
(floored at 1 so a site can revive when K recovers). Tier (village/town/city) is derived from
|
grow far faster than marginal ones;
|
||||||
population. The viewer compares before/after populations in `detectLiveEvents` to log kind=3
|
- carrying capacity `K = civMaxPopulation · habitability · siteQuality · conditions`, where
|
||||||
`WorldEvent`s (tier crossings, abandonment), draws markers (3D spheres + 2D dots, sized by tier; city/
|
**siteQuality** = `0.45 + civSiteVariety·(coastBonus + log10(1+discharge/20))` makes max city size vary
|
||||||
town names as 3D labels), adds a **Civ** tab (7th) and a cell-info line, and a `Habitability` colour
|
by an order of magnitude (a continental river or coast → a metropolis, a dry inland cell → a town —
|
||||||
mode (key `I`). `buildGeometry()` clears the set on reseed (like geography/volcanoes). Save **v20**
|
this is what spreads final sizes instead of all saturating equally);
|
||||||
appends the settlement records (population included); `sCellSettlement` is rebuilt on load. Knobs `civ*`.
|
- **conditions** = `harvest · drought · coldYear · flood · ash`, all **deterministic functions of
|
||||||
|
(≈20° region bucket, integer year, seed)** — constant within a year, region-correlated, recomputed
|
||||||
|
on a step-back (pure, so no extra saved/snapshot state): year-to-year harvests (swing scaled by the
|
||||||
|
seasonal-amplitude/continentality field), multi-year droughts (a slow noise interpolated across
|
||||||
|
`civDroughtPeriod`-year epochs, threshold raised by aridity `1−sMoist`), rare cold years (× the cell's
|
||||||
|
near-freezing-winter exposure), river floods (silt bonus / rare disaster), and the volcano-ash cut.
|
||||||
|
- **Storms** read live `storms()` (already snapshotted): a system within its `radius` of a town deals
|
||||||
|
direct deaths `civStormDeathRate·strength·overlap·(hurricane? civHurricaneDeathMult)` — a parked
|
||||||
|
hurricane can gut a coastal city.
|
||||||
|
- logistic step + an accelerated `civFamineRate` loss when `K<P` + the storm deaths; floored at 1 so a
|
||||||
|
site revives. So towns **grow, fluctuate, shrink in droughts, and collapse/abandon** under sustained
|
||||||
|
famine or an acute disaster.
|
||||||
|
Per-settlement derived `sCivCond` (combined multiplier) + `sCivDrought` (severity) drive the viewer:
|
||||||
|
markers are **withered-tinted** by hardship, cell-info shows "drought/conditions", and `detectLiveEvents`
|
||||||
|
logs kind=3 events with the **cause** — a storm over the shrunk town → "Hurricane <name> devastates X"
|
||||||
|
(reuses `weatherEventName`), else "Famine shrinks X to a Town" when `sCivDrought` is high, else tier
|
||||||
|
up/down/abandon. Markers (3D spheres + 2D dots, sized by tier; city/town 3D labels), a **Civ** tab (7th),
|
||||||
|
a cell-info line, and a `Habitability` colour mode (key `I`). `buildGeometry()` clears the set on reseed.
|
||||||
|
Save **v20** stores the settlement records (population included); `sCellSettlement` is rebuilt on load.
|
||||||
|
Knobs `civ*`.
|
||||||
|
|
||||||
## Headless testing
|
## Headless testing
|
||||||
|
|
||||||
|
|||||||
@ -95,6 +95,14 @@ 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).
|
||||||
|
const auto& cond = p.settlementCondition(); const auto& dro = p.settlementDrought();
|
||||||
|
if (si < (int)cond.size()) {
|
||||||
|
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)));
|
||||||
|
else L.push_back(std::string(TextFormat(" conditions %.0f%% (%s)", cd * 100.0,
|
||||||
|
cd > 1.05 ? "good harvest" : cd < 0.8 ? "hardship" : "normal")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Climate (derived; present once computeClimate() has run).
|
// Climate (derived; present once computeClimate() has run).
|
||||||
|
|||||||
@ -372,6 +372,20 @@ void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
|||||||
else if (aliveA && (int)ta > (int)tb)
|
else if (aliveA && (int)ta > (int)tb)
|
||||||
appendEvent(3, 1, liveTime, s.cell, s.id,
|
appendEvent(3, 1, liveTime, s.cell, s.id,
|
||||||
s.name + " grew into a " + settleTierName(ta), popLine(s));
|
s.name + " grew into a " + settleTierName(ta), popLine(s));
|
||||||
|
else if (aliveA && (int)ta < (int)tb) { // tier DOWN -- attribute the cause
|
||||||
|
const char* stormName = nullptr;
|
||||||
|
for (const auto& ws : planet.storms()) {
|
||||||
|
double ang = std::acos(std::clamp(planet.cells[s.cell].unit.dot(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}), -1.0, 1.0));
|
||||||
|
if (ang < ws.radius && ws.strength > 0.35) { stormName = weatherEventName(ws, planet); break; }
|
||||||
|
}
|
||||||
|
double dr = (k < planet.settlementDrought().size()) ? planet.settlementDrought()[k] : 0.0;
|
||||||
|
std::string title, detail = popLine(s);
|
||||||
|
if (stormName) title = std::string(stormName) + " devastates " + s.name;
|
||||||
|
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); }
|
||||||
|
else title = s.name + " declined to a " + settleTierName(ta);
|
||||||
|
appendEvent(3, 2, liveTime, s.cell, s.id, title, detail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -661,7 +675,7 @@ void Viewer::liveAdvance(double dtClock, double 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); // population grows/declines 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);
|
||||||
if (vu.breach) refreshView();
|
if (vu.breach) refreshView();
|
||||||
else if (vu.recolor || cu.recolor) recolor();
|
else if (vu.recolor || cu.recolor) recolor();
|
||||||
|
|||||||
@ -9,6 +9,17 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
// Tint a settlement marker by its live environmental condition (1 = thriving, <1 = hardship/drought):
|
||||||
|
// blend toward a dull withered brown-red and darken as conditions worsen.
|
||||||
|
static Color witherColor(Color base, double cond) {
|
||||||
|
double h = std::clamp((0.8 - cond) / 0.6, 0.0, 1.0); // 0 above 0.8 .. 1 at/below 0.2
|
||||||
|
if (h <= 0.0) return base;
|
||||||
|
const unsigned char w[3] = { 130, 80, 70 }; // withered brown-red
|
||||||
|
double dim = 1.0 - 0.35 * h;
|
||||||
|
auto L = [&](unsigned char b, unsigned char wc) { return (unsigned char)std::clamp((b * (1.0 - h) + wc * h) * dim, 0.0, 255.0); };
|
||||||
|
return Color{ L(base.r, w[0]), L(base.g, w[1]), L(base.b, w[2]), base.a };
|
||||||
|
}
|
||||||
|
|
||||||
// Label style for a geographic feature: font size + colour, returns true if it's a "minor" feature
|
// Label style for a geographic feature: font size + colour, returns true if it's a "minor" feature
|
||||||
// (peaks/rivers/lakes/seas/small islands) -- those are drawn only when zoomed in, to declutter.
|
// (peaks/rivers/lakes/seas/small islands) -- those are drawn only when zoomed in, to declutter.
|
||||||
static bool labelStyle(const GeoFeature& f, int& font, Color& col) {
|
static bool labelStyle(const GeoFeature& f, int& font, Color& col) {
|
||||||
@ -211,7 +222,9 @@ void Viewer::renderGlobe3D() {
|
|||||||
// Settlement markers (civilization): a dot per settlement, sized + coloured by tier; dim for ruins.
|
// Settlement markers (civilization): a dot per settlement, sized + coloured by tier; dim for ruins.
|
||||||
if (showSettlements && !planet.settlements.empty()) {
|
if (showSettlements && !planet.settlements.empty()) {
|
||||||
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
||||||
for (const Settlement& s : planet.settlements) {
|
const auto& cond = planet.settlementCondition();
|
||||||
|
for (size_t k = 0; k < planet.settlements.size(); ++k) {
|
||||||
|
const Settlement& s = planet.settlements[k];
|
||||||
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
||||||
const Cell& c = planet.cells[s.cell];
|
const Cell& c = planet.cells[s.cell];
|
||||||
float r = visBase + (float)c.elevation * elevExagg + 0.006f;
|
float r = visBase + (float)c.elevation * elevExagg + 0.006f;
|
||||||
@ -223,6 +236,7 @@ void Viewer::renderGlobe3D() {
|
|||||||
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
||||||
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
||||||
: Color{210, 130, 85, 255};
|
: Color{210, 130, 85, 255};
|
||||||
|
col = witherColor(col, k < cond.size() ? cond[k] : 1.0); // hardship -> withered tint
|
||||||
DrawSphere(p, rad, col);
|
DrawSphere(p, rad, col);
|
||||||
if (alive && t != SettleTier::Village) { // a ring marks notable settlements
|
if (alive && t != SettleTier::Village) { // a ring marks notable settlements
|
||||||
float rr = visBase + (float)c.elevation * elevExagg + 0.01f;
|
float rr = visBase + (float)c.elevation * elevExagg + 0.01f;
|
||||||
@ -355,8 +369,10 @@ void Viewer::renderMap2D() {
|
|||||||
}
|
}
|
||||||
if (showSettlements && !planet.settlements.empty()) {
|
if (showSettlements && !planet.settlements.empty()) {
|
||||||
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
||||||
|
const auto& cond = planet.settlementCondition();
|
||||||
float zf = (float)std::min(2.0, mapZoom);
|
float zf = (float)std::min(2.0, mapZoom);
|
||||||
for (const Settlement& s : planet.settlements) {
|
for (size_t k = 0; k < planet.settlements.size(); ++k) {
|
||||||
|
const Settlement& s = planet.settlements[k];
|
||||||
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
||||||
double lon, lat; dirToLonLat(planet.cells[s.cell].unit, lon, lat);
|
double lon, lat; dirToLonLat(planet.cells[s.cell].unit, lon, lat);
|
||||||
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
||||||
@ -367,6 +383,7 @@ void Viewer::renderMap2D() {
|
|||||||
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
||||||
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
||||||
: Color{210, 130, 85, 255};
|
: Color{210, 130, 85, 255};
|
||||||
|
col = witherColor(col, k < cond.size() ? cond[k] : 1.0);
|
||||||
DrawCircleV(sp, rad, col);
|
DrawCircleV(sp, rad, col);
|
||||||
if (alive && t != SettleTier::Village) DrawCircleLines((int)sp.x, (int)sp.y, rad + 2.0f, Color{255, 245, 210, 180});
|
if (alive && t != SettleTier::Village) DrawCircleLines((int)sp.x, (int)sp.y, rad + 2.0f, Color{255, 245, 210, 180});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +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();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Planet::clearDerivedState() {
|
void Planet::clearDerivedState() {
|
||||||
|
|||||||
@ -183,10 +183,15 @@ public:
|
|||||||
// / is abandoned). Saved (v20); step-back restores populations via WeatherSnapshot.
|
// / is abandoned). Saved (v20); step-back restores populations via WeatherSnapshot.
|
||||||
void computeHabitability();
|
void computeHabitability();
|
||||||
void placeSettlements();
|
void placeSettlements();
|
||||||
CivUpdate stepCivilization(double dtHours);
|
CivUpdate stepCivilization(double dtHours, double liveTime);
|
||||||
bool settlementsPlaced() const { return !settlements.empty(); }
|
bool settlementsPlaced() const { return !settlements.empty(); }
|
||||||
const std::vector<int>& cellSettlement() const { return sCellSettlement; } // settlement index per cell (-1)
|
const std::vector<int>& cellSettlement() const { return sCellSettlement; } // settlement index per cell (-1)
|
||||||
const std::vector<double>& habitability() const { return sHabitability; } // 0..1 per cell (derived)
|
const std::vector<double>& habitability() const { return sHabitability; } // 0..1 per cell (derived)
|
||||||
|
// Per-settlement live conditions (derived each stepCivilization; not saved). condition = the combined
|
||||||
|
// environmental multiplier on carrying capacity (1 = normal, <1 = hardship, >1 = boom); drought =
|
||||||
|
// 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>& settlementDrought() const { return sCivDrought; }
|
||||||
|
|
||||||
// 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;
|
||||||
@ -311,6 +316,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)
|
||||||
uint32_t sCivRng = 1;
|
uint32_t sCivRng = 1;
|
||||||
|
|
||||||
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
||||||
|
|||||||
@ -4,6 +4,16 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
|
// Small deterministic hashes/noise for the environment drivers (mirrors the subgrid value-noise in
|
||||||
|
// Planet.cpp; kept local). A driver is a pure function of (cell/region, year, seed) so it is constant
|
||||||
|
// within a year, region-correlated, and recomputed on a step-back -> reversible with no extra state.
|
||||||
|
namespace {
|
||||||
|
inline uint32_t civHash(uint32_t a) { a ^= a << 13; a ^= a >> 17; a ^= a << 5; return a ? a : 1u; }
|
||||||
|
inline double civHashf(uint32_t a) { return (civHash(a) & 0xFFFFFFu) / double(0x1000000); } // [0,1)
|
||||||
|
inline double civSigned(uint32_t a) { return civHashf(a) * 2.0 - 1.0; } // [-1,1)
|
||||||
|
inline double civSmooth(double t) { return t * t * (3.0 - 2.0 * t); }
|
||||||
|
}
|
||||||
|
|
||||||
// --- Civilization Step 2: settlements & habitability -------------------------
|
// --- Civilization Step 2: settlements & habitability -------------------------
|
||||||
// A per-cell habitability/food score, a one-time placement of settlement point-agents on the best
|
// A per-cell habitability/food score, a one-time placement of settlement point-agents on the best
|
||||||
// (well-spaced) cells, and their population growth/decline on the Live World clock toward a
|
// (well-spaced) cells, and their population growth/decline on the Live World clock toward a
|
||||||
@ -103,32 +113,119 @@ void Planet::placeSettlements() {
|
|||||||
settlements[k].id = (uint32_t)(k + 1);
|
settlements[k].id = (uint32_t)(k + 1);
|
||||||
sCellSettlement[settlements[k].cell] = k;
|
sCellSettlement[settlements[k].cell] = k;
|
||||||
}
|
}
|
||||||
|
sCivCond.assign(settlements.size(), 1.0);
|
||||||
|
sCivDrought.assign(settlements.size(), 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One live-frame civilization step: each settlement's population moves logistically toward its
|
// One live-frame civilization step. Each settlement's population moves logistically toward a food-driven
|
||||||
// food-driven carrying capacity K = civMaxPopulation * habitability (cut transiently where an active
|
// carrying capacity K that varies by the LOCAL environment AND over time -- year-to-year harvests,
|
||||||
// volcano ashes the area). Grows below K, declines above it; floored at 1 so an abandoned site can
|
// multi-year droughts (worse in arid regions), rare cold years and river floods, plus volcano ash -- and
|
||||||
// revive if K recovers. Pure of any string churn; the set never changes here.
|
// growth RATE scales with habitability so fertile cells grow far faster than marginal ones. Storms over a
|
||||||
CivUpdate Planet::stepCivilization(double dtHours) {
|
// town kill people directly (hurricanes worst). All drivers are deterministic functions of (cell, year,
|
||||||
|
// seed), so a step-back replays them; population is restored from the snapshot. Floored at 1 so an
|
||||||
|
// abandoned site can revive. Conditions are recomputed even when paused (dtHours == 0) so the view tints.
|
||||||
|
CivUpdate Planet::stepCivilization(double dtHours, double liveTime) {
|
||||||
CivUpdate up;
|
CivUpdate up;
|
||||||
if (settlements.empty() || dtHours <= 0.0) return up;
|
if (settlements.empty()) return up;
|
||||||
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); }
|
||||||
|
|
||||||
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 = dtHours / yearHours;
|
const double dtYears = std::max(0.0, dtHours) / yearHours;
|
||||||
for (Settlement& st : settlements) {
|
const int year = (int)std::floor(std::max(0.0, liveTime) / yearHours);
|
||||||
|
const uint32_t base = cfg.seed ? cfg.seed : 1u;
|
||||||
|
const bool haveMoist = (int)sMoist.size() == n, haveSeason = (int)sTempSummer.size() == n && (int)sTempWinter.size() == n;
|
||||||
|
const bool haveWinter = (int)sTempWinter.size() == n, haveDisch = (int)sDischarge.size() == n;
|
||||||
|
|
||||||
|
for (int k = 0; k < (int)settlements.size(); ++k) {
|
||||||
|
Settlement& st = settlements[k];
|
||||||
if (st.cell < 0 || st.cell >= n) continue;
|
if (st.cell < 0 || st.cell >= n) continue;
|
||||||
double K = cfg.civMaxPopulation * sHabitability[st.cell];
|
const int c = st.cell;
|
||||||
for (const Volcano& v : volcanoes) { // active ash plume nearby cuts carrying capacity
|
// A coarse spatial bucket (~20deg cells) so droughts / harvests vary REGIONALLY -- different parts
|
||||||
if (v.ashTimer <= 0.0 || v.cell < 0 || v.cell >= n) continue;
|
// of a continent have different fortunes, instead of every town drought-ing together.
|
||||||
double ang = std::acos(std::clamp(cells[st.cell].unit.dot(cells[v.cell].unit), -1.0, 1.0));
|
double clat = std::asin(std::clamp(cells[c].unit.y, -1.0, 1.0));
|
||||||
if (ang < cfg.volcanoBlastRadius * 1.5) { K *= 0.3; break; }
|
double clon = std::atan2(cells[c].unit.z, cells[c].unit.x);
|
||||||
|
int latB = (int)std::floor((clat + 1.5708) / 0.349), lonB = (int)std::floor((clon + 3.14160) / 0.349);
|
||||||
|
const uint32_t region = civHash((uint32_t)(latB * 131 + lonB) * 2654435761u + 0x9E3779B9u);
|
||||||
|
const double hab = sHabitability[c];
|
||||||
|
const double amp = haveSeason ? std::max(0.0, sTempSummer[c] - sTempWinter[c]) : 8.0; // climate variability
|
||||||
|
const double aridity = haveMoist ? std::clamp(1.0 - sMoist[c], 0.0, 1.0) : 0.4;
|
||||||
|
const double winter = haveWinter ? sTempWinter[c] : (((int)sTemp.size() == n) ? sTemp[c] : 10.0);
|
||||||
|
const bool onRiver = haveDisch && sDischarge[c] > cfg.riverThreshold;
|
||||||
|
|
||||||
|
// Harvest: region-correlated good/bad year, swing scaled by continentality (amp).
|
||||||
|
double varScale = cfg.civHarvestVar * std::clamp(0.3 + 0.7 * (amp / 30.0), 0.3, 1.6);
|
||||||
|
double rNoise = civSigned(base ^ civHash((uint32_t)region * 0x9E3779B9u + (uint32_t)year * 2654435761u));
|
||||||
|
double cNoise = civSigned(base ^ civHash((uint32_t)c * 2654435761u ^ ((uint32_t)year * 0x85EBCA6Bu)));
|
||||||
|
double harvest = std::clamp(1.0 + (0.7 * rNoise + 0.3 * cNoise) * varScale, 0.2, 1.7);
|
||||||
|
|
||||||
|
// Drought: a slow signal interpolated across multi-year epochs; arid regions cross the threshold
|
||||||
|
// more often, so droughts last several years and bite hardest in dry lands.
|
||||||
|
double ft = (double)year / std::max(1.0, cfg.civDroughtPeriod);
|
||||||
|
double fe = std::floor(ft); int e0 = (int)fe; double tf = civSmooth(ft - fe);
|
||||||
|
double d0 = civSigned(base ^ civHash((uint32_t)region * 0x1B873593u + (uint32_t)e0 * 40503u));
|
||||||
|
double d1 = civSigned(base ^ civHash((uint32_t)region * 0x1B873593u + (uint32_t)(e0 + 1) * 40503u));
|
||||||
|
double dn = d0 + (d1 - d0) * tf;
|
||||||
|
double thr = cfg.civDroughtThresh + cfg.civDroughtArid * aridity;
|
||||||
|
double drought = (dn < thr) ? std::clamp(thr - dn, 0.0, 1.0) : 0.0;
|
||||||
|
double droughtFactor = std::clamp(1.0 - cfg.civDroughtStrength * drought, 0.05, 1.0);
|
||||||
|
|
||||||
|
// Cold year: a rare bad year times how cold-marginal the cell is (near-freezing winter).
|
||||||
|
double coldExposure = std::clamp((5.0 - winter) / 30.0, 0.0, 1.0);
|
||||||
|
double cy = civHashf(base ^ civHash((uint32_t)region * 0x55u + (uint32_t)year * 0x9E3779B9u + 7u));
|
||||||
|
double coldEvent = (cy > 0.85) ? (cy - 0.85) / 0.15 : 0.0;
|
||||||
|
double coldFactor = std::clamp(1.0 - cfg.civColdYearStrength * coldEvent * coldExposure, 0.2, 1.0);
|
||||||
|
|
||||||
|
// Floods (river cells): fertile silt most years, a rare destructive flood.
|
||||||
|
double floodFactor = 1.0;
|
||||||
|
if (onRiver) {
|
||||||
|
double f = civHashf(base ^ civHash((uint32_t)c * 0x2545F491u + (uint32_t)year * 19349663u));
|
||||||
|
floodFactor = (f > 0.94) ? 0.5 : 1.0 + cfg.civFloodBonus * (0.3 + 0.7 * f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Volcano ash nearby (existing).
|
||||||
|
double ashFactor = 1.0;
|
||||||
|
for (const Volcano& v : volcanoes) {
|
||||||
|
if (v.ashTimer <= 0.0 || v.cell < 0 || v.cell >= n) continue;
|
||||||
|
double ang = std::acos(std::clamp(cells[c].unit.dot(cells[v.cell].unit), -1.0, 1.0));
|
||||||
|
if (ang < cfg.volcanoBlastRadius * 1.5) { ashFactor = 0.3; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Site quality: a settlement's MAX size varies enormously with its location -- a great river
|
||||||
|
// (discharge spans orders of magnitude) or a coast hosts a metropolis, a dry inland cell a town.
|
||||||
|
// This is what makes final sizes vary widely instead of all reaching the same cap.
|
||||||
|
double disch = haveDisch ? sDischarge[c] : 0.0;
|
||||||
|
bool coast = false;
|
||||||
|
for (int j : cells[c].neighbors) if (cells[j].elevation <= cfg.seaLevel) { coast = true; break; }
|
||||||
|
double riverQ = std::log10(1.0 + disch / 20.0); // ~0 tiny .. ~2.5 a continental river
|
||||||
|
double siteQ = std::clamp(0.45 + cfg.civSiteVariety * ((coast ? 1.2 : 0.0) + riverQ), 0.3, 6.0);
|
||||||
|
|
||||||
|
double cond = harvest * droughtFactor * coldFactor * floodFactor * ashFactor;
|
||||||
|
double K = cfg.civMaxPopulation * hab * siteQ * cond;
|
||||||
|
sCivCond[k] = cond; sCivDrought[k] = drought;
|
||||||
|
|
||||||
|
// Storms over the town kill people directly (acute), hurricanes worst. Storms are snapshotted, so
|
||||||
|
// this stays consistent on a step-back.
|
||||||
|
double stormLoss = 0.0;
|
||||||
|
for (const WeatherSystem& ws : sStorms) {
|
||||||
|
double ang = std::acos(std::clamp(cells[c].unit.dot(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}), -1.0, 1.0));
|
||||||
|
if (ang >= ws.radius) continue;
|
||||||
|
double overlap = 1.0 - ang / std::max(1e-6, ws.radius);
|
||||||
|
bool hur = ws.tropical && ws.strength >= cfg.weatherHurricaneStr;
|
||||||
|
stormLoss += cfg.civStormDeathRate * ws.strength * overlap * (hur ? cfg.civHurricaneDeathMult : 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dtYears > 0.0) {
|
||||||
double P = st.population;
|
double P = st.population;
|
||||||
P += cfg.civGrowthRate * P * (1.0 - P / std::max(1.0, K)) * dtYears; // logistic (declines when K<P)
|
double r = cfg.civGrowthRate * (cfg.civGrowthMin + (1.0 - cfg.civGrowthMin) * hab); // env-driven rate
|
||||||
P = std::max(1.0, P); // keep a seed so an abandoned site can revive
|
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
|
||||||
|
P -= stormLoss * P * dtYears; // acute storm deaths
|
||||||
|
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;
|
||||||
st.population = P;
|
st.population = P;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return up;
|
return up;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,9 @@
|
|||||||
D(civMinSpacingRadians) D(civMinHabitability) D(civSeedPopulation) D(civGrowthRate) \
|
D(civMinSpacingRadians) D(civMinHabitability) D(civSeedPopulation) D(civGrowthRate) \
|
||||||
D(civMaxPopulation) D(civTownPop) D(civCityPop) D(civAbandonPop) \
|
D(civMaxPopulation) D(civTownPop) D(civCityPop) D(civAbandonPop) \
|
||||||
D(civHabWaterWeight) D(civHabFoodWeight) D(civHabTempOpt) D(civHabElevPenalty) \
|
D(civHabWaterWeight) D(civHabFoodWeight) D(civHabTempOpt) D(civHabElevPenalty) \
|
||||||
|
D(civSiteVariety) D(civGrowthMin) D(civHarvestVar) D(civDroughtStrength) D(civDroughtPeriod) D(civDroughtThresh) \
|
||||||
|
D(civDroughtArid) D(civColdYearStrength) D(civFloodBonus) D(civFamineRate) \
|
||||||
|
D(civStormDeathRate) D(civHurricaneDeathMult) \
|
||||||
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
||||||
@ -263,6 +266,18 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.civHabFoodWeight, 0.0, 1.0, "civHabFoodWeight"));
|
E(rng(cfg.civHabFoodWeight, 0.0, 1.0, "civHabFoodWeight"));
|
||||||
E(rng(cfg.civHabTempOpt, -20.0, 50.0, "civHabTempOpt"));
|
E(rng(cfg.civHabTempOpt, -20.0, 50.0, "civHabTempOpt"));
|
||||||
E(rng(cfg.civHabElevPenalty, 0.0, 12000.0, "civHabElevPenalty"));
|
E(rng(cfg.civHabElevPenalty, 0.0, 12000.0, "civHabElevPenalty"));
|
||||||
|
E(rng(cfg.civSiteVariety, 0.0, 4.0, "civSiteVariety"));
|
||||||
|
E(rng(cfg.civGrowthMin, 0.0, 1.0, "civGrowthMin"));
|
||||||
|
E(rng(cfg.civHarvestVar, 0.0, 2.0, "civHarvestVar"));
|
||||||
|
E(rng(cfg.civDroughtStrength, 0.0, 1.0, "civDroughtStrength"));
|
||||||
|
E(rng(cfg.civDroughtPeriod, 0.1, 1000.0, "civDroughtPeriod"));
|
||||||
|
E(rng(cfg.civDroughtThresh, -2.0, 2.0, "civDroughtThresh"));
|
||||||
|
E(rng(cfg.civDroughtArid, 0.0, 4.0, "civDroughtArid"));
|
||||||
|
E(rng(cfg.civColdYearStrength, 0.0, 1.0, "civColdYearStrength"));
|
||||||
|
E(rng(cfg.civFloodBonus, 0.0, 2.0, "civFloodBonus"));
|
||||||
|
E(rng(cfg.civFamineRate, 0.0, 10.0, "civFamineRate"));
|
||||||
|
E(rng(cfg.civStormDeathRate, 0.0, 10.0, "civStormDeathRate"));
|
||||||
|
E(rng(cfg.civHurricaneDeathMult, 1.0, 50.0, "civHurricaneDeathMult"));
|
||||||
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"));
|
||||||
|
|||||||
@ -395,4 +395,19 @@ struct PlanetConfig {
|
|||||||
double civHabFoodWeight = 0.40; // habitability weight of food (flora/fauna + ecoregion)
|
double civHabFoodWeight = 0.40; // habitability weight of food (flora/fauna + ecoregion)
|
||||||
double civHabTempOpt = 18.0; // C: most comfortable annual-mean temperature
|
double civHabTempOpt = 18.0; // C: most comfortable annual-mean temperature
|
||||||
double civHabElevPenalty = 2500.0; // m above which high terrain steeply reduces habitability
|
double civHabElevPenalty = 2500.0; // m above which high terrain steeply reduces habitability
|
||||||
|
// Dynamic environment (PlanetCiv.cpp): growth differs by local conditions and varies over time
|
||||||
|
// (harvests, droughts, cold years, floods, storms) so settlements aren't static. Deterministic
|
||||||
|
// functions of (cell, year, seed) -> reversible with the live stepper; no save change.
|
||||||
|
double civSiteVariety = 1.0; // 0 = flat capacities, 1 = full site-quality spread (big rivers/coasts host large cities)
|
||||||
|
double civGrowthMin = 0.25; // growth-rate fraction at habitability 0 (1 = at habitability 1)
|
||||||
|
double civHarvestVar = 0.25; // base year-to-year harvest swing amplitude (scaled by climate variability)
|
||||||
|
double civDroughtStrength = 0.70; // how hard a full drought cuts a region's carrying capacity
|
||||||
|
double civDroughtPeriod = 8.0; // years per drought-noise epoch (drought duration scale)
|
||||||
|
double civDroughtThresh = -0.15; // drought-onset threshold on the slow noise (lower = rarer)
|
||||||
|
double civDroughtArid = 0.50; // extra drought-proneness in arid regions (× aridity)
|
||||||
|
double civColdYearStrength = 0.50; // crop loss in a rare cold year, × the cell's cold exposure
|
||||||
|
double civFloodBonus = 0.25; // fertile-silt bonus on river cells most years (rare flood disaster)
|
||||||
|
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 civHurricaneDeathMult= 3.0; // extra storm death multiplier for a hurricane/typhoon
|
||||||
};
|
};
|
||||||
|
|||||||
56
test_civ.cpp
56
test_civ.cpp
@ -79,19 +79,59 @@ int main() {
|
|||||||
check(capOk, "settlement count within the cap");
|
check(capOk, "settlement count within the cap");
|
||||||
check(uniqueNames, "settlement names are unique + non-empty");
|
check(uniqueNames, "settlement names are unique + non-empty");
|
||||||
|
|
||||||
std::printf("Civ: food-driven growth + decline\n");
|
std::printf("Civ: environment-driven growth (differentiated + dynamic)\n");
|
||||||
{
|
{
|
||||||
int gi = 0; for (size_t k = 0; k < S.size(); ++k) if (p.habitability()[S[k].cell] > p.habitability()[S[gi].cell]) gi = (int)k;
|
int gi = 0, lo = 0;
|
||||||
double p0 = p.settlements[gi].population;
|
for (size_t k = 0; k < S.size(); ++k) {
|
||||||
for (int k = 0; k < 400; ++k) p.stepCivilization(5.0 * yearH); // ~2000 yr of small steps
|
if (p.habitability()[S[k].cell] > p.habitability()[S[gi].cell]) gi = (int)k;
|
||||||
check(p.settlements[gi].population > p0 * 2.0, "a high-habitability settlement grows");
|
if (p.habitability()[S[k].cell] < p.habitability()[S[lo].cell]) lo = (int)k;
|
||||||
// Decline: push one well over its carrying capacity, then step -> it shrinks.
|
}
|
||||||
|
double g0 = p.settlements[gi].population;
|
||||||
|
double lt = 0.0; bool anyDecline = false;
|
||||||
|
std::vector<double> prev(p.settlements.size());
|
||||||
|
for (int yr = 0; yr < 600; ++yr) {
|
||||||
|
for (size_t k = 0; k < p.settlements.size(); ++k) prev[k] = p.settlements[k].population;
|
||||||
|
lt += 2.0 * yearH;
|
||||||
|
p.stepCivilization(2.0 * yearH, lt); // advance the clock so harvests/droughts vary
|
||||||
|
for (size_t k = 0; k < p.settlements.size(); ++k)
|
||||||
|
if (p.settlements[k].population > p.cfg.civAbandonPop && p.settlements[k].population < prev[k] * 0.999) anyDecline = true;
|
||||||
|
}
|
||||||
|
check(p.settlements[gi].population > g0 * 2.0, "a fertile settlement grows strongly");
|
||||||
|
// The user's complaint was "they grow the same amount everywhere": now sizes must vary widely.
|
||||||
|
std::vector<double> pops;
|
||||||
|
for (const auto& s : p.settlements) if (s.population >= p.cfg.civAbandonPop) pops.push_back(s.population);
|
||||||
|
std::sort(pops.begin(), pops.end());
|
||||||
|
double med = pops.empty() ? 0.0 : pops[pops.size() / 2];
|
||||||
|
double mx = pops.empty() ? 0.0 : pops.back();
|
||||||
|
std::printf(" alive %d median %.0f max %.0f (max/median %.1f)\n",
|
||||||
|
(int)pops.size(), med, mx, med > 0 ? mx / med : 0.0);
|
||||||
|
check(!pops.empty() && mx > med * 3.0, "settlement sizes vary widely (env-driven, not uniform growth)");
|
||||||
|
check(p.settlements[gi].population > p.settlements[lo].population, "fertile ends larger than marginal");
|
||||||
|
check(anyDecline, "settlements decline in bad years (harvest/drought dynamics, not monotonic)");
|
||||||
|
// Over-capacity settlement declines toward its food limit.
|
||||||
p.settlements[gi].population = 5.0e7;
|
p.settlements[gi].population = 5.0e7;
|
||||||
double over = p.settlements[gi].population;
|
double over = p.settlements[gi].population;
|
||||||
for (int k = 0; k < 400; ++k) p.stepCivilization(5.0 * yearH);
|
for (int yr = 0; yr < 200; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
|
||||||
check(p.settlements[gi].population < over, "an over-capacity settlement declines toward its food limit");
|
check(p.settlements[gi].population < over, "an over-capacity settlement declines toward its food limit");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::printf("Civ: a hurricane over a town kills people\n");
|
||||||
|
{
|
||||||
|
Planet w; w.generate(cfg); settle(w); drift(w, 400); w.placeSettlements();
|
||||||
|
if (!w.settlements.empty()) {
|
||||||
|
int si = 0; for (size_t k = 0; k < w.settlements.size(); ++k) if (w.settlements[k].population > w.settlements[si].population) si = (int)k;
|
||||||
|
w.settlements[si].population = 1.0e5;
|
||||||
|
WeatherSnapshot snap = w.captureWeather();
|
||||||
|
WeatherSystem ws; ws.id = 999; ws.pos = w.cells[w.settlements[si].cell].unit;
|
||||||
|
ws.radius = 0.3; ws.strength = 1.0; ws.tropical = true; ws.life = 1e9;
|
||||||
|
snap.storms.push_back(ws);
|
||||||
|
w.restoreWeather(snap); // inject a stationary hurricane over the town
|
||||||
|
double before = w.settlements[si].population;
|
||||||
|
double lt = 0.0; for (int yr = 0; yr < 3; ++yr) { lt += yearH; w.stepCivilization(yearH, lt); }
|
||||||
|
check(w.settlements[si].population < before * 0.9, "a hurricane parked over a town kills its population");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::printf("Civ: tiers\n");
|
std::printf("Civ: tiers\n");
|
||||||
check(settleTierOf(100.0, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::Village
|
check(settleTierOf(100.0, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::Village
|
||||||
&& settleTierOf(p.cfg.civTownPop, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::Town
|
&& settleTierOf(p.cfg.civTownPop, p.cfg.civTownPop, p.cfg.civCityPop) == SettleTier::Town
|
||||||
@ -111,7 +151,7 @@ int main() {
|
|||||||
for (int k = 0; k < 40; ++k) {
|
for (int k = 0; k < 40; ++k) {
|
||||||
double dx = x.cflDtMy(); x.advect(dx); x.step(); x.erode(dx);
|
double dx = x.cflDtMy(); x.advect(dx); x.step(); x.erode(dx);
|
||||||
double dy = y.cflDtMy(); y.advect(dy); y.step(); y.erode(dy);
|
double dy = y.cflDtMy(); y.advect(dy); y.step(); y.erode(dy);
|
||||||
if (k == 20) { y.placeSettlements(); y.stepCivilization(yearH); }
|
if (k == 20) { y.placeSettlements(); y.stepCivilization(yearH, yearH); }
|
||||||
}
|
}
|
||||||
bool terrainSame = true;
|
bool terrainSame = true;
|
||||||
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false;
|
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user