planetsim/test_colony.cpp
Jonas Reith cdf4319b31 Civ: colonization (kingdoms found colonies + islands) + reseed overlay cleanup
Two things:

1. Fix: pressing R (reseed) after civilization existed left stale territory/
   culture/war/diplomacy/trade overlay lines drawing on the new world.
   regenWorld() now clears the civ overlay segment vectors + their toggles and
   resets a civ colour mode back to Biome.

2. Feature: the settlement set is no longer fixed after U. stepColonization()
   runs once per sim year: a kingdom+ realm (totalPop >= civColonyMinPop) may
   found a new settlement on the best unclaimed, habitable, well-spaced cell
   within reach -- overland, or across water from a coastal member (so islands /
   other continents get colonised). The colony is APPENDED and bound to the
   founder's realm by allegiance (Step 5) + a colonial supply trade link
   (computeTrade, Step 7) so it survives on marginal land while the founder
   lives; when the founder's capital falls, allegiance clears -> supply ends ->
   the colony subsists locally and may wither. Deterministic (pure hashes).

   The growing set needs no save-format change: the v20 settlement block is
   variable-length, and restoreWeather now TRUNCATES settlements to the
   snapshot's count (step-back drops colonies founded after; they re-found
   identically on replay). Founding logs a kind=3 event. civColon* config knobs.

   (Determinism fix: within one stepColonization call the per-settlement arrays
   sSettleNation/sSettleAllegiance/sCivCond are resized on each append, else the
   next realm's member loop read out of bounds.)

test_colony.cpp: colonies founded (set grows), colony in founder's realm + a
supply link, cross-water/island colonies, founder loss frees + de-supplies,
step-back truncates, determinism, save round-trip. All 16 suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:09:22 +02:00

158 lines
7.9 KiB
C++

// Headless test for civilization colonization (kingdoms found new settlements + islands). No display.
//
// g++ -std=c++17 -O2 -Isrc/sim test_colony.cpp src/sim/IcoSphere.cpp src/sim/Planet.cpp \
// ... (the same source list as test_civ) ... src/sim/PlanetTrade.cpp src/sim/PlanetIO.cpp \
// -o /tmp/tcol && /tmp/tcol
//
// Verifies: colonies are founded (the settlement count grows); a colony belongs to its founder's realm
// (via allegiance); a colony has a supply link to its overlord + is more prosperous for it; killing the
// founder capital frees + de-supplies the colony; step-back truncates the set; determinism; save round-trip.
#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();
}
// Crank colonization so it happens readily; keep the initial set + years so realms grow into kingdoms.
static void eager(Planet& p) {
p.cfg.civColonizeRate = 0.9; p.cfg.civColonyMinPop = 1000.0; p.cfg.civColonyMinHab = 0.15;
p.cfg.civColonyReach = 0.25; p.cfg.civColonySeaReach = 0.5; p.cfg.civColonySpacing = 0.03; p.cfg.civMaxColonies = 200;
}
static void seedCiv(Planet& p, double yearH) {
p.placeSettlements();
double lt = 0.0; for (int yr = 0; yr < 400; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
eager(p);
}
// Run C years of colonization + recompute; return the settlement count reached.
static int runColonize(Planet& p, long years) {
for (long yr = 0; yr < years; ++yr) {
p.computeTerritory(); p.computeCultures(); p.computeTrade();
p.stepColonization(yr);
}
p.computeTerritory(); p.computeCultures(); p.computeTrade();
return (int)p.settlements.size();
}
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;
seedCiv(p, yearH);
int initial = (int)p.settlements.size();
std::printf("Colony: kingdoms found colonies (the settlement set grows)\n");
int grown = runColonize(p, 200);
std::printf(" settlements: %d initial -> %d after colonization\n", initial, grown);
check(grown > initial, "new colonies are founded over time");
std::printf("Colony: a colony belongs to its founder's realm + has a supply link\n");
{
// A colony = a settlement (index >= initial) whose allegiance points to a living overlord capital.
int colony = -1, overlord = -1;
for (int s = initial; s < (int)p.settlements.size(); ++s) {
int ov = p.settleAllegiance()[s];
if (ov >= 0 && ov < (int)p.settlements.size() && p.settlements[ov].population >= p.cfg.civAbandonPop) { colony = s; overlord = ov; break; }
}
check(colony >= 0, "a colony bound to a living overlord exists");
if (colony >= 0) {
int cnat = p.cellNation()[p.settlements[colony].cell];
int onat = p.settleNation()[overlord];
check(cnat == onat && cnat >= 0, "the colony's cell belongs to the founder's realm");
bool supplied = false;
for (const TradeLink& t : p.tradeLinks())
if ((t.a == colony && t.b == overlord) || (t.a == overlord && t.b == colony)) supplied = true;
check(supplied, "the colony has a supply trade link to its overlord");
check(p.prosperity()[colony] > 0.5, "the colony is prosperous (kept alive by supply)");
}
}
std::printf("Colony: island / cross-water colonies appear\n");
{
// A colony whose nearest OWN-REALM settlement is separated by ocean (a great-circle sample hits sea).
auto crossesSea = [&](int ca, int cb) {
for (int i = 1; i < 12; ++i) {
double u = i / 12.0;
Vec3 m = (p.cells[ca].unit * (1.0 - u) + p.cells[cb].unit * u).normalized();
double best = -2; int bc = 0;
for (int c = 0; c < n; ++c) { double d = p.cells[c].unit.dot(m); if (d > best) { best = d; bc = c; } }
if (p.cells[bc].elevation <= p.cfg.seaLevel) return true;
}
return false;
};
int island = 0;
for (int s = initial; s < (int)p.settlements.size(); ++s) {
int ov = p.settleAllegiance()[s]; if (ov < 0) continue;
if (crossesSea(p.settlements[s].cell, p.settlements[ov].cell)) { ++island; }
}
std::printf(" %d colonies are across water from their overlord\n", island);
check(island > 0, "at least one colony was founded across the sea (island / abroad)");
}
std::printf("Colony: losing the founder frees + de-supplies the colony\n");
{
int colony = -1, overlord = -1;
for (int s = initial; s < (int)p.settlements.size(); ++s) {
int ov = p.settleAllegiance()[s];
if (ov >= 0 && p.settlements[ov].population >= p.cfg.civAbandonPop) { colony = s; overlord = ov; break; }
}
if (colony < 0) { check(true, "(no living-overlord colony -- skipped)"); }
else {
double prospBefore = p.prosperity()[colony];
p.settlements[overlord].population = 0.0; // the founder capital falls
p.stepConflict(9999); // Step-5 pruning clears the dead overlord's allegiance
p.computeTerritory(); p.computeCultures(); p.computeTrade();
bool freed = p.settleAllegiance()[colony] < 0;
std::printf(" colony prosperity %.2f -> %.2f (freed: %s)\n", prospBefore, p.prosperity()[colony], freed ? "yes" : "no");
check(freed, "the colony is freed when its founder collapses");
check(p.prosperity()[colony] < prospBefore, "the colony loses its supply prosperity");
}
}
std::printf("Colony: step-back truncates the settlement set\n");
{
Planet q; q.generate(cfg); settle(q); drift(q, 400); seedCiv(q, yearH);
int base = (int)q.settlements.size();
q.computeTerritory(); q.computeCultures(); q.computeTrade();
WeatherSnapshot snap = q.captureWeather(); // capture BEFORE colonizing
int after = runColonize(q, 120);
check(after > base, "colonies were founded after the snapshot");
q.restoreWeather(snap);
check((int)q.settlements.size() == base, "restoreWeather truncates back to the pre-colony count");
}
std::printf("Colony: determinism + save round-trip\n");
{
Planet a; a.generate(cfg); settle(a); drift(a, 400); seedCiv(a, yearH); int na = runColonize(a, 150);
Planet b; b.generate(cfg); settle(b); drift(b, 400); seedCiv(b, yearH); int nb = runColonize(b, 150);
bool same = (na == nb);
if (same) for (int s = 0; s < na; ++s) if (a.settlements[s].cell != b.settlements[s].cell) same = false;
check(same, "colonization is deterministic (same colonies)");
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
a.writeState(ss);
Planet r;
bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true, true, true);
check(ok && (int)r.settlements.size() == na, "save/load round-trips the grown settlement set");
}
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL COLONY CHECKS PASSED\n", failures);
return failures ? 1 : 0;
}