planetsim/test_trade.cpp

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