Harden save/load state validation

This commit is contained in:
Jonas Reith 2026-07-01 20:32:02 +02:00
parent 99f4a925a1
commit 1090d2d46e
20 changed files with 270 additions and 96 deletions

View File

@ -84,11 +84,13 @@ CLI flags (applied before the first load/generate):
planet.cfg human-editable key=value config of every PlanetConfig parameter;
auto-created on first run, reload live with F2. Range-checked on
load; an invalid file reverts to safe defaults (not overwritten).
planet.save binary snapshot (versioned, currently v17: +geography/atlas; v16
+event log; v15 +stateful
volcanoes; v13 +Live World clock rate; v12 +step-back history
(~40 frames, so a load can rewind storms); v11 +weather systems/storms;
v10 +weather fields; v9 +moons; v8 +Live World clock; v7 +biota): seed + config + full planet state; F5
planet.save binary snapshot (versioned, currently v22: +diplomacy; v21
+conflict/wars; v20 +settlements; v19 +ecoregions; v18
+geography salt; v17 +geography/atlas; v16 +event log; v15
+stateful volcanoes; v13 +Live World clock rate; v12
+step-back history (~40 frames, so a load can rewind storms);
v11 +weather systems/storms; v10 +weather fields; v9 +moons;
v8 +Live World clock; v7 +biota): seed + config + full planet state; F5
writes it, F9 reloads and resumes deterministically. As of v6
the config is stored as a self-describing key=value block (like
planet.cfg), so adding/removing config fields no longer breaks saves

View File

@ -5,6 +5,8 @@
#include <cmath>
#include <cstring>
#include <fstream>
#include <set>
#include <utility>
namespace {
const char* weatherEventName(const WeatherSystem& ws, const Planet& p) {
@ -17,6 +19,42 @@ namespace {
int eventSeverityForWeather(const WeatherSystem& ws, const Planet& p) {
return (ws.tropical && ws.strength >= p.cfg.weatherHurricaneStr) ? 2 : (ws.tropical ? 1 : 0);
}
bool validFrameWar(const War& w, int settlementCount) {
return w.attacker >= 0 && w.attacker < settlementCount
&& w.defender >= 0 && w.defender < settlementCount
&& w.attacker != w.defender
&& w.battles >= 0
&& std::isfinite(w.warscore);
}
bool sanitizeFrameDiplomacy(std::vector<DiploTie>& ties, int settlementCount) {
std::set<std::pair<int, int>> seen;
for (DiploTie& t : ties) {
if (t.a < 0 || t.a >= settlementCount || t.b < 0 || t.b >= settlementCount || t.a == t.b)
return false;
if (t.a > t.b) std::swap(t.a, t.b);
if (!seen.insert({t.a, t.b}).second) return false;
if (!std::isfinite(t.attitude)) return false;
t.attitude = std::clamp(t.attitude, -1.0, 1.0);
if ((uint8_t)t.kind > (uint8_t)DiploKind::Rival) t.kind = DiploKind::Neutral;
}
return true;
}
bool validateFrameCivState(WeatherSnapshot& w, size_t currentSettlements) {
if (w.settlementPop.size() > currentSettlements) return false;
for (double p : w.settlementPop) if (!std::isfinite(p)) return false;
const int settlementCount = (int)w.settlementPop.size();
if (!w.settlementAllegiance.empty()) {
if ((int)w.settlementAllegiance.size() != settlementCount) return false;
for (int a : w.settlementAllegiance)
if (a < -1 || a >= settlementCount) return false;
}
std::set<std::pair<int, int>> warsSeen;
for (const War& war : w.wars) {
if (!validFrameWar(war, settlementCount)) return false;
if (!warsSeen.insert(std::minmax(war.attacker, war.defender)).second) return false;
}
return sanitizeFrameDiplomacy(w.diplomacy, settlementCount);
}
}
bool Viewer::init(int argc, char** argv) {
@ -166,21 +204,21 @@ void Viewer::recolor() {
? habitabilityColor(hab[i]) : Color{30, 42, 64, 255}; // ocean/ice: dim blue
break;
case ColorMode::Territory: {
int ni = (i < (int)cnat.size()) ? cnat[i] : -1;
int ni = (i < cnat.size()) ? cnat[i] : -1;
if (ni >= 0) vcolors[i] = nationColor(ni); // owned: realm tint
else vcolors[i] = (planet.cells[i].elevation > planet.cfg.seaLevel) // wilderness land vs sea
? Color{60, 64, 58, 255} : Color{26, 34, 52, 255};
break;
}
case ColorMode::Culture: {
int ci = (i < (int)ccult.size()) ? ccult[i] : -1;
int ci = (i < ccult.size()) ? ccult[i] : -1;
if (ci >= 0) vcolors[i] = cultureColor(ci); // owned: culture tint
else vcolors[i] = (planet.cells[i].elevation > planet.cfg.seaLevel) // uncultured land vs sea
? Color{60, 64, 58, 255} : Color{26, 34, 52, 255};
break;
}
case ColorMode::Wealth: {
double w = (i < (int)cwealth.size()) ? cwealth[i] : 0.0;
double w = (i < cwealth.size()) ? cwealth[i] : 0.0;
if (w > 0.0) vcolors[i] = wealthColor(w / wealthMax); // owned land: trade prosperity
else vcolors[i] = (planet.cells[i].elevation > planet.cfg.seaLevel) // wilderness land vs sea
? Color{40, 42, 46, 255} : Color{22, 30, 44, 255};
@ -428,7 +466,9 @@ void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
// -> realm foundings, tier rises (to a kingdom/empire), and collapses (capital lost/absorbed).
void Viewer::detectNationEvents(const std::vector<Nation>& before) {
auto byCapital = [](const std::vector<Nation>& v, int cap) -> const Nation* {
for (const Nation& nn : v) if (nn.capital == cap) return &nn; return nullptr;
for (const Nation& nn : v)
if (nn.capital == cap) return &nn;
return nullptr;
};
for (const Nation& nat : planet.nationList()) {
if (nat.capital < 0 || nat.capital >= (int)planet.settlements.size()) continue;
@ -643,6 +683,7 @@ void Viewer::loadGame(const char* path) {
|| !std::isfinite(v.activity) || !std::isfinite(v.baseElev)
|| !std::isfinite(v.built) || !std::isfinite(v.timer)
|| !std::isfinite(v.ashTimer) || !std::isfinite(v.ashCarry)) historyOk = false;
if (!validateFrameCivState(f.w, planet.settlements.size())) historyOk = false;
if (historyOk) wxUndo.push_back(std::move(f));
}
if (!historyOk) { wxUndo.clear(); skippedHistory = true; }

View File

@ -288,6 +288,11 @@ private:
uint32_t rnd();
double rndf(); // [0,1)
void buildGeometry(); // build icosphere + per-cell unit/neighbors
bool readStateImpl(std::istream& is, bool hasBiome, bool hasBiota,
bool hasMoons, bool hasWeather, bool hasStorms,
bool hasVolcanoes, bool hasStatefulVolcanoes, bool hasGeography,
bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
bool hasConflict, bool hasDiplo);
void clearDerivedState(); // clear geometry-dependent scratch/derived fields
void assignPlates();
void seedInitialRelief();

View File

@ -217,7 +217,7 @@ CivUpdate Planet::stepCivilization(double dtHours, double liveTime) {
double siteQ = std::clamp(0.45 + cfg.civSiteVariety * ((coast ? 1.2 : 0.0) + riverQ), 0.3, 6.0);
double cond = harvest * droughtFactor * coldFactor * floodFactor * ashFactor;
double prosp = (k < sProsperity.size()) ? sProsperity[k] : 0.0; // civ Step 7: trade wealth boosts capacity
double prosp = ((size_t)k < sProsperity.size()) ? sProsperity[(size_t)k] : 0.0; // civ Step 7: trade wealth boosts capacity
double K = cfg.civMaxPopulation * hab * siteQ * cond * (1.0 + cfg.tradeProsperityWeight * prosp);
sCivCond[k] = cond; sCivDrought[k] = drought;

View File

@ -277,7 +277,9 @@ std::string Planet::nameNewLand(int cell) {
uint32_t seed = geoSeed + (uint32_t)(cell * 2654435761u)
+ (uint32_t)geoFeatures.size() * 40503u;
auto inUse = [&](const std::string& nm) {
for (const GeoFeature& f : geoFeatures) if (f.name == nm) return true; return false;
for (const GeoFeature& f : geoFeatures)
if (f.name == nm) return true;
return false;
};
std::string nm = namegen::makeName(seed, bank);
for (int g = 0; inUse(nm) && g < 128; ++g) nm = namegen::makeName(seed += 0x9E3779B9u, bank);

View File

@ -8,6 +8,8 @@
#include <fstream>
#include <sstream>
#include <limits>
#include <set>
#include <utility>
// --- Config file (text) + save/load (binary) --------------------------------
@ -368,6 +370,16 @@ std::string validateConfig(const PlanetConfig& cfg) {
bad.push_back("volcanoDormantMinYears > volcanoDormantMaxYears");
if (cfg.volcanoAshMinYears > cfg.volcanoAshMaxYears)
bad.push_back("volcanoAshMinYears > volcanoAshMaxYears");
if (!(cfg.biomeIceTemp < cfg.biomeTundraTemp && cfg.biomeTundraTemp < cfg.biomeTaigaTemp))
bad.push_back("biome temperature thresholds must satisfy Ice < Tundra < Taiga");
if (cfg.biomeHillsElev >= cfg.biomeMountainElev)
bad.push_back("biomeHillsElev >= biomeMountainElev");
if (cfg.civAbandonPop >= cfg.civTownPop || cfg.civTownPop >= cfg.civCityPop)
bad.push_back("civilization population thresholds must satisfy Abandon < Town < City");
if (cfg.civSeedPopulation <= cfg.civAbandonPop)
bad.push_back("civSeedPopulation <= civAbandonPop");
if (!(cfg.diploRivalThreshold < cfg.diploNonAggThreshold && cfg.diploNonAggThreshold < cfg.diploAllyThreshold))
bad.push_back("diplomacy thresholds must satisfy Rival < NonAggression < Alliance");
if (bad.empty()) return {};
std::string msg = "Bad config:";
@ -412,6 +424,34 @@ namespace {
bool validBiomeByte(uint8_t b) {
return b <= (uint8_t)Biome::Mountains;
}
bool validWarRecord(const War& w, int settlementCount) {
return w.attacker >= 0 && w.attacker < settlementCount
&& w.defender >= 0 && w.defender < settlementCount
&& w.attacker != w.defender
&& w.battles >= 0
&& std::isfinite(w.warscore);
}
bool validateWars(std::vector<War>& wars, int settlementCount) {
std::set<std::pair<int, int>> seen;
for (const War& w : wars) {
if (!validWarRecord(w, settlementCount)) return false;
auto key = std::minmax(w.attacker, w.defender);
if (!seen.insert(key).second) return false;
}
return true;
}
bool sanitizeDiplo(std::vector<DiploTie>& ties, int settlementCount) {
std::set<std::pair<int, int>> seen;
for (DiploTie& t : ties) {
if (t.a < 0 || t.a >= settlementCount || t.b < 0 || t.b >= settlementCount || t.a == t.b)
return false;
if (t.a > t.b) std::swap(t.a, t.b);
if (!seen.insert({t.a, t.b}).second) return false;
if (!std::isfinite(t.attitude)) return false;
t.attitude = std::clamp(t.attitude, -1.0, 1.0);
}
return true;
}
}
// Full simulation state. Geometry (unit/neighbors) is NOT stored -- it is rebuilt
@ -519,6 +559,20 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
bool hasConflict, bool hasDiplo) {
Planet tmp;
tmp.drifting = drifting; // readState never serialized this flag; preserve old caller-visible behavior.
if (!tmp.readStateImpl(is, hasBiome, hasBiota, hasMoons, hasWeather, hasStorms, hasVolcanoes,
hasStatefulVolcanoes, hasGeography, hasGeoSalt, hasEcoregions,
hasSettlements, hasConflict, hasDiplo))
return false;
*this = std::move(tmp);
return true;
}
bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
bool hasConflict, bool hasDiplo) {
// Save blocks are append-only by version. If a caller asks for an older prefix,
// later blocks cannot exist in that stream even if the default arguments say otherwise.
if (!hasBiota) { hasMoons = false; hasWeather = false; hasStorms = false; hasVolcanoes = false; }
@ -759,6 +813,7 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
if (w.attacker < 0 || w.attacker >= (int)settlements.size()
|| w.defender < 0 || w.defender >= (int)settlements.size()) return false;
}
if (!validateWars(wars, (int)settlements.size())) return false;
readPod(is, sWarRng); readPod(is, sWarNextId);
if (!sWarRng) sWarRng = cfg.seed ? (cfg.seed ^ 0x5A7B0A11u) : 0x5A7B0A11u;
if (!sWarNextId) sWarNextId = 1;
@ -776,6 +831,7 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
if (t.a < 0 || t.a >= (int)settlements.size()
|| t.b < 0 || t.b >= (int)settlements.size()) return false;
}
if (!sanitizeDiplo(diplomacy, (int)settlements.size())) return false;
}
computeBiotaDensity(); // derived density scalars for the colour views
return (bool)is;

View File

@ -1,11 +1,11 @@
// Headless logic test for the Biota stage (flora / fauna / funga). No display.
//
// g++ -std=c++17 -O2 -Isrc/sim test_biota.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
// g++ -std=c++17 -O2 -Isrc/sim test_biota.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp
// src/sim/PlanetIO.cpp -o /tmp/tb && /tmp/tb
//
// Verifies: density ranges, zero life under ice + marine flora/fauna present in the
@ -95,8 +95,12 @@ int main() {
const auto& B = p.biota();
bool slotsOk = true, pointsOk = true, carnGated = true, iceEmpty = true, oceanFungaEmpty = true;
bool anyMarineOrg = false;
auto cost = [&](const std::vector<Organism>& v) { int s = 0; const auto& AR = biotaArchetypes();
for (auto& o : v) s += pointCost(AR[o.archetype].size); return s; };
auto cost = [&](const std::vector<Organism>& v) {
int s = 0;
const auto& AR = biotaArchetypes();
for (auto& o : v) s += pointCost(AR[o.archetype].size);
return s;
};
for (int i = 0; i < n; ++i) {
const CellBiota& cb = B[i];
if (p.cells[i].biome == Biome::Ice) { // ice (land or frozen sea): empty

View File

@ -1,12 +1,12 @@
// Headless test for civilization Step 2 (habitability + settlements). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_civ.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 \
// g++ -std=c++17 -O2 -Isrc/sim test_civ.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/PlanetIO.cpp -o /tmp/tc && /tmp/tc
//
// Verifies: habitability range/zeros; placement spacing/cap/land + unique names; food-driven growth

View File

@ -1,7 +1,7 @@
// 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 \
// 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

View File

@ -1,13 +1,13 @@
// Headless test for civilization Step 5 (conflict, war & shifting borders). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_conflict.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 \
// g++ -std=c++17 -O2 -Isrc/sim test_conflict.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/twar && /tmp/twar
//
// Verifies: wars start between neighbouring realms; conquest sets allegiance so computeTerritory moves
@ -146,6 +146,30 @@ int main() {
r.computeTerritory();
check(r.cellNation() == p2.cellNation(), "territory recomputed after load matches (allegiance honoured)");
}
{
Planet bad = p2;
bad.wars.clear();
War w; w.attacker = 0; w.defender = (int)bad.settlements.size() + 10;
bad.wars.push_back(w);
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),
"readState rejects active wars with invalid settlement endpoints");
}
if (p2.settlements.size() >= 2) {
Planet bad = p2;
bad.wars.clear();
War a; a.attacker = 0; a.defender = 1;
War b; b.attacker = 1; b.defender = 0;
bad.wars.push_back(a);
bad.wars.push_back(b);
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),
"readState rejects duplicate active wars for the same realm pair");
}
std::printf("Conflict: step-back snapshot round-trip\n");
{

View File

@ -1,12 +1,12 @@
// Headless test for civilization Step 4 (cultures, beliefs & governments). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_culture.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 \
// g++ -std=c++17 -O2 -Isrc/sim test_culture.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/PlanetIO.cpp -o /tmp/tc && /tmp/tc
//
// Verifies: one culture per inhabited continent; every living settlement has a culture; valid

View File

@ -1,13 +1,13 @@
// 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 \
// 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
@ -127,6 +127,32 @@ int main() {
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");
{

View File

@ -1,11 +1,11 @@
// Headless test for the geography / atlas stage (named feature extraction + naming). No display.
//
// g++ -std=c++17 -O2 -Isrc/sim test_geography.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 \
// g++ -std=c++17 -O2 -Isrc/sim test_geography.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/PlanetIO.cpp -o /tmp/tg && /tmp/tg
//
// Verifies: extraction (continents/oceans/ranges/rivers/lakes), per-cell membership consistency,

View File

@ -1,11 +1,11 @@
// Headless test for the Live World stage (insolation + live seasonal temperature).
// No display / raylib needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_live.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
// g++ -std=c++17 -O2 -Isrc/sim test_live.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp
// src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tl && /tmp/tl
//
// Verifies: insolation range; the sub-solar hemisphere is lit and the night side dark;

View File

@ -1,8 +1,8 @@
// Headless logic test for Phase 1 tectonics. No display / raylib needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_logic.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/PlanetIO.cpp \
// g++ -std=c++17 -O2 -Isrc/sim test_logic.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/PlanetIO.cpp
// -o /tmp/t && /tmp/t
//
// Verifies the invariants documented in CLAUDE.md so Planet::step() and the
@ -234,6 +234,20 @@ int main() {
finishManualState(badPlate);
Planet rp;
check(!rp.readState(badPlate), "readState() rejects invalid cell plate ids");
Planet victim; victim.generate(cfg);
const size_t beforeCells = victim.cells.size();
const int beforeSubdiv = victim.cfg.subdivisions;
const double beforeElev = victim.cells.empty() ? 0.0 : victim.cells[0].elevation;
std::stringstream badAfterBuild(std::ios::in | std::ios::out | std::ios::binary);
writeManualStatePrefix(badAfterBuild, "subdivisions = 0\nplateCount = 1\n", 77);
finishManualState(badAfterBuild);
bool rejected = !victim.readState(badAfterBuild);
bool unchanged = victim.cells.size() == beforeCells
&& victim.cfg.subdivisions == beforeSubdiv
&& !victim.cells.empty()
&& victim.cells[0].elevation == beforeElev;
check(rejected && unchanged, "failed readState() leaves the existing planet unchanged");
}
std::printf("\n%s (%d failure%s)\n",

View File

@ -1,12 +1,12 @@
// Headless test for civilization Step 3 (territory & nations / realms). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_nation.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 \
// g++ -std=c++17 -O2 -Isrc/sim test_nation.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/PlanetIO.cpp -o /tmp/tn && /tmp/tn
//
// Verifies: territory ownership + wilderness; bigger cities own more; realm grouping (kingdom vs

View File

@ -1,11 +1,11 @@
// Headless test for the Live World ocean/sky stage (moons + tides). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_ocean.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp \
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp \
// g++ -std=c++17 -O2 -Isrc/sim test_ocean.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp
// -o /tmp/to && /tmp/to
//
// Verifies: 1-3 moons generated deterministically; moon/sun directions are unit vectors that

View File

@ -1,13 +1,13 @@
// Headless test for civilization Step 7 (trade & economy). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_trade.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 \
// g++ -std=c++17 -O2 -Isrc/sim test_trade.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/PlanetTrade.cpp src/sim/PlanetIO.cpp -o /tmp/ttr && /tmp/ttr
//
// Verifies: trade links form (coastal pairs over a longer sea range); hubs are richer than isolated

View File

@ -1,11 +1,11 @@
// Headless test for Live World volcanoes (stateful growth, dormancy, explosions, ash,
// rewind snapshots and save/load). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_volcano.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 \
// g++ -std=c++17 -O2 -Isrc/sim test_volcano.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/PlanetIO.cpp -o /tmp/tv && /tmp/tv
#include "Planet.hpp"

View File

@ -1,10 +1,10 @@
// Headless test for the Live World weather cycle (humidity / cloud / rain). No display needed.
//
// g++ -std=c++17 -O2 -Isrc/sim test_weather.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
// g++ -std=c++17 -O2 -Isrc/sim test_weather.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/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp
// src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/tw && /tmp/tw
//
// Verifies: fields stay in range; oceans (the evaporation source) end up moister than land;