World-Creation stage after biomes. Two layers (raylib-free engine):
- Per-cell density scalars (flora/fauna/funga in [0,1]) derived from the
climate fields each tick (drive color modes 8/9/0). Flora = Liebig-min of
temp & moisture; fauna ~ flora with carnivores gated on local prey; funga =
moisture/organic-matter-led + cold-tolerant. Zero on water/ice.
- On-demand discrete population (key L, saved as v7): each land cell draws
broad archetypes from a comprehensive table into a per-kind slot cap +
density-scaled point budget (size -> cost), weighted by biome/climate
suitability and a regional bonus for same-biome neighbours. Separate RNG
seeded from cfg.seed so generating biota never perturbs tectonic determinism.
Organisms are labelled by taxonomy (Family + Size + role, e.g. "Felidae
(Big, Carnivore)") with the full Class > Order > Family tree stored, never an
informal common name. Cell-info panel word-wraps + aggregates duplicates so the
lists no longer get cut off.
New: src/sim/PlanetBiota.{hpp,cpp} + PlanetFlora/Fauna/FungiGen.cpp, color
modes/colors, bio* config knobs, save v7 (older saves load with empty
population), test_biota.cpp (densities, fauna<=capacity, carnivore gating,
slot/point budgets, determinism + RNG isolation, v7 round-trip). Docs updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
7.5 KiB
C++
155 lines
7.5 KiB
C++
// 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 + zeros on water/ice, 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 <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();
|
|
p.computeBiotaDensity();
|
|
}
|
|
|
|
static bool sameBiota(const std::vector<CellBiota>& a, const std::vector<CellBiota>& b) {
|
|
if (a.size() != b.size()) return false;
|
|
auto eq = [](const std::vector<Organism>& x, const std::vector<Organism>& 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, zerosOnWaterIce = true, faunaCap = true, faunaZero = true;
|
|
bool anyFloraHigh = false, anyFunga = 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 waterIce = (p.cells[i].elevation <= sea) || (p.cells[i].biome == Biome::Ice);
|
|
if (waterIce && (fl[i] != 0.0 || fa[i] != 0.0 || fu[i] != 0.0)) zerosOnWaterIce = false;
|
|
if (fa[i] > fl[i] * prod + 1e-9) faunaCap = false; // fauna <= herbivore capacity
|
|
if (fl[i] == 0.0 && fa[i] != 0.0) faunaZero = false; // no animals without plants
|
|
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(zerosOnWaterIce, "flora/fauna/funga = 0 on ocean & ice");
|
|
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, onLand = true;
|
|
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].elevation <= sea || p.cells[i].biome == Biome::Ice) {
|
|
if (!cb.flora.empty() || !cb.fauna.empty() || !cb.funga.empty()) onLand = false;
|
|
continue;
|
|
}
|
|
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(onLand, "no organisms on ocean/ice cells");
|
|
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<CellBiota> 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;
|
|
}
|