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:
parent
8cd487a5dc
commit
e27b91a128
19
BUILD.md
19
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)
|
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)
|
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)
|
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)
|
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
|
. / , step the live clock forward / back by one rate-unit (auto-pauses; back also
|
||||||
rewinds weather + storms via an undo history)
|
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
|
diploNonAggThreshold 0.2 attitude at/above which they sign a non-aggression pact
|
||||||
diploRivalThreshold -0.5 attitude at/below which they become rivals
|
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
|
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
|
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
|
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/PlanetBiota.cpp \
|
||||||
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.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/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
|
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 /
|
# 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.
|
# The CMake build also includes test_events for the viewer event journal.
|
||||||
|
|
||||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||||
|
|||||||
42
CLAUDE.md
42
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
|
**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 +
|
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
|
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
|
- **Geography & place-names (the atlas)** *(done — see `PlanetGeography.cpp` + `NameGen.cpp`)* — the
|
||||||
foundation everything civic references. `Planet::generateGeography()` extracts named features from
|
foundation everything civic references. `Planet::generateGeography()` extracts named features from
|
||||||
the (frozen) terrain by connectivity over the fixed grid — **continents/islands** (connected land),
|
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
|
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
|
("The X and the Y form an alliance", "…become rivals", "…sign a non-aggression pact", "Z joins the war
|
||||||
against W"). Knobs `diplo*`.
|
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);
|
*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
|
## Current state
|
||||||
|
|
||||||
@ -631,6 +647,7 @@ src/
|
|||||||
PlanetNation.* computeTerritory (realms + per-cell ownership + borders; civ Step 3)
|
PlanetNation.* computeTerritory (realms + per-cell ownership + borders; civ Step 3)
|
||||||
PlanetCulture.* computeCultures (cultures/ethos/faith per continent + governments; civ Step 4)
|
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)
|
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
|
PlanetIO.cpp config file (text) + binary save/load
|
||||||
render/ (raylib viewer)
|
render/ (raylib viewer)
|
||||||
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
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/PlanetBiota.cpp \
|
||||||
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.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/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
|
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
```
|
```
|
||||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
||||||
`test_weather.cpp`, `test_volcano.cpp`, `test_geography.cpp`, `test_ecoregions.cpp`, `test_civ.cpp`,
|
`test_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 /
|
`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 suites — same source list. CMake also builds `test_events` for the
|
Civilization / Nation / Culture / Conflict / Diplomacy / Trade suites — same source list. CMake also builds `test_events` for the
|
||||||
viewer event journal.)
|
viewer event journal.)
|
||||||
|
|
||||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
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) ·
|
`U` settlements (the dawn of civilization on first press; toggles markers after) ·
|
||||||
`P` territory / realms view + political borders (Realms tab lists nations) ·
|
`P` territory / realms view + political borders (Realms tab lists nations) ·
|
||||||
`X` culture / faiths view + cultural borders (Cultures tab lists peoples, ethos & religion) ·
|
`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 ·
|
`SPACE` or on-screen button pause ·
|
||||||
`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec → up to ~20 years/sec) ·
|
`[`/`]` 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
|
`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**
|
(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
|
(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/
|
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**
|
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).
|
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
|
`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
|
in the same yearly `stepConflict` tick; raise `diploDriftRate`/`diploAffinity` for a more alliance-heavy
|
||||||
world, lower the thresholds' spread for touchier politics.
|
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
|
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||||
knob for how fast/high relief builds.
|
knob for how fast/high relief builds.
|
||||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||||
|
|||||||
@ -39,6 +39,7 @@ set(SIM_SOURCES
|
|||||||
src/sim/PlanetNation.cpp
|
src/sim/PlanetNation.cpp
|
||||||
src/sim/PlanetCulture.cpp
|
src/sim/PlanetCulture.cpp
|
||||||
src/sim/PlanetConflict.cpp
|
src/sim/PlanetConflict.cpp
|
||||||
|
src/sim/PlanetTrade.cpp
|
||||||
src/sim/PlanetIO.cpp
|
src/sim/PlanetIO.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ if(UNIX AND NOT APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
enable_testing()
|
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)
|
add_executable(test_${test_name} test_${test_name}.cpp)
|
||||||
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
||||||
add_test(NAME ${test_name} COMMAND test_${test_name})
|
add_test(NAME ${test_name} COMMAND test_${test_name})
|
||||||
|
|||||||
@ -57,6 +57,7 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
|||||||
government per realm; derived, not saved).
|
government per realm; derived, not saved).
|
||||||
- `PlanetConflict.{hpp,cpp}` — `stepConflict` (civ Steps 5-6: wars/conquest/revolts + diplomacy/
|
- `PlanetConflict.{hpp,cpp}` — `stepConflict` (civ Steps 5-6: wars/conquest/revolts + diplomacy/
|
||||||
alliances/coalitions; stateful, saved v21/v22 + snapshotted).
|
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.
|
- `PlanetIO.cpp` — text config + binary save/load.
|
||||||
|
|
||||||
The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration),
|
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`.
|
an allies/rivals line; kind-6 events (`=` icon). `captureWeather`/`restoreWeather` carry `diplomacy`.
|
||||||
Knobs `diplo*`.
|
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
|
## Headless testing
|
||||||
|
|
||||||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
|
||||||
src/sim/PlanetFungiGen.cpp src/sim/NameGen.cpp src/sim/PlanetGeography.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/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
|
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
# test_biota / test_live / test_ocean / test_weather / test_volcano / test_geography / test_ecoregions /
|
# 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
|
(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).
|
data-parallel + double-buffered → bit-identical for any OpenMP thread count (determinism).
|
||||||
|
|||||||
@ -95,6 +95,7 @@ const char* colorModeName(ColorMode m) {
|
|||||||
case ColorMode::Habitability: return "Habitability";
|
case ColorMode::Habitability: return "Habitability";
|
||||||
case ColorMode::Territory: return "Territory / realms";
|
case ColorMode::Territory: return "Territory / realms";
|
||||||
case ColorMode::Culture: return "Culture / faiths";
|
case ColorMode::Culture: return "Culture / faiths";
|
||||||
|
case ColorMode::Wealth: return "Wealth / trade";
|
||||||
case ColorMode::TempSummer: return "Temperature (summer)";
|
case ColorMode::TempSummer: return "Temperature (summer)";
|
||||||
case ColorMode::TempWinter: return "Temperature (winter)";
|
case ColorMode::TempWinter: return "Temperature (winter)";
|
||||||
case ColorMode::Seasonality: return "Seasonality (summer-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 };
|
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) {
|
Color ecoregionColor(int id, Biome b, double productivity) {
|
||||||
if (id < 0) return Color{ 48, 52, 58, 255 };
|
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);
|
Color base = ColorFromHSV(std::fmod((id + 11) * 0.61803398875f, 1.0f) * 360.0f, 0.55f, 0.82f);
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
|
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
|
||||||
FloraDensity, FaunaDensity, FungaDensity,
|
FloraDensity, FaunaDensity, FungaDensity,
|
||||||
Ecoregion, Habitability, Territory, Culture,
|
Ecoregion, Habitability, Territory, Culture, Wealth,
|
||||||
TempSummer, TempWinter, Seasonality }; // 6 cycles these temp sub-views
|
TempSummer, TempWinter, Seasonality }; // 6 cycles these temp sub-views
|
||||||
|
|
||||||
Color elevationColor(double e, double seaLevel);
|
Color elevationColor(double e, double seaLevel);
|
||||||
@ -42,6 +42,8 @@ Color marineFaunaColor(double d01);
|
|||||||
Color ecoregionColor(int id, Biome b, double productivity);
|
Color ecoregionColor(int id, Biome b, double productivity);
|
||||||
// Habitability heat map (0..1): barren grey -> fertile green/gold (where civilization can thrive).
|
// Habitability heat map (0..1): barren grey -> fertile green/gold (where civilization can thrive).
|
||||||
Color habitabilityColor(double h01);
|
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).
|
// Per-nation territory tint (golden-ratio HSV, offset from plateColor so realms read distinctly).
|
||||||
Color nationColor(int id);
|
Color nationColor(int id);
|
||||||
// Per-culture tint (golden-ratio HSV, a different phase than nationColor so cultural blocs read distinctly).
|
// Per-culture tint (golden-ratio HSV, a different phase than nationColor so cultural blocs read distinctly).
|
||||||
|
|||||||
@ -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) {
|
void buildWarFrontier(const Planet& p, float radius, std::vector<Vector3>& segs) {
|
||||||
segs.clear();
|
segs.clear();
|
||||||
const std::vector<int>& cn = p.cellNation();
|
const std::vector<int>& cn = p.cellNation();
|
||||||
|
|||||||
@ -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);
|
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).
|
// 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);
|
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 -------------------------------------------------
|
// ---- Per-plate drift arrows -------------------------------------------------
|
||||||
struct PlateLabel { int id; Vector3 pos; };
|
struct PlateLabel { int id; Vector3 pos; };
|
||||||
|
|||||||
@ -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"));
|
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).
|
// Culture, ethos & faith of this cell's people (civ Step 4).
|
||||||
if (p.culturesBuilt()) {
|
if (p.culturesBuilt()) {
|
||||||
const auto& cc = p.cellCulture();
|
const auto& cc = p.cellCulture();
|
||||||
|
|||||||
@ -124,6 +124,9 @@ void Viewer::recolor() {
|
|||||||
const std::vector<double>& hab = planet.habitability();
|
const std::vector<double>& hab = planet.habitability();
|
||||||
const std::vector<int>& cnat = planet.cellNation();
|
const std::vector<int>& cnat = planet.cellNation();
|
||||||
const std::vector<int>& ccult = planet.cellCulture();
|
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());
|
vcolors.resize(planet.cells.size());
|
||||||
for (size_t i = 0; i < planet.cells.size(); ++i) {
|
for (size_t i = 0; i < planet.cells.size(); ++i) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
@ -176,6 +179,13 @@ void Viewer::recolor() {
|
|||||||
? Color{60, 64, 58, 255} : Color{26, 34, 52, 255};
|
? Color{60, 64, 58, 255} : Color{26, 34, 52, 255};
|
||||||
break;
|
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);
|
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();
|
if (vu.breach) refreshView();
|
||||||
else if (vu.recolor || cu.recolor
|
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();
|
rebuildLiveOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -785,10 +795,12 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
|
|||||||
void Viewer::rebuildTerritory() {
|
void Viewer::rebuildTerritory() {
|
||||||
planet.computeTerritory();
|
planet.computeTerritory();
|
||||||
planet.computeCultures();
|
planet.computeCultures();
|
||||||
|
planet.computeTrade(); // civ Step 7: trade links + prosperity (derived)
|
||||||
buildNationBorders(planet, borderR, nationBorders);
|
buildNationBorders(planet, borderR, nationBorders);
|
||||||
buildCultureBorders(planet, borderR, cultureBorders);
|
buildCultureBorders(planet, borderR, cultureBorders);
|
||||||
buildWarFrontier(planet, borderR + 0.001f, warFrontier); // civ Step 5: current war fronts (drawn red)
|
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
|
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);
|
double yearHours = std::max(1.0, planet.cfg.dayLengthHours * planet.cfg.yearLengthDays);
|
||||||
lastTerritoryYear = (long)std::floor(liveTime / yearHours);
|
lastTerritoryYear = (long)std::floor(liveTime / yearHours);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,6 +111,8 @@ struct Viewer {
|
|||||||
bool showCultureBorders = false; // draw cultural-region borders (on with the Culture view)
|
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> 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> 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)
|
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.
|
// World event journal: currently Live World events, shaped to be reused by later phases.
|
||||||
|
|||||||
@ -235,6 +235,16 @@ void Viewer::handleInput() {
|
|||||||
setStatus(mode == ColorMode::Culture ? "Cultures / faiths on" : "Cultures off");
|
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)
|
if (IsKeyPressed(KEY_W) && settled) { // enter / leave Live World (slow real-time clock)
|
||||||
liveWorld = !liveWorld;
|
liveWorld = !liveWorld;
|
||||||
if (liveWorld) {
|
if (liveWorld) {
|
||||||
|
|||||||
@ -120,6 +120,20 @@ void Viewer::renderGlobe3D() {
|
|||||||
}
|
}
|
||||||
rlEnd(); rlSetLineWidth(1.0f);
|
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()) {
|
if (showDrift && !driftArrows.empty()) {
|
||||||
rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(90, 230, 255, 255);
|
rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(90, 230, 255, 255);
|
||||||
for (size_t i = 0; i + 1 < driftArrows.size(); i += 2) {
|
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 && !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 && !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 (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 (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 (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, vr, mapLon);
|
||||||
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, 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
|
} else if (liveInfoTab == 6) { // Civ: settlements by population (largest first); click a row to fly there
|
||||||
const auto& S = planet.settlements;
|
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;
|
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("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});
|
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)
|
: s.population >= 1.0e3 ? TextFormat("%.0fk", s.population / 1.0e3)
|
||||||
: TextFormat("%.0f", s.population);
|
: TextFormat("%.0f", s.population);
|
||||||
DrawText(s.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, fg);
|
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);
|
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});
|
DrawText(TextFormat("%s %s", tag, pop), (int)(r.x + r.width) - 92, (int)row.y + 3, 11, Color{150, 158, 178, 255});
|
||||||
y += 20;
|
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("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]",
|
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"));
|
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",
|
showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off",
|
||||||
!planet.settlementsPlaced() ? "seed" : showSettlements ? "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 | +/-",
|
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"));
|
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
|
||||||
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
||||||
|
|||||||
@ -65,6 +65,8 @@ void Planet::buildGeometry() {
|
|||||||
sCivCond.clear(); sCivDrought.clear();
|
sCivCond.clear(); sCivDrought.clear();
|
||||||
nations.clear(); sCellNation.assign(cells.size(), -1); sSettleNation.clear();
|
nations.clear(); sCellNation.assign(cells.size(), -1); sSettleNation.clear();
|
||||||
cultures.clear(); sCellCulture.assign(cells.size(), -1); sSettleCulture.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();
|
wars.clear(); sSettleAllegiance.clear(); diplomacy.clear();
|
||||||
sWarRng = cfg.seed ? (cfg.seed ^ 0x5A7B0A11u) : 0x5A7B0A11u; sWarNextId = 1;
|
sWarRng = cfg.seed ? (cfg.seed ^ 0x5A7B0A11u) : 0x5A7B0A11u; sWarNextId = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include "PlanetNation.hpp" // Nation, NationTier, GovType
|
#include "PlanetNation.hpp" // Nation, NationTier, GovType
|
||||||
#include "PlanetCulture.hpp" // Culture, CultureEthos, Faith
|
#include "PlanetCulture.hpp" // Culture, CultureEthos, Faith
|
||||||
#include "PlanetConflict.hpp" // WarEvent, ConflictUpdate (War is in PlanetTypes.hpp)
|
#include "PlanetConflict.hpp" // WarEvent, ConflictUpdate (War is in PlanetTypes.hpp)
|
||||||
|
#include "PlanetTrade.hpp" // TradeKind, TradeLink
|
||||||
#include "PlanetEcoregions.hpp" // Ecoregion
|
#include "PlanetEcoregions.hpp" // Ecoregion
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@ -228,6 +229,15 @@ public:
|
|||||||
const std::vector<DiploTie>& diploList() const { return diplomacy; }
|
const std::vector<DiploTie>& diploList() const { return diplomacy; }
|
||||||
DiploKind diploBetween(int nationA, int nationB) const; // relation kind between two nation indices
|
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?
|
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
|
// Per-settlement live conditions (derived each stepCivilization; not saved). condition = the combined
|
||||||
// environmental multiplier on carrying capacity (1 = normal, <1 = hardship, >1 = boom); drought =
|
// 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.
|
// 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
|
// Cultures (derived from settlements + geography; not saved). sCellCulture: culture index per cell
|
||||||
// (-1 = none/wilderness/ocean); sSettleCulture: culture index per settlement.
|
// (-1 = none/wilderness/ocean); sSettleCulture: culture index per settlement.
|
||||||
std::vector<int> sCellCulture, sSettleCulture;
|
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
|
// 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.
|
// settlement index if conquered, else -1 (independent). Separate war RNG + a stable war-id counter.
|
||||||
std::vector<int> sSettleAllegiance;
|
std::vector<int> sSettleAllegiance;
|
||||||
|
|||||||
@ -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 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 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;
|
sCivCond[k] = cond; sCivDrought[k] = drought;
|
||||||
|
|
||||||
// Storms over the town kill people directly (acute), hurricanes worst. Storms are snapshotted, so
|
// Storms over the town kill people directly (acute), hurricanes worst. Storms are snapshotted, so
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
// --- Civilization Steps 5-6: conflict, war & diplomacy -----------------------
|
// --- Civilization Steps 5-6: conflict, war & diplomacy -----------------------
|
||||||
@ -180,6 +181,18 @@ ConflictUpdate Planet::stepConflict(long year) {
|
|||||||
}
|
}
|
||||||
wars.swap(keep);
|
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) --
|
// --- 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)
|
{ // drop ties whose endpoints are gone (kept dormant across a temporary capital swap otherwise)
|
||||||
std::vector<DiploTie> kd; kd.reserve(diplomacy.size());
|
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);
|
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 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);
|
DiploTie& t = tieRef(ca, cb);
|
||||||
double drift = cfg.diploDriftRate * (target - t.attitude);
|
double drift = cfg.diploDriftRate * (target - t.attitude);
|
||||||
if (realmsAtWar(A, B)) drift -= cfg.diploWarPenalty * cfg.diploDriftRate * 4.0; // war sours relations
|
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);
|
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);
|
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 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});
|
cands.push_back({A, B, hostility});
|
||||||
}
|
}
|
||||||
std::sort(cands.begin(), cands.end(), [](const Cand& x, const Cand& y) { return x.hostility > y.hostility; });
|
std::sort(cands.begin(), cands.end(), [](const Cand& x, const Cand& y) { return x.hostility > y.hostility; });
|
||||||
|
|||||||
@ -60,6 +60,8 @@
|
|||||||
D(warConquerScore) D(warSackChance) D(warExhaustion) D(warRevoltRate) D(warMinRealmPop) \
|
D(warConquerScore) D(warSackChance) D(warExhaustion) D(warRevoltRate) D(warMinRealmPop) \
|
||||||
D(diploDriftRate) D(diploAffinity) D(diploWarPenalty) D(diploTruceYears) D(diploWarGrudge) \
|
D(diploDriftRate) D(diploAffinity) D(diploWarPenalty) D(diploTruceYears) D(diploWarGrudge) \
|
||||||
D(diploAllyThreshold) D(diploNonAggThreshold) D(diploRivalThreshold) \
|
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(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
||||||
@ -309,6 +311,15 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.diploAllyThreshold, -1.0, 1.0, "diploAllyThreshold"));
|
E(rng(cfg.diploAllyThreshold, -1.0, 1.0, "diploAllyThreshold"));
|
||||||
E(rng(cfg.diploNonAggThreshold, -1.0, 1.0, "diploNonAggThreshold"));
|
E(rng(cfg.diploNonAggThreshold, -1.0, 1.0, "diploNonAggThreshold"));
|
||||||
E(rng(cfg.diploRivalThreshold, -1.0, 1.0, "diploRivalThreshold"));
|
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.subdivisions, 0, 7, "subdivisions"));
|
||||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||||
|
|||||||
@ -22,6 +22,7 @@ void Planet::computeTerritory() {
|
|||||||
const int n = (int)cells.size();
|
const int n = (int)cells.size();
|
||||||
nations.clear();
|
nations.clear();
|
||||||
sCellNation.assign(n, -1);
|
sCellNation.assign(n, -1);
|
||||||
|
sCellSettleOwner.assign(n, -1);
|
||||||
sSettleNation.assign(settlements.size(), -1);
|
sSettleNation.assign(settlements.size(), -1);
|
||||||
if (settlements.empty()) return;
|
if (settlements.empty()) return;
|
||||||
|
|
||||||
@ -106,6 +107,6 @@ void Planet::computeTerritory() {
|
|||||||
double score = range[k] - d;
|
double score = range[k] - d;
|
||||||
if (score > bestScore) { bestScore = score; owner = (int)k; }
|
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
73
src/sim/PlanetTrade.cpp
Normal 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
17
src/sim/PlanetTrade.hpp
Normal 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;
|
||||||
|
};
|
||||||
@ -478,4 +478,15 @@ struct PlanetConfig {
|
|||||||
double diploAllyThreshold = 0.5; // attitude at/above which two realms are allied
|
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 diploNonAggThreshold = 0.2; // attitude at/above which they sign a non-aggression pact
|
||||||
double diploRivalThreshold = -0.5; // attitude at/below which they become rivals
|
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
151
test_trade.cpp
Normal 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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user