Civ Step 8: cultural evolution (assimilation, conversion, schism; save v23)
Cultures stop being static one-per-continent blocs. Culture identities are now stateful: the list is append-only (seeded once at the dawn, schism children appended later, records frozen after creation) and the per-settlement culture is mutable state. - stepCulture(year) in the yearly tick (pure hashes, no RNG): assimilation (a conquered settlement adopts its ruler's culture, which drops the Step-5 revolt cultBonus -> assimilation pacifies provinces), border conversion (population x trade-prestige pressure; realm capitals exempt), schism (a far-flung coherent cluster -- typically overseas colonies -- breaks away as a new people with a local-bank NameGen name and ethos/faith re-derived from its own lands). - computeCultures() became a pure derived refresh (seeds only when the list is empty); seedCultures() reproduces the old per-continent peoples byte-identically for the dawn and pre-v23 loads. - Colonies inherit the founder's culture at founding; the Step-3 mono-cultural vassalage rule is culture-matched (regionId fallback) so schism clusters found their own realms -> colonial independence wars. - Per-cell culture view colours by the owning settlement's culture, so a conquered city keeps its colour until it assimilates. - Save v23: culture identities + sSettleCulture + next-id counter, also in the step-back frames; snapshots rewind via truncate-and-replay. Pre-v23 saves re-seed on the next refresh. - Kind-7 world events; Cultures tab hides extinct peoples and shows a schism child's founding year. Knobs cult* in planet.cfg. - New test_cultevo.cpp (30 checks); all 15 existing suites still pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1090d2d46e
commit
4cb0ef9328
73
CLAUDE.md
73
CLAUDE.md
@ -249,8 +249,36 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived
|
|||||||
colony of Y"). Knobs `civColon*`. *(Also fixed: pressing `R` (reseed) after civilization existed left
|
colony of Y"). Knobs `civColon*`. *(Also fixed: pressing `R` (reseed) after civilization existed left
|
||||||
stale territory/culture/war/diplomacy/trade overlay lines on the new world — `regenWorld()` now clears
|
stale territory/culture/war/diplomacy/trade overlay lines on the new world — `regenWorld()` now clears
|
||||||
the civ overlay vectors + toggles.)*
|
the civ overlay vectors + toggles.)*
|
||||||
*Next steps (not yet built): cultural **evolution** (spread along borders, drift, assimilation, schism);
|
- **Cultural evolution (Step 8)** *(done — see `PlanetCulture.cpp` `stepCulture`, save v23)* — cultures
|
||||||
tribute / vassalage treaties; an accumulated treasury (funding armies / buying peace).*
|
stop being static one-per-continent blocs. Culture identities are now **stateful**: the list is
|
||||||
|
**append-only** (seeded once at the dawn — the same one-per-continent peoples as before — with schism
|
||||||
|
children appended later; a record is **frozen after creation** so indices/colours stay stable and the
|
||||||
|
ethos/faith no longer silently re-derive) and the **per-settlement culture is mutable state**.
|
||||||
|
`stepCulture(year)` runs in the yearly tick (between `stepConflict` and `stepColonization`), all **pure
|
||||||
|
hashes** of (stable id, year, seed) — no RNG: **assimilation** (a settlement held by a foreign-culture
|
||||||
|
overlord adopts its ruler's culture, `cultAssimRate` — which drops the Step-5 revolt `cultBonus` on its
|
||||||
|
own, so assimilation pacifies provinces), **border conversion** (a settlement dwarfed by a nearby foreign
|
||||||
|
culture's weight = population × trade prestige converts toward it, `cultConvert*`/`cultSpreadRange`/
|
||||||
|
`cultPrestigeWeight`; **realm capitals are exempt** — they anchor identity), and **schism** (a large
|
||||||
|
culture's far-flung coherent cluster — typically overseas colonies — breaks away as a **new people**,
|
||||||
|
`cultSchism*`: a fresh NameGen name from the local language bank, ethos/faith **re-derived from its own
|
||||||
|
lands** — keeping the parent's faith name if the faith is unchanged — parent id + founding year recorded).
|
||||||
|
**Colonies now inherit the founder's culture** at founding (the seed for later colonial schisms), and the
|
||||||
|
Step-3 mono-cultural vassalage rule is **culture-matched** (regionId fallback before the dawn / for
|
||||||
|
-1 entries) — a schism cluster stops vassalizing to the old capital, founds its own realm, and the
|
||||||
|
culture/faith diplomacy affinity drifts parent and child toward rivalry → **colonial independence wars
|
||||||
|
emerge**. The per-cell culture view (`X`) now colours by the owning **settlement's** culture, so a
|
||||||
|
conquered city keeps its people's colour until it assimilates. `computeCultures()` became a pure derived
|
||||||
|
refresh (tallies/governments/cell view; seeds only when the list is empty — the dawn / pre-v23-load
|
||||||
|
path); the state — culture identities + `sSettleCulture` (+ a next-id counter) — is **saved (v23)** and
|
||||||
|
**snapshotted via truncate-and-replay**: the list is append-only and `stepCulture` is a pure function,
|
||||||
|
so a step-back frame stores just the per-settlement vector + the list *length*, `,` truncates schism
|
||||||
|
children and a replay re-creates them identically (the colony-truncation precedent). **Kind=7**
|
||||||
|
`WorldEvent`s ("X adopts the culture of the Velmar", "X embraces the ways of the Nharos", "The Nharos
|
||||||
|
break away from the Velmar"); the **Cultures** tab hides extinct peoples (members 0 — slots persist) and
|
||||||
|
shows a schism child's founding year. Knobs `cult*`.
|
||||||
|
*Next steps (not yet built): tribute / vassalage treaties; an accumulated treasury (funding armies /
|
||||||
|
buying peace).*
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
@ -661,7 +689,7 @@ src/
|
|||||||
PlanetEcoregions.* generateEcoregions() (named ecological provinces + dominant biota/productivity)
|
PlanetEcoregions.* generateEcoregions() (named ecological provinces + dominant biota/productivity)
|
||||||
PlanetCiv.* computeHabitability/placeSettlements/stepCivilization/stepColonization (settlements + colonies; civ Step 2)
|
PlanetCiv.* computeHabitability/placeSettlements/stepCivilization/stepColonization (settlements + colonies; civ Step 2)
|
||||||
PlanetNation.* computeTerritory (realms + per-cell ownership + borders; civ Step 3)
|
PlanetNation.* computeTerritory (realms + per-cell ownership + borders; civ Step 3)
|
||||||
PlanetCulture.* computeCultures (cultures/ethos/faith per continent + governments; civ Step 4)
|
PlanetCulture.* computeCultures (derived refresh + one-time seeding; civ Step 4) + stepCulture (assimilation/conversion/schism; civ Step 8, save v23)
|
||||||
PlanetConflict.* stepConflict (wars/conquest/revolts + diplomacy/alliances/coalitions; civ Steps 5-6, save v21/v22)
|
PlanetConflict.* stepConflict (wars/conquest/revolts + diplomacy/alliances/coalitions; civ Steps 5-6, save v21/v22)
|
||||||
PlanetTrade.* computeTrade (trade routes + prosperity/wealth feeding growth; civ Step 7, derived)
|
PlanetTrade.* computeTrade (trade routes + prosperity/wealth feeding growth; civ Step 7, derived)
|
||||||
PlanetIO.cpp config file (text) + binary save/load
|
PlanetIO.cpp config file (text) + binary save/load
|
||||||
@ -732,8 +760,8 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp \
|
|||||||
```
|
```
|
||||||
(Swap `test_logic.cpp` for `test_biota.cpp`, `test_live.cpp`, `test_ocean.cpp`,
|
(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`,
|
`test_weather.cpp`, `test_volcano.cpp`, `test_geography.cpp`, `test_ecoregions.cpp`, `test_civ.cpp`,
|
||||||
`test_nation.cpp`, `test_culture.cpp`, `test_conflict.cpp`, `test_diplomacy.cpp`, `test_trade.cpp` or `test_colony.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
`test_nation.cpp`, `test_culture.cpp`, `test_conflict.cpp`, `test_diplomacy.cpp`, `test_trade.cpp`, `test_colony.cpp` or `test_cultevo.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
||||||
Civilization / Nation / Culture / Conflict / Diplomacy / Trade / Colony suites — same source list. CMake also builds `test_events` for the
|
Civilization / Nation / Culture / Conflict / Diplomacy / Trade / Colony / Cultural-evolution suites — same source list. CMake also builds `test_events` for the
|
||||||
viewer event journal.)
|
viewer event journal.)
|
||||||
|
|
||||||
Use this to verify tectonics after changing `Planet::step()` without launching
|
Use this to verify tectonics after changing `Planet::step()` without launching
|
||||||
@ -803,11 +831,15 @@ Territory view on (`P`), realms **wage war** each year — red war-fronts appear
|
|||||||
(borders move) or fall to ruins, empires fracture as provinces revolt; realms also form **alliances**
|
(borders move) or fall to ruins, empires fracture as provinces revolt; realms also form **alliances**
|
||||||
(green arcs) and **rivalries** (dark-red arcs), allies **join each other's wars** (coalitions) and never
|
(green arcs) and **rivalries** (dark-red arcs), allies **join each other's wars** (coalitions) and never
|
||||||
fight each other, and wars end in **peace treaties**; the **Realms** tab lists each realm's allies/
|
fight each other, and wars end in **peace treaties**; the **Realms** tab lists each realm's allies/
|
||||||
rivals/wars and the **Events** tab logs it all. `Z` shows the **trade economy** — routes (cyan sea /
|
rivals/wars and the **Events** tab logs it all. **Cultures evolve** on the same yearly tick (`X` view) —
|
||||||
|
conquered cities keep their people's colour until they **assimilate** into the ruler's culture, border
|
||||||
|
towns **convert** under a dominant neighbour's cultural weight, and a far-flung colony cluster can
|
||||||
|
**schism** into a new people (a new name/colour) that founds its own realm — colonial independence wars
|
||||||
|
follow. `Z` shows the **trade economy** — routes (cyan sea /
|
||||||
amber land) + a **wealth heat map**; well-connected coastal/river **hubs** grow richer & bigger, a war
|
amber land) + a **wealth heat map**; well-connected coastal/river **hubs** grow richer & bigger, a war
|
||||||
cuts the routes between belligerents, and the **Civ** tab shows each settlement's wealth. `Y` makes the 3D camera **follow a storm** (cycles by strength,
|
cuts the routes between belligerents, and the **Civ** tab shows each settlement's wealth. `Y` makes the 3D camera **follow a storm** (cycles by strength,
|
||||||
off after the last); `.`/`,` step the clock forward/back by one rate-unit (back rewinds the sky **and**
|
off after the last); `.`/`,` step the clock forward/back by one rate-unit (back rewinds the sky **and**
|
||||||
weather/storms/volcano lifecycle **and wars/conquests** via snapshots). Mouse-wheel over the 2D map zooms (drag pans).
|
weather/storms/volcano lifecycle **and wars/conquests and cultural shifts** via snapshots). Mouse-wheel over the 2D map zooms (drag pans).
|
||||||
|
|
||||||
CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config
|
CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config
|
||||||
file (both applied before the initial load/generate).
|
file (both applied before the initial load/generate).
|
||||||
@ -818,7 +850,7 @@ PlanetConfig param, auto-created on first run, reload with `F2`) and
|
|||||||
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
||||||
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
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
|
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
||||||
save header is versioned (currently **22**; v2 adds the `[`/`]` drift rate, v3 a
|
save header is versioned (currently **23**; v2 adds the `[`/`]` drift rate, v3 a
|
||||||
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
`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, v7 appends the **biota population**
|
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, v8 appends the **Live World**
|
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
|
||||||
@ -832,8 +864,11 @@ appends the **geography/atlas** block — named features + per-cell region indic
|
|||||||
reshuffle salt, v19 the **ecoregions** block, v20 the **civilization settlements** block (the
|
reshuffle salt, v19 the **ecoregions** block, v20 the **civilization settlements** block (the
|
||||||
fixed settlement set + per-frame populations in the step-back history), v21 the **civilization
|
fixed settlement set + per-frame populations in the step-back history), v21 the **civilization
|
||||||
conflict** block — per-settlement **allegiance** (conquest) + active **wars** + the war RNG, also added
|
conflict** block — per-settlement **allegiance** (conquest) + active **wars** + the war RNG, also added
|
||||||
to the step-back frames so a load can rewind conquests, and v22 the **diplomacy** block — standing realm
|
to the step-back frames so a load can rewind conquests, v22 the **diplomacy** block — standing realm
|
||||||
**relations** (alliances / rivalries / truces), likewise per-frame;
|
**relations** (alliances / rivalries / truces), likewise per-frame, and v23 the **culture** block —
|
||||||
|
stateful culture identities (append-only; schism children) + the per-settlement culture + a next-id
|
||||||
|
counter (cultural evolution), likewise per-frame (per-settlement vector + the list *length*; a rewind
|
||||||
|
truncates, a replay re-creates);
|
||||||
newer-than-supported is
|
newer-than-supported is
|
||||||
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
||||||
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
|
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
|
||||||
@ -844,7 +879,9 @@ entry); v14 volcanoes are discarded and reseeded as v15 lifecycle agents, with o
|
|||||||
pre-v16 saves load with an empty event journal; pre-v17 saves load with no geography (regenerated on
|
pre-v16 saves load with an empty event journal; pre-v17 saves load with no geography (regenerated on
|
||||||
demand via `M`); pre-v19 saves load with no ecoregions (regenerated via `E`); pre-v20 saves load with
|
demand via `M`); pre-v19 saves load with no ecoregions (regenerated via `E`); pre-v20 saves load with
|
||||||
no settlements (re-seeded via `U`); pre-v21 saves load with no wars (everyone independent; wars begin
|
no settlements (re-seeded via `U`); pre-v21 saves load with no wars (everyone independent; wars begin
|
||||||
again as the clock runs); pre-v22 saves load with no diplomacy (relations re-form as the clock runs).
|
again as the clock runs); pre-v22 saves load with no diplomacy (relations re-form as the clock runs);
|
||||||
|
pre-v23 saves load with no culture state (re-seeded one-per-continent on the next refresh — the same
|
||||||
|
peoples as before; evolution starts from there).
|
||||||
A load drops any **stale** pre-load `wxUndo` history and reloads the
|
A load drops any **stale** pre-load `wxUndo` history and reloads the
|
||||||
saved one.
|
saved one.
|
||||||
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
||||||
@ -1006,7 +1043,8 @@ triangles (plates are fixed in phase 1).
|
|||||||
- **Culture, beliefs & governments (civ Step 4, key `X`):** **no config knobs** — the ethos/faith/
|
- **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
|
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.
|
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()`.
|
Since Step 8 identities are stateful (seeded once, saved v23); to retune the rules, edit
|
||||||
|
`envPick()`/`seedCultures()` in `PlanetCulture.cpp` (a reseed applies them).
|
||||||
- **Conflict & war (`war*` in PlanetConfig / `planet.cfg`; civ Step 5, save v21):** `warMaxConcurrent`
|
- **Conflict & war (`war*` in PlanetConfig / `planet.cfg`; civ Step 5, save v21):** `warMaxConcurrent`
|
||||||
(6, simultaneous wars), `warDeclareRate` (0.12, war-declaration chance × hostility), the hostility
|
(6, simultaneous wars), `warDeclareRate` (0.12, war-declaration chance × hostility), the hostility
|
||||||
weights `warAmbition` (1.0, size gap) / `warIdeology` (0.8, culture+faith difference) / `warBorder`
|
weights `warAmbition` (1.0, size gap) / `warIdeology` (0.8, culture+faith difference) / `warBorder`
|
||||||
@ -1043,6 +1081,17 @@ triangles (plates are fixed in phase 1).
|
|||||||
`civMaxColonies` (120, cap beyond `civMaxSettlements`), `civColonySupply` (2.0, prosperity from the
|
`civMaxColonies` (120, cap beyond `civMaxSettlements`), `civColonySupply` (2.0, prosperity from the
|
||||||
overlord supply link that keeps colonies alive). Runs in the yearly `stepColonization` tick; raise the
|
overlord supply link that keeps colonies alive). Runs in the yearly `stepColonization` tick; raise the
|
||||||
rate/reach for aggressive colonial empires.
|
rate/reach for aggressive colonial empires.
|
||||||
|
- **Cultural evolution (`cult*` in PlanetConfig / `planet.cfg`; civ Step 8, save v23):** assimilation —
|
||||||
|
`cultAssimRate` (0.03, per-year chance a conquered settlement adopts its ruler's culture); border
|
||||||
|
conversion — `cultConvertRate` (0.02, per-year chance scale), `cultConvertDominance` (2.5, foreign
|
||||||
|
pressure must exceed this × own support — raise to make cultures stickier), `cultSpreadRange` (0.25 rad,
|
||||||
|
how far a settlement projects cultural pressure), `cultPrestigeWeight` (0.5, trade prosperity's boost to
|
||||||
|
cultural weight — rich hubs radiate culture); schism — `cultSchismMinMembers` (6, min living settlements
|
||||||
|
to schism), `cultSchismRange` (0.55 rad from the population centroid past which members are "distant" —
|
||||||
|
lower = easier colonial breakaways), `cultSchismMinCluster` (2, distant settlements needed to break away
|
||||||
|
together), `cultSchismRate` (0.08, per-year chance per qualifying culture). All rates 0 = the static
|
||||||
|
pre-Step-8 world. Runs in the yearly `stepCulture` tick (pure hashes, no RNG); the culture list is
|
||||||
|
capped at 64 peoples.
|
||||||
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||||
knob for how fast/high relief builds.
|
knob for how fast/high relief builds.
|
||||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||||
|
|||||||
@ -78,7 +78,7 @@ if(UNIX AND NOT APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation culture conflict diplomacy trade colony)
|
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation culture conflict diplomacy trade colony cultevo)
|
||||||
add_executable(test_${test_name} test_${test_name}.cpp)
|
add_executable(test_${test_name} test_${test_name}.cpp)
|
||||||
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
target_link_libraries(test_${test_name} PRIVATE planetsim_sim)
|
||||||
add_test(NAME ${test_name} COMMAND test_${test_name})
|
add_test(NAME ${test_name} COMMAND test_${test_name})
|
||||||
|
|||||||
@ -53,6 +53,13 @@ namespace {
|
|||||||
if (!validFrameWar(war, settlementCount)) return false;
|
if (!validFrameWar(war, settlementCount)) return false;
|
||||||
if (!warsSeen.insert(std::minmax(war.attacker, war.defender)).second) return false;
|
if (!warsSeen.insert(std::minmax(war.attacker, war.defender)).second) return false;
|
||||||
}
|
}
|
||||||
|
// v23: per-settlement culture (indices into the frame's culture-list prefix).
|
||||||
|
if (w.cultureCount > 100000) return false;
|
||||||
|
if (!w.settlementCulture.empty()) {
|
||||||
|
if ((int)w.settlementCulture.size() != settlementCount) return false;
|
||||||
|
for (int c : w.settlementCulture)
|
||||||
|
if (c < -1 || c >= (int)w.cultureCount) return false;
|
||||||
|
}
|
||||||
return sanitizeFrameDiplomacy(w.diplomacy, settlementCount);
|
return sanitizeFrameDiplomacy(w.diplomacy, settlementCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -565,6 +572,11 @@ void Viewer::saveGame(const char* path) {
|
|||||||
// v22: per-frame diplomacy, so a load can rewind alliances/rivalries past the saved moment.
|
// v22: per-frame diplomacy, so a load can rewind alliances/rivalries past the saved moment.
|
||||||
{ uint64_t m = f.w.diplomacy.size(); os.write((char*)&m, 8);
|
{ uint64_t m = f.w.diplomacy.size(); os.write((char*)&m, 8);
|
||||||
if (m) os.write((const char*)f.w.diplomacy.data(), (std::streamsize)(m * sizeof(DiploTie))); }
|
if (m) os.write((const char*)f.w.diplomacy.data(), (std::streamsize)(m * sizeof(DiploTie))); }
|
||||||
|
// v23: per-frame culture state (per-settlement culture + culture-list length), so a load can
|
||||||
|
// rewind conversions/assimilations/schisms past the saved moment.
|
||||||
|
{ uint64_t m = f.w.settlementCulture.size(); os.write((char*)&m, 8);
|
||||||
|
if (m) os.write((const char*)f.w.settlementCulture.data(), (std::streamsize)(m * sizeof(int))); }
|
||||||
|
os.write((char*)&f.w.cultureCount, 4); os.write((char*)&f.w.cultureNextId, 4);
|
||||||
}
|
}
|
||||||
// v16: persistent world event journal, separate from step-back history.
|
// v16: persistent world event journal, separate from step-back history.
|
||||||
uint32_t en = (uint32_t)std::min<size_t>(events.size(), (size_t)EVENT_LOG_MAX);
|
uint32_t en = (uint32_t)std::min<size_t>(events.size(), (size_t)EVENT_LOG_MAX);
|
||||||
@ -598,7 +610,7 @@ void Viewer::loadGame(const char* path) {
|
|||||||
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
is.read(reinterpret_cast<char*>(&lh), sizeof lh); } // v8: Live World clock
|
||||||
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
|
if (ver >= 13) is.read(reinterpret_cast<char*>(&lr), sizeof lr); // v13: Live World rate
|
||||||
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
if (!is || std::memcmp(magic, "PLSV", 4) != 0 || ver > SAVE_VERSION) { setStatus("Load failed: bad file"); return; }
|
||||||
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14, ver >= 15, ver >= 17, ver >= 18, ver >= 19, ver >= 20, ver >= 21, ver >= 22)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 old volcanoes, v15 stateful volcanoes, v17 geography, v18 geography salt, v19 ecoregions, v20 settlements, v21 conflict, v22 diplomacy
|
if (!planet.readState(is, ver >= 4, ver >= 7, ver >= 9, ver >= 10, ver >= 11, ver >= 14, ver >= 15, ver >= 17, ver >= 18, ver >= 19, ver >= 20, ver >= 21, ver >= 22, ver >= 23)) { setStatus("Load failed: corrupt/mismatch"); return; } // v4 biome, v7 biota, v9 moons, v10 weather, v11 storms, v14 old volcanoes, v15 stateful volcanoes, v17 geography, v18 geography salt, v19 ecoregions, v20 settlements, v21 conflict, v22 diplomacy, v23 cultures
|
||||||
cfg = planet.cfg; // adopt the loaded config
|
cfg = planet.cfg; // adopt the loaded config
|
||||||
elapsedMy = em; settled = (st != 0);
|
elapsedMy = em; settled = (st != 0);
|
||||||
planet.drifting = settled; // resume drift boosts iff mid-drift
|
planet.drifting = settled; // resume drift boosts iff mid-drift
|
||||||
@ -667,6 +679,13 @@ void Viewer::loadGame(const char* path) {
|
|||||||
else { f.w.diplomacy.resize((size_t)m); if (m) is.read((char*)f.w.diplomacy.data(), (std::streamsize)(m * sizeof(DiploTie))); }
|
else { f.w.diplomacy.resize((size_t)m); if (m) is.read((char*)f.w.diplomacy.data(), (std::streamsize)(m * sizeof(DiploTie))); }
|
||||||
if (!is) historyOk = false;
|
if (!is) historyOk = false;
|
||||||
}
|
}
|
||||||
|
if (ver >= 23) { // v23: per-frame culture state
|
||||||
|
uint64_t m = 0; is.read((char*)&m, 8);
|
||||||
|
if (!is || m > 1000000) historyOk = false;
|
||||||
|
else { f.w.settlementCulture.resize((size_t)m); if (m) is.read((char*)f.w.settlementCulture.data(), (std::streamsize)(m * sizeof(int))); }
|
||||||
|
is.read((char*)&f.w.cultureCount, 4); is.read((char*)&f.w.cultureNextId, 4);
|
||||||
|
if (!is) historyOk = false;
|
||||||
|
}
|
||||||
auto sized = [&](const std::vector<double>& v) { return v.empty() || v.size() == planet.cells.size(); };
|
auto sized = [&](const std::vector<double>& v) { return v.empty() || v.size() == planet.cells.size(); };
|
||||||
if (!is || !sized(f.w.humidity) || !sized(f.w.cloud) || !sized(f.w.rain)
|
if (!is || !sized(f.w.humidity) || !sized(f.w.cloud) || !sized(f.w.rain)
|
||||||
|| f.w.humidity.size() != f.w.cloud.size() || f.w.humidity.size() != f.w.rain.size())
|
|| f.w.humidity.size() != f.w.cloud.size() || f.w.humidity.size() != f.w.rain.size())
|
||||||
@ -826,6 +845,10 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
|
|||||||
ConflictUpdate wu = planet.stepConflict(y);
|
ConflictUpdate wu = planet.stepConflict(y);
|
||||||
for (const WarEvent& e : wu.events)
|
for (const WarEvent& e : wu.events)
|
||||||
appendEvent(e.kind, e.severity, liveTime, e.cell, 0, e.title, e.detail);
|
appendEvent(e.kind, e.severity, liveTime, e.cell, 0, e.title, e.detail);
|
||||||
|
// Step 8 after conflict (assimilation reads this year's fresh allegiance) and before
|
||||||
|
// colonization (a colony is stamped with its founder's possibly-just-changed culture).
|
||||||
|
for (const WarEvent& e : planet.stepCulture(y)) // cultural evolution
|
||||||
|
appendEvent(e.kind, e.severity, liveTime, e.cell, 0, e.title, e.detail);
|
||||||
for (const WarEvent& e : planet.stepColonization(y)) // kingdoms found new colonies (appends settlements)
|
for (const WarEvent& e : planet.stepColonization(y)) // kingdoms found new colonies (appends settlements)
|
||||||
appendEvent(e.kind, e.severity, liveTime, e.cell, 0, e.title, e.detail);
|
appendEvent(e.kind, e.severity, liveTime, e.cell, 0, e.title, e.detail);
|
||||||
if (y < year) rebuildTerritory(); // recompute between years so the next year's war sees it
|
if (y < year) rebuildTerritory(); // recompute between years so the next year's war sees it
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||||
struct Viewer {
|
struct Viewer {
|
||||||
// ---- Files / save format ------------------------------------------------
|
// ---- Files / save format ------------------------------------------------
|
||||||
static constexpr uint32_t SAVE_VERSION = 22; // v22: civ diplomacy; v21: civ conflict/wars; v20: civ settlements; v19: ecoregions; v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
static constexpr uint32_t SAVE_VERSION = 23; // v23: civ cultural evolution; v22: civ diplomacy; v21: civ conflict/wars; v20: civ settlements; v19: ecoregions; v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
||||||
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
||||||
static constexpr int EVENT_LOG_MAX = 200;
|
static constexpr int EVENT_LOG_MAX = 200;
|
||||||
const char* CONFIG_PATH = "planet.cfg";
|
const char* CONFIG_PATH = "planet.cfg";
|
||||||
|
|||||||
@ -616,7 +616,7 @@ void Viewer::renderLiveInfo() {
|
|||||||
: Color{175, 205, 235, 255};
|
: Color{175, 205, 235, 255};
|
||||||
DrawRectangleRec(row, bg);
|
DrawRectangleRec(row, bg);
|
||||||
DrawRectangleLinesEx(row, 1, Color{70, 75, 92, 255});
|
DrawRectangleLinesEx(row, 1, Color{70, 75, 92, 255});
|
||||||
const char* icon = e.kind == 2 ? "^" : e.kind == 3 ? "*" : e.kind == 4 ? "#" : e.kind == 5 ? "!" : e.kind == 6 ? "=" : "~";
|
const char* icon = e.kind == 2 ? "^" : e.kind == 3 ? "*" : e.kind == 4 ? "#" : e.kind == 5 ? "!" : e.kind == 6 ? "=" : e.kind == 7 ? "@" : "~";
|
||||||
DrawText(icon, (int)row.x + 7, (int)row.y + 6, 18, fg);
|
DrawText(icon, (int)row.x + 7, (int)row.y + 6, 18, fg);
|
||||||
double d = e.timeHours / std::max(0.1, planet.cfg.dayLengthHours);
|
double d = e.timeHours / std::max(0.1, planet.cfg.dayLengthHours);
|
||||||
DrawText(TextFormat("D%.1f", d), (int)row.x + 24, (int)row.y + 5, 12, Color{145, 155, 175, 255});
|
DrawText(TextFormat("D%.1f", d), (int)row.x + 24, (int)row.y + 5, 12, Color{145, 155, 175, 255});
|
||||||
@ -798,6 +798,7 @@ void Viewer::renderLiveInfo() {
|
|||||||
for (int ci : idx) {
|
for (int ci : idx) {
|
||||||
if (y > (int)(r.y + r.height) - 22) break;
|
if (y > (int)(r.y + r.height) - 22) break;
|
||||||
const Culture& cu = C[ci];
|
const Culture& cu = C[ci];
|
||||||
|
if (cu.members <= 0) continue; // extinct people (Step 8): keeps its slot, not listed
|
||||||
// Representative cell: the largest living settlement of this culture (to fly to).
|
// Representative cell: the largest living settlement of this culture (to fly to).
|
||||||
int repCell = -1; double repPop = -1.0;
|
int repCell = -1; double repPop = -1.0;
|
||||||
for (size_t s = 0; s < planet.settlements.size(); ++s)
|
for (size_t s = 0; s < planet.settlements.size(); ++s)
|
||||||
@ -806,6 +807,10 @@ void Viewer::renderLiveInfo() {
|
|||||||
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 19.0f };
|
Rectangle row{ r.x + 10.0f, (float)y - 2.0f, r.width - 20.0f, 19.0f };
|
||||||
eventRowRects.push_back(row); atlasRowCells.push_back(repCell);
|
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(cu.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, cultureColor(ci));
|
||||||
|
// Step 8: a schism child shows its founding year (roots have been there since the dawn).
|
||||||
|
if (cu.foundedYear >= 0)
|
||||||
|
DrawText(TextFormat("b. yr %ld", cu.foundedYear),
|
||||||
|
(int)(r.x + r.width) - 180, (int)row.y + 3, 11, Color{135, 142, 160, 255});
|
||||||
DrawText(TextFormat("%s %s", cultureEthosName(cu.ethos), faithFocusName(cu.faith)),
|
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});
|
(int)(r.x + r.width) - 118, (int)row.y + 3, 11, Color{150, 158, 178, 255});
|
||||||
y += 20;
|
y += 20;
|
||||||
|
|||||||
@ -64,7 +64,7 @@ void Planet::buildGeometry() {
|
|||||||
sCellSettlement.assign(cells.size(), -1); sHabitability.clear();
|
sCellSettlement.assign(cells.size(), -1); sHabitability.clear();
|
||||||
sCivCond.clear(); sCivDrought.clear();
|
sCivCond.clear(); sCivDrought.clear();
|
||||||
nations.clear(); sCellNation.assign(cells.size(), -1); sSettleNation.clear();
|
nations.clear(); sCellNation.assign(cells.size(), -1); sSettleNation.clear();
|
||||||
cultures.clear(); sCellCulture.assign(cells.size(), -1); sSettleCulture.clear();
|
cultures.clear(); sCellCulture.assign(cells.size(), -1); sSettleCulture.clear(); sCultureNextId = 1;
|
||||||
sCellSettleOwner.assign(cells.size(), -1);
|
sCellSettleOwner.assign(cells.size(), -1);
|
||||||
sProsperity.clear(); sTradeLinks.clear(); sCellWealth.assign(cells.size(), 0.0);
|
sProsperity.clear(); sTradeLinks.clear(); sCellWealth.assign(cells.size(), 0.0);
|
||||||
wars.clear(); sSettleAllegiance.clear(); diplomacy.clear();
|
wars.clear(); sSettleAllegiance.clear(); diplomacy.clear();
|
||||||
|
|||||||
@ -210,11 +210,15 @@ public:
|
|||||||
const std::vector<int>& cellNation() const { return sCellNation; } // nation index per cell (-1 = wilderness/sea)
|
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)
|
const std::vector<int>& settleNation() const { return sSettleNation; } // nation index per settlement (-1 = dead)
|
||||||
|
|
||||||
// Cultures, beliefs & governments (PlanetCulture.cpp). computeCultures() groups settlements into
|
// Cultures, beliefs & governments (PlanetCulture.cpp). computeCultures() is the DERIVED refresh:
|
||||||
// peoples (one per inhabited continent), gives each a dominant ethos + religion, assigns each realm
|
// it seeds the cultures once (one per inhabited continent) when none exist, then only recomputes
|
||||||
// a government type (folded into nation.name), and tags cells/settlements with their culture. Runs
|
// tallies, governments (folded into nation.name) and the per-cell culture view. Runs AFTER
|
||||||
// AFTER computeTerritory() (reads nations/sCellNation). Purely derived, recomputed, not saved.
|
// computeTerritory() (reads nations/sCellNation/sCellSettleOwner). Culture identities + the
|
||||||
|
// per-settlement culture are STATEFUL since Step 8 (saved v23 + snapshotted); stepCulture() is
|
||||||
|
// the once-per-sim-year mutation pass (border conversion / assimilation / schism, pure hashes --
|
||||||
|
// no RNG touched, so step-back replays it exactly).
|
||||||
void computeCultures();
|
void computeCultures();
|
||||||
|
std::vector<WarEvent> stepCulture(long year);
|
||||||
bool culturesBuilt() const { return !cultures.empty(); }
|
bool culturesBuilt() const { return !cultures.empty(); }
|
||||||
const std::vector<Culture>& cultureList() const { return cultures; }
|
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>& cellCulture() const { return sCellCulture; } // culture index per cell (-1 = none)
|
||||||
@ -267,12 +271,14 @@ public:
|
|||||||
// whether the stream carries the active reshuffle salt (save v18+); hasEcoregions: whether the
|
// whether the stream carries the active reshuffle salt (save v18+); hasEcoregions: whether the
|
||||||
// stream carries the ecoregion atlas (save v19+). Older saves regenerate on demand.
|
// stream carries the ecoregion atlas (save v19+). Older saves regenerate on demand.
|
||||||
// hasSettlements: whether the stream carries the civilization settlements block (save v20+); older
|
// hasSettlements: whether the stream carries the civilization settlements block (save v20+); older
|
||||||
// saves load with none (re-seeded on demand via the civ key).
|
// saves load with none (re-seeded on demand via the civ key). hasCultures: whether the stream
|
||||||
|
// carries the stateful culture block (save v23+); older saves re-seed one culture per continent
|
||||||
|
// on the next computeCultures().
|
||||||
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
|
bool readState(std::istream& is, bool hasBiome = true, bool hasBiota = true,
|
||||||
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
|
bool hasMoons = true, bool hasWeather = true, bool hasStorms = true,
|
||||||
bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true,
|
bool hasVolcanoes = true, bool hasStatefulVolcanoes = true, bool hasGeography = true,
|
||||||
bool hasGeoSalt = true, bool hasEcoregions = true, bool hasSettlements = true,
|
bool hasGeoSalt = true, bool hasEcoregions = true, bool hasSettlements = true,
|
||||||
bool hasConflict = true, bool hasDiplo = true);
|
bool hasConflict = true, bool hasDiplo = true, bool hasCultures = true);
|
||||||
|
|
||||||
// Helpers for rendering / info.
|
// Helpers for rendering / info.
|
||||||
double cellWidthMeters() const; // approx lateral cell spacing
|
double cellWidthMeters() const; // approx lateral cell spacing
|
||||||
@ -292,7 +298,7 @@ private:
|
|||||||
bool hasMoons, bool hasWeather, bool hasStorms,
|
bool hasMoons, bool hasWeather, bool hasStorms,
|
||||||
bool hasVolcanoes, bool hasStatefulVolcanoes, bool hasGeography,
|
bool hasVolcanoes, bool hasStatefulVolcanoes, bool hasGeography,
|
||||||
bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
||||||
bool hasConflict, bool hasDiplo);
|
bool hasConflict, bool hasDiplo, bool hasCultures);
|
||||||
void clearDerivedState(); // clear geometry-dependent scratch/derived fields
|
void clearDerivedState(); // clear geometry-dependent scratch/derived fields
|
||||||
void assignPlates();
|
void assignPlates();
|
||||||
void seedInitialRelief();
|
void seedInitialRelief();
|
||||||
@ -382,9 +388,13 @@ private:
|
|||||||
// Territory & nations (derived from settlements; not saved). sCellNation: nation index per cell
|
// Territory & nations (derived from settlements; not saved). sCellNation: nation index per cell
|
||||||
// (-1 = wilderness/ocean); sSettleNation: nation index per settlement.
|
// (-1 = wilderness/ocean); sSettleNation: nation index per settlement.
|
||||||
std::vector<int> sCellNation, sSettleNation;
|
std::vector<int> sCellNation, sSettleNation;
|
||||||
// Cultures (derived from settlements + geography; not saved). sCellCulture: culture index per cell
|
// Cultures (civ Step 8: stateful, saved v23). sSettleCulture: culture index per settlement --
|
||||||
// (-1 = none/wilderness/ocean); sSettleCulture: culture index per settlement.
|
// mutable identity state (seeded at the dawn, changed only by stepCulture()/colony founding);
|
||||||
|
// sCellCulture: the derived per-cell view (refreshed by computeCultures(), not saved).
|
||||||
|
// sCultureNextId: stable id counter for appended (schism) cultures.
|
||||||
std::vector<int> sCellCulture, sSettleCulture;
|
std::vector<int> sCellCulture, sSettleCulture;
|
||||||
|
uint32_t sCultureNextId = 1;
|
||||||
|
void seedCultures(); // one-time seeding (dawn / pre-v23 load), PlanetCulture.cpp
|
||||||
// Per-cell owning settlement index (filled by computeTerritory alongside sCellNation; -1 = none).
|
// Per-cell owning settlement index (filled by computeTerritory alongside sCellNation; -1 = none).
|
||||||
// Reused by the trade wealth field so the heat map matches territory exactly.
|
// Reused by the trade wealth field so the heat map matches territory exactly.
|
||||||
std::vector<int> sCellSettleOwner;
|
std::vector<int> sCellSettleOwner;
|
||||||
|
|||||||
@ -319,6 +319,10 @@ std::vector<WarEvent> Planet::stepColonization(long year) {
|
|||||||
sSettleNation[newIdx] = (int)ni; // the colony is (already) the founder's realm
|
sSettleNation[newIdx] = (int)ni; // the colony is (already) the founder's realm
|
||||||
if (sSettleAllegiance.size() != settlements.size()) sSettleAllegiance.resize(settlements.size(), -1);
|
if (sSettleAllegiance.size() != settlements.size()) sSettleAllegiance.resize(settlements.size(), -1);
|
||||||
sSettleAllegiance[newIdx] = cap; // the colony belongs to the founder's realm
|
sSettleAllegiance[newIdx] = cap; // the colony belongs to the founder's realm
|
||||||
|
// Step 8: the colony carries its FOUNDER's culture (not the target continent's) -- the seed for
|
||||||
|
// later colonial schisms. The settlement name stays in the local land's language bank.
|
||||||
|
if (sSettleCulture.size() != settlements.size()) sSettleCulture.resize(settlements.size(), -1);
|
||||||
|
sSettleCulture[newIdx] = (cap < (int)sSettleCulture.size()) ? sSettleCulture[cap] : -1;
|
||||||
if (sCivCond.size() != settlements.size()) { sCivCond.resize(settlements.size(), 1.0); sCivDrought.resize(settlements.size(), 0.0); }
|
if (sCivCond.size() != settlements.size()) { sCivCond.resize(settlements.size(), 1.0); sCivDrought.resize(settlements.size(), 0.0); }
|
||||||
ev.push_back(WarEvent{ 1, best, "The " + nat.name + " founds the colony of " + settlements[newIdx].name,
|
ev.push_back(WarEvent{ 1, best, "The " + nat.name + " founds the colony of " + settlements[newIdx].name,
|
||||||
"A new settlement rises on distant land.", 3 });
|
"A new settlement rises on distant land.", 3 });
|
||||||
|
|||||||
@ -1,18 +1,93 @@
|
|||||||
#include "Planet.hpp"
|
#include "Planet.hpp"
|
||||||
#include "NameGen.hpp"
|
#include "NameGen.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
// --- Civilization Step 4: cultures, beliefs & governments --------------------
|
// --- Civilization Step 4 + Step 8: cultures, beliefs, governments & cultural evolution --------------
|
||||||
// Settlements on the same continent share a CULTURE (a people/language family with an environment-driven
|
// Settlements share a CULTURE (a people/language family with an environment-driven ethos + a religion);
|
||||||
// ethos + a religion); each realm gets a GOVERNMENT type folded into its name. A pure deterministic
|
// each realm gets a GOVERNMENT type folded into its name. Since Step 8 the culture layer is split:
|
||||||
// function of the settlement set + geography + biomes (no RNG -> tectonic stream untouched), computed
|
// - seedCultures() one-time seeding at the dawn (one culture per inhabited continent) -- also runs
|
||||||
// AFTER computeTerritory() so `nations` / `sCellNation` already exist. Recomputed rather than saved.
|
// for pre-v23 loads. Identities (name/ethos/faith) are FROZEN at creation.
|
||||||
|
// - computeCultures() the DERIVED refresh (tallies, governments, per-cell view). Runs on every
|
||||||
|
// territory rebuild / load / step-back, so it must never mutate identity state.
|
||||||
|
// - stepCulture() the once-per-sim-year mutation pass: assimilation (conquered settlements adopt
|
||||||
|
// their ruler's culture), border conversion (dominant foreign cultural pressure)
|
||||||
|
// and schism (a far-flung cluster becomes a new people). Pure hashes of
|
||||||
|
// (stable id, year, seed) -- no RNG touched, so step-back replays it exactly.
|
||||||
|
// The culture list is append-only and records are immutable after creation (indices/colors stay
|
||||||
|
// stable; extinct cultures keep their slot); the list + sSettleCulture are saved (v23) + snapshotted.
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// A pure hash for deterministic picks -- never touches rngState (mirrors civHash in PlanetCiv.cpp).
|
// 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; }
|
inline uint32_t cultHash(uint32_t a) { a ^= a << 13; a ^= a >> 17; a ^= a << 5; return a ? a : 1u; }
|
||||||
|
inline double cultHashf(uint32_t a) { return (cultHash(a) & 0xFFFFFFu) / double(0x1000000); } // [0,1)
|
||||||
|
|
||||||
|
// Environmental tally over a set of settlement cells -> the signals that pick an ethos + faith.
|
||||||
|
// Factored out of the old computeCultures() so schism children re-derive theirs the same way.
|
||||||
|
struct CultEnv { int cnt = 0, coast = 0, high = 0, arid = 0, fert = 0; int biome[13] = {0}; };
|
||||||
|
|
||||||
|
void envAdd(CultEnv& a, const std::vector<Cell>& cells, const std::vector<double>& moist,
|
||||||
|
double sea, int c) {
|
||||||
|
if (c < 0 || c >= (int)cells.size()) return;
|
||||||
|
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 || (moist.size() == cells.size() && moist[c] < 0.25)) a.arid++;
|
||||||
|
if (b == Biome::Grassland || b == Biome::Forest || b == Biome::Wetland || b == Biome::Savanna) a.fert++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ethos: the dominant environmental signal; if none is strong, a deterministic social pick.
|
||||||
|
// Faith: coastal peoples worship the Sea, else the dominant biome sets it, with a rare variation.
|
||||||
|
void envPick(const CultEnv& a, uint32_t seed, uint32_t rk, int members,
|
||||||
|
CultureEthos& ethos, Faith& faith) {
|
||||||
|
double cnt = std::max(1, a.cnt);
|
||||||
|
double fCoast = a.coast / cnt, fHigh = a.high / cnt, fArid = a.arid / cnt, fFert = a.fert / cnt;
|
||||||
|
|
||||||
|
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) ethos = best;
|
||||||
|
else {
|
||||||
|
uint32_t h = cultHash(seed ^ (rk * 2654435761u) ^ 0xC0FFEEu);
|
||||||
|
ethos = (members >= 3 && h % 3u == 0u) ? CultureEthos::Mercantile
|
||||||
|
: (h % 2u ? CultureEthos::Warlike : CultureEthos::Agrarian);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
faith = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The faith's proper name from a seed + language bank ("the X Faith" / "Cult of X" / "the X Path").
|
||||||
|
std::string makeFaithName(uint32_t faithSeed, int bank) {
|
||||||
|
std::string root = namegen::makeName(faithSeed, bank);
|
||||||
|
switch (cultHash(faithSeed) % 3u) {
|
||||||
|
case 0: return "the " + root + " Faith";
|
||||||
|
case 1: return "Cult of " + root;
|
||||||
|
default: return "the " + root + " Path";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* cultureEthosName(CultureEthos e) {
|
const char* cultureEthosName(CultureEthos e) {
|
||||||
@ -55,18 +130,21 @@ const char* govTypeName(GovType g) {
|
|||||||
return "Kingdom";
|
return "Kingdom";
|
||||||
}
|
}
|
||||||
|
|
||||||
void Planet::computeCultures() {
|
// One-time seeding: group living settlements into cultures, one per inhabited continent (regionId;
|
||||||
const int n = (int)cells.size();
|
// settlements with no geography fall back to a per-language-bank culture), then freeze each culture's
|
||||||
|
// ethos/faith/names. Byte-identical to the pre-Step-8 derivation, so a pre-v23 load or the dawn
|
||||||
|
// produces the same peoples as before.
|
||||||
|
void Planet::seedCultures() {
|
||||||
cultures.clear();
|
cultures.clear();
|
||||||
sCellCulture.assign(n, -1);
|
|
||||||
sSettleCulture.assign(settlements.size(), -1);
|
sSettleCulture.assign(settlements.size(), -1);
|
||||||
|
sCultureNextId = 1;
|
||||||
if (settlements.empty()) return;
|
if (settlements.empty()) return;
|
||||||
|
|
||||||
|
const int n = (int)cells.size();
|
||||||
const double sea = cfg.seaLevel, abP = cfg.civAbandonPop;
|
const double sea = cfg.seaLevel, abP = cfg.civAbandonPop;
|
||||||
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
||||||
|
|
||||||
// 1) Group living settlements into cultures, one per inhabited continent (regionId). Settlements
|
// 1) Group living settlements into cultures.
|
||||||
// with no geography (regionId < 0) fall back to a per-language-bank culture.
|
|
||||||
std::unordered_map<int, int> keyToCulture;
|
std::unordered_map<int, int> keyToCulture;
|
||||||
auto living = [&](const Settlement& s) {
|
auto living = [&](const Settlement& s) {
|
||||||
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
||||||
@ -79,7 +157,7 @@ void Planet::computeCultures() {
|
|||||||
int ci;
|
int ci;
|
||||||
if (it == keyToCulture.end()) {
|
if (it == keyToCulture.end()) {
|
||||||
ci = (int)cultures.size();
|
ci = (int)cultures.size();
|
||||||
Culture cu; cu.id = (uint32_t)ci + 1; cu.regionId = s.regionId; cu.bank = s.bank;
|
Culture cu; cu.id = sCultureNextId++; cu.regionId = s.regionId; cu.bank = s.bank;
|
||||||
cultures.push_back(cu);
|
cultures.push_back(cu);
|
||||||
keyToCulture[key] = ci;
|
keyToCulture[key] = ci;
|
||||||
} else ci = it->second;
|
} else ci = it->second;
|
||||||
@ -89,77 +167,68 @@ void Planet::computeCultures() {
|
|||||||
}
|
}
|
||||||
if (cultures.empty()) return;
|
if (cultures.empty()) return;
|
||||||
|
|
||||||
// 2) Environmental tally over each culture's settlement cells -> ethos + a dominant biome for faith.
|
// 2) Environmental tally over each culture's settlement cells.
|
||||||
struct Acc { int cnt = 0, coast = 0, high = 0, arid = 0, fert = 0; int biome[13] = {0}; };
|
std::vector<CultEnv> acc(cultures.size());
|
||||||
std::vector<Acc> acc(cultures.size());
|
|
||||||
const bool haveMoist = (int)sMoist.size() == n;
|
|
||||||
for (size_t k = 0; k < settlements.size(); ++k) {
|
for (size_t k = 0; k < settlements.size(); ++k) {
|
||||||
int ci = sSettleCulture[k]; if (ci < 0) continue;
|
int ci = sSettleCulture[k]; if (ci < 0) continue;
|
||||||
int c = settlements[k].cell;
|
envAdd(acc[ci], cells, sMoist, sea, 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).
|
// 3) Per-culture ethos, faith and names (all deterministic hashes of the region + seed).
|
||||||
for (size_t ci = 0; ci < cultures.size(); ++ci) {
|
for (size_t ci = 0; ci < cultures.size(); ++ci) {
|
||||||
Culture& cu = cultures[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;
|
uint32_t rk = (uint32_t)(cu.regionId >= 0 ? cu.regionId : (1000 - cu.bank)) + 1u;
|
||||||
|
envPick(acc[ci], seed, rk, cu.members, cu.ethos, cu.faith);
|
||||||
// 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 nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u);
|
||||||
uint32_t faithSeed = seed ^ cultHash(rk * 40503u + 0xFA17Fu);
|
uint32_t faithSeed = seed ^ cultHash(rk * 40503u + 0xFA17Fu);
|
||||||
cu.name = "the " + namegen::makeName(nameSeed, cu.bank);
|
cu.name = "the " + namegen::makeName(nameSeed, cu.bank);
|
||||||
std::string root = namegen::makeName(faithSeed, cu.bank);
|
cu.faithName = makeFaithName(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
|
// The DERIVED refresh: seeds once when no cultures exist, then only recomputes tallies, governments
|
||||||
|
// and the per-cell culture view from the (stateful) per-settlement culture. Safe to call on every
|
||||||
|
// territory rebuild / load / step-back -- it never reassigns a settlement's culture (only sanitizes
|
||||||
|
// out-of-range entries and backfills unset living ones from the nearest cultured neighbour).
|
||||||
|
void Planet::computeCultures() {
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
sCellCulture.assign(n, -1);
|
||||||
|
if (settlements.empty()) { sSettleCulture.clear(); return; }
|
||||||
|
if (cultures.empty()) seedCultures();
|
||||||
|
if (cultures.empty()) return; // no living settlement yet -> seeded on a later refresh
|
||||||
|
|
||||||
|
const double abP = cfg.civAbandonPop;
|
||||||
|
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
||||||
|
auto living = [&](size_t k) {
|
||||||
|
const Settlement& s = settlements[k];
|
||||||
|
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per-settlement culture is STATE (Step 8): resize/sanitize + backfill only, never reassign.
|
||||||
|
if (sSettleCulture.size() != settlements.size()) sSettleCulture.resize(settlements.size(), -1);
|
||||||
|
for (int& c : sSettleCulture) if (c < -1 || c >= (int)cultures.size()) c = -1;
|
||||||
|
for (size_t k = 0; k < settlements.size(); ++k) {
|
||||||
|
if (!living(k) || sSettleCulture[k] >= 0) continue;
|
||||||
|
int bestS = -1; double bestD = 1e9; // nearest living cultured settlement (deterministic)
|
||||||
|
for (size_t j = 0; j < settlements.size(); ++j) {
|
||||||
|
if (j == k || !living(j) || sSettleCulture[j] < 0) continue;
|
||||||
|
double d = std::acos(std::clamp(
|
||||||
|
cells[settlements[k].cell].unit.dot(cells[settlements[j].cell].unit), -1.0, 1.0));
|
||||||
|
if (d < bestD) { bestD = d; bestS = (int)j; }
|
||||||
|
}
|
||||||
|
if (bestS >= 0) sSettleCulture[k] = sSettleCulture[bestS];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derived tallies (living settlements only; extinct cultures keep their slot with members = 0).
|
||||||
|
for (Culture& cu : cultures) { cu.members = 0; cu.totalPop = 0.0; }
|
||||||
|
for (size_t k = 0; k < settlements.size(); ++k) {
|
||||||
|
int ci = sSettleCulture[k];
|
||||||
|
if (ci < 0 || !living(k)) continue;
|
||||||
|
cultures[ci].members++;
|
||||||
|
cultures[ci].totalPop += settlements[k].population;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Government per realm (from tier + a deterministic pick) + fold it into the realm's name, and
|
||||||
// tag the realm with its capital's culture.
|
// tag the realm with its capital's culture.
|
||||||
for (Nation& nat : nations) {
|
for (Nation& nat : nations) {
|
||||||
nat.cultureId = (nat.capital >= 0 && nat.capital < (int)sSettleCulture.size())
|
nat.cultureId = (nat.capital >= 0 && nat.capital < (int)sSettleCulture.size())
|
||||||
@ -183,9 +252,191 @@ void Planet::computeCultures() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Per-cell culture = the culture of the realm that owns the cell (reuse territory's ownership).
|
// Per-cell culture: prefer the owning SETTLEMENT's culture (Step 8 -- a conquered city keeps its
|
||||||
|
// people's colour inside the conqueror's realm until it assimilates); fall back to the realm's.
|
||||||
|
const bool haveOwner = (int)sCellSettleOwner.size() == n;
|
||||||
|
const bool haveNation = (int)sCellNation.size() == n;
|
||||||
|
if (!haveNation) return;
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
int ni = sCellNation[i];
|
int ni = sCellNation[i];
|
||||||
if (ni >= 0 && ni < (int)nations.size()) sCellCulture[i] = nations[ni].cultureId;
|
if (ni < 0 || ni >= (int)nations.size()) continue;
|
||||||
|
int cu = -1;
|
||||||
|
if (haveOwner) {
|
||||||
|
int ow = sCellSettleOwner[i];
|
||||||
|
if (ow >= 0 && ow < (int)sSettleCulture.size()) cu = sSettleCulture[ow];
|
||||||
|
}
|
||||||
|
sCellCulture[i] = (cu >= 0) ? cu : nations[ni].cultureId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Civilization Step 8: the yearly cultural-evolution pass ----------------------------------------
|
||||||
|
// Three passes, all pure hashes of (stable id, year, seed) so a step-back replays them exactly:
|
||||||
|
// A) assimilation -- a settlement held by a foreign-culture overlord adopts the ruler's culture
|
||||||
|
// (the Step-5 revolt cultBonus then drops on its own: assimilation pacifies provinces);
|
||||||
|
// B) border conversion -- a settlement dwarfed by a nearby foreign culture's weight (population +
|
||||||
|
// trade prestige) converts toward it (realm capitals are exempt: they anchor identity);
|
||||||
|
// C) schism -- a large culture's far-flung coherent cluster (overseas colonies) breaks away as a
|
||||||
|
// NEW people: a fresh name from the local language bank, ethos/faith re-derived from its own
|
||||||
|
// lands (appended to `cultures`; step-back truncates the list and replay re-creates it).
|
||||||
|
std::vector<WarEvent> Planet::stepCulture(long year) {
|
||||||
|
std::vector<WarEvent> ev;
|
||||||
|
const int n = (int)cells.size();
|
||||||
|
if (settlements.empty() || cultures.empty()) return ev;
|
||||||
|
if (sSettleCulture.size() != settlements.size()) return ev; // refresh hasn't run yet
|
||||||
|
const double abP = cfg.civAbandonPop;
|
||||||
|
const uint32_t seed = cfg.seed ? cfg.seed : 1u;
|
||||||
|
const size_t ns = settlements.size();
|
||||||
|
auto living = [&](size_t k) {
|
||||||
|
const Settlement& s = settlements[k];
|
||||||
|
return s.cell >= 0 && s.cell < n && s.population >= abP;
|
||||||
|
};
|
||||||
|
auto ang = [&](const Vec3& a, const Vec3& b) {
|
||||||
|
return std::acos(std::clamp(a.dot(b), -1.0, 1.0));
|
||||||
|
};
|
||||||
|
auto unitOf = [&](size_t k) -> const Vec3& { return cells[settlements[k].cell].unit; };
|
||||||
|
std::vector<char> converted(ns, 0); // at most one culture change per settlement per year
|
||||||
|
|
||||||
|
// A) Assimilation under foreign rule.
|
||||||
|
if (sSettleAllegiance.size() == ns && cfg.cultAssimRate > 0.0) {
|
||||||
|
for (size_t s = 0; s < ns; ++s) {
|
||||||
|
if (!living(s)) continue;
|
||||||
|
int ov = sSettleAllegiance[s];
|
||||||
|
if (ov < 0 || ov >= (int)ns || ov == (int)s || !living((size_t)ov)) continue;
|
||||||
|
int myCult = sSettleCulture[s], ovCult = sSettleCulture[ov];
|
||||||
|
if (ovCult < 0 || ovCult >= (int)cultures.size() || myCult == ovCult) continue;
|
||||||
|
if (cultHashf(settlements[s].id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0xA5513Au)
|
||||||
|
>= cfg.cultAssimRate) continue;
|
||||||
|
sSettleCulture[s] = ovCult; converted[s] = 1;
|
||||||
|
ev.push_back(WarEvent{ 1, settlements[s].cell,
|
||||||
|
settlements[s].name + " adopts the culture of " + cultures[ovCult].name,
|
||||||
|
"Generations under foreign rule erode the old ways.", 7 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// B) Border conversion under dominant foreign cultural pressure.
|
||||||
|
if (cfg.cultConvertRate > 0.0) {
|
||||||
|
std::vector<char> isCapital(ns, 0);
|
||||||
|
for (const Nation& nat : nations)
|
||||||
|
if (nat.capital >= 0 && nat.capital < (int)ns) isCapital[nat.capital] = 1;
|
||||||
|
const bool haveProsp = sProsperity.size() == ns;
|
||||||
|
std::vector<double> w(ns, 0.0); // cultural weight: population x (1 + prestige from trade)
|
||||||
|
for (size_t k = 0; k < ns; ++k)
|
||||||
|
if (living(k))
|
||||||
|
w[k] = settlements[k].population
|
||||||
|
* (1.0 + cfg.cultPrestigeWeight * (haveProsp ? sProsperity[k] : 0.0));
|
||||||
|
const double range = std::max(1e-6, cfg.cultSpreadRange);
|
||||||
|
std::vector<double> pressure(cultures.size(), 0.0);
|
||||||
|
for (size_t s = 0; s < ns; ++s) {
|
||||||
|
if (!living(s) || converted[s] || isCapital[s]) continue;
|
||||||
|
int myCult = sSettleCulture[s];
|
||||||
|
if (myCult < 0 || myCult >= (int)cultures.size()) continue;
|
||||||
|
std::fill(pressure.begin(), pressure.end(), 0.0);
|
||||||
|
for (size_t j = 0; j < ns; ++j) {
|
||||||
|
if (j == s || !living(j)) continue;
|
||||||
|
int cj = sSettleCulture[j]; if (cj < 0 || cj >= (int)cultures.size()) continue;
|
||||||
|
double d = ang(unitOf(s), unitOf(j));
|
||||||
|
if (d < range) pressure[cj] += w[j] * (1.0 - d / range);
|
||||||
|
}
|
||||||
|
double own = pressure[myCult] + w[s];
|
||||||
|
int cBest = -1; double pBest = 0.0;
|
||||||
|
for (size_t c = 0; c < cultures.size(); ++c)
|
||||||
|
if ((int)c != myCult && pressure[c] > pBest) { pBest = pressure[c]; cBest = (int)c; }
|
||||||
|
if (cBest < 0 || own <= 0.0 || pBest <= cfg.cultConvertDominance * own) continue;
|
||||||
|
double rate = cfg.cultConvertRate * std::min(3.0, pBest / (cfg.cultConvertDominance * own));
|
||||||
|
if (cultHashf(settlements[s].id * 2654435761u ^ (uint32_t)year * 19349663u ^ seed ^ 0xB07DE4u)
|
||||||
|
>= rate) continue;
|
||||||
|
sSettleCulture[s] = cBest; converted[s] = 1;
|
||||||
|
ev.push_back(WarEvent{ 1, settlements[s].cell,
|
||||||
|
settlements[s].name + " embraces the ways of " + cultures[cBest].name,
|
||||||
|
"Kinship and trade draw the town into a foreign sphere.", 7 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C) Schism: a distant coherent cluster of a large culture becomes a new people.
|
||||||
|
const size_t ncult = cultures.size(); // iterate the pre-pass list (children append)
|
||||||
|
if (cfg.cultSchismRate > 0.0 && (int)ncult < 64) {
|
||||||
|
std::vector<int> memCount(ncult, 0);
|
||||||
|
std::vector<Vec3> cSum(ncult);
|
||||||
|
for (size_t s = 0; s < ns; ++s) {
|
||||||
|
if (!living(s)) continue;
|
||||||
|
int ci = sSettleCulture[s]; if (ci < 0 || ci >= (int)ncult) continue;
|
||||||
|
memCount[ci]++;
|
||||||
|
cSum[ci] = cSum[ci] + unitOf(s) * std::max(1.0, settlements[s].population);
|
||||||
|
}
|
||||||
|
const double range = std::max(1e-6, cfg.cultSpreadRange);
|
||||||
|
for (size_t ci = 0; ci < ncult && (int)cultures.size() < 64; ++ci) {
|
||||||
|
if (memCount[ci] < std::max(1, cfg.cultSchismMinMembers)) continue;
|
||||||
|
if (cSum[ci].length() <= 1e-12) continue;
|
||||||
|
Vec3 centroid = cSum[ci].normalized(); // population-weighted cultural core
|
||||||
|
// One deterministic roll per culture-year (cheap early exit; pure hash, no state consumed).
|
||||||
|
if (cultHashf(cultures[ci].id * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0x5C1531u)
|
||||||
|
>= cfg.cultSchismRate) continue;
|
||||||
|
// Members far from the core; the breakaway cluster = the farthest one + distant members
|
||||||
|
// near it (a coherent region / overseas colony group, not scattered strays).
|
||||||
|
std::vector<int> distant; int far = -1; double farD = 0.0;
|
||||||
|
for (size_t s = 0; s < ns; ++s) {
|
||||||
|
if (!living(s) || sSettleCulture[s] != (int)ci) continue;
|
||||||
|
double d = ang(unitOf(s), centroid);
|
||||||
|
if (d > cfg.cultSchismRange) {
|
||||||
|
distant.push_back((int)s);
|
||||||
|
if (d > farD) { farD = d; far = (int)s; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (far < 0 || (int)distant.size() < std::max(1, cfg.cultSchismMinCluster)) continue;
|
||||||
|
std::vector<int> cluster;
|
||||||
|
for (int s : distant)
|
||||||
|
if (ang(unitOf((size_t)s), unitOf((size_t)far)) <= range) cluster.push_back(s);
|
||||||
|
if ((int)cluster.size() < std::max(1, cfg.cultSchismMinCluster)) continue;
|
||||||
|
if ((int)cluster.size() >= memCount[ci]) continue; // a schism splits, it never renames all
|
||||||
|
|
||||||
|
// Copy the parent fields we need BEFORE the push_back (it invalidates references).
|
||||||
|
const uint32_t parId = cultures[ci].id;
|
||||||
|
const std::string parName = cultures[ci].name;
|
||||||
|
const Faith parFaith = cultures[ci].faith;
|
||||||
|
const std::string parFaithName = cultures[ci].faithName;
|
||||||
|
const int parBank = cultures[ci].bank;
|
||||||
|
|
||||||
|
// Majority regionId of the cluster (ties resolve by cluster order -- deterministic).
|
||||||
|
std::unordered_map<int, int> regCount; int regBest = -1, regBestN = 0;
|
||||||
|
for (int s : cluster) {
|
||||||
|
int c = ++regCount[settlements[s].regionId];
|
||||||
|
if (c > regBestN) { regBestN = c; regBest = settlements[s].regionId; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Culture child;
|
||||||
|
child.id = sCultureNextId++;
|
||||||
|
child.parentId = (int)parId;
|
||||||
|
child.foundedYear = year;
|
||||||
|
child.regionId = regBest;
|
||||||
|
child.bank = (regBest >= 0) ? namegen::bankForRegion(cfg.seed, regBest) : parBank;
|
||||||
|
|
||||||
|
// Ethos/faith re-derived from the cluster's own lands (hash-salted by the child id so the
|
||||||
|
// faith can mutate); keep the parent's faith name if the faith itself is unchanged.
|
||||||
|
CultEnv env;
|
||||||
|
for (int s : cluster) envAdd(env, cells, sMoist, cfg.seaLevel, settlements[s].cell);
|
||||||
|
uint32_t rk = child.id * 0x9E3779B9u + 0xC41Du;
|
||||||
|
envPick(env, seed, rk, (int)cluster.size(), child.ethos, child.faith);
|
||||||
|
|
||||||
|
uint32_t nameSeed = seed ^ cultHash(rk * 2654435761u + 0x50C1A1u);
|
||||||
|
std::string root = namegen::makeName(nameSeed, child.bank);
|
||||||
|
auto usedName = [&](const std::string& r) {
|
||||||
|
for (const Culture& c : cultures) if (c.name == "the " + r) return true; return false;
|
||||||
|
};
|
||||||
|
for (int g = 0; usedName(root) && g < 128; ++g)
|
||||||
|
root = namegen::makeName(nameSeed += 0x9E3779B9u, child.bank);
|
||||||
|
child.name = "the " + root;
|
||||||
|
child.faithName = (child.faith == parFaith)
|
||||||
|
? parFaithName
|
||||||
|
: makeFaithName(seed ^ cultHash(rk * 40503u + 0xFA17Fu), child.bank);
|
||||||
|
|
||||||
|
int childIdx = (int)cultures.size();
|
||||||
|
for (int s : cluster) { sSettleCulture[s] = childIdx; converted[s] = 1; }
|
||||||
|
std::string title = child.name + " break away from " + parName;
|
||||||
|
title[0] = (char)std::toupper((unsigned char)title[0]);
|
||||||
|
ev.push_back(WarEvent{ 2, settlements[far].cell, title,
|
||||||
|
"A distant people drifts apart and names itself anew.", 7 });
|
||||||
|
cultures.push_back(std::move(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|||||||
@ -5,10 +5,13 @@
|
|||||||
|
|
||||||
// Civilization Step 4: culture, beliefs & governments. Settlements on the same continent share a
|
// 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
|
// 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,
|
// religion. Each realm (Step 3 Nation) gets a GOVERNMENT type folded into its name.
|
||||||
// all of this is a DETERMINISTIC function of the settlement set + geography + biomes/climate, computed
|
// Civilization Step 8 (cultural evolution) made culture identities STATEFUL: the culture list is
|
||||||
// with pure hashes (no RNG touched) -- so it is recomputed rather than saved (no save-format change),
|
// append-only (seeded once per continent at the dawn, schism children appended later; a record is
|
||||||
// and the live stepper rewinds it for free.
|
// immutable after creation so indices/colors stay stable) and the per-settlement culture is mutable
|
||||||
|
// state (conversion / assimilation / schism / colonial inheritance) -- both saved (v23) and
|
||||||
|
// snapshotted. members/totalPop tallies, Nation.cultureId/governments and the per-cell culture view
|
||||||
|
// stay DERIVED (refreshed by computeCultures(), which never mutates identity state).
|
||||||
|
|
||||||
enum class CultureEthos : uint8_t { Agrarian, Seafaring, Nomadic, Highland, Mercantile, Warlike };
|
enum class CultureEthos : uint8_t { Agrarian, Seafaring, Nomadic, Highland, Mercantile, Warlike };
|
||||||
enum class Faith : uint8_t { Sun, Moon, Sea, Sky, Earth, Ancestors, War, Harvest };
|
enum class Faith : uint8_t { Sun, Moon, Sea, Sky, Earth, Ancestors, War, Harvest };
|
||||||
@ -19,8 +22,10 @@ struct Culture {
|
|||||||
int bank = 0; // NameGen "language" bank (shared by the region's places)
|
int bank = 0; // NameGen "language" bank (shared by the region's places)
|
||||||
CultureEthos ethos = CultureEthos::Agrarian;
|
CultureEthos ethos = CultureEthos::Agrarian;
|
||||||
Faith faith = Faith::Earth;
|
Faith faith = Faith::Earth;
|
||||||
int members = 0; // number of settlements sharing this culture
|
int members = 0; // number of settlements sharing this culture (derived tally)
|
||||||
double totalPop = 0.0; // summed population of those settlements
|
double totalPop = 0.0; // summed population of those settlements (derived tally)
|
||||||
|
int parentId = -1; // (Step 8) culture id this one schismed from (-1 = a root people)
|
||||||
|
long foundedYear = -1; // (Step 8) sim year of the schism (-1 = present since the dawn)
|
||||||
std::string name; // the people / demonym, e.g. "the Velmar"
|
std::string name; // the people / demonym, e.g. "the Velmar"
|
||||||
std::string faithName; // the religion's proper name, e.g. "the Tidewardens"
|
std::string faithName; // the religion's proper name, e.g. "the Tidewardens"
|
||||||
};
|
};
|
||||||
|
|||||||
@ -66,12 +66,15 @@
|
|||||||
D(tradeWarBlock) D(tradeAllyBonus) D(tradeDiploBonus) D(tradeTemptWar) \
|
D(tradeWarBlock) D(tradeAllyBonus) D(tradeDiploBonus) D(tradeTemptWar) \
|
||||||
D(civColonizeRate) D(civColonyMinPop) D(civColonyMinHab) D(civColonyReach) D(civColonySeaReach) \
|
D(civColonizeRate) D(civColonyMinPop) D(civColonyMinHab) D(civColonyReach) D(civColonySeaReach) \
|
||||||
D(civColonySpacing) D(civColonySupply) \
|
D(civColonySpacing) D(civColonySupply) \
|
||||||
|
D(cultAssimRate) D(cultConvertRate) D(cultConvertDominance) D(cultSpreadRange) \
|
||||||
|
D(cultPrestigeWeight) D(cultSchismRange) D(cultSchismRate) \
|
||||||
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
||||||
I(volcanoMaxCount) \
|
I(volcanoMaxCount) \
|
||||||
I(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
|
I(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
|
||||||
I(geoOceanDeep) I(civMaxSettlements) I(civEmpireMinMembers) I(warMaxConcurrent) I(civMaxColonies) \
|
I(geoOceanDeep) I(civMaxSettlements) I(civEmpireMinMembers) I(warMaxConcurrent) I(civMaxColonies) \
|
||||||
|
I(cultSchismMinMembers) I(cultSchismMinCluster) \
|
||||||
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
||||||
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
|
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
|
||||||
U(seed)
|
U(seed)
|
||||||
@ -332,6 +335,15 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.civColonySpacing, 0.0, 3.14159, "civColonySpacing"));
|
E(rng(cfg.civColonySpacing, 0.0, 3.14159, "civColonySpacing"));
|
||||||
E(rng(cfg.civColonySupply, 0.0, 100.0, "civColonySupply"));
|
E(rng(cfg.civColonySupply, 0.0, 100.0, "civColonySupply"));
|
||||||
E(irng(cfg.civMaxColonies, 0, 1000000, "civMaxColonies"));
|
E(irng(cfg.civMaxColonies, 0, 1000000, "civMaxColonies"));
|
||||||
|
E(rng(cfg.cultAssimRate, 0.0, 1.0, "cultAssimRate"));
|
||||||
|
E(rng(cfg.cultConvertRate, 0.0, 1.0, "cultConvertRate"));
|
||||||
|
E(rng(cfg.cultConvertDominance, 0.0, 1000.0, "cultConvertDominance"));
|
||||||
|
E(rng(cfg.cultSpreadRange, 0.0, 3.14159, "cultSpreadRange"));
|
||||||
|
E(rng(cfg.cultPrestigeWeight, 0.0, 100.0, "cultPrestigeWeight"));
|
||||||
|
E(irng(cfg.cultSchismMinMembers, 1, 1000000, "cultSchismMinMembers"));
|
||||||
|
E(rng(cfg.cultSchismRange, 0.0, 3.14159, "cultSchismRange"));
|
||||||
|
E(irng(cfg.cultSchismMinCluster, 1, 1000000, "cultSchismMinCluster"));
|
||||||
|
E(rng(cfg.cultSchismRate, 0.0, 1.0, "cultSchismRate"));
|
||||||
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
||||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||||
@ -553,17 +565,33 @@ void Planet::writeState(std::ostream& os) const {
|
|||||||
writePod(os, t.a); writePod(os, t.b); writePod(os, t.attitude);
|
writePod(os, t.a); writePod(os, t.b); writePod(os, t.attitude);
|
||||||
writePod(os, t.truceUntil); uint8_t k = (uint8_t)t.kind; writePod(os, k);
|
writePod(os, t.truceUntil); uint8_t k = (uint8_t)t.kind; writePod(os, k);
|
||||||
}
|
}
|
||||||
|
// v23: cultural evolution (civ Step 8). Culture identities are stateful (schism children append;
|
||||||
|
// frozen names/ethos/faith) and the per-settlement culture is mutable state. members/totalPop are
|
||||||
|
// derived tallies -> not written (recomputed by computeCultures()).
|
||||||
|
uint64_t ncu = cultures.size(); writePod(os, ncu);
|
||||||
|
for (const Culture& cu : cultures) {
|
||||||
|
writePod(os, cu.id); writePod(os, cu.regionId); writePod(os, cu.bank);
|
||||||
|
uint8_t e = (uint8_t)cu.ethos; writePod(os, e);
|
||||||
|
uint8_t f = (uint8_t)cu.faith; writePod(os, f);
|
||||||
|
writePod(os, cu.parentId); writePod(os, cu.foundedYear);
|
||||||
|
uint64_t L = cu.name.size(); writePod(os, L);
|
||||||
|
if (L) os.write(cu.name.data(), (std::streamsize)L);
|
||||||
|
L = cu.faithName.size(); writePod(os, L);
|
||||||
|
if (L) os.write(cu.faithName.data(), (std::streamsize)L);
|
||||||
|
}
|
||||||
|
writeVec(os, sSettleCulture);
|
||||||
|
writePod(os, sCultureNextId);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
||||||
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
||||||
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
||||||
bool hasConflict, bool hasDiplo) {
|
bool hasConflict, bool hasDiplo, bool hasCultures) {
|
||||||
Planet tmp;
|
Planet tmp;
|
||||||
tmp.drifting = drifting; // readState never serialized this flag; preserve old caller-visible behavior.
|
tmp.drifting = drifting; // readState never serialized this flag; preserve old caller-visible behavior.
|
||||||
if (!tmp.readStateImpl(is, hasBiome, hasBiota, hasMoons, hasWeather, hasStorms, hasVolcanoes,
|
if (!tmp.readStateImpl(is, hasBiome, hasBiota, hasMoons, hasWeather, hasStorms, hasVolcanoes,
|
||||||
hasStatefulVolcanoes, hasGeography, hasGeoSalt, hasEcoregions,
|
hasStatefulVolcanoes, hasGeography, hasGeoSalt, hasEcoregions,
|
||||||
hasSettlements, hasConflict, hasDiplo))
|
hasSettlements, hasConflict, hasDiplo, hasCultures))
|
||||||
return false;
|
return false;
|
||||||
*this = std::move(tmp);
|
*this = std::move(tmp);
|
||||||
return true;
|
return true;
|
||||||
@ -572,7 +600,7 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
|||||||
bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool hasMoons,
|
||||||
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
bool hasWeather, bool hasStorms, bool hasVolcanoes, bool hasStatefulVolcanoes,
|
||||||
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
bool hasGeography, bool hasGeoSalt, bool hasEcoregions, bool hasSettlements,
|
||||||
bool hasConflict, bool hasDiplo) {
|
bool hasConflict, bool hasDiplo, bool hasCultures) {
|
||||||
// Save blocks are append-only by version. If a caller asks for an older prefix,
|
// Save blocks are append-only by version. If a caller asks for an older prefix,
|
||||||
// later blocks cannot exist in that stream even if the default arguments say otherwise.
|
// later blocks cannot exist in that stream even if the default arguments say otherwise.
|
||||||
if (!hasBiota) { hasMoons = false; hasWeather = false; hasStorms = false; hasVolcanoes = false; }
|
if (!hasBiota) { hasMoons = false; hasWeather = false; hasStorms = false; hasVolcanoes = false; }
|
||||||
@ -583,6 +611,7 @@ bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool
|
|||||||
if (!hasEcoregions) hasSettlements = false; // settlements follow the ecoregion block
|
if (!hasEcoregions) hasSettlements = false; // settlements follow the ecoregion block
|
||||||
if (!hasSettlements) hasConflict = false; // conflict follows the settlement block
|
if (!hasSettlements) hasConflict = false; // conflict follows the settlement block
|
||||||
if (!hasConflict) hasDiplo = false; // diplomacy follows the conflict block
|
if (!hasConflict) hasDiplo = false; // diplomacy follows the conflict block
|
||||||
|
if (!hasDiplo) hasCultures = false; // culture block follows the diplomacy block
|
||||||
|
|
||||||
// Read the length-prefixed key=value config block (see writeState). A default
|
// 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
|
// PlanetConfig is parsed over, so fields absent from an older save keep their
|
||||||
@ -833,6 +862,39 @@ bool Planet::readStateImpl(std::istream& is, bool hasBiome, bool hasBiota, bool
|
|||||||
}
|
}
|
||||||
if (!sanitizeDiplo(diplomacy, (int)settlements.size())) return false;
|
if (!sanitizeDiplo(diplomacy, (int)settlements.size())) return false;
|
||||||
}
|
}
|
||||||
|
// v23: cultural evolution (civ Step 8). Culture identities + the per-settlement culture (pre-v23
|
||||||
|
// saves keep none -> re-seeded one-per-continent on the next computeCultures()).
|
||||||
|
if (hasCultures) {
|
||||||
|
uint64_t ncu = 0; readPod(is, ncu);
|
||||||
|
if (!is || ncu > 100000) return false;
|
||||||
|
cultures.resize((size_t)ncu);
|
||||||
|
uint32_t maxId = 0;
|
||||||
|
for (Culture& cu : cultures) {
|
||||||
|
readPod(is, cu.id); readPod(is, cu.regionId); readPod(is, cu.bank);
|
||||||
|
uint8_t e = 0, f = 0; readPod(is, e); readPod(is, f);
|
||||||
|
readPod(is, cu.parentId); readPod(is, cu.foundedYear);
|
||||||
|
if (!is) return false;
|
||||||
|
if (e > (uint8_t)CultureEthos::Warlike || f > (uint8_t)Faith::Harvest) return false;
|
||||||
|
cu.ethos = (CultureEthos)e; cu.faith = (Faith)f;
|
||||||
|
if (cu.parentId < -1 || cu.foundedYear < -1) return false;
|
||||||
|
uint64_t L = 0; readPod(is, L);
|
||||||
|
if (!is || L > 256) return false;
|
||||||
|
cu.name.resize((size_t)L);
|
||||||
|
if (L) is.read(cu.name.data(), (std::streamsize)L);
|
||||||
|
readPod(is, L);
|
||||||
|
if (!is || L > 256) return false;
|
||||||
|
cu.faithName.resize((size_t)L);
|
||||||
|
if (L) is.read(cu.faithName.data(), (std::streamsize)L);
|
||||||
|
if (!is) return false;
|
||||||
|
if (cu.id > maxId) maxId = cu.id;
|
||||||
|
}
|
||||||
|
if (!readVec(is, sSettleCulture, 1000000)) return false;
|
||||||
|
if (sSettleCulture.size() != settlements.size()) sSettleCulture.assign(settlements.size(), -1);
|
||||||
|
for (int& c : sSettleCulture) if (c < -1 || c >= (int)cultures.size()) c = -1;
|
||||||
|
readPod(is, sCultureNextId);
|
||||||
|
if (!is) return false;
|
||||||
|
if (sCultureNextId <= maxId) sCultureNextId = maxId + 1; // stale/zero counter -> recompute
|
||||||
|
}
|
||||||
computeBiotaDensity(); // derived density scalars for the colour views
|
computeBiotaDensity(); // derived density scalars for the colour views
|
||||||
return (bool)is;
|
return (bool)is;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,17 @@ void Planet::computeTerritory() {
|
|||||||
});
|
});
|
||||||
std::vector<int> capitalOf(settlements.size(), -1);
|
std::vector<int> capitalOf(settlements.size(), -1);
|
||||||
const bool haveAllegiance = (sSettleAllegiance.size() == settlements.size());
|
const bool haveAllegiance = (sSettleAllegiance.size() == settlements.size());
|
||||||
|
// Realms stay mono-cultural (civ Step 4): a settlement only vassalizes to a capital of its own
|
||||||
|
// CULTURE (Step 8 -- so conversions/schisms redraw realms). When either side has no culture yet
|
||||||
|
// (before the first computeCultures(), or a -1 entry) fall back to the old same-continent rule.
|
||||||
|
const bool haveCulture = (sSettleCulture.size() == settlements.size());
|
||||||
|
auto sameCulture = [&](int a, int b) {
|
||||||
|
if (haveCulture) {
|
||||||
|
int ca = sSettleCulture[a], cb = sSettleCulture[b];
|
||||||
|
if (ca >= 0 && cb >= 0) return ca == cb;
|
||||||
|
}
|
||||||
|
return settlements[a].regionId == settlements[b].regionId;
|
||||||
|
};
|
||||||
for (int s : order) {
|
for (int s : order) {
|
||||||
if (range[s] <= 0.0) continue;
|
if (range[s] <= 0.0) continue;
|
||||||
// Conquest override (civ Step 5): a conquered settlement joins its overlord's realm, crossing
|
// Conquest override (civ Step 5): a conquered settlement joins its overlord's realm, crossing
|
||||||
@ -63,7 +74,7 @@ void Planet::computeTerritory() {
|
|||||||
for (int c : order) {
|
for (int c : order) {
|
||||||
if (c == s) break; // order is descending -> rest are smaller
|
if (c == s) break; // order is descending -> rest are smaller
|
||||||
if (capitalOf[c] != c) continue; // candidate must itself be a capital
|
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)
|
if (!sameCulture(c, s)) continue; // realms stay mono-cultural
|
||||||
double d = ang(s, c);
|
double d = ang(s, c);
|
||||||
if (d < cfg.civVassalRange * range[c] && d < joinAng) { joinAng = d; joinCap = c; }
|
if (d < cfg.civVassalRange * range[c] && d < joinAng) { joinAng = d; joinCap = c; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,6 +130,13 @@ struct WeatherSnapshot {
|
|||||||
uint32_t warRng = 0, warNextId = 0;
|
uint32_t warRng = 0, warNextId = 0;
|
||||||
// Civilization Step 6 (diplomacy): standing realm relations (alliances / rivalries / truces).
|
// Civilization Step 6 (diplomacy): standing realm relations (alliances / rivalries / truces).
|
||||||
std::vector<DiploTie> diplomacy;
|
std::vector<DiploTie> diplomacy;
|
||||||
|
// Civilization Step 8 (cultural evolution): per-settlement culture (mutable via conversion /
|
||||||
|
// assimilation) + the culture-list LENGTH. Culture records are append-only and immutable after
|
||||||
|
// creation, so a step back TRUNCATES the list (like colonies truncate settlements) and a
|
||||||
|
// deterministic replay re-creates any schism child identically -- no string-bearing records here
|
||||||
|
// (the viewer's persisted history frames are written as raw POD).
|
||||||
|
std::vector<int> settlementCulture;
|
||||||
|
uint32_t cultureCount = 0, cultureNextId = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Plate {
|
struct Plate {
|
||||||
@ -500,4 +507,16 @@ struct PlanetConfig {
|
|||||||
double civColonySpacing = 0.05; // rad: a colony must be at least this far from every settlement
|
double civColonySpacing = 0.05; // rad: a colony must be at least this far from every settlement
|
||||||
int civMaxColonies = 120; // cap on colonies founded beyond the initial civMaxSettlements
|
int civMaxColonies = 120; // cap on colonies founded beyond the initial civMaxSettlements
|
||||||
double civColonySupply = 2.0; // prosperity from the overlord supply link (keeps colonies alive)
|
double civColonySupply = 2.0; // prosperity from the overlord supply link (keeps colonies alive)
|
||||||
|
// Civilization Step 8: cultural evolution (stateful, saved v23). Cultures spread along borders
|
||||||
|
// (settlements convert under dominant foreign cultural pressure), conquered settlements assimilate
|
||||||
|
// into their ruler's culture, and far-flung cultures (overseas colonies) schism into new peoples.
|
||||||
|
double cultAssimRate = 0.03; // per-year chance a settlement under foreign allegiance adopts its ruler's culture
|
||||||
|
double cultConvertRate = 0.02; // per-year chance scale for border conversion under dominant foreign pressure
|
||||||
|
double cultConvertDominance = 2.5; // foreign cultural pressure must exceed this x the own-culture support
|
||||||
|
double cultSpreadRange = 0.25; // rad: how far a settlement projects cultural pressure (~1600 km)
|
||||||
|
double cultPrestigeWeight = 0.5; // how much trade prosperity boosts a settlement's cultural weight
|
||||||
|
int cultSchismMinMembers = 6; // a culture needs at least this many living settlements to schism
|
||||||
|
double cultSchismRange = 0.55; // rad from the culture's population centroid past which members are "distant"
|
||||||
|
int cultSchismMinCluster = 2; // distant settlements needed to break away together
|
||||||
|
double cultSchismRate = 0.08; // per-year chance a qualifying distant cluster becomes a new people
|
||||||
};
|
};
|
||||||
|
|||||||
@ -36,6 +36,10 @@ WeatherSnapshot Planet::captureWeather() const {
|
|||||||
s.settlementAllegiance = sSettleAllegiance; // civ Step 5: conquest state
|
s.settlementAllegiance = sSettleAllegiance; // civ Step 5: conquest state
|
||||||
s.wars = wars; s.warRng = sWarRng; s.warNextId = sWarNextId;
|
s.wars = wars; s.warRng = sWarRng; s.warNextId = sWarNextId;
|
||||||
s.diplomacy = diplomacy; // civ Step 6: standing realm relations
|
s.diplomacy = diplomacy; // civ Step 6: standing realm relations
|
||||||
|
// Civ Step 8: per-settlement culture + the culture-list length. Records are append-only +
|
||||||
|
// immutable, so the length is enough -- a restore truncates and replay re-creates schism children.
|
||||||
|
s.settlementCulture = sSettleCulture;
|
||||||
|
s.cultureCount = (uint32_t)cultures.size(); s.cultureNextId = sCultureNextId;
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +62,13 @@ void Planet::restoreWeather(const WeatherSnapshot& s) {
|
|||||||
if (sSettleAllegiance.size() != settlements.size()) sSettleAllegiance.assign(settlements.size(), -1);
|
if (sSettleAllegiance.size() != settlements.size()) sSettleAllegiance.assign(settlements.size(), -1);
|
||||||
wars = s.wars; sWarRng = s.warRng ? s.warRng : sWarRng; sWarNextId = s.warNextId ? s.warNextId : sWarNextId;
|
wars = s.wars; sWarRng = s.warRng ? s.warRng : sWarRng; sWarNextId = s.warNextId ? s.warNextId : sWarNextId;
|
||||||
diplomacy = s.diplomacy; // civ Step 6: restore realm relations
|
diplomacy = s.diplomacy; // civ Step 6: restore realm relations
|
||||||
|
// Civ Step 8: truncate schism children born after the frame (append-only list) + restore the
|
||||||
|
// per-settlement culture; replay re-creates the same children (stepCulture is a pure function).
|
||||||
|
if (s.cultureCount <= cultures.size()) cultures.resize(s.cultureCount);
|
||||||
|
sSettleCulture = s.settlementCulture;
|
||||||
|
if (sSettleCulture.size() != settlements.size()) sSettleCulture.assign(settlements.size(), -1);
|
||||||
|
for (int& c : sSettleCulture) if (c < -1 || c >= (int)cultures.size()) c = -1;
|
||||||
|
sCultureNextId = s.cultureNextId ? s.cultureNextId : sCultureNextId;
|
||||||
sHasWeather = !sHumidity.empty();
|
sHasWeather = !sHumidity.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
315
test_cultevo.cpp
Normal file
315
test_cultevo.cpp
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
// Headless test for civilization Step 8 (cultural evolution). No display needed.
|
||||||
|
//
|
||||||
|
// g++ -std=c++17 -O2 -Isrc/sim test_cultevo.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/PlanetConflict.cpp
|
||||||
|
// src/sim/PlanetTrade.cpp src/sim/PlanetIO.cpp -o /tmp/tcev && /tmp/tcev
|
||||||
|
//
|
||||||
|
// Verifies: seeding parity (one culture per continent, frozen identities); colonies inherit the
|
||||||
|
// founder's culture; assimilation under foreign allegiance; border conversion (capitals exempt);
|
||||||
|
// schism (a distant cluster becomes a new people); determinism + RNG isolation; snapshot
|
||||||
|
// truncate-and-replay; save-v23 round-trip + corrupt-stream rejection; pre-v23 compatibility.
|
||||||
|
|
||||||
|
#include "Planet.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <set>
|
||||||
|
#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();
|
||||||
|
}
|
||||||
|
static void quietCulture(Planet& p) { // no evolution: state should then never change
|
||||||
|
p.cfg.cultAssimRate = 0.0; p.cfg.cultConvertRate = 0.0; p.cfg.cultSchismRate = 0.0;
|
||||||
|
}
|
||||||
|
static bool culturesEqual(const std::vector<Culture>& a, const std::vector<Culture>& b) {
|
||||||
|
if (a.size() != b.size()) return false;
|
||||||
|
for (size_t i = 0; i < a.size(); ++i)
|
||||||
|
if (a[i].id != b[i].id || a[i].regionId != b[i].regionId || a[i].bank != b[i].bank
|
||||||
|
|| a[i].ethos != b[i].ethos || a[i].faith != b[i].faith
|
||||||
|
|| a[i].parentId != b[i].parentId || a[i].foundedYear != b[i].foundedYear
|
||||||
|
|| a[i].name != b[i].name || a[i].faithName != b[i].faithName) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// The shared yearly civ sequence (mirrors the viewer's year tick, minus weather).
|
||||||
|
static std::vector<WarEvent> civYear(Planet& p, long yr, bool wars = false, bool colonize = false) {
|
||||||
|
p.computeTerritory(); p.computeCultures();
|
||||||
|
if (wars) p.stepConflict(yr);
|
||||||
|
std::vector<WarEvent> ev = p.stepCulture(yr);
|
||||||
|
if (colonize) for (WarEvent& e : p.stepColonization(yr)) ev.push_back(e);
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: seeding parity (frozen one-per-continent identities)\n");
|
||||||
|
p.placeSettlements();
|
||||||
|
double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
|
||||||
|
p.computeTerritory(); p.computeCultures();
|
||||||
|
{
|
||||||
|
check(!p.cultureList().empty(), "cultures seeded at the dawn");
|
||||||
|
// One culture per distinct key (continent regionId / -1-bank fallback) among living settlements.
|
||||||
|
std::set<int> keys;
|
||||||
|
for (size_t k = 0; k < p.settlements.size(); ++k) {
|
||||||
|
const Settlement& s = p.settlements[k];
|
||||||
|
if (s.population < p.cfg.civAbandonPop) continue;
|
||||||
|
keys.insert(s.regionId >= 0 ? s.regionId : (-1 - s.bank));
|
||||||
|
}
|
||||||
|
check(p.cultureList().size() == keys.size(), "one culture per inhabited continent");
|
||||||
|
bool rootsOk = true, idsOk = true;
|
||||||
|
for (size_t i = 0; i < p.cultureList().size(); ++i) {
|
||||||
|
const Culture& cu = p.cultureList()[i];
|
||||||
|
if (cu.parentId != -1 || cu.foundedYear != -1 || cu.name.empty() || cu.faithName.empty()) rootsOk = false;
|
||||||
|
if (cu.id != (uint32_t)i + 1) idsOk = false;
|
||||||
|
}
|
||||||
|
check(rootsOk, "all seeded cultures are roots (no parent, present since the dawn)");
|
||||||
|
check(idsOk, "culture ids are dense from 1");
|
||||||
|
// A quiet world (all rates 0) never mutates culture state.
|
||||||
|
Planet q0 = p; quietCulture(q0);
|
||||||
|
std::vector<int> sc0 = q0.settleCulture();
|
||||||
|
for (long yr = 0; yr < 30; ++yr) civYear(q0, yr);
|
||||||
|
check(q0.settleCulture() == sc0 && q0.cultureList().size() == p.cultureList().size(),
|
||||||
|
"no evolution with all cult* rates 0 (Steps 3-7 behaviour preserved)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: colonies inherit the founder's culture\n");
|
||||||
|
{
|
||||||
|
Planet c = p; quietCulture(c);
|
||||||
|
c.computeHabitability();
|
||||||
|
c.cfg.civColonizeRate = 1.0; c.cfg.civColonyMinPop = 1000.0; c.cfg.civColonyMinHab = 0.1;
|
||||||
|
size_t before = c.settlements.size();
|
||||||
|
for (long yr = 0; yr < 40 && c.settlements.size() < before + 6; ++yr) civYear(c, yr, false, true);
|
||||||
|
std::printf(" %zu colonies founded\n", c.settlements.size() - before);
|
||||||
|
check(c.settlements.size() > before, "colonies are founded");
|
||||||
|
bool inherit = c.settlements.size() > before;
|
||||||
|
for (size_t k = before; k < c.settlements.size(); ++k) {
|
||||||
|
int ov = c.settleAllegiance()[k];
|
||||||
|
if (ov < 0 || c.settleCulture()[k] != c.settleCulture()[ov]) inherit = false;
|
||||||
|
}
|
||||||
|
check(inherit, "every colony carries its founder capital's culture");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: assimilation under foreign rule\n");
|
||||||
|
{
|
||||||
|
Planet a = p; quietCulture(a); a.cfg.cultAssimRate = 1.0;
|
||||||
|
// Force a conquest scenario: a living settlement held by a living foreign-culture capital
|
||||||
|
// (allegiance + culture are snapshot fields, so a hacked snapshot injects the state cleanly).
|
||||||
|
int s = -1, ov = -1;
|
||||||
|
const auto& sc = a.settleCulture();
|
||||||
|
for (size_t i = 0; i < a.settlements.size() && s < 0; ++i)
|
||||||
|
for (size_t j = 0; j < a.settlements.size(); ++j)
|
||||||
|
if (i != j && a.settlements[i].population >= a.cfg.civAbandonPop
|
||||||
|
&& a.settlements[j].population >= a.cfg.civAbandonPop
|
||||||
|
&& sc[i] >= 0 && sc[j] >= 0 && sc[i] != sc[j]) { s = (int)i; ov = (int)j; break; }
|
||||||
|
if (s < 0) { std::printf(" (single-culture world: skipping)\n"); }
|
||||||
|
else {
|
||||||
|
WeatherSnapshot snap = a.captureWeather();
|
||||||
|
if (snap.settlementAllegiance.size() != a.settlements.size()) // no war has sized it yet
|
||||||
|
snap.settlementAllegiance.assign(a.settlements.size(), -1);
|
||||||
|
snap.settlementAllegiance[s] = ov;
|
||||||
|
a.restoreWeather(snap);
|
||||||
|
int want = a.settleCulture()[ov];
|
||||||
|
std::vector<WarEvent> ev = a.stepCulture(1);
|
||||||
|
bool flipped = a.settleCulture()[s] == want;
|
||||||
|
bool logged = false;
|
||||||
|
for (const WarEvent& e : ev)
|
||||||
|
if (e.kind == 7 && e.title.find("adopts the culture of") != std::string::npos) logged = true;
|
||||||
|
check(flipped, "a conquered settlement assimilates into its ruler's culture");
|
||||||
|
check(logged, "assimilation emits a kind-7 event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: border conversion (capitals exempt)\n");
|
||||||
|
{
|
||||||
|
Planet b = p; quietCulture(b);
|
||||||
|
b.cfg.cultConvertRate = 1.0; b.cfg.cultConvertDominance = 0.01; b.cfg.cultSpreadRange = 3.0;
|
||||||
|
b.computeTerritory(); b.computeCultures();
|
||||||
|
std::vector<int> caps;
|
||||||
|
for (const Nation& nat : b.nationList()) caps.push_back(nat.capital);
|
||||||
|
std::vector<int> capCultBefore;
|
||||||
|
for (int c : caps) capCultBefore.push_back(b.settleCulture()[c]);
|
||||||
|
int conversions = 0;
|
||||||
|
for (long yr = 0; yr < 5; ++yr)
|
||||||
|
for (const WarEvent& e : civYear(b, yr))
|
||||||
|
if (e.kind == 7 && e.title.find("embraces the ways of") != std::string::npos) ++conversions;
|
||||||
|
std::printf(" %d conversions in 5 years\n", conversions);
|
||||||
|
check(conversions > 0, "settlements convert under dominant foreign cultural pressure");
|
||||||
|
bool capsStable = true; // capitals anchor identity: only assimilation/schism may move them (both off/limited here)
|
||||||
|
for (size_t i = 0; i < caps.size(); ++i)
|
||||||
|
if (b.settleCulture()[caps[i]] != capCultBefore[i]) capsStable = false;
|
||||||
|
check(capsStable, "realm capitals never convert via border pressure");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: schism (a distant cluster becomes a new people)\n");
|
||||||
|
long schismYear = -1;
|
||||||
|
{
|
||||||
|
quietCulture(p);
|
||||||
|
p.cfg.cultSchismRate = 1.0; p.cfg.cultSchismRange = 0.10; p.cfg.cultSchismMinMembers = 3;
|
||||||
|
p.cfg.cultSchismMinCluster = 1;
|
||||||
|
size_t before = p.cultureList().size();
|
||||||
|
for (long yr = 0; yr < 20 && p.cultureList().size() == before; ++yr) {
|
||||||
|
for (const WarEvent& e : civYear(p, yr))
|
||||||
|
if (e.kind == 7 && e.title.find("break away from") != std::string::npos) schismYear = yr;
|
||||||
|
}
|
||||||
|
check(p.cultureList().size() > before, "a schism appends a new culture");
|
||||||
|
if (p.cultureList().size() > before) {
|
||||||
|
const Culture& child = p.cultureList().back();
|
||||||
|
bool parentOk = false;
|
||||||
|
for (size_t i = 0; i < before; ++i) if ((int)p.cultureList()[i].id == child.parentId) parentOk = true;
|
||||||
|
check(parentOk, "the child records its parent culture");
|
||||||
|
check(child.foundedYear >= 0 && child.foundedYear == schismYear, "the child records its founding year");
|
||||||
|
bool nameUnique = !child.name.empty();
|
||||||
|
for (size_t i = 0; i + 1 < p.cultureList().size(); ++i)
|
||||||
|
if (p.cultureList()[i].name == child.name) nameUnique = false;
|
||||||
|
check(nameUnique, "the child's people name is unique");
|
||||||
|
int adopted = 0;
|
||||||
|
for (int c : p.settleCulture()) if (c == (int)p.cultureList().size() - 1) ++adopted;
|
||||||
|
check(adopted >= 1, "the breakaway cluster adopted the new culture");
|
||||||
|
p.computeTerritory(); p.computeCultures(); // the child cluster may found its own realm
|
||||||
|
check((int)p.cultureList().back().members == adopted, "derived tallies pick the child up");
|
||||||
|
}
|
||||||
|
check(p.cultureList().size() <= 64, "the culture list stays bounded");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: 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();
|
||||||
|
quietCulture(q);
|
||||||
|
q.cfg.cultSchismRate = 1.0; q.cfg.cultSchismRange = 0.10; q.cfg.cultSchismMinMembers = 3;
|
||||||
|
q.cfg.cultSchismMinCluster = 1;
|
||||||
|
size_t before = q.cultureList().size();
|
||||||
|
for (long yr = 0; yr < 20 && q.cultureList().size() == before; ++yr) civYear(q, yr);
|
||||||
|
check(culturesEqual(q.cultureList(), p.cultureList()) && q.settleCulture() == p.settleCulture(),
|
||||||
|
"two identical worlds evolve identical cultures");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: 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.cfg.cultConvertRate = 1.0; y.cfg.cultConvertDominance = 0.01; y.cfg.cultSpreadRange = 3.0;
|
||||||
|
for (long yr = 0; yr < 3; ++yr) civYear(y, yr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool same = true;
|
||||||
|
for (int i = 0; i < n; ++i) if (std::fabs(x.cells[i].elevation - y.cells[i].elevation) > 1e-9) same = false;
|
||||||
|
check(same, "stepCulture never perturbs tectonic evolution");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Interleaved no-op stepCulture calls must not disturb the war RNG stream.
|
||||||
|
Planet w1 = p, w2 = p; quietCulture(w1); quietCulture(w2);
|
||||||
|
w1.cfg.warDeclareRate = 2.0; w1.cfg.warMinRealmPop = 1.0;
|
||||||
|
w2.cfg.warDeclareRate = 2.0; w2.cfg.warMinRealmPop = 1.0;
|
||||||
|
for (long yr = 100; yr < 130; ++yr) {
|
||||||
|
w1.computeTerritory(); w1.computeCultures(); w1.stepConflict(yr);
|
||||||
|
w2.computeTerritory(); w2.computeCultures(); w2.stepConflict(yr); w2.stepCulture(yr);
|
||||||
|
}
|
||||||
|
bool warsSame = w1.warList().size() == w2.warList().size()
|
||||||
|
&& w1.settleAllegiance() == w2.settleAllegiance();
|
||||||
|
check(warsSame, "stepCulture uses no RNG (interleaving leaves wars identical)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: snapshot truncate-and-replay\n");
|
||||||
|
{
|
||||||
|
WeatherSnapshot snap = p.captureWeather();
|
||||||
|
std::vector<Culture> cBefore = p.cultureList();
|
||||||
|
std::vector<int> scBefore = p.settleCulture();
|
||||||
|
// Mutate: aggressive conversion + schism for 15 years.
|
||||||
|
p.cfg.cultConvertRate = 1.0; p.cfg.cultConvertDominance = 0.01; p.cfg.cultSpreadRange = 3.0;
|
||||||
|
p.cfg.cultSchismRate = 1.0; p.cfg.cultSchismRange = 0.10;
|
||||||
|
for (long yr = 50; yr < 65; ++yr) civYear(p, yr);
|
||||||
|
std::vector<Culture> cAfter = p.cultureList();
|
||||||
|
std::vector<int> scAfter = p.settleCulture();
|
||||||
|
bool changed = !culturesEqual(cAfter, cBefore) || scAfter != scBefore;
|
||||||
|
p.restoreWeather(snap);
|
||||||
|
p.computeTerritory(); p.computeCultures(); // the derived refresh after a step-back
|
||||||
|
check(changed, "the mutation actually changed the culture state");
|
||||||
|
check(p.cultureList().size() == cBefore.size() && p.settleCulture() == scBefore,
|
||||||
|
"restoreWeather truncates schism children + rewinds settlement cultures");
|
||||||
|
// Deterministic replay re-creates the same children + conversions.
|
||||||
|
for (long yr = 50; yr < 65; ++yr) civYear(p, yr);
|
||||||
|
check(culturesEqual(p.cultureList(), cAfter) && p.settleCulture() == scAfter,
|
||||||
|
"replaying the same years re-creates identical schisms/conversions");
|
||||||
|
// Out-of-range snapshot entries are clamped on restore.
|
||||||
|
WeatherSnapshot odd = p.captureWeather();
|
||||||
|
if (!odd.settlementCulture.empty()) {
|
||||||
|
odd.settlementCulture[0] = 9999;
|
||||||
|
p.restoreWeather(odd);
|
||||||
|
check(p.settleCulture()[0] == -1, "restoreWeather clamps out-of-range culture entries");
|
||||||
|
odd.settlementCulture[0] = -1; // leave p in a sane (clamped) state for the save tests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: save v23 round-trip\n");
|
||||||
|
p.computeTerritory(); p.computeCultures();
|
||||||
|
{
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
p.writeState(ss);
|
||||||
|
Planet r;
|
||||||
|
bool ok = r.readState(ss);
|
||||||
|
check(ok, "readState accepts the v23 stream");
|
||||||
|
check(culturesEqual(r.cultureList(), p.cultureList()), "culture identities survive save/load");
|
||||||
|
check(r.settleCulture() == p.settleCulture(), "per-settlement culture survives save/load");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Planet bad = p;
|
||||||
|
if (!bad.cultures.empty()) {
|
||||||
|
bad.cultures[0].ethos = (CultureEthos)200;
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
bad.writeState(ss);
|
||||||
|
Planet r;
|
||||||
|
check(!r.readState(ss), "readState rejects an out-of-range culture ethos");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Cultural evolution: pre-v23 compatibility\n");
|
||||||
|
{
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
p.writeState(ss);
|
||||||
|
Planet r;
|
||||||
|
// The culture block is the last one; reading with hasCultures=false ignores the trailing bytes.
|
||||||
|
bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true,
|
||||||
|
true, true, false);
|
||||||
|
check(ok, "readState accepts the stream as pre-v23");
|
||||||
|
check(r.cultureList().empty(), "pre-v23 load has no culture state");
|
||||||
|
r.computeTerritory(); r.computeCultures();
|
||||||
|
check(!r.cultureList().empty(), "cultures re-seed on the next refresh");
|
||||||
|
bool allRoots = true;
|
||||||
|
for (const Culture& cu : r.cultureList())
|
||||||
|
if (cu.parentId != -1 || cu.foundedYear != -1) allRoots = false;
|
||||||
|
check(allRoots, "re-seeded cultures are one-per-continent roots");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL CULTURAL-EVOLUTION CHECKS PASSED\n", failures);
|
||||||
|
return failures ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user