Group settlements into realms (city-states / kingdoms / empires) and give them
territory + political borders, all derived deterministically from the saved
settlement set (no save-version bump, step-back free).
- PlanetNation.{hpp,cpp}: computeTerritory() — size-scaled influence range per
settlement, realm grouping (a town joins the nearest larger capital within its
annexation reach, else founds its own nation), tier by member count / total pop,
and per-cell ownership maximising range - angular-distance (wilderness frontiers
where no settlement reaches). No RNG → tectonic stream untouched.
- Territory colour mode + nationColor, buildNationBorders (plate dual-contour
reused), realm labels at capitals, a Realms info tab, cell-info realm line, and
kind=4 WorldEvents (realm founded / rises to empire / collapsed). Key P toggles
the view + borders; territory recomputed once per sim year, on placement, load
and step-back.
- civTerritory*/civVassalRange/civEmpire* config knobs (self-describing → no save
break); test_nation.cpp covers ownership/wilderness, empires vs city-states,
realm grouping, tiers, determinism, RNG isolation, save/load parity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
7.3 KiB
C++
142 lines
7.3 KiB
C++
// Headless test for civilization Step 3 (territory & nations / realms). No display needed.
|
|
//
|
|
// g++ -std=c++17 -O2 -Isrc/sim test_nation.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/PlanetIO.cpp -o /tmp/tn && /tmp/tn
|
|
//
|
|
// Verifies: territory ownership + wilderness; bigger cities own more; realm grouping (kingdom vs
|
|
// city-state); tiers; ocean/ice unowned; determinism + RNG isolation; save->load->recompute parity.
|
|
|
|
#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();
|
|
}
|
|
static int ownedCells(const Planet& p, int nationIdx) {
|
|
int c = 0; for (int v : p.cellNation()) if (v == nationIdx) ++c; return c;
|
|
}
|
|
|
|
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 sea = p.cfg.seaLevel, yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
|
p.placeSettlements();
|
|
// run civilization a while so populations diverge (capitals, towns)
|
|
double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
|
|
|
|
std::printf("Territory: extraction\n");
|
|
p.computeTerritory();
|
|
check(!p.nationList().empty(), "computeTerritory produces nations");
|
|
check((int)p.cellNation().size() == n && (int)p.settleNation().size() == (int)p.settlements.size(), "index arrays sized");
|
|
|
|
bool seaUnowned = true, ownedIsLand = true, idxOk = true, settleMatch = true;
|
|
int ownedLand = 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
int ni = p.cellNation()[i];
|
|
if (ni >= (int)p.nationList().size()) idxOk = false;
|
|
if (ni >= 0) {
|
|
++ownedLand;
|
|
if (p.cells[i].elevation <= sea || p.cells[i].biome == Biome::Ice) ownedIsLand = false;
|
|
}
|
|
if ((p.cells[i].elevation <= sea) && ni >= 0) seaUnowned = false;
|
|
}
|
|
// every living settlement's home cell belongs to its own nation
|
|
for (size_t k = 0; k < p.settlements.size(); ++k) {
|
|
if (p.settlements[k].population < p.cfg.civAbandonPop) continue;
|
|
int ni = p.settleNation()[k];
|
|
if (ni < 0 || p.cellNation()[p.settlements[k].cell] != ni) settleMatch = false;
|
|
}
|
|
std::printf(" %d nations, %d owned land cells of %d\n", (int)p.nationList().size(), ownedLand, n);
|
|
check(idxOk, "per-cell nation indices in range");
|
|
check(seaUnowned, "ocean cells are unowned (wilderness)");
|
|
check(ownedIsLand, "owned cells are land + non-ice");
|
|
check(ownedLand > 0 && ownedLand < n, "some land is owned, some is wilderness (influence-limited)");
|
|
check(settleMatch, "a living settlement's home cell belongs to its own nation");
|
|
|
|
std::printf("Territory: bigger realms control more land\n");
|
|
{
|
|
double empCells = 0, csCells = 0; int empN = 0, csN = 0;
|
|
for (size_t ni = 0; ni < p.nationList().size(); ++ni) {
|
|
int cells = ownedCells(p, (int)ni);
|
|
if (p.nationList()[ni].tier == NationTier::Empire) { empCells += cells; ++empN; }
|
|
else if (p.nationList()[ni].tier == NationTier::CityState) { csCells += cells; ++csN; }
|
|
}
|
|
double empAvg = empN ? empCells / empN : 0.0, csAvg = csN ? csCells / csN : 0.0;
|
|
std::printf(" empire avg %.1f cells (%d) ; city-state avg %.1f cells (%d)\n", empAvg, empN, csAvg, csN);
|
|
check(empN == 0 || csN == 0 || empAvg > csAvg, "empires control more territory on average than city-states");
|
|
}
|
|
|
|
std::printf("Territory: realms (kingdoms / city-states / empires)\n");
|
|
{
|
|
int kingdoms = 0, cityStates = 0, empires = 0, multiMember = 0;
|
|
for (const Nation& nat : p.nationList()) {
|
|
if (nat.tier == NationTier::Empire) ++empires;
|
|
else if (nat.tier == NationTier::Kingdom) ++kingdoms;
|
|
else ++cityStates;
|
|
if (nat.members > 1) ++multiMember;
|
|
// a kingdom/empire name references its capital; city-states are bare
|
|
bool ok = !nat.name.empty();
|
|
if (nat.tier == NationTier::Kingdom && nat.name.rfind("Kingdom of ", 0) != 0) ok = false;
|
|
check(ok || nat.tier == NationTier::CityState || nat.tier == NationTier::Empire, "nation has a sensible name");
|
|
}
|
|
std::printf(" %d empires, %d kingdoms, %d city-states (%d multi-settlement realms)\n",
|
|
empires, kingdoms, cityStates, multiMember);
|
|
check(multiMember >= 1, "at least one realm groups multiple settlements (a kingdom forms)");
|
|
}
|
|
|
|
std::printf("Territory: determinism\n");
|
|
Planet q; q.generate(cfg); settle(q); drift(q, 400); q.placeSettlements();
|
|
double lt2 = 0.0; for (int yr = 0; yr < 600; ++yr) { lt2 += 2.0 * yearH; q.stepCivilization(2.0 * yearH, lt2); }
|
|
q.computeTerritory();
|
|
bool same = (q.cellNation() == p.cellNation()) && (q.nationList().size() == p.nationList().size());
|
|
check(same, "computeTerritory is deterministic");
|
|
|
|
std::printf("Territory: 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(); }
|
|
}
|
|
bool terrainSame = true;
|
|
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false;
|
|
check(terrainSame, "computeTerritory never perturbs tectonic evolution");
|
|
|
|
std::printf("Territory: save -> load -> recompute parity\n");
|
|
{
|
|
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);
|
|
check(ok, "readState accepts the v20 stream");
|
|
r.computeTerritory(); // territory is derived, not saved -- recompute on both sides must match
|
|
check(r.cellNation() == p.cellNation(), "territory recomputed after load matches (derived, not saved)");
|
|
}
|
|
|
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL NATION CHECKS PASSED\n", failures);
|
|
return failures ? 1 : 0;
|
|
}
|