Add marine flora & fauna (life in the ocean)

The biota density + population layers were hard-gated on elevation<=sea, so
the ocean was barren in the flora/fauna views and held no organisms.

- Density: ocean cells (not under polar Ice) get a marine primary productivity
  in computeFloraDensity -- base + (1-base)*max(shelf, coast), where shelf is
  shallowness (light) and coast is a BFS ring-distance from land (nutrients).
  Rich shelves/coasts, lower open ocean, zero under ice; sMoist (a land field)
  is not used at sea. computeFaunaDensity now skips only Ice, so marine fauna =
  flora*productivity with the existing carnivore prey-gate clustering big
  predators on rich shelves. Funga stays land-only.
- Population: append Ocean-masked archetypes (Kelp/Seagrass/Phytoplankton;
  Forage fish/Reef fish/Shark/Baleen whale/Seal/Squid; moistMin=0, SST-zoned).
  generateBiota fills ocean cells (skip Ice; no marine funga). fillFlora/
  fillFauna unchanged -- their biome-mask filter zones marine vs terrestrial.
  Append-only, so v7 saves are unaffected.
- Render: distinct marine ramps (marineFloraColor blue->teal/green bloom,
  marineFaunaColor blue->cyan->warm) for water cells in the 8/9 views; land
  ramps + funga view unchanged.
- Config: bioMarineBase/bioMarineShelfDepth/bioMarineCoastRings (self-describing
  config -> no save bump).
- test_biota.cpp updated: zero life under ice, marine flora/fauna present at sea
  + populate ocean tiles, funga 0 on water; capacity/carnivore-gate/budgets/
  determinism still hold. All five headless suites pass; GUI build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jonas Reith 2026-06-29 12:04:53 +02:00
parent 2b0e1f8633
commit b005969ee1
11 changed files with 180 additions and 40 deletions

View File

@ -319,8 +319,8 @@ Working and verified (logic tested headless):
(`bioCarnPreyMin`), funga = flora-like but moisture/organic-matter-led + cold-tolerant.
Derived each tick (like climate), drive color modes `8`/`9`/`0`. (2) A discrete
**slot/point population** `Planet::generateBiota()` (key `L`, on a settled world) — each land
cell draws broad **archetypes** from a comprehensive table (`biotaArchetypes()`, 36 entries
across Flora/Fauna/Funga, each with Class/Order/Family/Size + a biome mask + climate
cell draws broad **archetypes** from a comprehensive table (`biotaArchetypes()`, 49 entries
across Flora/Fauna/Funga incl. marine, each with Class/Order/Family/Size + a biome mask + climate
tolerance) into a per-kind slot cap + a density-scaled point budget (Tiny=1…Huge=5 cost),
weighted by suitability and a **regional bonus** for archetypes already placed in same-biome
neighbours (homogeneous regions, variety at boundaries). Organisms are labelled by their
@ -334,6 +334,27 @@ Working and verified (logic tested headless):
organism list. `bio*` config knobs. Headless `test_biota.cpp`: density ranges/zeros, fauna≤
capacity, carnivore gating, slot/point budgets, determinism + RNG isolation, v7 round-trip,
pre-v7 loads empty. v7 reads v6-and-older (no biota block → empty population; press `L`).
- **Biota — marine flora & fauna (life in the ocean):** the biota layers used to be 0 on every
water cell (a hard `elevation<=sea` gate + no Ocean-masked archetypes), so the sea read as
barren. Now ocean cells (not under polar `Ice`) get a **marine primary productivity** in
`computeFloraDensity`: `base + (1-base)·max(shelf, coast)` where `shelf` = shallowness
(`1 - depth/bioMarineShelfDepth`, light to the photic floor) and `coast` = a multi-source BFS
**ring-distance from land** (nutrient runoff; mirrors the continentality BFS, seeded from land
not ocean) — so productivity is rich on sunlit shelves/coasts, lower in the deep open ocean,
zero only under ice; `sMoist` (a *land* rainfall field) is **not** used at sea. `computeFaunaDensity`
now skips only `Ice` (was all water) so marine fauna = `flora·productivity`, with the existing
carnivore prey-gate clustering sharks/seals/squid on rich shelves. **Funga stays land-only.**
`generateBiota` populates ocean cells too (skip `Ice`; no marine funga) — `fillFlora`/`fillFauna`
are unchanged because their biome-mask filter draws only the **new Ocean-masked archetypes**
appended to `biotaArchetypes()`: marine flora **Kelp / Seagrass / Phytoplankton** and marine fauna
**Forage fish / Reef fish / Shark / Baleen whale / Seal / Squid** (all `moistMin=0`, SST-zoned;
append-only so v7 saves are unaffected — old saves just lack them until `L`). The flora/fauna
color views (`8`/`9`) render ocean on a **distinct marine ramp** (`marineFloraColor` deep
blue→teal/green bloom, `marineFaunaColor` deep blue→cyan→warm) so the sea still reads as sea;
land ramps + the funga view unchanged. New `bioMarineBase`/`bioMarineShelfDepth`/
`bioMarineCoastRings` config knobs (self-describing config → **no save bump**). `test_biota.cpp`
updated: zero life under ice, marine flora/fauna present at sea + populate ocean tiles, funga 0
on water, capacity/carnivore-gate/budgets/determinism still hold.
- **Live World — clock + day/night + live seasons + snow line:** the first **Live World** stage
(the slow real-time arc after World Creation). Engine (`src/sim/PlanetLive.cpp`, raylib-free,
derived/not-saved): `computeInsolation(dayOfYear01, timeOfDay01)``sInsolation` (0..1 cosine
@ -703,12 +724,17 @@ triangles (plates are fixed in phase 1).
- Biota (`bio*` in PlanetConfig / `planet.cfg`) — density: `bioVegTempMin`/`bioVegTempOpt`/
`bioVegMoistRef` (flora temp/moisture limits), `bioFaunaProductivity` (animals per unit
flora), `bioCarnPreyMin`/`bioCarnScale` (carnivore prey gate + ramp), `bioFungaMoistRef`/
`bioFungaFloraWeight`/`bioFungaTempMin` (funga moisture/organic-matter/cold rules);
`bioFungaFloraWeight`/`bioFungaTempMin` (funga moisture/organic-matter/cold rules); **marine
flora/fauna**: `bioMarineBase` (0.15, open-ocean baseline density far from land), `bioMarineShelfDepth`
(2500 m, depth over which shelf/light productivity fades to the base), `bioMarineCoastRings`
(3, ocean rings from land over which coastal-nutrient richness fades to the base — lower = a
tighter coastal band, higher = life further offshore);
slot/point population: `bioFloraSlots`/`bioFaunaSlots`/`bioFungaSlots` (distinct-type cap),
`bioFloraPoints`/`bioFaunaPoints`/`bioFungaPoints` (point budget at full density, scaled by
it; Tiny=1…Huge=5), `bioRegionBonus` (how strongly a cell copies same-biome neighbours →
homogeneity vs variety). To add organisms, append to `biotaArchetypes()` in PlanetBiota.cpp
(append-only — indices are serialized in v7 saves).
homogeneity vs variety). To add organisms (incl. marine — give them a `B::Ocean` biome mask
and `moistMin=0`), append to `biotaArchetypes()` in PlanetBiota.cpp (append-only — indices are
serialized in v7 saves).
- **Live World (`dayLengthHours`/`yearLengthDays`/`snowTemp`/`seaIceTemp`, `planet.cfg`):**
`dayLengthHours` (24) sets the day/night period (and the `d/s` rate unit), `yearLengthDays`
(365.25) the season period; `axialTilt` drives the seasonal declination (0 = no day/night

View File

@ -84,8 +84,10 @@ fine with an empty population (`readState(is, hasBiome, hasBiota)`; `hasBiota =
Two layers (`PlanetBiota.cpp` + the three `*Gen.cpp`): (1) derived per-cell **density** scalars
(0..1) recomputed each tick like climate — flora = Liebig-min(temp, moisture), fauna ∝ flora
(carnivores gated on neighbourhood prey ≥ `bioCarnPreyMin`), funga = moisture/organic-matter-led
+ cold-tolerant; 0 on water/Ice. (2) On-demand discrete **population** `generateBiota()`: each
land cell draws broad archetypes from the comprehensive append-only `biotaArchetypes()` table
+ cold-tolerant. All three are 0 under polar `Ice`; funga is also 0 on water, while flora/fauna
extend into the ocean as marine productivity (see below). (2) On-demand discrete **population**
`generateBiota()`: each land **or ocean** cell draws broad archetypes from the comprehensive
append-only `biotaArchetypes()` table
into a per-kind slot cap + density-scaled point budget (size → cost Tiny=1…Huge=5), weighted by
biome/climate suitability and a **regional bonus** for archetypes already in same-biome
neighbours (single index-ordered pass → homogeneous regions, boundary variety). Organisms are
@ -95,6 +97,22 @@ get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seede
`Planet::rngState`) so populating biota never perturbs tectonic determinism — asserted in
`test_biota.cpp`. The archetype table is **append-only** (indices are serialized in v7 saves).
**Marine flora & fauna (life in the ocean).** Funga stays land-only, but flora and fauna now
extend over water (the old hard `elevation<=sea` gate left the sea barren). Ocean cells (not under
polar `Ice`) get a marine primary productivity in `computeFloraDensity`:
`base + (1-base)·max(shelf, coast)`, where `shelf = clamp(1 - depth/bioMarineShelfDepth)` (light to
the photic floor) and `coast = clamp(1 - ringDistFromLand/bioMarineCoastRings)` (land-runoff
nutrients, a multi-source BFS ring-distance seeded from land — the continentality BFS mirrored).
`sMoist` is a land rainfall field and is not used at sea. `computeFaunaDensity` now skips only `Ice`,
so marine fauna = `flora·productivity` and the carnivore prey-gate clusters sharks/seals/squid on
rich shelves. `generateBiota` fills ocean cells too (skip `Ice`; no marine funga); `fillFlora`/
`fillFauna` are unchanged because their biome-mask filter draws only the **Ocean-masked** archetypes
appended to the table (Kelp/Seagrass/Phytoplankton; Forage fish/Reef fish/Shark/Baleen whale/Seal/
Squid — all `moistMin=0`, SST-zoned). The flora/fauna colour views render ocean on a distinct
marine ramp (`marineFloraColor`/`marineFaunaColor`). Knobs: `bioMarineBase`/`bioMarineShelfDepth`/
`bioMarineCoastRings`. No save bump (densities derived; archetypes append-only; config
self-describing).
## Climate + biome model (derived, not saved)
`computeClimate()` builds two derived per-cell fields:

View File

@ -124,6 +124,21 @@ Color fungaColor(double d01) { // pale -> violet/brown
static const unsigned char lo[3] = { 215, 205, 210 }, hi[3] = { 110, 55, 120 };
return ramp2(d01, lo, hi);
}
Color marineFloraColor(double d01) { // deep ocean blue -> bright teal/green bloom
static const unsigned char lo[3] = { 18, 45, 80 }, hi[3] = { 60, 215, 160 };
return ramp2(d01, lo, hi);
}
Color marineFaunaColor(double d01) { // deep blue -> cyan -> warm (rich shelves)
double t = std::clamp(d01, 0.0, 1.0);
static const unsigned char key[3][3] = {
{ 18, 45, 80 }, // 0.0 deep blue
{ 50, 175, 200 }, // 0.5 cyan
{ 235, 195, 90 }, // 1.0 warm/gold
};
double s = t * 2.0; int k = std::min(1, (int)s); double f = s - k;
auto L = [&](int c) { return (unsigned char)(key[k][c] + (key[k + 1][c] - key[k][c]) * f); };
return Color{ L(0), L(1), L(2), 255 };
}
// Temperature ramp over ~[-40, 40] C: deep blue -> cyan -> green -> yellow -> red.
Color tempColor(double celsius) {

View File

@ -33,3 +33,8 @@ Color tideColor(double level, double range);
Color floraColor(double d01);
Color faunaColor(double d01);
Color fungaColor(double d01);
// Marine biota density ramps (0..1) -- a distinct sea palette so ocean reads as
// ocean: marine flora deep blue->bright teal/green bloom, marine fauna deep
// blue->cyan->warm. Used for water cells in the flora/fauna views.
Color marineFloraColor(double d01);
Color marineFaunaColor(double d01);

View File

@ -125,8 +125,10 @@ void Viewer::recolor() {
case ColorMode::Seasonality: vcolors[i] = (summer.empty()||winter.empty()) ? Color{90,90,90,255}
: seasonColor(summer[i] - winter[i]); break;
case ColorMode::Precip: vcolors[i] = moist.empty() ? Color{90,90,90,255} : precipColor(moist[i]); break;
case ColorMode::FloraDensity: vcolors[i] = flora.empty() ? Color{90,90,90,255} : floraColor(flora[i]); break;
case ColorMode::FaunaDensity: vcolors[i] = fauna.empty() ? Color{90,90,90,255} : faunaColor(fauna[i]); break;
case ColorMode::FloraDensity: vcolors[i] = flora.empty() ? Color{90,90,90,255}
: (planet.cells[i].elevation <= planet.cfg.seaLevel ? marineFloraColor(flora[i]) : floraColor(flora[i])); break;
case ColorMode::FaunaDensity: vcolors[i] = fauna.empty() ? Color{90,90,90,255}
: (planet.cells[i].elevation <= planet.cfg.seaLevel ? marineFaunaColor(fauna[i]) : faunaColor(fauna[i])); break;
case ColorMode::FungaDensity: vcolors[i] = funga.empty() ? Color{90,90,90,255} : fungaColor(funga[i]); break;
default: vcolors[i] = elevationColor(planet.cells[i].elevation, planet.cfg.seaLevel);
}

View File

@ -106,6 +106,28 @@ const std::vector<BiotaArchetype>& biotaArchetypes() {
S::Tiny, M({B::Forest, B::Grassland, B::Savanna, B::Wetland}), 0.0, 38.0, 0.30, false });
a.push_back({ "Puffball", K::Funga, R::Decomposer, "Agaricomycetes", "Agaricales", "Lycoperdaceae",
S::Small, M({B::Grassland, B::Savanna, B::Tundra}), -10.0, 32.0, 0.20, true });
// ---- Marine flora (primary producers) -- Ocean-masked, moisture-independent
// (sMoist is a land rainfall field, so moistMin = 0; SST zones them) ----
a.push_back({ "Kelp", K::Flora, R::Aquatic, "Phaeophyceae", "Laminariales", "Laminariaceae",
S::Big, M({B::Ocean}), -2.0, 20.0, 0.0, false }); // cold-temperate forests
a.push_back({ "Seagrass", K::Flora, R::Aquatic, "Liliopsida", "Alismatales", "Zosteraceae",
S::Small, M({B::Ocean, B::Beach}), 10.0, 34.0, 0.0, false }); // warm shallows
a.push_back({ "Phytoplankton", K::Flora, R::Aquatic, "Bacillariophyceae", "Naviculales", "Naviculaceae",
S::Tiny, M({B::Ocean}), -2.0, 34.0, 0.0, false }); // ubiquitous drifting base
// ---- Marine fauna -- Ocean-masked. Big predators (shark/seal/squid) are
// gated on local prey in fillFauna, so they cluster on rich shelves ----
a.push_back({ "Forage fish", K::Fauna, R::Herbivore, "Actinopterygii", "Clupeiformes", "Clupeidae",
S::Tiny, M({B::Ocean}), -2.0, 32.0, 0.0, false }); // plankton-grazing shoals
a.push_back({ "Reef fish", K::Fauna, R::Omnivore, "Actinopterygii", "Perciformes", "Serranidae",
S::Small, M({B::Ocean}), 4.0, 34.0, 0.0, false });
a.push_back({ "Shark", K::Fauna, R::Carnivore, "Chondrichthyes", "Carcharhiniformes", "Carcharhinidae",
S::Big, M({B::Ocean}), 2.0, 32.0, 0.0, false });
a.push_back({ "Baleen whale", K::Fauna, R::Herbivore, "Mammalia", "Cetacea", "Balaenopteridae",
S::Huge, M({B::Ocean}), -2.0, 30.0, 0.0, false }); // filter-feeder on plankton
a.push_back({ "Seal", K::Fauna, R::Carnivore, "Mammalia", "Carnivora", "Phocidae",
S::Medium, M({B::Ocean}), -12.0, 18.0, 0.0, false }); // cold-water pinniped
a.push_back({ "Squid", K::Fauna, R::Carnivore, "Cephalopoda", "Myopsida", "Loliginidae",
S::Small, M({B::Ocean}), 2.0, 30.0, 0.0, false });
return a;
}();
return T;
@ -230,13 +252,16 @@ void Planet::generateBiota() {
uint32_t rng = cfg.seed ? (cfg.seed ^ 0xB107A5EDu) : 0xB107A5EDu;
std::vector<char> done(n, 0);
for (int i = 0; i < n; ++i) {
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) { done[i] = 1; continue; }
if (cells[i].biome == Biome::Ice) { done[i] = 1; continue; } // frozen poles/peaks: empty
bool ocean = cells[i].elevation <= sea;
std::vector<int> nbr; // already-filled neighbours sharing this biome
for (int j : cells[i].neighbors)
if (done[j] && cells[j].biome == cells[i].biome) nbr.push_back(j);
// On ocean cells fillFlora/fillFauna draw only Ocean-masked (marine)
// archetypes via the biome-mask filter; funga stays land-only (no marine fungi).
sBiota[i].flora = fillFlora(i, nbr, rng);
sBiota[i].fauna = fillFauna(i, nbr, rng);
sBiota[i].funga = fillFunga(i, nbr, rng);
sBiota[i].funga = ocean ? std::vector<Organism>{} : fillFunga(i, nbr, rng);
if (!sBiota[i].flora.empty() || !sBiota[i].fauna.empty() || !sBiota[i].funga.empty())
sHasBiota = true;
done[i] = 1;

View File

@ -13,12 +13,12 @@ void Planet::computeFaunaDensity() {
const int n = (int)cells.size();
if ((int)sFloraDensity.size() != n) computeFloraDensity();
sFaunaDensity.assign(n, 0.0);
const double sea = cfg.seaLevel;
const double prod = cfg.bioFaunaProductivity;
for (int i = 0; i < n; ++i) {
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) continue;
// Herbivore capacity scales with plant productivity; overall animal
// richness mostly tracks it (herbivores are the bulk of the biomass).
if (cells[i].biome == Biome::Ice) continue; // ice: no animals (land or frozen sea)
// Animal richness tracks primary productivity (herbivores/grazers are the
// bulk of the biomass) -- on land that is plant flora, at sea the marine
// flora (plankton/algae) computed by computeFloraDensity for ocean cells.
sFaunaDensity[i] = std::clamp(sFloraDensity[i] * prod, 0.0, 1.0);
}
}

View File

@ -3,10 +3,12 @@
#include <algorithm>
#include <cmath>
// --- Biota: Flora (plants) ---------------------------------------------------
// Density = an NPP-style Liebig minimum of a temperature factor and a moisture
// factor (lush warm-wet, ~0 in ice/desert/alpine), 0 on water. fillFlora() draws
// plant archetypes suited to the cell's biome/climate into its slot/point budget.
// --- Biota: Flora (plants + marine primary producers) ------------------------
// Land: density = an NPP-style Liebig minimum of a temperature factor and a
// moisture factor (lush warm-wet, ~0 in desert/alpine). Ocean: marine primary
// productivity (phytoplankton/algae/kelp/seagrass) -- rich on sunlit shelves and
// near land (nutrients), lower in the deep open ocean, 0 only under polar Ice.
// fillFlora() draws archetypes suited to the cell's biome/climate into its budget.
void Planet::computeFloraDensity() {
const int n = (int)cells.size();
@ -15,11 +17,36 @@ void Planet::computeFloraDensity() {
const double sea = cfg.seaLevel;
const double tMin = cfg.bioVegTempMin, tOpt = cfg.bioVegTempOpt;
const double mRef = std::max(1e-6, cfg.bioVegMoistRef);
// Coastal proximity for the ocean: multi-source BFS ring distance from LAND
// cells outward over the fixed neighbour graph (deterministic, order-independent,
// O(n)). Near-land seas get a land-runoff nutrient boost (cf. continentality in
// PlanetClimate.cpp, seeded from ocean -- here we seed from land).
const int coastRings = std::max(1, cfg.bioMarineCoastRings);
std::vector<int> landDist(n, -1), frontier, next;
for (int i = 0; i < n; ++i)
if (cells[i].elevation > sea) { landDist[i] = 0; frontier.push_back(i); }
for (int r = 1; r <= coastRings && !frontier.empty(); ++r) {
next.clear();
for (int i : frontier)
for (int nb : cells[i].neighbors)
if (landDist[nb] < 0) { landDist[nb] = r; next.push_back(nb); }
frontier.swap(next);
}
const double mBase = std::clamp(cfg.bioMarineBase, 0.0, 1.0);
const double shelfDepth = std::max(1.0, cfg.bioMarineShelfDepth);
for (int i = 0; i < n; ++i) {
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) continue;
if (cells[i].biome == Biome::Ice) continue; // frozen poles/peaks: no life
if (cells[i].elevation > sea) { // land: temp+moisture Liebig minimum
double tf = std::clamp((sTemp[i] - tMin) / std::max(1e-6, tOpt - tMin), 0.0, 1.0);
double mf = std::clamp(sMoist[i] / mRef, 0.0, 1.0);
sFloraDensity[i] = std::min(tf, mf);
} else { // ocean: shelf (light) + coastal nutrients
double depth = sea - cells[i].elevation;
double shelf = std::clamp(1.0 - depth / shelfDepth, 0.0, 1.0);
double coast = (landDist[i] < 0) ? 0.0
: std::clamp(1.0 - (double)landDist[i] / coastRings, 0.0, 1.0);
sFloraDensity[i] = std::clamp(mBase + (1.0 - mBase) * std::max(shelf, coast), 0.0, 1.0);
}
}
}

View File

@ -34,7 +34,7 @@
D(seasonAmpMax) D(seasonLatExp) D(seasonOceanFactor) \
D(bioVegTempMin) D(bioVegTempOpt) D(bioVegMoistRef) D(bioFaunaProductivity) \
D(bioCarnPreyMin) D(bioCarnScale) D(bioFungaMoistRef) D(bioFungaFloraWeight) \
D(bioFungaTempMin) D(bioRegionBonus) \
D(bioFungaTempMin) D(bioRegionBonus) D(bioMarineBase) D(bioMarineShelfDepth) \
D(dayLengthHours) D(yearLengthDays) D(snowTemp) D(seaIceTemp) \
D(tideAmplitude) D(tideSunFactor) \
D(weatherEvapRate) D(weatherWindKmh) D(weatherSatBase) D(weatherSatTempCoef) \
@ -46,7 +46,7 @@
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) \
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
U(seed)
// Write all config fields as `key = value` lines (no header). Shared by the text
@ -191,6 +191,8 @@ std::string validateConfig(const PlanetConfig& cfg) {
E(rng(cfg.bioFungaFloraWeight, 0.0, 1.0, "bioFungaFloraWeight"));
E(rng(cfg.bioFungaTempMin, -50.0, 20.0, "bioFungaTempMin"));
E(rng(cfg.bioRegionBonus, 0.0, 10.0, "bioRegionBonus"));
E(rng(cfg.bioMarineBase, 0.0, 1.0, "bioMarineBase"));
E(rng(cfg.bioMarineShelfDepth, 1.0, 11000.0, "bioMarineShelfDepth"));
E(rng(cfg.dayLengthHours, 0.1, 1.0e5, "dayLengthHours"));
E(rng(cfg.yearLengthDays, 1.0, 1.0e7, "yearLengthDays"));
E(rng(cfg.snowTemp, -60.0, 30.0, "snowTemp"));
@ -232,6 +234,7 @@ std::string validateConfig(const PlanetConfig& cfg) {
E(irng(cfg.bioFloraPoints, 1, 100000, "bioFloraPoints"));
E(irng(cfg.bioFaunaPoints, 1, 100000, "bioFaunaPoints"));
E(irng(cfg.bioFungaPoints, 1, 100000, "bioFungaPoints"));
E(irng(cfg.bioMarineCoastRings, 1, 100, "bioMarineCoastRings"));
if (cfg.oceanBase >= cfg.continentBase)
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");

View File

@ -263,6 +263,9 @@ struct PlanetConfig {
double bioFungaFloraWeight = 0.6; // how much fungi lean on flora (organic matter) 0..1
double bioFungaTempMin = -15.0; // C above which fungi are not cold-limited (cold-tolerant)
double bioRegionBonus = 0.5; // weight boost for archetypes present in same-biome neighbours
double bioMarineBase = 0.15; // open-ocean baseline marine flora density (deep, far from land)
double bioMarineShelfDepth = 2500.0; // m of depth over which shelf (light) productivity fades to base
int bioMarineCoastRings = 3; // ocean rings from land over which coastal richness fades to base
int bioFloraSlots = 12; // max distinct flora per cell (point budget caps abundance)
int bioFaunaSlots = 10; // max distinct fauna per cell
int bioFungaSlots = 8; // max distinct funga per cell

View File

@ -8,10 +8,10 @@
// 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).
// 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"
@ -61,20 +61,29 @@ int main() {
// --- 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;
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 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
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(zerosOnWaterIce, "flora/fauna/funga = 0 on ocean & ice");
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)");
@ -84,15 +93,20 @@ int main() {
p.generateBiota();
check(p.biotaPopulated(), "generateBiota() populates a land world");
const auto& B = p.biota();
bool slotsOk = true, pointsOk = true, carnGated = true, onLand = true;
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; };
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;
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;
@ -109,7 +123,9 @@ int main() {
if (sum / c <= p.cfg.bioCarnPreyMin) carnGated = false;
}
}
check(onLand, "no organisms on ocean/ice cells");
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");