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