planetsim/test_trade.cpp
Jonas Reith e27b91a128 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>
2026-07-01 13:36:41 +02:00

152 lines
8.3 KiB
C++

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