diff --git a/BUILD.md b/BUILD.md index db8e99c..7455d0e 100644 --- a/BUILD.md +++ b/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 diff --git a/CLAUDE.md b/CLAUDE.md index ff7ee12..e0b3d86 100644 --- a/CLAUDE.md +++ b/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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 88685f5..f0e7eaa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/docs/design-notes.md b/docs/design-notes.md index b141d71..e607741 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -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). diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index a243183..dbc1b74 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -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 } } diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index fd997c8..e120b62 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -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 stepColonization(long year); bool settlementsPlaced() const { return !settlements.empty(); } const std::vector& cellSettlement() const { return sCellSettlement; } // settlement index per cell (-1) const std::vector& habitability() const { return sHabitability; } // 0..1 per cell (derived) diff --git a/src/sim/PlanetCiv.cpp b/src/sim/PlanetCiv.cpp index 59ff106..1e5129f 100644 --- a/src/sim/PlanetCiv.cpp +++ b/src/sim/PlanetCiv.cpp @@ -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 Planet::stepColonization(long year) { + std::vector 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 members; std::vector 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; +} diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index a7b546c..eb4df0f 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -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")); diff --git a/src/sim/PlanetTrade.cpp b/src/sim/PlanetTrade.cpp index d68d4d0..81c8582 100644 --- a/src/sim/PlanetTrade.cpp +++ b/src/sim/PlanetTrade.cpp @@ -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; diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index 331cf59..c7bc267 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -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) }; diff --git a/src/sim/PlanetWeather.cpp b/src/sim/PlanetWeather.cpp index 7ac1aeb..c7be9c1 100644 --- a/src/sim/PlanetWeather.cpp +++ b/src/sim/PlanetWeather.cpp @@ -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 diff --git a/test_colony.cpp b/test_colony.cpp new file mode 100644 index 0000000..a56327b --- /dev/null +++ b/test_colony.cpp @@ -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 +#include +#include +#include + +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; +}