Two features (the ecoregions layer was authored locally and was still uncommitted; civilization Step 2 builds on top and is intermingled in shared files, so they land together): Ecoregions (v19, PlanetEcoregions.*, key E): - generateEcoregions() flood-fills cells sharing biome + land/water context + productivity band into named ecological provinces (dominant flora/fauna/funga + per-kind productivity), a separate sEcoRng. ColorMode::Ecoregion + an Eco tab + cell-info dominants. Saved v19 (v18 geography reshuffle salt already in). Civilizations Step 2 (v20, PlanetCiv.*, keys U/I): - computeHabitability(): derived per-cell food/livability (climate comfort + water access (rivers/lakes/coast) + food (flora/fauna + ecoregion productivity), gated by freezing winters / high terrain). ColorMode:: Habitability (key I). - placeSettlements() (key U, "the dawn"): one-time greedy placement on the best well-spaced fertile cells (separate sCivRng; named from the continent's NameGen bank). The set is fixed, so the only mutable per-step state is each settlement's population. - stepCivilization(): logistic growth toward K = civMaxPopulation*habitability, cut where an active volcano ashes the area, so settlements grow / decline / are abandoned (floored at 1 so a site can revive). Tiers village->town->city. Runs in liveAdvance; detectLiveEvents logs kind=3 events. - Step-back snapshots only the population vector (WeatherSnapshot.settlementPop). 3D + 2D tier-sized markers + city/town labels, a Civ tab, cell-info line. buildGeometry() clears settlements on reseed. Save v20; sCellSettlement rebuilt on load. civ* config knobs. New test_ecoregions.cpp + test_civ.cpp; all 10 headless suites pass; GUI build clean. CLAUDE.md / design-notes / BUILD.md updated (roadmap Step 2 done). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
20 KiB
Planet Sim - World Creation
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 → 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.)
Build (Nobara / Linux)
Dependencies (raylib build needs these dev headers):
sudo dnf install cmake gcc-c++ mesa-libGL-devel libX11-devel \
libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel \
wayland-devel libxkbcommon-devel
Then:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/planetsim
raylib 5.5 is fetched automatically via CMake FetchContent.
OpenMP parallelizes the tectonic step (auto-detected by CMake; ships with
gcc, no extra package needed). It's optional -- without it the sim still runs,
serial. The step is memory-bandwidth-bound, so OMP_NUM_THREADS=4 to 8 gives
the full ~2.8x speedup; the default uses all cores for no extra gain:
OMP_NUM_THREADS=6 ./build/planetsim
Controls
LMB drag orbit camera (in the 3D view)
wheel zoom
hover show cell info (works in the 3D globe and the 2D map)
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)
W enter / leave Live World (slow real-time clock; settled world)
N toggle the day/night terminator (Live World)
T toggle the tide-coloured coastline (Live World)
O toggle ocean-current arrows (warm = poleward/red, cold = equatorward/blue)
K toggle weather clouds/rain cover (Live World)
V toggle volcano markers (Live World; cones + eruption glow, build into islands)
M toggle place-name labels (names continents/oceans/ranges/rivers/lakes; Atlas tab)
E ecoregions colour view (names ecological provinces on first use; Eco tab)
I habitability heat map (where civilization can thrive)
U settlements: the dawn of civilization on first press, then toggle markers (Civ tab)
Y follow-cam: cycle the 3D camera through active storms (Live World; off after last)
. / , step the live clock forward / back by one rate-unit (auto-pauses; back also
rewinds weather + storms via an undo history)
wheel over the 2D map: zoom toward cursor (1-8x); drag pans when zoomed
SPACE pause while forming / re-evolve once settled (or the on-screen button)
[ / ] drift speed (My/s) -- in Live World: live clock rate (hours/s, hour->month)
S single tectonic tick (in Live World: step the clock forward, like '.')
F fast-forward Phase-1 forming to settled (instant)
R reseed planet (restart forming)
+ / - subdivision level (detail), 1..7
F5 / F9 save / load full state (planet.save)
F12 screenshot to screenshot.png
F2 reload planet.cfg (validated) and regenerate
CLI flags (applied before the first load/generate):
--seed N override the config seed
--config PATH use an alternate config file instead of planet.cfg
Files (written in the working directory)
planet.cfg human-editable key=value config of every PlanetConfig parameter;
auto-created on first run, reload live with F2. Range-checked on
load; an invalid file reverts to safe defaults (not overwritten).
planet.save binary snapshot (versioned, currently v17: +geography/atlas; v16
+event log; v15 +stateful
volcanoes; v13 +Live World clock rate; v12 +step-back history
(~40 frames, so a load can rewind storms); v11 +weather systems/storms;
v10 +weather fields; v9 +moons; v8 +Live World clock; v7 +biota): seed + config + full planet state; F5
writes it, F9 reloads and resumes deterministically. As of v6
the config is stored as a self-describing key=value block (like
planet.cfg), so adding/removing config fields no longer breaks saves
(unknown keys ignored, missing keys default). v6 cannot load pre-v6
saves (one-time break) -- regenerate them.
Config (planet.cfg, or PlanetConfig defaults in code)
radius 6.371e6 m (Earth) -- free to change
subdivisions 5 (~10k cells, ~223 km/cell)
plateCount 12
seaLevel 0 m
axialTilt 23.44 deg obliquity: leans the 3D globe + spin axis (visual; seasons later)
Tectonic tuning (also PlanetConfig): relief builds gradually toward an isostatic equilibrium instead of saturating to the clamp.
continentBase 300 m resting elevation of continental crust
oceanBase -6000 m deep abyssal floor (oldest oceanic crust)
upliftGain 1.3e5 m/tick per unit convergence stress
beltWidth 3 cell-rings a mountain belt spreads inland
relax 0.02 isostatic relaxation toward base, per tick
Orogeny + seafloor (PlanetConfig): tall, persistent mountains and age-based ocean depth. The orogeny boosts (collision/arc/persistence) are drift-only -- Phase 1 forms as before; tall mountains grow during Phase-2 drift.
collisionFactor 1.8 continent-continent uplift (Himalaya); raise = taller
arcFactor 1.4 continental subduction-arc uplift (Andes)
isostaticPersist 0.85 how strongly high crust resists relax (0..<1)
rootScale 2500 m above continentBase where persistence saturates
peakSoftCapStart 7000 m below this a tick's uplift always takes (P=1)
peakSoftCapEnd 12000 m at/above this uplift never takes (P=0); also the elev clamp
peakFailDrop 200 m max random drop when the grow roll loses (drift-only)
ridgeDepth -2500 m shallow elevation of brand-new crust at a ridge
seafloorSubsidence 280 m per sqrt(My): seafloor deepens with crustal age
seafloorSeedAge 80 My initial oceanic age spread at generation
Hydrology (PlanetConfig, Phase 2.5): macro drainage network + fluvial erosion.
Drift keeps running at a finer timestep during hydrology. (Config keys keep the
phase3* names for save/cfg compatibility; the UI labels this "Phase 2.5".)
phase3AfterMy 300 My drift before the "Start Phase 2.5?" prompt
phase3DtScale 0.2 hydrology timestep = cflDtMy()*this (finer = slower drift)
rainfall 1.0 uniform precip per cell (drainage-area unit)
riverThreshold 25 discharge above which a cell is a river (lower = richer network)
riverIncision 0.02 K in stream-power incision K*Q^m*S^n*dt (raise = carve faster)
riverDischargeExp 0.5 m (discharge exponent)
riverSlopeExp 1.0 n (slope exponent)
riverTransport 0.1 transport capacity coefficient (cap = this*Q*S)
depFrac 0.25 fraction of excess sediment deposited per cell
Biomes (PlanetConfig, Phase 3): per-cell biome classification thresholds. Temperature = biomeEquatorTemp - biomePoleDrop*(|lat|/90)^biomeLatExp - biomeElevLapse*elevAbove.
biomeEquatorTemp 30 C temperature at the equator, sea level
biomePoleDrop 58 C equator->pole temperature drop
biomeLatExp 1.3 >1 keeps mid-latitudes temperate (cold concentrates at poles)
biomeElevLapse 0.006 C lost per metre above sea level
biomeIceTemp -9.5 C below -> Ice (polar caps + snowcaps); raise = bigger caps
biomeTundraTemp 2 C below (and above ice) -> Tundra/Taiga
biomeTaigaTemp 10 C cool + wet -> boreal forest
biomeSavannaTemp 22 C warm + moderate moisture -> savanna
biomeMountainElev 3000 m above sea level -> Mountains
biomeHillsElev 1200 m above sea level -> Hills
biomeBeachBand 60 m above sea level + adjacent ocean -> Beach
biomeLowlandElev 500 m wetlands only below this elevation
biomeWetlandMoist 0.72 moisture above this (low lowland) -> Wetland
biomeDesertMoist 0.28 moisture below this -> Desert
biomeGrassMoist 0.50 moisture below this -> Grassland/Savanna, else Forest
biomeTaigaMoist 0.40 cool + above this -> Taiga (else Tundra)
biomeLakeMinDepth 20 m filled-basin depth above sea level counting as a Lake
biomeSeasonWeight 0.6 winter temp weight in the Tundra/Taiga cutoffs (0 = annual mean only)
Seasons (PlanetConfig): axialTilt (above) drives per-cell summer/winter temps; color key 6 cycles temperature -> summer -> winter -> seasonality.
seasonAmpMax 18 C max seasonal half-range at full tilt/latitude/interior
seasonLatExp 1.2 latitude shape exponent (>1 pushes swing toward the poles)
seasonOceanFactor 0.15 ocean/coast seasonal-swing floor (thermal inertia)
seasonContinentRings 6 ocean-distance rings to reach full continentality (1 ~ 223 km)
Climate (PlanetConfig, Phase 3): temperature uses the biome* temp params above; precipitation advects ocean moisture along zonal winds (windward rain, leeward rain shadow, dry interiors) then diffuses it. Color modes 6 (temperature) / 7 (precipitation).
climateOceanMoisture 1.0 moisture air carries leaving the ocean (source)
climateRainEfficiency 0.5 fraction of available moisture*belt that rains per cell
climateOrographic 3.0 extra rain per unit normalized upslope (windward)
climateOroRefHeight 500 m upslope that counts as one orographic unit
climateContinentality 0.05 fractional moisture lost per inland cell (dries interiors)
climateWindPasses 50 moisture-advection iterations (steady state)
climateMoistureSmooth 12 precipitation diffusion passes (raise = smoother, more grass/forest)
climateCurrentFactor 4 C max coastal warming/cooling from ocean currents (0 = off; O shows arrows)
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
Live World (PlanetConfig): the slow real-time clock (hours -> weeks/months) over the finished planet -- a moving day/night terminator, a live seasonal temperature cycle and a moving snow line. axialTilt (above) drives the seasonal declination. Press W to enter.
dayLengthHours 24 hours in one planetary day (rotation -> day/night)
yearLengthDays 365.25 days in one planetary year (orbit -> seasons)
snowTemp 0 C land below the live temperature shows snow
seaIceTemp -2 C ocean below the live temperature shows sea ice
tideAmplitude 0.6 m equilibrium-tide scale per unit tide-raising weight
tideSunFactor 0.46 sun's tide weight relative to a unit moon (Earth ~0.46)
Moons (1-3, randomized in generateMoons() + saved v9): orbit on the live clock, raise the tides with the sun, and render as small lit spheres with phases, orbit rings and eclipses.
Weather (PlanetConfig, Live World): a dynamic clouds/rain cycle on the live clock (key K), saved v10. Evaporate over warm seas -> advect along the wind -> condense -> rain -> dissipate.
weatherEvapRate 0.4 /h ocean evaporation toward marine saturation
weatherWindKmh 45 km/h wind speed advecting humidity/cloud
weatherSatBase 0.4 air saturation at 0 C (lower = cloudier)
weatherSatTempCoef 0.025 saturation rise per +1 C
weatherCondense 0.6 /h supersaturation -> cloud
weatherOrographic 0.0009 extra condensation per m of windward upslope
weatherRainThresh 0.5 cloud cover above this rains
weatherRainRate 0.5 /h rain rate from excess cloud
weatherCloudDissip 0.12 /h cloud clearing
weatherSystemMax 8 max concurrent moving weather systems
weatherSpawnRate 0.06 /h genesis probability scale
weatherSystemSpeed 28 km/h drift speed of systems
weatherTropicalSST 26 C min sea-surface temp for tropical genesis
weatherSystemRadius 0.16 rad angular radius of a system's cloud/rain shield
weatherSystemCloud 1.2 /h cloud stamped at a system core
weatherSystemRain 1.6 /h rain at a system core
weatherHurricaneStr 0.6 strength above which a tropical system is a hurricane/typhoon
Live info tabs (Live World): the panel beside the 2D map has Sky, Tides, Weather and Events. Events are a saved, capped journal (newest 200) for storm genesis/intensification, volcano dormancy, eruptions and volcanic-island breaches; clicking an event selects and centres its cell.
Volcanoes (PlanetConfig, Live World, key V): placed by tectonic context on entering Live World, then integrated as stateful lifecycle agents: grow, go dormant, explode, puff ash, and regrow weaker. Step-back snapshots include volcano state. Saved v15+.
volcanoProbRidge 0.55 per-cell placement prob on a young spreading-ridge cell
volcanoProbBorder 0.06 per-cell placement prob on a normal plate-border cell
volcanoProbInterior 0.003 per-cell placement prob elsewhere (hotspots)
volcanoMaxCount 60 global cap (reservoir-sampled, ratios preserved)
volcanoBuildRate 0.02 m/h of growth at activity=1 while growing
volcanoFreeHeight 1000 m absolute height below which vents cannot go dormant
volcanoInitialBuildMax 2500 m max pre-built height when Live World starts
volcanoMaxHeight 3200 m built height where dormancy becomes certain
volcanoDormancyRate 1.0 per-year dormancy hazard scale above free height
volcanoDormantMinYears 120 minimum dormancy before explosion
volcanoDormantMaxYears 1200 maximum dormancy before explosion
volcanoExplodeDropFrac 0.20 fraction of built height shaved by explosion
volcanoActivityDecay 0.70 activity multiplier after each explosion
volcanoDeadActivity 0.05 growth stops at/below this activity
volcanoBlastRadius 0.09 radian radius of the instant explosion ash blast
volcanoBlastCloud 1.5 cloud cover added inside the blast
volcanoAshMinYears 0.5 minimum sustained ash emission after explosion
volcanoAshMaxYears 3.0 maximum sustained ash emission after explosion
volcanoAshPuffCellsPerWeek 2.0 average local cells puffed per week
volcanoAshCloud 0.9 cloud cover injected by sustained ash
volcanoAshCooling 6 C peak local cooling under an active ash plume
Geography / atlas (PlanetConfig, key M): extract + name geographic features from the frozen terrain (continents, islands, oceans, seas, lakes, mountain ranges, peaks, rivers). Generated once on a settled world, saved v17. The foundation of the civilization arc.
geoContinentMinCells 40 land component >= this many cells = Continent (else Island)
geoSeaMaxCells 60 ocean basin <= this many cells = Sea (else Ocean)
geoOceanSepRadians 1.4 min angular gap between ocean-basin centres (higher = fewer oceans)
geoOceanDeep 4 min rings from land for a cell to seed an ocean basin
geoMountainElev 2500 m min elevation for a mountain-range cell
geoRangeMinCells 4 min cells for a named mountain range
geoRiverMinDischarge 80 min mouth discharge for a named river
geoMaxRivers 40 cap on named rivers (largest by discharge)
geoMaxPeaks 40 cap on named peaks (highest)
Civilization (PlanetConfig, key U): settlements placed once on the best fertile cells, then their population grows/declines on the Live World clock toward a food-driven carrying capacity. Saved v20.
civMaxSettlements 80 cap on settlement sites
civMinSpacingRadians 0.10 min angular gap between sites (~640 km)
civMinHabitability 0.22 don't place a settlement below this habitability
civSeedPopulation 250 initial village population
civGrowthRate 0.02 logistic growth rate per year
civMaxPopulation 2e6 carrying capacity at habitability 1
civTownPop 5000 population at/above which a settlement is a Town
civCityPop 100000 population at/above which a settlement is a City
civAbandonPop 50 below this = abandoned/ruins (can revive)
civHabWaterWeight 0.45 habitability weight of water access (rivers/lakes/coast)
civHabFoodWeight 0.40 habitability weight of food (flora/fauna + ecoregion productivity)
civHabTempOpt 18 C most comfortable annual-mean temperature
civHabElevPenalty 2500 m high terrain steeply penalised above this
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/PlanetClimate.cpp src/sim/PlanetLive.cpp \
src/sim/PlanetOcean.cpp src/sim/PlanetWeather.cpp src/sim/PlanetVolcano.cpp \
src/sim/PlanetBiota.cpp \
src/sim/PlanetFloraGen.cpp src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp \
src/sim/NameGen.cpp src/sim/PlanetGeography.cpp src/sim/PlanetEcoregions.cpp \
src/sim/PlanetCiv.cpp \
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
# Same source list for every suite: swap test_logic.cpp -> test_biota / test_live / test_ocean /
# test_weather / test_volcano / test_geography / test_ecoregions / test_civ.
# The CMake build also includes test_events for the viewer event journal.
Verifies geometry, plate assignment, gradual non-saturating relief and determinism. Run after changing Planet::step().
Notes for later phases
- Cell has a
subgridshared_ptr hook (null in phase 1) for the future fine-resolution per-cell mesh (phases 4/5: civilization/culture). neighborsadjacency already built -> reuse for diffusion, climate, cross-cell-boundary interaction.- elevation is continuous meters (double); climate in phase 3 can read it as a smooth function, not coarse bands.