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>
158 lines
7.9 KiB
C++
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;
|
|
}
|