planetsim/test_diplomacy.cpp

172 lines
8.9 KiB
C++

// 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 <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();
}
// 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<DiploTie>& a, const std::vector<DiploTie>& 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<DiploTie> 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;
}