Add Biota stage: flora, fauna & funga (density + slot/point population)
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>
This commit is contained in:
parent
acc0e5eec9
commit
53e371bb2f
31
BUILD.md
31
BUILD.md
@ -3,7 +3,8 @@
|
||||
Fixed icosphere geometry; properties (elevation, plate, age, climate, biome) flow
|
||||
over it. World creation runs as continuous, overlapping stages on a geological clock:
|
||||
tectonics (boundary stress forms mountains/trenches) → continental drift & erosion →
|
||||
hydrology (rivers/lakes) → climate (temperature/precipitation) → biomes. Initial
|
||||
hydrology (rivers/lakes) → climate (temperature/precipitation) → biomes → biota
|
||||
(flora/fauna/funga). Initial
|
||||
terrain forming is a generator (a couple hundred paced ticks, ~3 s, auto-pauses at
|
||||
equilibrium), then drift/erosion/etc. continue. Press R to reseed, SPACE to pause.
|
||||
(The eventual goal is a separate slow real-time "Live World" weather/life mode.)
|
||||
@ -39,11 +40,13 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
||||
click open tile detail panel (subtiles grid, hoverable) + overlay
|
||||
C close the detail panel
|
||||
1 .. 7 color by elevation / plate / age / crust type / biome / temperature / precipitation
|
||||
8 / 9 / 0 color by biota density: flora / fauna / funga
|
||||
B toggle plate borders (on by default)
|
||||
D toggle per-plate drift arrows + P<id> labels (on by default)
|
||||
G toggle lat/lon graticule (+ degree numbers on the 2D map edges)
|
||||
J toggle rivers (Phase 2.5 hydrology)
|
||||
H start/stop Phase 2.5 (hydrology: rivers, lakes, fluvial erosion)
|
||||
L generate biota population (flora/fauna/funga; settled world; re-press regenerates)
|
||||
SPACE pause while forming / re-evolve once settled (or the on-screen button)
|
||||
[ / ] drift speed (My per real second, Phase 2/3)
|
||||
S single tectonic tick
|
||||
@ -147,14 +150,38 @@ shadow, dry interiors) then diffuses it. Color modes 6 (temperature) / 7 (precip
|
||||
climateWindPasses 50 moisture-advection iterations (steady state)
|
||||
climateMoistureSmooth 12 precipitation diffusion passes (raise = smoother, more grass/forest)
|
||||
|
||||
Biota (PlanetConfig): flora/fauna/funga. Density scalars drive color modes 8/9/0;
|
||||
the discrete slot/point population is generated on demand (L) and saved (v7).
|
||||
|
||||
bioVegTempMin -5 C below this no plant growth
|
||||
bioVegTempOpt 15 C at/above this temperature isn't limiting (flora)
|
||||
bioVegMoistRef 0.5 normalized moisture where water isn't limiting (flora)
|
||||
bioFaunaProductivity 0.9 herbivore carrying capacity per unit vegetation
|
||||
bioCarnPreyMin 0.30 min local prey (fauna density) to support carnivores
|
||||
bioCarnScale 1.0 carnivore weight ramp above the prey threshold
|
||||
bioFungaMoistRef 0.4 normalized moisture where fungi aren't water-limited
|
||||
bioFungaFloraWeight 0.6 how much fungi lean on flora (organic matter), 0..1
|
||||
bioFungaTempMin -15 C above this fungi are not cold-limited (cold-tolerant)
|
||||
bioRegionBonus 0.5 weight boost for archetypes present in same-biome neighbours
|
||||
bioFloraSlots 12 max distinct flora per cell (point budget caps abundance)
|
||||
bioFaunaSlots 10 max distinct fauna per cell
|
||||
bioFungaSlots 8 max distinct funga per cell
|
||||
bioFloraPoints 20 flora point budget at full density (scaled by density)
|
||||
bioFaunaPoints 16 fauna point budget at full density
|
||||
bioFungaPoints 14 funga point budget at full density
|
||||
|
||||
## Headless logic test (no display)
|
||||
|
||||
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/PlanetBiomes.cpp src/sim/PlanetIO.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/t && /tmp/t
|
||||
|
||||
# Biota suite: same source list, swap test_logic.cpp -> test_biota.cpp
|
||||
|
||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||
determinism. Run after changing Planet::step().
|
||||
|
||||
|
||||
72
CLAUDE.md
72
CLAUDE.md
@ -40,10 +40,14 @@ dynamic weather and life.
|
||||
updates as terrain changes). Color modes `6`/`7`.
|
||||
- **Biomes** *(done)* — per-cell `Cell.biome` (13 biomes incl. polar Ice) from elevation
|
||||
+ the climate fields (`Planet::classifyBiomes`), color mode `5`, saved per cell.
|
||||
- **Fauna & flora** *(next, planned — see `docs/fauna-flora-plan.md`)* — derived
|
||||
carrying-capacity densities: vegetation (flora) + a herbivore/carnivore food chain (fauna),
|
||||
computed from the climate fields each tick (the living/evolving ecosystem is reserved for
|
||||
Live World). Other follow-ups: feed precipitation into hydrology rainfall; seasons (obliquity).
|
||||
- **Biota — flora, fauna & funga** *(done — see `docs/fauna-flora-plan.md` +
|
||||
`docs/fauna_generation_plan.md`)* — two layers: per-cell **density scalars** (flora,
|
||||
fauna, funga ∈ [0,1]) derived from the climate fields each tick (drive the colour views),
|
||||
plus a discrete **slot/point population** of broad archetypes (Class/Order/Family/Size),
|
||||
generated **on demand** (`L`) and **saved** (save v7). Fauna is a herbivore/carnivore/
|
||||
omnivore food chain (predators gated on local prey); funga uses a flora-like but
|
||||
moisture/organic-matter-led rule. The living/evolving ecosystem is reserved for Live World.
|
||||
Other follow-ups: feed precipitation into hydrology rainfall; seasons (obliquity).
|
||||
|
||||
> Durable design context (module layout, save format, climate/biome model, conventions)
|
||||
> lives in **`docs/design-notes.md`** — important because Claude's auto-memory does not
|
||||
@ -256,6 +260,28 @@ Working and verified (logic tested headless):
|
||||
updating with `1`–`7`. The HUD title/status and the hydrology prompt were reworded to drop
|
||||
the rigid "Phase N" labels (now "World Creation: forming / drift & erosion / hydrology");
|
||||
internal `phase*` names are unchanged. Render/text only — no sim/save/config change.
|
||||
- **Biota (flora/fauna/funga):** the World-Creation stage after biomes. Two layers (src/sim,
|
||||
raylib-free): (1) **density scalars** `sFloraDensity`/`sFaunaDensity`/`sFungaDensity` ∈ [0,1]
|
||||
via `Planet::computeBiotaDensity()` — flora = NPP Liebig-min of temp & moisture (0 on
|
||||
water/Ice), fauna = herbivore capacity ∝ flora with carnivores gated on local prey
|
||||
(`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
|
||||
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
|
||||
**taxonomy** — Family + Size + role (e.g. *Felidae (Big, Carnivore)*, with the full
|
||||
*Class > Order > Family* tree in `organismTaxonomy()`), never an informal common name like
|
||||
"big cat"; generalist families get a biome adjective (*Desert Muridae*). Uses a **separate
|
||||
RNG** seeded from `cfg.seed` so generating biota never
|
||||
perturbs tectonic determinism. Population is **saved** (`sBiota`, save **v7**); densities are
|
||||
derived/not-saved. New files `PlanetBiota.{hpp,cpp}` + `PlanetFlora/Fauna/FungiGen.cpp`;
|
||||
color modes `floraColor`/`faunaColor`/`fungaColor`; cell-info shows density % + the per-kind
|
||||
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`).
|
||||
- Mouse hover (in either view) shows per-cell info. Clicking a tile opens a
|
||||
right-side detail panel: tile info header + the tile's subgrid drawn as a
|
||||
flat hoverable grid of subtiles (neighbor-owned subtiles dimmed). A high-res
|
||||
@ -286,9 +312,16 @@ src/
|
||||
PlanetDrift.cpp cflDtMy/advect + plate lifecycle (fission/kick/baby/fuse)
|
||||
PlanetErosion.cpp erode() + adjustSeaLevel()
|
||||
PlanetHydrology.cpp routeFlow/computeHydrology/hydrology (Phase 3)
|
||||
PlanetClimate.cpp computeClimate() (temperature + orographic precipitation)
|
||||
PlanetBiomes.cpp classifyBiomes() (per-cell Cell.biome from elev + climate)
|
||||
PlanetBiota.hpp BiotaKind/SizeClass/EcoRole/Organism/CellBiota + archetype table decls
|
||||
PlanetBiota.cpp archetype library + slot/point draw + generateBiota/computeBiotaDensity
|
||||
PlanetFloraGen.cpp computeFloraDensity + fillFlora
|
||||
PlanetFaunaGen.cpp computeFaunaDensity + fillFauna (carnivores gated on prey)
|
||||
PlanetFungiGen.cpp computeFungaDensity + fillFunga (moisture/organic-matter rule)
|
||||
PlanetIO.cpp config file (text) + binary save/load
|
||||
render/ (raylib viewer)
|
||||
Colors.* cell color modes (elevation/plate/age/crust/lake)
|
||||
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
||||
Map2D.* Equal Earth 2D map: positions + projection/draw helpers
|
||||
Overlays.* borders, drift arrows, rivers, graticule, segments, subgrids
|
||||
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
|
||||
@ -344,9 +377,12 @@ raylib 5.5 is fetched automatically — do not vendor it.
|
||||
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/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetIO.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/t && /tmp/t
|
||||
```
|
||||
(Swap `test_logic.cpp` for `test_biota.cpp` to run the Biota suite — same source list.)
|
||||
|
||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
||||
the window (the engine lives in `src/sim` and is raylib-free, so it links without
|
||||
@ -371,13 +407,14 @@ compute-shader port -- a Phase-2 effort.
|
||||
|
||||
LMB drag orbit · wheel zoom · hover for cell info (3D or map) ·
|
||||
click a tile to open its detail panel (subtiles) · `C` close panel ·
|
||||
drag the 2D map to pan it east/west · `1`..`7` color by
|
||||
elevation/plate/age/crust-type/biome/temperature/precipitation (active mode shown
|
||||
top-center of the globe) ·
|
||||
drag the 2D map to pan it east/west · `1`..`0` color by
|
||||
elevation/plate/age/crust-type/biome/temperature/precipitation/flora/fauna/funga
|
||||
(`8`/`9`/`0` = biota density; active mode shown top-center of the globe) ·
|
||||
`B` plate borders · `D` drift vectors · `G` lat/lon grid · `J` rivers (Phase 3,
|
||||
all in 3D + 2D) · `SPACE` or on-screen button pause · `[`/`]` drift speed (My/sec) ·
|
||||
`S` single tick · `F` fast-forward Phase-1 forming to settled ·
|
||||
`H` toggle Phase 3 (hydrology) · `R` reseed ·
|
||||
`H` toggle Phase 3 (hydrology) · `L` generate biota population (flora/fauna/funga,
|
||||
on a settled world; re-press regenerates) · `R` reseed ·
|
||||
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
||||
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
|
||||
|
||||
@ -395,9 +432,11 @@ PlanetConfig param, auto-created on first run, reload with `F2`) and
|
||||
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
||||
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
||||
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
||||
save header is versioned (currently **6**; v2 adds the `[`/`]` drift rate, v3 a
|
||||
save header is versioned (currently **7**; v2 adds the `[`/`]` drift rate, v3 a
|
||||
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
||||
key=value text block** instead of a raw POD dump); newer-than-supported is rejected.
|
||||
key=value text block** instead of a raw POD dump, v7 appends the **biota population**
|
||||
block — three Organism lists per cell, gated by a flag byte); newer-than-supported is
|
||||
rejected. Older saves (no biota block) load fine with an empty population (press `L`).
|
||||
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
||||
config is parsed like `planet.cfg` (unknown keys ignored, missing keys keep defaults),
|
||||
written at `precision(17)` so doubles round-trip exactly. (v6 cannot load pre-v6 saves —
|
||||
@ -454,6 +493,15 @@ triangles (plates are fixed in phase 1).
|
||||
`climateContinentality` (inland drying), `climateMoistureSmooth` (diffusion passes →
|
||||
wet/dry transition zones; raise for smoother, more grassland/forest), `climateOceanMoisture`,
|
||||
`climateOroRefHeight`, `climateWindPasses`. Temperature uses the `biome*` temp params.
|
||||
- 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);
|
||||
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).
|
||||
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||
knob for how fast/high relief builds.
|
||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||
|
||||
@ -26,6 +26,10 @@ add_executable(planetsim
|
||||
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
|
||||
# Viewer (raylib) -- src/render
|
||||
src/render/Colors.cpp
|
||||
|
||||
@ -32,15 +32,19 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
||||
steepest-descent→rivers, mass-conserving stream-power incision).
|
||||
- `PlanetClimate.cpp` — `computeClimate()` (temperature + orographic precipitation).
|
||||
- `PlanetBiomes.cpp` — `classifyBiomes()` (per-cell `Cell.biome` from elevation + climate).
|
||||
- `PlanetBiota.{hpp,cpp}` — Biota types + archetype table + slot/point draw +
|
||||
`computeBiotaDensity()`/`generateBiota()` (flora/fauna/funga).
|
||||
- `PlanetFloraGen.cpp` / `PlanetFaunaGen.cpp` / `PlanetFungiGen.cpp` — per-kind density +
|
||||
per-cell `fill*` (fauna gates carnivores on local prey; funga is moisture/organic-led).
|
||||
- `PlanetIO.cpp` — text config + binary save/load.
|
||||
- (`PlanetBiosphere.cpp` — fauna/flora, planned; see `fauna-flora-plan.md`.)
|
||||
|
||||
The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration),
|
||||
`ViewerInput.cpp` (camera/picking/keys), `ViewerRender.cpp` (globe/map/panels/HUD/prompt),
|
||||
plus topical helpers `Colors` / `Map2D` / `Overlays` / `Picking` / `Panels`.
|
||||
|
||||
Per-tick order in `Viewer::refreshView()`: `computeHydrology()` (if hydrology on) →
|
||||
`computeClimate()` → `classifyBiomes()` → (`computeBiosphere()` when added) → `recolor()`.
|
||||
`computeClimate()` → `classifyBiomes()` → `computeBiotaDensity()` → `recolor()`. The discrete
|
||||
biota *population* (`generateBiota()`) is NOT in this per-tick path — it's on-demand (key `L`).
|
||||
|
||||
## Core principle (do not violate)
|
||||
|
||||
@ -56,14 +60,33 @@ between world and model space must compensate with `rotateZ(v, ±tilt)` (src/ren
|
||||
Picking.cpp): 3D picking un-rotates the ray hit by `−tilt` before `nearestCell`; 3D plate
|
||||
labels rotate by `+tilt` before projecting. The 2D map + biome/climate are tilt-independent.
|
||||
|
||||
## Save format (v6) — self-describing config
|
||||
## Save format (v7) — self-describing config + biota population
|
||||
|
||||
`planet.save` stores `PlanetConfig` as a **self-describing key=value text block** (not a raw
|
||||
POD dump), parsed like `planet.cfg` (`writeConfigFields`/`parseConfigStream` shared in
|
||||
PlanetIO.cpp), written at `precision(17)` so doubles round-trip exactly. Consequence:
|
||||
**adding/removing PlanetConfig fields no longer breaks saves** (unknown keys ignored, missing
|
||||
keys keep defaults). v6 cannot load pre-v6 saves (one-time break; a length guard fails it
|
||||
gracefully). Per-cell `Cell.biome` is saved (a byte appended after `invader`).
|
||||
gracefully). Per-cell `Cell.biome` is saved (a byte appended after `invader`). **v7** appends
|
||||
the **biota population** (`sBiota`): a flag byte, then three `Organism{uint16 archetype, uint8
|
||||
biome}` lists per cell. Densities are derived (not saved). Older saves without the block load
|
||||
fine with an empty population (`readState(is, hasBiome, hasBiota)`; `hasBiota = ver>=7`).
|
||||
|
||||
## Biota (flora / fauna / funga) — density + slot/point population
|
||||
|
||||
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
|
||||
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
|
||||
labelled by **taxonomy** — Family + Size + role (full `Class > Order > Family` in
|
||||
`organismTaxonomy()`), never informal common names ("Felidae", not "cat"); generalist families
|
||||
get a biome adjective ("Desert Muridae"). Generation uses a **separate RNG seeded from `cfg.seed`** (not
|
||||
`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).
|
||||
|
||||
## Climate + biome model (derived, not saved)
|
||||
|
||||
@ -88,7 +111,9 @@ Engine is raylib-free, so logic is tested without a display. Build/run:
|
||||
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/PlanetBiomes.cpp src/sim/PlanetClimate.cpp \
|
||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||
src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp \
|
||||
src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||
# test_biota.cpp uses the same source list (Biota suite).
|
||||
```
|
||||
(add new `src/sim/*.cpp` to that list as stages are added). `Planet::step()` passes are
|
||||
data-parallel + double-buffered → bit-identical for any OpenMP thread count (determinism).
|
||||
|
||||
@ -1,82 +1,56 @@
|
||||
# Next step — Fauna & Flora (derived carrying-capacity densities)
|
||||
# Biota — Flora · Fauna · Funga (density + slot/point population)
|
||||
|
||||
> Status: **planned, not yet implemented.** This is the next World-Creation stage after
|
||||
> climate. Approved design captured here so it can be picked up verbatim (e.g. after the
|
||||
> project moves to another machine — the auto-memory does not travel, this doc does).
|
||||
> Status: **implemented.** This supersedes the original density-only sketch. The discrete
|
||||
> slot/point design comes from `docs/fauna_generation_plan.md`; this doc records the unified,
|
||||
> shipped design. Code: `src/sim/PlanetBiota.{hpp,cpp}` + `PlanetFloraGen/FaunaGen/FungiGen.cpp`;
|
||||
> test `test_biota.cpp`; controls `8`/`9`/`0` (density views) + `L` (generate population).
|
||||
|
||||
## Decision summary
|
||||
## Three kinds (Biota)
|
||||
|
||||
- **Derived carrying-capacity map**, not a live simulation: vegetation + wildlife
|
||||
**density scalars** recomputed each tick from the climate fields + terrain, exactly like
|
||||
biomes/climate. It answers *"what could live here"*, not *"what is living/evolving here"*.
|
||||
- The actual **time-evolving ecosystem** (population growth, migration, predator-prey,
|
||||
extinction, seasons, species) is reserved for the future **Live World** real-time mode
|
||||
(year/season timescales — not the My geological clock).
|
||||
- Fauna uses a **herbivore + carnivore** food-chain split.
|
||||
- Fields are **derived / not saved** (deterministic functions of climate) → **no
|
||||
save-format change**. New config knobs ride the v6 self-describing save with no break.
|
||||
Living things are split into **Flora** (plants), **Fauna** (animals) and **Funga** (fungi).
|
||||
Funga is *not* a plant but is generated with a flora-like environmental system and its own
|
||||
rules; all three share the same per-cell slot/point logic.
|
||||
|
||||
## Part A — Biosphere fields (`src/sim/`, raylib-free)
|
||||
## Two layers
|
||||
|
||||
- **`Planet.hpp`**: declare `void computeBiosphere();` + accessors
|
||||
`const std::vector<double>& vegetation()/herbivores()/carnivores() const`. Add scratch
|
||||
members `std::vector<double> sVeg, sHerb, sCarn;` (not saved).
|
||||
- **New `src/sim/PlanetBiosphere.cpp`** — `Planet::computeBiosphere()` (run *after*
|
||||
`computeClimate()`; reads `sTemp` + `sMoist`):
|
||||
- **Vegetation (flora)** `sVeg[i] ∈ [0,1]`: `0` on water (`elev ≤ seaLevel`). On land, an
|
||||
NPP-style **Liebig minimum** of a temperature factor and a moisture factor:
|
||||
- `tF = clamp((sTemp[i] − bioVegTempMin)/(bioVegTempOpt − bioVegTempMin), 0, 1)`
|
||||
- `mF = clamp(sMoist[i]/bioVegMoistRef, 0, 1)`
|
||||
- `sVeg = min(tF, mF)` → lush warm-wet, ~0 in ice/desert/alpine (cold- or water-limited),
|
||||
matching the biomes.
|
||||
- **Herbivores** `sHerb[i] = clamp(sVeg[i] · bioHerbProductivity, 0, 1)` (capacity scales
|
||||
with plant productivity).
|
||||
- **Carnivores** `sCarn[i]`: present only where prey is abundant, ramping with it:
|
||||
`sCarn = sHerb > bioCarnPreyMin ? clamp((sHerb − bioCarnPreyMin)/(1 − bioCarnPreyMin)
|
||||
· bioCarnScale, 0, 1) : 0` → concentrated in the richest regions (an energy-pyramid feel,
|
||||
visually distinct from the broader herbivore pattern).
|
||||
- **`src/sim/Planet.cpp` `generate()`**: call `computeBiosphere()` after `classifyBiomes()`.
|
||||
1. **Density scalars** (`sFloraDensity`/`sFaunaDensity`/`sFungaDensity` ∈ [0,1]) — derived from
|
||||
the climate fields each tick (like biomes/climate), drive the colour views. 0 on water/Ice.
|
||||
- Flora = NPP-style **Liebig minimum** of a temperature factor
|
||||
`clamp((T−bioVegTempMin)/(bioVegTempOpt−bioVegTempMin),0,1)` and a moisture factor
|
||||
`clamp(sMoist/bioVegMoistRef,0,1)`.
|
||||
- Fauna = herbivore carrying capacity `clamp(floraDensity·bioFaunaProductivity,0,1)`;
|
||||
carnivores in the *population* are gated on neighbourhood prey ≥ `bioCarnPreyMin`.
|
||||
- Funga = `min(moistFactor, bioFungaFloraWeight·floraDensity + (1−w)) · coldTolerance`
|
||||
(`bioFungaMoistRef`, `bioFungaTempMin`) — thrives moist + with organic matter, persists in
|
||||
cold shade where flora thins, ~0 in hot dry desert.
|
||||
2. **Discrete slot/point population** (`sBiota`, saved v7) — generated **on demand**
|
||||
(`generateBiota()`, key `L`, settled world). Each land cell draws broad **archetypes** (NOT
|
||||
real species) from the comprehensive append-only table `biotaArchetypes()` — each carries a
|
||||
Class/Order/Family + Size (Tiny..Huge), a biome mask and a climate tolerance. A cell has a
|
||||
per-kind **slot** cap (distinct types) and a density-scaled **point** budget (size → cost
|
||||
Tiny=1…Huge=5); archetypes are added until slots full or points exhausted. Selection is
|
||||
weighted by biome/climate suitability × a **regional bonus** for archetypes already placed in
|
||||
already-filled, same-biome neighbours (homogeneous regions, variety at boundaries). Organisms
|
||||
are **named by taxonomy** — Family + Size + role (full `Class > Order > Family` available),
|
||||
never an informal common name ("Felidae", not "cat"; Felidae is the family, cat a common
|
||||
name). Generalist families get a biome adjective ("Desert Muridae" / "Forest Muridae").
|
||||
|
||||
## Part B — Config knobs (`PlanetTypes.hpp` + `PlanetIO.cpp`)
|
||||
## Config knobs (`bio*`, in `planet.cfg`)
|
||||
|
||||
Add to `PlanetConfig` + `CONFIG_FIELDS` + `validateConfig`:
|
||||
`bioVegTempMin(-5)`, `bioVegTempOpt(15)`, `bioVegMoistRef(0.5)`, `bioFaunaProductivity(0.9)`,
|
||||
`bioCarnPreyMin(0.30)`, `bioCarnScale(1.0)`, `bioFungaMoistRef(0.4)`, `bioFungaFloraWeight(0.6)`,
|
||||
`bioFungaTempMin(-15)`, `bioRegionBonus(0.5)`, slot caps `bioFlora/Fauna/FungaSlots(12/10/8)`,
|
||||
point budgets `bioFlora/Fauna/FungaPoints(20/16/14)`.
|
||||
|
||||
| key | default | meaning |
|
||||
|---|---|---|
|
||||
| `bioVegTempMin` | −5 °C | below this, no plant growth |
|
||||
| `bioVegTempOpt` | 15 °C | at/above this, temperature isn't limiting |
|
||||
| `bioVegMoistRef` | 0.5 | normalized moisture at which water isn't limiting |
|
||||
| `bioHerbProductivity` | 0.9 | herbivore capacity per unit vegetation |
|
||||
| `bioCarnPreyMin` | 0.30 | min herbivore density to support carnivores |
|
||||
| `bioCarnScale` | 1.0 | carnivore density ramp above the prey threshold |
|
||||
## Determinism & persistence
|
||||
|
||||
## Part C — Rendering (`src/render/`)
|
||||
|
||||
- **`Colors.{hpp,cpp}`**: `ColorMode` += `Vegetation`, `Herbivores`, `Carnivores`; add
|
||||
`vegColor` (barren tan → lush green), `herbColor` (pale → amber/orange), `carnColor`
|
||||
(pale → red/violet); extend `colorModeName`.
|
||||
- **`Viewer.cpp`**: `recolor()` adds the three cases (read the accessors, empty-guarded);
|
||||
`refreshView()` calls `planet.computeBiosphere()` after `classifyBiomes()`.
|
||||
- **`ViewerInput.cpp`**: `KEY_EIGHT` → Vegetation, `KEY_NINE` → Herbivores, `KEY_ZERO` →
|
||||
Carnivores (each `recolor()`). The top-center view-name label already shows the mode.
|
||||
- **`ViewerRender.cpp` `renderHUD`**: controls line gains `8 veg 9 herbiv 0 carniv`.
|
||||
- **`Panels.cpp` `cellInfo()`**: add a life line — `vegetation X% herbivores Y%
|
||||
carnivores Z%` (guarded on the arrays being sized).
|
||||
|
||||
## Verification (headless — extend the `src/sim` test set with `PlanetBiosphere.cpp`)
|
||||
|
||||
- **Vegetation:** 0 on ocean & Ice cells; near-0 on Desert; high (>0.6) on warm-wet Forest;
|
||||
all in [0,1]; finite; deterministic.
|
||||
- **Fauna food chain:** `sHerb` correlates with `sVeg`; `sCarn[i] > 0 ⇒ sHerb[i] >
|
||||
bioCarnPreyMin` and carnivore-positive cells are a strict subset of herbivore-rich cells;
|
||||
all in [0,1]; deterministic.
|
||||
- `test_logic`, biomes+config, climate suites still pass; app builds clean; no save change.
|
||||
- **GUI:** `8` lush green continents thinning to barren in deserts/poles/peaks; `9` grazer
|
||||
density following productivity; `0` predators concentrated in the richest belts; cell info
|
||||
shows vegetation/herbivore/carnivore %.
|
||||
`generateBiota()` uses a **separate RNG seeded from `cfg.seed`** (not `Planet::rngState`), so it
|
||||
never perturbs tectonic determinism (asserted in `test_biota.cpp`). The population is **saved**
|
||||
(save **v7**); densities are derived/not-saved. Older saves load with an empty population (press
|
||||
`L`). The archetype table is **append-only** — indices are serialized.
|
||||
|
||||
## Out of scope (future / Live World)
|
||||
|
||||
Time-evolving populations (growth, migration, predator-prey, extinction), seasons, and
|
||||
species/typed organisms — all part of the future **Live World** real-time simulation that
|
||||
runs the finished planet at hours/days/weeks/months with dynamic weather + life.
|
||||
Time-evolving populations (growth, migration, predator-prey dynamics, extinction), seasons, and
|
||||
true species/typed organisms — reserved for the **Live World** real-time mode. This stage
|
||||
answers *"what could live here"*, not *"what is living/evolving here"*.
|
||||
|
||||
91
docs/fauna_generation_plan.md
Normal file
91
docs/fauna_generation_plan.md
Normal file
@ -0,0 +1,91 @@
|
||||
Fauna Generation Plan
|
||||
|
||||
For fauna, I want the system to handle animals in broad ecological groups: predators, herbivores, and animals that fall somewhere in between.
|
||||
1. Basic Animal Types
|
||||
|
||||
First, we should define basic animal types that can exist in many different environments. These are not specific species, but general animal categories.
|
||||
|
||||
For example:
|
||||
|
||||
Rodents
|
||||
Desert rodents in desert regions
|
||||
Swamp rodents in swamp regions
|
||||
Forest rodents in forest regions
|
||||
|
||||
The same approach should be used for other common animal groups that can appear across many biomes.
|
||||
2. Animal Classification
|
||||
|
||||
For regional animals, we should describe them more by broad biological classification and size rather than by exact genus or species.
|
||||
|
||||
The classification should use something like:
|
||||
|
||||
Class
|
||||
Order
|
||||
Family
|
||||
Size category
|
||||
|
||||
Example:
|
||||
text
|
||||
|
||||
Animal: Cat-like predator
|
||||
Class: Mammal
|
||||
Order: Carnivora
|
||||
Family: Felidae
|
||||
Size: Small
|
||||
|
||||
A tiger-like animal would use the same family but have a larger size:
|
||||
text
|
||||
|
||||
Animal: Tiger-like predator
|
||||
Class: Mammal
|
||||
Order: Carnivora
|
||||
Family: Felidae
|
||||
Size: Big
|
||||
|
||||
Size categories could be:
|
||||
|
||||
Tiny
|
||||
Small
|
||||
Medium
|
||||
Big
|
||||
Huge
|
||||
|
||||
If more biological information is needed, please ask me. You can also look up general classification information if needed.
|
||||
3. Cell Population System
|
||||
|
||||
Each map cell should be populated using a slot and point system.
|
||||
|
||||
For example:
|
||||
|
||||
Each cell has 10 fauna slots
|
||||
Each cell has 20 fauna points
|
||||
|
||||
Every animal takes up a certain number of points depending on its size:
|
||||
Size Point Cost
|
||||
Tiny 1
|
||||
Small 2
|
||||
Medium 3
|
||||
Big 4
|
||||
Huge 5
|
||||
|
||||
Animals are added to the cell until either:
|
||||
|
||||
All slots are filled, or
|
||||
All points are used
|
||||
|
||||
Once one of these limits is reached, the system stops adding animals to that cell.
|
||||
4. Regional Distribution
|
||||
|
||||
To create a more homogeneous and natural distribution, the system should check neighboring cells when populating fauna.
|
||||
|
||||
The rules could be:
|
||||
|
||||
If neighboring cells have the same climate, there is a high probability that the same or similar animals appear there.
|
||||
If neighboring cells have a different climate, it is more likely animals should be generated instead.
|
||||
Similar biomes should share more fauna.
|
||||
Very different biomes should have more distinct fauna.
|
||||
|
||||
This should help avoid every cell feeling completely random while still allowing variety between different regions.
|
||||
5. Overall Goal
|
||||
|
||||
The goal is to create a fauna system that feels natural, biome-based, and regionally consistent, without needing to define every animal as an exact real-world species. Animals should be generated from broad biological groups, ecological roles, and size categories.
|
||||
368
docs/flora_generation_plan.md
Normal file
368
docs/flora_generation_plan.md
Normal file
@ -0,0 +1,368 @@
|
||||
Flora Generation Plan
|
||||
|
||||
For flora, I want the system to generate plant life based on biome, climate, terrain, water availability, and regional consistency. Like fauna, flora should not always be exact real-world species, but broader plant types that fit naturally into the environment.
|
||||
1. Basic Plant Types
|
||||
|
||||
First, we should define basic plant categories that can appear across many environments, with variations depending on biome and climate.
|
||||
|
||||
For example:
|
||||
|
||||
Grasses
|
||||
Dry grass in savannas
|
||||
Marsh grass in wetlands
|
||||
Alpine grass in mountains
|
||||
Shrubs
|
||||
Desert shrubs
|
||||
Thorny shrubs
|
||||
Berry bushes
|
||||
Trees
|
||||
Tropical trees
|
||||
Conifer trees
|
||||
Deciduous trees
|
||||
Mangrove trees
|
||||
Fungi
|
||||
Forest mushrooms
|
||||
Swamp fungi
|
||||
Cave fungi
|
||||
|
||||
These should work similarly to the “basic animal types” from the fauna system. A plant type can exist in many places, but its exact form changes depending on the environment.
|
||||
2. Flora Classification
|
||||
|
||||
Flora should be described using broad plant groups rather than exact species.
|
||||
|
||||
Possible classification fields:
|
||||
|
||||
Plant group
|
||||
Growth form
|
||||
Size category
|
||||
Climate preference
|
||||
Water requirement
|
||||
Terrain preference
|
||||
Ecological role
|
||||
|
||||
Example:
|
||||
text
|
||||
|
||||
Plant: Desert shrub
|
||||
Group: Angiosperm
|
||||
Growth Form: Shrub
|
||||
Size: Small
|
||||
Climate: Arid / Hot
|
||||
Water Requirement: Low
|
||||
Terrain: Sandy / Rocky
|
||||
Role: Ground cover / Food source
|
||||
|
||||
Another example:
|
||||
text
|
||||
|
||||
Plant: Giant rainforest tree
|
||||
Group: Angiosperm
|
||||
Growth Form: Tree
|
||||
Size: Huge
|
||||
Climate: Tropical / Humid
|
||||
Water Requirement: High
|
||||
Terrain: Soil-rich lowland
|
||||
Role: Canopy / Habitat
|
||||
|
||||
Possible growth forms:
|
||||
|
||||
Grass
|
||||
Moss
|
||||
Fern
|
||||
Shrub
|
||||
Bush
|
||||
Tree
|
||||
Vine
|
||||
Reed
|
||||
Cactus / succulent
|
||||
Fungus
|
||||
Aquatic plant
|
||||
|
||||
Possible size categories:
|
||||
|
||||
Tiny
|
||||
Small
|
||||
Medium
|
||||
Big
|
||||
Huge
|
||||
|
||||
3. Biome-Based Flora
|
||||
|
||||
Each biome should have a set of likely plant types.
|
||||
|
||||
Examples:
|
||||
Desert
|
||||
|
||||
Likely flora:
|
||||
|
||||
Cacti / succulents
|
||||
Dry shrubs
|
||||
Thorn bushes
|
||||
Sparse grasses
|
||||
Drought-resistant flowers
|
||||
|
||||
Rare flora:
|
||||
|
||||
Oasis trees
|
||||
Reeds near water
|
||||
Desert fungi after rain
|
||||
|
||||
Forest
|
||||
|
||||
Likely flora:
|
||||
|
||||
Deciduous trees
|
||||
Conifer trees
|
||||
Ferns
|
||||
Moss
|
||||
Mushrooms
|
||||
Berry bushes
|
||||
|
||||
Rare flora:
|
||||
|
||||
Ancient giant trees
|
||||
Poisonous flowers
|
||||
Medicinal herbs
|
||||
|
||||
Swamp / Wetland
|
||||
|
||||
Likely flora:
|
||||
|
||||
Reeds
|
||||
Mangroves
|
||||
Water lilies
|
||||
Moss
|
||||
Wetland grasses
|
||||
Fungi
|
||||
|
||||
Rare flora:
|
||||
|
||||
Carnivorous plants
|
||||
Giant swamp trees
|
||||
Rare medicinal roots
|
||||
|
||||
Grassland / Steppe
|
||||
|
||||
Likely flora:
|
||||
|
||||
Grasses
|
||||
Wildflowers
|
||||
Small shrubs
|
||||
Herbs
|
||||
|
||||
Rare flora:
|
||||
|
||||
Lone trees
|
||||
Thorn bushes
|
||||
Seasonal flowers
|
||||
|
||||
Mountain / Alpine
|
||||
|
||||
Likely flora:
|
||||
|
||||
Alpine grass
|
||||
Lichens
|
||||
Moss
|
||||
Small shrubs
|
||||
Hardy flowers
|
||||
|
||||
Rare flora:
|
||||
|
||||
Ancient mountain trees
|
||||
Rare herbs
|
||||
Snow-resistant plants
|
||||
|
||||
Tundra
|
||||
|
||||
Likely flora:
|
||||
|
||||
Moss
|
||||
Lichens
|
||||
Low shrubs
|
||||
Cold-resistant grasses
|
||||
|
||||
Rare flora:
|
||||
|
||||
Seasonal flowers
|
||||
Fungi
|
||||
Hardy berry bushes
|
||||
|
||||
4. Cell Population System
|
||||
|
||||
Flora can also use a slot and point system, similar to fauna.
|
||||
|
||||
For example:
|
||||
|
||||
Each cell has 15 flora slots
|
||||
Each cell has 30 flora points
|
||||
|
||||
Plants are added until either the slots or points are filled.
|
||||
|
||||
Suggested point cost by size:
|
||||
Size Point Cost
|
||||
Tiny 1
|
||||
Small 2
|
||||
Medium 3
|
||||
Big 5
|
||||
Huge 8
|
||||
|
||||
Flora may need more slots than fauna because plants are usually more numerous and layered.
|
||||
|
||||
A cell could contain:
|
||||
text
|
||||
|
||||
Cell Flora:
|
||||
- Forest floor moss, Tiny, 1 point
|
||||
- Ferns, Small, 2 points
|
||||
- Berry bushes, Small, 2 points
|
||||
- Deciduous trees, Big, 5 points
|
||||
- Ancient oak-like trees, Huge, 8 points
|
||||
|
||||
The system stops adding plants once either:
|
||||
|
||||
All flora slots are filled, or
|
||||
All flora points are used
|
||||
|
||||
5. Flora Layers
|
||||
|
||||
To make flora feel more natural, each cell can have vegetation layers.
|
||||
|
||||
Possible layers:
|
||||
|
||||
Ground layer
|
||||
Herb layer
|
||||
Shrub layer
|
||||
Understory layer
|
||||
Canopy layer
|
||||
Aquatic layer
|
||||
|
||||
Not every biome needs every layer.
|
||||
|
||||
Example for a forest:
|
||||
text
|
||||
|
||||
Ground Layer: Moss, fungi
|
||||
Herb Layer: Ferns, flowers
|
||||
Shrub Layer: Berry bushes
|
||||
Understory Layer: Young trees
|
||||
Canopy Layer: Large trees
|
||||
|
||||
Example for a desert:
|
||||
text
|
||||
|
||||
Ground Layer: Dry grass, small succulents
|
||||
Shrub Layer: Thorn bushes
|
||||
Tree Layer: Rare oasis trees
|
||||
|
||||
This helps prevent weird combinations, like a dense rainforest canopy appearing in a dry desert cell.
|
||||
6. Climate and Terrain Rules
|
||||
|
||||
Flora should be strongly influenced by environmental conditions.
|
||||
|
||||
Important factors:
|
||||
|
||||
Temperature
|
||||
Rainfall
|
||||
Soil quality
|
||||
Terrain type
|
||||
Elevation
|
||||
Nearby water
|
||||
Sunlight
|
||||
Seasonality
|
||||
|
||||
Examples:
|
||||
|
||||
High rainfall increases trees, moss, ferns, and fungi.
|
||||
Low rainfall increases cacti, succulents, dry shrubs, and sparse grasses.
|
||||
Cold climate increases moss, lichen, conifers, and low shrubs.
|
||||
High elevation reduces large trees and favors alpine plants.
|
||||
Wet terrain increases reeds, aquatic plants, mangroves, and swamp trees.
|
||||
Poor soil reduces plant density.
|
||||
Fertile soil increases plant diversity and size.
|
||||
|
||||
7. Regional Distribution
|
||||
|
||||
Like fauna, flora should check neighboring cells to create a more natural and homogeneous distribution.
|
||||
|
||||
Rules could be:
|
||||
|
||||
Same biome and same climate: high probability of sharing the same flora.
|
||||
Same climate but different terrain: moderate probability of similar flora.
|
||||
Different climate: generate new flora.
|
||||
Nearby rivers, lakes, and coasts can spread wetland or aquatic plants.
|
||||
Mountain ranges, deserts, and oceans can act as barriers to plant spread.
|
||||
|
||||
This means forests should gradually change into grasslands or swamps instead of switching completely from one cell to the next.
|
||||
8. Rarity and Special Plants
|
||||
|
||||
The system should support common, uncommon, rare, and unique flora.
|
||||
|
||||
Possible rarity levels:
|
||||
|
||||
Common
|
||||
Uncommon
|
||||
Rare
|
||||
Very rare
|
||||
Unique
|
||||
|
||||
Examples:
|
||||
text
|
||||
|
||||
Common: Grass, moss, reeds
|
||||
Uncommon: Berry bushes, medicinal herbs
|
||||
Rare: Carnivorous plants, glowing mushrooms
|
||||
Very Rare: Ancient trees, magical flowers
|
||||
Unique: World-tree fragment, legendary herb
|
||||
|
||||
Rarity can be affected by biome and world rules.
|
||||
|
||||
For example:
|
||||
|
||||
Carnivorous plants are rare in swamps.
|
||||
Glowing mushrooms are rare in caves or dark forests.
|
||||
Ancient trees are rare in old forests.
|
||||
Medicinal herbs are uncommon in mountains or forests.
|
||||
|
||||
9. Ecological Role
|
||||
|
||||
Each plant type should have an ecological role. This can later connect flora to fauna, crafting, survival, or gameplay systems.
|
||||
|
||||
Possible roles:
|
||||
|
||||
Food source
|
||||
Shelter
|
||||
Nesting material
|
||||
Medicine
|
||||
Poison
|
||||
Crafting material
|
||||
Building material
|
||||
Fuel
|
||||
Soil stabilizer
|
||||
Water indicator
|
||||
Magical / special resource
|
||||
|
||||
Example:
|
||||
text
|
||||
|
||||
Plant: Berry bush
|
||||
Growth Form: Bush
|
||||
Size: Small
|
||||
Climate: Temperate
|
||||
Water Requirement: Medium
|
||||
Role: Food source for animals and humans
|
||||
|
||||
Example:
|
||||
text
|
||||
|
||||
Plant: Thorn shrub
|
||||
Growth Form: Shrub
|
||||
Size: Small
|
||||
Climate: Arid
|
||||
Water Requirement: Low
|
||||
Role: Shelter for small animals, natural barrier
|
||||
|
||||
10. Overall Goal
|
||||
|
||||
The goal is to create a flora system that feels natural, biome-based, and regionally consistent. Plants should be generated from broad categories, growth forms, ecological roles, and environmental requirements rather than needing to define every exact species.
|
||||
|
||||
The flora system should also support later gameplay features such as animal habitats, food chains, crafting materials, medicine, poison, and rare discoveries.
|
||||
@ -88,10 +88,40 @@ const char* colorModeName(ColorMode m) {
|
||||
case ColorMode::Biome: return "Biome";
|
||||
case ColorMode::Temperature: return "Temperature";
|
||||
case ColorMode::Precip: return "Precipitation";
|
||||
case ColorMode::FloraDensity: return "Flora density";
|
||||
case ColorMode::FaunaDensity: return "Fauna density";
|
||||
case ColorMode::FungaDensity: return "Funga density";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
// Two-colour density ramp helper: barren -> rich.
|
||||
static Color ramp2(double d01, const unsigned char lo[3], const unsigned char hi[3]) {
|
||||
double t = std::clamp(d01, 0.0, 1.0);
|
||||
auto L = [&](int c) { return (unsigned char)(lo[c] + (hi[c] - lo[c]) * t); };
|
||||
return Color{ L(0), L(1), L(2), 255 };
|
||||
}
|
||||
|
||||
Color floraColor(double d01) { // barren tan -> lush green
|
||||
static const unsigned char lo[3] = { 200, 190, 150 }, hi[3] = { 25, 120, 35 };
|
||||
return ramp2(d01, lo, hi);
|
||||
}
|
||||
Color faunaColor(double d01) { // pale -> amber -> red
|
||||
double t = std::clamp(d01, 0.0, 1.0);
|
||||
static const unsigned char key[3][3] = {
|
||||
{ 225, 220, 195 }, // 0.0 pale
|
||||
{ 220, 160, 60 }, // 0.5 amber
|
||||
{ 180, 55, 40 }, // 1.0 red
|
||||
};
|
||||
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 };
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Temperature ramp over ~[-40, 40] C: deep blue -> cyan -> green -> yellow -> red.
|
||||
Color tempColor(double celsius) {
|
||||
double t = std::clamp((celsius + 40.0) / 80.0, 0.0, 1.0); // 0 cold .. 1 hot
|
||||
|
||||
@ -4,7 +4,8 @@
|
||||
|
||||
// Cell color mapping for the viewer. Pure functions of cell properties.
|
||||
|
||||
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip };
|
||||
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
|
||||
FloraDensity, FaunaDensity, FungaDensity };
|
||||
|
||||
Color elevationColor(double e, double seaLevel);
|
||||
Color plateColor(int id);
|
||||
@ -21,3 +22,8 @@ const char* colorModeName(ColorMode m);
|
||||
// (tan dry -> green -> blue wet).
|
||||
Color tempColor(double celsius);
|
||||
Color precipColor(double moist01);
|
||||
// Biota density ramps (0..1): flora barren->lush green, fauna pale->amber/red,
|
||||
// funga pale->violet/brown.
|
||||
Color floraColor(double d01);
|
||||
Color faunaColor(double d01);
|
||||
Color fungaColor(double d01);
|
||||
|
||||
@ -1,10 +1,34 @@
|
||||
#include "Panels.hpp"
|
||||
#include "Colors.hpp" // elevationColor (subtile grid)
|
||||
#include "PlanetBiota.hpp" // organismName / sizeName / roleName
|
||||
#include "Projection.hpp" // dirToLonLat
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
|
||||
// Draw `text` word-wrapped to `maxW` pixels starting at (x,y); continuation lines
|
||||
// are indented. Returns the y after the last line; stops drawing past `maxY` (but
|
||||
// keeps advancing y so callers can detect the overflow). Long biota lists would
|
||||
// otherwise run off the right edge of the cell-info panel.
|
||||
static int drawWrapped(const std::string& text, int x, int y, int font, Color col,
|
||||
int maxW, int lineH, int maxY) {
|
||||
std::istringstream iss(text);
|
||||
std::string word, line;
|
||||
int indent = 0;
|
||||
auto flush = [&]() {
|
||||
if (!line.empty()) { if (y + lineH <= maxY) DrawText(line.c_str(), x + indent, y, font, col);
|
||||
y += lineH; line.clear(); indent = 14; }
|
||||
};
|
||||
while (iss >> word) {
|
||||
std::string test = line.empty() ? word : line + " " + word;
|
||||
if (MeasureText(test.c_str(), font) > maxW - indent && !line.empty()) { flush(); line = word; }
|
||||
else line = test;
|
||||
}
|
||||
flush();
|
||||
return y;
|
||||
}
|
||||
|
||||
// elev/age come from the display snapshot so the readout matches what is drawn.
|
||||
static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, double age) {
|
||||
@ -32,6 +56,39 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
|
||||
L.push_back(std::string(TextFormat("river: discharge %.0f", p.discharge()[i])));
|
||||
if (sized(p.lakeDepth()) && p.lakeDepth()[i] > p.cfg.biomeLakeMinDepth && elev > p.cfg.seaLevel)
|
||||
L.push_back(std::string(TextFormat("lake: depth %.0f m", p.lakeDepth()[i])));
|
||||
// Biota: density scalars (present after computeBiotaDensity()) + the discrete
|
||||
// population list (present once generateBiota()/L has run).
|
||||
if (sized(p.floraDensity()) && sized(p.faunaDensity()) && sized(p.fungaDensity()))
|
||||
L.push_back(std::string(TextFormat("flora %.0f%% fauna %.0f%% funga %.0f%%",
|
||||
p.floraDensity()[i] * 100.0, p.faunaDensity()[i] * 100.0, p.fungaDensity()[i] * 100.0)));
|
||||
if (p.biotaPopulated() && i < (int)p.biota().size()) {
|
||||
const CellBiota& cb = p.biota()[i];
|
||||
// Each organism reads as Family (Size, Role) -- proper taxonomy, never an
|
||||
// informal common name; generalists carry a biome adjective ("Forest Felidae").
|
||||
// Identical archetypes in a cell aggregate to "... xN" so the list stays clean.
|
||||
auto listKind = [&](const char* tag, const std::vector<Organism>& v) {
|
||||
if (v.empty()) return;
|
||||
std::vector<std::pair<Organism, int>> uniq; // representative + count, first-seen order
|
||||
for (const Organism& o : v) {
|
||||
bool found = false;
|
||||
for (auto& u : uniq) if (u.first.archetype == o.archetype) { ++u.second; found = true; break; }
|
||||
if (!found) uniq.push_back({o, 1});
|
||||
}
|
||||
std::string s = tag;
|
||||
int shown = (int)std::min<size_t>(uniq.size(), 6);
|
||||
for (int k = 0; k < shown; ++k) {
|
||||
const BiotaArchetype& a = biotaArchetypes()[uniq[k].first.archetype];
|
||||
s += (k ? ", " : " ") + organismName(uniq[k].first) +
|
||||
" (" + sizeName(a.size) + ", " + roleName(a.role) + ")";
|
||||
if (uniq[k].second > 1) s += TextFormat(" x%d", uniq[k].second);
|
||||
}
|
||||
if ((int)uniq.size() > shown) s += TextFormat(", +%d more", (int)uniq.size() - shown);
|
||||
L.push_back(s);
|
||||
};
|
||||
listKind("Flora:", cb.flora);
|
||||
listKind("Fauna:", cb.fauna);
|
||||
listKind("Funga:", cb.funga);
|
||||
}
|
||||
return L;
|
||||
}
|
||||
|
||||
@ -46,9 +103,10 @@ void drawDetailPanel(const Planet& p, const std::shared_ptr<SubGrid>& sg,
|
||||
ty += 28;
|
||||
// Cramped above the subtile grid -> stop before overlapping it (the full list is
|
||||
// always shown in the top-right hover panel, which has room).
|
||||
int infoMaxW = (int)(panel.x + panel.width) - tx - 10;
|
||||
for (auto& s : cellInfo(p, macro, macroElev, macroAge)) {
|
||||
if (ty + 18 > (int)grid.y) break;
|
||||
DrawText(s.c_str(), tx, ty, 15, Color{210, 210, 220, 255}); ty += 18;
|
||||
if (ty + 16 > (int)grid.y) break;
|
||||
ty = drawWrapped(s, tx, ty, 14, Color{210, 210, 220, 255}, infoMaxW, 16, (int)grid.y);
|
||||
}
|
||||
|
||||
if (!sg || sg->res < 2) return;
|
||||
@ -97,8 +155,11 @@ void drawHoverPanel(const Planet& p, Rectangle r, int hovered, int selected) {
|
||||
return;
|
||||
}
|
||||
if (hovered < 0) { DrawText("(selected tile)", x, y, 18, Color{210, 180, 120, 255}); y += 30; }
|
||||
int maxW = (int)(r.x + r.width) - x - 14; // wrap to the panel's inner width
|
||||
int maxY = (int)(r.y + r.height) - 10; // clamp to the panel bottom
|
||||
for (auto& s : cellInfo(p, shown, p.cells[shown].elevation, p.cells[shown].geoAge)) {
|
||||
DrawText(s.c_str(), x, y, 24, Color{215, 220, 230, 255}); y += 32;
|
||||
y = drawWrapped(s, x, y, 20, Color{215, 220, 230, 255}, maxW, 26, maxY);
|
||||
if (y > maxY) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -97,6 +97,9 @@ void Viewer::recolor() {
|
||||
double maxAge = 1.0; for (const auto& c : planet.cells) maxAge = std::max(maxAge, c.geoAge);
|
||||
const std::vector<double>& temp = planet.temperature();
|
||||
const std::vector<double>& moist = planet.moisture(); // 0..1, already robustly normalized
|
||||
const std::vector<double>& flora = planet.floraDensity();
|
||||
const std::vector<double>& fauna = planet.faunaDensity();
|
||||
const std::vector<double>& funga = planet.fungaDensity();
|
||||
vcolors.resize(planet.cells.size());
|
||||
for (size_t i = 0; i < planet.cells.size(); ++i) {
|
||||
switch (mode) {
|
||||
@ -112,6 +115,9 @@ void Viewer::recolor() {
|
||||
case ColorMode::Biome: vcolors[i] = biomeColor(planet.cells[i].biome); break;
|
||||
case ColorMode::Temperature: vcolors[i] = temp.empty() ? Color{90,90,90,255} : tempColor(temp[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::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);
|
||||
}
|
||||
}
|
||||
@ -128,6 +134,7 @@ void Viewer::refreshView() {
|
||||
if (phase3) planet.computeHydrology(); // refresh lakes/rivers for the view
|
||||
planet.computeClimate(); // temperature + precipitation fields
|
||||
planet.classifyBiomes(); // keep cell.biome current (reads the climate)
|
||||
planet.computeBiotaDensity(); // flora/fauna/funga density (population is on-demand)
|
||||
recolor();
|
||||
if (settled) { // Phase 2: plates moved -> boundaries moved
|
||||
buildBorders(planet, borderR, borders, ridgeBorders);
|
||||
@ -188,7 +195,7 @@ void Viewer::loadGame(const char* path) {
|
||||
if (ver >= 2) is.read(reinterpret_cast<char*>(&dr), sizeof dr);
|
||||
if (ver >= 3) is.read(reinterpret_cast<char*>(&p3), sizeof p3);
|
||||
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
||||
if (!planet.readState(is, ver >= 4)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: per-cell biome
|
||||
if (!planet.readState(is, ver >= 4, ver >= 7)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4: biome, v7: biota
|
||||
cfg = planet.cfg; // adopt the loaded config
|
||||
elapsedMy = em; settled = (st != 0);
|
||||
planet.drifting = settled; // resume drift boosts iff mid-drift
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||
struct Viewer {
|
||||
// ---- Files / save format ------------------------------------------------
|
||||
static constexpr uint32_t SAVE_VERSION = 6; // v6: self-describing (text) config block; v4: +biome; v3: +phase3
|
||||
static constexpr uint32_t SAVE_VERSION = 7; // v7: +biota population; v6: self-describing config; v4: +biome; v3: +phase3
|
||||
const char* CONFIG_PATH = "planet.cfg";
|
||||
const char* SAVE_PATH = "planet.save";
|
||||
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
||||
|
||||
@ -104,6 +104,9 @@ void Viewer::handleInput() {
|
||||
if (IsKeyPressed(KEY_FIVE)) { mode = ColorMode::Biome; recolor(); }
|
||||
if (IsKeyPressed(KEY_SIX)) { mode = ColorMode::Temperature; recolor(); }
|
||||
if (IsKeyPressed(KEY_SEVEN)) { mode = ColorMode::Precip; recolor(); }
|
||||
if (IsKeyPressed(KEY_EIGHT)) { mode = ColorMode::FloraDensity; recolor(); }
|
||||
if (IsKeyPressed(KEY_NINE)) { mode = ColorMode::FaunaDensity; recolor(); }
|
||||
if (IsKeyPressed(KEY_ZERO)) { mode = ColorMode::FungaDensity; recolor(); }
|
||||
if (IsKeyPressed(KEY_B)) showBorders = !showBorders;
|
||||
if (IsKeyPressed(KEY_D)) showDrift = !showDrift;
|
||||
if (IsKeyPressed(KEY_G)) showGrat = !showGrat;
|
||||
@ -116,6 +119,12 @@ void Viewer::handleInput() {
|
||||
else { phase3PromptAt = elapsedMy + planet.cfg.phase3AfterMy; rivers.clear(); bigRivers.clear(); setStatus("Hydrology OFF"); }
|
||||
refreshView();
|
||||
}
|
||||
if (IsKeyPressed(KEY_L) && settled) { // generate / regenerate biota population
|
||||
planet.generateBiota();
|
||||
if (mode != ColorMode::FaunaDensity && mode != ColorMode::FungaDensity)
|
||||
{ mode = ColorMode::FloraDensity; recolor(); }
|
||||
setStatus("Biota generated (flora/fauna/funga)");
|
||||
}
|
||||
if (IsKeyPressed(KEY_C)) { selectedCell = -1; subgrids.clear(); }
|
||||
if (IsKeyPressed(KEY_R)) { cfg.seed = (uint32_t)(GetTime() * 100000) | 1; regen(); }
|
||||
if (IsKeyPressed(KEY_S)) { stepOnce(); refreshView(); } // one tick (handy while paused/settled)
|
||||
|
||||
@ -178,11 +178,11 @@ void Viewer::renderHUD() {
|
||||
}
|
||||
y += 8;
|
||||
line("hover: cell info | click tile: open detail panel | C close");
|
||||
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp 7 precip");
|
||||
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp 7 precip 8 flora 9 fauna 0 funga");
|
||||
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s]",
|
||||
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off"));
|
||||
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | R reseed | +/-",
|
||||
phase3 ? "on" : "off"));
|
||||
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | R reseed | +/-",
|
||||
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off"));
|
||||
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
||||
if (!statusMsg.empty() && GetTime() < statusUntil) {
|
||||
y += 4; DrawText(statusMsg.c_str(), 12, y, 18, Color{120, 230, 140, 255}); y += 22;
|
||||
|
||||
@ -35,6 +35,7 @@ void Planet::generate(const PlanetConfig& c) {
|
||||
seedInitialRelief();
|
||||
computeClimate(); // temperature + precipitation fields (biomes read these)
|
||||
classifyBiomes(); // give the fresh world an initial biome per cell
|
||||
computeBiotaDensity(); // derived flora/fauna/funga density (population is on-demand)
|
||||
}
|
||||
|
||||
// Build the icosphere and copy fixed geometry (unit direction + neighbor
|
||||
@ -47,6 +48,8 @@ void Planet::buildGeometry() {
|
||||
cells[i].unit = sphere.positions[i];
|
||||
cells[i].neighbors = sphere.neighbors[i];
|
||||
}
|
||||
sBiota.assign(cells.size(), {}); // empty biota population until generateBiota()
|
||||
sHasBiota = false;
|
||||
}
|
||||
|
||||
void Planet::assignPlates() {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#include "Vec3.hpp"
|
||||
#include "IcoSphere.hpp"
|
||||
#include "PlanetTypes.hpp" // Cell, Plate, SubGrid/SubCell, PlanetConfig
|
||||
#include "PlanetBiota.hpp" // BiotaKind, Organism, CellBiota
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
@ -58,6 +59,20 @@ public:
|
||||
// fields (temperature + normalized precipitation). Derived + written back into
|
||||
// cell.biome (saved). Assumes computeClimate() ran this tick. Re-run as terrain evolves.
|
||||
void classifyBiomes();
|
||||
|
||||
// Biota stage (flora/fauna/funga). computeBiotaDensity() builds the derived
|
||||
// per-cell density scalars (0..1) each tick (like climate; not saved); call it
|
||||
// after classifyBiomes(). generateBiota() does the on-demand slot/point fill of
|
||||
// the discrete population into sBiota (saved) -- NOT called per tick. See
|
||||
// PlanetBiota.cpp + PlanetFloraGen/FaunaGen/FungiGen.cpp.
|
||||
void computeBiotaDensity();
|
||||
void generateBiota();
|
||||
bool biotaPopulated() const;
|
||||
const std::vector<double>& floraDensity() const { return sFloraDensity; }
|
||||
const std::vector<double>& faunaDensity() const { return sFaunaDensity; }
|
||||
const std::vector<double>& fungaDensity() const { return sFungaDensity; }
|
||||
const std::vector<CellBiota>& biota() const { return sBiota; }
|
||||
|
||||
// Derived hydrology fields (recomputed each route; not saved). Empty until
|
||||
// the first computeHydrology()/hydrology() call.
|
||||
const std::vector<double>& lakeDepth() const { return sLakeDepth; }
|
||||
@ -73,7 +88,8 @@ public:
|
||||
void writeState(std::ostream& os) const;
|
||||
// hasBiome: whether the stream carries the per-cell biome byte (save v4+). For
|
||||
// older saves (v3) pass false -- biomes are reclassified after the cells load.
|
||||
bool readState(std::istream& is, bool hasBiome = true);
|
||||
// hasBiota: whether the stream carries the biota population block (save v7+).
|
||||
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true);
|
||||
|
||||
// Helpers for rendering / info.
|
||||
double cellWidthMeters() const; // approx lateral cell spacing
|
||||
@ -110,6 +126,17 @@ private:
|
||||
// Phase-3 hydrology helpers (see hydrology()).
|
||||
void routeFlow(); // depression-fill -> lakes, flow, discharge
|
||||
|
||||
// Biota helpers (PlanetFloraGen/FaunaGen/FungiGen.cpp). compute*Density write the
|
||||
// derived scalars; fill* draw the per-cell population (nbr = already-filled,
|
||||
// same-biome neighbours, for regional consistency).
|
||||
void computeFloraDensity();
|
||||
void computeFaunaDensity();
|
||||
void computeFungaDensity();
|
||||
double neighbourhoodPrey(int i) const; // mean fauna density over i + neighbours
|
||||
std::vector<Organism> fillFlora(int i, const std::vector<int>& nbr, uint32_t& rng);
|
||||
std::vector<Organism> fillFauna(int i, const std::vector<int>& nbr, uint32_t& rng);
|
||||
std::vector<Organism> fillFunga(int i, const std::vector<int>& nbr, uint32_t& rng);
|
||||
|
||||
int driftIter = 0; // counts advect() calls (gates periodic checks)
|
||||
int erodeIter = 0; // counts erode() calls (gates sea-level control)
|
||||
std::vector<int> sPrevCount; // per-plate cell count at the previous check
|
||||
@ -129,6 +156,12 @@ private:
|
||||
std::vector<double> sTemp, sPrecip, sMoist;
|
||||
std::vector<Vec3> sWind;
|
||||
std::vector<int> sUpwind;
|
||||
|
||||
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
||||
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
||||
std::vector<double> sFloraDensity, sFaunaDensity, sFungaDensity;
|
||||
std::vector<CellBiota> sBiota;
|
||||
bool sHasBiota = false;
|
||||
};
|
||||
|
||||
// Human-editable config file (key = value text). All PlanetConfig input
|
||||
|
||||
244
src/sim/PlanetBiota.cpp
Normal file
244
src/sim/PlanetBiota.cpp
Normal file
@ -0,0 +1,244 @@
|
||||
#include "Planet.hpp"
|
||||
#include "PlanetBiota.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <initializer_list>
|
||||
|
||||
// --- Biota stage: archetype library + shared slot/point machinery + driver ----
|
||||
// The per-kind environmental rules + density live in PlanetFloraGen/FaunaGen/
|
||||
// FungiGen.cpp; this file holds the comprehensive archetype table, the display
|
||||
// helpers, the shared draw (slot/point) routine, and the on-demand generateBiota()
|
||||
// driver that fills the discrete population with neighbour-aware regional spread.
|
||||
|
||||
// Comprehensive archetype library. APPEND-ONLY: indices are serialized inside
|
||||
// saved Organisms, so never reorder or delete entries (add new ones at the end).
|
||||
const std::vector<BiotaArchetype>& biotaArchetypes() {
|
||||
using K = BiotaKind; using R = EcoRole; using S = SizeClass; using B = Biome;
|
||||
static const std::vector<BiotaArchetype> T = [] {
|
||||
auto M = [](std::initializer_list<B> bs) {
|
||||
uint32_t m = 0; for (B b : bs) m |= 1u << (unsigned)b; return m;
|
||||
};
|
||||
std::vector<BiotaArchetype> a;
|
||||
// ---- Flora ----------------------------------------------------------
|
||||
a.push_back({ "Broadleaf tree", K::Flora, R::Tree, "Magnoliopsida", "Fagales", "Fagaceae",
|
||||
S::Big, M({B::Forest, B::Hills, B::Wetland, B::Grassland}), 4.0, 35.0, 0.45, false });
|
||||
a.push_back({ "Conifer", K::Flora, R::Tree, "Pinopsida", "Pinales", "Pinaceae",
|
||||
S::Big, M({B::Taiga, B::Forest, B::Mountains, B::Hills}), -12.0, 20.0, 0.28, false });
|
||||
a.push_back({ "Tropical palm", K::Flora, R::Tree, "Liliopsida", "Arecales", "Arecaceae",
|
||||
S::Medium, M({B::Forest, B::Beach, B::Wetland}), 18.0, 42.0, 0.40, false });
|
||||
a.push_back({ "Tall grass", K::Flora, R::Grass, "Liliopsida", "Poales", "Poaceae",
|
||||
S::Tiny, M({B::Grassland, B::Savanna, B::Wetland}), 2.0, 38.0, 0.25, false });
|
||||
a.push_back({ "Steppe grass", K::Flora, R::Grass, "Liliopsida", "Poales", "Poaceae",
|
||||
S::Tiny, M({B::Grassland, B::Savanna, B::Tundra, B::Desert}), -6.0, 38.0, 0.10, true });
|
||||
a.push_back({ "Scrub", K::Flora, R::Shrub, "Magnoliopsida", "Lamiales", "Lamiaceae",
|
||||
S::Small, M({B::Savanna, B::Grassland, B::Desert, B::Hills, B::Tundra}), -6.0, 40.0, 0.08, true });
|
||||
a.push_back({ "Succulent", K::Flora, R::Succulent, "Magnoliopsida", "Caryophyllales", "Cactaceae",
|
||||
S::Small, M({B::Desert, B::Savanna}), 4.0, 48.0, 0.0, false });
|
||||
a.push_back({ "Reed", K::Flora, R::Aquatic, "Liliopsida", "Poales", "Cyperaceae",
|
||||
S::Small, M({B::Wetland, B::Beach}), 2.0, 36.0, 0.50, false });
|
||||
a.push_back({ "Mangrove", K::Flora, R::Tree, "Magnoliopsida", "Malpighiales", "Rhizophoraceae",
|
||||
S::Medium, M({B::Beach, B::Wetland}), 16.0, 40.0, 0.40, false });
|
||||
a.push_back({ "Fern", K::Flora, R::Shrub, "Polypodiopsida", "Polypodiales", "Polypodiaceae",
|
||||
S::Tiny, M({B::Forest, B::Taiga, B::Wetland}), 0.0, 32.0, 0.40, false });
|
||||
a.push_back({ "Cushion moss", K::Flora, R::Moss, "Bryopsida", "Bryales", "Bryaceae",
|
||||
S::Tiny, M({B::Tundra, B::Mountains, B::Taiga}), -28.0, 10.0, 0.15, false });
|
||||
a.push_back({ "Alpine wildflower", K::Flora, R::Shrub, "Magnoliopsida", "Saxifragales", "Saxifragaceae",
|
||||
S::Tiny, M({B::Mountains, B::Hills, B::Tundra}), -16.0, 16.0, 0.20, true });
|
||||
a.push_back({ "Giant canopy tree", K::Flora, R::Tree, "Magnoliopsida", "Malvales", "Malvaceae",
|
||||
S::Huge, M({B::Forest}), 18.0, 40.0, 0.60, false }); // rainforest emergent
|
||||
a.push_back({ "Berry bush", K::Flora, R::Shrub, "Magnoliopsida", "Rosales", "Rosaceae",
|
||||
S::Small, M({B::Forest, B::Taiga, B::Grassland, B::Hills}), -8.0, 28.0, 0.35, false });
|
||||
a.push_back({ "Wildflower", K::Flora, R::Shrub, "Magnoliopsida", "Asterales", "Asteraceae",
|
||||
S::Tiny, M({B::Grassland, B::Savanna, B::Hills, B::Forest}), 2.0, 35.0, 0.20, false });
|
||||
a.push_back({ "Water lily", K::Flora, R::Aquatic, "Magnoliopsida", "Nymphaeales", "Nymphaeaceae",
|
||||
S::Tiny, M({B::Wetland}), 8.0, 36.0, 0.60, false });
|
||||
// ---- Fauna (named by Family; `name` is the family vernacular, display uses
|
||||
// the Family rank -- e.g. Felidae, never "cat") -------------------
|
||||
a.push_back({ "Murid", K::Fauna, R::Herbivore, "Mammalia", "Rodentia", "Muridae",
|
||||
S::Tiny, M({B::Grassland, B::Savanna, B::Forest, B::Taiga, B::Desert,
|
||||
B::Wetland, B::Tundra, B::Hills, B::Mountains, B::Beach}), -12.0, 42.0, 0.0, true });
|
||||
a.push_back({ "Leporid", K::Fauna, R::Herbivore, "Mammalia", "Lagomorpha", "Leporidae",
|
||||
S::Small, M({B::Grassland, B::Savanna, B::Tundra, B::Forest, B::Desert}), -16.0, 38.0, 0.05, true });
|
||||
a.push_back({ "Cervid", K::Fauna, R::Herbivore, "Mammalia", "Artiodactyla", "Cervidae",
|
||||
S::Medium, M({B::Forest, B::Grassland, B::Savanna, B::Taiga, B::Hills}), -12.0, 35.0, 0.20, false });
|
||||
a.push_back({ "Bovid", K::Fauna, R::Herbivore, "Mammalia", "Artiodactyla", "Bovidae",
|
||||
S::Big, M({B::Grassland, B::Savanna}), 0.0, 40.0, 0.18, false });
|
||||
a.push_back({ "Proboscid", K::Fauna, R::Herbivore, "Mammalia", "Proboscidea", "Elephantidae",
|
||||
S::Huge, M({B::Savanna, B::Grassland, B::Forest}), 10.0, 42.0, 0.30, false });
|
||||
a.push_back({ "Suid", K::Fauna, R::Omnivore, "Mammalia", "Artiodactyla", "Suidae",
|
||||
S::Medium, M({B::Forest, B::Wetland, B::Grassland, B::Hills}), -2.0, 36.0, 0.25, false });
|
||||
a.push_back({ "Ursid", K::Fauna, R::Omnivore, "Mammalia", "Carnivora", "Ursidae",
|
||||
S::Big, M({B::Forest, B::Taiga, B::Mountains, B::Tundra}), -22.0, 26.0, 0.20, false });
|
||||
a.push_back({ "Cercopithecid", K::Fauna, R::Omnivore, "Mammalia", "Primates", "Cercopithecidae",
|
||||
S::Medium, M({B::Forest, B::Wetland}), 15.0, 40.0, 0.45, false });
|
||||
a.push_back({ "Felid (small)", K::Fauna, R::Carnivore, "Mammalia", "Carnivora", "Felidae",
|
||||
S::Small, M({B::Forest, B::Grassland, B::Savanna, B::Desert, B::Wetland, B::Hills}), 0.0, 42.0, 0.10, true });
|
||||
a.push_back({ "Felid (large)", K::Fauna, R::Carnivore, "Mammalia", "Carnivora", "Felidae",
|
||||
S::Big, M({B::Savanna, B::Grassland, B::Forest, B::Hills}), 5.0, 42.0, 0.18, true });
|
||||
a.push_back({ "Canid", K::Fauna, R::Carnivore, "Mammalia", "Carnivora", "Canidae",
|
||||
S::Medium, M({B::Grassland, B::Savanna, B::Forest, B::Taiga, B::Tundra,
|
||||
B::Desert, B::Hills, B::Mountains}), -26.0, 40.0, 0.05, true });
|
||||
a.push_back({ "Mustelid", K::Fauna, R::Carnivore, "Mammalia", "Carnivora", "Mustelidae",
|
||||
S::Tiny, M({B::Forest, B::Taiga, B::Wetland, B::Grassland, B::Tundra}), -22.0, 30.0, 0.15, false });
|
||||
a.push_back({ "Varanid", K::Fauna, R::Carnivore, "Reptilia", "Squamata", "Varanidae",
|
||||
S::Small, M({B::Desert, B::Savanna}), 12.0, 50.0, 0.0, false });
|
||||
a.push_back({ "Ranid", K::Fauna, R::Omnivore, "Amphibia", "Anura", "Ranidae",
|
||||
S::Tiny, M({B::Wetland, B::Forest, B::Beach}), 5.0, 35.0, 0.50, false });
|
||||
a.push_back({ "Phasianid", K::Fauna, R::Herbivore, "Aves", "Galliformes", "Phasianidae",
|
||||
S::Medium, M({B::Grassland, B::Savanna, B::Tundra, B::Desert, B::Forest}), -12.0, 42.0, 0.05, true });
|
||||
a.push_back({ "Accipitrid", K::Fauna, R::Carnivore, "Aves", "Accipitriformes", "Accipitridae",
|
||||
S::Small, M({B::Grassland, B::Savanna, B::Forest, B::Mountains, B::Tundra,
|
||||
B::Desert, B::Hills, B::Wetland}), -16.0, 42.0, 0.0, true });
|
||||
// ---- Funga ----------------------------------------------------------
|
||||
a.push_back({ "Decomposer mushroom", K::Funga, R::Decomposer, "Agaricomycetes", "Agaricales", "Agaricaceae",
|
||||
S::Tiny, M({B::Forest, B::Taiga, B::Wetland, B::Grassland, B::Hills}), -6.0, 32.0, 0.35, true });
|
||||
a.push_back({ "Mould", K::Funga, R::Decomposer, "Eurotiomycetes", "Eurotiales", "Aspergillaceae",
|
||||
S::Tiny, M({B::Wetland, B::Forest, B::Beach}), 0.0, 40.0, 0.45, false });
|
||||
a.push_back({ "Mycorrhizal fungus", K::Funga, R::Mycorrhizal, "Agaricomycetes", "Boletales", "Boletaceae",
|
||||
S::Tiny, M({B::Forest, B::Taiga, B::Hills}), -12.0, 30.0, 0.30, false });
|
||||
a.push_back({ "Bracket fungus", K::Funga, R::Decomposer, "Agaricomycetes", "Polyporales", "Polyporaceae",
|
||||
S::Small, M({B::Forest, B::Taiga}), -6.0, 28.0, 0.40, false });
|
||||
a.push_back({ "Lichen", K::Funga, R::Lichen, "Lecanoromycetes", "Lecanorales", "Parmeliaceae",
|
||||
S::Tiny, M({B::Tundra, B::Mountains, B::Taiga, B::Beach}), -32.0, 16.0, 0.10, true });
|
||||
a.push_back({ "Slime mould", K::Funga, R::Decomposer, "Myxomycetes", "Physarales", "Physaraceae",
|
||||
S::Tiny, M({B::Forest, B::Wetland}), 5.0, 32.0, 0.50, false });
|
||||
a.push_back({ "Parasitic blight", K::Funga, R::Parasite, "Sordariomycetes", "Hypocreales", "Clavicipitaceae",
|
||||
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 });
|
||||
return a;
|
||||
}();
|
||||
return T;
|
||||
}
|
||||
|
||||
int pointCost(SizeClass s) { return (int)s + 1; } // Tiny=1 .. Huge=5
|
||||
|
||||
const char* sizeName(SizeClass s) {
|
||||
switch (s) { case SizeClass::Tiny: return "Tiny"; case SizeClass::Small: return "Small";
|
||||
case SizeClass::Medium: return "Medium"; case SizeClass::Big: return "Big";
|
||||
case SizeClass::Huge: return "Huge"; }
|
||||
return "?";
|
||||
}
|
||||
|
||||
const char* roleName(EcoRole r) {
|
||||
switch (r) {
|
||||
case EcoRole::Grass: return "Grass"; case EcoRole::Shrub: return "Shrub";
|
||||
case EcoRole::Tree: return "Tree"; case EcoRole::Succulent: return "Succulent";
|
||||
case EcoRole::Moss: return "Moss"; case EcoRole::Aquatic: return "Aquatic";
|
||||
case EcoRole::Herbivore: return "Herbivore"; case EcoRole::Carnivore: return "Carnivore";
|
||||
case EcoRole::Omnivore: return "Omnivore"; case EcoRole::Decomposer: return "Decomposer";
|
||||
case EcoRole::Mycorrhizal: return "Mycorrhizal"; case EcoRole::Lichen: return "Lichen";
|
||||
case EcoRole::Parasite: return "Parasite";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
const char* kindName(BiotaKind k) {
|
||||
switch (k) { case BiotaKind::Flora: return "Flora"; case BiotaKind::Fauna: return "Fauna";
|
||||
case BiotaKind::Funga: return "Funga"; }
|
||||
return "?";
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Adjective prefix for generalist display names ("Desert rodent" etc.).
|
||||
const char* biomeAdjective(Biome b) {
|
||||
switch (b) {
|
||||
case Biome::Desert: return "Desert ";
|
||||
case Biome::Forest: return "Forest ";
|
||||
case Biome::Wetland: return "Swamp ";
|
||||
case Biome::Savanna: return "Savanna ";
|
||||
case Biome::Grassland: return "Steppe ";
|
||||
case Biome::Taiga: return "Boreal ";
|
||||
case Biome::Tundra: return "Tundra ";
|
||||
case Biome::Mountains: return "Mountain ";
|
||||
case Biome::Hills: return "Hill ";
|
||||
case Biome::Beach: return "Coastal ";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display label is built from the TAXONOMY (Family), not an informal common name --
|
||||
// e.g. a Felidae of Big size reads "Felidae", never "big cat" (cat is a common name,
|
||||
// Felidae is the family). Generalists get a biome adjective ("Forest Felidae").
|
||||
std::string organismName(const Organism& o) {
|
||||
const auto& AR = biotaArchetypes();
|
||||
if (o.archetype >= AR.size()) return "?";
|
||||
const BiotaArchetype& a = AR[o.archetype];
|
||||
std::string fam = a.familyName;
|
||||
return a.generalist ? std::string(biomeAdjective((Biome)o.biome)) + fam : fam;
|
||||
}
|
||||
|
||||
// Full Class > Order > Family taxonomy string (the "tree" path) for an organism.
|
||||
std::string organismTaxonomy(const Organism& o) {
|
||||
const auto& AR = biotaArchetypes();
|
||||
if (o.archetype >= AR.size()) return "?";
|
||||
const BiotaArchetype& a = AR[o.archetype];
|
||||
return std::string(a.className) + " > " + a.orderName + " > " + a.familyName;
|
||||
}
|
||||
|
||||
double biotaSuitability(const BiotaArchetype& a, Biome biome, double temp, double moist) {
|
||||
if (!(a.biomeMask & (1u << (unsigned)biome))) return 0.0;
|
||||
double tf = 1.0;
|
||||
if (temp < a.tempMin) tf = std::max(0.0, 1.0 - (a.tempMin - temp) / 8.0);
|
||||
else if (temp > a.tempMax) tf = std::max(0.0, 1.0 - (temp - a.tempMax) / 8.0);
|
||||
double mf = (moist >= a.moistMin) ? 1.0 : std::max(0.0, 1.0 - (a.moistMin - moist) / 0.2);
|
||||
return tf * mf;
|
||||
}
|
||||
|
||||
std::vector<Organism> biotaDraw(const std::vector<BiotaCandidate>& cands, Biome biome,
|
||||
int maxSlots, int maxPoints, uint32_t& rng) {
|
||||
std::vector<Organism> out;
|
||||
if (cands.empty() || maxSlots <= 0 || maxPoints <= 0) return out;
|
||||
const auto& AR = biotaArchetypes();
|
||||
double total = 0.0; for (const auto& c : cands) total += c.weight;
|
||||
if (total <= 0.0) return out;
|
||||
int minCost = 99; for (const auto& c : cands) minCost = std::min(minCost, pointCost(AR[c.arch].size));
|
||||
int slots = maxSlots, pts = maxPoints, guard = maxSlots * 8;
|
||||
while (slots > 0 && pts >= minCost && guard-- > 0) {
|
||||
double r = biotaRndf(rng) * total, acc = 0.0; int pick = cands.back().arch;
|
||||
for (const auto& c : cands) { acc += c.weight; if (r <= acc) { pick = c.arch; break; } }
|
||||
int cost = pointCost(AR[pick].size);
|
||||
if (cost <= pts) { out.push_back({ (uint16_t)pick, (uint8_t)biome }); --slots; pts -= cost; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- Planet driver -----------------------------------------------------------
|
||||
|
||||
void Planet::computeBiotaDensity() {
|
||||
const int n = (int)cells.size();
|
||||
if ((int)sTemp.size() != n || (int)sMoist.size() != n) computeClimate(); // safety
|
||||
computeFloraDensity();
|
||||
computeFaunaDensity(); // reads sFloraDensity
|
||||
computeFungaDensity(); // reads sFloraDensity
|
||||
}
|
||||
|
||||
bool Planet::biotaPopulated() const { return sHasBiota; }
|
||||
|
||||
// On-demand: fill the discrete slot/point population. Deterministic (separate RNG
|
||||
// seeded from cfg.seed, so it never perturbs the tectonic stream). Regional
|
||||
// consistency comes from biasing each cell's draws toward archetypes already
|
||||
// placed in its already-filled, same-biome neighbours (single index-ordered pass).
|
||||
void Planet::generateBiota() {
|
||||
const int n = (int)cells.size();
|
||||
if ((int)sFloraDensity.size() != n || (int)sFaunaDensity.size() != n ||
|
||||
(int)sFungaDensity.size() != n) computeBiotaDensity();
|
||||
sBiota.assign(n, {});
|
||||
sHasBiota = false;
|
||||
const double sea = cfg.seaLevel;
|
||||
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; }
|
||||
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);
|
||||
sBiota[i].flora = fillFlora(i, nbr, rng);
|
||||
sBiota[i].fauna = fillFauna(i, nbr, rng);
|
||||
sBiota[i].funga = fillFunga(i, nbr, rng);
|
||||
if (!sBiota[i].flora.empty() || !sBiota[i].fauna.empty() || !sBiota[i].funga.empty())
|
||||
sHasBiota = true;
|
||||
done[i] = 1;
|
||||
}
|
||||
}
|
||||
81
src/sim/PlanetBiota.hpp
Normal file
81
src/sim/PlanetBiota.hpp
Normal file
@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
#include "PlanetTypes.hpp" // Biome
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
// --- Biota stage: Flora / Fauna / Funga --------------------------------------
|
||||
// Two layers (see docs/fauna-flora-plan.md + docs/fauna_generation_plan.md):
|
||||
// * density scalars (0..1) per kind, derived from climate each tick (drive the
|
||||
// colour views) -- "how full a cell gets";
|
||||
// * a discrete slot/point POPULATION of broad archetypes, generated on demand
|
||||
// and saved -- "what fills it".
|
||||
// Raylib-free, deterministic. Geometry never moves: biota is per-cell data flowed
|
||||
// over the fixed grid (see CLAUDE.md core principle).
|
||||
|
||||
enum class BiotaKind : uint8_t { Flora, Fauna, Funga };
|
||||
|
||||
// Size category -> point cost (Tiny=1 .. Huge=5). Keep order stable (serialized
|
||||
// indirectly via the archetype table).
|
||||
enum class SizeClass : uint8_t { Tiny, Small, Medium, Big, Huge };
|
||||
|
||||
// Functional / trophic role. Grouped by kind; append new roles at the end.
|
||||
enum class EcoRole : uint8_t {
|
||||
Grass, Shrub, Tree, Succulent, Moss, Aquatic, // flora growth forms
|
||||
Herbivore, Carnivore, Omnivore, // fauna trophic roles
|
||||
Decomposer, Mycorrhizal, Lichen, Parasite // funga roles
|
||||
};
|
||||
|
||||
// A broad biological group (NOT a real species). The world is populated by
|
||||
// drawing these into cells; a "generalist" gets a biome adjective at display time
|
||||
// ("Desert rodent" / "Forest rodent"), per the fauna doc.
|
||||
struct BiotaArchetype {
|
||||
const char* name;
|
||||
BiotaKind kind;
|
||||
EcoRole role;
|
||||
const char* className; // taxonomy labels (broad, illustrative)
|
||||
const char* orderName;
|
||||
const char* familyName;
|
||||
SizeClass size;
|
||||
uint32_t biomeMask; // OR of (1u << (int)Biome) for the biomes it inhabits
|
||||
double tempMin, tempMax; // climate tolerance, deg C
|
||||
double moistMin; // min normalized moisture (0..1)
|
||||
bool generalist; // display name gets a biome adjective
|
||||
};
|
||||
|
||||
// One placed organism (saved). `archetype` indexes the global table; `biome` is
|
||||
// the cell's biome at placement time (for the display adjective).
|
||||
struct Organism { uint16_t archetype; uint8_t biome; };
|
||||
|
||||
// Per-cell population, one list per kind (saved as part of the planet state).
|
||||
struct CellBiota { std::vector<Organism> flora, fauna, funga; };
|
||||
|
||||
// Global archetype table (append-only / never reorder -- indices are serialized,
|
||||
// same convention as the Biome enum). Defined in PlanetBiota.cpp.
|
||||
const std::vector<BiotaArchetype>& biotaArchetypes();
|
||||
|
||||
// Helpers (PlanetBiota.cpp).
|
||||
int pointCost(SizeClass s); // 1..5
|
||||
const char* sizeName(SizeClass s);
|
||||
const char* roleName(EcoRole r);
|
||||
const char* kindName(BiotaKind k);
|
||||
std::string organismName(const Organism& o); // family-based label (generalists get a biome adjective)
|
||||
std::string organismTaxonomy(const Organism& o); // "Class > Order > Family" path
|
||||
|
||||
// Environmental suitability of an archetype in a cell (0 = can't live here).
|
||||
double biotaSuitability(const BiotaArchetype& a, Biome biome, double temp, double moist);
|
||||
|
||||
// One candidate archetype + its draw weight for a cell.
|
||||
struct BiotaCandidate { int arch; double weight; };
|
||||
|
||||
// Slot/point fill: draw archetypes weighted by `weight` until slots full OR points
|
||||
// exhausted OR nothing affordable remains. Deterministic given `rng`.
|
||||
std::vector<Organism> biotaDraw(const std::vector<BiotaCandidate>& cands, Biome biome,
|
||||
int maxSlots, int maxPoints, uint32_t& rng);
|
||||
|
||||
// Local xorshift RNG (kept separate from Planet::rngState so generating biota
|
||||
// never perturbs tectonic determinism).
|
||||
inline uint32_t biotaRnd(uint32_t& s) {
|
||||
uint32_t x = s; x ^= x << 13; x ^= x >> 17; x ^= x << 5; s = x; return x;
|
||||
}
|
||||
inline double biotaRndf(uint32_t& s) { return (biotaRnd(s) & 0xFFFFFF) / double(0x1000000); }
|
||||
66
src/sim/PlanetFaunaGen.cpp
Normal file
66
src/sim/PlanetFaunaGen.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
#include "Planet.hpp"
|
||||
#include "PlanetBiota.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// --- Biota: Fauna (animals) --------------------------------------------------
|
||||
// Density tracks herbivore carrying capacity (~ flora productivity); carnivores
|
||||
// are gated on local prey abundance so predators concentrate in the richest belts
|
||||
// (an energy pyramid). fillFauna() places herbivores/omnivores from the prey pool,
|
||||
// then carnivores only where the neighbourhood prey clears bioCarnPreyMin.
|
||||
|
||||
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).
|
||||
sFaunaDensity[i] = std::clamp(sFloraDensity[i] * prod, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Local prey abundance = mean fauna density over the cell + its neighbours.
|
||||
double Planet::neighbourhoodPrey(int i) const {
|
||||
double sum = sFaunaDensity[i]; int c = 1;
|
||||
for (int j : cells[i].neighbors) { sum += sFaunaDensity[j]; ++c; }
|
||||
return sum / c;
|
||||
}
|
||||
|
||||
std::vector<Organism> Planet::fillFauna(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
||||
double dens = sFaunaDensity[i];
|
||||
int pts = (int)std::lround(cfg.bioFaunaPoints * dens);
|
||||
if (pts <= 0) return {};
|
||||
Biome biome = cells[i].biome;
|
||||
double temp = sTemp[i], moist = sMoist[i];
|
||||
// Predators present only where prey is abundant; ramp scales their weight.
|
||||
double prey = neighbourhoodPrey(i);
|
||||
bool allowCarn = prey > cfg.bioCarnPreyMin;
|
||||
double carnRamp = allowCarn
|
||||
? std::clamp((prey - cfg.bioCarnPreyMin) / std::max(1e-6, 1.0 - cfg.bioCarnPreyMin)
|
||||
* cfg.bioCarnScale, 0.0, 1.0)
|
||||
: 0.0;
|
||||
const auto& AR = biotaArchetypes();
|
||||
std::vector<BiotaCandidate> cands;
|
||||
for (int a = 0; a < (int)AR.size(); ++a) {
|
||||
if (AR[a].kind != BiotaKind::Fauna) continue;
|
||||
bool carn = AR[a].role == EcoRole::Carnivore;
|
||||
if (carn && !allowCarn) continue;
|
||||
double s = biotaSuitability(AR[a], biome, temp, moist);
|
||||
if (s <= 0.0) continue;
|
||||
if (carn) s *= carnRamp; // rarer predators in poorer regions
|
||||
if (s <= 0.0) continue;
|
||||
double pres = 0.0;
|
||||
if (!nbr.empty()) {
|
||||
int hit = 0;
|
||||
for (int j : nbr)
|
||||
for (const Organism& o : sBiota[j].fauna) if (o.archetype == a) { ++hit; break; }
|
||||
pres = (double)hit / nbr.size();
|
||||
}
|
||||
cands.push_back({ a, s * (1.0 + cfg.bioRegionBonus * pres) });
|
||||
}
|
||||
return biotaDraw(cands, biome, cfg.bioFaunaSlots, pts, rng);
|
||||
}
|
||||
48
src/sim/PlanetFloraGen.cpp
Normal file
48
src/sim/PlanetFloraGen.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
#include "Planet.hpp"
|
||||
#include "PlanetBiota.hpp"
|
||||
#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.
|
||||
|
||||
void Planet::computeFloraDensity() {
|
||||
const int n = (int)cells.size();
|
||||
if ((int)sTemp.size() != n || (int)sMoist.size() != n) computeClimate();
|
||||
sFloraDensity.assign(n, 0.0);
|
||||
const double sea = cfg.seaLevel;
|
||||
const double tMin = cfg.bioVegTempMin, tOpt = cfg.bioVegTempOpt;
|
||||
const double mRef = std::max(1e-6, cfg.bioVegMoistRef);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) continue;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Organism> Planet::fillFlora(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
||||
double dens = sFloraDensity[i];
|
||||
int pts = (int)std::lround(cfg.bioFloraPoints * dens);
|
||||
if (pts <= 0) return {};
|
||||
Biome biome = cells[i].biome;
|
||||
double temp = sTemp[i], moist = sMoist[i];
|
||||
const auto& AR = biotaArchetypes();
|
||||
std::vector<BiotaCandidate> cands;
|
||||
for (int a = 0; a < (int)AR.size(); ++a) {
|
||||
if (AR[a].kind != BiotaKind::Flora) continue;
|
||||
double s = biotaSuitability(AR[a], biome, temp, moist);
|
||||
if (s <= 0.0) continue;
|
||||
double pres = 0.0; // regional consistency: same archetype next door
|
||||
if (!nbr.empty()) {
|
||||
int hit = 0;
|
||||
for (int j : nbr)
|
||||
for (const Organism& o : sBiota[j].flora) if (o.archetype == a) { ++hit; break; }
|
||||
pres = (double)hit / nbr.size();
|
||||
}
|
||||
cands.push_back({ a, s * (1.0 + cfg.bioRegionBonus * pres) });
|
||||
}
|
||||
return biotaDraw(cands, biome, cfg.bioFloraSlots, pts, rng);
|
||||
}
|
||||
53
src/sim/PlanetFungiGen.cpp
Normal file
53
src/sim/PlanetFungiGen.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
#include "Planet.hpp"
|
||||
#include "PlanetBiota.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// --- Biota: Funga (fungi) ----------------------------------------------------
|
||||
// Fungi are NOT plants but share the environmental + slot/point machinery (see the
|
||||
// user's note). Their rules differ from flora: moisture-led and organic-matter-led
|
||||
// (they feed on dead/living flora), with a colder tolerance than plants -- so they
|
||||
// thrive in moist forests/wetlands, persist in cold shade where flora thins, and
|
||||
// fall to ~0 in hot dry deserts.
|
||||
|
||||
void Planet::computeFungaDensity() {
|
||||
const int n = (int)cells.size();
|
||||
if ((int)sFloraDensity.size() != n) computeFloraDensity();
|
||||
sFungaDensity.assign(n, 0.0);
|
||||
const double sea = cfg.seaLevel;
|
||||
const double mRef = std::max(1e-6, cfg.bioFungaMoistRef);
|
||||
const double tMin = cfg.bioFungaTempMin, w = std::clamp(cfg.bioFungaFloraWeight, 0.0, 1.0);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (cells[i].elevation <= sea || cells[i].biome == Biome::Ice) continue;
|
||||
double mf = std::clamp(sMoist[i] / mRef, 0.0, 1.0);
|
||||
// Cold-tolerant: full down to tMin, tapering below it.
|
||||
double tf = (sTemp[i] >= tMin) ? 1.0 : std::clamp(1.0 - (tMin - sTemp[i]) / 10.0, 0.0, 1.0);
|
||||
// Substrate: leans on flora (organic matter) but always some dead matter.
|
||||
double organic = w * sFloraDensity[i] + (1.0 - w);
|
||||
sFungaDensity[i] = std::clamp(std::min(mf, organic) * tf, 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Organism> Planet::fillFunga(int i, const std::vector<int>& nbr, uint32_t& rng) {
|
||||
double dens = sFungaDensity[i];
|
||||
int pts = (int)std::lround(cfg.bioFungaPoints * dens);
|
||||
if (pts <= 0) return {};
|
||||
Biome biome = cells[i].biome;
|
||||
double temp = sTemp[i], moist = sMoist[i];
|
||||
const auto& AR = biotaArchetypes();
|
||||
std::vector<BiotaCandidate> cands;
|
||||
for (int a = 0; a < (int)AR.size(); ++a) {
|
||||
if (AR[a].kind != BiotaKind::Funga) continue;
|
||||
double s = biotaSuitability(AR[a], biome, temp, moist);
|
||||
if (s <= 0.0) continue;
|
||||
double pres = 0.0;
|
||||
if (!nbr.empty()) {
|
||||
int hit = 0;
|
||||
for (int j : nbr)
|
||||
for (const Organism& o : sBiota[j].funga) if (o.archetype == a) { ++hit; break; }
|
||||
pres = (double)hit / nbr.size();
|
||||
}
|
||||
cands.push_back({ a, s * (1.0 + cfg.bioRegionBonus * pres) });
|
||||
}
|
||||
return biotaDraw(cands, biome, cfg.bioFungaSlots, pts, rng);
|
||||
}
|
||||
@ -29,9 +29,14 @@
|
||||
D(biomeLakeMinDepth) \
|
||||
D(climateOceanMoisture) D(climateRainEfficiency) D(climateOrographic) \
|
||||
D(climateOroRefHeight) D(climateContinentality) \
|
||||
D(bioVegTempMin) D(bioVegTempOpt) D(bioVegMoistRef) D(bioFaunaProductivity) \
|
||||
D(bioCarnPreyMin) D(bioCarnScale) D(bioFungaMoistRef) D(bioFungaFloraWeight) \
|
||||
D(bioFungaTempMin) D(bioRegionBonus) \
|
||||
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||
I(climateWindPasses) I(climateMoistureSmooth) \
|
||||
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
||||
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) \
|
||||
U(seed)
|
||||
|
||||
// Write all config fields as `key = value` lines (no header). Shared by the text
|
||||
@ -158,6 +163,16 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
||||
E(rng(cfg.climateOrographic, 0.0, 50.0, "climateOrographic"));
|
||||
E(rng(cfg.climateOroRefHeight, 1.0, 1.0e5, "climateOroRefHeight"));
|
||||
E(rng(cfg.climateContinentality, 0.0, 1.0, "climateContinentality"));
|
||||
E(rng(cfg.bioVegTempMin, -40.0, 30.0, "bioVegTempMin"));
|
||||
E(rng(cfg.bioVegTempOpt, -20.0, 50.0, "bioVegTempOpt"));
|
||||
E(rng(cfg.bioVegMoistRef, 0.01, 1.0, "bioVegMoistRef"));
|
||||
E(rng(cfg.bioFaunaProductivity, 0.0, 2.0, "bioFaunaProductivity"));
|
||||
E(rng(cfg.bioCarnPreyMin, 0.0, 1.0, "bioCarnPreyMin"));
|
||||
E(rng(cfg.bioCarnScale, 0.0, 5.0, "bioCarnScale"));
|
||||
E(rng(cfg.bioFungaMoistRef, 0.01, 1.0, "bioFungaMoistRef"));
|
||||
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(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||
@ -169,6 +184,12 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
||||
E(irng(cfg.seaLevelEvery, 1, 100000, "seaLevelEvery"));
|
||||
E(irng(cfg.climateWindPasses, 1, 1000, "climateWindPasses"));
|
||||
E(irng(cfg.climateMoistureSmooth, 0, 100, "climateMoistureSmooth"));
|
||||
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
|
||||
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
|
||||
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));
|
||||
E(irng(cfg.bioFloraPoints, 1, 100000, "bioFloraPoints"));
|
||||
E(irng(cfg.bioFaunaPoints, 1, 100000, "bioFaunaPoints"));
|
||||
E(irng(cfg.bioFungaPoints, 1, 100000, "bioFungaPoints"));
|
||||
|
||||
if (cfg.oceanBase >= cfg.continentBase)
|
||||
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
||||
@ -227,9 +248,18 @@ void Planet::writeState(std::ostream& os) const {
|
||||
writeVec(os, sPrevCount);
|
||||
writeVec(os, sStaleStreak);
|
||||
writeVec(os, sFreePlateIds);
|
||||
// v7: discrete biota population (sBiota). A flag byte gates the block so a
|
||||
// not-yet-populated world stays compact; otherwise three Organism lists per cell.
|
||||
uint8_t hasBio = sHasBiota ? 1 : 0; writePod(os, hasBio);
|
||||
if (hasBio) {
|
||||
uint64_t nb = sBiota.size(); writePod(os, nb);
|
||||
for (const CellBiota& cb : sBiota) {
|
||||
writeVec(os, cb.flora); writeVec(os, cb.fauna); writeVec(os, cb.funga);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Planet::readState(std::istream& is, bool hasBiome) {
|
||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota) {
|
||||
// Read the length-prefixed key=value config block (see writeState). A default
|
||||
// PlanetConfig is parsed over, so fields absent from an older save keep their
|
||||
// current defaults. The length guard rejects pre-v6 (raw-POD-config) saves.
|
||||
@ -259,5 +289,20 @@ bool Planet::readState(std::istream& is, bool hasBiome) {
|
||||
readVec(is, sStaleStreak);
|
||||
readVec(is, sFreePlateIds);
|
||||
if (!hasBiome) classifyBiomes(); // old (v3) save: reclassify from loaded state
|
||||
// v7: discrete biota population. buildGeometry() already sized sBiota empty;
|
||||
// older saves (hasBiota=false) just keep the empty population (press L to fill).
|
||||
sHasBiota = false;
|
||||
if (hasBiota) {
|
||||
uint8_t hasBio = 0; readPod(is, hasBio);
|
||||
if (hasBio) {
|
||||
uint64_t nb = 0; readPod(is, nb);
|
||||
if (!is || nb != sBiota.size()) return false;
|
||||
for (CellBiota& cb : sBiota) {
|
||||
readVec(is, cb.flora); readVec(is, cb.fauna); readVec(is, cb.funga);
|
||||
if (!cb.flora.empty() || !cb.fauna.empty() || !cb.funga.empty()) sHasBiota = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
computeBiotaDensity(); // derived density scalars for the colour views
|
||||
return (bool)is;
|
||||
}
|
||||
|
||||
@ -187,4 +187,24 @@ struct PlanetConfig {
|
||||
double climateContinentality = 0.05; // moisture lost per land cell crossed (dries interiors)
|
||||
int climateWindPasses = 50; // moisture-advection iterations (steady state)
|
||||
int climateMoistureSmooth = 12; // precipitation diffusion passes (wet/dry transition zones)
|
||||
|
||||
// --- Biota: flora / fauna / funga (see PlanetBiota.cpp + *Gen.cpp) -------
|
||||
// Density scalars (derived each tick) drive the colour views; the discrete
|
||||
// slot/point population (generated on demand, saved) draws archetypes by size.
|
||||
double bioVegTempMin = -5.0; // C below which plants don't grow
|
||||
double bioVegTempOpt = 15.0; // C at/above which temperature isn't limiting
|
||||
double bioVegMoistRef = 0.5; // normalized moisture where water isn't limiting
|
||||
double bioFaunaProductivity = 0.9; // herbivore capacity per unit vegetation
|
||||
double bioCarnPreyMin = 0.30; // min local prey (fauna density) to support carnivores
|
||||
double bioCarnScale = 1.0; // carnivore weight ramp above the prey threshold
|
||||
double bioFungaMoistRef = 0.4; // normalized moisture where fungi aren't water-limited
|
||||
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
|
||||
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
|
||||
int bioFloraPoints = 20; // flora point budget at full density (scaled by density)
|
||||
int bioFaunaPoints = 16; // fauna point budget at full density
|
||||
int bioFungaPoints = 14; // funga point budget at full density
|
||||
};
|
||||
|
||||
154
test_biota.cpp
Normal file
154
test_biota.cpp
Normal file
@ -0,0 +1,154 @@
|
||||
// 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user