// Headless test for civilization Step 6 (diplomacy, alliances & coalitions). No display needed. // // g++ -std=c++17 -O2 -Isrc/sim test_diplomacy.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/PlanetIO.cpp -o /tmp/tdip && /tmp/tdip // // Verifies: alliances + rivalries form; allies never war on each other; coalitions form (allies join a // war); wars end in truces; determinism + RNG isolation; save-v22 + snapshot 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(); } // Encourage lively diplomacy AND wars (so alliances/rivalries/coalitions/truces all get exercised). static void livelyDiplo(Planet& p) { p.cfg.diploDriftRate = 0.5; p.cfg.diploTruceYears = 20.0; p.cfg.warDeclareRate = 2.0; p.cfg.warAmbition = 2.0; p.cfg.warBorder = 1.5; p.cfg.warConquerScore = 5.0; p.cfg.warExhaustion = 1.0; // wars end by EXHAUSTION -> a truce (not conquest) p.cfg.warMinRealmPop = 1.0; p.cfg.warMaxConcurrent = 50; p.cfg.warSackChance = 0.0; } static bool tiesEqual(const std::vector& a, const std::vector& b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) if (a[i].a != b[i].a || a[i].b != b[i].b || a[i].kind != b[i].kind || a[i].truceUntil != b[i].truceUntil || std::fabs(a[i].attitude - b[i].attitude) > 1e-12) return false; return true; } // Run the full civ+diplomacy sequence into p; return via out-params what was observed. static void runWorld(Planet& p, int& maxAllies, int& maxRivals, bool& alliedAtWar, bool& sawTruce, int& joinEvents, int& maxSimulWars) { 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); } livelyDiplo(p); maxAllies = maxRivals = joinEvents = maxSimulWars = 0; alliedAtWar = false; sawTruce = false; for (long yr = 0; yr < 400; ++yr) { p.computeTerritory(); p.computeCultures(); ConflictUpdate u = p.stepConflict(yr); for (const WarEvent& e : u.events) if (e.title.find("joins the war") != std::string::npos) ++joinEvents; int nA = 0, nR = 0; for (const DiploTie& t : p.diploList()) { if (t.kind == DiploKind::Alliance) ++nA; else if (t.kind == DiploKind::Rival) ++nR; if (t.truceUntil > yr) sawTruce = true; } maxAllies = std::max(maxAllies, nA); maxRivals = std::max(maxRivals, nR); // Allies must never be in an active war with each other. for (const War& w : p.warList()) { int lo = std::min(w.attacker, w.defender), hi = std::max(w.attacker, w.defender); for (const DiploTie& t : p.diploList()) if (t.a == lo && t.b == hi && t.kind == DiploKind::Alliance) alliedAtWar = true; } // Any realm fighting on >1 front this year (coalitions produce this). for (size_t ni = 0; ni < p.nationList().size(); ++ni) { int cap = p.nationList()[ni].capital, cnt = 0; for (const War& w : p.warList()) if (w.attacker == cap || w.defender == cap) ++cnt; maxSimulWars = std::max(maxSimulWars, cnt); } } p.computeTerritory(); } 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; std::printf("Diplomacy: alliances, rivalries, allies-at-peace, truces, coalitions\n"); int maxAllies, maxRivals, joinEvents, maxSimulWars; bool alliedAtWar, sawTruce; runWorld(p, maxAllies, maxRivals, alliedAtWar, sawTruce, joinEvents, maxSimulWars); std::printf(" peak alliances %d ; peak rivalries %d ; truces seen %s ; join-events %d ; max fronts for a realm %d\n", maxAllies, maxRivals, sawTruce ? "yes" : "no", joinEvents, maxSimulWars); check(maxAllies >= 1, "alliances form between realms"); check(maxRivals >= 1, "rivalries form between realms"); check(!alliedAtWar, "allies are never at war with each other"); check(sawTruce, "a war ends in a truce (no immediate re-declaration)"); check(joinEvents > 0 || maxSimulWars >= 2, "coalitions form (an ally joins / a realm fights multiple fronts)"); std::printf("Diplomacy: determinism\n"); Planet q; q.generate(cfg); settle(q); drift(q, 400); int a2, r2, j2, m2; bool aw2, tr2; runWorld(q, a2, r2, aw2, tr2, j2, m2); check(tiesEqual(q.diploList(), p.diploList()), "diplomacy is deterministic (identical ties)"); std::printf("Diplomacy: 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(); livelyDiplo(y); y.stepConflict(0); } } { 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, "diplomacy never perturbs tectonic evolution"); } std::printf("Diplomacy: save v22 round-trip\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, true, true); check(ok, "readState accepts the v22 stream"); check(tiesEqual(r.diploList(), p.diploList()), "diplomacy survives save/load"); } { Planet bad = p; bad.diplomacy.clear(); DiploTie t; t.a = 0; t.b = (int)bad.settlements.size() + 10; bad.diplomacy.push_back(t); std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary); bad.writeState(ss); Planet r; check(!r.readState(ss, true, true, true, true, true, true, true, true, true, true, true, true, true), "readState rejects diplomacy ties with invalid settlement endpoints"); } if (p.settlements.size() >= 2) { Planet odd = p; odd.diplomacy.clear(); DiploTie t; t.a = 1; t.b = 0; t.attitude = 99.0; t.kind = (DiploKind)255; odd.diplomacy.push_back(t); std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary); odd.writeState(ss); Planet r; bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true, true, true); bool sanitized = ok && r.diploList().size() == 1 && r.diploList()[0].a == 0 && r.diploList()[0].b == 1 && std::fabs(r.diploList()[0].attitude - 1.0) < 1e-12 && r.diploList()[0].kind == DiploKind::Neutral; check(sanitized, "readState canonicalizes and clamps salvageable diplomacy ties"); } std::printf("Diplomacy: step-back snapshot round-trip\n"); { WeatherSnapshot snap = p.captureWeather(); std::vector before = p.diploList(); p.cfg.diploDriftRate = 1.0; for (long yr = 400; yr < 415; ++yr) { p.computeTerritory(); p.computeCultures(); p.stepConflict(yr); } bool changed = !tiesEqual(p.diploList(), before); p.restoreWeather(snap); check(changed, "the mutation actually changed the diplomacy state"); check(tiesEqual(p.diploList(), before), "restoreWeather rewinds the diplomacy (step-back)"); } std::printf(failures ? "\nFAILURES: %d\n" : "\nALL DIPLOMACY CHECKS PASSED\n", failures); return failures ? 1 : 0; }