// 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 // src/sim/PlanetIO.cpp -o /tmp/tb && /tmp/tb // // Verifies: density ranges, zero life under ice + marine flora/fauna present in the // ocean (funga land-only), fauna<=flora capacity, carnivores only where prey is // sufficient, slot/point budgets respected, determinism + RNG isolation from // tectonics, and save v7 round-trip (plus v6-style read leaving the population empty). #include "Planet.hpp" #include "PlanetBiota.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(); p.computeBiotaDensity(); } static bool sameBiota(const std::vector& a, const std::vector& b) { if (a.size() != b.size()) return false; auto eq = [](const std::vector& x, const std::vector& y) { if (x.size() != y.size()) return false; for (size_t k = 0; k < x.size(); ++k) if (x[k].archetype != y[k].archetype || x[k].biome != y[k].biome) return false; return true; }; for (size_t i = 0; i < a.size(); ++i) if (!eq(a[i].flora, b[i].flora) || !eq(a[i].fauna, b[i].fauna) || !eq(a[i].funga, b[i].funga)) return false; return true; } int main() { PlanetConfig cfg; cfg.seed = 4242; cfg.subdivisions = 5; Planet p; p.generate(cfg); settle(p); const int n = (int)p.cells.size(); const double sea = p.cfg.seaLevel; // --- Density fields ------------------------------------------------------ const auto& fl = p.floraDensity(); const auto& fa = p.faunaDensity(); const auto& fu = p.fungaDensity(); check((int)fl.size() == n && (int)fa.size() == n && (int)fu.size() == n, "density fields sized n"); bool ranged = true, iceZero = true, oceanFungaZero = true, faunaCap = true, faunaZero = true; bool anyFloraHigh = false, anyFunga = false, anyMarineFlora = false, anyMarineFauna = false; const double prod = p.cfg.bioFaunaProductivity; for (int i = 0; i < n; ++i) { for (double d : {fl[i], fa[i], fu[i]}) if (!(std::isfinite(d) && d >= 0.0 && d <= 1.0)) ranged = false; bool ice = (p.cells[i].biome == Biome::Ice); bool ocean = (p.cells[i].elevation <= sea); if (ice && (fl[i] != 0.0 || fa[i] != 0.0 || fu[i] != 0.0)) iceZero = false; // no life under ice if (ocean && !ice) { // marine flora/fauna allowed; funga land-only if (fu[i] != 0.0) oceanFungaZero = false; if (fl[i] > 0.0) anyMarineFlora = true; if (fa[i] > 0.0) anyMarineFauna = true; } if (fa[i] > fl[i] * prod + 1e-9) faunaCap = false; // fauna <= herbivore capacity (land + sea) if (fl[i] == 0.0 && fa[i] != 0.0) faunaZero = false; // no animals without producers if (p.cells[i].biome == Biome::Forest && fl[i] > 0.6) anyFloraHigh = true; if (fu[i] > 0.05) anyFunga = true; } check(ranged, "all densities finite in [0,1]"); check(iceZero, "flora/fauna/funga = 0 on ice cells"); check(oceanFungaZero, "funga = 0 on ocean (marine fungi out of scope)"); check(anyMarineFlora, "marine flora present in the ocean"); check(anyMarineFauna, "marine fauna present in the ocean"); check(faunaCap, "fauna density <= flora * productivity"); check(faunaZero, "no fauna where flora is zero"); check(anyFloraHigh, "some forest cells are lush (flora > 0.6)"); check(anyFunga, "funga present somewhere"); // --- Discrete population: slots/points + carnivore gating ---------------- p.generateBiota(); check(p.biotaPopulated(), "generateBiota() populates a land world"); 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; }; 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 if (!cb.flora.empty() || !cb.fauna.empty() || !cb.funga.empty()) iceEmpty = false; continue; } if (p.cells[i].elevation <= sea) { // ocean: marine flora/fauna, no funga if (!cb.funga.empty()) oceanFungaEmpty = false; if (!cb.flora.empty() || !cb.fauna.empty()) anyMarineOrg = true; } if ((int)cb.flora.size() > p.cfg.bioFloraSlots || (int)cb.fauna.size() > p.cfg.bioFaunaSlots || (int)cb.funga.size() > p.cfg.bioFungaSlots) slotsOk = false; if (cost(cb.flora) > (int)std::lround(p.cfg.bioFloraPoints * fl[i]) || cost(cb.fauna) > (int)std::lround(p.cfg.bioFaunaPoints * fa[i]) || cost(cb.funga) > (int)std::lround(p.cfg.bioFungaPoints * fu[i])) pointsOk = false; // Carnivore present => local prey (mean fauna density over i + neighbours) clears the threshold. bool hasCarn = false; for (const Organism& o : cb.fauna) if (biotaArchetypes()[o.archetype].role == EcoRole::Carnivore) hasCarn = true; if (hasCarn) { double sum = fa[i]; int c = 1; for (int j : p.cells[i].neighbors) { sum += fa[j]; ++c; } if (sum / c <= p.cfg.bioCarnPreyMin) carnGated = false; } } check(iceEmpty, "no organisms on ice cells"); check(oceanFungaEmpty, "no funga on ocean cells (marine fungi out of scope)"); check(anyMarineOrg, "marine flora/fauna populate the ocean"); check(slotsOk, "per-cell organism count <= slot budget"); check(pointsOk, "per-cell point cost <= density-scaled point budget"); check(carnGated, "carnivores only where neighbourhood prey > bioCarnPreyMin"); // --- Determinism: same seed -> identical population ---------------------- std::vector first = p.biota(); p.generateBiota(); check(sameBiota(first, p.biota()), "generateBiota() is deterministic (re-run identical)"); // --- RNG isolation: generating biota must not perturb tectonics ---------- { Planet a; a.generate(cfg); settle(a); a.drifting = true; Planet b; b.generate(cfg); settle(b); b.drifting = true; b.generateBiota(); // only b generates biota double dt = a.cflDtMy(); for (int k = 0; k < 5; ++k) { a.advect(dt); a.step(); a.erode(dt); b.advect(dt); b.step(); b.erode(dt); } bool identical = a.cells.size() == b.cells.size(); for (size_t i = 0; identical && i < a.cells.size(); ++i) if (a.cells[i].elevation != b.cells[i].elevation || a.cells[i].plateId != b.cells[i].plateId) identical = false; check(identical, "biota generation does not change tectonic evolution (separate RNG)"); } // --- Save v7 round-trip + v6-style read (empty population) --------------- { std::ostringstream os(std::ios::binary); p.writeState(os); std::string blob = os.str(); Planet q; std::istringstream is(blob, std::ios::binary); bool ok = q.readState(is, /*hasBiome*/true, /*hasBiota*/true); check(ok && q.biotaPopulated() && sameBiota(p.biota(), q.biota()), "save v7 round-trips the biota population"); Planet r; std::istringstream is2(blob, std::ios::binary); bool ok2 = r.readState(is2, /*hasBiome*/true, /*hasBiota*/false); // old (pre-v7) read path check(ok2 && !r.biotaPopulated(), "pre-v7 read leaves population empty (loads fine)"); } std::printf("\n%s (%d failure%s)\n", failures ? "FAILURES" : "ALL PASS", failures, failures == 1 ? "" : "s"); return failures ? 1 : 0; }