Geography polish: ocean basins, unique name roots, name new islands
Feedback fixes on the just-shipped atlas:
- Ocean basins: the connected world ocean was always one feature ("1 ocean",
unrealistic). Now generateGeography partitions it via a distance-from-land
watershed -- greedy farthest-first deep-water centres (geoOceanSepRadians apart,
>= geoOceanDeep rings from land) + multi-source BFS Voronoi over the ocean
graph. Each basin -> a named Ocean (or Sea if small). Defaults (sep 1.4 rad,
deep 4) give ~4-6 oceans on an Earth-like world, 1 on a waterworld. New geo*
knobs geoOceanSepRadians / geoOceanDeep.
- Shared names: features now dedupe on the PROPER-NOUN root (not the formatted
string), so a continent "Karn", a "Karn River" and "Karn Mountains" can't
coexist. NameGen also avoids identical adjacent syllables ("shio-shio").
- New islands: Planet::nameNewLand(cell) adds a volcanic island to the atlas on
the fly when it breaches the sea -- joins an adjacent landmass or mints a fresh
unique Island name; the island-formation WorldEvent now carries that name.
test_geography adds: proper-noun-root uniqueness, nameNewLand join vs mint.
All 8 headless suites pass; GUI build clean. Docs updated. (Save still v17 --
GeoFeature layout unchanged.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a4a06996fa
commit
8ae2f14bae
4
BUILD.md
4
BUILD.md
@ -268,7 +268,9 @@ terrain (continents, islands, oceans, seas, lakes, mountain ranges, peaks, river
|
||||
once on a settled world, saved v17. The foundation of the civilization arc.
|
||||
|
||||
geoContinentMinCells 40 land component >= this many cells = Continent (else Island)
|
||||
geoSeaMaxCells 60 ocean component <= this many cells = Sea (else Ocean)
|
||||
geoSeaMaxCells 60 ocean basin <= this many cells = Sea (else Ocean)
|
||||
geoOceanSepRadians 1.4 min angular gap between ocean-basin centres (higher = fewer oceans)
|
||||
geoOceanDeep 4 min rings from land for a cell to seed an ocean basin
|
||||
geoMountainElev 2500 m min elevation for a mountain-range cell
|
||||
geoRangeMinCells 4 min cells for a named mountain range
|
||||
geoRiverMinDischarge 80 min mouth discharge for a named river
|
||||
|
||||
21
CLAUDE.md
21
CLAUDE.md
@ -112,14 +112,17 @@ on the Live World clock). **Step 1 of the roadmap is done:**
|
||||
- **Geography & place-names (the atlas)** *(done — see `PlanetGeography.cpp` + `NameGen.cpp`)* — the
|
||||
foundation everything civic references. `Planet::generateGeography()` extracts named features from
|
||||
the (frozen) terrain by connectivity over the fixed grid — **continents/islands** (connected land),
|
||||
**oceans/seas** (connected water), **lakes** (inland filled basins), **mountain ranges + peaks**
|
||||
(connected high terrain), **rivers** (largest discharge mouths traced upstream via `flowTo`) — and
|
||||
**oceans** (the connected world ocean split into basins by a distance-from-land watershed → ~4–6
|
||||
named oceans) **/ seas** (small water bodies), **lakes** (inland filled basins), **mountain ranges +
|
||||
peaks** (connected high terrain), **rivers** (largest discharge mouths traced upstream via `flowTo`) — and
|
||||
names each with a deterministic procedural namer (`NameGen`: syllable banks, a "language" per
|
||||
continent so a region's places share a sound). A separate RNG (`sGeoRng`) keeps tectonic determinism
|
||||
intact. Generated once on a settled world (key `M`, in or out of Live World), drawn as labels on the
|
||||
globe + 2D map (minor features only when zoomed), listed in an **Atlas** tab (click a row to fly
|
||||
there), and shown in cell-info as a "region" line. Per-cell feature-index arrays give O(1) "which
|
||||
features is this cell in" (the hook for territory/borders later). Saved (**v17**). Knobs `geo*`.
|
||||
features is this cell in" (the hook for territory/borders later). Names dedupe on the proper-noun
|
||||
root (no two features share a base name); a **new volcanic island** is named on the fly when it
|
||||
breaches (the island-formation event carries its name). Saved (**v17**). Knobs `geo*`.
|
||||
*Next steps (not yet built): settlements + food/habitability, territory + borders, culture +
|
||||
beliefs, conflict + diplomacy.*
|
||||
|
||||
@ -812,11 +815,17 @@ triangles (plates are fixed in phase 1).
|
||||
`volcanoAshCooling` (6 °C). Marker sizes/colours are render constants (ViewerRender.cpp).
|
||||
- **Geography / atlas (`geo*` in PlanetConfig / `planet.cfg`):** feature-extraction thresholds —
|
||||
`geoContinentMinCells` (40, land component ≥ this = Continent, else Island), `geoSeaMaxCells`
|
||||
(60, ocean component ≤ this = Sea, else Ocean), `geoMountainElev` (2500 m, min elevation for a
|
||||
(60, ocean **basin** ≤ this = Sea, else Ocean), `geoMountainElev` (2500 m, min elevation for a
|
||||
mountain-range cell), `geoRangeMinCells` (4, min cells for a named range), `geoRiverMinDischarge`
|
||||
(80, min mouth discharge for a named river), and the label-clutter caps `geoMaxRivers` (40) /
|
||||
`geoMaxPeaks` (40, largest/highest kept). Name flavour (syllable banks, a "language" per continent)
|
||||
+ label fonts/colours are constants in NameGen.cpp / ViewerRender.cpp, not config.
|
||||
`geoMaxPeaks` (40, largest/highest kept). **Ocean basins** — the connected world ocean is split
|
||||
into several named oceans by a distance-from-land watershed: `geoOceanSepRadians` (1.40 rad, min
|
||||
angular gap between basin centres — *raise → fewer oceans*, lower → more) and `geoOceanDeep` (4, min
|
||||
rings from land for a cell to seed a basin); defaults give ~4–6 oceans on an Earth-like world, 1 on a
|
||||
waterworld. Feature names are deduped on the **proper-noun root** (no shared roots across kinds), and
|
||||
a new volcanic island is named on the fly (`Planet::nameNewLand`, joins an adjacent landmass or mints
|
||||
a fresh Island). Name flavour (syllable banks, a "language" per continent) + label fonts/colours are
|
||||
constants in NameGen.cpp / ViewerRender.cpp, not config.
|
||||
- `upliftGain` (PlanetConfig) — m/tick per unit convergence stress; main
|
||||
knob for how fast/high relief builds.
|
||||
- `relax` (PlanetConfig) — isostatic relaxation toward base elevation. Peaks
|
||||
|
||||
@ -305,7 +305,10 @@ Save **v16** appends the event log; pre-v16 saves load with an empty journal.
|
||||
civilization arc: name the world so everything civic can reference it. `Planet::generateGeography()`
|
||||
extracts geographic features from the frozen terrain purely by **connectivity over the fixed grid**
|
||||
(the same flood-fill idiom as `coalesceBabyPlates` / the enclosed-sea fill): connected land →
|
||||
**Continent** (≥ `geoContinentMinCells`) or **Island**; connected water → **Ocean** or **Sea** (≤
|
||||
**Continent** (≥ `geoContinentMinCells`) or **Island**; the connected world ocean is split into
|
||||
**basins** (a single connected body reads wrong as one name) by a distance-from-land watershed —
|
||||
greedy farthest-first deep-water **centres** (`geoOceanSepRadians` apart, ≥ `geoOceanDeep` rings from
|
||||
land) then multi-source BFS Voronoi over the ocean graph — each basin → an **Ocean** (or **Sea** if ≤
|
||||
`geoSeaMaxCells`); inland filled basins (`lakeDepth`) → **Lake**; connected `> geoMountainElev` land →
|
||||
**MountainRange** + its highest cell as a **Peak**; the largest `discharge` mouths traced upstream via
|
||||
`flowTo` → **River**. It first calls `computeHydrology()` (routing only — no elevation change) so the
|
||||
@ -323,6 +326,10 @@ names as labels on the globe (the plate-label manual projection) + 2D map (minor
|
||||
zoomed, to declutter), lists them in an **Atlas** tab (5th live-info tab; click a row → `focusCell`),
|
||||
and adds a "region" line to cell-info. Save v17 appends the feature records (with `std::string` names,
|
||||
written field-by-field) + the POD index arrays; pre-v17 saves load with none and regenerate on demand.
|
||||
Names dedupe on the **proper-noun root** (not the formatted string), so a continent, its river and its
|
||||
mountains can't share a base name. New land created during Live World (a volcanic island breaching the
|
||||
sea) is added to the atlas on the fly by `Planet::nameNewLand(cell)` — it joins an adjacent existing
|
||||
landmass or mints a fresh unique Island name, which the island-formation `WorldEvent` then carries.
|
||||
|
||||
## Headless testing
|
||||
|
||||
|
||||
@ -316,7 +316,11 @@ void Viewer::detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
||||
double oldElev = old->baseElev + old->built;
|
||||
double newElev = v.baseElev + v.built;
|
||||
if (old->submarine && oldElev <= planet.cfg.seaLevel && newElev > planet.cfg.seaLevel) {
|
||||
appendEvent(2, 1, liveTime, v.cell, v.id, "Volcanic island formed",
|
||||
// Add the new land to the atlas: join an adjacent landmass or mint a fresh Island name.
|
||||
std::string land = planet.nameNewLand(v.cell);
|
||||
std::string title = land.empty() ? std::string("Volcanic island formed")
|
||||
: land + " formed";
|
||||
appendEvent(2, 1, liveTime, v.cell, v.id, title,
|
||||
std::string(TextFormat("%s breached sea level (+%.0f m built)", kind, v.built)));
|
||||
}
|
||||
if (old->phase != 1 && v.phase == 1) {
|
||||
|
||||
@ -58,13 +58,17 @@ std::string makeName(uint32_t seed, int bank, int minSyl, int maxSyl) {
|
||||
uint32_t s = seed ? seed : 0xA5A5A5A5u;
|
||||
nx(s); nx(s); // mix the seed before first use
|
||||
int syl = minSyl + (int)(nx(s) % (uint32_t)(maxSyl - minSyl + 1));
|
||||
std::string out;
|
||||
std::string out, prevSyl;
|
||||
for (int i = 0; i < syl; ++i) {
|
||||
out += pick(b.onset, s);
|
||||
out += pick(b.nucleus, s);
|
||||
// Coda more likely on the final syllable; never two empty-onset vowels colliding awkwardly.
|
||||
bool last = (i == syl - 1);
|
||||
if (last || (nx(s) & 3u) == 0u) out += pick(b.coda, s);
|
||||
std::string cur;
|
||||
for (int tries = 0; tries < 4; ++tries) {
|
||||
cur = pick(b.onset, s);
|
||||
cur += pick(b.nucleus, s);
|
||||
if (last || (nx(s) & 3u) == 0u) cur += pick(b.coda, s); // coda likelier on the final syllable
|
||||
if (cur != prevSyl) break; // avoid an identical adjacent syllable ("shio-shio")
|
||||
}
|
||||
out += cur; prevSyl = cur;
|
||||
}
|
||||
// Collapse runs of 3+ identical letters to 2 (kills "oooo" / "lll" -> "oo" / "ll").
|
||||
std::string clean;
|
||||
|
||||
@ -148,6 +148,11 @@ public:
|
||||
// lakes, mountain ranges/peaks, rivers) and names them with a separate RNG (tectonic determinism
|
||||
// intact). Saved (v17). The per-cell index arrays give O(1) "which features is this cell in".
|
||||
void generateGeography();
|
||||
// Name a cell that has just become land (e.g. a volcanic island breaching the sea): join an
|
||||
// adjacent existing landmass, else create + name a new Island. Returns the land feature's name
|
||||
// ("" if the atlas isn't built). Appended to geoFeatures (saved). Called by the viewer on a
|
||||
// volcanic island-formation event so the new island enters the atlas.
|
||||
std::string nameNewLand(int cell);
|
||||
bool geographyBuilt() const { return !geoFeatures.empty(); }
|
||||
const std::vector<GeoFeature>& geography() const { return geoFeatures; }
|
||||
const std::vector<int>& cellLand() const { return sCellLand; } // continent/island feature index (-1)
|
||||
|
||||
@ -40,7 +40,9 @@ void Planet::generateGeography() {
|
||||
sCellLand.assign(n, -1); sCellWater.assign(n, -1);
|
||||
sCellRange.assign(n, -1); sCellRiver.assign(n, -1);
|
||||
|
||||
std::set<std::string> usedNames;
|
||||
// Dedupe on the PROPER NOUN (the base, before the kind suffix), so no two features share a root --
|
||||
// a continent "Karn", a "Karn River" and the "Karn Mountains" would read as the same name.
|
||||
std::set<std::string> usedNouns;
|
||||
auto formatName = [](FeatureKind k, const std::string& p) -> std::string {
|
||||
switch (k) {
|
||||
case FeatureKind::Ocean: return p + " Ocean";
|
||||
@ -60,11 +62,11 @@ void Planet::generateGeography() {
|
||||
return h ? h : 1u;
|
||||
};
|
||||
auto makeFeatureName = [&](FeatureKind k, uint32_t seed, int bank) {
|
||||
std::string nm = formatName(k, namegen::makeName(seed, bank));
|
||||
for (int guard = 0; usedNames.count(nm) && guard < 64; ++guard)
|
||||
nm = formatName(k, namegen::makeName(seed += 0x9E3779B9u, bank));
|
||||
usedNames.insert(nm);
|
||||
return nm;
|
||||
std::string p = namegen::makeName(seed, bank);
|
||||
for (int guard = 0; usedNouns.count(p) && guard < 128; ++guard)
|
||||
p = namegen::makeName(seed += 0x9E3779B9u, bank);
|
||||
usedNouns.insert(p);
|
||||
return formatName(k, p);
|
||||
};
|
||||
auto bankOf = [&](int regionId, int fallbackAnchor) {
|
||||
return regionId >= 0 ? namegen::bankForRegion(cfg.seed, regionId)
|
||||
@ -118,14 +120,63 @@ void Planet::generateGeography() {
|
||||
for (int i : comp) sCellLand[i] = fi;
|
||||
}
|
||||
|
||||
// --- Water bodies -> Ocean / Sea -------------------------------------------------------------
|
||||
auto water = components([&](int i) { return cells[i].elevation <= sea; });
|
||||
std::sort(water.begin(), water.end(), bySizeDesc);
|
||||
for (auto& comp : water) {
|
||||
int anchor = centroidCell(comp);
|
||||
FeatureKind k = (int)comp.size() <= cfg.geoSeaMaxCells ? FeatureKind::Sea : FeatureKind::Ocean;
|
||||
int fi = addFeature(k, anchor, -1, (int)comp.size(), bankOf(-1, anchor));
|
||||
for (int i : comp) sCellWater[i] = fi;
|
||||
// --- Water bodies -> Ocean basins / Seas -----------------------------------------------------
|
||||
// A single connected world ocean is unrealistic as one name (Earth's oceans are arbitrary
|
||||
// subdivisions of one connected body). So partition the ocean into BASINS: distance-from-land
|
||||
// gives each ocean cell its depth-from-coast; pick well-separated deep cells as basin centres
|
||||
// (greedy, farthest-first by angular separation); multi-source BFS over the ocean graph assigns
|
||||
// every ocean cell to its nearest centre. Each basin -> a named Ocean (or Sea if small). On a
|
||||
// one-basin waterworld this yields a single ocean; on an Earth-like world, several.
|
||||
{
|
||||
std::vector<int> dLand(n, -1), frontier, nextf;
|
||||
for (int i = 0; i < n; ++i) if (cells[i].elevation > sea) { dLand[i] = 0; frontier.push_back(i); }
|
||||
for (int r = 1; !frontier.empty(); ++r) {
|
||||
nextf.clear();
|
||||
for (int i : frontier) for (int j : cells[i].neighbors)
|
||||
if (dLand[j] < 0) { dLand[j] = r; nextf.push_back(j); }
|
||||
frontier.swap(nextf);
|
||||
}
|
||||
std::vector<int> oc;
|
||||
for (int i = 0; i < n; ++i) if (cells[i].elevation <= sea) oc.push_back(i);
|
||||
std::sort(oc.begin(), oc.end(), [&](int a, int b) { return dLand[a] > dLand[b]; }); // deepest first
|
||||
std::vector<int> centres;
|
||||
const double sepCos = std::cos(std::max(0.05, cfg.geoOceanSepRadians));
|
||||
for (int i : oc) {
|
||||
if (dLand[i] < cfg.geoOceanDeep) break; // remaining cells are coastal (shallow)
|
||||
bool ok = true;
|
||||
for (int s : centres) if (cells[i].unit.dot(cells[s].unit) > sepCos) { ok = false; break; }
|
||||
if (ok) centres.push_back(i);
|
||||
}
|
||||
std::vector<int> basin(n, -1), bfront, bnext;
|
||||
for (size_t s = 0; s < centres.size(); ++s) { basin[centres[s]] = (int)s; bfront.push_back(centres[s]); }
|
||||
while (!bfront.empty()) { // multi-source Voronoi over the ocean graph
|
||||
bnext.clear();
|
||||
for (int i : bfront) for (int j : cells[i].neighbors)
|
||||
if (cells[j].elevation <= sea && basin[j] < 0) { basin[j] = basin[i]; bnext.push_back(j); }
|
||||
bfront.swap(bnext);
|
||||
}
|
||||
int nextBasin = (int)centres.size();
|
||||
for (int i = 0; i < n; ++i) { // shallow/enclosed water with no deep centre -> its own Sea
|
||||
if (cells[i].elevation > sea || basin[i] >= 0) continue;
|
||||
int b = nextBasin++;
|
||||
std::vector<int> st{ i }; basin[i] = b;
|
||||
while (!st.empty()) {
|
||||
int u = st.back(); st.pop_back();
|
||||
for (int j : cells[u].neighbors)
|
||||
if (cells[j].elevation <= sea && basin[j] < 0) { basin[j] = b; st.push_back(j); }
|
||||
}
|
||||
}
|
||||
std::vector<std::vector<int>> mem(nextBasin);
|
||||
for (int i = 0; i < n; ++i) if (cells[i].elevation <= sea && basin[i] >= 0) mem[basin[i]].push_back(i);
|
||||
std::vector<int> order(nextBasin); for (int b = 0; b < nextBasin; ++b) order[b] = b;
|
||||
std::sort(order.begin(), order.end(), [&](int a, int b) { return mem[a].size() > mem[b].size(); });
|
||||
for (int b : order) {
|
||||
if (mem[b].empty()) continue;
|
||||
int anchor = centroidCell(mem[b]);
|
||||
FeatureKind k = (int)mem[b].size() <= cfg.geoSeaMaxCells ? FeatureKind::Sea : FeatureKind::Ocean;
|
||||
int fi = addFeature(k, anchor, -1, (int)mem[b].size(), bankOf(-1, anchor));
|
||||
for (int i : mem[b]) sCellWater[i] = fi;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Lakes (inland filled basins above sea level) --------------------------------------------
|
||||
@ -194,3 +245,34 @@ void Planet::generateGeography() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Name a cell that has just become land (a volcanic island breaching the sea). Joins an adjacent
|
||||
// existing landmass if there is one; otherwise mints a new Island feature with a unique name. Returns
|
||||
// the land feature's name. Appended to geoFeatures + sCellLand (saved). Uses the per-cell hash, not a
|
||||
// global pass, so it never re-extracts the whole atlas.
|
||||
std::string Planet::nameNewLand(int cell) {
|
||||
if (geoFeatures.empty()) return ""; // no atlas yet -> nothing to extend
|
||||
if (cell < 0 || cell >= (int)cells.size()) return "";
|
||||
if ((int)sCellLand.size() != (int)cells.size()) return "";
|
||||
if (sCellLand[cell] >= 0 && sCellLand[cell] < (int)geoFeatures.size())
|
||||
return geoFeatures[sCellLand[cell]].name; // already part of a named landmass
|
||||
for (int j : cells[cell].neighbors) { // adjacent to existing land -> join it
|
||||
int lf = sCellLand[j];
|
||||
if (lf >= 0 && lf < (int)geoFeatures.size()) { sCellLand[cell] = lf; return geoFeatures[lf].name; }
|
||||
}
|
||||
// Isolated new island: mint a unique name (dedup against existing feature names).
|
||||
int bank = namegen::bankForRegion(cfg.seed, 1000 + cell);
|
||||
uint32_t seed = (cfg.seed ^ 0x6E0C12A7u) + (uint32_t)(cell * 2654435761u)
|
||||
+ (uint32_t)geoFeatures.size() * 40503u;
|
||||
auto inUse = [&](const std::string& nm) {
|
||||
for (const GeoFeature& f : geoFeatures) if (f.name == nm) return true; return false;
|
||||
};
|
||||
std::string nm = namegen::makeName(seed, bank);
|
||||
for (int g = 0; inUse(nm) && g < 128; ++g) nm = namegen::makeName(seed += 0x9E3779B9u, bank);
|
||||
int fi = (int)geoFeatures.size();
|
||||
GeoFeature f; f.id = (uint32_t)fi + 1; f.kind = FeatureKind::Island;
|
||||
f.anchorCell = cell; f.regionId = fi; f.size = 1; f.name = nm;
|
||||
geoFeatures.push_back(std::move(f));
|
||||
sCellLand[cell] = fi;
|
||||
return geoFeatures[fi].name;
|
||||
}
|
||||
|
||||
@ -48,12 +48,13 @@
|
||||
D(volcanoExplodeDropFrac) D(volcanoActivityDecay) D(volcanoDeadActivity) \
|
||||
D(volcanoBlastRadius) D(volcanoBlastCloud) D(volcanoAshMinYears) D(volcanoAshMaxYears) \
|
||||
D(volcanoAshPuffCellsPerWeek) D(volcanoAshCloud) D(volcanoAshCooling) \
|
||||
D(geoMountainElev) D(geoRiverMinDischarge) \
|
||||
D(geoMountainElev) D(geoRiverMinDischarge) D(geoOceanSepRadians) \
|
||||
I(subdivisions) I(plateCount) I(beltWidth) I(splitCheckEvery) I(stalemateWindows) \
|
||||
I(miniPlateCells) I(fuseMinPlates) I(babyMinCells) I(seaLevelEvery) \
|
||||
I(climateWindPasses) I(climateMoistureSmooth) I(seasonContinentRings) I(weatherSystemMax) \
|
||||
I(volcanoMaxCount) \
|
||||
I(geoContinentMinCells) I(geoSeaMaxCells) I(geoRangeMinCells) I(geoMaxRivers) I(geoMaxPeaks) \
|
||||
I(geoOceanDeep) \
|
||||
I(bioFloraSlots) I(bioFaunaSlots) I(bioFungaSlots) \
|
||||
I(bioFloraPoints) I(bioFaunaPoints) I(bioFungaPoints) I(bioMarineCoastRings) \
|
||||
U(seed)
|
||||
@ -246,6 +247,7 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
||||
E(rng(cfg.volcanoAshCooling, 0.0, 40.0, "volcanoAshCooling"));
|
||||
E(rng(cfg.geoMountainElev, 0.0, 12000.0, "geoMountainElev"));
|
||||
E(rng(cfg.geoRiverMinDischarge, 0.0, 1.0e9, "geoRiverMinDischarge"));
|
||||
E(rng(cfg.geoOceanSepRadians, 0.05, 3.14159, "geoOceanSepRadians"));
|
||||
E(irng(cfg.subdivisions, 0, 7, "subdivisions"));
|
||||
E(irng(cfg.plateCount, 1, 100, "plateCount"));
|
||||
E(irng(cfg.beltWidth, 1, 12, "beltWidth"));
|
||||
@ -265,6 +267,7 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
||||
E(irng(cfg.geoRangeMinCells, 1, 1000000, "geoRangeMinCells"));
|
||||
E(irng(cfg.geoMaxRivers, 0, 100000, "geoMaxRivers"));
|
||||
E(irng(cfg.geoMaxPeaks, 0, 100000, "geoMaxPeaks"));
|
||||
E(irng(cfg.geoOceanDeep, 1, 1000, "geoOceanDeep"));
|
||||
E(irng(cfg.bioFloraSlots, 1, 1000, "bioFloraSlots"));
|
||||
E(irng(cfg.bioFaunaSlots, 1, 1000, "bioFaunaSlots"));
|
||||
E(irng(cfg.bioFungaSlots, 1, 1000, "bioFungaSlots"));
|
||||
|
||||
@ -366,7 +366,9 @@ struct PlanetConfig {
|
||||
// on a settled world (key M), saved (v17). Tune to control what counts as a continent vs island,
|
||||
// an ocean vs sea, a named mountain range / major river, and to cap label clutter.
|
||||
int geoContinentMinCells = 40; // land component >= this many cells = Continent (else Island)
|
||||
int geoSeaMaxCells = 60; // ocean component <= this many cells = Sea (else Ocean)
|
||||
int geoSeaMaxCells = 60; // ocean/basin <= this many cells = Sea (else Ocean)
|
||||
double geoOceanSepRadians = 1.40; // min angular separation between ocean-basin centres (radians; ~4-6 oceans)
|
||||
int geoOceanDeep = 4; // min rings from land for a cell to seed an ocean basin
|
||||
double geoMountainElev = 2500.0;// m: min elevation for mountain-range membership
|
||||
int geoRangeMinCells = 4; // min cells for a named mountain range
|
||||
double geoRiverMinDischarge = 80.0; // min mouth discharge for a named river
|
||||
|
||||
@ -121,6 +121,27 @@ int main() {
|
||||
}
|
||||
check(namesOk, "every feature has a unique non-empty name");
|
||||
|
||||
// Proper-noun uniqueness: strip the kind suffix/prefix and assert no two features share a root
|
||||
// (so a continent "Karn", a "Karn River" and "Karn Mountains" can't coexist).
|
||||
{
|
||||
auto strip = [](const GeoFeature& f) -> std::string {
|
||||
const std::string& s = f.name;
|
||||
auto cut = [&](const std::string& suf) { return s.size() > suf.size() && s.compare(s.size() - suf.size(), suf.size(), suf) == 0 ? s.substr(0, s.size() - suf.size()) : s; };
|
||||
switch (f.kind) {
|
||||
case FeatureKind::Ocean: return cut(" Ocean");
|
||||
case FeatureKind::Sea: return cut(" Sea");
|
||||
case FeatureKind::MountainRange: return cut(" Mountains");
|
||||
case FeatureKind::River: return cut(" River");
|
||||
case FeatureKind::Lake: return s.rfind("Lake ", 0) == 0 ? s.substr(5) : s;
|
||||
case FeatureKind::Peak: return s.rfind("Mount ", 0) == 0 ? s.substr(6) : s;
|
||||
default: return s;
|
||||
}
|
||||
};
|
||||
std::set<std::string> roots; bool rootsUnique = true;
|
||||
for (const GeoFeature& f : F) if (!roots.insert(strip(f)).second) rootsUnique = false;
|
||||
check(rootsUnique, "no two features share a proper-noun root");
|
||||
}
|
||||
|
||||
std::printf("Geography: determinism (same seed -> identical atlas)\n");
|
||||
Planet q; q.generate(cfg); settle(q); drift(q, 120); q.generateGeography();
|
||||
bool sameAtlas = (q.geography().size() == F.size());
|
||||
@ -158,6 +179,30 @@ int main() {
|
||||
check(r.cellLand() == p.cellLand() && r.cellRiver() == p.cellRiver(), "per-cell region arrays round-trip");
|
||||
}
|
||||
|
||||
// Naming new land last -- it mutates p's atlas, so it must run after the determinism/save checks.
|
||||
std::printf("Geography: naming a new (volcanic) island\n");
|
||||
{
|
||||
int oceanAdjLand = -1, oceanIsolated = -1;
|
||||
for (int i = 0; i < n && (oceanAdjLand < 0 || oceanIsolated < 0); ++i) {
|
||||
if (p.cells[i].elevation > sea) continue;
|
||||
bool nearLand = false; for (int j : p.cells[i].neighbors) if (p.cells[j].elevation > sea) nearLand = true;
|
||||
if (nearLand && oceanAdjLand < 0) oceanAdjLand = i;
|
||||
if (!nearLand && oceanIsolated < 0) oceanIsolated = i;
|
||||
}
|
||||
if (oceanAdjLand >= 0) {
|
||||
size_t before = p.geography().size();
|
||||
std::string nm = p.nameNewLand(oceanAdjLand);
|
||||
check(!nm.empty() && p.cellLand()[oceanAdjLand] >= 0, "coastal new land joins an adjacent named landmass");
|
||||
check(p.geography().size() == before, "joining an existing landmass adds no new feature");
|
||||
}
|
||||
if (oceanIsolated >= 0) {
|
||||
size_t b2 = p.geography().size();
|
||||
std::string nm = p.nameNewLand(oceanIsolated);
|
||||
check(!nm.empty() && p.geography().size() == b2 + 1, "an isolated new island mints a fresh feature");
|
||||
check(p.geography().back().kind == FeatureKind::Island && p.cellLand()[oceanIsolated] >= 0, "the new island is a named Island feature");
|
||||
}
|
||||
}
|
||||
|
||||
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL GEOGRAPHY CHECKS PASSED\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user