Give realms independent names, gate colonization on culture, label culture-less realms
1. Realm names no longer literally reuse the capital settlement's own name
("Kingdom of X" where X == the city). Kingdoms/empires now get an
independently generated "place name" (namegen::makeName, keyed on the
capital settlement's permanent id + its own language bank, deterministic
and stable across yearly recomputes) so a realm and the city it's ruled
from read as two distinct places, same as any real one.
2. stepColonization() now skips a realm with no assigned culture
(nat.cultureId < 0) -- nothing to pass on to a new colony, and it stops a
culture-less realm from propagating that gap to its colonies.
3. Realm labels (3D, 2D, and the atlas export) now always show a culture
line -- either the real "(the Velmar)" or an explicit "(cultureless)" in
a muted colour, instead of silently omitting the line when
Nation.cultureId is unset. Makes a genuinely culture-less realm (can
still arise from conquest/schism edge cases) a readable, intentional-
looking state rather than a gap that looks like a rendering bug.
test_culture.cpp updated to rebuild the new independent name deterministically
(mirroring PlanetCulture.cpp's private name-generation formula) instead of
asserting the old capital-name-based format, plus a new assertion that a
realm's place name differs from its capital's own name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
This commit is contained in:
parent
f18ff8f254
commit
af7b6146fd
@ -485,16 +485,16 @@ void Viewer::drawMapOverlays(Rectangle vr, double lonOffset, float scale, float
|
||||
int w = MeasureText(nat.name.c_str(), font);
|
||||
DrawText(nat.name.c_str(), (int)lp.x - w / 2 + 1, nameFillY + 1, font, Color{0, 0, 0, 205});
|
||||
DrawText(nat.name.c_str(), (int)lp.x - w / 2, nameFillY, font, Color{245, 235, 210, 255});
|
||||
if (nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size()) {
|
||||
const std::string& cname = planet.cultureList()[nat.cultureId].name;
|
||||
if (!cname.empty()) {
|
||||
std::string ctext = "(" + cname + ")";
|
||||
int cw = MeasureText(ctext.c_str(), cultureFont);
|
||||
int cFillY = (int)lp.y - cultureFont - 8;
|
||||
DrawText(ctext.c_str(), (int)lp.x - cw / 2 + 1, cFillY + 1, cultureFont, Color{0, 0, 0, 205});
|
||||
DrawText(ctext.c_str(), (int)lp.x - cw / 2, cFillY, cultureFont, Color{215, 205, 180, 235});
|
||||
}
|
||||
}
|
||||
// Always show the culture line, even when the realm has none yet -- "(cultureless)" makes
|
||||
// that an explicit, readable state rather than an unlabeled gap that looks like a bug.
|
||||
bool hasCulture = nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size()
|
||||
&& !planet.cultureList()[nat.cultureId].name.empty();
|
||||
std::string ctext = hasCulture ? "(" + planet.cultureList()[nat.cultureId].name + ")" : "(cultureless)";
|
||||
Color ccol = hasCulture ? Color{215, 205, 180, 235} : Color{140, 140, 150, 200};
|
||||
int cw = MeasureText(ctext.c_str(), cultureFont);
|
||||
int cFillY = (int)lp.y - cultureFont - 8;
|
||||
DrawText(ctext.c_str(), (int)lp.x - cw / 2 + 1, cFillY + 1, cultureFont, Color{0, 0, 0, 205});
|
||||
DrawText(ctext.c_str(), (int)lp.x - cw / 2, cFillY, cultureFont, ccol);
|
||||
}
|
||||
}
|
||||
// Place-name labels (the atlas). Minor features only when the map is zoomed in (on-screen) or
|
||||
@ -640,22 +640,22 @@ void Viewer::exportAtlasImage() {
|
||||
double lon, lat; dirToLonLat(planet.cells[cell].unit, lon, lat);
|
||||
Vector2 lp = projLonLat(lon, lat, mapLon, er);
|
||||
float cultureFont = std::max(8.0f, std::round(font * 0.72f));
|
||||
// Always give the realm a culture line, even when it has none yet -- "(cultureless)"
|
||||
// makes that an explicit, readable state (e.g. a very young realm) rather than an
|
||||
// unlabeled gap that looks like a bug.
|
||||
bool hasCulture = nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size()
|
||||
&& !planet.cultureList()[nat.cultureId].name.empty();
|
||||
std::string ctext = hasCulture ? "(" + planet.cultureList()[nat.cultureId].name + ")" : "(cultureless)";
|
||||
Color ccol = hasCulture ? Color{215, 205, 180, 235} : Color{140, 140, 150, 200};
|
||||
float stackTop = lp.y - settleFontMax * 0.5f - pad; // top edge of the settlement's own box
|
||||
float cultureCenterY = stackTop - pad - cultureFont * 0.5f;
|
||||
float realmCenterY = (hasCulture ? cultureCenterY - cultureFont * 0.5f - pad : stackTop - pad)
|
||||
- pad - font * 0.5f;
|
||||
float realmCenterY = cultureCenterY - cultureFont * 0.5f - pad - pad - font * 0.5f;
|
||||
labels.push_back({ Vector2{lp.x, realmCenterY}, nat.name, font, Color{245, 235, 210, 255},
|
||||
prio, (float)nat.totalPop, false });
|
||||
// The realm's dominant culture, as a smaller "(the Velmar)" label right above the realm
|
||||
// name -- a lower-priority candidate in the same greedy pass, so on a dense world it
|
||||
// simply drops out on its own if it doesn't fit, without any special-case logic.
|
||||
if (hasCulture) {
|
||||
const std::string& cname = planet.cultureList()[nat.cultureId].name;
|
||||
labels.push_back({ Vector2{lp.x, cultureCenterY}, "(" + cname + ")", (int)cultureFont,
|
||||
Color{215, 205, 180, 235}, prio + 1, (float)nat.totalPop, false });
|
||||
}
|
||||
// A lower-priority candidate in the same greedy pass, so on a dense world it simply drops
|
||||
// out on its own if it doesn't fit, without any special-case logic.
|
||||
labels.push_back({ Vector2{lp.x, cultureCenterY}, ctext, (int)cultureFont, ccol,
|
||||
prio + 1, (float)nat.totalPop, false });
|
||||
}
|
||||
}
|
||||
if (!planet.settlements.empty()) {
|
||||
@ -1292,16 +1292,16 @@ void Viewer::renderFrame() {
|
||||
int w = MeasureText(nat.name.c_str(), font);
|
||||
DrawText(nat.name.c_str(), (int)sx - w / 2 + 1, nameFillY + 1, font, Color{0, 0, 0, 205});
|
||||
DrawText(nat.name.c_str(), (int)sx - w / 2, nameFillY, font, Color{245, 235, 210, 255});
|
||||
if (nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size()) {
|
||||
const std::string& cname = planet.cultureList()[nat.cultureId].name;
|
||||
if (!cname.empty()) {
|
||||
std::string ctext = "(" + cname + ")";
|
||||
int cw = MeasureText(ctext.c_str(), cultureFont);
|
||||
int cFillY = (int)sy - cultureFont - 8;
|
||||
DrawText(ctext.c_str(), (int)sx - cw / 2 + 1, cFillY + 1, cultureFont, Color{0, 0, 0, 205});
|
||||
DrawText(ctext.c_str(), (int)sx - cw / 2, cFillY, cultureFont, Color{215, 205, 180, 235});
|
||||
}
|
||||
}
|
||||
// Always show the culture line, even when the realm has none yet -- "(cultureless)" makes
|
||||
// that an explicit, readable state rather than an unlabeled gap that looks like a bug.
|
||||
bool hasCulture = nat.cultureId >= 0 && nat.cultureId < (int)planet.cultureList().size()
|
||||
&& !planet.cultureList()[nat.cultureId].name.empty();
|
||||
std::string ctext = hasCulture ? "(" + planet.cultureList()[nat.cultureId].name + ")" : "(cultureless)";
|
||||
Color ccol = hasCulture ? Color{215, 205, 180, 235} : Color{140, 140, 150, 200};
|
||||
int cw = MeasureText(ctext.c_str(), cultureFont);
|
||||
int cFillY = (int)sy - cultureFont - 8;
|
||||
DrawText(ctext.c_str(), (int)sx - cw / 2 + 1, cFillY + 1, cultureFont, Color{0, 0, 0, 205});
|
||||
DrawText(ctext.c_str(), (int)sx - cw / 2, cFillY, cultureFont, ccol);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -306,6 +306,7 @@ std::vector<WarEvent> Planet::stepColonization(long year) {
|
||||
if ((int)settlements.size() >= totalCap) break;
|
||||
const Nation& nat = nations[ni];
|
||||
if (nat.totalPop < cfg.civColonyMinPop) continue; // only sizeable realms (kingdoms+)
|
||||
if (nat.cultureId < 0) continue; // a culture-less realm doesn't found colonies (nothing to pass on)
|
||||
int cap = nat.capital; if (!alive(cap)) continue;
|
||||
// Deterministic per-realm yearly gate.
|
||||
if (civHashf((uint32_t)cap * 2654435761u ^ (uint32_t)year * 40503u ^ seed ^ 0xC0107Eu) >= cfg.civColonizeRate) continue;
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
// --- Civilization Step 4 + Step 8: cultures, beliefs, governments & cultural evolution --------------
|
||||
// Settlements share a CULTURE (a people/language family with an environment-driven ethos + a religion);
|
||||
@ -229,7 +230,15 @@ void Planet::computeCultures() {
|
||||
}
|
||||
|
||||
// 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. The realm's "place name" is independently generated
|
||||
// (NOT the capital settlement's own name) -- a kingdom and the city it's ruled from read as two
|
||||
// distinct places, same as any real one. Deterministic: keyed on the capital SETTLEMENT's
|
||||
// permanent id (stable even as territory/nations get rebuilt from scratch every year -- unlike
|
||||
// Nation.id, which is just this pass's insertion order and would make the name flicker) and its
|
||||
// own language bank, with a different hash salt than the settlement's own name so the two only
|
||||
// rarely coincide; deduped against other realms named this same pass, same retry pattern as
|
||||
// settlement/colony naming elsewhere in this codebase.
|
||||
std::unordered_set<std::string> usedRealmPlaces;
|
||||
for (Nation& nat : nations) {
|
||||
nat.cultureId = (nat.capital >= 0 && nat.capital < (int)sSettleCulture.size())
|
||||
? sSettleCulture[nat.capital] : -1;
|
||||
@ -239,16 +248,24 @@ void Planet::computeCultures() {
|
||||
case NationTier::Kingdom: nat.gov = (gh == 0) ? GovType::Kingdom : (gh == 1) ? GovType::Duchy : GovType::Theocracy; break;
|
||||
case NationTier::Empire: nat.gov = (gh == 0) ? GovType::Empire : (gh == 1) ? GovType::Autocracy: GovType::Confederation; break;
|
||||
}
|
||||
const std::string& capName = settlements[nat.capital].name;
|
||||
uint32_t capId = (nat.capital >= 0 && nat.capital < (int)settlements.size())
|
||||
? settlements[nat.capital].id : 0u;
|
||||
int capBank = (nat.capital >= 0 && nat.capital < (int)settlements.size())
|
||||
? settlements[nat.capital].bank : 0;
|
||||
uint32_t placeSeed = seed ^ cultHash(capId * 2654435761u ^ 0x5EA1C1Cu);
|
||||
std::string place = namegen::makeName(placeSeed, capBank);
|
||||
for (int g = 0; usedRealmPlaces.count(place) && g < 128; ++g)
|
||||
place = namegen::makeName(placeSeed += 0x9E3779B9u, capBank);
|
||||
usedRealmPlaces.insert(place);
|
||||
switch (nat.gov) {
|
||||
case GovType::Tribe: nat.name = "Chiefdom of " + capName; break;
|
||||
case GovType::CityRepublic: nat.name = "Republic of " + capName; break;
|
||||
case GovType::Duchy: nat.name = "Duchy of " + capName; break;
|
||||
case GovType::Kingdom: nat.name = "Kingdom of " + capName; break;
|
||||
case GovType::Theocracy: nat.name = capName + " Theocracy"; break;
|
||||
case GovType::Confederation: nat.name = capName + " Confederation"; break;
|
||||
case GovType::Empire: nat.name = capName + " Empire"; break;
|
||||
case GovType::Autocracy: nat.name = capName + " Dominion"; break;
|
||||
case GovType::Tribe: nat.name = "Chiefdom of " + place; break;
|
||||
case GovType::CityRepublic: nat.name = "Republic of " + place; break;
|
||||
case GovType::Duchy: nat.name = "Duchy of " + place; break;
|
||||
case GovType::Kingdom: nat.name = "Kingdom of " + place; break;
|
||||
case GovType::Theocracy: nat.name = place + " Theocracy"; break;
|
||||
case GovType::Confederation: nat.name = place + " Confederation"; break;
|
||||
case GovType::Empire: nat.name = place + " Empire"; break;
|
||||
case GovType::Autocracy: nat.name = place + " Dominion"; break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -380,6 +397,8 @@ std::vector<WarEvent> Planet::stepCulture(long year) {
|
||||
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;
|
||||
int sc = settlements[s].cell;
|
||||
if (sc >= 0 && sc < n && (cells[sc].editLock & LockCulture)) continue;
|
||||
double d = ang(unitOf(s), centroid);
|
||||
if (d > cfg.cultSchismRange) {
|
||||
distant.push_back((int)s);
|
||||
|
||||
@ -14,12 +14,14 @@
|
||||
// determinism + RNG isolation; save->load->recompute parity.
|
||||
|
||||
#include "Planet.hpp"
|
||||
#include "NameGen.hpp"
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
|
||||
static int failures = 0;
|
||||
static void check(bool cond, const char* what) {
|
||||
@ -99,25 +101,40 @@ int main() {
|
||||
|
||||
std::printf("Culture: governments plausible per tier + reflected in realm name\n");
|
||||
{
|
||||
bool govOk = true, nameOk = true, cultOk = true;
|
||||
// Mirrors PlanetCulture.cpp's private cultHash() exactly, to rebuild the independently
|
||||
// generated realm "place name" (deliberately NOT the capital settlement's own name -- a
|
||||
// kingdom and the city it's ruled from read as two distinct places).
|
||||
auto cultHash = [](uint32_t a) -> uint32_t { a ^= a << 13; a ^= a >> 17; a ^= a << 5; return a ? a : 1u; };
|
||||
bool govOk = true, nameOk = true, cultOk = true, distinctOk = true;
|
||||
std::unordered_set<std::string> usedRealmPlaces;
|
||||
const uint32_t seed = p.cfg.seed ? p.cfg.seed : 1u;
|
||||
for (const Nation& nat : p.nationList()) {
|
||||
bool tierOk =
|
||||
(nat.tier == NationTier::CityState && (nat.gov == GovType::CityRepublic || nat.gov == GovType::Tribe || nat.gov == GovType::Theocracy)) ||
|
||||
(nat.tier == NationTier::Kingdom && (nat.gov == GovType::Kingdom || nat.gov == GovType::Duchy || nat.gov == GovType::Theocracy)) ||
|
||||
(nat.tier == NationTier::Empire && (nat.gov == GovType::Empire || nat.gov == GovType::Autocracy || nat.gov == GovType::Confederation));
|
||||
if (!tierOk) govOk = false;
|
||||
// Rebuild the expected government-aware name and compare.
|
||||
// Rebuild the expected government-aware name (independent place name, not the capital's
|
||||
// own) and compare.
|
||||
const std::string& cap = p.settlements[nat.capital].name;
|
||||
uint32_t capId = p.settlements[nat.capital].id;
|
||||
int capBank = p.settlements[nat.capital].bank;
|
||||
uint32_t placeSeed = seed ^ cultHash(capId * 2654435761u ^ 0x5EA1C1Cu);
|
||||
std::string place = namegen::makeName(placeSeed, capBank);
|
||||
for (int g = 0; usedRealmPlaces.count(place) && g < 128; ++g)
|
||||
place = namegen::makeName(placeSeed += 0x9E3779B9u, capBank);
|
||||
usedRealmPlaces.insert(place);
|
||||
if (place == cap) distinctOk = false; // extremely unlikely, but the realm name must read as its own place
|
||||
std::string exp;
|
||||
switch (nat.gov) {
|
||||
case GovType::Tribe: exp = "Chiefdom of " + cap; break;
|
||||
case GovType::CityRepublic: exp = "Republic of " + cap; break;
|
||||
case GovType::Duchy: exp = "Duchy of " + cap; break;
|
||||
case GovType::Kingdom: exp = "Kingdom of " + cap; break;
|
||||
case GovType::Theocracy: exp = cap + " Theocracy"; break;
|
||||
case GovType::Confederation: exp = cap + " Confederation"; break;
|
||||
case GovType::Empire: exp = cap + " Empire"; break;
|
||||
case GovType::Autocracy: exp = cap + " Dominion"; break;
|
||||
case GovType::Tribe: exp = "Chiefdom of " + place; break;
|
||||
case GovType::CityRepublic: exp = "Republic of " + place; break;
|
||||
case GovType::Duchy: exp = "Duchy of " + place; break;
|
||||
case GovType::Kingdom: exp = "Kingdom of " + place; break;
|
||||
case GovType::Theocracy: exp = place + " Theocracy"; break;
|
||||
case GovType::Confederation: exp = place + " Confederation"; break;
|
||||
case GovType::Empire: exp = place + " Empire"; break;
|
||||
case GovType::Autocracy: exp = place + " Dominion"; break;
|
||||
}
|
||||
if (nat.name != exp) nameOk = false;
|
||||
if (nat.cultureId < 0 || nat.cultureId >= (int)p.cultureList().size()) cultOk = false;
|
||||
@ -125,6 +142,7 @@ int main() {
|
||||
check(govOk, "each realm's government fits its tier");
|
||||
check(nameOk, "each realm name reflects its government");
|
||||
check(cultOk, "each realm is tagged with a valid culture");
|
||||
check(distinctOk, "each realm's place name differs from its capital's own name");
|
||||
}
|
||||
|
||||
std::printf("Culture: realms are mono-cultural (no realm spans two continents)\n");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user