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 <noreply@anthropic.com>
This commit is contained in:
Jonas Reith 2026-07-01 13:36:41 +02:00
parent 8cd487a5dc
commit e27b91a128
23 changed files with 477 additions and 19 deletions

View File

@ -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

View File

@ -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 16 of the roadmap are done (plus a derived ecoregions atlas):**
on the Live World clock). **Steps 17 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 16 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

View File

@ -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})

View File

@ -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).

View File

@ -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);

View File

@ -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).

View File

@ -102,6 +102,26 @@ void buildDiploLinks(const Planet& p, float radius, std::vector<Vector3>& ally,
}
}
void buildTradeRoutes(const Planet& p, float radius, std::vector<Vector3>& sea, std::vector<Vector3>& 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<Vector3>& segs) {
segs.clear();
const std::vector<int>& cn = p.cellNation();

View File

@ -24,6 +24,8 @@ void buildCultureBorders(const Planet& p, float radius, std::vector<Vector3>& se
void buildWarFrontier(const Planet& p, float radius, std::vector<Vector3>& segs);
// Great-circle arcs between allied (-> ally) and rival (-> rival) realm capitals (civ Step 6).
void buildDiploLinks(const Planet& p, float radius, std::vector<Vector3>& ally, std::vector<Vector3>& 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<Vector3>& sea, std::vector<Vector3>& land);
// ---- Per-plate drift arrows -------------------------------------------------
struct PlateLabel { int id; Vector3 pos; };

View File

@ -124,6 +124,14 @@ static std::vector<std::string> 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();

View File

@ -124,6 +124,9 @@ void Viewer::recolor() {
const std::vector<double>& hab = planet.habitability();
const std::vector<int>& cnat = planet.cellNation();
const std::vector<int>& ccult = planet.cellCulture();
const std::vector<double>& 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);
}

View File

@ -111,6 +111,8 @@ struct Viewer {
bool showCultureBorders = false; // draw cultural-region borders (on with the Culture view)
std::vector<Vector3> warFrontier; // red frontier segments between realms currently at war (civ Step 5)
std::vector<Vector3> allyLinks, rivalLinks; // civ Step 6: capital-to-capital arcs (allies green, rivals red)
std::vector<Vector3> 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.

View File

@ -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) {

View File

@ -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");

View File

@ -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;
}

View File

@ -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 <vector>
#include <memory>
@ -228,6 +229,15 @@ public:
const std::vector<DiploTie>& 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<double>& prosperity() const { return sProsperity; } // per settlement (0..~)
const std::vector<TradeLink>& tradeLinks() const { return sTradeLinks; }
const std::vector<double>& 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<int> 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<int> 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<double> sProsperity, sCellWealth;
std::vector<TradeLink> 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<int> sSettleAllegiance;

View File

@ -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

View File

@ -2,6 +2,7 @@
#include <algorithm>
#include <cmath>
#include <map>
#include <set>
#include <unordered_map>
// --- 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<std::pair<int, int>> 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<double> 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<DiploTie> 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; });

View File

@ -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"));

View File

@ -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)
}
}

73
src/sim/PlanetTrade.cpp Normal file
View File

@ -0,0 +1,73 @@
#include "Planet.hpp"
#include <algorithm>
#include <cmath>
// --- 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<uint8_t> 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];
}
}

17
src/sim/PlanetTrade.hpp Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include <vector>
#include <cstdint>
// 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;
};

View File

@ -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)
};

151
test_trade.cpp Normal file
View File

@ -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 <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();
}
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<int> 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;
}