Civ: colonization (kingdoms found colonies + islands) + reseed overlay cleanup
Two things: 1. Fix: pressing R (reseed) after civilization existed left stale territory/ culture/war/diplomacy/trade overlay lines drawing on the new world. regenWorld() now clears the civ overlay segment vectors + their toggles and resets a civ colour mode back to Biome. 2. Feature: the settlement set is no longer fixed after U. stepColonization() runs once per sim year: a kingdom+ realm (totalPop >= civColonyMinPop) may found a new settlement on the best unclaimed, habitable, well-spaced cell within reach -- overland, or across water from a coastal member (so islands / other continents get colonised). The colony is APPENDED and bound to the founder's realm by allegiance (Step 5) + a colonial supply trade link (computeTrade, Step 7) so it survives on marginal land while the founder lives; when the founder's capital falls, allegiance clears -> supply ends -> the colony subsists locally and may wither. Deterministic (pure hashes). The growing set needs no save-format change: the v20 settlement block is variable-length, and restoreWeather now TRUNCATES settlements to the snapshot's count (step-back drops colonies founded after; they re-found identically on replay). Founding logs a kind=3 event. civColon* config knobs. (Determinism fix: within one stepColonization call the per-settlement arrays sSettleNation/sSettleAllegiance/sCivCond are resized on each append, else the next realm's member loop read out of bounds.) test_colony.cpp: colonies founded (set grows), colony in founder's realm + a supply link, cross-water/island colonies, founder loss frees + de-supplies, step-back truncates, determinism, save round-trip. All 16 suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e27b91a128
commit
cdf4319b31
15
BUILD.md
15
BUILD.md
@ -370,6 +370,19 @@ motivation. Recomputed each sim year; sea cyan / land+river amber routes + a gol
|
||||
tradeDiploBonus 0.1 attitude nudge/yr between trade-partner realms (reward alliance)
|
||||
tradeTemptWar 0.3 war-hostility weight of a wealthy weak target (tempt war)
|
||||
|
||||
Colonization (PlanetConfig, derived, no save bump): once per sim year a kingdom+ realm founds a new
|
||||
settlement on good unclaimed land -- overland, or across water from a coastal member (islands). Colonies
|
||||
are bound to the founder by allegiance + a colonial supply trade link, surviving while the founder lives.
|
||||
|
||||
civColonizeRate 0.15 per-eligible-realm per-year chance to found a colony
|
||||
civColonyMinPop 2e5 a realm must reach this total population to colonize (kingdoms+)
|
||||
civColonyMinHab 0.30 minimum habitability of a colony site
|
||||
civColonyReach 0.12 rad overland colonization reach from a realm settlement
|
||||
civColonySeaReach 0.35 rad extra reach over water from a coastal member (islands / abroad)
|
||||
civColonySpacing 0.05 rad a colony must be at least this far from every settlement
|
||||
civMaxColonies 120 cap on colonies founded beyond civMaxSettlements
|
||||
civColonySupply 2.0 prosperity from the overlord supply link (keeps colonies alive)
|
||||
|
||||
Culture, beliefs & governments (key X — derived from settlements + geography, not saved): settlements
|
||||
on a continent share a culture (a people with an environment-driven ethos + a religion); each realm
|
||||
gets a government type folded into its name (Republic of X / Duchy of X / X Theocracy / ...); realms
|
||||
@ -390,7 +403,7 @@ src/sim/PlanetCulture.cpp (computeCultures).
|
||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||
|
||||
# Same source list for every suite: swap test_logic.cpp -> test_biota / test_live / test_ocean /
|
||||
# test_weather / test_volcano / test_geography / test_ecoregions / test_civ / test_nation / test_culture / test_conflict / test_diplomacy / test_trade.
|
||||
# test_weather / test_volcano / test_geography / test_ecoregions / test_civ / test_nation / test_culture / test_conflict / test_diplomacy / test_trade / test_colony.
|
||||
# The CMake build also includes test_events for the viewer event journal.
|
||||
|
||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||
|
||||
30
CLAUDE.md
30
CLAUDE.md
@ -233,6 +233,22 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived
|
||||
sim year, no save state, no version bump**, rewinds for free. Render: a **Wealth heat map** (`wealthColor`,
|
||||
gold hubs) + a **trade-route overlay** (sea cyan / river+land amber) under key **`Z`**, a gold wealth dot
|
||||
per settlement in the **Civ** tab, and a cell-info prosperity/route-count line. Knobs `trade*`.
|
||||
- **Colonization (kingdoms found colonies + islands)** *(done — see `PlanetCiv.cpp` `stepColonization`)* —
|
||||
the settlement set is no longer fixed after `U`. Once per sim year a **kingdom+** realm (`totalPop ≥
|
||||
civColonyMinPop`) may **found a new settlement** on the best **unclaimed** (wilderness) high-habitability,
|
||||
well-spaced cell within reach of a member — **overland** (`civColonyReach`) or **across water** from a
|
||||
**coastal** member (`civColonySeaReach`, so **islands / other continents** are colonised). The colony is
|
||||
**appended** to `settlements`, bound to the founder's realm by **allegiance** (Step 5, so it joins the
|
||||
founder's realm even across cultures/water) and given a **colonial supply trade link** to the founder
|
||||
(Step 7, `civColonySupply`) so its prosperity keeps it alive on marginal land — **as long as the founder
|
||||
is alive** (when the founder's capital falls, Step-5 pruning clears the allegiance → the supply ends →
|
||||
the colony subsists on local food/trade and may wither). Deterministic (pure hashes, no RNG); the set
|
||||
**grows by appending** (saved as-is — the v20 settlement block is variable-length; **no save-version
|
||||
bump**), and **step-back truncates** it (`restoreWeather` resizes to the snapshot's settlement count → a
|
||||
colony re-founds identically on replay). Founding logs a **kind=3** event ("The Kingdom of X founds the
|
||||
colony of Y"). Knobs `civColon*`. *(Also fixed: pressing `R` (reseed) after civilization existed left
|
||||
stale territory/culture/war/diplomacy/trade overlay lines on the new world — `regenWorld()` now clears
|
||||
the civ overlay vectors + toggles.)*
|
||||
*Next steps (not yet built): cultural **evolution** (spread along borders, drift, assimilation, schism);
|
||||
tribute / vassalage treaties; an accumulated treasury (funding armies / buying peace).*
|
||||
|
||||
@ -643,7 +659,7 @@ src/
|
||||
NameGen.* deterministic procedural name generator (syllable banks; reused by civ arc)
|
||||
PlanetGeography.* generateGeography() (named features: continents/oceans/ranges/rivers/lakes)
|
||||
PlanetEcoregions.* generateEcoregions() (named ecological provinces + dominant biota/productivity)
|
||||
PlanetCiv.* computeHabitability/placeSettlements/stepCivilization (settlements; civ Step 2)
|
||||
PlanetCiv.* computeHabitability/placeSettlements/stepCivilization/stepColonization (settlements + colonies; civ Step 2)
|
||||
PlanetNation.* computeTerritory (realms + per-cell ownership + borders; civ Step 3)
|
||||
PlanetCulture.* computeCultures (cultures/ethos/faith per continent + governments; civ Step 4)
|
||||
PlanetConflict.* stepConflict (wars/conquest/revolts + diplomacy/alliances/coalitions; civ Steps 5-6, save v21/v22)
|
||||
@ -716,8 +732,8 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
||||
```
|
||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
||||
`test_weather.cpp`, `test_volcano.cpp`, `test_geography.cpp`, `test_ecoregions.cpp`, `test_civ.cpp`,
|
||||
`test_nation.cpp`, `test_culture.cpp`, `test_conflict.cpp`, `test_diplomacy.cpp` or `test_trade.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
||||
Civilization / Nation / Culture / Conflict / Diplomacy / Trade suites — same source list. CMake also builds `test_events` for the
|
||||
`test_nation.cpp`, `test_culture.cpp`, `test_conflict.cpp`, `test_diplomacy.cpp`, `test_trade.cpp` or `test_colony.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
||||
Civilization / Nation / Culture / Conflict / Diplomacy / Trade / Colony suites — same source list. CMake also builds `test_events` for the
|
||||
viewer event journal.)
|
||||
|
||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
||||
@ -1019,6 +1035,14 @@ triangles (plates are fixed in phase 1).
|
||||
`rebuildTerritory` (no RNG, not saved). Wealth-ramp + route colours are render constants
|
||||
(Colors.cpp/Overlays.cpp). Raise `tradeProsperityWeight` for a more hub-dominated world, `tradeSeaRange`
|
||||
for more maritime trade.
|
||||
- **Colonization (`civColon*` / `civMaxColonies` in PlanetConfig / `planet.cfg`; derived, no save bump):**
|
||||
`civColonizeRate` (0.15, per-eligible-realm per-year chance to found a colony), `civColonyMinPop` (2e5,
|
||||
a realm must reach this to colonize — "kingdoms"), `civColonyMinHab` (0.30, min site habitability),
|
||||
`civColonyReach` (0.12 rad, overland reach) / `civColonySeaReach` (0.35, extra reach over water from a
|
||||
coastal member → islands), `civColonySpacing` (0.05 rad, min gap from existing settlements),
|
||||
`civMaxColonies` (120, cap beyond `civMaxSettlements`), `civColonySupply` (2.0, prosperity from the
|
||||
overlord supply link that keeps colonies alive). Runs in the yearly `stepColonization` tick; raise the
|
||||
rate/reach for aggressive colonial empires.
|
||||
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||
knob for how fast/high relief builds.
|
||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||
|
||||
@ -78,7 +78,7 @@ if(UNIX AND NOT APPLE)
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation culture conflict diplomacy trade)
|
||||
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation culture conflict diplomacy trade colony)
|
||||
add_executable(test_${test_name} test_${test_name}.cpp)
|
||||
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
||||
add_test(NAME ${test_name} COMMAND test_${test_name})
|
||||
|
||||
@ -529,6 +529,29 @@ colour mode (`wealthColor`, normalised to the current max) + `buildTradeRoutes`
|
||||
amber) under key `Z`, a gold wealth dot per settlement in the Civ tab, a cell-info prosperity/route line.
|
||||
Knobs `trade*`.
|
||||
|
||||
## Colonization (kingdoms found colonies + islands)
|
||||
|
||||
`Planet::stepColonization(long year)` (`PlanetCiv.cpp`) runs once per sim year (viewer year-tick, after
|
||||
`stepConflict`, before `rebuildTerritory`). The settlement set is no longer fixed: a **kingdom+** realm
|
||||
(`totalPop ≥ civColonyMinPop`) that passes a deterministic `civHashf(capital, year, seed) < civColonizeRate`
|
||||
gate **founds** a new settlement on the best **unclaimed** (`sCellNation[c] == -1`), habitable, well-spaced
|
||||
cell within `civColonyReach` overland — or `civColonySeaReach` from a **coastal** member, so islands / other
|
||||
continents are reachable across water. The colony is **appended** to `settlements`, bound to the founder by
|
||||
**allegiance** (Step 5 → joins the founder's realm across cultures) and by a **colonial supply link** added
|
||||
in `computeTrade` (Step 7: any settlement with a living-overlord allegiance gets a distance-independent
|
||||
supply `TradeLink` → prosperity → survives while the founder lives; Step-5 pruning clears the allegiance when
|
||||
the capital falls). Pure hashes → deterministic (**critical**: within one call, the per-settlement arrays
|
||||
`sSettleNation`/`sSettleAllegiance`/`sCivCond` are resized on each append, or the next realm's member loop
|
||||
reads out of bounds).
|
||||
|
||||
The growing set is cheap because the save already stores a variable-length settlement vector (v20) and the
|
||||
step-back snapshot stores per-settlement vectors whose **size is the count** — so **no `SAVE_VERSION` bump**,
|
||||
`captureWeather` unchanged, and `restoreWeather` **truncates** `settlements` (+ `sCivCond`/`sCivDrought` +
|
||||
rebuilds `sCellSettlement`) to `settlementPop.size()`, dropping colonies founded after the snapshot (they
|
||||
re-found identically on replay). Founding logs a **kind=3** event. `regenWorld()` (reseed) now also clears
|
||||
the viewer-side civ overlay segment lists + toggles (a bug fix — they used to linger on the new world).
|
||||
Knobs `civColon*` / `civMaxColonies`.
|
||||
|
||||
## Headless testing
|
||||
|
||||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
||||
@ -544,7 +567,7 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp src/sim/Planet
|
||||
src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp src/sim/PlanetTrade.cpp \
|
||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||
# test_biota / test_live / test_ocean / test_weather / test_volcano / test_geography / test_ecoregions /
|
||||
# test_civ / test_nation / test_culture / test_conflict / test_diplomacy / test_trade use the same source list.
|
||||
# test_civ / test_nation / test_culture / test_conflict / test_diplomacy / test_trade / test_colony use the same source list.
|
||||
```
|
||||
(add new `src/sim/*.cpp` to that list as stages are added). `Planet::step()` passes are
|
||||
data-parallel + double-buffered → bit-identical for any OpenMP thread count (determinism).
|
||||
|
||||
@ -287,6 +287,14 @@ void Viewer::regenWorld() { // after generate(): geometry change
|
||||
settled = false; settleRun = 0; formAccum = 0.0; stepCount = 0; paused = false;
|
||||
liveWorld = false; followId = 0; wxUndo.clear(); events.clear(); nextEventId = 1; // reseed/regen drops back to World Creation
|
||||
liveInfoTab = 0; eventRowRects.clear(); eventRowIndices.clear();
|
||||
// Civilization overlays are viewer-side segment lists; buildGeometry() cleared the engine data but not
|
||||
// these, so drop them (and their toggles) on a reseed or they keep drawing over the new world.
|
||||
nationBorders.clear(); cultureBorders.clear(); warFrontier.clear();
|
||||
allyLinks.clear(); rivalLinks.clear(); tradeSea.clear(); tradeLand.clear();
|
||||
showNationBorders = showCultureBorders = showTradeRoutes = false;
|
||||
lastTerritoryYear = -1;
|
||||
if (mode == ColorMode::Territory || mode == ColorMode::Culture || mode == ColorMode::Wealth)
|
||||
mode = ColorMode::Biome; // civ colour views have no data until settlements are placed
|
||||
planet.drifting = false; // Phase 1: original forming behavior
|
||||
phase3 = false; phase3Prompt = false; phase3PromptAt = planet.cfg.phase3AfterMy;
|
||||
rivers.clear(); bigRivers.clear();
|
||||
@ -776,6 +784,8 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
|
||||
ConflictUpdate wu = planet.stepConflict(y);
|
||||
for (const WarEvent& e : wu.events)
|
||||
appendEvent(e.kind, e.severity, liveTime, e.cell, 0, e.title, e.detail);
|
||||
for (const WarEvent& e : planet.stepColonization(y)) // kingdoms found new colonies (appends settlements)
|
||||
appendEvent(e.kind, e.severity, liveTime, e.cell, 0, e.title, e.detail);
|
||||
if (y < year) rebuildTerritory(); // recompute between years so the next year's war sees it
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,6 +192,10 @@ public:
|
||||
void computeHabitability();
|
||||
void placeSettlements();
|
||||
CivUpdate stepCivilization(double dtHours, double liveTime);
|
||||
// Colonization (once per sim year): a kingdom+ realm may found a new settlement on good unclaimed land
|
||||
// (or an island, from a coastal member) bound to it by allegiance. Appends to `settlements`; returns
|
||||
// the founding events. Deterministic (pure hashes) so step-back replays it.
|
||||
std::vector<WarEvent> stepColonization(long year);
|
||||
bool settlementsPlaced() const { return !settlements.empty(); }
|
||||
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)
|
||||
|
||||
@ -245,3 +245,83 @@ CivUpdate Planet::stepCivilization(double dtHours, double liveTime) {
|
||||
}
|
||||
return up;
|
||||
}
|
||||
|
||||
// Colonization: once per sim year, a kingdom+ realm may found a new settlement on good unclaimed land --
|
||||
// overland, or across water from a coastal member (islands / abroad). The colony is APPENDED and bound to
|
||||
// the founder's realm by allegiance (Step 5) + a colonial supply trade link (Step 7, in computeTrade), so
|
||||
// it survives while the founder lives. Deterministic (pure hashes) so a step-back truncates + replays it.
|
||||
std::vector<WarEvent> Planet::stepColonization(long year) {
|
||||
std::vector<WarEvent> ev;
|
||||
const int n = (int)cells.size();
|
||||
if (settlements.empty() || nations.empty()) return ev;
|
||||
if ((int)sCellNation.size() != n || sSettleNation.size() != settlements.size()) return ev;
|
||||
if ((int)sHabitability.size() != n) return ev;
|
||||
const int totalCap = std::max(0, cfg.civMaxSettlements) + std::max(0, cfg.civMaxColonies);
|
||||
if ((int)settlements.size() >= totalCap) return ev;
|
||||
const double sea = cfg.seaLevel, abP = cfg.civAbandonPop;
|
||||
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
||||
auto alive = [&](int s) { return s >= 0 && s < (int)settlements.size() && settlements[s].population >= abP; };
|
||||
auto angC = [&](int ca, int cb) { return std::acos(std::clamp(cells[ca].unit.dot(cells[cb].unit), -1.0, 1.0)); };
|
||||
auto coastal = [&](int c) { for (int j : cells[c].neighbors) if (cells[j].elevation <= sea) return true; return false; };
|
||||
const bool haveSettleCell = ((int)sCellSettlement.size() == n);
|
||||
|
||||
for (size_t ni = 0; ni < nations.size(); ++ni) {
|
||||
if ((int)settlements.size() >= totalCap) break;
|
||||
const Nation& nat = nations[ni];
|
||||
if (nat.totalPop < cfg.civColonyMinPop) continue; // only sizeable realms (kingdoms+)
|
||||
int cap = nat.capital; if (!alive(cap)) continue;
|
||||
// Deterministic per-realm yearly gate.
|
||||
if (civHashf((uint32_t)cap * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0xC0107Eu) >= cfg.civColonizeRate) continue;
|
||||
|
||||
// The realm's living members + which are coastal (only coastal members grant the extended sea reach).
|
||||
std::vector<int> members; std::vector<char> mCoast;
|
||||
for (size_t s = 0; s < settlements.size(); ++s)
|
||||
if (sSettleNation[s] == (int)ni && alive((int)s)) { members.push_back((int)s); mCoast.push_back(coastal(settlements[s].cell) ? 1 : 0); }
|
||||
if (members.empty()) continue;
|
||||
|
||||
// Best unclaimed, habitable, well-spaced site within reach of a member (sea reach from a coastal one).
|
||||
int best = -1; double bestHab = -1.0;
|
||||
for (int c = 0; c < n; ++c) {
|
||||
if (cells[c].elevation <= sea || cells[c].biome == Biome::Ice) continue;
|
||||
if (sCellNation[c] != -1) continue; // wilderness only
|
||||
if (haveSettleCell && sCellSettlement[c] != -1) continue;
|
||||
if (sHabitability[c] < cfg.civColonyMinHab || sHabitability[c] <= bestHab) continue;
|
||||
double dAny = 1e9, dCoast = 1e9;
|
||||
for (size_t m = 0; m < members.size(); ++m) {
|
||||
double d = angC(c, settlements[members[m]].cell);
|
||||
if (d < dAny) dAny = d;
|
||||
if (mCoast[m] && d < dCoast) dCoast = d;
|
||||
}
|
||||
if (dAny > cfg.civColonyReach && dCoast > cfg.civColonyReach + cfg.civColonySeaReach) continue;
|
||||
bool tooClose = false; // spacing from every settlement
|
||||
for (size_t s = 0; s < settlements.size(); ++s)
|
||||
if (alive((int)s) && angC(c, settlements[s].cell) < cfg.civColonySpacing) { tooClose = true; break; }
|
||||
if (tooClose) continue;
|
||||
bestHab = sHabitability[c]; best = c;
|
||||
}
|
||||
if (best < 0) continue;
|
||||
|
||||
int regId = ((int)sCellLand.size() == n) ? sCellLand[best] : -1;
|
||||
int bank = (regId >= 0) ? namegen::bankForRegion(cfg.seed, regId) : namegen::bankForRegion(cfg.seed, 2000 + best);
|
||||
uint32_t nameSeed = seed ^ (uint32_t)(best * 2654435761u) ^ (uint32_t)year * 19349663u;
|
||||
std::string nm = namegen::makeName(nameSeed, bank);
|
||||
auto used = [&](const std::string& s) { for (const Settlement& t : settlements) if (t.name == s) return true; return false; };
|
||||
for (int g = 0; used(nm) && g < 128; ++g) nm = namegen::makeName(nameSeed += 0x9E3779B9u, bank);
|
||||
|
||||
int newIdx = (int)settlements.size();
|
||||
Settlement st; st.cell = best; st.bank = bank; st.regionId = regId; st.population = cfg.civSeedPopulation;
|
||||
st.name = nm; st.id = (uint32_t)newIdx + 1;
|
||||
settlements.push_back(std::move(st));
|
||||
if (haveSettleCell) sCellSettlement[best] = newIdx;
|
||||
// Keep the per-settlement arrays in step with the grown set THIS call (they're reassigned next
|
||||
// computeTerritory); without this the next realm's member loop reads sSettleNation out of bounds.
|
||||
if (sSettleNation.size() != settlements.size()) sSettleNation.resize(settlements.size(), -1);
|
||||
sSettleNation[newIdx] = (int)ni; // the colony is (already) the founder's realm
|
||||
if (sSettleAllegiance.size() != settlements.size()) sSettleAllegiance.resize(settlements.size(), -1);
|
||||
sSettleAllegiance[newIdx] = cap; // the colony belongs to the founder's realm
|
||||
if (sCivCond.size() != settlements.size()) { sCivCond.resize(settlements.size(), 1.0); sCivDrought.resize(settlements.size(), 0.0); }
|
||||
ev.push_back(WarEvent{ 1, best, "The " + nat.name + " founds the colony of " + settlements[newIdx].name,
|
||||
"A new settlement rises on distant land.", 3 });
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
|
||||
@ -62,12 +62,14 @@
|
||||
D(diploAllyThreshold) D(diploNonAggThreshold) D(diploRivalThreshold) \
|
||||
D(tradeLandRange) D(tradeSeaRange) D(tradeRiverBonus) D(tradeMinVolume) D(tradeProsperityWeight) \
|
||||
D(tradeWarBlock) D(tradeAllyBonus) D(tradeDiploBonus) D(tradeTemptWar) \
|
||||
D(civColonizeRate) D(civColonyMinPop) D(civColonyMinHab) D(civColonyReach) D(civColonySeaReach) \
|
||||
D(civColonySpacing) D(civColonySupply) \
|
||||
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
||||
I(volcanoMaxCount) \
|
||||
I(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
|
||||
I(geoOceanDeep) I(civMaxSettlements) I(civEmpireMinMembers) I(warMaxConcurrent) \
|
||||
I(geoOceanDeep) I(civMaxSettlements) I(civEmpireMinMembers) I(warMaxConcurrent) I(civMaxColonies) \
|
||||
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
||||
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
|
||||
U(seed)
|
||||
@ -320,6 +322,14 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
||||
E(rng(cfg.tradeAllyBonus, 0.0, 100.0, "tradeAllyBonus"));
|
||||
E(rng(cfg.tradeDiploBonus, 0.0, 10.0, "tradeDiploBonus"));
|
||||
E(rng(cfg.tradeTemptWar, 0.0, 100.0, "tradeTemptWar"));
|
||||
E(rng(cfg.civColonizeRate, 0.0, 1.0, "civColonizeRate"));
|
||||
E(rng(cfg.civColonyMinPop, 0.0, 1.0e12, "civColonyMinPop"));
|
||||
E(rng(cfg.civColonyMinHab, 0.0, 1.0, "civColonyMinHab"));
|
||||
E(rng(cfg.civColonyReach, 0.0, 3.14159, "civColonyReach"));
|
||||
E(rng(cfg.civColonySeaReach, 0.0, 3.14159, "civColonySeaReach"));
|
||||
E(rng(cfg.civColonySpacing, 0.0, 3.14159, "civColonySpacing"));
|
||||
E(rng(cfg.civColonySupply, 0.0, 100.0, "civColonySupply"));
|
||||
E(irng(cfg.civMaxColonies, 0, 1000000, "civMaxColonies"));
|
||||
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||
|
||||
@ -60,6 +60,21 @@ void Planet::computeTrade() {
|
||||
sProsperity[a] += vol; sProsperity[b] += vol;
|
||||
}
|
||||
}
|
||||
// Colonial / imperial supply lines: a settlement bound by allegiance to a living overlord capital gets a
|
||||
// guaranteed supply link (independent of distance -- across the sea), so colonies + conquered provinces
|
||||
// stay viable while their overlord lives (Step 5 clears the allegiance when the capital falls -> supply
|
||||
// ends and the colony subsists on local trade only).
|
||||
if (sSettleAllegiance.size() == settlements.size() && cfg.civColonySupply > 0.0)
|
||||
for (size_t s = 0; s < settlements.size(); ++s) {
|
||||
if (!alive((int)s)) continue;
|
||||
int ov = sSettleAllegiance[s];
|
||||
if (ov < 0 || ov == (int)s || !alive(ov)) continue;
|
||||
TradeKind kind = (coastal[s] || coastal[ov]) ? TradeKind::Sea : TradeKind::Land;
|
||||
sTradeLinks.push_back(TradeLink{ (int)s, ov, (float)cfg.civColonySupply, kind });
|
||||
sProsperity[s] += cfg.civColonySupply;
|
||||
sProsperity[ov] += cfg.civColonySupply * 0.25; // the colony enriches its overlord a little
|
||||
}
|
||||
|
||||
// A small base (local commerce) for any living settlement, then clamp.
|
||||
for (size_t k = 0; k < settlements.size(); ++k)
|
||||
sProsperity[k] = alive((int)k) ? std::min(3.0, sProsperity[k] + 0.1) : 0.0;
|
||||
|
||||
@ -489,4 +489,15 @@ struct PlanetConfig {
|
||||
double tradeAllyBonus = 1.5; // trade-volume multiplier between allied realms
|
||||
double tradeDiploBonus = 0.1; // attitude nudge/year between trade-partner realms (reward alliance)
|
||||
double tradeTemptWar = 0.3; // war-hostility weight of a wealthy target (rich neighbours tempt war)
|
||||
// Civilization colonization (kingdoms found new settlements over time, incl. islands). New settlements
|
||||
// are appended, bound to the founder's realm by allegiance + a colonial supply trade link. Derived +
|
||||
// deterministic; the settlement set grows (saved as-is, step-back truncates), no save-format change.
|
||||
double civColonizeRate = 0.15; // per-eligible-realm per-year chance to found a colony
|
||||
double civColonyMinPop = 2.0e5; // a realm must reach this total population to colonize (kingdoms+)
|
||||
double civColonyMinHab = 0.30; // minimum habitability of a colony site
|
||||
double civColonyReach = 0.12; // rad: overland colonization reach from a realm settlement
|
||||
double civColonySeaReach = 0.35; // rad: reach over water from a COASTAL member (islands / abroad)
|
||||
double civColonySpacing = 0.05; // rad: a colony must be at least this far from every settlement
|
||||
int civMaxColonies = 120; // cap on colonies founded beyond the initial civMaxSettlements
|
||||
double civColonySupply = 2.0; // prosperity from the overlord supply link (keeps colonies alive)
|
||||
};
|
||||
|
||||
@ -43,9 +43,18 @@ void Planet::restoreWeather(const WeatherSnapshot& s) {
|
||||
sHumidity = s.humidity; sCloud = s.cloud; sRain = s.rain;
|
||||
sStorms = s.storms; sWeatherRng = s.rng; sStormNextId = s.nextId;
|
||||
volcanoes = s.volcanoes; sVolRng = s.volRng;
|
||||
if (s.settlementPop.size() == settlements.size()) // restore populations (set is fixed)
|
||||
for (size_t k = 0; k < settlements.size(); ++k) settlements[k].population = s.settlementPop[k];
|
||||
sSettleAllegiance = s.settlementAllegiance; // civ Step 5: restore conquest state
|
||||
// Colonies are appended settlements, so the snapshot count may be < the current count -- TRUNCATE back,
|
||||
// dropping any colony founded after this frame (it re-founds identically on the deterministic replay).
|
||||
size_t cnt = s.settlementPop.size();
|
||||
if (cnt <= settlements.size()) {
|
||||
settlements.resize(cnt);
|
||||
for (size_t k = 0; k < cnt; ++k) settlements[k].population = s.settlementPop[k];
|
||||
sCivCond.assign(cnt, 1.0); sCivDrought.assign(cnt, 0.0);
|
||||
sCellSettlement.assign(cells.size(), -1);
|
||||
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;
|
||||
}
|
||||
sSettleAllegiance = s.settlementAllegiance; // civ Step 5: restore conquest/colony allegiance
|
||||
if (sSettleAllegiance.size() != settlements.size()) sSettleAllegiance.assign(settlements.size(), -1);
|
||||
wars = s.wars; sWarRng = s.warRng ? s.warRng : sWarRng; sWarNextId = s.warNextId ? s.warNextId : sWarNextId;
|
||||
diplomacy = s.diplomacy; // civ Step 6: restore realm relations
|
||||
|
||||
157
test_colony.cpp
Normal file
157
test_colony.cpp
Normal file
@ -0,0 +1,157 @@
|
||||
// Headless test for civilization colonization (kingdoms found new settlements + islands). No display.
|
||||
//
|
||||
// g++ -std=c++17 -O2 -Isrc/sim test_colony.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \
|
||||
// ... (the same source list as test_civ) ... src/sim/PlanetTrade.cpp src/sim/PlanetIO.cpp \
|
||||
// -o /tmp/tcol && /tmp/tcol
|
||||
//
|
||||
// Verifies: colonies are founded (the settlement count grows); a colony belongs to its founder's realm
|
||||
// (via allegiance); a colony has a supply link to its overlord + is more prosperous for it; killing the
|
||||
// founder capital frees + de-supplies the colony; step-back truncates the set; determinism; save round-trip.
|
||||
|
||||
#include "Planet.hpp"
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
static int failures = 0;
|
||||
static void check(bool cond, const char* what) {
|
||||
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||
if (!cond) ++failures;
|
||||
}
|
||||
static void settle(Planet& p, int maxSteps = 800) {
|
||||
int run = 0;
|
||||
for (int s = 0; s < maxSteps; ++s) { double mc = p.step(); if (mc < 2.0) { if (++run >= 3) break; } else run = 0; }
|
||||
p.computeClimate(); p.classifyBiomes();
|
||||
}
|
||||
static void drift(Planet& p, int iters) {
|
||||
p.drifting = true;
|
||||
for (int k = 0; k < iters; ++k) { double dt = p.cflDtMy(); p.advect(dt); p.step(); p.erode(dt); if (k >= iters/2) p.hydrology(dt*0.2); }
|
||||
p.computeClimate(); p.classifyBiomes();
|
||||
}
|
||||
// Crank colonization so it happens readily; keep the initial set + years so realms grow into kingdoms.
|
||||
static void eager(Planet& p) {
|
||||
p.cfg.civColonizeRate = 0.9; p.cfg.civColonyMinPop = 1000.0; p.cfg.civColonyMinHab = 0.15;
|
||||
p.cfg.civColonyReach = 0.25; p.cfg.civColonySeaReach = 0.5; p.cfg.civColonySpacing = 0.03; p.cfg.civMaxColonies = 200;
|
||||
}
|
||||
static void seedCiv(Planet& p, double yearH) {
|
||||
p.placeSettlements();
|
||||
double lt = 0.0; for (int yr = 0; yr < 400; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
|
||||
eager(p);
|
||||
}
|
||||
// Run C years of colonization + recompute; return the settlement count reached.
|
||||
static int runColonize(Planet& p, long years) {
|
||||
for (long yr = 0; yr < years; ++yr) {
|
||||
p.computeTerritory(); p.computeCultures(); p.computeTrade();
|
||||
p.stepColonization(yr);
|
||||
}
|
||||
p.computeTerritory(); p.computeCultures(); p.computeTrade();
|
||||
return (int)p.settlements.size();
|
||||
}
|
||||
|
||||
int main() {
|
||||
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 4242;
|
||||
Planet p; p.generate(cfg); settle(p); drift(p, 400);
|
||||
const int n = (int)p.cells.size();
|
||||
const double yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
||||
seedCiv(p, yearH);
|
||||
int initial = (int)p.settlements.size();
|
||||
|
||||
std::printf("Colony: kingdoms found colonies (the settlement set grows)\n");
|
||||
int grown = runColonize(p, 200);
|
||||
std::printf(" settlements: %d initial -> %d after colonization\n", initial, grown);
|
||||
check(grown > initial, "new colonies are founded over time");
|
||||
|
||||
std::printf("Colony: a colony belongs to its founder's realm + has a supply link\n");
|
||||
{
|
||||
// A colony = a settlement (index >= initial) whose allegiance points to a living overlord capital.
|
||||
int colony = -1, overlord = -1;
|
||||
for (int s = initial; s < (int)p.settlements.size(); ++s) {
|
||||
int ov = p.settleAllegiance()[s];
|
||||
if (ov >= 0 && ov < (int)p.settlements.size() && p.settlements[ov].population >= p.cfg.civAbandonPop) { colony = s; overlord = ov; break; }
|
||||
}
|
||||
check(colony >= 0, "a colony bound to a living overlord exists");
|
||||
if (colony >= 0) {
|
||||
int cnat = p.cellNation()[p.settlements[colony].cell];
|
||||
int onat = p.settleNation()[overlord];
|
||||
check(cnat == onat && cnat >= 0, "the colony's cell belongs to the founder's realm");
|
||||
bool supplied = false;
|
||||
for (const TradeLink& t : p.tradeLinks())
|
||||
if ((t.a == colony && t.b == overlord) || (t.a == overlord && t.b == colony)) supplied = true;
|
||||
check(supplied, "the colony has a supply trade link to its overlord");
|
||||
check(p.prosperity()[colony] > 0.5, "the colony is prosperous (kept alive by supply)");
|
||||
}
|
||||
}
|
||||
|
||||
std::printf("Colony: island / cross-water colonies appear\n");
|
||||
{
|
||||
// A colony whose nearest OWN-REALM settlement is separated by ocean (a great-circle sample hits sea).
|
||||
auto crossesSea = [&](int ca, int cb) {
|
||||
for (int i = 1; i < 12; ++i) {
|
||||
double u = i / 12.0;
|
||||
Vec3 m = (p.cells[ca].unit * (1.0 - u) + p.cells[cb].unit * u).normalized();
|
||||
double best = -2; int bc = 0;
|
||||
for (int c = 0; c < n; ++c) { double d = p.cells[c].unit.dot(m); if (d > best) { best = d; bc = c; } }
|
||||
if (p.cells[bc].elevation <= p.cfg.seaLevel) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
int island = 0;
|
||||
for (int s = initial; s < (int)p.settlements.size(); ++s) {
|
||||
int ov = p.settleAllegiance()[s]; if (ov < 0) continue;
|
||||
if (crossesSea(p.settlements[s].cell, p.settlements[ov].cell)) { ++island; }
|
||||
}
|
||||
std::printf(" %d colonies are across water from their overlord\n", island);
|
||||
check(island > 0, "at least one colony was founded across the sea (island / abroad)");
|
||||
}
|
||||
|
||||
std::printf("Colony: losing the founder frees + de-supplies the colony\n");
|
||||
{
|
||||
int colony = -1, overlord = -1;
|
||||
for (int s = initial; s < (int)p.settlements.size(); ++s) {
|
||||
int ov = p.settleAllegiance()[s];
|
||||
if (ov >= 0 && p.settlements[ov].population >= p.cfg.civAbandonPop) { colony = s; overlord = ov; break; }
|
||||
}
|
||||
if (colony < 0) { check(true, "(no living-overlord colony -- skipped)"); }
|
||||
else {
|
||||
double prospBefore = p.prosperity()[colony];
|
||||
p.settlements[overlord].population = 0.0; // the founder capital falls
|
||||
p.stepConflict(9999); // Step-5 pruning clears the dead overlord's allegiance
|
||||
p.computeTerritory(); p.computeCultures(); p.computeTrade();
|
||||
bool freed = p.settleAllegiance()[colony] < 0;
|
||||
std::printf(" colony prosperity %.2f -> %.2f (freed: %s)\n", prospBefore, p.prosperity()[colony], freed ? "yes" : "no");
|
||||
check(freed, "the colony is freed when its founder collapses");
|
||||
check(p.prosperity()[colony] < prospBefore, "the colony loses its supply prosperity");
|
||||
}
|
||||
}
|
||||
|
||||
std::printf("Colony: step-back truncates the settlement set\n");
|
||||
{
|
||||
Planet q; q.generate(cfg); settle(q); drift(q, 400); seedCiv(q, yearH);
|
||||
int base = (int)q.settlements.size();
|
||||
q.computeTerritory(); q.computeCultures(); q.computeTrade();
|
||||
WeatherSnapshot snap = q.captureWeather(); // capture BEFORE colonizing
|
||||
int after = runColonize(q, 120);
|
||||
check(after > base, "colonies were founded after the snapshot");
|
||||
q.restoreWeather(snap);
|
||||
check((int)q.settlements.size() == base, "restoreWeather truncates back to the pre-colony count");
|
||||
}
|
||||
|
||||
std::printf("Colony: determinism + save round-trip\n");
|
||||
{
|
||||
Planet a; a.generate(cfg); settle(a); drift(a, 400); seedCiv(a, yearH); int na = runColonize(a, 150);
|
||||
Planet b; b.generate(cfg); settle(b); drift(b, 400); seedCiv(b, yearH); int nb = runColonize(b, 150);
|
||||
bool same = (na == nb);
|
||||
if (same) for (int s = 0; s < na; ++s) if (a.settlements[s].cell != b.settlements[s].cell) same = false;
|
||||
check(same, "colonization is deterministic (same colonies)");
|
||||
|
||||
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||
a.writeState(ss);
|
||||
Planet r;
|
||||
bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true, true, true);
|
||||
check(ok && (int)r.settlements.size() == na, "save/load round-trips the grown settlement set");
|
||||
}
|
||||
|
||||
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL COLONY CHECKS PASSED\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user