diff --git a/BUILD.md b/BUILD.md index e90c99f..e52fd5e 100644 --- a/BUILD.md +++ b/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 diff --git a/CLAUDE.md b/CLAUDE.md index d115e01..1c3c1fc 100644 --- a/CLAUDE.md +++ b/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 diff --git a/docs/design-notes.md b/docs/design-notes.md index 39d5417..10e727a 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -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 diff --git a/src/render/Viewer.cpp b/src/render/Viewer.cpp index 4a878b5..6e0e73d 100644 --- a/src/render/Viewer.cpp +++ b/src/render/Viewer.cpp @@ -316,7 +316,11 @@ void Viewer::detectLiveEvents(const std::vector& 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) { diff --git a/src/sim/NameGen.cpp b/src/sim/NameGen.cpp index a693e3a..16b8f38 100644 --- a/src/sim/NameGen.cpp +++ b/src/sim/NameGen.cpp @@ -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; diff --git a/src/sim/Planet.hpp b/src/sim/Planet.hpp index f6cc121..7b3372a 100644 --- a/src/sim/Planet.hpp +++ b/src/sim/Planet.hpp @@ -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& geography() const { return geoFeatures; } const std::vector& cellLand() const { return sCellLand; } // continent/island feature index (-1) diff --git a/src/sim/PlanetGeography.cpp b/src/sim/PlanetGeography.cpp index bdf9551..f18f475 100644 --- a/src/sim/PlanetGeography.cpp +++ b/src/sim/PlanetGeography.cpp @@ -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 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 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 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 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 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 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 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> 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 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; +} diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 0486d3b..995693b 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -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")); diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index 17ef1e4..8465fe6 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -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 diff --git a/test_geography.cpp b/test_geography.cpp index 0467e84..4e9244e 100644 --- a/test_geography.cpp +++ b/test_geography.cpp @@ -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 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; }