From e27b91a12840fc5cf3e9650f1841ccd5c48d3b03 Mon Sep 17 00:00:00 2001 From: Jonas Reith Date: Wed, 1 Jul 2026 13:36:41 +0200 Subject: [PATCH] Civ Step 7: trade & economy Settlements no longer prosper on local food alone. A derived economic layer: trade routes link nearby settlements, prosperity accrues at hubs, and prosperity boosts population growth. Like territory/culture it's a pure function of the (saved) settlements + geography + wars/diplomacy -- recomputed each sim year, NO new saved state and NO save-version bump. - PlanetTrade.{hpp,cpp}: computeTrade() builds TradeLinks (sea when both coastal -> longer reach / river / overland) with volume from populations x proximity x a political factor (blockaded by war, boosted by an alliance); prosperity = base + sum of link volumes (hubs get rich); a per-cell wealth field via a new sCellSettleOwner filled by computeTerritory. - Prosperity feeds carrying capacity in stepCivilization (K *= 1 + tradeProsperityWeight*prosperity), so connected coastal/river hubs grow bigger. - Light feedback in stepConflict (reads last year's economy): trade partners get a diplomacy attitude nudge (reward alliance); a wealthy weak neighbour raises war hostility (tempt war). - Render: a Wealth heat map (wealthColor) + trade-route overlay (sea cyan / land+river amber) under key Z, a gold wealth dot per settlement in the Civ tab, a cell-info prosperity/route line. trade* config knobs (no save block). - test_trade.cpp: links form (sea over longer range), hubs richer than isolated, war blocks trade, prosperity raises carrying capacity, determinism, RNG isolation, save->load->recompute parity. All 15 suites green. Co-Authored-By: Claude Opus 4.8 --- BUILD.md | 19 ++++- CLAUDE.md | 42 ++++++++-- CMakeLists.txt | 3 +- docs/design-notes.md | 27 ++++++- src/render/Colors.cpp | 13 ++++ src/render/Colors.hpp | 4 +- src/render/Overlays.cpp | 20 +++++ src/render/Overlays.hpp | 2 + src/render/Panels.cpp | 8 ++ src/render/Viewer.cpp | 14 +++- src/render/Viewer.hpp | 2 + src/render/ViewerInput.cpp | 10 +++ src/render/ViewerRender.cpp | 23 +++++- src/sim/Planet.cpp | 2 + src/sim/Planet.hpp | 17 ++++ src/sim/PlanetCiv.cpp | 3 +- src/sim/PlanetConflict.cpp | 21 ++++- src/sim/PlanetIO.cpp | 11 +++ src/sim/PlanetNation.cpp | 3 +- src/sim/PlanetTrade.cpp | 73 +++++++++++++++++ src/sim/PlanetTrade.hpp | 17 ++++ src/sim/PlanetTypes.hpp | 11 +++ test_trade.cpp | 151 ++++++++++++++++++++++++++++++++++++ 23 files changed, 477 insertions(+), 19 deletions(-) create mode 100644 src/sim/PlanetTrade.cpp create mode 100644 src/sim/PlanetTrade.hpp create mode 100644 test_trade.cpp diff --git a/BUILD.md b/BUILD.md index ff7a8e5..db8e99c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -59,6 +59,7 @@ the full ~2.8x speedup; the default uses all cores for no extra gain: U settlements: the dawn of civilization on first press, then toggle markers (Civ tab) P territory / realms view + borders + war fronts + alliance/rivalry arcs (Realms tab) X culture / faiths view + cultural borders (peoples/ethos/religion in the Cultures tab) + Z wealth / trade view + trade routes (sea/river/overland; prosperity feeds city growth) Y follow-cam: cycle the 3D camera through active storms (Live World; off after last) . / , step the live clock forward / back by one rate-unit (auto-pauses; back also rewinds weather + storms via an undo history) @@ -355,6 +356,20 @@ yearly stepConflict tick; green arcs = alliances, dark-red arcs = rivalries. diploNonAggThreshold 0.2 attitude at/above which they sign a non-aggression pact diploRivalThreshold -0.5 attitude at/below which they become rivals +Trade & economy (PlanetConfig, key Z — derived, not saved): trade routes link nearby settlements (sea / +river / overland); prosperity accrues at hubs and boosts population growth; trade feeds diplomacy + war +motivation. Recomputed each sim year; sea cyan / land+river amber routes + a gold wealth heat map. + + tradeLandRange 0.10 rad overland trade reach + tradeSeaRange 0.30 rad extra reach when both settlements are coastal (sea route) + tradeRiverBonus 0.06 rad reach bonus when either settlement sits on a river + tradeMinVolume 0.05 links below this volume are dropped + tradeProsperityWeight 0.8 how much a hub's prosperity multiplies its carrying capacity + tradeWarBlock 0.0 trade-volume factor between realms at war (0 = blockade) + tradeAllyBonus 1.5 trade-volume factor between allied realms + 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) + 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 @@ -371,11 +386,11 @@ src/sim/PlanetCulture.cpp (computeCultures). src/sim/PlanetBiota.cpp \ src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \ src/sim/NameGen.cpp src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp \ - src/sim/PlanetCiv.cpp src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp \ + src/sim/PlanetCiv.cpp src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp src/sim/PlanetTrade.cpp \ 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_weather / test_volcano / test_geography / test_ecoregions / test_civ / test_nation / test_culture / test_conflict / test_diplomacy / test_trade. # 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 afde762..ff7ee12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,7 +108,7 @@ the fixed-grid Eulerian model + the climate fields are the groundwork for it. **Civilizations (in progress — the long arc after the world is finished):** the eventual goal is people who eat, name their world, found villages→cities, build kingdoms/empires, draw cultural + geographic borders, and go to war. Built in phases (cell = territory, settlements = point agents, all -on the Live World clock). **Steps 1–6 of the roadmap are done (plus a derived ecoregions atlas):** +on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived ecoregions atlas):** - **Geography & place-names (the atlas)** *(done — see `PlanetGeography.cpp` + `NameGen.cpp`)* — the foundation everything civic references. `Planet::generateGeography()` extracts named features from the (frozen) terrain by connectivity over the fixed grid — **continents/islands** (connected land), @@ -217,8 +217,24 @@ on the Live World clock). **Steps 1–6 of the roadmap are done (plus a derived active-wars list in the **Realms** tab, a cell-info "allies/rivals" line, and **kind=6** `WorldEvent`s ("The X and the Y form an alliance", "…become rivals", "…sign a non-aggression pact", "Z joins the war against W"). Knobs `diplo*`. +- **Trade & economy (Step 7)** *(done — see `PlanetTrade.cpp`)* — settlements no longer prosper on local + food alone. `computeTrade()` (deterministic, no RNG, runs in `rebuildTerritory` **after** + `computeTerritory`/`computeCultures`) builds **trade routes** between nearby settlements — a **sea** route + when both are coastal (a much longer reach), a **river** route on rivers, else **overland** — with a + volume from both populations × proximity × a per-kind bonus × a **political factor** (blockaded to ~0 + between realms **at war**, boosted `tradeAllyBonus` between **allies**). Each settlement's **prosperity** + = a small base + Σ its link volumes, so a settlement central to many strong routes becomes a rich **hub**. + Prosperity **multiplies the carrying capacity** in `stepCivilization` (`K ·= 1 + tradeProsperityWeight· + prosperity`), so well-connected coastal/river hubs grow into far bigger cities while isolated/blockaded + towns lag. Two light feedbacks close the loop (read last year's economy in `stepConflict`): **trade + partners** get a diplomacy attitude nudge (`tradeDiploBonus` — *reward alliance*) and a **wealthy weak + neighbour** raises war hostility (`tradeTemptWar` — *rich neighbours tempt war*). Like territory/culture, + trade is a **pure function of the (saved) settlements + geography + wars/diplomacy** — **recomputed each + 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*`. *Next steps (not yet built): cultural **evolution** (spread along borders, drift, assimilation, schism); - tribute / vassalage treaties; trade & economy.* + tribute / vassalage treaties; an accumulated treasury (funding armies / buying peace).* ## Current state @@ -631,6 +647,7 @@ src/ 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) + PlanetTrade.* computeTrade (trade routes + prosperity/wealth feeding growth; civ Step 7, derived) PlanetIO.cpp config file (text) + binary save/load render/ (raylib viewer) Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota) @@ -694,13 +711,13 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \ src/sim/PlanetBiota.cpp \ src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \ src/sim/NameGen.cpp src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp \ - src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp \ + src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp src/sim/PlanetTrade.cpp \ src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t ``` (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` or `test_diplomacy.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions / -Civilization / Nation / Culture / Conflict / Diplomacy suites — same source list. CMake also builds `test_events` for the +`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 viewer event journal.) Use this to verify tectonics after changing `Planet::step()` without launching @@ -738,6 +755,7 @@ all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured co `U` settlements (the dawn of civilization on first press; toggles markers after) · `P` territory / realms view + political borders (Realms tab lists nations) · `X` culture / faiths view + cultural borders (Cultures tab lists peoples, ethos & religion) · +`Z` wealth / trade view + trade routes (sea/river/overland; prosperity feeds city growth) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec → up to ~20 years/sec) · `S` single tick (in **Live World** steps the clock forward) · `.`/`,` step the live clock @@ -769,7 +787,9 @@ Territory view on (`P`), realms **wage war** each year — red war-fronts appear (borders move) or fall to ruins, empires fracture as provinces revolt; realms also form **alliances** (green arcs) and **rivalries** (dark-red arcs), allies **join each other's wars** (coalitions) and never fight each other, and wars end in **peace treaties**; the **Realms** tab lists each realm's allies/ -rivals/wars and the **Events** tab logs it all. `Y` makes the 3D camera **follow a storm** (cycles by strength, +rivals/wars and the **Events** tab logs it all. `Z` shows the **trade economy** — routes (cyan sea / +amber land) + a **wealth heat map**; well-connected coastal/river **hubs** grow richer & bigger, a war +cuts the routes between belligerents, and the **Civ** tab shows each settlement's wealth. `Y` makes the 3D camera **follow a storm** (cycles by strength, off after the last); `.`/`,` step the clock forward/back by one rate-unit (back rewinds the sky **and** weather/storms/volcano lifecycle **and wars/conquests** via snapshots). Mouse-wheel over the 2D map zooms (drag pans). @@ -989,6 +1009,16 @@ triangles (plates are fixed in phase 1). `diploRivalThreshold` (−0.5). Ally/rival arc colours are render constants (ViewerRender/Overlays). Runs in the same yearly `stepConflict` tick; raise `diploDriftRate`/`diploAffinity` for a more alliance-heavy world, lower the thresholds' spread for touchier politics. +- **Trade & economy (`trade*` in PlanetConfig / `planet.cfg`; civ Step 7, key `Z`, derived → no save + bump):** route reach — `tradeLandRange` (0.10 rad overland), `tradeSeaRange` (0.30, extra when both + coastal), `tradeRiverBonus` (0.06, on rivers); `tradeMinVolume` (0.05, link cutoff); economy — + `tradeProsperityWeight` (0.8, how much a hub's prosperity multiplies its carrying capacity → bigger + cities), `tradeWarBlock` (0.0, trade-volume factor between realms at war), `tradeAllyBonus` (1.5, between + allies); feedback — `tradeDiploBonus` (0.1, attitude/yr between trade partners), `tradeTemptWar` (0.3, + war-hostility from a wealthy weak target). Prosperity/routes are recomputed each sim year in + `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. - `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 225d854..88685f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ set(SIM_SOURCES src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp + src/sim/PlanetTrade.cpp src/sim/PlanetIO.cpp ) @@ -77,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) +foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation culture conflict diplomacy trade) 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 6009a53..b141d71 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -57,6 +57,7 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`). government per realm; derived, not saved). - `PlanetConflict.{hpp,cpp}` — `stepConflict` (civ Steps 5-6: wars/conquest/revolts + diplomacy/ alliances/coalitions; stateful, saved v21/v22 + snapshotted). +- `PlanetTrade.{hpp,cpp}` — `computeTrade` (civ Step 7: trade routes + prosperity/wealth; derived, not saved). - `PlanetIO.cpp` — text config + binary save/load. The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration), @@ -506,6 +507,28 @@ Territory view (`P`); the Realms tab shows per-realm ally/rival counts + the act an allies/rivals line; kind-6 events (`=` icon). `captureWeather`/`restoreWeather` carry `diplomacy`. Knobs `diplo*`. +## Civilization Step 7 — trade & economy + +`PlanetTrade.cpp` (engine, raylib-free, **no RNG**). `computeTrade()` is a **pure derived function** of the +settlement set + geography + wars/diplomacy — recomputed each sim year in `rebuildTerritory` (after +`computeTerritory`/`computeCultures`), **not saved** (no `SAVE_VERSION` bump), rewinds for free. Per living +settlement it flags **coastal** (a neighbour ≤ seaLevel) and **river** (`sDischarge` > `riverThreshold`); +then for each settlement pair within an effective reach (`tradeLandRange` + `tradeRiverBonus` if either is +on a river + `tradeSeaRange` if both are coastal) it adds a **`TradeLink`** whose **kind** is Sea/River/ +Land and whose **volume** = f(both populations, proximity, a per-kind bonus) × a political factor +(`tradeWarBlock` if the realms are at war, `tradeAllyBonus` if allied — via `realmsAtWar`/`realmsAllied`). +**Prosperity** `sProsperity[s]` = a small base + Σ its link volumes (a route hub → rich), clamped; the +**wealth field** `sCellWealth[c]` = the prosperity of the cell's owning settlement (`sCellSettleOwner`, now +filled by `computeTerritory`), so the heat map matches territory. + +Prosperity feeds `stepCivilization` (`K ·= 1 + tradeProsperityWeight·prosperity`, PlanetCiv.cpp) so hubs +grow into bigger cities; it's refreshed yearly and read each frame. Two light feedbacks in `stepConflict` +read **last year's** economy (one-year lag, deterministic): trade-partner realm pairs get a `tradeDiploBonus` +attitude nudge, and a wealthy weak neighbour adds a `tradeTemptWar` war-hostility term. Render: a `Wealth` +colour mode (`wealthColor`, normalised to the current max) + `buildTradeRoutes` arcs (sea cyan / land+river +amber) under key `Z`, a gold wealth dot per settlement in the Civ tab, a cell-info prosperity/route line. +Knobs `trade*`. + ## Headless testing Engine is raylib-free, so logic is tested without a display. Build/run: @@ -518,10 +541,10 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp src/sim/Planet src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \ src/sim/PlanetFungiGen.cpp src/sim/NameGen.cpp src/sim/PlanetGeography.cpp \ src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp src/sim/PlanetNation.cpp \ - src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp \ + 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 use the same source list. +# test_civ / test_nation / test_culture / test_conflict / test_diplomacy / test_trade 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/Colors.cpp b/src/render/Colors.cpp index aac75e0..379ec5d 100644 --- a/src/render/Colors.cpp +++ b/src/render/Colors.cpp @@ -95,6 +95,7 @@ const char* colorModeName(ColorMode m) { case ColorMode::Habitability: return "Habitability"; case ColorMode::Territory: return "Territory / realms"; case ColorMode::Culture: return "Culture / faiths"; + case ColorMode::Wealth: return "Wealth / trade"; case ColorMode::TempSummer: return "Temperature (summer)"; case ColorMode::TempWinter: return "Temperature (winter)"; case ColorMode::Seasonality: return "Seasonality (summer-winter)"; @@ -168,6 +169,18 @@ Color habitabilityColor(double h01) { // barren grey -> green -> fertile gold return Color{ L(0), L(1), L(2), 255 }; } +Color wealthColor(double w01) { // dim slate -> teal -> bright gold (trade hubs) + double t = std::clamp(w01, 0.0, 1.0); + static const unsigned char key[3][3] = { + { 44, 48, 56 }, // 0.0 poor slate + { 60, 150, 140 }, // 0.5 teal + { 245, 210, 70 }, // 1.0 golden hub + }; + double s = t * 2.0; int k = std::min(1, (int)s); double f = s - k; + auto L = [&](int c) { return (unsigned char)(key[k][c] + (key[k + 1][c] - key[k][c]) * f); }; + return Color{ L(0), L(1), L(2), 255 }; +} + Color ecoregionColor(int id, Biome b, double productivity) { if (id < 0) return Color{ 48, 52, 58, 255 }; Color base = ColorFromHSV(std::fmod((id + 11) * 0.61803398875f, 1.0f) * 360.0f, 0.55f, 0.82f); diff --git a/src/render/Colors.hpp b/src/render/Colors.hpp index a047888..05cb58c 100644 --- a/src/render/Colors.hpp +++ b/src/render/Colors.hpp @@ -6,7 +6,7 @@ enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip, FloraDensity, FaunaDensity, FungaDensity, - Ecoregion, Habitability, Territory, Culture, + Ecoregion, Habitability, Territory, Culture, Wealth, TempSummer, TempWinter, Seasonality }; // 6 cycles these temp sub-views Color elevationColor(double e, double seaLevel); @@ -42,6 +42,8 @@ Color marineFaunaColor(double d01); Color ecoregionColor(int id, Biome b, double productivity); // Habitability heat map (0..1): barren grey -> fertile green/gold (where civilization can thrive). Color habitabilityColor(double h01); +// Wealth / trade-prosperity heat map (0..1): dim slate -> teal -> bright gold (rich trade hubs). +Color wealthColor(double w01); // Per-nation territory tint (golden-ratio HSV, offset from plateColor so realms read distinctly). Color nationColor(int id); // Per-culture tint (golden-ratio HSV, a different phase than nationColor so cultural blocs read distinctly). diff --git a/src/render/Overlays.cpp b/src/render/Overlays.cpp index 621d752..0b4413f 100644 --- a/src/render/Overlays.cpp +++ b/src/render/Overlays.cpp @@ -102,6 +102,26 @@ void buildDiploLinks(const Planet& p, float radius, std::vector& ally, } } +void buildTradeRoutes(const Planet& p, float radius, std::vector& sea, std::vector& land) { + sea.clear(); land.clear(); + const auto& links = p.tradeLinks(); + if (links.empty()) return; + const int steps = 6; // a few segments so the route arcs over the surface + for (const TradeLink& t : links) { + if (t.a < 0 || t.a >= (int)p.settlements.size() || t.b < 0 || t.b >= (int)p.settlements.size()) continue; + Vec3 A = p.cells[p.settlements[t.a].cell].unit, B = p.cells[p.settlements[t.b].cell].unit; + auto& out = (t.kind == TradeKind::Sea) ? sea : land; + Vector3 prev{}; + for (int i = 0; i <= steps; ++i) { + double u = (double)i / steps; + Vec3 m = (A * (1.0 - u) + B * u).normalized() * (radius * 1.015); + Vector3 cur{ (float)m.x, (float)m.y, (float)m.z }; + if (i > 0) { out.push_back(prev); out.push_back(cur); } + prev = cur; + } + } +} + void buildWarFrontier(const Planet& p, float radius, std::vector& segs) { segs.clear(); const std::vector& cn = p.cellNation(); diff --git a/src/render/Overlays.hpp b/src/render/Overlays.hpp index 9153328..b78bd3c 100644 --- a/src/render/Overlays.hpp +++ b/src/render/Overlays.hpp @@ -24,6 +24,8 @@ void buildCultureBorders(const Planet& p, float radius, std::vector& se void buildWarFrontier(const Planet& p, float radius, std::vector& segs); // Great-circle arcs between allied (-> ally) and rival (-> rival) realm capitals (civ Step 6). void buildDiploLinks(const Planet& p, float radius, std::vector& ally, std::vector& rival); +// Trade routes between linked settlements, split by kind (civ Step 7): sea routes vs river/overland. +void buildTradeRoutes(const Planet& p, float radius, std::vector& sea, std::vector& land); // ---- Per-plate drift arrows ------------------------------------------------- struct PlateLabel { int id; Vector3 pos; }; diff --git a/src/render/Panels.cpp b/src/render/Panels.cpp index 2d6a3ee..ad9e3ca 100644 --- a/src/render/Panels.cpp +++ b/src/render/Panels.cpp @@ -124,6 +124,14 @@ static std::vector cellInfo(const Planet& p, int i, double elev, do L.push_back(std::string("realm: wilderness")); } } + // Trade wealth of the settlement on this cell (civ Step 7). + if (p.tradeBuilt() && i < (int)p.cellSettlement().size() && p.cellSettlement()[i] >= 0) { + int si = p.cellSettlement()[i]; + if (si < (int)p.prosperity().size()) { + int routes = 0; for (const TradeLink& t : p.tradeLinks()) if (t.a == si || t.b == si) ++routes; + L.push_back(std::string(TextFormat("prosperity: %.2f / trade: %d routes", p.prosperity()[si], routes))); + } + } // Culture, ethos & faith of this cell's people (civ Step 4). if (p.culturesBuilt()) { const auto& cc = p.cellCulture(); diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index 09c2c90..a243183 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -124,6 +124,9 @@ void Viewer::recolor() { const std::vector& hab = planet.habitability(); const std::vector& cnat = planet.cellNation(); const std::vector& ccult = planet.cellCulture(); + const std::vector& cwealth = planet.cellWealth(); + double wealthMax = 1e-6; // normalise the wealth heat map to the current max + for (double w : cwealth) if (w > wealthMax) wealthMax = w; vcolors.resize(planet.cells.size()); for (size_t i = 0; i < planet.cells.size(); ++i) { switch (mode) { @@ -176,6 +179,13 @@ void Viewer::recolor() { ? Color{60, 64, 58, 255} : Color{26, 34, 52, 255}; break; } + case ColorMode::Wealth: { + double w = (i < (int)cwealth.size()) ? cwealth[i] : 0.0; + if (w > 0.0) vcolors[i] = wealthColor(w / wealthMax); // owned land: trade prosperity + else vcolors[i] = (planet.cells[i].elevation > planet.cfg.seaLevel) // wilderness land vs sea + ? Color{40, 42, 46, 255} : Color{22, 30, 44, 255}; + break; + } default: vcolors[i] = elevationColor(planet.cells[i].elevation, planet.cfg.seaLevel); } } @@ -776,7 +786,7 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) { } if (vu.breach) refreshView(); else if (vu.recolor || cu.recolor - || (territoryChanged && (mode == ColorMode::Territory || mode == ColorMode::Culture))) recolor(); + || (territoryChanged && (mode == ColorMode::Territory || mode == ColorMode::Culture || mode == ColorMode::Wealth))) recolor(); rebuildLiveOverlay(); } @@ -785,10 +795,12 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) { void Viewer::rebuildTerritory() { planet.computeTerritory(); planet.computeCultures(); + planet.computeTrade(); // civ Step 7: trade links + prosperity (derived) buildNationBorders(planet, borderR, nationBorders); buildCultureBorders(planet, borderR, cultureBorders); buildWarFrontier(planet, borderR + 0.001f, warFrontier); // civ Step 5: current war fronts (drawn red) buildDiploLinks(planet, borderR, allyLinks, rivalLinks); // civ Step 6: alliance/rivalry arcs + buildTradeRoutes(planet, borderR, tradeSea, tradeLand); // civ Step 7: trade routes double yearHours = std::max(1.0, planet.cfg.dayLengthHours * planet.cfg.yearLengthDays); lastTerritoryYear = (long)std::floor(liveTime / yearHours); } diff --git a/src/render/Viewer.hpp b/src/render/Viewer.hpp index 1abd60f..81111fa 100644 --- a/src/render/Viewer.hpp +++ b/src/render/Viewer.hpp @@ -111,6 +111,8 @@ struct Viewer { bool showCultureBorders = false; // draw cultural-region borders (on with the Culture view) std::vector warFrontier; // red frontier segments between realms currently at war (civ Step 5) std::vector allyLinks, rivalLinks; // civ Step 6: capital-to-capital arcs (allies green, rivals red) + std::vector tradeSea, tradeLand; // civ Step 7: trade routes (sea cyan / river+land amber) + bool showTradeRoutes = false; // draw trade routes (on with the Wealth view) long lastTerritoryYear = -1; // sim year territory was last recomputed (recompute when it ticks) // World event journal: currently Live World events, shaped to be reused by later phases. diff --git a/src/render/ViewerInput.cpp b/src/render/ViewerInput.cpp index 88a8634..8060af7 100644 --- a/src/render/ViewerInput.cpp +++ b/src/render/ViewerInput.cpp @@ -235,6 +235,16 @@ void Viewer::handleInput() { setStatus(mode == ColorMode::Culture ? "Cultures / faiths on" : "Cultures off"); } } + if (IsKeyPressed(KEY_Z) && settled) { // toggle the wealth / trade colour view + trade routes + if (!planet.settlementsPlaced()) setStatus("Press U for the dawn of civilization first"); + else { + if (!planet.tradeBuilt() || (int)planet.cellWealth().size() != (int)planet.cells.size()) rebuildTerritory(); + mode = (mode == ColorMode::Wealth) ? ColorMode::Biome : ColorMode::Wealth; + showTradeRoutes = (mode == ColorMode::Wealth); + recolor(); + setStatus(mode == ColorMode::Wealth ? "Wealth / trade on" : "Wealth off"); + } + } if (IsKeyPressed(KEY_W) && settled) { // enter / leave Live World (slow real-time clock) liveWorld = !liveWorld; if (liveWorld) { diff --git a/src/render/ViewerRender.cpp b/src/render/ViewerRender.cpp index 092d276..4e9e63d 100644 --- a/src/render/ViewerRender.cpp +++ b/src/render/ViewerRender.cpp @@ -120,6 +120,20 @@ void Viewer::renderGlobe3D() { } rlEnd(); rlSetLineWidth(1.0f); } + if (showTradeRoutes) { // civ Step 7: trade routes (sea cyan, land/river amber) + rlSetLineWidth(1.5f); rlBegin(RL_LINES); + rlColor4ub(90, 200, 235, 200); + for (size_t i = 0; i + 1 < tradeSea.size(); i += 2) { + rlVertex3f(tradeSea[i].x, tradeSea[i].y, tradeSea[i].z); + rlVertex3f(tradeSea[i + 1].x, tradeSea[i + 1].y, tradeSea[i + 1].z); + } + rlColor4ub(230, 180, 90, 200); + for (size_t i = 0; i + 1 < tradeLand.size(); i += 2) { + rlVertex3f(tradeLand[i].x, tradeLand[i].y, tradeLand[i].z); + rlVertex3f(tradeLand[i + 1].x, tradeLand[i + 1].y, tradeLand[i + 1].z); + } + rlEnd(); rlSetLineWidth(1.0f); + } if (showDrift && !driftArrows.empty()) { rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(90, 230, 255, 255); for (size_t i = 0; i + 1 < driftArrows.size(); i += 2) { @@ -378,6 +392,8 @@ void Viewer::renderMap2D() { if (showNationBorders && !warFrontier.empty()) drawSegments2D(warFrontier, Color{235, 40, 30, 255}, 2.5f, vr, mapLon); if (showNationBorders && !allyLinks.empty()) drawSegments2D(allyLinks, Color{70, 220, 120, 220}, 1.5f, vr, mapLon); if (showNationBorders && !rivalLinks.empty()) drawSegments2D(rivalLinks, Color{150, 40, 60, 220}, 1.5f, vr, mapLon); + if (showTradeRoutes && !tradeSea.empty()) drawSegments2D(tradeSea, Color{90, 200, 235, 220}, 1.2f, vr, mapLon); + if (showTradeRoutes && !tradeLand.empty()) drawSegments2D(tradeLand, Color{230, 180, 90, 220}, 1.2f, vr, mapLon); if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, vr, mapLon); if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, vr, mapLon); if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, vr, mapLon); @@ -678,6 +694,7 @@ void Viewer::renderLiveInfo() { } } else if (liveInfoTab == 6) { // Civ: settlements by population (largest first); click a row to fly there const auto& S = planet.settlements; + const auto& prosp = planet.prosperity(); // civ Step 7: trade wealth per settlement const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop; DrawText("Settlements", x, y, 18, Color{200, 205, 220, 255}); DrawText(TextFormat("%d", (int)S.size()), (int)(r.x + r.width) - 40, y + 2, 13, Color{145, 155, 175, 255}); @@ -703,6 +720,8 @@ void Viewer::renderLiveInfo() { : s.population >= 1.0e3 ? TextFormat("%.0fk", s.population / 1.0e3) : TextFormat("%.0f", s.population); DrawText(s.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, fg); + double pr = (si < (int)prosp.size()) ? prosp[si] : 0.0; // gold wealth dot (bigger = richer hub) + if (alive && pr > 0.15) DrawCircle((int)(r.x + r.width) - 106, (int)row.y + 9, 2.0f + (float)std::min(1.0, pr / 3.0) * 3.0f, Color{240, 205, 90, 225}); const char* tag = !alive ? "ruins" : settleTierName(t); DrawText(TextFormat("%s %s", tag, pop), (int)(r.x + r.width) - 92, (int)row.y + 3, 11, Color{150, 158, 178, 255}); y += 20; @@ -871,10 +890,10 @@ void Viewer::renderHUD() { line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga E eco (*6 cycles mean/summer/winter/season)"); line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]", showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off")); - line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | E eco | I habitability | U settlements [%s] | P territory [%s] | X culture [%s]", + line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | E eco | I habitability | U settlements [%s] | P territory [%s] | X culture [%s] | Z trade [%s]", showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off", !planet.settlementsPlaced() ? "seed" : showSettlements ? "on" : "off", - showNationBorders ? "on" : "off", showCultureBorders ? "on" : "off")); + showNationBorders ? "on" : "off", showCultureBorders ? "on" : "off", showTradeRoutes ? "on" : "off")); line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-", phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off")); line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg"); diff --git a/src/sim/Planet.cpp b/src/sim/Planet.cpp index 9b0a65a..f998649 100644 --- a/src/sim/Planet.cpp +++ b/src/sim/Planet.cpp @@ -65,6 +65,8 @@ void Planet::buildGeometry() { sCivCond.clear(); sCivDrought.clear(); nations.clear(); sCellNation.assign(cells.size(), -1); sSettleNation.clear(); cultures.clear(); sCellCulture.assign(cells.size(), -1); sSettleCulture.clear(); + sCellSettleOwner.assign(cells.size(), -1); + sProsperity.clear(); sTradeLinks.clear(); sCellWealth.assign(cells.size(), 0.0); wars.clear(); sSettleAllegiance.clear(); diplomacy.clear(); sWarRng = cfg.seed ? (cfg.seed ^ 0x5A7B0A11u) : 0x5A7B0A11u; sWarNextId = 1; } diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index 23b2332..fd997c8 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -8,6 +8,7 @@ #include "PlanetNation.hpp" // Nation, NationTier, GovType #include "PlanetCulture.hpp" // Culture, CultureEthos, Faith #include "PlanetConflict.hpp" // WarEvent, ConflictUpdate (War is in PlanetTypes.hpp) +#include "PlanetTrade.hpp" // TradeKind, TradeLink #include "PlanetEcoregions.hpp" // Ecoregion #include #include @@ -228,6 +229,15 @@ public: const std::vector& diploList() const { return diplomacy; } DiploKind diploBetween(int nationA, int nationB) const; // relation kind between two nation indices bool realmsAllied(int nationA, int nationB) const; // are these two realms allied? + + // Trade & economy (PlanetTrade.cpp; civ Step 7). computeTrade() links nearby settlements (sea/river/ + // overland), accrues prosperity at hubs (which feeds population growth) and fills a per-cell wealth + // field. A pure derived function of settlements + geography + wars/diplomacy -- recomputed, not saved. + void computeTrade(); + bool tradeBuilt() const { return !sProsperity.empty(); } + const std::vector& prosperity() const { return sProsperity; } // per settlement (0..~) + const std::vector& tradeLinks() const { return sTradeLinks; } + const std::vector& cellWealth() const { return sCellWealth; } // per cell (heat map) // 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. @@ -366,6 +376,13 @@ private: // Cultures (derived from settlements + geography; not saved). sCellCulture: culture index per cell // (-1 = none/wilderness/ocean); sSettleCulture: culture index per settlement. std::vector sCellCulture, sSettleCulture; + // Per-cell owning settlement index (filled by computeTerritory alongside sCellNation; -1 = none). + // Reused by the trade wealth field so the heat map matches territory exactly. + std::vector sCellSettleOwner; + // Trade & economy (civ Step 7; derived, not saved). Per-settlement prosperity, the trade-link list, + // and a per-cell wealth field (owning settlement's prosperity). Recomputed each sim year. + std::vector sProsperity, sCellWealth; + std::vector sTradeLinks; // Conflict & war (civ Step 5; stateful, saved v21). sSettleAllegiance[s] = the overlord's capital // settlement index if conquered, else -1 (independent). Separate war RNG + a stable war-id counter. std::vector sSettleAllegiance; diff --git a/src/sim/PlanetCiv.cpp b/src/sim/PlanetCiv.cpp index 0ab3506..59ff106 100644 --- a/src/sim/PlanetCiv.cpp +++ b/src/sim/PlanetCiv.cpp @@ -217,7 +217,8 @@ CivUpdate Planet::stepCivilization(double dtHours, double liveTime) { 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; + double prosp = (k < sProsperity.size()) ? sProsperity[k] : 0.0; // civ Step 7: trade wealth boosts capacity + double K = cfg.civMaxPopulation * hab * siteQ * cond * (1.0 + cfg.tradeProsperityWeight * prosp); sCivCond[k] = cond; sCivDrought[k] = drought; // Storms over the town kill people directly (acute), hurricanes worst. Storms are snapshotted, so diff --git a/src/sim/PlanetConflict.cpp b/src/sim/PlanetConflict.cpp index 41e1ffb..efbb2a7 100644 --- a/src/sim/PlanetConflict.cpp +++ b/src/sim/PlanetConflict.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include // --- Civilization Steps 5-6: conflict, war & diplomacy ----------------------- @@ -180,6 +181,18 @@ ConflictUpdate Planet::stepConflict(long year) { } wars.swap(keep); + // Trade feedback inputs (civ Step 7; LAST year's derived economy -- computeTrade runs after this in + // rebuildTerritory -- empty on the first year). Trade fosters friendship; a rich weak realm tempts war. + std::set> tradePairs; + if (!sTradeLinks.empty() && sSettleNation.size() == settlements.size()) + for (const TradeLink& L : sTradeLinks) { + int na = sSettleNation[L.a], nb = sSettleNation[L.b]; + if (na >= 0 && nb >= 0 && na != nb) tradePairs.insert({ std::min(na, nb), std::max(na, nb) }); + } + std::vector realmProsp(nations.size(), 0.0); + if (sProsperity.size() == settlements.size() && sSettleNation.size() == settlements.size()) + for (size_t s = 0; s < settlements.size(); ++s) { int nn = sSettleNation[s]; if (nn >= 0 && nn < (int)nations.size()) realmProsp[nn] += sProsperity[s]; } + // --- 2.5) Diplomacy: prune dead ties, drift attitudes, form/break relations (civ Step 6) -- { // drop ties whose endpoints are gone (kept dormant across a temporary capital swap otherwise) std::vector kd; kd.reserve(diplomacy.size()); @@ -198,7 +211,9 @@ ConflictUpdate Planet::stepConflict(long year) { } uint32_t pk = warHash((uint32_t)std::min(ca, cb) * 2654435761u ^ (uint32_t)std::max(ca, cb) * 40503u ^ (uint32_t)year * 19349663u ^ seed ^ 0xD1D0u); double noise = (pk & 0xFFFFu) / 65535.0 * 2.0 - 1.0; // [-1,1] per-pair personality - double target = std::clamp(cfg.diploAffinity * 0.5 * aff + 0.25 * noise, -1.0, 1.0); + double target = cfg.diploAffinity * 0.5 * aff + 0.25 * noise; + if (tradePairs.count({ std::min(A, B), std::max(A, B) })) target += cfg.tradeDiploBonus; // trade fosters friendship + target = std::clamp(target, -1.0, 1.0); DiploTie& t = tieRef(ca, cb); double drift = cfg.diploDriftRate * (target - t.attitude); if (realmsAtWar(A, B)) drift -= cfg.diploWarPenalty * cfg.diploDriftRate * 4.0; // war sours relations @@ -245,7 +260,9 @@ ConflictUpdate Planet::stepConflict(long year) { double frontN = std::min(1.0, closePairs / 4.0); uint32_t pk = warHash((uint32_t)std::min(ca, cb) * 73856093u ^ (uint32_t)std::max(ca, cb) * 19349663u ^ (uint32_t)year * 83492791u ^ seed); double noise = (pk & 0xFFFFu) / 65535.0; - double hostility = cfg.warAmbition * sizeGap + cfg.warIdeology * std::max(0.0, -att) + cfg.warBorder * (0.5 * frontN + 0.5 * prox) + 0.4 * noise; + double weakerProsp = (sA <= sB) ? realmProsp[A] : realmProsp[B]; // a wealthy weak neighbour tempts war + double hostility = cfg.warAmbition * sizeGap + cfg.warIdeology * std::max(0.0, -att) + + cfg.warBorder * (0.5 * frontN + 0.5 * prox) + cfg.tradeTemptWar * std::min(1.0, weakerProsp / 5.0) + 0.4 * noise; cands.push_back({A, B, hostility}); } std::sort(cands.begin(), cands.end(), [](const Cand& x, const Cand& y) { return x.hostility > y.hostility; }); diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 577081e..a7b546c 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -60,6 +60,8 @@ D(warConquerScore) D(warSackChance) D(warExhaustion) D(warRevoltRate) D(warMinRealmPop) \ D(diploDriftRate) D(diploAffinity) D(diploWarPenalty) D(diploTruceYears) D(diploWarGrudge) \ D(diploAllyThreshold) D(diploNonAggThreshold) D(diploRivalThreshold) \ + D(tradeLandRange) D(tradeSeaRange) D(tradeRiverBonus) D(tradeMinVolume) D(tradeProsperityWeight) \ + D(tradeWarBlock) D(tradeAllyBonus) D(tradeDiploBonus) D(tradeTemptWar) \ 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) \ @@ -309,6 +311,15 @@ std::string validateConfig(const PlanetConfig& cfg) { E(rng(cfg.diploAllyThreshold, -1.0, 1.0, "diploAllyThreshold")); E(rng(cfg.diploNonAggThreshold, -1.0, 1.0, "diploNonAggThreshold")); E(rng(cfg.diploRivalThreshold, -1.0, 1.0, "diploRivalThreshold")); + E(rng(cfg.tradeLandRange, 0.0, 3.14159, "tradeLandRange")); + E(rng(cfg.tradeSeaRange, 0.0, 3.14159, "tradeSeaRange")); + E(rng(cfg.tradeRiverBonus, 0.0, 3.14159, "tradeRiverBonus")); + E(rng(cfg.tradeMinVolume, 0.0, 1.0e9, "tradeMinVolume")); + E(rng(cfg.tradeProsperityWeight, 0.0, 100.0, "tradeProsperityWeight")); + E(rng(cfg.tradeWarBlock, 0.0, 10.0, "tradeWarBlock")); + 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(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/PlanetNation.cpp b/src/sim/PlanetNation.cpp index 31695a4..7d361a6 100644 --- a/src/sim/PlanetNation.cpp +++ b/src/sim/PlanetNation.cpp @@ -22,6 +22,7 @@ void Planet::computeTerritory() { const int n = (int)cells.size(); nations.clear(); sCellNation.assign(n, -1); + sCellSettleOwner.assign(n, -1); sSettleNation.assign(settlements.size(), -1); if (settlements.empty()) return; @@ -106,6 +107,6 @@ void Planet::computeTerritory() { double score = range[k] - d; if (score > bestScore) { bestScore = score; owner = (int)k; } } - if (owner >= 0) sCellNation[i] = sSettleNation[owner]; + if (owner >= 0) { sCellNation[i] = sSettleNation[owner]; sCellSettleOwner[i] = owner; } // owner reused by trade (Step 7) } } diff --git a/src/sim/PlanetTrade.cpp b/src/sim/PlanetTrade.cpp new file mode 100644 index 0000000..d68d4d0 --- /dev/null +++ b/src/sim/PlanetTrade.cpp @@ -0,0 +1,73 @@ +#include "Planet.hpp" +#include +#include + +// --- Civilization Step 7: trade & economy ------------------------------------ +// computeTrade() links nearby settlements (sea / river / overland), accrues prosperity at well-connected +// hubs, and fills a per-cell wealth field. A pure derived function of the settlement set + geography + +// wars/diplomacy (no RNG), recomputed each sim year in rebuildTerritory (after computeTerritory, which +// provides the realms for political gating + the per-cell owning settlement). Not saved. + +void Planet::computeTrade() { + const int n = (int)cells.size(); + sProsperity.assign(settlements.size(), 0.0); + sTradeLinks.clear(); + sCellWealth.assign(n, 0.0); + if (settlements.empty()) return; + + const double sea = cfg.seaLevel, abP = cfg.civAbandonPop; + const bool haveDisch = (int)sDischarge.size() == n; + const bool haveNat = (sSettleNation.size() == settlements.size()); + 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)); }; + + // Per-settlement coastal + river flags (the same signals stepCivilization's site quality uses). + std::vector coastal(settlements.size(), 0), river(settlements.size(), 0); + for (size_t k = 0; k < settlements.size(); ++k) { + if (!alive((int)k)) continue; + int c = settlements[k].cell; + for (int j : cells[c].neighbors) if (cells[j].elevation <= sea) { coastal[k] = 1; break; } + if (haveDisch && sDischarge[c] > cfg.riverThreshold) river[k] = 1; + } + + // Trade links between nearby living settlements. + for (size_t a = 0; a < settlements.size(); ++a) { + if (!alive((int)a)) continue; + for (size_t b = a + 1; b < settlements.size(); ++b) { + if (!alive((int)b)) continue; + double d = angC(settlements[a].cell, settlements[b].cell); + bool bothCoast = coastal[a] && coastal[b]; + double reach = cfg.tradeLandRange + + ((river[a] || river[b]) ? cfg.tradeRiverBonus : 0.0) + + (bothCoast ? cfg.tradeSeaRange : 0.0); + if (d > reach) continue; + TradeKind kind = (bothCoast && d > cfg.tradeLandRange) ? TradeKind::Sea + : (river[a] || river[b]) ? TradeKind::River : TradeKind::Land; + // Volume: both populations matter, closer is stronger, sea/river a touch better. + double popF = std::log10(1.0 + settlements[a].population / 1000.0) + * std::log10(1.0 + settlements[b].population / 1000.0); + double prox = 1.0 - d / std::max(1e-6, reach); + double kindF = (kind == TradeKind::Sea) ? 1.2 : (kind == TradeKind::River) ? 1.1 : 1.0; + double vol = 0.25 * popF * (0.3 + 0.7 * prox) * kindF; + // Political factor: blockaded by war, boosted by an alliance. + int nA = haveNat ? sSettleNation[a] : -1, nB = haveNat ? sSettleNation[b] : -1; + if (nA >= 0 && nB >= 0 && nA != nB) { + if (realmsAtWar(nA, nB)) vol *= cfg.tradeWarBlock; + else if (realmsAllied(nA, nB)) vol *= cfg.tradeAllyBonus; + } + if (vol < cfg.tradeMinVolume) continue; + sTradeLinks.push_back(TradeLink{ (int)a, (int)b, (float)vol, kind }); + sProsperity[a] += vol; sProsperity[b] += vol; + } + } + // 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; + + // Wealth field: each owned land cell takes its owner settlement's prosperity (matches territory). + const bool haveOwner = ((int)sCellSettleOwner.size() == n); + for (int i = 0; i < n; ++i) { + int o = haveOwner ? sCellSettleOwner[i] : -1; + if (o >= 0 && o < (int)sProsperity.size()) sCellWealth[i] = sProsperity[o]; + } +} diff --git a/src/sim/PlanetTrade.hpp b/src/sim/PlanetTrade.hpp new file mode 100644 index 0000000..14b54b7 --- /dev/null +++ b/src/sim/PlanetTrade.hpp @@ -0,0 +1,17 @@ +#pragma once +#include +#include + +// Civilization Step 7: trade & economy. Trade routes link nearby settlements (by sea / river / overland), +// prosperity accrues at well-connected hubs, and prosperity boosts population growth. Like territory and +// cultures, this is a PURE DETERMINISTIC function of the (saved) settlement set + geography + wars + +// diplomacy -- recomputed each sim year, not saved (no save-format change), no RNG, rewinds for free. + +enum class TradeKind : uint8_t { Land, River, Sea }; + +struct TradeLink { + int a = -1; // settlement index of one endpoint + int b = -1; // settlement index of the other endpoint + float volume = 0.0f; // trade volume (drives both endpoints' prosperity) + TradeKind kind = TradeKind::Land; +}; diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index 916f1f4..331cf59 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -478,4 +478,15 @@ struct PlanetConfig { double diploAllyThreshold = 0.5; // attitude at/above which two realms are allied double diploNonAggThreshold = 0.2; // attitude at/above which they sign a non-aggression pact double diploRivalThreshold = -0.5; // attitude at/below which they become rivals + // Civilization Step 7: trade & economy (derived, not saved). Trade routes link nearby settlements; + // prosperity accrues at hubs and boosts growth; trade feeds back into diplomacy + war motivation. + double tradeLandRange = 0.10; // rad: overland trade reach (~630 km) + double tradeSeaRange = 0.30; // rad: extra reach when both settlements are coastal (sea route) + double tradeRiverBonus = 0.06; // rad: reach bonus when either settlement sits on a river + double tradeMinVolume = 0.05; // links below this volume are dropped + double tradeProsperityWeight= 0.8; // how much a hub's prosperity multiplies its carrying capacity + double tradeWarBlock = 0.0; // trade-volume multiplier between realms at war (0 = blockade) + 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) }; diff --git a/test_trade.cpp b/test_trade.cpp new file mode 100644 index 0000000..6751cff --- /dev/null +++ b/test_trade.cpp @@ -0,0 +1,151 @@ +// Headless test for civilization Step 7 (trade & economy). No display needed. +// +// g++ -std=c++17 -O2 -Isrc/sim test_trade.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \ +// src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp src/sim/PlanetErosion.cpp \ +// src/sim/PlanetHydrology.cpp src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \ +// src/sim/PlanetLive.cpp src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp \ +// src/sim/PlanetVolcano.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \ +// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/NameGen.cpp \ +// src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp src/sim/PlanetCiv.cpp \ +// src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp \ +// src/sim/PlanetTrade.cpp src/sim/PlanetIO.cpp -o /tmp/ttr && /tmp/ttr +// +// Verifies: trade links form (coastal pairs over a longer sea range); hubs are richer than isolated +// settlements; war blocks trade; prosperity raises carrying capacity (bigger connected cities); +// determinism; save->load->recompute parity (derived, not saved). + +#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(); +} + +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; + p.placeSettlements(); + double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); } + + std::printf("Trade: links + prosperity\n"); + p.computeTerritory(); p.computeCultures(); p.computeTrade(); + check(p.tradeBuilt(), "computeTrade produces prosperity"); + check((int)p.cellWealth().size() == n && p.prosperity().size() == p.settlements.size(), "arrays sized"); + check(!p.tradeLinks().empty(), "trade links form between settlements"); + { + bool sawSea = false, endpointsValid = true; + for (const TradeLink& t : p.tradeLinks()) { + if (t.kind == TradeKind::Sea) sawSea = true; + if (t.a < 0 || t.b < 0 || t.a >= (int)p.settlements.size() || t.b >= (int)p.settlements.size() || t.volume <= 0.0f) endpointsValid = false; + } + check(endpointsValid, "every link has valid endpoints + positive volume"); + std::printf(" %d links (sea route present: %s)\n", (int)p.tradeLinks().size(), sawSea ? "yes" : "no"); + check(sawSea, "coastal settlements link by sea (longer range)"); + } + + std::printf("Trade: hubs are richer than isolated settlements\n"); + { + // The most-connected settlement (a hub) should out-prosper the least-connected living one. + std::vector deg(p.settlements.size(), 0); + for (const TradeLink& t : p.tradeLinks()) { ++deg[t.a]; ++deg[t.b]; } + int hub = -1, iso = -1; + for (size_t k = 0; k < p.settlements.size(); ++k) { + if (p.settlements[k].population < p.cfg.civAbandonPop) continue; + if (hub < 0 || deg[k] > deg[hub]) hub = (int)k; + if (iso < 0 || deg[k] < deg[iso]) iso = (int)k; + } + std::printf(" hub deg %d prosperity %.2f ; isolated deg %d prosperity %.2f\n", + deg[hub], p.prosperity()[hub], deg[iso], p.prosperity()[iso]); + check(hub >= 0 && iso >= 0 && p.prosperity()[hub] > p.prosperity()[iso], "a trade hub is richer than an isolated settlement"); + } + + std::printf("Trade: war blocks trade between belligerents\n"); + { + // Find two settlements in different realms that are currently trade partners; force a war -> volume drops. + p.cfg.tradeWarBlock = 0.0; + int a = -1, b = -1; + for (const TradeLink& t : p.tradeLinks()) { + int na = p.settleNation()[t.a], nb = p.settleNation()[t.b]; + if (na >= 0 && nb >= 0 && na != nb) { a = t.a; b = t.b; break; } + } + if (a < 0) { std::printf(" (no cross-realm trade link on this seed -- skipping)\n"); check(true, "war-block check (skipped: no cross-realm link)"); } + else { + double volPeace = 0.0; for (const TradeLink& t : p.tradeLinks()) if ((t.a==a&&t.b==b)||(t.a==b&&t.b==a)) volPeace = t.volume; + War w; w.attacker = p.nationList()[p.settleNation()[a]].capital; w.defender = p.nationList()[p.settleNation()[b]].capital; + p.wars.push_back(w); + p.computeTrade(); + double volWar = 0.0; for (const TradeLink& t : p.tradeLinks()) if ((t.a==a&&t.b==b)||(t.a==b&&t.b==a)) volWar = t.volume; + p.wars.clear(); p.computeTrade(); + std::printf(" link volume peace %.3f -> war %.3f\n", volPeace, volWar); + check(volPeace > 0.0 && volWar < volPeace, "a war cuts the trade volume between the two realms"); + } + } + + std::printf("Trade: prosperity raises carrying capacity (bigger connected cities)\n"); + { + // Two identical worlds; one grows with trade wealth on, one with it off (weight 0). + Planet a; a.generate(cfg); settle(a); drift(a, 400); a.placeSettlements(); + Planet b; b.generate(cfg); settle(b); drift(b, 400); b.placeSettlements(); + b.cfg.tradeProsperityWeight = 0.0; // no economic boost + double la = 0.0, lb = 0.0; + for (int yr = 0; yr < 500; ++yr) { + la += 2.0 * yearH; a.computeTerritory(); a.computeCultures(); a.computeTrade(); a.stepCivilization(2.0 * yearH, la); + lb += 2.0 * yearH; b.computeTerritory(); b.computeCultures(); b.computeTrade(); b.stepCivilization(2.0 * yearH, lb); + } + double totA = 0.0, totB = 0.0; + for (const Settlement& s : a.settlements) totA += s.population; + for (const Settlement& s : b.settlements) totB += s.population; + std::printf(" total population with trade %.0f vs no-trade %.0f\n", totA, totB); + check(totA > totB, "trade wealth increases total population vs no economic boost"); + } + + std::printf("Trade: determinism + save->load->recompute parity\n"); + { + Planet q; q.generate(cfg); settle(q); drift(q, 400); q.placeSettlements(); + double lq = 0.0; for (int yr = 0; yr < 600; ++yr) { lq += 2.0 * yearH; q.stepCivilization(2.0 * yearH, lq); } + q.computeTerritory(); q.computeCultures(); q.computeTrade(); + check(q.prosperity() == p.prosperity() && q.cellWealth() == p.cellWealth(), "computeTrade is deterministic"); + + std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary); + p.writeState(ss); + Planet r; + bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true, true, true); + check(ok, "readState accepts the stream (no trade block -- derived)"); + r.computeTerritory(); r.computeCultures(); r.computeTrade(); + check(r.prosperity() == p.prosperity(), "trade recomputed after load matches (derived, not saved)"); + } + + std::printf("Trade: RNG isolation from tectonics\n"); + { + Planet x; x.generate(cfg); settle(x); + Planet y; y.generate(cfg); settle(y); + for (int k = 0; k < 40; ++k) { + double dx = x.cflDtMy(); x.advect(dx); x.step(); x.erode(dx); + double dy = y.cflDtMy(); y.advect(dy); y.step(); y.erode(dy); + if (k == 20) { y.placeSettlements(); y.stepCivilization(yearH, yearH); y.computeTerritory(); y.computeCultures(); y.computeTrade(); } + } + bool same = true; + for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) same = false; + check(same, "computeTrade never perturbs tectonic evolution"); + } + + std::printf(failures ? "\nFAILURES: %d\n" : "\nALL TRADE CHECKS PASSED\n", failures); + return failures ? 1 : 0; +}