diff --git a/BUILD.md b/BUILD.md index 7455d0e..7283288 100644 --- a/BUILD.md +++ b/BUILD.md @@ -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 diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index 4207e30..91596b3 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include 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& ties, int settlementCount) { + std::set> 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> 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& beforeStorms, // -> realm foundings, tier rises (to a kingdom/empire), and collapses (capital lost/absorbed). void Viewer::detectNationEvents(const std::vector& before) { auto byCapital = [](const std::vector& 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; } diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index e120b62..929d5da 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -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(); diff --git a/src/sim/PlanetCiv.cpp b/src/sim/PlanetCiv.cpp index 1e5129f..deb358f 100644 --- a/src/sim/PlanetCiv.cpp +++ b/src/sim/PlanetCiv.cpp @@ -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; diff --git a/src/sim/PlanetGeography.cpp b/src/sim/PlanetGeography.cpp index 6a81257..a3d287f 100644 --- a/src/sim/PlanetGeography.cpp +++ b/src/sim/PlanetGeography.cpp @@ -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); diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 95d5936..3ed055d 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include // --- 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& wars, int settlementCount) { + std::set> 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& ties, int settlementCount) { + std::set> 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; diff --git a/test_biota.cpp b/test_biota.cpp index 82c09ef..75b0a3c 100644 --- a/test_biota.cpp +++ b/test_biota.cpp @@ -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& 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& 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 diff --git a/test_civ.cpp b/test_civ.cpp index 734fb0c..2cfe206 100644 --- a/test_civ.cpp +++ b/test_civ.cpp @@ -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 diff --git a/test_colony.cpp b/test_colony.cpp index a56327b..789b685 100644 --- a/test_colony.cpp +++ b/test_colony.cpp @@ -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 diff --git a/test_conflict.cpp b/test_conflict.cpp index 766d749..b315f65 100644 --- a/test_conflict.cpp +++ b/test_conflict.cpp @@ -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"); { diff --git a/test_culture.cpp b/test_culture.cpp index 8b3042f..dcf95db 100644 --- a/test_culture.cpp +++ b/test_culture.cpp @@ -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 diff --git a/test_diplomacy.cpp b/test_diplomacy.cpp index 2a97139..2fd4743 100644 --- a/test_diplomacy.cpp +++ b/test_diplomacy.cpp @@ -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"); { diff --git a/test_geography.cpp b/test_geography.cpp index c7912d2..5430e0b 100644 --- a/test_geography.cpp +++ b/test_geography.cpp @@ -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, diff --git a/test_live.cpp b/test_live.cpp index c67091b..a34697e 100644 --- a/test_live.cpp +++ b/test_live.cpp @@ -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; diff --git a/test_logic.cpp b/test_logic.cpp index 43c2cc7..d97b280 100644 --- a/test_logic.cpp +++ b/test_logic.cpp @@ -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", diff --git a/test_nation.cpp b/test_nation.cpp index 1f9257e..c51287e 100644 --- a/test_nation.cpp +++ b/test_nation.cpp @@ -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 diff --git a/test_ocean.cpp b/test_ocean.cpp index 34ee9bc..b358b01 100644 --- a/test_ocean.cpp +++ b/test_ocean.cpp @@ -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 diff --git a/test_trade.cpp b/test_trade.cpp index 6751cff..86c7a63 100644 --- a/test_trade.cpp +++ b/test_trade.cpp @@ -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 diff --git a/test_volcano.cpp b/test_volcano.cpp index 40501c1..9bd6d90 100644 --- a/test_volcano.cpp +++ b/test_volcano.cpp @@ -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" diff --git a/test_weather.cpp b/test_weather.cpp index 72b71c1..9eab235 100644 --- a/test_weather.cpp +++ b/test_weather.cpp @@ -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;