Civ Step 4: culture, beliefs & governments
Give the world peoples and faiths on top of Step 3's realms. All derived
deterministically from the (saved) settlement set + geography (pure hashes,
no RNG, recomputed each sim year -> no save-version bump, step-back free).
- PlanetCulture.{hpp,cpp}: computeCultures() groups living settlements into one
culture per inhabited continent, each with a generated people name, a dominant
ethos picked from its cells' environment (Seafaring/Highland/Nomadic/Agrarian,
else a hashed Mercantile/Warlike), and a religion (a Faith focus biased by the
dominant biome + a generated faith name). Runs after computeTerritory().
- Governments: each realm (Nation) gets a GovType from its tier + a deterministic
pick, folded into its name ("Republic of X", "Duchy of X", "X Theocracy", "X
Confederation", "X Dominion", ...) -- culture-driven renaming.
- Culture-driven grouping: realm vassalage is now restricted to the same
continent, so every kingdom/empire is mono-cultural.
- Render: a Culture colour mode (cultureColor, cultural blocs) + pale
buildCultureBorders lines + a Cultures tab + a cell-info culture/faith line +
government-aware realm labels, all under key X. Cultures recompute with
territory in rebuildTerritory().
- test_culture.cpp: one culture per continent, valid ethos/faith, governments per
tier reflected in names, mono-cultural realms, determinism, RNG isolation,
save->load->recompute parity. All 12 suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0638398d44
commit
91f1b90a93
11
BUILD.md
11
BUILD.md
@ -58,6 +58,7 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
||||
I habitability heat map (where civilization can thrive)
|
||||
U settlements: the dawn of civilization on first press, then toggle markers (Civ tab)
|
||||
P territory / realms view + political borders (nations listed in the Realms tab)
|
||||
X culture / faiths view + cultural borders (peoples/ethos/religion in the Cultures 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)
|
||||
@ -323,6 +324,12 @@ territory, leaving wilderness frontiers between realms.
|
||||
civEmpireMinMembers 5 settlements in a realm to count as an empire
|
||||
civEmpirePop 5e6 total realm population to count as an empire
|
||||
|
||||
Culture, beliefs & governments (key X — derived from settlements + geography, not saved): settlements
|
||||
on a continent share a culture (a people with an environment-driven ethos + a religion); each realm
|
||||
gets a government type folded into its name (Republic of X / Duchy of X / X Theocracy / ...); realms
|
||||
stay mono-cultural. No config knobs — the ethos/faith/government rules are constants in
|
||||
src/sim/PlanetCulture.cpp (computeCultures).
|
||||
|
||||
## Headless logic test (no display)
|
||||
|
||||
g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
||||
@ -333,11 +340,11 @@ territory, leaving wilderness frontiers between realms.
|
||||
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/PlanetNation.cpp \
|
||||
src/sim/PlanetCiv.cpp src/sim/PlanetNation.cpp src/sim/PlanetCulture.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 / test_nation.
|
||||
# test_weather / test_volcano / test_geography / test_ecoregions / test_civ / test_nation / test_culture.
|
||||
# The CMake build also includes test_events for the viewer event journal.
|
||||
|
||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||
|
||||
36
CLAUDE.md
36
CLAUDE.md
@ -108,7 +108,7 @@ the fixed-grid Eulerian model + the climate fields are the groundwork for it.
|
||||
**Civilizations (in progress — the long arc after the world is finished):** the eventual goal is
|
||||
people who eat, name their world, found villages→cities, build kingdoms/empires, draw cultural +
|
||||
geographic borders, and go to war. Built in phases (cell = territory, settlements = point agents, all
|
||||
on the Live World clock). **Steps 1–3 of the roadmap are done (plus a derived ecoregions atlas):**
|
||||
on the Live World clock). **Steps 1–4 of the roadmap are done (plus a derived ecoregions atlas):**
|
||||
- **Geography & place-names (the atlas)** *(done — see `PlanetGeography.cpp` + `NameGen.cpp`)* — the
|
||||
foundation everything civic references. `Planet::generateGeography()` extracts named features from
|
||||
the (frozen) terrain by connectivity over the fixed grid — **continents/islands** (connected land),
|
||||
@ -166,7 +166,25 @@ on the Live World clock). **Steps 1–3 of the roadmap are done (plus a derived
|
||||
**pure function of the (saved) settlements**, so they're **recomputed** (each sim year / on placement /
|
||||
load / step-back) — **no save state, no version bump**, peaceful & population-driven. Knobs `civTerritory*`/
|
||||
`civVassalRange`/`civEmpire*`.
|
||||
*Next steps (not yet built): culture + beliefs + governments (and culture-driven borders/renaming),
|
||||
- **Culture, beliefs & governments (Step 4)** *(done — see `PlanetCulture.cpp`)* — settlements on the
|
||||
same continent share a **culture** (a people/language family). `computeCultures()` (deterministic, no
|
||||
RNG, runs right after `computeTerritory()`) groups living settlements by continent (`Settlement.regionId`)
|
||||
into **one culture per inhabited continent**, each with: a generated **people name** ("the Velmar", from
|
||||
the continent's `NameGen` bank), a dominant **ethos** picked from the aggregate environment of its cells
|
||||
(coastal→**Seafaring**, mountains/hills→**Highland**, desert/dry→**Nomadic**, fertile→**Agrarian**, else a
|
||||
hash-picked **Mercantile/Warlike**), and a **religion** — a `Faith` focus biased by the dominant biome
|
||||
(coast→Sea, mountains→Sky, desert→Sun, cold→Ancestors, forest→Harvest…) plus a generated **faith name**
|
||||
("the Tidewardens"). Each **realm** (Step-3 `Nation`) also gets a **government** (`GovType`) from its tier +
|
||||
a deterministic pick (City-state→Republic/Chiefdom/Theocracy, Kingdom→Kingdom/Duchy/Theocracy, Empire→
|
||||
Empire/Autocracy/Confederation), folded into its **name** ("Republic of X", "Duchy of X", "X Theocracy",
|
||||
"X Confederation", "X Dominion"…) — **culture-driven renaming**. **Culture-driven grouping**: realm
|
||||
vassalage is now restricted to the **same continent**, so every kingdom/empire is **mono-cultural**.
|
||||
Render: a **Culture** colour mode (`cultureColor`, cultural blocs over the political map) + pale
|
||||
**cultural-region borders** + a **Cultures** tab (key **`X`**), a cell-info culture/faith line, and the
|
||||
government-aware realm labels. Like territory, cultures are a **pure function of the (saved) settlements +
|
||||
geography**, so they're **recomputed** each sim year / on placement / load / step-back — **no save state,
|
||||
no version bump**. Knobs: none (ethos/faith/government rules are internal constants).
|
||||
*Next steps (not yet built): cultural **evolution** (spread along borders, drift, assimilation, schism);
|
||||
conflict + diplomacy (war moving borders by force), trade.*
|
||||
|
||||
## Current state
|
||||
@ -578,6 +596,7 @@ src/
|
||||
PlanetEcoregions.* generateEcoregions() (named ecological provinces + dominant biota/productivity)
|
||||
PlanetCiv.* computeHabitability/placeSettlements/stepCivilization (settlements; civ Step 2)
|
||||
PlanetNation.* computeTerritory (realms + per-cell ownership + borders; civ Step 3)
|
||||
PlanetCulture.* computeCultures (cultures/ethos/faith per continent + governments; civ Step 4)
|
||||
PlanetIO.cpp config file (text) + binary save/load
|
||||
render/ (raylib viewer)
|
||||
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
||||
@ -641,13 +660,13 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.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/PlanetNation.cpp \
|
||||
src/sim/PlanetNation.cpp src/sim/PlanetCulture.cpp \
|
||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||
```
|
||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
||||
`test_weather.cpp`, `test_volcano.cpp`, `test_geography.cpp`, `test_ecoregions.cpp`, `test_civ.cpp` or
|
||||
`test_nation.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
||||
Civilization / Nation suites — same source list. CMake also builds `test_events` for the
|
||||
`test_weather.cpp`, `test_volcano.cpp`, `test_geography.cpp`, `test_ecoregions.cpp`, `test_civ.cpp`,
|
||||
`test_nation.cpp` or `test_culture.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
||||
Civilization / Nation / Culture suites — same source list. CMake also builds `test_events` for the
|
||||
viewer event journal.)
|
||||
|
||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
||||
@ -684,6 +703,7 @@ all in 3D + 2D) · `N` day/night terminator (Live World) · `T` tide-coloured co
|
||||
`E` ecoregions colour view (names ecology on first use) · `I` habitability heat map ·
|
||||
`U` settlements (the dawn of civilization on first press; toggles markers after) ·
|
||||
`P` territory / realms view + political borders (Realms tab lists nations) ·
|
||||
`X` culture / faiths view + cultural borders (Cultures tab lists peoples, ethos & religion) ·
|
||||
`SPACE` or on-screen button pause ·
|
||||
`[`/`]` drift speed (My/sec) — in **Live World** the live-clock rate (hours/sec, hour→month) ·
|
||||
`S` single tick (in **Live World** steps the clock forward) · `.`/`,` step the live clock
|
||||
@ -904,6 +924,10 @@ triangles (plates are fixed in phase 1).
|
||||
threshold `civEmpireMinMembers` (5 settlements) / `civEmpirePop` (5e6 total). Territory + realms are
|
||||
**derived** (recomputed each sim year, not saved). Realm colours/border colour/labels are render
|
||||
constants (Colors.cpp / ViewerRender.cpp).
|
||||
- **Culture, beliefs & governments (civ Step 4, key `X`):** **no config knobs** — the ethos/faith/
|
||||
government rules are internal constants in `PlanetCulture.cpp` (ethos-signal threshold 0.34, the
|
||||
biome→faith map, the tier→government picks) and the culture colours/border colour are render constants.
|
||||
Cultures are derived (recomputed with territory, not saved). To retune, edit `computeCultures()`.
|
||||
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||
knob for how fast/high relief builds.
|
||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||
|
||||
@ -37,6 +37,7 @@ set(SIM_SOURCES
|
||||
src/sim/PlanetEcoregions.cpp
|
||||
src/sim/PlanetCiv.cpp
|
||||
src/sim/PlanetNation.cpp
|
||||
src/sim/PlanetCulture.cpp
|
||||
src/sim/PlanetIO.cpp
|
||||
)
|
||||
|
||||
@ -75,7 +76,7 @@ if(UNIX AND NOT APPLE)
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation)
|
||||
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation culture)
|
||||
add_executable(test_${test_name} test_${test_name}.cpp)
|
||||
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
||||
add_test(NAME ${test_name} COMMAND test_${test_name})
|
||||
|
||||
@ -53,6 +53,8 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
||||
- `PlanetCiv.{hpp,cpp}` — `computeHabitability`/`placeSettlements`/`stepCivilization` (civ Step 2:
|
||||
habitability + settlements that grow/decline on the live clock; saved v20).
|
||||
- `PlanetNation.{hpp,cpp}` — `computeTerritory` (civ Step 3: realms + per-cell ownership; derived, not saved).
|
||||
- `PlanetCulture.{hpp,cpp}` — `computeCultures` (civ Step 4: cultures/ethos/faith per continent +
|
||||
government per realm; derived, not saved).
|
||||
- `PlanetIO.cpp` — text config + binary save/load.
|
||||
|
||||
The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration),
|
||||
@ -420,6 +422,31 @@ across a recompute by capital id). The viewer recomputes territory + rebuilds bo
|
||||
year** (`liveAdvance` year-tick), and on placement / load / step-back; `buildGeometry()` clears
|
||||
`nations`/`sCellNation`/`sSettleNation` on reseed. Knobs `civTerritory*`/`civVassal*`/`civEmpire*`.
|
||||
|
||||
## Civilization Step 4 — culture, beliefs & governments
|
||||
|
||||
`PlanetCulture.cpp` (engine, raylib-free, **no RNG** → tectonic stream untouched). `computeCultures()`
|
||||
runs right after `computeTerritory()` (it reads `nations` / `sCellNation`) and is a **pure deterministic
|
||||
function** of the settlement set + geography + biomes — so it is *recomputed*, never saved (no
|
||||
`SAVE_VERSION` bump), and step-back replays it for free. (1) **One culture per inhabited continent**:
|
||||
living settlements are grouped by `Settlement.regionId` (its continent geography-feature; fall back to
|
||||
the language `bank` when there's no geography). Each `Culture` gets a **people name** (`namegen::makeName`
|
||||
from the continent's bank), a dominant **ethos** = argmax of environmental fractions over its settlement
|
||||
cells (coastal→Seafaring, mountains/hills→Highland, desert/dry→Nomadic, fertile-biome→Agrarian; if none
|
||||
≥ 0.34, a `cultHash` pick of Mercantile/Warlike/Agrarian), and a **religion** — a `Faith` focus from the
|
||||
dominant biome (coast→Sea, mountains→Sky, desert→Sun, cold→Ancestors, forest/wetland→Harvest, with a
|
||||
rare hash Moon/War) plus a generated `faithName`. (2) **Government per realm**: each `Nation` gets a
|
||||
`GovType` from its tier + a `cultHash` pick and its `name` is **rewritten** to fold it in ("Republic of
|
||||
X", "Duchy of X", "X Theocracy", "X Confederation", "X Dominion"…); `nation.cultureId` = its capital's
|
||||
culture. (3) **Per-cell culture** `sCellCulture[c]` = the culture of `sCellNation[c]`'s nation (reuses
|
||||
territory's ownership) → the culture view shows **cultural blocs** over the political map.
|
||||
|
||||
**Culture-driven grouping**: `computeTerritory()`'s realm-join test now requires the vassal and its
|
||||
candidate capital to share a `regionId`, so every realm is **mono-cultural** (no kingdom spans two
|
||||
continents). Render: a `Culture` colour mode (`cultureColor`) + pale `buildCultureBorders` lines + a
|
||||
`Cultures` tab (9th) + a cell-info culture/faith line + government-aware realm labels, all under key `X`;
|
||||
`rebuildTerritory()` computes territory **then** cultures and rebuilds both border sets each sim year /
|
||||
placement / load / step-back. No config knobs (rules are constants in `computeCultures()`).
|
||||
|
||||
## Headless testing
|
||||
|
||||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
||||
@ -431,8 +458,11 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp src/sim/Planet
|
||||
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/PlanetNation.cpp \
|
||||
src/sim/PlanetCulture.cpp \
|
||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||
# test_biota / test_live / test_ocean / test_weather / test_volcano / test_geography use the same list.
|
||||
# test_biota / test_live / test_ocean / test_weather / test_volcano / test_geography / test_ecoregions /
|
||||
# test_civ / test_nation / test_culture use the same source list.
|
||||
```
|
||||
(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).
|
||||
|
||||
@ -94,6 +94,7 @@ const char* colorModeName(ColorMode m) {
|
||||
case ColorMode::Ecoregion: return "Ecoregions";
|
||||
case ColorMode::Habitability: return "Habitability";
|
||||
case ColorMode::Territory: return "Territory / realms";
|
||||
case ColorMode::Culture: return "Culture / faiths";
|
||||
case ColorMode::TempSummer: return "Temperature (summer)";
|
||||
case ColorMode::TempWinter: return "Temperature (winter)";
|
||||
case ColorMode::Seasonality: return "Seasonality (summer-winter)";
|
||||
@ -149,6 +150,12 @@ Color nationColor(int id) { // distinct per-realm tint (offset hue/s
|
||||
return ColorFromHSV(h, 0.58f, 0.92f);
|
||||
}
|
||||
|
||||
Color cultureColor(int id) { // per-culture tint (different phase so blocs read vs realms)
|
||||
if (id < 0) return Color{ 40, 44, 50, 255 }; // no culture: dim grey
|
||||
float h = std::fmod((id + 2) * 0.61803398875f + 0.47f, 1.0f) * 360.0f;
|
||||
return ColorFromHSV(h, 0.50f, 0.86f); // slightly softer/paler than realm tints
|
||||
}
|
||||
|
||||
Color habitabilityColor(double h01) { // barren grey -> green -> fertile gold
|
||||
double t = std::clamp(h01, 0.0, 1.0);
|
||||
static const unsigned char key[3][3] = {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
enum class ColorMode { Elevation, Plate, Age, Crust, Biome, Temperature, Precip,
|
||||
FloraDensity, FaunaDensity, FungaDensity,
|
||||
Ecoregion, Habitability, Territory,
|
||||
Ecoregion, Habitability, Territory, Culture,
|
||||
TempSummer, TempWinter, Seasonality }; // 6 cycles these temp sub-views
|
||||
|
||||
Color elevationColor(double e, double seaLevel);
|
||||
@ -44,3 +44,5 @@ Color ecoregionColor(int id, Biome b, double productivity);
|
||||
Color habitabilityColor(double h01);
|
||||
// Per-nation territory tint (golden-ratio HSV, offset from plateColor so realms read distinctly).
|
||||
Color nationColor(int id);
|
||||
// Per-culture tint (golden-ratio HSV, a different phase than nationColor so cultural blocs read distinctly).
|
||||
Color cultureColor(int id);
|
||||
|
||||
@ -41,9 +41,11 @@ void buildBorders(const Planet& p, float radius,
|
||||
}
|
||||
}
|
||||
|
||||
void buildNationBorders(const Planet& p, float radius, std::vector<Vector3>& segs) {
|
||||
// Trace boundaries where a per-cell integer label differs across a triangle's cells (the plate
|
||||
// dual-contour, parameterised by the label array). Shared by nation + culture borders.
|
||||
static void buildLabelBorders(const Planet& p, float radius, const std::vector<int>& cn,
|
||||
std::vector<Vector3>& segs) {
|
||||
segs.clear();
|
||||
const std::vector<int>& cn = p.cellNation();
|
||||
if ((int)cn.size() != (int)p.cells.size()) return;
|
||||
auto midV = [&](int i, int j) -> Vector3 {
|
||||
Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius;
|
||||
@ -69,6 +71,14 @@ void buildNationBorders(const Planet& p, float radius, std::vector<Vector3>& seg
|
||||
}
|
||||
}
|
||||
|
||||
void buildNationBorders(const Planet& p, float radius, std::vector<Vector3>& segs) {
|
||||
buildLabelBorders(p, radius, p.cellNation(), segs);
|
||||
}
|
||||
|
||||
void buildCultureBorders(const Planet& p, float radius, std::vector<Vector3>& segs) {
|
||||
buildLabelBorders(p, radius, p.cellCulture(), segs);
|
||||
}
|
||||
|
||||
void buildDriftArrows(const Planet& p, float radius,
|
||||
std::vector<Vector3>& out, std::vector<PlateLabel>& labels) {
|
||||
out.clear(); labels.clear();
|
||||
|
||||
@ -18,6 +18,8 @@ void buildBorders(const Planet& p, float radius,
|
||||
// Same dual-contour, but separating cells of different `cellNation()` (a realm's outline:
|
||||
// inter-realm borders + its coast + wilderness frontier). One segment list.
|
||||
void buildNationBorders(const Planet& p, float radius, std::vector<Vector3>& segs);
|
||||
// Like buildNationBorders but on the per-cell culture split (civ Step 4): cultural-region outlines.
|
||||
void buildCultureBorders(const Planet& p, float radius, std::vector<Vector3>& segs);
|
||||
|
||||
// ---- Per-plate drift arrows -------------------------------------------------
|
||||
struct PlateLabel { int id; Vector3 pos; };
|
||||
|
||||
@ -116,6 +116,16 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
|
||||
L.push_back(std::string("realm: wilderness"));
|
||||
}
|
||||
}
|
||||
// Culture, ethos & faith of this cell's people (civ Step 4).
|
||||
if (p.culturesBuilt()) {
|
||||
const auto& cc = p.cellCulture();
|
||||
int ci = (i < (int)cc.size()) ? cc[i] : -1;
|
||||
if (ci >= 0 && ci < (int)p.cultureList().size()) {
|
||||
const Culture& cu = p.cultureList()[ci];
|
||||
L.push_back(std::string("culture: ") + cu.name + " (" + cultureEthosName(cu.ethos) + ")");
|
||||
L.push_back(std::string("faith: ") + cu.faithName + " (" + faithFocusName(cu.faith) + ")");
|
||||
}
|
||||
}
|
||||
// Climate (derived; present once computeClimate() has run).
|
||||
if (sized(p.temperature()) && sized(p.moisture()))
|
||||
L.push_back(std::string(TextFormat("temp %.1f C precip %.0f%%",
|
||||
|
||||
@ -123,6 +123,7 @@ void Viewer::recolor() {
|
||||
const auto& eco = planet.ecoregions();
|
||||
const std::vector<double>& hab = planet.habitability();
|
||||
const std::vector<int>& cnat = planet.cellNation();
|
||||
const std::vector<int>& ccult = planet.cellCulture();
|
||||
vcolors.resize(planet.cells.size());
|
||||
for (size_t i = 0; i < planet.cells.size(); ++i) {
|
||||
switch (mode) {
|
||||
@ -168,6 +169,13 @@ void Viewer::recolor() {
|
||||
? Color{60, 64, 58, 255} : Color{26, 34, 52, 255};
|
||||
break;
|
||||
}
|
||||
case ColorMode::Culture: {
|
||||
int ci = (i < (int)ccult.size()) ? ccult[i] : -1;
|
||||
if (ci >= 0) vcolors[i] = cultureColor(ci); // owned: culture tint
|
||||
else vcolors[i] = (planet.cells[i].elevation > planet.cfg.seaLevel) // uncultured land vs sea
|
||||
? Color{60, 64, 58, 255} : Color{26, 34, 52, 255};
|
||||
break;
|
||||
}
|
||||
default: vcolors[i] = elevationColor(planet.cells[i].elevation, planet.cfg.seaLevel);
|
||||
}
|
||||
}
|
||||
@ -728,14 +736,18 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
|
||||
}
|
||||
}
|
||||
if (vu.breach) refreshView();
|
||||
else if (vu.recolor || cu.recolor || (territoryChanged && mode == ColorMode::Territory)) recolor();
|
||||
else if (vu.recolor || cu.recolor
|
||||
|| (territoryChanged && (mode == ColorMode::Territory || mode == ColorMode::Culture))) recolor();
|
||||
rebuildLiveOverlay();
|
||||
}
|
||||
|
||||
// Recompute realms/territory from the (derived) settlement set + rebuild the nation-border segments.
|
||||
// Recompute realms/territory + cultures from the (derived) settlement set and rebuild the border
|
||||
// segments (political + cultural). Cultures depend on territory, so compute them right after.
|
||||
void Viewer::rebuildTerritory() {
|
||||
planet.computeTerritory();
|
||||
planet.computeCultures();
|
||||
buildNationBorders(planet, borderR, nationBorders);
|
||||
buildCultureBorders(planet, borderR, cultureBorders);
|
||||
double yearHours = std::max(1.0, planet.cfg.dayLengthHours * planet.cfg.yearLengthDays);
|
||||
lastTerritoryYear = (long)std::floor(liveTime / yearHours);
|
||||
}
|
||||
|
||||
@ -104,6 +104,8 @@ struct Viewer {
|
||||
std::vector<int> atlasRowCells; // cell to focus per visible Atlas/Eco/Civ/Realms-tab row (parallel to the list)
|
||||
std::vector<Vector3> nationBorders; // political border segments (rebuilt on year tick / placement / load)
|
||||
bool showNationBorders = false; // draw nation/realm borders (on with the Territory view)
|
||||
std::vector<Vector3> cultureBorders; // cultural-region border segments (civ Step 4, rebuilt with territory)
|
||||
bool showCultureBorders = false; // draw cultural-region borders (on with the Culture view)
|
||||
long lastTerritoryYear = -1; // sim year territory was last recomputed (recompute when it ticks)
|
||||
|
||||
// World event journal: currently Live World events, shaped to be reused by later phases.
|
||||
|
||||
@ -225,6 +225,16 @@ void Viewer::handleInput() {
|
||||
setStatus(mode == ColorMode::Territory ? "Territory / realms on" : "Territory off");
|
||||
}
|
||||
}
|
||||
if (IsKeyPressed(KEY_X) && settled) { // toggle the culture / faiths colour view + cultural borders
|
||||
if (!planet.settlementsPlaced()) setStatus("Press U for the dawn of civilization first");
|
||||
else {
|
||||
if (!planet.culturesBuilt() || (int)planet.cellCulture().size() != (int)planet.cells.size()) rebuildTerritory();
|
||||
mode = (mode == ColorMode::Culture) ? ColorMode::Biome : ColorMode::Culture;
|
||||
showCultureBorders = (mode == ColorMode::Culture);
|
||||
recolor();
|
||||
setStatus(mode == ColorMode::Culture ? "Cultures / faiths on" : "Cultures off");
|
||||
}
|
||||
}
|
||||
if (IsKeyPressed(KEY_W) && settled) { // enter / leave Live World (slow real-time clock)
|
||||
liveWorld = !liveWorld;
|
||||
if (liveWorld) {
|
||||
|
||||
@ -90,6 +90,14 @@ void Viewer::renderGlobe3D() {
|
||||
}
|
||||
rlEnd(); rlSetLineWidth(1.0f);
|
||||
}
|
||||
if (showCultureBorders && !cultureBorders.empty()) { // cultural-region borders (pale, distinct from political)
|
||||
rlSetLineWidth(3.0f); rlBegin(RL_LINES); rlColor4ub(245, 240, 220, 220);
|
||||
for (size_t i = 0; i + 1 < cultureBorders.size(); i += 2) {
|
||||
rlVertex3f(cultureBorders[i].x, cultureBorders[i].y, cultureBorders[i].z);
|
||||
rlVertex3f(cultureBorders[i + 1].x, cultureBorders[i + 1].y, cultureBorders[i + 1].z);
|
||||
}
|
||||
rlEnd(); rlSetLineWidth(1.0f);
|
||||
}
|
||||
if (showDrift && !driftArrows.empty()) {
|
||||
rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(90, 230, 255, 255);
|
||||
for (size_t i = 0; i + 1 < driftArrows.size(); i += 2) {
|
||||
@ -344,6 +352,7 @@ void Viewer::renderMap2D() {
|
||||
if (showBorders && !borders.empty()) drawSegments2D(borders, Color{255, 235, 90, 255}, 2.0f, vr, mapLon);
|
||||
if (showBorders && !ridgeBorders.empty()) drawSegments2D(ridgeBorders, Color{220, 70, 60, 255}, 2.0f, vr, mapLon);
|
||||
if (showNationBorders && !nationBorders.empty()) drawSegments2D(nationBorders, Color{18, 18, 26, 235}, 2.0f, vr, mapLon);
|
||||
if (showCultureBorders && !cultureBorders.empty()) drawSegments2D(cultureBorders, Color{245, 240, 220, 230}, 2.5f, vr, mapLon);
|
||||
if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, vr, mapLon);
|
||||
if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, vr, mapLon);
|
||||
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, vr, mapLon);
|
||||
@ -443,16 +452,16 @@ void Viewer::renderLiveInfo() {
|
||||
DrawRectangleLinesEx(r, 1, Color{90, 90, 110, 255});
|
||||
int x = (int)r.x + 14, y = (int)r.y + 10;
|
||||
DrawText("Live info", x, y, 20, RAYWHITE);
|
||||
const char* tabs[8] = { "Sky", "Tides", "Weather", "Events", "Atlas", "Eco", "Civ", "Realms" };
|
||||
const char* tabs[9] = { "Sky", "Tides", "Weather", "Events", "Atlas", "Eco", "Civ", "Realms", "Culture" };
|
||||
float tx = r.x + 10.0f, ty = r.y + 38.0f;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
float tw = (r.width - 20.0f) / 8.0f;
|
||||
for (int i = 0; i < 9; ++i) {
|
||||
float tw = (r.width - 20.0f) / 9.0f;
|
||||
Rectangle tr{ tx + i * tw, ty, tw - 4.0f, 24.0f };
|
||||
liveInfoTabRects.push_back(tr);
|
||||
bool on = liveInfoTab == i;
|
||||
DrawRectangleRec(tr, on ? Color{42, 48, 68, 255} : Color{18, 22, 34, 255});
|
||||
DrawRectangleLinesEx(tr, 1, on ? Color{125, 145, 190, 255} : Color{65, 70, 90, 255});
|
||||
int tfs = 12; // smaller font: 7 tabs are narrow
|
||||
int tfs = 11; // smaller font: 9 tabs are narrow
|
||||
int w = MeasureText(tabs[i], tfs);
|
||||
DrawText(tabs[i], (int)(tr.x + (tr.width - w) * 0.5f), (int)tr.y + 6, tfs,
|
||||
on ? RAYWHITE : Color{155, 165, 185, 255});
|
||||
@ -674,7 +683,7 @@ void Viewer::renderLiveInfo() {
|
||||
y += 20;
|
||||
}
|
||||
}
|
||||
} else { // Realms: nations by population (largest first); click a row to fly to the capital
|
||||
} else if (liveInfoTab == 7) { // Realms: nations by population (largest first); click a row to fly to the capital
|
||||
const auto& N = planet.nationList();
|
||||
DrawText("Realms", x, y, 18, Color{200, 205, 220, 255});
|
||||
DrawText(TextFormat("%d", (int)N.size()), (int)(r.x + r.width) - 40, y + 2, 13, Color{145, 155, 175, 255});
|
||||
@ -702,6 +711,34 @@ void Viewer::renderLiveInfo() {
|
||||
y += 20;
|
||||
}
|
||||
}
|
||||
} else { // Cultures: peoples by population (largest first); click a row to fly to their largest city
|
||||
const auto& C = planet.cultureList();
|
||||
DrawText("Cultures", x, y, 18, Color{200, 205, 220, 255});
|
||||
DrawText(TextFormat("%d", (int)C.size()), (int)(r.x + r.width) - 40, y + 2, 13, Color{145, 155, 175, 255});
|
||||
y += 26;
|
||||
if (C.empty()) {
|
||||
DrawText(planet.settlementsPlaced() ? "press X for the culture view" : "press U then X", x, y, 13, Color{150, 155, 170, 255});
|
||||
} else {
|
||||
const auto& sc = planet.settleCulture();
|
||||
std::vector<int> idx(C.size());
|
||||
for (size_t i = 0; i < C.size(); ++i) idx[i] = (int)i;
|
||||
std::sort(idx.begin(), idx.end(), [&](int a, int b) { return C[a].totalPop > C[b].totalPop; });
|
||||
for (int ci : idx) {
|
||||
if (y > (int)(r.y + r.height) - 22) break;
|
||||
const Culture& cu = C[ci];
|
||||
// Representative cell: the largest living settlement of this culture (to fly to).
|
||||
int repCell = -1; double repPop = -1.0;
|
||||
for (size_t s = 0; s < planet.settlements.size(); ++s)
|
||||
if (s < sc.size() && sc[s] == ci && planet.settlements[s].population > repPop)
|
||||
{ repPop = planet.settlements[s].population; repCell = planet.settlements[s].cell; }
|
||||
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 19.0f };
|
||||
eventRowRects.push_back(row); atlasRowCells.push_back(repCell);
|
||||
DrawText(cu.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, cultureColor(ci));
|
||||
DrawText(TextFormat("%s %s", cultureEthosName(cu.ethos), faithFocusName(cu.faith)),
|
||||
(int)(r.x + r.width) - 118, (int)row.y + 3, 11, Color{150, 158, 178, 255});
|
||||
y += 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -780,10 +817,10 @@ void Viewer::renderHUD() {
|
||||
line("1 elev 2 plates 3 age 4 crust 5 biome 6 temp* 7 precip 8 flora 9 fauna 0 funga E eco (*6 cycles mean/summer/winter/season)");
|
||||
line(TextFormat("B borders [%s] | D vectors [%s] | G grid [%s] | J rivers [%s] | N day/night [%s] | T tides [%s] | O currents [%s]",
|
||||
showBorders ? "on" : "off", showDrift ? "on" : "off", showGrat ? "on" : "off", showRivers ? "on" : "off", dayNightOn ? "on" : "off", showTides ? "on" : "off", showCurrents ? "on" : "off"));
|
||||
line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | E eco | I habitability | U settlements [%s] | P territory [%s]",
|
||||
line(TextFormat("K clouds [%s] | V volcanoes [%s] | M names [%s] | E eco | I habitability | U settlements [%s] | P territory [%s] | X culture [%s]",
|
||||
showClouds ? "on" : "off", showVolcanoes ? "on" : "off", showNames ? "on" : "off",
|
||||
!planet.settlementsPlaced() ? "seed" : showSettlements ? "on" : "off",
|
||||
showNationBorders ? "on" : "off"));
|
||||
showNationBorders ? "on" : "off", showCultureBorders ? "on" : "off"));
|
||||
line(TextFormat("SPACE pause | [ / ] speed | S step | F fast-fwd | H hydrology [%s] | L biota [%s] | W live [%s] | R reseed | +/-",
|
||||
phase3 ? "on" : "off", planet.biotaPopulated() ? "on" : "off", liveWorld ? "on" : "off"));
|
||||
line("F5 save | F9 load | F12 screenshot | F2 reload planet.cfg");
|
||||
|
||||
@ -64,6 +64,7 @@ void Planet::buildGeometry() {
|
||||
sCellSettlement.assign(cells.size(), -1); sHabitability.clear();
|
||||
sCivCond.clear(); sCivDrought.clear();
|
||||
nations.clear(); sCellNation.assign(cells.size(), -1); sSettleNation.clear();
|
||||
cultures.clear(); sCellCulture.assign(cells.size(), -1); sSettleCulture.clear();
|
||||
}
|
||||
|
||||
void Planet::clearDerivedState() {
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
#include "PlanetBiota.hpp" // BiotaKind, Organism, CellBiota
|
||||
#include "PlanetGeography.hpp" // FeatureKind, GeoFeature
|
||||
#include "PlanetCiv.hpp" // Settlement, SettleTier, CivUpdate
|
||||
#include "PlanetNation.hpp" // Nation, NationTier
|
||||
#include "PlanetNation.hpp" // Nation, NationTier, GovType
|
||||
#include "PlanetCulture.hpp" // Culture, CultureEthos, Faith
|
||||
#include "PlanetEcoregions.hpp" // Ecoregion
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
@ -24,6 +25,7 @@ public:
|
||||
std::vector<Ecoregion> ecoRegions; // named ecological provinces (saved v19+)
|
||||
std::vector<Settlement> settlements; // civilization: settlements placed once, grow/decline (saved v20+)
|
||||
std::vector<Nation> nations; // realms grouped from settlements (derived each computeTerritory, not saved)
|
||||
std::vector<Culture> cultures; // peoples/cultures, one per inhabited continent (derived, not saved)
|
||||
|
||||
// Phase flag: false during Phase-1 forming (modest, original tectonics that
|
||||
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
||||
@ -199,6 +201,16 @@ public:
|
||||
const std::vector<Nation>& nationList() const { return nations; }
|
||||
const std::vector<int>& cellNation() const { return sCellNation; } // nation index per cell (-1 = wilderness/sea)
|
||||
const std::vector<int>& settleNation() const { return sSettleNation; } // nation index per settlement (-1 = dead)
|
||||
|
||||
// Cultures, beliefs & governments (PlanetCulture.cpp). computeCultures() groups settlements into
|
||||
// peoples (one per inhabited continent), gives each a dominant ethos + religion, assigns each realm
|
||||
// a government type (folded into nation.name), and tags cells/settlements with their culture. Runs
|
||||
// AFTER computeTerritory() (reads nations/sCellNation). Purely derived, recomputed, not saved.
|
||||
void computeCultures();
|
||||
bool culturesBuilt() const { return !cultures.empty(); }
|
||||
const std::vector<Culture>& cultureList() const { return cultures; }
|
||||
const std::vector<int>& cellCulture() const { return sCellCulture; } // culture index per cell (-1 = none)
|
||||
const std::vector<int>& settleCulture() const { return sSettleCulture; } // culture index per settlement (-1 = none)
|
||||
// Per-settlement live conditions (derived each stepCivilization; not saved). condition = the combined
|
||||
// environmental multiplier on carrying capacity (1 = normal, <1 = hardship, >1 = boom); drought =
|
||||
// current drought severity 0..1. Parallel to `settlements`. Used by the viewer for tint + events.
|
||||
@ -333,6 +345,9 @@ private:
|
||||
// Territory & nations (derived from settlements; not saved). sCellNation: nation index per cell
|
||||
// (-1 = wilderness/ocean); sSettleNation: nation index per settlement.
|
||||
std::vector<int> sCellNation, sSettleNation;
|
||||
// Cultures (derived from settlements + geography; not saved). sCellCulture: culture index per cell
|
||||
// (-1 = none/wilderness/ocean); sSettleCulture: culture index per settlement.
|
||||
std::vector<int> sCellCulture, sSettleCulture;
|
||||
|
||||
// Biota: derived density scalars (0..1; recomputed each tick, not saved) and the
|
||||
// on-demand discrete population (saved). sHasBiota latches once generated/loaded.
|
||||
|
||||
191
src/sim/PlanetCulture.cpp
Normal file
191
src/sim/PlanetCulture.cpp
Normal file
@ -0,0 +1,191 @@
|
||||
#include "Planet.hpp"
|
||||
#include "NameGen.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
|
||||
// --- Civilization Step 4: cultures, beliefs & governments --------------------
|
||||
// Settlements on the same continent share a CULTURE (a people/language family with an environment-driven
|
||||
// ethos + a religion); each realm gets a GOVERNMENT type folded into its name. A pure deterministic
|
||||
// function of the settlement set + geography + biomes (no RNG -> tectonic stream untouched), computed
|
||||
// AFTER computeTerritory() so `nations` / `sCellNation` already exist. Recomputed rather than saved.
|
||||
|
||||
namespace {
|
||||
// A pure hash for deterministic picks -- never touches rngState (mirrors civHash in PlanetCiv.cpp).
|
||||
inline uint32_t cultHash(uint32_t a) { a ^= a << 13; a ^= a >> 17; a ^= a << 5; return a ? a : 1u; }
|
||||
}
|
||||
|
||||
const char* cultureEthosName(CultureEthos e) {
|
||||
switch (e) {
|
||||
case CultureEthos::Agrarian: return "Agrarian";
|
||||
case CultureEthos::Seafaring: return "Seafaring";
|
||||
case CultureEthos::Nomadic: return "Nomadic";
|
||||
case CultureEthos::Highland: return "Highland";
|
||||
case CultureEthos::Mercantile: return "Mercantile";
|
||||
case CultureEthos::Warlike: return "Warlike";
|
||||
}
|
||||
return "Agrarian";
|
||||
}
|
||||
|
||||
const char* faithFocusName(Faith f) {
|
||||
switch (f) {
|
||||
case Faith::Sun: return "Sun";
|
||||
case Faith::Moon: return "Moon";
|
||||
case Faith::Sea: return "Sea";
|
||||
case Faith::Sky: return "Sky";
|
||||
case Faith::Earth: return "Earth";
|
||||
case Faith::Ancestors: return "Ancestors";
|
||||
case Faith::War: return "War";
|
||||
case Faith::Harvest: return "Harvest";
|
||||
}
|
||||
return "Earth";
|
||||
}
|
||||
|
||||
const char* govTypeName(GovType g) {
|
||||
switch (g) {
|
||||
case GovType::Tribe: return "Chiefdom";
|
||||
case GovType::CityRepublic: return "Republic";
|
||||
case GovType::Duchy: return "Duchy";
|
||||
case GovType::Kingdom: return "Kingdom";
|
||||
case GovType::Theocracy: return "Theocracy";
|
||||
case GovType::Confederation: return "Confederation";
|
||||
case GovType::Empire: return "Empire";
|
||||
case GovType::Autocracy: return "Autocracy";
|
||||
}
|
||||
return "Kingdom";
|
||||
}
|
||||
|
||||
void Planet::computeCultures() {
|
||||
const int n = (int)cells.size();
|
||||
cultures.clear();
|
||||
sCellCulture.assign(n, -1);
|
||||
sSettleCulture.assign(settlements.size(), -1);
|
||||
if (settlements.empty()) return;
|
||||
|
||||
const double sea = cfg.seaLevel, abP = cfg.civAbandonPop;
|
||||
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
||||
|
||||
// 1) Group living settlements into cultures, one per inhabited continent (regionId). Settlements
|
||||
// with no geography (regionId < 0) fall back to a per-language-bank culture.
|
||||
std::unordered_map<int, int> keyToCulture;
|
||||
auto living = [&](const Settlement& s) {
|
||||
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
||||
};
|
||||
for (size_t k = 0; k < settlements.size(); ++k) {
|
||||
const Settlement& s = settlements[k];
|
||||
if (!living(s)) continue;
|
||||
int key = (s.regionId >= 0) ? s.regionId : (-1 - s.bank);
|
||||
auto it = keyToCulture.find(key);
|
||||
int ci;
|
||||
if (it == keyToCulture.end()) {
|
||||
ci = (int)cultures.size();
|
||||
Culture cu; cu.id = (uint32_t)ci + 1; cu.regionId = s.regionId; cu.bank = s.bank;
|
||||
cultures.push_back(cu);
|
||||
keyToCulture[key] = ci;
|
||||
} else ci = it->second;
|
||||
sSettleCulture[k] = ci;
|
||||
cultures[ci].members++;
|
||||
cultures[ci].totalPop += s.population;
|
||||
}
|
||||
if (cultures.empty()) return;
|
||||
|
||||
// 2) Environmental tally over each culture's settlement cells -> ethos + a dominant biome for faith.
|
||||
struct Acc { int cnt = 0, coast = 0, high = 0, arid = 0, fert = 0; int biome[13] = {0}; };
|
||||
std::vector<Acc> acc(cultures.size());
|
||||
const bool haveMoist = (int)sMoist.size() == n;
|
||||
for (size_t k = 0; k < settlements.size(); ++k) {
|
||||
int ci = sSettleCulture[k]; if (ci < 0) continue;
|
||||
int c = settlements[k].cell;
|
||||
Acc& a = acc[ci]; a.cnt++;
|
||||
bool coast = false;
|
||||
for (int j : cells[c].neighbors) if (cells[j].elevation <= sea) { coast = true; break; }
|
||||
if (coast) a.coast++;
|
||||
Biome b = cells[c].biome;
|
||||
if ((int)b >= 0 && (int)b < 13) a.biome[(int)b]++;
|
||||
if (b == Biome::Mountains || b == Biome::Hills) a.high++;
|
||||
if (b == Biome::Desert || (haveMoist && sMoist[c] < 0.25)) a.arid++;
|
||||
if (b == Biome::Grassland || b == Biome::Forest || b == Biome::Wetland || b == Biome::Savanna) a.fert++;
|
||||
}
|
||||
|
||||
// 3) Per-culture ethos, faith and names (all deterministic hashes of the region + seed).
|
||||
for (size_t ci = 0; ci < cultures.size(); ++ci) {
|
||||
Culture& cu = cultures[ci];
|
||||
const Acc& a = acc[ci];
|
||||
double cnt = std::max(1, a.cnt);
|
||||
double fCoast = a.coast / cnt, fHigh = a.high / cnt, fArid = a.arid / cnt, fFert = a.fert / cnt;
|
||||
uint32_t rk = (uint32_t)(cu.regionId >= 0 ? cu.regionId : (1000 - cu.bank)) + 1u;
|
||||
|
||||
// Ethos: the dominant environmental signal; if none is strong, a deterministic social pick.
|
||||
CultureEthos best = CultureEthos::Agrarian; double bestF = fFert;
|
||||
if (fCoast > bestF) { bestF = fCoast; best = CultureEthos::Seafaring; }
|
||||
if (fHigh > bestF) { bestF = fHigh; best = CultureEthos::Highland; }
|
||||
if (fArid > bestF) { bestF = fArid; best = CultureEthos::Nomadic; }
|
||||
if (bestF >= 0.34) cu.ethos = best;
|
||||
else {
|
||||
uint32_t h = cultHash(seed ^ (rk * 2654435761u) ^ 0xC0FFEEu);
|
||||
cu.ethos = (cu.members >= 3 && h % 3u == 0u) ? CultureEthos::Mercantile
|
||||
: (h % 2u ? CultureEthos::Warlike : CultureEthos::Agrarian);
|
||||
}
|
||||
|
||||
// Faith focus: coastal peoples worship the Sea, else the dominant biome sets it, with a rare
|
||||
// deterministic Moon/War variation.
|
||||
Faith f;
|
||||
if (fCoast >= 0.5) f = Faith::Sea;
|
||||
else {
|
||||
int dom = 0; for (int bi = 1; bi < 13; ++bi) if (a.biome[bi] > a.biome[dom]) dom = bi;
|
||||
switch ((Biome)dom) {
|
||||
case Biome::Mountains: case Biome::Hills: f = Faith::Sky; break;
|
||||
case Biome::Desert: case Biome::Savanna: f = Faith::Sun; break;
|
||||
case Biome::Tundra: case Biome::Taiga: case Biome::Ice: f = Faith::Ancestors; break;
|
||||
case Biome::Forest: case Biome::Wetland: f = Faith::Harvest; break;
|
||||
case Biome::Beach: case Biome::Lake: case Biome::Ocean:f = Faith::Sea; break;
|
||||
default: f = Faith::Earth; break;
|
||||
}
|
||||
uint32_t h = cultHash(seed ^ (rk * 40503u) ^ 0xFA17u);
|
||||
if (h % 7u == 0u) f = Faith::Moon;
|
||||
else if (h % 13u == 0u) f = Faith::War;
|
||||
}
|
||||
cu.faith = f;
|
||||
|
||||
// People (demonym) + the faith's proper name, from the region's language bank.
|
||||
uint32_t nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u);
|
||||
uint32_t faithSeed = seed ^ cultHash(rk * 40503u + 0xFA17Fu);
|
||||
cu.name = "the " + namegen::makeName(nameSeed, cu.bank);
|
||||
std::string root = namegen::makeName(faithSeed, cu.bank);
|
||||
switch (cultHash(faithSeed) % 3u) {
|
||||
case 0: cu.faithName = "the " + root + " Faith"; break;
|
||||
case 1: cu.faithName = "Cult of " + root; break;
|
||||
default: cu.faithName = "the " + root + " Path"; break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Government per realm (from tier + a deterministic pick) + fold it into the realm's name, and
|
||||
// tag the realm with its capital's culture.
|
||||
for (Nation& nat : nations) {
|
||||
nat.cultureId = (nat.capital >= 0 && nat.capital < (int)sSettleCulture.size())
|
||||
? sSettleCulture[nat.capital] : -1;
|
||||
uint32_t gh = cultHash((nat.id * 2654435761u) ^ seed ^ 0x604Fu) % 3u;
|
||||
switch (nat.tier) {
|
||||
case NationTier::CityState: nat.gov = (gh == 0) ? GovType::CityRepublic : (gh == 1) ? GovType::Tribe : GovType::Theocracy; break;
|
||||
case NationTier::Kingdom: nat.gov = (gh == 0) ? GovType::Kingdom : (gh == 1) ? GovType::Duchy : GovType::Theocracy; break;
|
||||
case NationTier::Empire: nat.gov = (gh == 0) ? GovType::Empire : (gh == 1) ? GovType::Autocracy: GovType::Confederation; break;
|
||||
}
|
||||
const std::string& capName = settlements[nat.capital].name;
|
||||
switch (nat.gov) {
|
||||
case GovType::Tribe: nat.name = "Chiefdom of " + capName; break;
|
||||
case GovType::CityRepublic: nat.name = "Republic of " + capName; break;
|
||||
case GovType::Duchy: nat.name = "Duchy of " + capName; break;
|
||||
case GovType::Kingdom: nat.name = "Kingdom of " + capName; break;
|
||||
case GovType::Theocracy: nat.name = capName + " Theocracy"; break;
|
||||
case GovType::Confederation: nat.name = capName + " Confederation"; break;
|
||||
case GovType::Empire: nat.name = capName + " Empire"; break;
|
||||
case GovType::Autocracy: nat.name = capName + " Dominion"; break;
|
||||
}
|
||||
}
|
||||
|
||||
// 5) Per-cell culture = the culture of the realm that owns the cell (reuse territory's ownership).
|
||||
for (int i = 0; i < n; ++i) {
|
||||
int ni = sCellNation[i];
|
||||
if (ni >= 0 && ni < (int)nations.size()) sCellCulture[i] = nations[ni].cultureId;
|
||||
}
|
||||
}
|
||||
29
src/sim/PlanetCulture.hpp
Normal file
29
src/sim/PlanetCulture.hpp
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
// Civilization Step 4: culture, beliefs & governments. Settlements on the same continent share a
|
||||
// CULTURE -- a people/language family with a dominant ethos (drawn from their environment) and a
|
||||
// religion. Each realm (Step 3 Nation) gets a GOVERNMENT type folded into its name. Like territory,
|
||||
// all of this is a DETERMINISTIC function of the settlement set + geography + biomes/climate, computed
|
||||
// with pure hashes (no RNG touched) -- so it is recomputed rather than saved (no save-format change),
|
||||
// and the live stepper rewinds it for free.
|
||||
|
||||
enum class CultureEthos : uint8_t { Agrarian, Seafaring, Nomadic, Highland, Mercantile, Warlike };
|
||||
enum class Faith : uint8_t { Sun, Moon, Sea, Sky, Earth, Ancestors, War, Harvest };
|
||||
|
||||
struct Culture {
|
||||
uint32_t id = 0;
|
||||
int regionId = -1; // the continent/island geography-feature index this people belongs to
|
||||
int bank = 0; // NameGen "language" bank (shared by the region's places)
|
||||
CultureEthos ethos = CultureEthos::Agrarian;
|
||||
Faith faith = Faith::Earth;
|
||||
int members = 0; // number of settlements sharing this culture
|
||||
double totalPop = 0.0; // summed population of those settlements
|
||||
std::string name; // the people / demonym, e.g. "the Velmar"
|
||||
std::string faithName; // the religion's proper name, e.g. "the Tidewardens"
|
||||
};
|
||||
|
||||
const char* cultureEthosName(CultureEthos e); // "Agrarian" / "Seafaring" / "Nomadic" / ...
|
||||
const char* faithFocusName(Faith f); // "Sun" / "Sea" / "Ancestors" / ... (the object of worship)
|
||||
@ -55,6 +55,7 @@ void Planet::computeTerritory() {
|
||||
for (int c : order) {
|
||||
if (c == s) break; // order is descending -> rest are smaller
|
||||
if (capitalOf[c] != c) continue; // candidate must itself be a capital
|
||||
if (settlements[c].regionId != settlements[s].regionId) continue; // realms stay mono-cultural (civ Step 4)
|
||||
double d = ang(s, c);
|
||||
if (d < cfg.civVassalRange * range[c] && d < joinAng) { joinAng = d; joinCap = c; }
|
||||
}
|
||||
|
||||
@ -12,13 +12,20 @@
|
||||
|
||||
enum class NationTier : uint8_t { CityState, Kingdom, Empire };
|
||||
|
||||
// Government type of a realm (civ Step 4). Derived from tier + a deterministic pick and folded into the
|
||||
// realm's name ("Republic of X", "Duchy of X", "Holy X", "X Confederation", ...). Set by computeCultures.
|
||||
enum class GovType : uint8_t { Tribe, CityRepublic, Duchy, Kingdom, Theocracy, Confederation, Empire, Autocracy };
|
||||
|
||||
struct Nation {
|
||||
uint32_t id = 0;
|
||||
int capital = -1; // settlement index of the realm's capital (its largest city)
|
||||
int members = 0; // number of settlements in the realm
|
||||
double totalPop = 0.0; // summed population of its settlements
|
||||
NationTier tier = NationTier::CityState;
|
||||
std::string name; // e.g. "Kingdom of X" / "X Empire" / a city-state's bare name
|
||||
uint32_t id = 0;
|
||||
int capital = -1; // settlement index of the realm's capital (its largest city)
|
||||
int members = 0; // number of settlements in the realm
|
||||
double totalPop = 0.0; // summed population of its settlements
|
||||
NationTier tier = NationTier::CityState;
|
||||
GovType gov = GovType::Tribe; // civ Step 4: government type (set by computeCultures)
|
||||
int cultureId = -1; // civ Step 4: index into `cultures` of the realm's culture (-1 = none)
|
||||
std::string name; // e.g. "Kingdom of X" / "X Empire" / a city-state's bare name
|
||||
};
|
||||
|
||||
const char* nationTierName(NationTier t); // "City-state" / "Kingdom" / "Empire"
|
||||
const char* govTypeName(GovType g); // "Chiefdom" / "Republic" / "Duchy" / "Theocracy" / ...
|
||||
|
||||
184
test_culture.cpp
Normal file
184
test_culture.cpp
Normal file
@ -0,0 +1,184 @@
|
||||
// Headless test for civilization Step 4 (cultures, beliefs & governments). No display needed.
|
||||
//
|
||||
// g++ -std=c++17 -O2 -Isrc/sim test_culture.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/PlanetNation.cpp src/sim/PlanetCulture.cpp src/sim/PlanetIO.cpp -o /tmp/tc && /tmp/tc
|
||||
//
|
||||
// Verifies: one culture per inhabited continent; every living settlement has a culture; valid
|
||||
// ethos/faith; governments plausible per tier + reflected in the realm name; realms are mono-cultural;
|
||||
// determinism + RNG isolation; save->load->recompute parity.
|
||||
|
||||
#include "Planet.hpp"
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#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();
|
||||
}
|
||||
static void drift(Planet& p, int iters) {
|
||||
p.drifting = true;
|
||||
for (int k = 0; k < iters; ++k) { double dt = p.cflDtMy(); p.advect(dt); p.step(); p.erode(dt); if (k >= iters/2) p.hydrology(dt*0.2); }
|
||||
p.computeClimate(); p.classifyBiomes();
|
||||
}
|
||||
// Grouping key matching computeCultures: continent if geography is present, else a per-bank fallback.
|
||||
static int cultKey(const Settlement& s) { return (s.regionId >= 0) ? s.regionId : (-1 - s.bank); }
|
||||
|
||||
int main() {
|
||||
PlanetConfig cfg; cfg.subdivisions = 5; cfg.seed = 4242;
|
||||
Planet p; p.generate(cfg); settle(p); drift(p, 400);
|
||||
const int n = (int)p.cells.size();
|
||||
const double abP = p.cfg.civAbandonPop, yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
||||
p.placeSettlements();
|
||||
double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
|
||||
|
||||
std::printf("Culture: extraction\n");
|
||||
p.computeTerritory();
|
||||
p.computeCultures();
|
||||
check(!p.cultureList().empty(), "computeCultures produces cultures");
|
||||
check((int)p.cellCulture().size() == n && (int)p.settleCulture().size() == (int)p.settlements.size(), "index arrays sized");
|
||||
|
||||
std::printf("Culture: one culture per inhabited continent\n");
|
||||
{
|
||||
// The set of distinct grouping keys among living settlements == the number of cultures, and
|
||||
// every living settlement's key maps to exactly one culture (and vice versa).
|
||||
std::set<int> keys;
|
||||
std::map<int,int> keyToCult; // grouping key -> culture index (must be consistent)
|
||||
bool consistent = true, allLiving = true;
|
||||
for (size_t k = 0; k < p.settlements.size(); ++k) {
|
||||
const Settlement& s = p.settlements[k];
|
||||
if (s.population < abP) continue;
|
||||
int ci = p.settleCulture()[k];
|
||||
if (ci < 0 || ci >= (int)p.cultureList().size()) { allLiving = false; continue; }
|
||||
int key = cultKey(s);
|
||||
keys.insert(key);
|
||||
auto it = keyToCult.find(key);
|
||||
if (it == keyToCult.end()) keyToCult[key] = ci;
|
||||
else if (it->second != ci) consistent = false; // same continent -> must be same culture
|
||||
}
|
||||
// No two distinct keys share a culture.
|
||||
std::set<int> usedCults;
|
||||
bool distinct = true;
|
||||
for (auto& kv : keyToCult) { if (usedCults.count(kv.second)) distinct = false; usedCults.insert(kv.second); }
|
||||
std::printf(" %d cultures, %d inhabited continents/keys\n", (int)p.cultureList().size(), (int)keys.size());
|
||||
check(allLiving, "every living settlement has a valid culture");
|
||||
check(consistent, "settlements on the same continent share one culture");
|
||||
check(distinct, "no two continents share a culture");
|
||||
check((int)p.cultureList().size() == (int)keys.size(), "exactly one culture per inhabited continent");
|
||||
}
|
||||
|
||||
std::printf("Culture: valid ethos / faith / members\n");
|
||||
{
|
||||
bool ethosOk = true, faithOk = true, membersOk = true, namesOk = true;
|
||||
for (const Culture& c : p.cultureList()) {
|
||||
if ((int)c.ethos < 0 || (int)c.ethos > 5) ethosOk = false;
|
||||
if ((int)c.faith < 0 || (int)c.faith > 7) faithOk = false;
|
||||
if (c.members < 1 || c.totalPop <= 0.0) membersOk = false;
|
||||
if (c.name.empty() || c.faithName.empty()) namesOk = false;
|
||||
}
|
||||
check(ethosOk, "every culture ethos is a valid enum");
|
||||
check(faithOk, "every culture faith is a valid enum");
|
||||
check(membersOk, "every culture has >=1 member and positive population");
|
||||
check(namesOk, "every culture has a people name + a faith name");
|
||||
}
|
||||
|
||||
std::printf("Culture: governments plausible per tier + reflected in realm name\n");
|
||||
{
|
||||
bool govOk = true, nameOk = true, cultOk = true;
|
||||
for (const Nation& nat : p.nationList()) {
|
||||
bool tierOk =
|
||||
(nat.tier == NationTier::CityState && (nat.gov == GovType::CityRepublic || nat.gov == GovType::Tribe || nat.gov == GovType::Theocracy)) ||
|
||||
(nat.tier == NationTier::Kingdom && (nat.gov == GovType::Kingdom || nat.gov == GovType::Duchy || nat.gov == GovType::Theocracy)) ||
|
||||
(nat.tier == NationTier::Empire && (nat.gov == GovType::Empire || nat.gov == GovType::Autocracy || nat.gov == GovType::Confederation));
|
||||
if (!tierOk) govOk = false;
|
||||
// Rebuild the expected government-aware name and compare.
|
||||
const std::string& cap = p.settlements[nat.capital].name;
|
||||
std::string exp;
|
||||
switch (nat.gov) {
|
||||
case GovType::Tribe: exp = "Chiefdom of " + cap; break;
|
||||
case GovType::CityRepublic: exp = "Republic of " + cap; break;
|
||||
case GovType::Duchy: exp = "Duchy of " + cap; break;
|
||||
case GovType::Kingdom: exp = "Kingdom of " + cap; break;
|
||||
case GovType::Theocracy: exp = cap + " Theocracy"; break;
|
||||
case GovType::Confederation: exp = cap + " Confederation"; break;
|
||||
case GovType::Empire: exp = cap + " Empire"; break;
|
||||
case GovType::Autocracy: exp = cap + " Dominion"; break;
|
||||
}
|
||||
if (nat.name != exp) nameOk = false;
|
||||
if (nat.cultureId < 0 || nat.cultureId >= (int)p.cultureList().size()) cultOk = false;
|
||||
}
|
||||
check(govOk, "each realm's government fits its tier");
|
||||
check(nameOk, "each realm name reflects its government");
|
||||
check(cultOk, "each realm is tagged with a valid culture");
|
||||
}
|
||||
|
||||
std::printf("Culture: realms are mono-cultural (no realm spans two continents)\n");
|
||||
{
|
||||
std::map<int,int> nationCulture; // nation index -> the single culture of its members
|
||||
bool mono = true;
|
||||
for (size_t k = 0; k < p.settlements.size(); ++k) {
|
||||
int ni = p.settleNation()[k]; if (ni < 0) continue;
|
||||
int ci = p.settleCulture()[k];
|
||||
auto it = nationCulture.find(ni);
|
||||
if (it == nationCulture.end()) nationCulture[ni] = ci;
|
||||
else if (it->second != ci) mono = false;
|
||||
}
|
||||
check(mono, "every settlement in a realm shares one culture");
|
||||
}
|
||||
|
||||
std::printf("Culture: determinism\n");
|
||||
Planet q; q.generate(cfg); settle(q); drift(q, 400); q.placeSettlements();
|
||||
double lt2 = 0.0; for (int yr = 0; yr < 600; ++yr) { lt2 += 2.0 * yearH; q.stepCivilization(2.0 * yearH, lt2); }
|
||||
q.computeTerritory(); q.computeCultures();
|
||||
{
|
||||
bool same = (q.cellCulture() == p.cellCulture()) && (q.cultureList().size() == p.cultureList().size());
|
||||
if (same) for (size_t i = 0; i < q.cultureList().size(); ++i)
|
||||
if (q.cultureList()[i].name != p.cultureList()[i].name ||
|
||||
q.cultureList()[i].ethos != p.cultureList()[i].ethos ||
|
||||
q.cultureList()[i].faith != p.cultureList()[i].faith) same = false;
|
||||
check(same, "computeCultures is deterministic");
|
||||
}
|
||||
|
||||
std::printf("Culture: RNG isolation from tectonics\n");
|
||||
Planet x; x.generate(cfg); settle(x);
|
||||
Planet y; y.generate(cfg); settle(y);
|
||||
for (int k = 0; k < 40; ++k) {
|
||||
double dx = x.cflDtMy(); x.advect(dx); x.step(); x.erode(dx);
|
||||
double dy = y.cflDtMy(); y.advect(dy); y.step(); y.erode(dy);
|
||||
if (k == 20) { y.placeSettlements(); y.stepCivilization(yearH, yearH); y.computeTerritory(); y.computeCultures(); }
|
||||
}
|
||||
{
|
||||
bool terrainSame = true;
|
||||
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) terrainSame = false;
|
||||
check(terrainSame, "computeCultures never perturbs tectonic evolution");
|
||||
}
|
||||
|
||||
std::printf("Culture: save -> load -> recompute parity\n");
|
||||
{
|
||||
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||
p.writeState(ss);
|
||||
Planet r;
|
||||
bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true);
|
||||
check(ok, "readState accepts the v20 stream");
|
||||
r.computeTerritory(); r.computeCultures(); // cultures are derived, not saved
|
||||
check(r.cellCulture() == p.cellCulture(), "culture recomputed after load matches (derived, not saved)");
|
||||
}
|
||||
|
||||
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CULTURE CHECKS PASSED\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user