Civ Step 6: diplomacy, alliances & coalitions (save v22)
Wars stop being isolated 1-v-1 grudges. Each pair of nearby realms carries an
attitude that drifts over time, crystallising into alliances / non-aggression
pacts / rivalries. Allies don't fight and join each other's wars (coalitions),
and wars end in real peace treaties. Extends the Step-5 conflict subsystem
in place (same stepConflict tick, same war RNG), saved v22 + snapshotted.
- PlanetTypes.hpp: DiploKind + DiploTie {a,b capital indices, attitude, truceUntil,
kind}; diplo* config knobs; WeatherSnapshot carries diplomacy.
- PlanetConflict.cpp: a diplomacy pass in stepConflict (revolts -> prosecute ->
diplomacy -> declare). Attitude drifts from culture/faith affinity + a war
penalty + truce recovery + per-pair noise; reclassified by threshold with
hysteresis (kind-6 events on change). War-declare skips allied/non-aggression/
truced pairs; hostility now rises as attitude falls (rivals fight); a defender's
allies join by declaring their own war on the aggressor; a war end sets a truce
+ grudge. diploBetween/realmsAllied helpers.
- Save v22 + step-back: allegiance/wars block joined by a diplomacy block (new
hasDiplo readState param + per-frame history block); captureWeather/restoreWeather
carry the ties.
- Render: green alliance / dark-red rivalry great-circle arcs over the Territory
view (P), per-realm ally/rival counts + active-wars list in the Realms tab, a
cell-info allies/rivals line, kind-6 events (= icon).
- test_diplomacy.cpp: alliances/rivalries form, allies never war, coalitions form,
truces after peace, determinism, RNG isolation, save-v22 + snapshot round-trip.
All 14 suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cc6813e211
commit
8cd487a5dc
17
BUILD.md
17
BUILD.md
@ -57,7 +57,7 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
|||||||
E ecoregions colour view (names ecological provinces on first use; Eco tab)
|
E ecoregions colour view (names ecological provinces on first use; Eco tab)
|
||||||
I habitability heat map (where civilization can thrive)
|
I habitability heat map (where civilization can thrive)
|
||||||
U settlements: the dawn of civilization on first press, then toggle markers (Civ tab)
|
U settlements: the dawn of civilization on first press, then toggle markers (Civ tab)
|
||||||
P territory / realms view + political borders + war fronts (Realms tab lists nations & wars)
|
P territory / realms view + borders + war fronts + alliance/rivalry arcs (Realms tab)
|
||||||
X culture / faiths view + cultural borders (peoples/ethos/religion in the Cultures tab)
|
X culture / faiths view + cultural borders (peoples/ethos/religion in the Cultures tab)
|
||||||
Y follow-cam: cycle the 3D camera through active storms (Live World; off after last)
|
Y follow-cam: cycle the 3D camera through active storms (Live World; off after last)
|
||||||
. / , step the live clock forward / back by one rate-unit (auto-pauses; back also
|
. / , step the live clock forward / back by one rate-unit (auto-pauses; back also
|
||||||
@ -342,6 +342,19 @@ rise and fall. Runs on the yearly Live-World tick.
|
|||||||
warRevoltRate 0.04 per-year base revolt chance of a held foreign/distant city
|
warRevoltRate 0.04 per-year base revolt chance of a held foreign/distant city
|
||||||
warMinRealmPop 2000 realms below this population don't start wars
|
warMinRealmPop 2000 realms below this population don't start wars
|
||||||
|
|
||||||
|
Diplomacy (PlanetConfig, save v22 — on the Territory view P): realm-pair attitudes drift into alliances /
|
||||||
|
rivalries; allies never fight + join each other's wars (coalitions); wars end in truces. Runs in the same
|
||||||
|
yearly stepConflict tick; green arcs = alliances, dark-red arcs = rivalries.
|
||||||
|
|
||||||
|
diploDriftRate 0.06 per-year attitude change scale
|
||||||
|
diploAffinity 1.0 attitude pull from shared culture + faith (vs difference)
|
||||||
|
diploWarPenalty 0.5 extra per-year attitude drop while two realms are at war
|
||||||
|
diploTruceYears 12 post-war truce length (no new war between the two)
|
||||||
|
diploWarGrudge 0.4 one-off attitude drop when a war between them ends (a scar)
|
||||||
|
diploAllyThreshold 0.5 attitude at/above which two realms are allied
|
||||||
|
diploNonAggThreshold 0.2 attitude at/above which they sign a non-aggression pact
|
||||||
|
diploRivalThreshold -0.5 attitude at/below which they become rivals
|
||||||
|
|
||||||
Culture, beliefs & governments (key X — derived from settlements + geography, not saved): settlements
|
Culture, beliefs & governments (key X — derived from settlements + geography, not saved): settlements
|
||||||
on a continent share a culture (a people with an environment-driven ethos + a religion); each realm
|
on a continent share a culture (a people with an environment-driven ethos + a religion); each realm
|
||||||
gets a government type folded into its name (Republic of X / Duchy of X / X Theocracy / ...); realms
|
gets a government type folded into its name (Republic of X / Duchy of X / X Theocracy / ...); realms
|
||||||
@ -362,7 +375,7 @@ src/sim/PlanetCulture.cpp (computeCultures).
|
|||||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
|
|
||||||
# Same source list for every suite: swap test_logic.cpp -> test_biota / test_live / test_ocean /
|
# Same source list for every suite: swap test_logic.cpp -> test_biota / test_live / test_ocean /
|
||||||
# test_weather / test_volcano / test_geography / test_ecoregions / test_civ / test_nation / test_culture / test_conflict.
|
# test_weather / test_volcano / test_geography / test_ecoregions / test_civ / test_nation / test_culture / test_conflict / test_diplomacy.
|
||||||
# The CMake build also includes test_events for the viewer event journal.
|
# The CMake build also includes test_events for the viewer event journal.
|
||||||
|
|
||||||
Verifies geometry, plate assignment, gradual non-saturating relief and
|
Verifies geometry, plate assignment, gradual non-saturating relief and
|
||||||
|
|||||||
48
CLAUDE.md
48
CLAUDE.md
@ -108,7 +108,7 @@ the fixed-grid Eulerian model + the climate fields are the groundwork for it.
|
|||||||
**Civilizations (in progress — the long arc after the world is finished):** the eventual goal is
|
**Civilizations (in progress — the long arc after the world is finished):** the eventual goal is
|
||||||
people who eat, name their world, found villages→cities, build kingdoms/empires, draw cultural +
|
people who eat, name their world, found villages→cities, build kingdoms/empires, draw cultural +
|
||||||
geographic borders, and go to war. Built in phases (cell = territory, settlements = point agents, all
|
geographic borders, and go to war. Built in phases (cell = territory, settlements = point agents, all
|
||||||
on the Live World clock). **Steps 1–5 of the roadmap are done (plus a derived ecoregions atlas):**
|
on the Live World clock). **Steps 1–6 of the roadmap are done (plus a derived ecoregions atlas):**
|
||||||
- **Geography & place-names (the atlas)** *(done — see `PlanetGeography.cpp` + `NameGen.cpp`)* — the
|
- **Geography & place-names (the atlas)** *(done — see `PlanetGeography.cpp` + `NameGen.cpp`)* — the
|
||||||
foundation everything civic references. `Planet::generateGeography()` extracts named features from
|
foundation everything civic references. `Planet::generateGeography()` extracts named features from
|
||||||
the (frozen) terrain by connectivity over the fixed grid — **continents/islands** (connected land),
|
the (frozen) terrain by connectivity over the fixed grid — **continents/islands** (connected land),
|
||||||
@ -202,8 +202,23 @@ on the Live World clock). **Steps 1–5 of the roadmap are done (plus a derived
|
|||||||
marker in the **Realms** tab (with an active-wars list) over the Territory view (`P`), a cell-info "AT
|
marker in the **Realms** tab (with an active-wars list) over the Territory view (`P`), a cell-info "AT
|
||||||
WAR" flag, an active-war HUD count, and **kind=5** `WorldEvent`s ("The X Empire declares war on the Y
|
WAR" flag, an active-war HUD count, and **kind=5** `WorldEvent`s ("The X Empire declares war on the Y
|
||||||
Kingdom", "The X captures/sacks Z", "Z revolts against the X", "Peace between…"). Knobs `war*`.
|
Kingdom", "The X captures/sacks Z", "Z revolts against the X", "Peace between…"). Knobs `war*`.
|
||||||
|
- **Diplomacy, alliances & coalitions (Step 6)** *(done — see `PlanetConflict.cpp`, save v22)* — wars stop
|
||||||
|
being isolated 1-v-1 grudges. Each pair of nearby realms carries an **attitude** (−1..+1) that **drifts**
|
||||||
|
each year in the diplomacy pass of `stepConflict` (shared vs different culture/faith affinity, plus a
|
||||||
|
penalty while at war, plus recovery under a truce, plus a per-pair personality noise), crystallising via
|
||||||
|
thresholds (hysteresis) into a **`DiploTie`** = Alliance / Non-aggression / Rival. **Allies never fight**
|
||||||
|
(war declaration skips allied/non-aggression/truced pairs) and **join each other's wars** — when A
|
||||||
|
attacks B, each ally of B declares its own war on A (a **coalition**, via the existing 1-v-1 machinery),
|
||||||
|
so an aggressor is worn down on many fronts. Hostility now rises as **attitude falls** (rivals war), and
|
||||||
|
a war ends in a real **peace treaty** (a `truceUntil` + a lasting attitude **grudge**). Ties are keyed on
|
||||||
|
**capital settlement index** (the same stable realm id wars use) and, like war, are **stateful** — saved
|
||||||
|
(**v22**) + snapshotted (step-back rewinds alliances/rivalries). Render: **alliance (green) / rivalry
|
||||||
|
(dark-red) arcs** between capitals over the Territory view (`P`), an allies/rivals summary per row + the
|
||||||
|
active-wars list in the **Realms** tab, a cell-info "allies/rivals" line, and **kind=6** `WorldEvent`s
|
||||||
|
("The X and the Y form an alliance", "…become rivals", "…sign a non-aggression pact", "Z joins the war
|
||||||
|
against W"). Knobs `diplo*`.
|
||||||
*Next steps (not yet built): cultural **evolution** (spread along borders, drift, assimilation, schism);
|
*Next steps (not yet built): cultural **evolution** (spread along borders, drift, assimilation, schism);
|
||||||
alliances / diplomacy / treaties; trade & economy.*
|
tribute / vassalage treaties; trade & economy.*
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
@ -615,7 +630,7 @@ src/
|
|||||||
PlanetCiv.* computeHabitability/placeSettlements/stepCivilization (settlements; civ Step 2)
|
PlanetCiv.* computeHabitability/placeSettlements/stepCivilization (settlements; 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 (cultures/ethos/faith per continent + governments; civ Step 4)
|
||||||
PlanetConflict.* stepConflict (wars, casualties, conquest/allegiance, revolts; civ Step 5, save v21)
|
PlanetConflict.* stepConflict (wars/conquest/revolts + diplomacy/alliances/coalitions; civ Steps 5-6, save v21/v22)
|
||||||
PlanetIO.cpp config file (text) + binary save/load
|
PlanetIO.cpp config file (text) + binary save/load
|
||||||
render/ (raylib viewer)
|
render/ (raylib viewer)
|
||||||
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
Colors.* cell color modes (elevation/plate/age/crust/biome/climate/biota)
|
||||||
@ -684,8 +699,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` or `test_conflict.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
`test_nation.cpp`, `test_culture.cpp`, `test_conflict.cpp` or `test_diplomacy.cpp` to run the Biota / Live World / Ocean / Weather / Volcano / Geography / Ecoregions /
|
||||||
Civilization / Nation / Culture / Conflict suites — same source list. CMake also builds `test_events` for the
|
Civilization / Nation / Culture / Conflict / Diplomacy 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
|
||||||
@ -751,8 +766,10 @@ sun, raise tides — `T` colours the coastline by the live tide level (amber low
|
|||||||
tectonic context on entry and run a stateful lifecycle — submarine ones can build into new
|
tectonic context on entry and run a stateful lifecycle — submarine ones can build into new
|
||||||
**volcanic islands**; `V` toggles the cone/eruption markers. With settlements placed (`U`) and the
|
**volcanic islands**; `V` toggles the cone/eruption markers. With settlements placed (`U`) and the
|
||||||
Territory view on (`P`), realms **wage war** each year — red war-fronts appear, cities change hands
|
Territory view on (`P`), realms **wage war** each year — red war-fronts appear, cities change hands
|
||||||
(borders move) or fall to ruins, empires fracture as provinces revolt; the **Realms** tab lists active
|
(borders move) or fall to ruins, empires fracture as provinces revolt; realms also form **alliances**
|
||||||
wars and the **Events** tab logs them. `Y` makes the 3D camera **follow a storm** (cycles by strength,
|
(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/
|
||||||
|
rivals/wars and the **Events** tab logs it all. `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** via snapshots). Mouse-wheel over the 2D map zooms (drag pans).
|
||||||
|
|
||||||
@ -765,7 +782,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 **21**; v2 adds the `[`/`]` drift rate, v3 a
|
save header is versioned (currently **22**; 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**
|
||||||
@ -777,9 +794,10 @@ appends the old pure-function **volcanoes** block, v15 replaces it with stateful
|
|||||||
agents plus volcano state in step-back frames, v16 appends the saved **event journal**, v17
|
agents plus volcano state in step-back frames, v16 appends the saved **event journal**, v17
|
||||||
appends the **geography/atlas** block — named features + per-cell region indices, v18 a geography
|
appends the **geography/atlas** block — named features + per-cell region indices, v18 a geography
|
||||||
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), and 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;
|
to the step-back frames so a load can rewind conquests, and v22 the **diplomacy** block — standing realm
|
||||||
|
**relations** (alliances / rivalries / truces), likewise per-frame;
|
||||||
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
|
||||||
@ -790,7 +808,7 @@ 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).
|
again as the clock runs); pre-v22 saves load with no diplomacy (relations re-form as the clock runs).
|
||||||
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
|
||||||
@ -963,6 +981,14 @@ triangles (plates are fixed in phase 1).
|
|||||||
distant city), `warMinRealmPop` (2000, realms below this don't start wars). Adjacency is by settlement
|
distant city), `warMinRealmPop` (2000, realms below this don't start wars). Adjacency is by settlement
|
||||||
proximity (`civTerritoryMax·1.5`); the red war-front/marker colours are render constants. War runs on
|
proximity (`civTerritoryMax·1.5`); the red war-front/marker colours are render constants. War runs on
|
||||||
the yearly tick and is **saved** (allegiance + wars + war RNG) — tune for a warlike or peaceable world.
|
the yearly tick and is **saved** (allegiance + wars + war RNG) — tune for a warlike or peaceable world.
|
||||||
|
- **Diplomacy (`diplo*` in PlanetConfig / `planet.cfg`; civ Step 6, save v22):** realm-pair attitude drift
|
||||||
|
→ alliances / rivalries / coalitions. `diploDriftRate` (0.06, how fast attitudes move), `diploAffinity`
|
||||||
|
(1.0, pull from shared culture + faith vs difference), `diploWarPenalty` (0.5, extra attitude drop while
|
||||||
|
at war), `diploTruceYears` (12, post-war truce length), `diploWarGrudge` (0.4, one-off attitude drop when
|
||||||
|
a war ends), and the kind thresholds `diploAllyThreshold` (0.5) / `diploNonAggThreshold` (0.2) /
|
||||||
|
`diploRivalThreshold` (−0.5). Ally/rival arc colours are render constants (ViewerRender/Overlays). Runs
|
||||||
|
in the same yearly `stepConflict` tick; raise `diploDriftRate`/`diploAffinity` for a more alliance-heavy
|
||||||
|
world, lower the thresholds' spread for touchier politics.
|
||||||
- `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
|
||||||
|
|||||||
@ -77,7 +77,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)
|
foreach(test_name logic biota ocean live weather volcano geography ecoregions civ nation culture conflict diplomacy)
|
||||||
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})
|
||||||
|
|||||||
@ -55,8 +55,8 @@ include path, so includes stay flat (`#include "Planet.hpp"`, `"Viewer.hpp"`).
|
|||||||
- `PlanetNation.{hpp,cpp}` — `computeTerritory` (civ Step 3: realms + per-cell ownership; derived, not saved).
|
- `PlanetNation.{hpp,cpp}` — `computeTerritory` (civ Step 3: realms + per-cell ownership; derived, not saved).
|
||||||
- `PlanetCulture.{hpp,cpp}` — `computeCultures` (civ Step 4: cultures/ethos/faith per continent +
|
- `PlanetCulture.{hpp,cpp}` — `computeCultures` (civ Step 4: cultures/ethos/faith per continent +
|
||||||
government per realm; derived, not saved).
|
government per realm; derived, not saved).
|
||||||
- `PlanetConflict.{hpp,cpp}` — `stepConflict` (civ Step 5: wars, casualties, conquest/allegiance,
|
- `PlanetConflict.{hpp,cpp}` — `stepConflict` (civ Steps 5-6: wars/conquest/revolts + diplomacy/
|
||||||
revolts; stateful, saved v21 + snapshotted).
|
alliances/coalitions; stateful, saved v21/v22 + snapshotted).
|
||||||
- `PlanetIO.cpp` — text config + binary save/load.
|
- `PlanetIO.cpp` — text config + binary save/load.
|
||||||
|
|
||||||
The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration),
|
The viewer is one `Viewer` struct: `Viewer.{hpp,cpp}` (state + setup + sim orchestration),
|
||||||
@ -481,6 +481,31 @@ vassals consistent). State — `sSettleAllegiance` + `wars` + `sWarRng`/`sWarNex
|
|||||||
Realms tab, a cell-info "AT WAR" flag, a HUD war count, and **kind=5** events (declare / capture / sack /
|
Realms tab, a cell-info "AT WAR" flag, a HUD war count, and **kind=5** events (declare / capture / sack /
|
||||||
revolt / peace). Knobs `war*`.
|
revolt / peace). Knobs `war*`.
|
||||||
|
|
||||||
|
## Civilization Step 6 — diplomacy, alliances & coalitions
|
||||||
|
|
||||||
|
Same file (`PlanetConflict.cpp`), same yearly `stepConflict` tick, same `sWarRng` — diplomacy is the war
|
||||||
|
subsystem's politics layer. A **`DiploTie`** { `a`, `b` (capital settlement indices, a<b), `attitude`
|
||||||
|
∈[−1,1], `truceUntil`, `DiploKind` } per interacting realm pair; the vector `diplomacy` is **stateful**,
|
||||||
|
saved (**v22**) + snapshotted (a per-frame block in the step-back history, like the v21 war block). New
|
||||||
|
yearly order inside `stepConflict`: **revolts → prosecute → diplomacy → declare**.
|
||||||
|
- **Prosecute → peace treaty**: when a war ends by exhaustion, the pair's tie gets a
|
||||||
|
`truceUntil = year + diploTruceYears` and an attitude drop of `diploWarGrudge` (a scar).
|
||||||
|
- **Diplomacy pass**: prune ties whose endpoint settlement died; for each near realm pair (the
|
||||||
|
settlement-proximity adjacency, now built once at the top and shared with war-declare) drift `attitude`
|
||||||
|
toward a target set by culture+faith affinity (`diploAffinity`) plus a per-pair personality noise, minus
|
||||||
|
a war penalty, plus truce recovery; reclassify into Alliance / Non-aggression / Rival by threshold
|
||||||
|
(`diploAllyThreshold`/`diploNonAggThreshold`/`diploRivalThreshold`, with a ±0.08 hysteresis) and emit a
|
||||||
|
**kind-6** event on a kind change.
|
||||||
|
- **Declare**: skip allied / non-aggression / under-truce pairs; hostility's ideology term is now
|
||||||
|
`max(0, −attitude)` (rivals fight); after A declares on B, every **Alliance** tie of B makes that ally
|
||||||
|
declare its own war on A (a **coalition**, reusing the 1-v-1 war path), capped by `warMaxConcurrent`.
|
||||||
|
|
||||||
|
Query helpers `diploBetween(nationA,nationB)` / `realmsAllied(...)` map nation index → capital → tie.
|
||||||
|
Render: `buildDiploLinks` (great-circle **arcs** between allied/rival capitals — green/dark-red) over the
|
||||||
|
Territory view (`P`); the Realms tab shows per-realm ally/rival counts + the active-wars list; cell-info
|
||||||
|
an allies/rivals line; kind-6 events (`=` icon). `captureWeather`/`restoreWeather` carry `diplomacy`.
|
||||||
|
Knobs `diplo*`.
|
||||||
|
|
||||||
## Headless testing
|
## Headless testing
|
||||||
|
|
||||||
Engine is raylib-free, so logic is tested without a display. Build/run:
|
Engine is raylib-free, so logic is tested without a display. Build/run:
|
||||||
@ -496,7 +521,7 @@ g++ -std=c++17 -O2 -Isrc/sim test_logic.cpp src/sim/IcoSphere.cpp src/sim/Planet
|
|||||||
src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp \
|
src/sim/PlanetCulture.cpp src/sim/PlanetConflict.cpp \
|
||||||
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
src/sim/PlanetIO.cpp -o /tmp/t && /tmp/t
|
||||||
# test_biota / test_live / test_ocean / test_weather / test_volcano / test_geography / test_ecoregions /
|
# test_biota / test_live / test_ocean / test_weather / test_volcano / test_geography / test_ecoregions /
|
||||||
# test_civ / test_nation / test_culture / test_conflict use the same source list.
|
# test_civ / test_nation / test_culture / test_conflict / test_diplomacy use the same source list.
|
||||||
```
|
```
|
||||||
(add new `src/sim/*.cpp` to that list as stages are added). `Planet::step()` passes are
|
(add new `src/sim/*.cpp` to that list as stages are added). `Planet::step()` passes are
|
||||||
data-parallel + double-buffered → bit-identical for any OpenMP thread count (determinism).
|
data-parallel + double-buffered → bit-identical for any OpenMP thread count (determinism).
|
||||||
|
|||||||
@ -79,6 +79,29 @@ void buildCultureBorders(const Planet& p, float radius, std::vector<Vector3>& se
|
|||||||
buildLabelBorders(p, radius, p.cellCulture(), segs);
|
buildLabelBorders(p, radius, p.cellCulture(), segs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void buildDiploLinks(const Planet& p, float radius, std::vector<Vector3>& ally, std::vector<Vector3>& rival) {
|
||||||
|
ally.clear(); rival.clear();
|
||||||
|
const auto& ties = p.diploList();
|
||||||
|
if (ties.empty()) return;
|
||||||
|
const double abP = p.cfg.civAbandonPop;
|
||||||
|
auto alive = [&](int s) { return s >= 0 && s < (int)p.settlements.size() && p.settlements[s].population >= abP; };
|
||||||
|
const int steps = 10; // segments per arc
|
||||||
|
for (const DiploTie& t : ties) {
|
||||||
|
if (t.kind != DiploKind::Alliance && t.kind != DiploKind::Rival) continue;
|
||||||
|
if (!alive(t.a) || !alive(t.b)) continue;
|
||||||
|
Vec3 A = p.cells[p.settlements[t.a].cell].unit, B = p.cells[p.settlements[t.b].cell].unit;
|
||||||
|
auto& out = (t.kind == DiploKind::Alliance) ? ally : rival;
|
||||||
|
Vector3 prev{};
|
||||||
|
for (int i = 0; i <= steps; ++i) {
|
||||||
|
double u = (double)i / steps;
|
||||||
|
Vec3 m = (A * (1.0 - u) + B * u).normalized() * (radius * 1.03); // raised so it arcs over the surface
|
||||||
|
Vector3 cur{ (float)m.x, (float)m.y, (float)m.z };
|
||||||
|
if (i > 0) { out.push_back(prev); out.push_back(cur); }
|
||||||
|
prev = cur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void buildWarFrontier(const Planet& p, float radius, std::vector<Vector3>& segs) {
|
void buildWarFrontier(const Planet& p, float radius, std::vector<Vector3>& segs) {
|
||||||
segs.clear();
|
segs.clear();
|
||||||
const std::vector<int>& cn = p.cellNation();
|
const std::vector<int>& cn = p.cellNation();
|
||||||
|
|||||||
@ -22,6 +22,8 @@ void buildNationBorders(const Planet& p, float radius, std::vector<Vector3>& seg
|
|||||||
void buildCultureBorders(const Planet& p, float radius, std::vector<Vector3>& segs);
|
void buildCultureBorders(const Planet& p, float radius, std::vector<Vector3>& segs);
|
||||||
// Frontier segments between two cells whose realms are in an active war (civ Step 5): the war front.
|
// Frontier segments between two cells whose realms are in an active war (civ Step 5): the war front.
|
||||||
void buildWarFrontier(const Planet& p, float radius, std::vector<Vector3>& segs);
|
void buildWarFrontier(const Planet& p, float radius, std::vector<Vector3>& segs);
|
||||||
|
// Great-circle arcs between allied (-> ally) and rival (-> rival) realm capitals (civ Step 6).
|
||||||
|
void buildDiploLinks(const Planet& p, float radius, std::vector<Vector3>& ally, std::vector<Vector3>& rival);
|
||||||
|
|
||||||
// ---- Per-plate drift arrows -------------------------------------------------
|
// ---- Per-plate drift arrows -------------------------------------------------
|
||||||
struct PlateLabel { int id; Vector3 pos; };
|
struct PlateLabel { int id; Vector3 pos; };
|
||||||
|
|||||||
@ -114,6 +114,12 @@ static std::vector<std::string> cellInfo(const Planet& p, int i, double elev, do
|
|||||||
bool war = false; // civ Step 5: is this realm at war?
|
bool war = false; // civ Step 5: is this realm at war?
|
||||||
for (const War& w : p.warList()) if (w.attacker == nat.capital || w.defender == nat.capital) { war = true; break; }
|
for (const War& w : p.warList()) if (w.attacker == nat.capital || w.defender == nat.capital) { war = true; break; }
|
||||||
L.push_back(std::string("realm: ") + nat.name + " (" + nationTierName(nat.tier) + ")" + (war ? " - AT WAR" : ""));
|
L.push_back(std::string("realm: ") + nat.name + " (" + nationTierName(nat.tier) + ")" + (war ? " - AT WAR" : ""));
|
||||||
|
int al = 0, rv = 0; // civ Step 6: standing relations
|
||||||
|
for (const DiploTie& t : p.diploList()) {
|
||||||
|
if (t.a != nat.capital && t.b != nat.capital) continue;
|
||||||
|
if (t.kind == DiploKind::Alliance) ++al; else if (t.kind == DiploKind::Rival) ++rv;
|
||||||
|
}
|
||||||
|
if (al > 0 || rv > 0) L.push_back("allies: " + std::to_string(al) + " / rivals: " + std::to_string(rv));
|
||||||
} else if (p.cells[i].elevation > p.cfg.seaLevel) {
|
} else if (p.cells[i].elevation > p.cfg.seaLevel) {
|
||||||
L.push_back(std::string("realm: wilderness"));
|
L.push_back(std::string("realm: wilderness"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -503,6 +503,9 @@ void Viewer::saveGame(const char* path) {
|
|||||||
{ uint64_t m = f.w.wars.size(); os.write((char*)&m, 8);
|
{ uint64_t m = f.w.wars.size(); os.write((char*)&m, 8);
|
||||||
if (m) os.write((const char*)f.w.wars.data(), (std::streamsize)(m * sizeof(War))); }
|
if (m) os.write((const char*)f.w.wars.data(), (std::streamsize)(m * sizeof(War))); }
|
||||||
os.write((char*)&f.w.warRng, 4); os.write((char*)&f.w.warNextId, 4);
|
os.write((char*)&f.w.warRng, 4); os.write((char*)&f.w.warNextId, 4);
|
||||||
|
// 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);
|
||||||
|
if (m) os.write((const char*)f.w.diplomacy.data(), (std::streamsize)(m * sizeof(DiploTie))); }
|
||||||
}
|
}
|
||||||
// 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);
|
||||||
@ -536,7 +539,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)) { 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
|
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
|
||||||
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
|
||||||
@ -599,6 +602,12 @@ void Viewer::loadGame(const char* path) {
|
|||||||
is.read((char*)&f.w.warRng, 4); is.read((char*)&f.w.warNextId, 4);
|
is.read((char*)&f.w.warRng, 4); is.read((char*)&f.w.warNextId, 4);
|
||||||
if (!is) historyOk = false;
|
if (!is) historyOk = false;
|
||||||
}
|
}
|
||||||
|
if (ver >= 22) { // v22: per-frame diplomacy
|
||||||
|
uint64_t m = 0; is.read((char*)&m, 8);
|
||||||
|
if (!is || m > 1000000) historyOk = false;
|
||||||
|
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;
|
||||||
|
}
|
||||||
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())
|
||||||
@ -756,7 +765,7 @@ void Viewer::liveAdvance(double dtClock, double dtWeather) {
|
|||||||
for (long y = firstYear; y <= year; ++y) {
|
for (long y = firstYear; y <= year; ++y) {
|
||||||
ConflictUpdate wu = planet.stepConflict(y);
|
ConflictUpdate wu = planet.stepConflict(y);
|
||||||
for (const WarEvent& e : wu.events)
|
for (const WarEvent& e : wu.events)
|
||||||
appendEvent(5, 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -779,6 +788,7 @@ void Viewer::rebuildTerritory() {
|
|||||||
buildNationBorders(planet, borderR, nationBorders);
|
buildNationBorders(planet, borderR, nationBorders);
|
||||||
buildCultureBorders(planet, borderR, cultureBorders);
|
buildCultureBorders(planet, borderR, cultureBorders);
|
||||||
buildWarFrontier(planet, borderR + 0.001f, warFrontier); // civ Step 5: current war fronts (drawn red)
|
buildWarFrontier(planet, borderR + 0.001f, warFrontier); // civ Step 5: current war fronts (drawn red)
|
||||||
|
buildDiploLinks(planet, borderR, allyLinks, rivalLinks); // civ Step 6: alliance/rivalry arcs
|
||||||
double yearHours = std::max(1.0, planet.cfg.dayLengthHours * planet.cfg.yearLengthDays);
|
double yearHours = std::max(1.0, planet.cfg.dayLengthHours * planet.cfg.yearLengthDays);
|
||||||
lastTerritoryYear = (long)std::floor(liveTime / yearHours);
|
lastTerritoryYear = (long)std::floor(liveTime / yearHours);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 = 21; // 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 = 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 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";
|
||||||
@ -110,6 +110,7 @@ struct Viewer {
|
|||||||
std::vector<Vector3> cultureBorders; // cultural-region border segments (civ Step 4, rebuilt with territory)
|
std::vector<Vector3> cultureBorders; // cultural-region border segments (civ Step 4, rebuilt with territory)
|
||||||
bool showCultureBorders = false; // draw cultural-region borders (on with the Culture view)
|
bool showCultureBorders = false; // draw cultural-region borders (on with the Culture view)
|
||||||
std::vector<Vector3> warFrontier; // red frontier segments between realms currently at war (civ Step 5)
|
std::vector<Vector3> warFrontier; // red frontier segments between realms currently at war (civ Step 5)
|
||||||
|
std::vector<Vector3> allyLinks, rivalLinks; // civ Step 6: capital-to-capital arcs (allies green, rivals red)
|
||||||
long lastTerritoryYear = -1; // sim year territory was last recomputed (recompute when it ticks)
|
long lastTerritoryYear = -1; // sim year territory was last recomputed (recompute when it ticks)
|
||||||
|
|
||||||
// World event journal: currently Live World events, shaped to be reused by later phases.
|
// World event journal: currently Live World events, shaped to be reused by later phases.
|
||||||
|
|||||||
@ -106,6 +106,20 @@ void Viewer::renderGlobe3D() {
|
|||||||
}
|
}
|
||||||
rlEnd(); rlSetLineWidth(1.0f);
|
rlEnd(); rlSetLineWidth(1.0f);
|
||||||
}
|
}
|
||||||
|
if (showNationBorders) { // civ Step 6: diplomacy arcs (allies green, rivals dark red)
|
||||||
|
rlSetLineWidth(2.0f); rlBegin(RL_LINES);
|
||||||
|
rlColor4ub(70, 220, 120, 200);
|
||||||
|
for (size_t i = 0; i + 1 < allyLinks.size(); i += 2) {
|
||||||
|
rlVertex3f(allyLinks[i].x, allyLinks[i].y, allyLinks[i].z);
|
||||||
|
rlVertex3f(allyLinks[i + 1].x, allyLinks[i + 1].y, allyLinks[i + 1].z);
|
||||||
|
}
|
||||||
|
rlColor4ub(150, 40, 60, 200);
|
||||||
|
for (size_t i = 0; i + 1 < rivalLinks.size(); i += 2) {
|
||||||
|
rlVertex3f(rivalLinks[i].x, rivalLinks[i].y, rivalLinks[i].z);
|
||||||
|
rlVertex3f(rivalLinks[i + 1].x, rivalLinks[i + 1].y, rivalLinks[i + 1].z);
|
||||||
|
}
|
||||||
|
rlEnd(); rlSetLineWidth(1.0f);
|
||||||
|
}
|
||||||
if (showDrift && !driftArrows.empty()) {
|
if (showDrift && !driftArrows.empty()) {
|
||||||
rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(90, 230, 255, 255);
|
rlSetLineWidth(2.5f); rlBegin(RL_LINES); rlColor4ub(90, 230, 255, 255);
|
||||||
for (size_t i = 0; i + 1 < driftArrows.size(); i += 2) {
|
for (size_t i = 0; i + 1 < driftArrows.size(); i += 2) {
|
||||||
@ -362,6 +376,8 @@ void Viewer::renderMap2D() {
|
|||||||
if (showNationBorders && !nationBorders.empty()) drawSegments2D(nationBorders, Color{18, 18, 26, 235}, 2.0f, vr, mapLon);
|
if (showNationBorders && !nationBorders.empty()) drawSegments2D(nationBorders, Color{18, 18, 26, 235}, 2.0f, vr, mapLon);
|
||||||
if (showCultureBorders && !cultureBorders.empty()) drawSegments2D(cultureBorders, Color{245, 240, 220, 230}, 2.5f, vr, mapLon);
|
if (showCultureBorders && !cultureBorders.empty()) drawSegments2D(cultureBorders, Color{245, 240, 220, 230}, 2.5f, vr, mapLon);
|
||||||
if (showNationBorders && !warFrontier.empty()) drawSegments2D(warFrontier, Color{235, 40, 30, 255}, 2.5f, vr, mapLon);
|
if (showNationBorders && !warFrontier.empty()) drawSegments2D(warFrontier, Color{235, 40, 30, 255}, 2.5f, vr, mapLon);
|
||||||
|
if (showNationBorders && !allyLinks.empty()) drawSegments2D(allyLinks, Color{70, 220, 120, 220}, 1.5f, vr, mapLon);
|
||||||
|
if (showNationBorders && !rivalLinks.empty()) drawSegments2D(rivalLinks, Color{150, 40, 60, 220}, 1.5f, vr, mapLon);
|
||||||
if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, vr, mapLon);
|
if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, vr, mapLon);
|
||||||
if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, vr, mapLon);
|
if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, vr, mapLon);
|
||||||
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, vr, mapLon);
|
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, vr, mapLon);
|
||||||
@ -584,7 +600,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 ? "!" : "~";
|
const char* icon = e.kind == 2 ? "^" : e.kind == 3 ? "*" : e.kind == 4 ? "#" : e.kind == 5 ? "!" : e.kind == 6 ? "=" : "~";
|
||||||
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});
|
||||||
@ -703,6 +719,13 @@ void Viewer::renderLiveInfo() {
|
|||||||
return "a fallen realm";
|
return "a fallen realm";
|
||||||
};
|
};
|
||||||
auto atWar = [&](int cap) { for (const War& w : W) if (w.attacker == cap || w.defender == cap) return true; return false; };
|
auto atWar = [&](int cap) { for (const War& w : W) if (w.attacker == cap || w.defender == cap) return true; return false; };
|
||||||
|
auto tieCounts = [&](int cap, int& allies, int& rivals) { // Step 6: standing relations of a realm
|
||||||
|
allies = 0; rivals = 0;
|
||||||
|
for (const DiploTie& t : planet.diploList()) {
|
||||||
|
if (t.a != cap && t.b != cap) continue;
|
||||||
|
if (t.kind == DiploKind::Alliance) ++allies; else if (t.kind == DiploKind::Rival) ++rivals;
|
||||||
|
}
|
||||||
|
};
|
||||||
if (!W.empty()) { // active wars summary
|
if (!W.empty()) { // active wars summary
|
||||||
DrawText(TextFormat("Wars: %d", (int)W.size()), x, y, 14, Color{235, 90, 80, 255});
|
DrawText(TextFormat("Wars: %d", (int)W.size()), x, y, 14, Color{235, 90, 80, 255});
|
||||||
y += 19;
|
y += 19;
|
||||||
@ -733,6 +756,9 @@ void Viewer::renderLiveInfo() {
|
|||||||
: nat.totalPop >= 1.0e3 ? TextFormat("%.0fk", nat.totalPop / 1.0e3)
|
: nat.totalPop >= 1.0e3 ? TextFormat("%.0fk", nat.totalPop / 1.0e3)
|
||||||
: TextFormat("%.0f", nat.totalPop);
|
: TextFormat("%.0f", nat.totalPop);
|
||||||
DrawText(nat.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, fg);
|
DrawText(nat.name.c_str(), (int)row.x + 6, (int)row.y + 2, 14, fg);
|
||||||
|
int al = 0, rv = 0; tieCounts(nat.capital, al, rv);
|
||||||
|
if (al > 0) DrawText(TextFormat("+%d", al), (int)(r.x + r.width) - 152, (int)row.y + 3, 11, Color{90, 210, 130, 255}); // allies
|
||||||
|
if (rv > 0) DrawText(TextFormat("-%d", rv), (int)(r.x + r.width) - 128, (int)row.y + 3, 11, Color{225, 95, 95, 255}); // rivals
|
||||||
if (atWar(nat.capital)) DrawCircle((int)(r.x + r.width) - 104, (int)row.y + 9, 3.5f, Color{235, 60, 45, 255}); // at-war marker
|
if (atWar(nat.capital)) DrawCircle((int)(r.x + r.width) - 104, (int)row.y + 9, 3.5f, Color{235, 60, 45, 255}); // at-war marker
|
||||||
DrawText(TextFormat("%dx %s", nat.members, tp), (int)(r.x + r.width) - 92, (int)row.y + 3, 11, Color{150, 158, 178, 255});
|
DrawText(TextFormat("%dx %s", nat.members, tp), (int)(r.x + r.width) - 92, (int)row.y + 3, 11, Color{150, 158, 178, 255});
|
||||||
y += 20;
|
y += 20;
|
||||||
|
|||||||
@ -65,7 +65,7 @@ void Planet::buildGeometry() {
|
|||||||
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();
|
||||||
wars.clear(); sSettleAllegiance.clear();
|
wars.clear(); sSettleAllegiance.clear(); diplomacy.clear();
|
||||||
sWarRng = cfg.seed ? (cfg.seed ^ 0x5A7B0A11u) : 0x5A7B0A11u; sWarNextId = 1;
|
sWarRng = cfg.seed ? (cfg.seed ^ 0x5A7B0A11u) : 0x5A7B0A11u; sWarNextId = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ public:
|
|||||||
std::vector<Nation> nations; // realms grouped from settlements (derived each computeTerritory, not saved)
|
std::vector<Nation> nations; // realms grouped from settlements (derived each computeTerritory, not saved)
|
||||||
std::vector<Culture> cultures; // peoples/cultures, one per inhabited continent (derived, not saved)
|
std::vector<Culture> cultures; // peoples/cultures, one per inhabited continent (derived, not saved)
|
||||||
std::vector<War> wars; // civ Step 5: active wars between realms (stateful, saved v21)
|
std::vector<War> wars; // civ Step 5: active wars between realms (stateful, saved v21)
|
||||||
|
std::vector<DiploTie> diplomacy; // civ Step 6: standing realm relations (stateful, saved v22)
|
||||||
|
|
||||||
// Phase flag: false during Phase-1 forming (modest, original tectonics that
|
// Phase flag: false during Phase-1 forming (modest, original tectonics that
|
||||||
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
// settle), true during Phase-2 drift. Gates the increment-4 orogeny boosts
|
||||||
@ -223,6 +224,10 @@ public:
|
|||||||
const std::vector<War>& warList() const { return wars; }
|
const std::vector<War>& warList() const { return wars; }
|
||||||
const std::vector<int>& settleAllegiance() const { return sSettleAllegiance; } // overlord capital settlement index (-1 = free)
|
const std::vector<int>& settleAllegiance() const { return sSettleAllegiance; } // overlord capital settlement index (-1 = free)
|
||||||
bool realmsAtWar(int nationA, int nationB) const; // are these two nation indices in an active war?
|
bool realmsAtWar(int nationA, int nationB) const; // are these two nation indices in an active war?
|
||||||
|
// Diplomacy (civ Step 6). Relations are updated inside stepConflict(); these query them by nation index.
|
||||||
|
const std::vector<DiploTie>& diploList() const { return diplomacy; }
|
||||||
|
DiploKind diploBetween(int nationA, int nationB) const; // relation kind between two nation indices
|
||||||
|
bool realmsAllied(int nationA, int nationB) const; // are these two realms allied?
|
||||||
// Per-settlement live conditions (derived each stepCivilization; not saved). condition = the combined
|
// Per-settlement live conditions (derived each stepCivilization; not saved). condition = the combined
|
||||||
// environmental multiplier on carrying capacity (1 = normal, <1 = hardship, >1 = boom); drought =
|
// environmental multiplier on carrying capacity (1 = normal, <1 = hardship, >1 = boom); drought =
|
||||||
// current drought severity 0..1. Parallel to `settlements`. Used by the viewer for tint + events.
|
// current drought severity 0..1. Parallel to `settlements`. Used by the viewer for tint + events.
|
||||||
@ -253,7 +258,7 @@ public:
|
|||||||
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 hasConflict = true, bool hasDiplo = true);
|
||||||
|
|
||||||
// Helpers for rendering / info.
|
// Helpers for rendering / info.
|
||||||
double cellWidthMeters() const; // approx lateral cell spacing
|
double cellWidthMeters() const; // approx lateral cell spacing
|
||||||
|
|||||||
@ -4,12 +4,14 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
// --- Civilization Step 5: conflict, war & shifting borders -------------------
|
// --- Civilization Steps 5-6: conflict, war & diplomacy -----------------------
|
||||||
// stepConflict() runs once per sim year. It reads the current realms (derived last recompute) and
|
// stepConflict() runs once per sim year. It reads the current realms (derived last recompute) and
|
||||||
// MUTATES the world: revolts + casualties + conquests set per-settlement allegiance and populations,
|
// MUTATES the world: revolts + casualties + conquests set per-settlement allegiance and populations,
|
||||||
// and territory recomputes afterwards so borders move. Realms are identified by their capital SETTLEMENT
|
// and territory recomputes afterwards so borders move. Realms are identified by their capital SETTLEMENT
|
||||||
// INDEX (stable across the yearly recompute). A separate war RNG (sWarRng) keeps tectonics deterministic;
|
// INDEX (stable across the yearly recompute). Step 6 adds a diplomacy pass -- per-realm-pair attitudes
|
||||||
// the state (allegiance + wars + RNG) is saved (v21) and snapshotted so step-back rewinds conquests.
|
// drift into alliances/rivalries, allies don't fight + join each other's wars (coalitions), and wars end
|
||||||
|
// in truces. A separate war RNG (sWarRng) keeps tectonics deterministic; the state (allegiance + wars +
|
||||||
|
// diplomacy + RNG) is saved (v21/v22) and snapshotted so step-back rewinds conquests + relations.
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
inline uint32_t warHash(uint32_t a) { a ^= a << 13; a ^= a >> 17; a ^= a << 5; return a ? a : 1u; }
|
inline uint32_t warHash(uint32_t a) { a ^= a << 13; a ^= a >> 17; a ^= a << 5; return a ? a : 1u; }
|
||||||
@ -24,6 +26,15 @@ bool Planet::realmsAtWar(int a, int b) const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diplomatic relation between two nation indices (ties are keyed on capital settlement index).
|
||||||
|
DiploKind Planet::diploBetween(int a, int b) const {
|
||||||
|
if (a < 0 || b < 0 || a >= (int)nations.size() || b >= (int)nations.size()) return DiploKind::Neutral;
|
||||||
|
int lo = std::min(nations[a].capital, nations[b].capital), hi = std::max(nations[a].capital, nations[b].capital);
|
||||||
|
for (const DiploTie& t : diplomacy) if (t.a == lo && t.b == hi) return t.kind;
|
||||||
|
return DiploKind::Neutral;
|
||||||
|
}
|
||||||
|
bool Planet::realmsAllied(int a, int b) const { return diploBetween(a, b) == DiploKind::Alliance; }
|
||||||
|
|
||||||
ConflictUpdate Planet::stepConflict(long year) {
|
ConflictUpdate Planet::stepConflict(long year) {
|
||||||
ConflictUpdate up;
|
ConflictUpdate up;
|
||||||
if (settlements.empty() || nations.empty()) return up;
|
if (settlements.empty() || nations.empty()) return up;
|
||||||
@ -58,6 +69,36 @@ ConflictUpdate Planet::stepConflict(long year) {
|
|||||||
auto rnd = [&]() { sWarRng ^= sWarRng << 13; sWarRng ^= sWarRng >> 17; sWarRng ^= sWarRng << 5; if (!sWarRng) sWarRng = 1u; return (sWarRng & 0xFFFFFFu) / double(0x1000000); };
|
auto rnd = [&]() { sWarRng ^= sWarRng << 13; sWarRng ^= sWarRng >> 17; sWarRng ^= sWarRng << 5; if (!sWarRng) sWarRng = 1u; return (sWarRng & 0xFFFFFFu) / double(0x1000000); };
|
||||||
bool activity = false;
|
bool activity = false;
|
||||||
|
|
||||||
|
// Diplomacy ties are keyed on capital settlement index (a<b). Find / find-or-create a tie.
|
||||||
|
auto tieIdx = [&](int ca, int cb) -> int {
|
||||||
|
int lo = std::min(ca, cb), hi = std::max(ca, cb);
|
||||||
|
for (size_t i = 0; i < diplomacy.size(); ++i) if (diplomacy[i].a == lo && diplomacy[i].b == hi) return (int)i;
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
auto tieRef = [&](int ca, int cb) -> DiploTie& {
|
||||||
|
int i = tieIdx(ca, cb);
|
||||||
|
if (i >= 0) return diplomacy[(size_t)i];
|
||||||
|
DiploTie t; t.a = std::min(ca, cb); t.b = std::max(ca, cb); diplomacy.push_back(t);
|
||||||
|
return diplomacy.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Realm adjacency by SETTLEMENT PROXIMITY (territory is influence-limited with wilderness marches),
|
||||||
|
// built once and reused by both the diplomacy pass and war declaration.
|
||||||
|
const double neighborRange = cfg.civTerritoryMax * 1.5;
|
||||||
|
std::map<std::pair<int, int>, std::pair<double, int>> pinfo; // (loNat,hiNat) -> (minDist, closePairs)
|
||||||
|
for (size_t a = 0; a < settlements.size(); ++a) {
|
||||||
|
int na = sSettleNation[a]; if (na < 0 || !alive((int)a)) continue;
|
||||||
|
for (size_t b = a + 1; b < settlements.size(); ++b) {
|
||||||
|
int nb = sSettleNation[b]; if (nb < 0 || nb == na || !alive((int)b)) continue;
|
||||||
|
double d = angCell(settlements[a].cell, settlements[b].cell);
|
||||||
|
if (d >= neighborRange) continue;
|
||||||
|
auto key = std::make_pair(std::min(na, nb), std::max(na, nb));
|
||||||
|
auto it = pinfo.find(key);
|
||||||
|
if (it == pinfo.end()) pinfo[key] = {d, 1};
|
||||||
|
else { it->second.first = std::min(it->second.first, d); it->second.second++; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- 1) Prune stale allegiances + revolts (contestable conquest) ---------
|
// --- 1) Prune stale allegiances + revolts (contestable conquest) ---------
|
||||||
for (size_t s = 0; s < settlements.size(); ++s) {
|
for (size_t s = 0; s < settlements.size(); ++s) {
|
||||||
int ov = sSettleAllegiance[s];
|
int ov = sSettleAllegiance[s];
|
||||||
@ -126,33 +167,64 @@ ConflictUpdate Planet::stepConflict(long year) {
|
|||||||
w.warscore *= 0.3; // the front resets after a city changes hands
|
w.warscore *= 0.3; // the front resets after a city changes hands
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (std::fabs(w.warscore) >= cfg.warExhaustion || w.battles > 60) { // exhausted -> peace
|
if (std::fabs(w.warscore) >= cfg.warExhaustion || w.battles > 60) { // exhausted -> peace treaty
|
||||||
|
DiploTie& t = tieRef(w.attacker, w.defender); // a truce + a lasting scar
|
||||||
|
t.truceUntil = year + (long)cfg.diploTruceYears;
|
||||||
|
t.attitude = std::clamp(t.attitude - cfg.diploWarGrudge, -1.0, 1.0);
|
||||||
up.events.push_back(WarEvent{1, settlements[nations[na].capital].cell,
|
up.events.push_back(WarEvent{1, settlements[nations[na].capital].cell,
|
||||||
"Peace between the " + nations[na].name + " and the " + nations[nd].name,
|
"Peace between the " + nations[na].name + " and the " + nations[nd].name,
|
||||||
"The war ends, both sides exhausted."});
|
"The war ends in a truce, both sides exhausted."});
|
||||||
continue; // drop the war
|
continue; // drop the war
|
||||||
}
|
}
|
||||||
keep.push_back(w);
|
keep.push_back(w);
|
||||||
}
|
}
|
||||||
wars.swap(keep);
|
wars.swap(keep);
|
||||||
|
|
||||||
// --- 3) Declare new wars from neighbouring-realm hostility ---------------
|
// --- 2.5) Diplomacy: prune dead ties, drift attitudes, form/break relations (civ Step 6) --
|
||||||
// Territory is influence-limited (wilderness marches between realms), so realm adjacency is by
|
{ // drop ties whose endpoints are gone (kept dormant across a temporary capital swap otherwise)
|
||||||
// SETTLEMENT PROXIMITY -- realms that are merely near each other can contest the land between them.
|
std::vector<DiploTie> kd; kd.reserve(diplomacy.size());
|
||||||
const double neighborRange = cfg.civTerritoryMax * 1.5;
|
for (const DiploTie& t : diplomacy) if (alive(t.a) && alive(t.b)) kd.push_back(t);
|
||||||
std::map<std::pair<int, int>, std::pair<double, int>> pinfo; // (loNat,hiNat) -> (minDist, closePairs)
|
diplomacy.swap(kd);
|
||||||
for (size_t a = 0; a < settlements.size(); ++a) {
|
}
|
||||||
int na = sSettleNation[a]; if (na < 0 || !alive((int)a)) continue;
|
for (auto& kv : pinfo) {
|
||||||
for (size_t b = a + 1; b < settlements.size(); ++b) {
|
int A = kv.first.first, B = kv.first.second;
|
||||||
int nb = sSettleNation[b]; if (nb < 0 || nb == na || !alive((int)b)) continue;
|
int ca = nations[A].capital, cb = nations[B].capital;
|
||||||
double d = angCell(settlements[a].cell, settlements[b].cell);
|
int cA = nations[A].cultureId, cB = nations[B].cultureId;
|
||||||
if (d >= neighborRange) continue;
|
double aff = 0.0; // shared culture + faith -> friendly
|
||||||
auto key = std::make_pair(std::min(na, nb), std::max(na, nb));
|
if (cA >= 0 && cB >= 0) {
|
||||||
auto it = pinfo.find(key);
|
aff += (cA == cB) ? 1.0 : -0.6;
|
||||||
if (it == pinfo.end()) pinfo[key] = {d, 1};
|
if (cA < (int)cultures.size() && cB < (int)cultures.size())
|
||||||
else { it->second.first = std::min(it->second.first, d); it->second.second++; }
|
aff += (cultures[cA].faith == cultures[cB].faith) ? 0.5 : -0.3;
|
||||||
|
}
|
||||||
|
uint32_t pk = warHash((uint32_t)std::min(ca, cb) * 2654435761u ^ (uint32_t)std::max(ca, cb) * 40503u ^ (uint32_t)year * 19349663u ^ seed ^ 0xD1D0u);
|
||||||
|
double noise = (pk & 0xFFFFu) / 65535.0 * 2.0 - 1.0; // [-1,1] per-pair personality
|
||||||
|
double target = std::clamp(cfg.diploAffinity * 0.5 * aff + 0.25 * noise, -1.0, 1.0);
|
||||||
|
DiploTie& t = tieRef(ca, cb);
|
||||||
|
double drift = cfg.diploDriftRate * (target - t.attitude);
|
||||||
|
if (realmsAtWar(A, B)) drift -= cfg.diploWarPenalty * cfg.diploDriftRate * 4.0; // war sours relations
|
||||||
|
if (t.truceUntil > year) drift += (0.0 - t.attitude) * cfg.diploDriftRate * 0.5; // truce eases tempers
|
||||||
|
t.attitude = std::clamp(t.attitude + drift, -1.0, 1.0);
|
||||||
|
DiploKind old = t.kind, nk; // classify with hysteresis
|
||||||
|
if (t.attitude >= cfg.diploAllyThreshold) nk = DiploKind::Alliance;
|
||||||
|
else if (t.attitude <= cfg.diploRivalThreshold) nk = DiploKind::Rival;
|
||||||
|
else if (t.attitude >= cfg.diploNonAggThreshold) nk = DiploKind::NonAggression;
|
||||||
|
else nk = DiploKind::Neutral;
|
||||||
|
if (old == DiploKind::Alliance && t.attitude >= cfg.diploAllyThreshold - 0.08) nk = DiploKind::Alliance;
|
||||||
|
if (old == DiploKind::Rival && t.attitude <= cfg.diploRivalThreshold + 0.08) nk = DiploKind::Rival;
|
||||||
|
if (nk != old) {
|
||||||
|
t.kind = nk; activity = true;
|
||||||
|
const std::string& nA = nations[A].name; const std::string& nB = nations[B].name;
|
||||||
|
int cell = settlements[ca].cell; std::string title, detail;
|
||||||
|
if (nk == DiploKind::Alliance) { title = "The " + nA + " and the " + nB + " form an alliance"; detail = "A pact of mutual defence."; }
|
||||||
|
else if (nk == DiploKind::Rival) { title = "The " + nA + " and the " + nB + " become rivals"; detail = "Enmity hardens between them."; }
|
||||||
|
else if (nk == DiploKind::NonAggression) { title = "The " + nA + " and the " + nB + " sign a non-aggression pact"; detail = "They agree to keep the peace."; }
|
||||||
|
else { title = (old == DiploKind::Alliance) ? "The alliance of the " + nA + " and the " + nB + " dissolves"
|
||||||
|
: "The " + nA + " and the " + nB + " normalise relations"; detail = ""; }
|
||||||
|
up.events.push_back(WarEvent{1, cell, title, detail, 6});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 3) Declare new wars (alliance-gated; rivals fight; allies form coalitions) -----------
|
||||||
struct Cand { int a, b; double hostility; };
|
struct Cand { int a, b; double hostility; };
|
||||||
std::vector<Cand> cands;
|
std::vector<Cand> cands;
|
||||||
for (auto& kv : pinfo) {
|
for (auto& kv : pinfo) {
|
||||||
@ -160,17 +232,20 @@ ConflictUpdate Planet::stepConflict(long year) {
|
|||||||
double minD = kv.second.first; int closePairs = kv.second.second;
|
double minD = kv.second.first; int closePairs = kv.second.second;
|
||||||
if (realmsAtWar(A, B)) continue;
|
if (realmsAtWar(A, B)) continue;
|
||||||
if (nations[A].totalPop < cfg.warMinRealmPop || nations[B].totalPop < cfg.warMinRealmPop) continue;
|
if (nations[A].totalPop < cfg.warMinRealmPop || nations[B].totalPop < cfg.warMinRealmPop) continue;
|
||||||
|
int ca = nations[A].capital, cb = nations[B].capital;
|
||||||
|
int ti = tieIdx(ca, cb);
|
||||||
|
if (ti >= 0) { // allies/pacts/truces don't fight
|
||||||
|
if (diplomacy[(size_t)ti].kind == DiploKind::Alliance || diplomacy[(size_t)ti].kind == DiploKind::NonAggression) continue;
|
||||||
|
if (diplomacy[(size_t)ti].truceUntil > year) continue;
|
||||||
|
}
|
||||||
|
double att = (ti >= 0) ? diplomacy[(size_t)ti].attitude : 0.0;
|
||||||
double sA = realmStrength(A), sB = realmStrength(B);
|
double sA = realmStrength(A), sB = realmStrength(B);
|
||||||
double sizeGap = std::fabs(sA - sB) / (sA + sB);
|
double sizeGap = std::fabs(sA - sB) / (sA + sB);
|
||||||
int cA = nations[A].cultureId, cB = nations[B].cultureId;
|
double prox = 1.0 - std::min(1.0, minD / neighborRange);
|
||||||
double ideo = (cA != cB ? 1.0 : 0.0);
|
|
||||||
if (cA >= 0 && cB >= 0 && cA < (int)cultures.size() && cB < (int)cultures.size() && cultures[cA].faith != cultures[cB].faith) ideo += 0.5;
|
|
||||||
double prox = 1.0 - std::min(1.0, minD / neighborRange); // closer realms are more contested
|
|
||||||
double frontN = std::min(1.0, closePairs / 4.0);
|
double frontN = std::min(1.0, closePairs / 4.0);
|
||||||
int ca = nations[A].capital, cb = nations[B].capital;
|
|
||||||
uint32_t pk = warHash((uint32_t)std::min(ca, cb) * 73856093u ^ (uint32_t)std::max(ca, cb) * 19349663u ^ (uint32_t)year * 83492791u ^ seed);
|
uint32_t pk = warHash((uint32_t)std::min(ca, cb) * 73856093u ^ (uint32_t)std::max(ca, cb) * 19349663u ^ (uint32_t)year * 83492791u ^ seed);
|
||||||
double noise = (pk & 0xFFFFu) / 65535.0;
|
double noise = (pk & 0xFFFFu) / 65535.0;
|
||||||
double hostility = cfg.warAmbition * sizeGap + cfg.warIdeology * ideo + cfg.warBorder * (0.5 * frontN + 0.5 * prox) + 0.4 * noise;
|
double hostility = cfg.warAmbition * sizeGap + cfg.warIdeology * std::max(0.0, -att) + cfg.warBorder * (0.5 * frontN + 0.5 * prox) + 0.4 * noise;
|
||||||
cands.push_back({A, B, hostility});
|
cands.push_back({A, B, hostility});
|
||||||
}
|
}
|
||||||
std::sort(cands.begin(), cands.end(), [](const Cand& x, const Cand& y) { return x.hostility > y.hostility; });
|
std::sort(cands.begin(), cands.end(), [](const Cand& x, const Cand& y) { return x.hostility > y.hostility; });
|
||||||
@ -179,11 +254,26 @@ ConflictUpdate Planet::stepConflict(long year) {
|
|||||||
if (rnd() >= cfg.warDeclareRate * c.hostility) continue;
|
if (rnd() >= cfg.warDeclareRate * c.hostility) continue;
|
||||||
int att = (realmStrength(c.a) >= realmStrength(c.b)) ? c.a : c.b;
|
int att = (realmStrength(c.a) >= realmStrength(c.b)) ? c.a : c.b;
|
||||||
int def = (att == c.a) ? c.b : c.a;
|
int def = (att == c.a) ? c.b : c.a;
|
||||||
War w; w.id = sWarNextId++; w.attacker = nations[att].capital; w.defender = nations[def].capital; w.startYear = year;
|
int attCap = nations[att].capital, defCap = nations[def].capital;
|
||||||
|
War w; w.id = sWarNextId++; w.attacker = attCap; w.defender = defCap; w.startYear = year;
|
||||||
wars.push_back(w); activity = true;
|
wars.push_back(w); activity = true;
|
||||||
up.events.push_back(WarEvent{1, settlements[nations[att].capital].cell,
|
up.events.push_back(WarEvent{1, settlements[attCap].cell,
|
||||||
"The " + nations[att].name + " declares war on the " + nations[def].name,
|
"The " + nations[att].name + " declares war on the " + nations[def].name,
|
||||||
"Armies march to the frontier."});
|
"Armies march to the frontier."});
|
||||||
|
// Coalition: the defender's allies join the fight against the aggressor.
|
||||||
|
for (const DiploTie& t : diplomacy) {
|
||||||
|
if (t.kind != DiploKind::Alliance) continue;
|
||||||
|
int allyCap = (t.a == defCap) ? t.b : (t.b == defCap) ? t.a : -1;
|
||||||
|
if (allyCap < 0 || !alive(allyCap)) continue;
|
||||||
|
int allyNat = nationOfCap(allyCap);
|
||||||
|
if (allyNat < 0 || allyNat == att || realmsAtWar(allyNat, att)) continue;
|
||||||
|
if ((int)wars.size() >= cfg.warMaxConcurrent) break;
|
||||||
|
War cw; cw.id = sWarNextId++; cw.attacker = allyCap; cw.defender = attCap; cw.startYear = year;
|
||||||
|
wars.push_back(cw);
|
||||||
|
up.events.push_back(WarEvent{1, settlements[allyCap].cell,
|
||||||
|
"The " + nations[allyNat].name + " joins the war against the " + nations[att].name,
|
||||||
|
"Honouring its alliance with the " + nations[def].name + ".", 6});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
up.changed = activity;
|
up.changed = activity;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ struct WarEvent {
|
|||||||
int cell = -1; // where it happened (for the event's focus jump)
|
int cell = -1; // where it happened (for the event's focus jump)
|
||||||
std::string title;
|
std::string title;
|
||||||
std::string detail;
|
std::string detail;
|
||||||
|
uint8_t kind = 5; // WorldEvent kind: 5 war (default), 6 diplomacy
|
||||||
};
|
};
|
||||||
|
|
||||||
// What stepConflict() changed this year -- the viewer logs the events and recolors if borders moved.
|
// What stepConflict() changed this year -- the viewer logs the events and recolors if borders moved.
|
||||||
|
|||||||
@ -58,6 +58,8 @@
|
|||||||
D(civTerritoryBase) D(civTerritoryScale) D(civTerritoryMax) D(civVassalRange) D(civEmpirePop) \
|
D(civTerritoryBase) D(civTerritoryScale) D(civTerritoryMax) D(civVassalRange) D(civEmpirePop) \
|
||||||
D(warDeclareRate) D(warAmbition) D(warIdeology) D(warBorder) D(warWarlikeMult) D(warCasualtyRate) \
|
D(warDeclareRate) D(warAmbition) D(warIdeology) D(warBorder) D(warWarlikeMult) D(warCasualtyRate) \
|
||||||
D(warConquerScore) D(warSackChance) D(warExhaustion) D(warRevoltRate) D(warMinRealmPop) \
|
D(warConquerScore) D(warSackChance) D(warExhaustion) D(warRevoltRate) D(warMinRealmPop) \
|
||||||
|
D(diploDriftRate) D(diploAffinity) D(diploWarPenalty) D(diploTruceYears) D(diploWarGrudge) \
|
||||||
|
D(diploAllyThreshold) D(diploNonAggThreshold) D(diploRivalThreshold) \
|
||||||
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) \
|
||||||
@ -299,6 +301,14 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.warRevoltRate, 0.0, 10.0, "warRevoltRate"));
|
E(rng(cfg.warRevoltRate, 0.0, 10.0, "warRevoltRate"));
|
||||||
E(rng(cfg.warMinRealmPop, 0.0, 1.0e12, "warMinRealmPop"));
|
E(rng(cfg.warMinRealmPop, 0.0, 1.0e12, "warMinRealmPop"));
|
||||||
E(irng(cfg.warMaxConcurrent, 0, 100000, "warMaxConcurrent"));
|
E(irng(cfg.warMaxConcurrent, 0, 100000, "warMaxConcurrent"));
|
||||||
|
E(rng(cfg.diploDriftRate, 0.0, 10.0, "diploDriftRate"));
|
||||||
|
E(rng(cfg.diploAffinity, 0.0, 100.0, "diploAffinity"));
|
||||||
|
E(rng(cfg.diploWarPenalty, 0.0, 100.0, "diploWarPenalty"));
|
||||||
|
E(rng(cfg.diploTruceYears, 0.0, 1.0e6, "diploTruceYears"));
|
||||||
|
E(rng(cfg.diploWarGrudge, 0.0, 2.0, "diploWarGrudge"));
|
||||||
|
E(rng(cfg.diploAllyThreshold, -1.0, 1.0, "diploAllyThreshold"));
|
||||||
|
E(rng(cfg.diploNonAggThreshold, -1.0, 1.0, "diploNonAggThreshold"));
|
||||||
|
E(rng(cfg.diploRivalThreshold, -1.0, 1.0, "diploRivalThreshold"));
|
||||||
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"));
|
||||||
@ -476,12 +486,18 @@ void Planet::writeState(std::ostream& os) const {
|
|||||||
writePod(os, w.startYear); writePod(os, w.warscore); writePod(os, w.battles);
|
writePod(os, w.startYear); writePod(os, w.warscore); writePod(os, w.battles);
|
||||||
}
|
}
|
||||||
writePod(os, sWarRng); writePod(os, sWarNextId);
|
writePod(os, sWarRng); writePod(os, sWarNextId);
|
||||||
|
// v22: diplomacy (civ Step 6). Standing realm relations (alliances / rivalries / truces).
|
||||||
|
uint64_t nd = diplomacy.size(); writePod(os, nd);
|
||||||
|
for (const DiploTie& t : diplomacy) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 hasConflict, bool hasDiplo) {
|
||||||
// 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; }
|
||||||
@ -491,6 +507,7 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
|||||||
if (!hasGeoSalt) hasEcoregions = false; // ecoregions follow the v18 salt
|
if (!hasGeoSalt) hasEcoregions = false; // ecoregions follow the v18 salt
|
||||||
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
|
||||||
|
|
||||||
// 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
|
||||||
@ -723,6 +740,18 @@ bool Planet::readState(std::istream& is, bool hasBiome, bool hasBiota, bool hasM
|
|||||||
if (!sWarRng) sWarRng = cfg.seed ? (cfg.seed ^ 0x5A7B0A11u) : 0x5A7B0A11u;
|
if (!sWarRng) sWarRng = cfg.seed ? (cfg.seed ^ 0x5A7B0A11u) : 0x5A7B0A11u;
|
||||||
if (!sWarNextId) sWarNextId = 1;
|
if (!sWarNextId) sWarNextId = 1;
|
||||||
}
|
}
|
||||||
|
// v22: diplomacy (civ Step 6). Standing realm relations (pre-v22 saves keep none; they re-form live).
|
||||||
|
if (hasDiplo) {
|
||||||
|
uint64_t nd = 0; readPod(is, nd);
|
||||||
|
if (!is || nd > 1000000) return false;
|
||||||
|
diplomacy.resize((size_t)nd);
|
||||||
|
for (DiploTie& t : diplomacy) {
|
||||||
|
readPod(is, t.a); readPod(is, t.b); readPod(is, t.attitude);
|
||||||
|
readPod(is, t.truceUntil); uint8_t k = 0; readPod(is, k);
|
||||||
|
t.kind = (k <= (uint8_t)DiploKind::Rival) ? (DiploKind)k : DiploKind::Neutral;
|
||||||
|
if (!is || !std::isfinite(t.attitude)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
computeBiotaDensity(); // derived density scalars for the colour views
|
computeBiotaDensity(); // derived density scalars for the colour views
|
||||||
return (bool)is;
|
return (bool)is;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,6 +98,20 @@ struct War {
|
|||||||
int battles = 0; // number of resolved battle-years
|
int battles = 0; // number of resolved battle-years
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Civilization Step 6: diplomacy. A standing relationship between two realms (by capital SETTLEMENT
|
||||||
|
// INDEX, a<b -- the same stable id wars use). attitude drifts from culture/faith affinity, proximity and
|
||||||
|
// the scars of past wars; it crystallises into an alliance / non-aggression pact / rivalry. Stateful,
|
||||||
|
// saved (v22) + snapshotted.
|
||||||
|
enum class DiploKind : uint8_t { Neutral, NonAggression, Alliance, Rival };
|
||||||
|
|
||||||
|
struct DiploTie {
|
||||||
|
int a = -1; // capital settlement index of one realm (a < b)
|
||||||
|
int b = -1; // capital settlement index of the other realm
|
||||||
|
double attitude = 0.0; // -1 hostile .. +1 friendly
|
||||||
|
long truceUntil = -1; // no war between them until this sim year (-1 = none)
|
||||||
|
DiploKind kind = DiploKind::Neutral;
|
||||||
|
};
|
||||||
|
|
||||||
// A full snapshot of the (integrated, non-analytic) weather state, for the viewer's step-back
|
// A full snapshot of the (integrated, non-analytic) weather state, for the viewer's step-back
|
||||||
// undo history -- weather can't be reversed in closed form, so we restore a saved frame instead.
|
// undo history -- weather can't be reversed in closed form, so we restore a saved frame instead.
|
||||||
struct WeatherSnapshot {
|
struct WeatherSnapshot {
|
||||||
@ -114,6 +128,8 @@ struct WeatherSnapshot {
|
|||||||
std::vector<int> settlementAllegiance;
|
std::vector<int> settlementAllegiance;
|
||||||
std::vector<War> wars;
|
std::vector<War> wars;
|
||||||
uint32_t warRng = 0, warNextId = 0;
|
uint32_t warRng = 0, warNextId = 0;
|
||||||
|
// Civilization Step 6 (diplomacy): standing realm relations (alliances / rivalries / truces).
|
||||||
|
std::vector<DiploTie> diplomacy;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Plate {
|
struct Plate {
|
||||||
@ -452,4 +468,14 @@ struct PlanetConfig {
|
|||||||
double warExhaustion = 1.5; // |warscore| (or battle count) past which a war ends in peace
|
double warExhaustion = 1.5; // |warscore| (or battle count) past which a war ends in peace
|
||||||
double warRevoltRate = 0.04; // per-year base revolt chance of a held foreign/distant city
|
double warRevoltRate = 0.04; // per-year base revolt chance of a held foreign/distant city
|
||||||
double warMinRealmPop = 2000.0; // realms below this population don't start wars
|
double warMinRealmPop = 2000.0; // realms below this population don't start wars
|
||||||
|
// Civilization Step 6: diplomacy (stateful, saved v22). Realm attitudes drift into alliances /
|
||||||
|
// rivalries; allies don't fight + join each other's wars; wars end in truces.
|
||||||
|
double diploDriftRate = 0.06; // per-year attitude change scale
|
||||||
|
double diploAffinity = 1.0; // attitude pull from shared culture + faith (vs difference)
|
||||||
|
double diploWarPenalty = 0.5; // extra per-year attitude drop while two realms are at war
|
||||||
|
double diploTruceYears = 12.0; // post-war truce length (no new war between the two)
|
||||||
|
double diploWarGrudge = 0.4; // one-off attitude drop when a war between them ends (a scar)
|
||||||
|
double diploAllyThreshold = 0.5; // attitude at/above which two realms are allied
|
||||||
|
double diploNonAggThreshold = 0.2; // attitude at/above which they sign a non-aggression pact
|
||||||
|
double diploRivalThreshold = -0.5; // attitude at/below which they become rivals
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,6 +35,7 @@ WeatherSnapshot Planet::captureWeather() const {
|
|||||||
for (const Settlement& st : settlements) s.settlementPop.push_back(st.population);
|
for (const Settlement& st : settlements) s.settlementPop.push_back(st.population);
|
||||||
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
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ void Planet::restoreWeather(const WeatherSnapshot& s) {
|
|||||||
sSettleAllegiance = s.settlementAllegiance; // civ Step 5: restore conquest state
|
sSettleAllegiance = s.settlementAllegiance; // civ Step 5: restore conquest state
|
||||||
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
|
||||||
sHasWeather = !sHumidity.empty();
|
sHasWeather = !sHumidity.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
145
test_diplomacy.cpp
Normal file
145
test_diplomacy.cpp
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
// Headless test for civilization Step 6 (diplomacy, alliances & coalitions). No display needed.
|
||||||
|
//
|
||||||
|
// g++ -std=c++17 -O2 -Isrc/sim test_diplomacy.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/PlanetIO.cpp -o /tmp/tdip && /tmp/tdip
|
||||||
|
//
|
||||||
|
// Verifies: alliances + rivalries form; allies never war on each other; coalitions form (allies join a
|
||||||
|
// war); wars end in truces; determinism + RNG isolation; save-v22 + snapshot round-trip.
|
||||||
|
|
||||||
|
#include "Planet.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
static void check(bool cond, const char* what) {
|
||||||
|
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||||
|
if (!cond) ++failures;
|
||||||
|
}
|
||||||
|
static void settle(Planet& p, int maxSteps = 800) {
|
||||||
|
int run = 0;
|
||||||
|
for (int s = 0; s < maxSteps; ++s) { double mc = p.step(); if (mc < 2.0) { if (++run >= 3) break; } else run = 0; }
|
||||||
|
p.computeClimate(); p.classifyBiomes();
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
// Encourage lively diplomacy AND wars (so alliances/rivalries/coalitions/truces all get exercised).
|
||||||
|
static void livelyDiplo(Planet& p) {
|
||||||
|
p.cfg.diploDriftRate = 0.5; p.cfg.diploTruceYears = 20.0;
|
||||||
|
p.cfg.warDeclareRate = 2.0; p.cfg.warAmbition = 2.0; p.cfg.warBorder = 1.5;
|
||||||
|
p.cfg.warConquerScore = 5.0; p.cfg.warExhaustion = 1.0; // wars end by EXHAUSTION -> a truce (not conquest)
|
||||||
|
p.cfg.warMinRealmPop = 1.0; p.cfg.warMaxConcurrent = 50; p.cfg.warSackChance = 0.0;
|
||||||
|
}
|
||||||
|
static bool tiesEqual(const std::vector<DiploTie>& a, const std::vector<DiploTie>& b) {
|
||||||
|
if (a.size() != b.size()) return false;
|
||||||
|
for (size_t i = 0; i < a.size(); ++i)
|
||||||
|
if (a[i].a != b[i].a || a[i].b != b[i].b || a[i].kind != b[i].kind || a[i].truceUntil != b[i].truceUntil
|
||||||
|
|| std::fabs(a[i].attitude - b[i].attitude) > 1e-12) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Run the full civ+diplomacy sequence into p; return via out-params what was observed.
|
||||||
|
static void runWorld(Planet& p, int& maxAllies, int& maxRivals, bool& alliedAtWar, bool& sawTruce,
|
||||||
|
int& joinEvents, int& maxSimulWars) {
|
||||||
|
const double yearH = p.cfg.dayLengthHours * p.cfg.yearLengthDays;
|
||||||
|
p.placeSettlements();
|
||||||
|
double lt = 0.0; for (int yr = 0; yr < 600; ++yr) { lt += 2.0 * yearH; p.stepCivilization(2.0 * yearH, lt); }
|
||||||
|
livelyDiplo(p);
|
||||||
|
maxAllies = maxRivals = joinEvents = maxSimulWars = 0; alliedAtWar = false; sawTruce = false;
|
||||||
|
for (long yr = 0; yr < 400; ++yr) {
|
||||||
|
p.computeTerritory(); p.computeCultures();
|
||||||
|
ConflictUpdate u = p.stepConflict(yr);
|
||||||
|
for (const WarEvent& e : u.events) if (e.title.find("joins the war") != std::string::npos) ++joinEvents;
|
||||||
|
int nA = 0, nR = 0;
|
||||||
|
for (const DiploTie& t : p.diploList()) {
|
||||||
|
if (t.kind == DiploKind::Alliance) ++nA; else if (t.kind == DiploKind::Rival) ++nR;
|
||||||
|
if (t.truceUntil > yr) sawTruce = true;
|
||||||
|
}
|
||||||
|
maxAllies = std::max(maxAllies, nA); maxRivals = std::max(maxRivals, nR);
|
||||||
|
// Allies must never be in an active war with each other.
|
||||||
|
for (const War& w : p.warList()) {
|
||||||
|
int lo = std::min(w.attacker, w.defender), hi = std::max(w.attacker, w.defender);
|
||||||
|
for (const DiploTie& t : p.diploList())
|
||||||
|
if (t.a == lo && t.b == hi && t.kind == DiploKind::Alliance) alliedAtWar = true;
|
||||||
|
}
|
||||||
|
// Any realm fighting on >1 front this year (coalitions produce this).
|
||||||
|
for (size_t ni = 0; ni < p.nationList().size(); ++ni) {
|
||||||
|
int cap = p.nationList()[ni].capital, cnt = 0;
|
||||||
|
for (const War& w : p.warList()) if (w.attacker == cap || w.defender == cap) ++cnt;
|
||||||
|
maxSimulWars = std::max(maxSimulWars, cnt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.computeTerritory();
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Diplomacy: alliances, rivalries, allies-at-peace, truces, coalitions\n");
|
||||||
|
int maxAllies, maxRivals, joinEvents, maxSimulWars; bool alliedAtWar, sawTruce;
|
||||||
|
runWorld(p, maxAllies, maxRivals, alliedAtWar, sawTruce, joinEvents, maxSimulWars);
|
||||||
|
std::printf(" peak alliances %d ; peak rivalries %d ; truces seen %s ; join-events %d ; max fronts for a realm %d\n",
|
||||||
|
maxAllies, maxRivals, sawTruce ? "yes" : "no", joinEvents, maxSimulWars);
|
||||||
|
check(maxAllies >= 1, "alliances form between realms");
|
||||||
|
check(maxRivals >= 1, "rivalries form between realms");
|
||||||
|
check(!alliedAtWar, "allies are never at war with each other");
|
||||||
|
check(sawTruce, "a war ends in a truce (no immediate re-declaration)");
|
||||||
|
check(joinEvents > 0 || maxSimulWars >= 2, "coalitions form (an ally joins / a realm fights multiple fronts)");
|
||||||
|
|
||||||
|
std::printf("Diplomacy: determinism\n");
|
||||||
|
Planet q; q.generate(cfg); settle(q); drift(q, 400);
|
||||||
|
int a2, r2, j2, m2; bool aw2, tr2; runWorld(q, a2, r2, aw2, tr2, j2, m2);
|
||||||
|
check(tiesEqual(q.diploList(), p.diploList()), "diplomacy is deterministic (identical ties)");
|
||||||
|
|
||||||
|
std::printf("Diplomacy: RNG isolation from tectonics\n");
|
||||||
|
Planet x; x.generate(cfg); settle(x);
|
||||||
|
Planet y; y.generate(cfg); settle(y);
|
||||||
|
for (int k = 0; k < 40; ++k) {
|
||||||
|
double dx = x.cflDtMy(); x.advect(dx); x.step(); x.erode(dx);
|
||||||
|
double dy = y.cflDtMy(); y.advect(dy); y.step(); y.erode(dy);
|
||||||
|
if (k == 20) { y.placeSettlements(); y.stepCivilization(yearH, yearH); y.computeTerritory(); y.computeCultures(); livelyDiplo(y); y.stepConflict(0); }
|
||||||
|
}
|
||||||
|
{
|
||||||
|
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, "diplomacy never perturbs tectonic evolution");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Diplomacy: save v22 round-trip\n");
|
||||||
|
{
|
||||||
|
std::stringstream ss(std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
p.writeState(ss);
|
||||||
|
Planet r;
|
||||||
|
bool ok = r.readState(ss, true, true, true, true, true, true, true, true, true, true, true, true, true);
|
||||||
|
check(ok, "readState accepts the v22 stream");
|
||||||
|
check(tiesEqual(r.diploList(), p.diploList()), "diplomacy survives save/load");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("Diplomacy: step-back snapshot round-trip\n");
|
||||||
|
{
|
||||||
|
WeatherSnapshot snap = p.captureWeather();
|
||||||
|
std::vector<DiploTie> before = p.diploList();
|
||||||
|
p.cfg.diploDriftRate = 1.0;
|
||||||
|
for (long yr = 400; yr < 415; ++yr) { p.computeTerritory(); p.computeCultures(); p.stepConflict(yr); }
|
||||||
|
bool changed = !tiesEqual(p.diploList(), before);
|
||||||
|
p.restoreWeather(snap);
|
||||||
|
check(changed, "the mutation actually changed the diplomacy state");
|
||||||
|
check(tiesEqual(p.diploList(), before), "restoreWeather rewinds the diplomacy (step-back)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL DIPLOMACY CHECKS PASSED\n", failures);
|
||||||
|
return failures ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user