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:
parent
2b0e1f8633
commit
b005969ee1
36
CLAUDE.md
36
CLAUDE.md
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)");
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user