From c32a3b177c74933d8ef6917b799b009c1927ae89 Mon Sep 17 00:00:00 2001 From: beezm Date: Sat, 22 Aug 2026 03:50:33 -0400 Subject: [PATCH] =?UTF-8?q?chat2/07:=20the=20region-labeling=20layer=20?= =?UTF-8?q?=E2=80=94=20label=20all=20land,=20tag=20by=20construction,=20tu?= =?UTF-8?q?nable=20speck=20revert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core/Scripts/RegionLabeling.cs is the shared-infra contract, built to the letter: it runs on the CLASSIFY (raw) field; land is 8-connected, the deliberate complement of water's 4 (a diagonal isthmus joins; the water either side stays separate); a component is a maximal 8-connected set of land cells; the MAINLAND is the component containing the map centre — not merely the largest, which a later fragmentation step could flip — with a flagged fallback to the largest if the centre were ever water (asserted, never needed: oracle m); every other component is an island; per component id / sizeCells / centroid / hemisphere (by centroid, one label per island) / isMainland. Ids come from a fixed scan order and are proven stable across two generations (oracle o, 16.8M cells). It knows nothing about offshore or stamped. Engine-free, in Core as C++-candidate math; the hemisphere convention moved there with it, OffshoreAnalysis aliases it. Tools/Scripts/RegionPass.cs is pass 1c: label, revert, relabel, tag. The island tag (renamed IsIsland; IslandHemisphere from the component's centroid; Pass1Result.Regions carries the whole table) is now a CONSEQUENCE of labeling — every non-mainland component. That is the fix for the chat2/06 overlay, which tagged only what the offshore pass raised: 1063685222 has 11 natural islands including a 94,511-cell detached mass, 20260821 has 19, all grey in 06's tags.png and all coloured now. The offshore pass itself is untouched; its internal Tag stays for its own guards and is no longer exported. The speck revert (TerrainGenConfig.SpeckRevert / MinLandComponentFrac) lowers every non-mainland component below the threshold to the mean of its ring of adjacent sea cells, held strictly below sea. Origin-blind: a natural nub goes the same way as an offshore dot (6 natural components / 273 cells on the bare 1063685222 field at threshold_mid — reported as a3r, informational). Lower-only and component-only are asserted cell by cell in the pass and re-proven on the finished fields by oracle n (mainland bit-identical filter OFF vs ON; every changed cell in a sub-threshold island, lowered below sea); the mainland is never a candidate and its size is asserted unchanged across the revert. A reverted offshore island leaves its submerged skirt as a shoal — not this component, by the rule. Classify/render consistency is by construction (pass 1, curve identity at sea) and asserted by oracle k. Deliberately OFF in the bare TerrainGenConfig for the reason the shelf and islets are: the raw field has natural specks, so default-ON would move the calibration pool and every regression dump; the batch turns it on. Thresholds swept on 8 seeds at 4096 (1e-5 / 3e-5 / 1e-4 of the map = 168 / 503 / 1,678 cells): low removes 0–6 nubs per seed, mid (the config default, equal to the offshore guard) 2–11, high 26–36 — most of the offshore islands, the "fewer, bigger" bookend. The count/size table carries natural / pre / post counts per hemisphere, min/median/mean/max and a log-spaced size histogram — the instrument for the southern-stretch step. Oracle, all passing: a1, a3, a4 (8192, 67M cells) with labeling ON + revert OFF; a6 NEW — labeling ON + revert OFF on the 06 preset bit-identical to the 06 batch's render field (labeling is pure analysis); j0; m, n, o, i, j, k, l, b per field. Batch: BatchRoot(7, "region_labeling") — exactly 4 plates (three thresholds on 1063685222, threshold_mid on 20260821, the table's most-natural-islands seed), each with grayscale / .f32 / relief / the labeled-regions overlay / the tag overlay, plus count_size_table.md/.csv. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7 --- Core/README.md | 7 +- Core/Scripts/RegionLabeling.cs | 229 ++++++++ Core/Scripts/RegionLabeling.cs.uid | 1 + Tools/README.md | 25 +- Tools/Scenes/RegionLabelingTool.tscn | 6 + Tools/Scripts/OffshoreAnalysis.cs | 17 +- Tools/Scripts/OffshoreIslandsTool.cs | 10 +- Tools/Scripts/Pass1Result.cs | 50 +- Tools/Scripts/Pass2Result.cs | 10 +- Tools/Scripts/RegionLabelingTool.cs | 611 +++++++++++++++++++++ Tools/Scripts/RegionLabelingTool.cs.uid | 1 + Tools/Scripts/RegionOverlayRenderer.cs | 77 +++ Tools/Scripts/RegionOverlayRenderer.cs.uid | 1 + Tools/Scripts/RegionPass.cs | 203 +++++++ Tools/Scripts/RegionPass.cs.uid | 1 + Tools/Scripts/Shaping.cs | 8 +- Tools/Scripts/ShapingOracle.cs | 80 ++- Tools/Scripts/TerrainGenConfig.cs | 26 + Tools/Scripts/Topography.cs | 29 +- 19 files changed, 1336 insertions(+), 56 deletions(-) create mode 100644 Core/Scripts/RegionLabeling.cs create mode 100644 Core/Scripts/RegionLabeling.cs.uid create mode 100644 Tools/Scenes/RegionLabelingTool.tscn create mode 100644 Tools/Scripts/RegionLabelingTool.cs create mode 100644 Tools/Scripts/RegionLabelingTool.cs.uid create mode 100644 Tools/Scripts/RegionOverlayRenderer.cs create mode 100644 Tools/Scripts/RegionOverlayRenderer.cs.uid create mode 100644 Tools/Scripts/RegionPass.cs create mode 100644 Tools/Scripts/RegionPass.cs.uid diff --git a/Core/README.md b/Core/README.md index b3e4cc5..8e656df 100644 --- a/Core/README.md +++ b/Core/README.md @@ -37,6 +37,7 @@ resolution and the file-safety rails. Constants and contracts. | `Scripts/CurveKnots.cs` | The six INPUT knots — percentiles of the measured land CDF, plus the reference's for comparison. | | `Scripts/CurveAnchors.cs` | The OUTPUT anchors — the storm-ladder elevations each band lands at. | | `Scripts/TerrainDetailPass.cs` | Shelf micro-relief + the shelf-edge **knot warp**. Output-height only. | +| `Scripts/RegionLabeling.cs` | ⭐⭐ **The region-labeling layer** (chat2/07) — shared infrastructure. 8-connected land components on the CLASSIFY field; mainland = the centre component; per component id / size / centroid / hemisphere (by centroid) / isMainland. Pure, engine-free, C++-candidate; a **contract** downstream phases consume (islands first; biomes, placement, rivers, the crater later). The hemisphere convention lives here. | | `Scripts/ToolingPaths.cs` | Every tooling path, env-overridable, resolved in one place. | | `Scripts/FileSafety.cs` | The permanent file-safety rules, as throws rather than sentences. | @@ -56,9 +57,11 @@ resolution and the file-safety rails. Constants and contracts. - **No water.** Water is an overlay over the columns (levels-not-cells), never a band. - **No biomes.** Biomes are a later *classification* of finished shape, not an input to it (D-049). -- **No algorithms** *beyond the height curve*. Phase 2 added `HeightCurve` and +- **No algorithms** *beyond the height curve and the region layer*. Phase 2 added `HeightCurve` and `TerrainDetailPass` here because they are pure, engine-free, C++-candidate math that defines the - world's elevation profile — a contract, not a tool's dial. Stratigraphy, feature passes, meshing + world's elevation profile — a contract, not a tool's dial. chat2/07 added `RegionLabeling` on the + same grounds: region identity is a contract every later phase reads, and the flood fill is a hot + path. (The speck REVERT that uses it is a pass, and lives in `Tools/` — `RegionPass`.) Stratigraphy, feature passes, meshing and run-splitting on dig are still later phases. - **No erosion, rivers, water bodies, crater carve, coast shelf or offshore islets.** Later chat2 tasks; the curve is deliberately the only pass-2 element present. diff --git a/Core/Scripts/RegionLabeling.cs b/Core/Scripts/RegionLabeling.cs new file mode 100644 index 0000000..15467f9 --- /dev/null +++ b/Core/Scripts/RegionLabeling.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; + +namespace IslaApocalypse.Core +{ + /// One maximal 8-connected component of land, as the region layer exposes it. + public sealed class LandRegion + { + /// 1-based, assigned in deterministic scan order (x outer, y inner) — stable per seed across runs. + public int Id; + + /// Cells in the component. + public long SizeCells; + + /// Centroid in map cells. + public double CentroidX, CentroidY; + + /// + /// / , decided by the + /// CENTROID — one label per component; a straddler is decided by where its mass is, never per cell. + /// + public byte Hemisphere; + + /// True for exactly one component: the one containing the map centre (or the flagged fallback). + public bool IsMainland; + + /// Bounding box, inclusive. Convenience for overlays and guards; not part of the contract. + public int MinX, MinY, MaxX, MaxY; + } + + /// The result of one labeling: the per-cell id map and the per-component table. + public sealed class RegionLabels + { + public int MapSize; + + /// Per cell, x * MapSize + y: the component id, or 0 for water. + public int[] Id; + + /// Every component, indexed by Id - 1, in id order. + public List Regions; + + /// The mainland's id (0 only if there is no land at all). + public int MainlandId; + + /// + /// ⚠ Whether the map-centre cell was land. Expected always true (the massif is centred and + /// stable). When false the mainland fell back to the LARGEST component and the caller must + /// report it loudly — the contract's mainland definition did not hold on this field. + /// + public bool CentreWasLand; + + public long LandCells; + public int IslandCount => Regions.Count - (MainlandId > 0 ? 1 : 0); + + public LandRegion Mainland => MainlandId > 0 ? Regions[MainlandId - 1] : null; + public LandRegion Of(int id) => Regions[id - 1]; + public int IdAt(int x, int y) => Id[x * MapSize + y]; + } + + /// + /// ⭐⭐ THE REGION-LABELING LAYER — shared infrastructure (chat2/07). Flood-fills land into distinct + /// components, identifies mainland vs islands, and exposes per-component data. Islands are its first + /// consumer; later phases (biomes, placement, rivers, the crater) CONSUME this layer rather than + /// rebuild it. Engine-free; pure analysis over a float[,]; C++-candidate. + /// + /// ═══ THE CONTRACT — build to it exactly (recorded at graduation as the shared-infra contract) ═══ + /// + /// FIELD It runs on the CLASSIFY (raw, uncurved) height — region identity partitions on the + /// same authoritative field as water bodies and biome regions (D-046), so islands / + /// water / biomes line up by construction. Raw is authoritative for region identity. + /// It does NOT run on the render field. + /// + /// CONNECTIVITY Land is 8-CONNECTED. Deliberately the complement of water's 4-connectivity — + /// foreground/background using opposite connectivity is the topologically sound + /// pairing (a diagonal isthmus reads as JOINED; the water on either side of it reads + /// as SEPARATE), not a conflict with the water model. + /// + /// COMPONENT A component = a maximal 8-connected set of land cells (land = classify height ≥ sea). + /// + /// MAINLAND The component containing the MAP CENTRE (the mountain/massif is always centred and + /// stable) — NOT merely the largest component, because a later fragmentation step + /// could make "largest" flip seed to seed. Every OTHER land component is an island. + /// ⚠ The crater is NOT central — it is a northern-coastline feature, unrelated to the + /// centre or the mountain, and plays no part here. + /// Defensively: if the centre cell is not land, the layer reports it ( = false) and falls back to the largest + /// component, FLAGGED — the caller asserts rather than assumes. + /// + /// PER COMPONENT id · sizeCells · centroid (x, y) · hemisphere (north / south, BY THE CENTROID — + /// one label per island; a straddler is decided by its centroid, never per cell) · + /// isMainland. + /// + /// Ids are assigned in deterministic scan order (x outer, y inner, first-seen), so they are stable + /// per seed across runs. Nothing here knows about "offshore" or "stamped" — it labels land. + /// + /// ═══ THE HEMISPHERE CONVENTION — read from the code, not invented (chat2/05) ═══ + /// + /// Pass 1's latitude scalar is y / MapSize; the spine's "southern fade" and the "southern + /// sinker" bite at high y. So y increases SOUTHWARD: NORTH = rows [0, MapSize/2), SOUTH = rows + /// [MapSize/2, MapSize). The clean row midline, never the wobbled latitude field. + /// + public static class RegionLabeling + { + public const byte HemiNone = 0; + public const byte HemiNorth = 1; + public const byte HemiSouth = 2; + + /// The convention, in one place. Every consumer reads hemisphere through this. + public static byte HemisphereOfRow(int y, int mapSize) => y < mapSize / 2 ? HemiNorth : HemiSouth; + + public static string HemisphereName(byte h) => h switch + { + HemiNorth => "north", HemiSouth => "south", _ => "none", + }; + + // 8-connectivity, fixed order (determinism: the fill order never changes). + private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 }; + private static readonly int[] DY = { -1, 0, 1, -1, 1, -1, 0, 1 }; + + /// + /// Label every 8-connected land component of (land = height ≥ + /// ). Pure: the field is read, never written. + /// + public static RegionLabels Label(float[,] classify, int mapSize, float sea) + { + int n = mapSize; + var id = new int[n * n]; + var regions = new List(); + var stack = new Stack(); + long landCells = 0; + + for (int sx = 0; sx < n; sx++) + { + for (int sy = 0; sy < n; sy++) + { + if (classify[sx, sy] < sea || id[sx * n + sy] != 0) continue; + + var r = new LandRegion { Id = regions.Count + 1, MinX = sx, MaxX = sx, MinY = sy, MaxY = sy }; + double sumX = 0, sumY = 0; + id[sx * n + sy] = r.Id; + stack.Push(sx * n + sy); + + while (stack.Count > 0) + { + int cur = stack.Pop(); + int cx = cur / n, cy = cur % n; + r.SizeCells++; sumX += cx; sumY += cy; + if (cx < r.MinX) r.MinX = cx; if (cx > r.MaxX) r.MaxX = cx; + if (cy < r.MinY) r.MinY = cy; if (cy > r.MaxY) r.MaxY = cy; + + for (int k = 0; k < 8; k++) + { + int nx = cx + DX[k], ny = cy + DY[k]; + if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue; + int ni = nx * n + ny; + if (id[ni] != 0 || classify[nx, ny] < sea) continue; + id[ni] = r.Id; + stack.Push(ni); + } + } + + r.CentroidX = sumX / r.SizeCells; + r.CentroidY = sumY / r.SizeCells; + r.Hemisphere = HemisphereOfRow((int)Math.Round(r.CentroidY), n); + landCells += r.SizeCells; + regions.Add(r); + } + } + + var labels = new RegionLabels { MapSize = n, Id = id, Regions = regions, LandCells = landCells }; + + // ⭐ MAINLAND = the component containing the map centre. Asserted by the caller; the + // fallback (largest) exists so a run can finish and REPORT the violation rather than crash. + int centre = (n / 2) * n + (n / 2); + labels.CentreWasLand = id[centre] != 0; + if (labels.CentreWasLand) labels.MainlandId = id[centre]; + else + { + long best = -1; + foreach (var r in regions) if (r.SizeCells > best) { best = r.SizeCells; labels.MainlandId = r.Id; } + } + if (labels.MainlandId > 0) regions[labels.MainlandId - 1].IsMainland = true; + return labels; + } + + /// + /// Size statistics over the islands (non-mainland components): count, min / median / mean / + /// max cells, and a log-spaced histogram — the instrument that turns "nice pieces vs shattered + /// gravel" into numbers. + /// + public static (int count, long min, long median, double mean, long max, int[] histogram) + IslandSizes(RegionLabels labels) + { + var sizes = new List(); + foreach (var r in labels.Regions) if (!r.IsMainland) sizes.Add(r.SizeCells); + var hist = new int[HistogramEdges.Length + 1]; + if (sizes.Count == 0) return (0, 0, 0, 0.0, 0, hist); + sizes.Sort(); + double sum = 0; + foreach (long s in sizes) { sum += s; hist[HistogramBin(s)]++; } + return (sizes.Count, sizes[0], sizes[sizes.Count / 2], sum / sizes.Count, sizes[sizes.Count - 1], hist); + } + + /// Histogram bin edges (cells): [0,64) [64,256) [256,1024) [1024,4096) [4096,16384) [16384,∞). + public static readonly long[] HistogramEdges = { 64, 256, 1024, 4096, 16384 }; + + public static int HistogramBin(long cells) + { + for (int i = 0; i < HistogramEdges.Length; i++) if (cells < HistogramEdges[i]) return i; + return HistogramEdges.Length; + } + + public static string HistogramLabel(int bin) => bin == 0 ? $"<{HistogramEdges[0]}" + : bin < HistogramEdges.Length ? $"{HistogramEdges[bin - 1]}–{HistogramEdges[bin] - 1}" + : $"≥{HistogramEdges[^1]}"; + + /// Island counts per hemisphere (non-mainland components, by centroid). + public static (int north, int south) IslandsByHemisphere(RegionLabels labels) + { + int nN = 0, nS = 0; + foreach (var r in labels.Regions) + { + if (r.IsMainland) continue; + if (r.Hemisphere == HemiNorth) nN++; else if (r.Hemisphere == HemiSouth) nS++; + } + return (nN, nS); + } + } +} diff --git a/Core/Scripts/RegionLabeling.cs.uid b/Core/Scripts/RegionLabeling.cs.uid new file mode 100644 index 0000000..824714a --- /dev/null +++ b/Core/Scripts/RegionLabeling.cs.uid @@ -0,0 +1 @@ +uid://bsb27ghrajnhc diff --git a/Tools/README.md b/Tools/README.md index 94c3b77..fac50a7 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -36,6 +36,9 @@ constants, carried over verbatim — not re-derived from a design summary** (→ | `Scripts/OffshoreAnalysis.cs` | Island components, N/S counts, the moat check, the separation-guard geometry, **the hemisphere convention** | | `Scripts/OffshoreDiagnosis.cs` | The per-hemisphere **measurement** — valid-zone area, binding gate, noise peaks over threshold (chat2/06 §2) | | `Scripts/TagOverlayRenderer.cs` | The island-tag / hemisphere debug overlay | +| `Scripts/RegionPass.cs` | ⭐ **Pass 1c** (chat2/07) — `Core.RegionLabeling` over the classify field, the **origin-blind speck revert** (lower-only + component-only asserted, mainland never), the island tag **by construction** | +| `Scripts/RegionOverlayRenderer.cs` | The labeled-regions overlay: mainland one tint, each island its own colour, reverted specks dark red | +| `Scripts/RegionLabelingTool.cs` + `Scenes/RegionLabelingTool.tscn` | The chat2/07 batch — 3 revert thresholds + a second seed, the count/size instrument | | `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The offshore batch — chat2/06: 4 plates + the count table + the diagnosis (the chat2/05 version is at `3b96e06`) | | `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** | | `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles | @@ -221,13 +224,23 @@ the per-hemisphere counts are a statistical outcome of the tuning, read off the > once. Batch tools turn them on explicitly. **Flipping the defaults is the act that retires the > Phase-1 dumps — do it in a task that re-baselines the oracles.** → `TerrainGenConfig`. -> ### ⭐ The island tag — data, set here, read by nothing yet. +**Pass 1c — region labeling + the speck revert + the island tag (chat2/07).** `RegionPass` runs +`Core.RegionLabeling` over the finished classify field — **land 8-connected, mainland = the component +containing the map centre, every other component an island** — then the config-gated **speck revert** +(`TerrainGenConfig.SpeckRevert` / `MinLandComponentFrac`): every non-mainland component below the +threshold is lowered to its ring's seabed. Origin-blind (a natural nub goes like an offshore dot), +**lower-only and component-only, asserted**, mainland never a candidate. Then the island tag, by +construction. ⚠ `SpeckRevert` defaults OFF in the bare config for the same reason as the shelf/islets +(it would move the calibration pool and every regression dump); the batch turns it on. + +> ### ⭐ The island tag — data, set BY THE REGION LAYER, read by nothing yet. > -> `Pass1Result.IsOffshoreIsland` / `IslandHemisphere` (carried through `Pass2Result`) mark every cell -> the islet layer lifted above sea, with its hemisphere. **NORTH = rows `[0, MapSize/2)`, SOUTH = -> `[MapSize/2, MapSize)`** — y runs south, as the spine's southern fade and the southern sinker -> already encode. A biome / fertility / placement pass reads it from there and never re-derives -> island-land from geometry. Null when offshore is off. +> `Pass1Result.IsIsland` / `IslandHemisphere` (carried through `Pass2Result`) mark every cell of every +> non-mainland land component — natural detached masses and offshore-pass islands alike — with the +> COMPONENT's hemisphere (by centroid). **NORTH = rows `[0, MapSize/2)`, SOUTH = `[MapSize/2, +> MapSize)`** — y runs south. `Pass1Result.Regions` carries the full per-component table. A biome / +> fertility / placement pass reads these and never re-derives island-land from geometry. Null when +> region labeling is off. (chat2/05–06 tagged only what the offshore pass raised — fixed in 07.) > ### ⚠ The one honest coupling: islets TURN WATER INTO LAND. > diff --git a/Tools/Scenes/RegionLabelingTool.tscn b/Tools/Scenes/RegionLabelingTool.tscn new file mode 100644 index 0000000..c4fd7d8 --- /dev/null +++ b/Tools/Scenes/RegionLabelingTool.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://cregions07isla"] + +[ext_resource type="Script" path="res://Tools/Scripts/RegionLabelingTool.cs" id="1_rlt"] + +[node name="RegionLabelingTool" type="Node"] +script = ExtResource("1_rlt") diff --git a/Tools/Scripts/OffshoreAnalysis.cs b/Tools/Scripts/OffshoreAnalysis.cs index 79e3de4..210eb03 100644 --- a/Tools/Scripts/OffshoreAnalysis.cs +++ b/Tools/Scripts/OffshoreAnalysis.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using IslaApocalypse.Core; namespace IslaApocalypse.Tools { @@ -44,17 +45,15 @@ namespace IslaApocalypse.Tools /// public static class OffshoreAnalysis { - public const byte HemiNone = 0; - public const byte HemiNorth = 1; - public const byte HemiSouth = 2; + // The convention now lives in Core (RegionLabeling, chat2/07) — one definition; these are aliases. + public const byte HemiNone = RegionLabeling.HemiNone; + public const byte HemiNorth = RegionLabeling.HemiNorth; + public const byte HemiSouth = RegionLabeling.HemiSouth; - /// The convention, in one place. Every consumer of the tag reads hemisphere through this. - public static byte HemisphereOfRow(int y, int mapSize) => y < mapSize / 2 ? HemiNorth : HemiSouth; + /// . + public static byte HemisphereOfRow(int y, int mapSize) => RegionLabeling.HemisphereOfRow(y, mapSize); - public static string HemisphereName(byte h) => h switch - { - HemiNorth => "north", HemiSouth => "south", _ => "none", - }; + public static string HemisphereName(byte h) => RegionLabeling.HemisphereName(h); // 8-connectivity, fixed order. private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 }; diff --git a/Tools/Scripts/OffshoreIslandsTool.cs b/Tools/Scripts/OffshoreIslandsTool.cs index f84c755..5417d50 100644 --- a/Tools/Scripts/OffshoreIslandsTool.cs +++ b/Tools/Scripts/OffshoreIslandsTool.cs @@ -12,6 +12,10 @@ namespace IslaApocalypse.Tools /// NO FORCED COUNT. (chat2/05's version of this tool — faithful / floor_only / hybrid / dense — /// is in git history at 3b96e06; the forced floor it batched was reverted out on look.) /// + /// ⚠ Since chat2/07 the island tag is set BY THE REGION LAYER (every non-mainland land component, + /// natural islets included), so this tool's counts now include natural islands; the chat2/06 batch + /// of record (offshore-pass islands only) was produced at e8571b2. + /// /// ═══ WHAT IT PRODUCES — a fixed budget: 4 plates + a count table + a diagnosis ═══ /// /// PLATES (4, at ISLA_MAPSIZE, grayscale + .f32 + relief + tags overlay): @@ -253,7 +257,7 @@ namespace IslaApocalypse.Tools Pass2Result p2 = Shaping.Shape(p1, cfg); if (!notesShown) { foreach (string n in p1.Notes) GD.Print(" " + n); notesShown = true; } - var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, tableSize); + var comps = OffshoreAnalysis.Components(p1.IsIsland, p1.Height, sea, tableSize); var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps); var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0); var checks = new List @@ -300,7 +304,7 @@ namespace IslaApocalypse.Tools cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone(); Pass1Result p1 = Topography.Generate(cfg); Pass2Result p2 = Shaping.Shape(p1, cfg); - var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, mapSize); + var comps = OffshoreAnalysis.Components(p1.IsIsland, p1.Height, sea, mapSize); var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps); var moat = ShapingOracle.MoatIntact(p1, comps); moat.Name += $" [plate {lv.Label} {seed}]"; var tag = ShapingOracle.TagCoastlineConsistent(p2, sea); tag.Name += $" [plate {lv.Label} {seed}]"; @@ -463,7 +467,7 @@ namespace IslaApocalypse.Tools .SavePng(Path.Combine(dir, "relief.png")); // ⭐ The tag overlay — the one artifact that shows the DATA this pass set. - TagOverlayRenderer.SavePng(p2.Height, p2.IsOffshoreIsland, p2.IslandHemisphere, p2.MapSize, sea, + TagOverlayRenderer.SavePng(p2.Height, p2.IsIsland, p2.IslandHemisphere, p2.MapSize, sea, countN, countS, Path.Combine(dir, "tags.png")); } diff --git a/Tools/Scripts/Pass1Result.cs b/Tools/Scripts/Pass1Result.cs index 27647ec..8dabf47 100644 --- a/Tools/Scripts/Pass1Result.cs +++ b/Tools/Scripts/Pass1Result.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using IslaApocalypse.Core; namespace IslaApocalypse.Tools { @@ -70,27 +71,34 @@ namespace IslaApocalypse.Tools /// public readonly float HMaxSeedBeforeOffshore; - // ═══ ⭐ THE OFFSHORE TAG — chat2/05's one forward-looking piece ═══ + // ═══ ⭐ THE ISLAND TAG — BY CONSTRUCTION from the region layer (chat2/07; chat2/05's tag, fixed) ═══ // // DATA, set here, carried downstream, READ BY NOTHING IN THIS PHASE. It exists so a later - // pass (biome, fertility, placement) can find offshore-island land without re-deriving it - // from geometry. Downstream that ignores it is unaffected; downstream that reads it gets a - // clean flag. Both arrays are NULL when the offshore layer is off — a consumer checks for - // null, not for all-false. + // pass (biome, fertility, placement) can find island land without re-deriving it from + // geometry. Since chat2/07 it is a CONSEQUENCE OF LABELING: every cell of every non-mainland + // land component is tagged — the big organic detached masses the same as an offshore-pass + // dot (chat2/05–06 tagged only what the offshore pass raised; that was the bug). Both arrays + // are NULL when region labeling is off — a consumer checks for null, not for all-false. + + /// Per column: is this land an island (any non-mainland land component)? Null when labeling is off. + public readonly bool[,] IsIsland; /// - /// Per column: is this land an offshore island (as opposed to mainland)? Set for every cell - /// the islet layer lifted from below sea to at-or-above sea. Null when offshore is off. - /// - public readonly bool[,] IsOffshoreIsland; - - /// - /// Per column: / - /// for tagged cells, otherwise. The convention is the - /// row midline — see . Null when offshore is off. + /// Per column: / + /// for tagged cells (the COMPONENT's hemisphere, by centroid), + /// otherwise. Null when labeling is off. /// public readonly byte[,] IslandHemisphere; + /// ⭐ The region layer's output for this field: id map + per-component table (post-revert). Null when labeling is off. + public readonly RegionLabels Regions; + + /// The labeling BEFORE the speck revert (== when the revert is off or removed nothing). For the overlay's "where a speck was". Null when labeling is off. + public readonly RegionLabels RegionsPre; + + /// The region pass's numbers — pre/post-revert counts and sizes, what was reverted. Null when labeling is off. + public readonly RegionLedger RegionLedger; + /// Cells the islet layer lifted above sea. The reference printed this too. public readonly long OffshoreLiftedCells; @@ -106,8 +114,9 @@ namespace IslaApocalypse.Tools public Pass1Result(int mapSize, int seed, float[,] height, float[,] preTrenchFalloff, float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs, float hMaxSeedBeforeOffshore = float.NaN, - bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null, - long offshoreLiftedCells = 0, IReadOnlyList notes = null, OffshoreLedger offshoreLedger = null) + bool[,] isIsland = null, byte[,] islandHemisphere = null, + long offshoreLiftedCells = 0, IReadOnlyList notes = null, OffshoreLedger offshoreLedger = null, + RegionLabels regions = null, RegionLedger regionLedger = null, RegionLabels regionsPre = null) { MapSize = mapSize; Seed = seed; @@ -118,15 +127,18 @@ namespace IslaApocalypse.Tools HMinSeed = hMinSeed; ElapsedMs = elapsedMs; HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore; - IsOffshoreIsland = isOffshoreIsland; + IsIsland = isIsland; IslandHemisphere = islandHemisphere; + Regions = regions; + RegionsPre = regionsPre; + RegionLedger = regionLedger; OffshoreLiftedCells = offshoreLiftedCells; OffshoreLedger = offshoreLedger; Notes = notes ?? System.Array.Empty(); } - /// Whether the offshore layer ran on this field (the tag arrays are present). - public bool HasOffshoreTag => IsOffshoreIsland != null; + /// Whether the region layer ran on this field (the tag arrays are present). + public bool HasIslandTag => IsIsland != null; /// Fraction of the map at or above the sea threshold. A cheap shape sanity number. public float LandFraction(float seaLevel) diff --git a/Tools/Scripts/Pass2Result.cs b/Tools/Scripts/Pass2Result.cs index ecc6b4a..6b8db64 100644 --- a/Tools/Scripts/Pass2Result.cs +++ b/Tools/Scripts/Pass2Result.cs @@ -95,10 +95,10 @@ namespace IslaApocalypse.Tools // it from here, checks for null (offshore off), and never re-derives island-land from // geometry. - /// , the same array. Null when offshore is off. - public readonly bool[,] IsOffshoreIsland; + /// , the same array. Null when region labeling is off. + public readonly bool[,] IsIsland; - /// , the same array. Null when offshore is off. + /// , the same array. Null when region labeling is off. public readonly byte[,] IslandHemisphere; /// Was shelf detail applied? Requires — it warps the curve's knots. @@ -136,9 +136,9 @@ namespace IslaApocalypse.Tools bool curveOn, bool detailOn, string curveModeLabel, string variantLabel, ContinuousCurve continuous, CurveKnots knots, CurveAnchors anchors, float hMaxSeed, float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs, - List notes, bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null) + List notes, bool[,] isIsland = null, byte[,] islandHemisphere = null) { - IsOffshoreIsland = isOffshoreIsland; + IsIsland = isIsland; IslandHemisphere = islandHemisphere; MapSize = mapSize; Seed = seed; diff --git a/Tools/Scripts/RegionLabelingTool.cs b/Tools/Scripts/RegionLabelingTool.cs new file mode 100644 index 0000000..2c44186 --- /dev/null +++ b/Tools/Scripts/RegionLabelingTool.cs @@ -0,0 +1,611 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Godot; +using IslaApocalypse.Core; + +namespace IslaApocalypse.Tools +{ + /// + /// ⭐ THE REGION-LABELING BATCH (chat2/07) — the general region layer on the current terrain, the + /// island tag fixed by construction, and the tunable speck revert swept. + /// + /// ═══ WHAT IT PRODUCES — a fixed budget: 4 plates + the count/size table ═══ + /// + /// PLATES (4 fields, each grayscale + .f32 + relief + the LABELED-REGIONS overlay + the tag overlay): + /// {plate}_threshold_low / _mid / _high three revert thresholds on ONE seed — the developer + /// dials "where too-small-to-keep sits" by eye. + /// {second}_threshold_mid the preset on a second seed — the table seed with the + /// most NATURAL islands (offshore off), auto-picked or + /// ISLA_SECOND_SEED — labeling + threshold are not + /// seed-specific; the big organic masses tag correctly. + /// + /// THE COUNT/SIZE TABLE (data): per seed, natural islands (offshore off), pre-revert islands, and + /// post-revert islands at each threshold, with size min / median / mean / max and a log-spaced + /// size histogram — the instrument the later southern-stretch step tunes against. + /// + /// Every "current terrain" field = shelf ON + the chat2/06 organic preset (`density_mid`) + region + /// labeling ON; the revert is the variable. The curve is the tagged curve, unchanged. + /// + /// ═══ RUNNING IT ═══ + /// + /// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \ + /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/RegionLabelingTool.tscn + /// + /// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR + /// ISLA_MAPSIZE plate + table size (default 4096) + /// ISLA_TABLE_SIZE count-table size (default = ISLA_MAPSIZE; a probe may drop it) + /// ISLA_CALIB_SIZE curve calibration size (default 2048, task 01's) + /// ISLA_TABLE_SEEDS the table seeds (default 8 below) + /// ISLA_PLATE_SEED the three-threshold seed (default 1063685222) + /// ISLA_SECOND_SEED the second plate seed (default 0 = auto: most natural islands) + /// ISLA_THR_LOW / ISLA_THR_MID / ISLA_THR_HIGH thresholds, fraction of map area (probe overrides) + /// ISLA_TABLE_ONLY=1 probe: table only (no regressions, no plates) + /// ISLA_SKIP_8K=1 skip the 8192 regression (a4) + /// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE / ISLA_T06_SOURCE the regression dumps' batches + /// + public partial class RegionLabelingTool : Node + { + private static readonly int[] DefaultTableSeeds = + { + 1063685222, 20260821, 8675309, 123456789, 271828182, 999999937, 90210, 424242, + }; + + /// ⚠ Task 01's pool, verbatim — the curve's identity. + private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 }; + + private const int DefaultMapSize = 4096; + private const int DefaultCalibSize = 2048; + private const int GallerySize = 8192; + + public override void _Ready() + { + try { Run(); } + catch (Exception e) + { + GD.PrintErr("=================================================================="); + GD.PrintErr($" REFUSED: {e.Message}"); + GD.PrintErr(e.StackTrace); + GD.PrintErr("=================================================================="); + GetTree().Quit(2); + } + } + + private sealed class Level { public string Label; public float Frac; } + + private sealed class Row + { + public string Level; public int Seed; public long ThresholdCells; + public int Natural, NaturalN, NaturalS; // offshore OFF, revert OFF + public int Pre, PreN, PreS; // offshore ON, revert OFF + public int Post, PostN, PostS; // offshore ON, revert at this level + public int RevertedComps; public long RevertedCells; + public long PreMin, PreMed, PreMax, PostMin, PostMed, PostMax; public double PreMean, PostMean; + public int[] PreHist, PostHist; + public long MainlandCells; public bool Ok; public ulong Ms; + } + + private void Run() + { + ToolingPaths.Configure(OS.GetUserDataDir()); + + int task = EnvInt("ISLA_TASK", 7); + string descr = EnvStr("ISLA_BATCH", "region_labeling"); + int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize); + int tableSize = EnvInt("ISLA_TABLE_SIZE", mapSize); + int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize); + int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds); + int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222); + int secondEnv = EnvInt("ISLA_SECOND_SEED", 0); + string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); + string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore"); + string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery"); + string t06Source = EnvStr("ISLA_T06_SOURCE", "06_offshore_organic_tune"); + bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; + bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1"; + bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1"; + + var levels = new List + { + new() { Label = "threshold_low", Frac = EnvFloat("ISLA_THR_LOW", RegionPass.ThresholdLowFrac) }, + new() { Label = "threshold_mid", Frac = EnvFloat("ISLA_THR_MID", RegionPass.ThresholdMidFrac) }, + new() { Label = "threshold_high", Frac = EnvFloat("ISLA_THR_HIGH", RegionPass.ThresholdHighFrac) }, + }; + Level mid = levels[1]; + + string batchRoot = ToolingPaths.BatchRoot(task, descr); + DirAccess.MakeDirRecursiveAbsolute(batchRoot); + DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot)); + + var anchors = CurveAnchors.Default; + float sea = 0.15f; + + GD.Print("=================================================================="); + GD.Print(" REGION LABELING (chat2/07) — label all land, fix the tag, tunable speck revert"); + GD.Print("=================================================================="); + GD.Print($"MapSize : {mapSize} (plates) table at {tableSize} curve calibrated at {calibSize} (offshore OFF)"); + GD.Print($"table : {string.Join(", ", tableSeeds)}"); + GD.Print($"plate seed: {plateSeed} second seed: {(secondEnv > 0 ? secondEnv.ToString() : "auto (most natural islands)")}"); + foreach (var l in levels) GD.Print($" {l.Label,-15} {l.Frac:G3} of map area = {Cells(l.Frac, mapSize):N0} cells at {mapSize} ({Cells(l.Frac, tableSize):N0} at {tableSize})"); + GD.Print($"terrain : shelf ON + offshore {OffshoreSettings.Organic().Describe()}"); + GD.Print($"contract : classify field · land 8-connected · mainland = centre component · id/size/centroid/hemisphere(centroid)/isMainland"); + GD.Print($"batch : {batchRoot}{(tableOnly ? " ⚠ ISLA_TABLE_ONLY — a probe, not the batch of record" : "")}"); + GD.Print("=================================================================="); + + // ═══ 0. THE CURVE ═══ + GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, offshore off) ---"); + var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors); + GD.Print($" {knots}"); + GD.Print($" {calibration.Describe()}"); + + TerrainGenConfig Cfg(int size, int seed, string label, bool offshoreOn, bool revertOn, float frac) + { + var c = BaseConfig(size, seed, knots, anchors, calibration, label); + if (offshoreOn) { c.CoastShelf = true; c.Offshore = OffshoreSettings.Organic(); } + c.RegionLabeling = true; + c.SpeckRevert = revertOn; + c.MinLandComponentFrac = frac; + return c; + } + + // ═══ 1. REGRESSIONS ═══ + var hard = new List(); + if (!tableOnly) + { + GD.Print($"\n--- 1. REGRESSIONS at {calibSize}, seed {plateSeed} ---"); + var offCfg = Cfg(calibSize, plateSeed, "off", offshoreOn: false, revertOn: false, mid.Frac); + Pass1Result p1 = Topography.Generate(offCfg); + + var curveOff = offCfg.Clone(); curveOff.Curve = false; + Pass2Result pOff = Shaping.Shape(p1, curveOff); + string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32"); + hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF, revert OFF (labeling on) == Phase-1 .f32 dump", + pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump)); + + Pass2Result pRest = Shaping.Shape(p1, offCfg); + string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32"); + float[,] t03 = HeightField.Load(t03Dump, calibSize); + hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF, revert OFF (labeling on) == task-03 .f32 dump", + pRest.Height, t03, calibSize, t03Dump)); + + // Informational: offshore OFF, revert ON — how many NATURAL speck cells the revert removes + // from the bare field. Allowed to differ (the revert may change terrain); reported, not asserted. + var revCfg = Cfg(calibSize, plateSeed, "off_revert", offshoreOn: false, revertOn: true, mid.Frac); + Pass1Result p1Rev = Topography.Generate(revCfg); + Pass2Result pRev = Shaping.Shape(p1Rev, revCfg); + var info = ShapingOracle.DumpRegression("a3r", "(informational) offshore OFF, revert ON at threshold_mid vs task-03 dump — the natural specks removed", pRev.Height, t03, calibSize, t03Dump); + info.Detail = (info.Passed ? "no natural speck below the threshold on this seed — " : "") + info.Detail + + $" · reverted {p1Rev.RegionLedger.RevertedComponents} natural components / {p1Rev.RegionLedger.RevertedCells:N0} cells"; + info.Passed = true; + hard.Add(info); + + var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only"; + Pass1Result p1Shelf = Topography.Generate(shelfCfg); + var j0 = ShapingOracle.MainlandUnmoved(p1, p1Shelf, sea); + j0.Name = "shelf alone: every land cell bit-identical (shelf is below-sea only)"; + hard.Add(j0); + hard.Add(ShapingOracle.CentreIsLand(p1)); + foreach (var c in hard) GD.Print(" " + c); + + if (!skip8k) + { + string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32"); + if (File.Exists(t04Dump)) + { + GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, revert OFF …"); + var gCfg = Cfg(GallerySize, plateSeed, "off", offshoreOn: false, revertOn: false, mid.Frac); + Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg); + var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF, revert OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump", + pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump); + hard.Add(a4); GD.Print(" " + a4); + } + else GD.Print($" a4: ⚠ skipped — no 04 gallery dump at {t04Dump}"); + } + else GD.Print(" a4: skipped (ISLA_SKIP_8K)"); + + // ⭐ a6 — labeling ON, revert OFF, on the chat2/06 preset: bit-identical to the 06 batch's + // render field. Labeling is pure analysis; only the revert may change terrain. + string t06Dump = Path.Combine(ToolingPaths.BatchesRoot, t06Source, $"{plateSeed}_density_mid", "height.f32"); + if (File.Exists(t06Dump) && mapSize == 4096) + { + var c6 = Cfg(mapSize, plateSeed, "density_mid", offshoreOn: true, revertOn: false, mid.Frac); + Pass2Result p6 = Shaping.Shape(Topography.Generate(c6), c6); + var a6 = ShapingOracle.DumpRegression("a6", "offshore density_mid ON, labeling ON, revert OFF == task-06 .f32 dump (labeling is pure analysis)", + p6.Height, HeightField.Load(t06Dump, mapSize), mapSize, t06Dump); + hard.Add(a6); GD.Print(" " + a6); + } + else GD.Print($" a6: ⚠ skipped — {(mapSize != 4096 ? "map size is not the 06 batch's 4096" : $"no 06 dump at {t06Dump}")}"); + } + + // ═══ 2. DETERMINISM ═══ + GD.Print($"\n--- 2. DETERMINISM at {tableSize}, seed {plateSeed}, {mid.Label} ---"); + var perFieldChecks = new List(); + { + var cA = Cfg(tableSize, plateSeed, mid.Label, true, true, mid.Frac); + var cB = Cfg(tableSize, plateSeed, mid.Label, true, true, mid.Frac); + var det = ShapingOracle.LabelsDeterministic(Topography.Generate(cA), Topography.Generate(cB)); + det.Name += $" [{plateSeed}]"; + perFieldChecks.Add(det); GD.Print(" " + det); + } + + // ═══ 3. THE COUNT/SIZE TABLE ═══ + GD.Print($"\n--- 3. COUNT/SIZE TABLE at {tableSize} ---"); + var rows = new List(); + var naturalCount = new Dictionary(); + bool notesShown = false; + foreach (int seed in tableSeeds) + { + // natural: offshore OFF, revert OFF + Pass1Result pNat = Topography.Generate(Cfg(tableSize, seed, "natural", false, false, mid.Frac)); + var (natN, natS) = RegionLabeling.IslandsByHemisphere(pNat.Regions); + naturalCount[seed] = pNat.Regions.IslandCount; + // pre: offshore ON, revert OFF + var cPre = Cfg(tableSize, seed, "pre", true, false, mid.Frac); + Pass1Result pPre = Topography.Generate(cPre); + { + var cj = ShapingOracle.MainlandUnmoved(pNat, pPre, sea); cj.Name += $" [offshore on vs off, {seed}]"; perFieldChecks.Add(cj); + var cm = ShapingOracle.CentreIsLand(pPre); cm.Name += $" [pre {seed}]"; perFieldChecks.Add(cm); + } + if (!notesShown) { foreach (string n in pPre.Notes) GD.Print(" " + n); } + GD.Print($" seed {seed,-11} natural islands {pNat.Regions.IslandCount,3} (N {natN} / S {natS}) pre-revert {pPre.Regions.IslandCount,3} (N {pPre.RegionLedger.PostNorth} / S {pPre.RegionLedger.PostSouth}) mainland {pPre.Regions.Mainland.SizeCells:N0} cells"); + + foreach (var lv in levels) + { + var cfg = Cfg(tableSize, seed, lv.Label, true, true, lv.Frac); + Pass1Result p1 = Topography.Generate(cfg); + Pass2Result p2 = Shaping.Shape(p1, cfg); + if (!notesShown) { foreach (string n in p1.Notes) if (n.StartsWith("[Regions]")) GD.Print(" " + n); notesShown = true; } + var led = p1.RegionLedger; + long thr = led.ThresholdCells; + var comps = OffshoreAnalysis.Components(p1.IsIsland, p1.Height, sea, tableSize); + var checks = new List + { + ShapingOracle.CentreIsLand(p1), + ShapingOracle.RevertGuards(pPre, p1, sea, thr), + ShapingOracle.MoatIntact(p1, comps), + ShapingOracle.TagCoastlineConsistent(p2, sea), + ShapingOracle.HMaxAfterOffshore(p1), + ShapingOracle.ClassifyFidelity(p1, p2), + }; + foreach (var c in checks) { c.Name += $" [{lv.Label} {seed}]"; perFieldChecks.Add(c); } + bool ok = checks.TrueForAll(c => c.Passed); + var row = new Row + { + Level = lv.Label, Seed = seed, ThresholdCells = thr, + Natural = pNat.Regions.IslandCount, NaturalN = natN, NaturalS = natS, + Pre = led.PreIslands, PreN = led.PreNorth, PreS = led.PreSouth, + Post = led.PostIslands, PostN = led.PostNorth, PostS = led.PostSouth, + RevertedComps = led.RevertedComponents, RevertedCells = led.RevertedCells, + PreMin = led.PreMin, PreMed = led.PreMedian, PreMean = led.PreMean, PreMax = led.PreMax, + PostMin = led.PostMin, PostMed = led.PostMedian, PostMean = led.PostMean, PostMax = led.PostMax, + PreHist = led.PreHistogram, PostHist = led.PostHistogram, + MainlandCells = p1.Regions.Mainland.SizeCells, Ok = ok, Ms = p1.ElapsedMs, + }; + rows.Add(row); + GD.Print($" {lv.Label,-15} seed {seed,-11} thr {thr,5} pre {row.Pre,3} → post {row.Post,3} (N {row.PostN,2} / S {row.PostS,2}) reverted {row.RevertedComps,3} comps / {row.RevertedCells,7:N0} cells " + + $"post size min {row.PostMin,5} med {row.PostMed,5} max {row.PostMax,6} {(ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms"); + } + } + + // ═══ 4. THE PLATES ═══ + int secondSeed = secondEnv > 0 ? secondEnv : PickSecondSeed(naturalCount, tableSeeds, plateSeed); + GD.Print($"\n second seed: {secondSeed}{(secondEnv > 0 ? " (ISLA_SECOND_SEED)" : $" (auto: most natural islands among the table seeds — {naturalCount.GetValueOrDefault(secondSeed)})")}"); + var plateRows = new List(); + if (!tableOnly) + { + GD.Print($"\n--- 4. PLATES at {mapSize} ---"); + var plates = new List<(int seed, Level lv)> { (plateSeed, levels[0]), (plateSeed, levels[1]), (plateSeed, levels[2]), (secondSeed, mid) }; + foreach (var (seed, lv) in plates) + { + var cfg = Cfg(mapSize, seed, lv.Label, true, true, lv.Frac); + Pass1Result p1 = Topography.Generate(cfg); + Pass2Result p2 = Shaping.Shape(p1, cfg); + var led = p1.RegionLedger; + var cm = ShapingOracle.CentreIsLand(p1); cm.Name += $" [plate {lv.Label} {seed}]"; + var ck = ShapingOracle.TagCoastlineConsistent(p2, sea); ck.Name += $" [plate {lv.Label} {seed}]"; + perFieldChecks.Add(cm); perFieldChecks.Add(ck); + WritePlate(batchRoot, p1, p2, sea, anchors, skipRaw); + plateRows.Add(new Row + { + Level = lv.Label, Seed = seed, ThresholdCells = led.ThresholdCells, + Pre = led.PreIslands, PreN = led.PreNorth, PreS = led.PreSouth, Post = led.PostIslands, PostN = led.PostNorth, PostS = led.PostSouth, + RevertedComps = led.RevertedComponents, RevertedCells = led.RevertedCells, + PostMin = led.PostMin, PostMed = led.PostMedian, PostMean = led.PostMean, PostMax = led.PostMax, + MainlandCells = p1.Regions.Mainland.SizeCells, Ok = cm.Passed && ck.Passed, Ms = p1.ElapsedMs, + }); + GD.Print($" plate {seed}_{lv.Label}: pre {led.PreIslands} → post {led.PostIslands} (N {led.PostNorth} / S {led.PostSouth}), reverted {led.RevertedComponents} comps {(cm.Passed && ck.Passed ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms"); + } + } + + bool allOk = hard.TrueForAll(c => c.Passed) && perFieldChecks.TrueForAll(c => c.Passed); + GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}"); + foreach (var c in perFieldChecks) if (!c.Passed) GD.PrintErr(" " + c); + + WriteTable(batchRoot, tableSize, levels, rows); + WriteIndex(batchRoot, mapSize, tableSize, calibSize, plateSeed, secondSeed, tableSeeds, levels, rows, plateRows, hard, perFieldChecks, allOk, tableOnly); + + GD.Print("\n=================================================================="); + GD.Print($" DONE — {batchRoot}"); + GD.Print($" ORACLE {(allOk ? "HARD CHECKS ALL PASS" : "*** FAILURES — see the table ***")}"); + GD.Print("=================================================================="); + GetTree().Quit(allOk ? 0 : 3); + } + + private static long Cells(float frac, int size) => Math.Max(1L, (long)Math.Round(frac * (double)size * size)); + + private static int PickSecondSeed(Dictionary natural, int[] seeds, int plateSeed) + { + int best = 0, bestN = -1; + foreach (int s in seeds) + { + if (s == plateSeed) continue; + int n = natural.GetValueOrDefault(s); + if (n > bestN) { best = s; bestN = n; } + } + return best == 0 ? plateSeed : best; + } + + // ---- the curve, measured exactly as tasks 03–06 did -------------------- + + private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors) + { + var rawPool = new LandHistogram(sea); + var pass1 = new Dictionary(); + foreach (int s in CalibrationSeeds) + { + var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // offshore OFF, revert OFF by default + pass1[s] = p1; + rawPool.Accumulate(p1.Height, calibSize); + } + var knots = new CurveKnots(2, "v2_balanced", + rawPool.Quantile(CurveKnots.Percentiles[0]), rawPool.Quantile(CurveKnots.Percentiles[1]), + rawPool.Quantile(CurveKnots.Percentiles[2]), rawPool.Quantile(CurveKnots.Percentiles[3]), + rawPool.Quantile(CurveKnots.Percentiles[4]), rawPool.Quantile(CurveKnots.Percentiles[5])); + + float ceilingRaw = knots.K2; + var rawAbove = new LandHistogram(sea); + var outAbove = new LandHistogram(sea); + foreach (int s in CalibrationSeeds) + { + var scfg = new TerrainGenConfig + { + MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, + CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", + }; + Pass2Result st = Shaping.Shape(pass1[s], scfg); + rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); + outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); + } + var pcts = ClimbCalibration.DefaultPercentiles; + var rawQ = new float[pcts.Length]; + var outQ = new float[pcts.Length]; + for (int i = 0; i < pcts.Length; i++) { rawQ[i] = rawAbove.Quantile(pcts[i]); outQ[i] = outAbove.Quantile(pcts[i]); } + + var cal = ClimbCalibration.FromPercentiles(pcts, rawQ, outQ, ceilingRaw, + HeightCurve.EffectiveSpikeMax(pass1[CalibrationSeeds[0]].HMaxSeed, knots, anchors), + anchors.RedCeil, anchors.PeakCap, mountainLift: 1.0f, peakSharpness: 1.0f); + return (knots, cal); + } + + private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, + ClimbCalibration cal, string label) => new TerrainGenConfig + { + MapSize = mapSize, Seed = seed, VariantLabel = label, + Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, + Knots = k, Anchors = a, ClimbCalibration = cal, LowlandCeilingM = 30f, + CoastShelf = false, Offshore = new OffshoreSettings(), // OFF unless the variant turns it on + }; + + // ---- output ----------------------------------------------------------- + + private static void WritePlate(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw) + { + string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}"); + DirAccess.MakeDirRecursiveAbsolute(dir); + + GrayscaleRenderer.SavePng(p2.Height, p2.MapSize, Path.Combine(dir, "grayscale.png")); + if (!skipRaw) HeightField.Save(p2.Height, p2.MapSize, Path.Combine(dir, "height.f32")); + + var look = new LookConfig + { + Name = "hillshade_even", Palette = ReliefPalette.Kind.ProvisionalEven, + ZExaggeration = 18f, LightAzimuth = 315f, LightAltitude = 45f, + HillshadeStrength = 0.30f, SeaLevel = sea, + }; + Image map = ReliefRenderer.Render(p2.Height, p2.MapSize, look); + LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap, $"{p2.VariantLabel.ToUpperInvariant()} {p2.Seed}") + .SavePng(Path.Combine(dir, "relief.png")); + + // ⭐ The labeled-regions overlay — the point of this task. + var led = p1.RegionLedger; + RegionOverlayRenderer.SavePng(p1.Regions, led.RevertOn ? p1.RegionsPre : null, p1.MapSize, + led.RevertedComponents, led.ThresholdCells, Path.Combine(dir, "regions.png")); + + // The hemisphere tag overlay (chat2/05's), now showing the tag by construction. + var (n, s) = RegionLabeling.IslandsByHemisphere(p1.Regions); + TagOverlayRenderer.SavePng(p2.Height, p2.IsIsland, p2.IslandHemisphere, p2.MapSize, sea, n, s, Path.Combine(dir, "tags.png")); + } + + private static string HistRow(int[] h) + { + if (h == null) return "—"; + var sb = new StringBuilder(); + for (int i = 0; i < h.Length; i++) { if (i > 0) sb.Append(" · "); sb.Append(h[i]); } + return sb.ToString(); + } + + private static string TableMarkdown(List levels, List rows, int tableSize) + { + var sb = new StringBuilder(); + var histHead = new StringBuilder(); + for (int i = 0; i <= RegionLabeling.HistogramEdges.Length; i++) { if (i > 0) histHead.Append(" · "); histHead.Append(RegionLabeling.HistogramLabel(i)); } + sb.AppendLine($"| Level | Seed | threshold (cells) | natural islands (offshore off) N / S | pre-revert islands N / S | **post-revert islands N / S** | reverted comps / cells | pre size min / med / mean / max | **post size min / med / mean / max** | post histogram ({histHead}) | mainland cells | oracle |"); + sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|---|"); + foreach (var lv in levels) + foreach (var r in rows) + { + if (r.Level != lv.Label) continue; + sb.AppendLine($"| `{r.Level}` | `{r.Seed}` | {r.ThresholdCells:N0} | {r.Natural} ({r.NaturalN} / {r.NaturalS}) | {r.Pre} ({r.PreN} / {r.PreS}) | **{r.Post} ({r.PostN} / {r.PostS})** | {r.RevertedComps} / {r.RevertedCells:N0} | " + + $"{r.PreMin} / {r.PreMed} / {r.PreMean:F0} / {r.PreMax} | **{r.PostMin} / {r.PostMed} / {r.PostMean:F0} / {r.PostMax}** | {HistRow(r.PostHist)} | {r.MainlandCells:N0} | {(r.Ok ? "pass" : "**FAIL**")} |"); + } + sb.AppendLine(); + sb.AppendLine("**Per level (over the seeds):**"); + sb.AppendLine(); + sb.AppendLine("| Level | threshold | post islands min / mean / max | post N min / mean / max | post S min / mean / max | reverted comps (total) | reverted cells (total) | post median island (median over seeds) | smallest surviving island |"); + sb.AppendLine("|---|---|---|---|---|---|---|---|---|"); + foreach (var lv in levels) + { + int cnt = 0, minP = int.MaxValue, maxP = 0, minN = int.MaxValue, maxN = 0, minS = int.MaxValue, maxS = 0; double sumP = 0, sumN = 0, sumS = 0; + long revC = 0, revCells = 0, smallest = long.MaxValue; var meds = new List(); long thr = 0; + foreach (var r in rows) + { + if (r.Level != lv.Label) continue; + cnt++; thr = r.ThresholdCells; + minP = Math.Min(minP, r.Post); maxP = Math.Max(maxP, r.Post); sumP += r.Post; + minN = Math.Min(minN, r.PostN); maxN = Math.Max(maxN, r.PostN); sumN += r.PostN; + minS = Math.Min(minS, r.PostS); maxS = Math.Max(maxS, r.PostS); sumS += r.PostS; + revC += r.RevertedComps; revCells += r.RevertedCells; meds.Add(r.PostMed); + if (r.Post > 0) smallest = Math.Min(smallest, r.PostMin); + } + if (cnt == 0) continue; + meds.Sort(); + sb.AppendLine($"| `{lv.Label}` | {lv.Frac:G3} = {thr:N0} cells | {minP} / {sumP / cnt:F1} / {maxP} | {minN} / {sumN / cnt:F1} / {maxN} | {minS} / {sumS / cnt:F1} / {maxS} | {revC} | {revCells:N0} | {meds[meds.Count / 2]} | {(smallest == long.MaxValue ? 0 : smallest)} |"); + } + return sb.ToString(); + } + + private static void WriteTable(string batchRoot, int tableSize, List levels, List rows) + { + var sb = new StringBuilder(); + sb.AppendLine($"# The count/size table — {rows.Count / Math.Max(1, levels.Count)} seeds × {levels.Count} revert thresholds at {tableSize}"); + sb.AppendLine(); + sb.AppendLine("Islands = non-mainland 8-connected land components of the CLASSIFY field (mainland = the centre component)."); + sb.AppendLine("*natural* = offshore off, revert off; *pre-revert* = offshore `density_mid` on, revert off; *post-revert* = the same with"); + sb.AppendLine("the speck revert on at the level's threshold. Sizes in cells. Histogram bins are cells, log-spaced."); + sb.AppendLine(); + sb.Append(TableMarkdown(levels, rows, tableSize)); + WriteText(Path.Combine(batchRoot, "count_size_table.md"), sb.ToString()); + + var csv = new StringBuilder(); + csv.AppendLine("level,seed,threshold_cells,natural,natural_n,natural_s,pre,pre_n,pre_s,post,post_n,post_s,reverted_comps,reverted_cells,pre_min,pre_median,pre_mean,pre_max,post_min,post_median,post_mean,post_max,post_hist,mainland_cells,oracle,ms"); + var ic = System.Globalization.CultureInfo.InvariantCulture; + foreach (var r in rows) + csv.AppendLine(string.Join(",", r.Level, r.Seed, r.ThresholdCells, r.Natural, r.NaturalN, r.NaturalS, r.Pre, r.PreN, r.PreS, r.Post, r.PostN, r.PostS, + r.RevertedComps, r.RevertedCells, r.PreMin, r.PreMed, r.PreMean.ToString("F1", ic), r.PreMax, r.PostMin, r.PostMed, r.PostMean.ToString("F1", ic), r.PostMax, + "\"" + HistRow(r.PostHist) + "\"", r.MainlandCells, r.Ok ? "pass" : "FAIL", r.Ms)); + WriteText(Path.Combine(batchRoot, "count_size_table.csv"), csv.ToString()); + } + + private static void WriteIndex(string batchRoot, int mapSize, int tableSize, int calibSize, int plateSeed, int secondSeed, + int[] tableSeeds, List levels, List rows, List plateRows, + List hard, List perField, bool allOk, bool tableOnly) + { + var sb = new StringBuilder(); + sb.AppendLine("# Batch 07 — region labeling: label all land, fix the tag, tunable speck revert"); + sb.AppendLine(); + sb.AppendLine("The **region-labeling layer** (`Core.RegionLabeling`) flood-fills the CLASSIFY field's land into 8-connected"); + sb.AppendLine("components, names the **centre component** the mainland, and exposes id / size / centroid / hemisphere (by"); + sb.AppendLine("centroid) / isMainland. The **island tag is now a consequence of labeling** — every non-mainland component,"); + sb.AppendLine("natural detached masses included. The **speck revert** (origin-blind, lower-only, component-only, mainland never)"); + sb.AppendLine("lowers sub-threshold islands to their ring's seabed; the threshold is the dial swept here."); + sb.AppendLine(); + if (tableOnly) sb.AppendLine("> ⚠ **ISLA_TABLE_ONLY** — a probe run: table only, no regressions, no plates. Not the batch of record.\n"); + sb.AppendLine("## ⭐ Open this first"); + sb.AppendLine(); + sb.AppendLine($"1. **`{plateSeed}_threshold_mid/regions.png`** — the labeled-regions overlay: grey = mainland (the centre component),"); + sb.AppendLine(" every island its own colour, dark red = where a reverted speck was. Then `relief.png` for the clean ocean."); + sb.AppendLine($"2. **`{plateSeed}_threshold_low/`** and **`{plateSeed}_threshold_high/`** beside it — same seed, lower / higher cutoff."); + sb.AppendLine($"3. **`{secondSeed}_threshold_mid/regions.png`** + `tags.png` — the second seed (most natural islands): the big organic"); + sb.AppendLine(" detached masses are labeled and TAGGED (cyan / orange), which task 06's overlay left grey."); + sb.AppendLine("4. Then the count/size table — the instrument for the later southern-stretch step."); + sb.AppendLine(); + sb.AppendLine("**The contract (verbatim):** field = classify (raw, uncurved) · land 8-connected (the complement of water's 4) ·"); + sb.AppendLine("component = maximal 8-connected set of land cells (classify ≥ sea) · mainland = the component containing the map"); + sb.AppendLine("centre (not merely the largest; the crater is NOT central) · per component: id, sizeCells, centroid, hemisphere"); + sb.AppendLine($"(by centroid), isMainland. NORTH = rows `[0, {mapSize / 2})`, SOUTH = rows `[{mapSize / 2}, {mapSize})`; y runs south."); + sb.AppendLine(); + sb.AppendLine("## The four plates"); + sb.AppendLine(); + sb.AppendLine("| Plate | threshold (cells) | pre-revert islands N / S | **post-revert islands N / S** | reverted comps / cells | post size min / med / mean / max | mainland cells | oracle |"); + sb.AppendLine("|---|---|---|---|---|---|---|---|"); + foreach (var r in plateRows) + sb.AppendLine($"| `{r.Seed}_{r.Level}/` | {r.ThresholdCells:N0} | {r.Pre} ({r.PreN} / {r.PreS}) | **{r.Post} ({r.PostN} / {r.PostS})** | {r.RevertedComps} / {r.RevertedCells:N0} | {r.PostMin} / {r.PostMed} / {r.PostMean:F0} / {r.PostMax} | {r.MainlandCells:N0} | {(r.Ok ? "pass" : "**FAIL**")} |"); + if (plateRows.Count == 0) sb.AppendLine("| *(no plates — probe run)* | | | | | | | |"); + sb.AppendLine(); + sb.AppendLine($"## ⭐ The count/size table — {tableSeeds.Length} seeds × 3 thresholds at {tableSize}"); + sb.AppendLine(); + sb.Append(TableMarkdown(levels, rows, tableSize)); + sb.AppendLine(); + sb.AppendLine("Also as plain data: `count_size_table.md` / `.csv`."); + sb.AppendLine(); + sb.AppendLine("## The levels"); + sb.AppendLine(); + sb.AppendLine("| Level | `MinLandComponentFrac` | cells at the plate size | cells at 8192 |"); + sb.AppendLine("|---|---|---|---|"); + foreach (var lv in levels) sb.AppendLine($"| `{lv.Label}`{(lv.Label == "threshold_mid" ? " ⭐ config default" : "")} | {lv.Frac:G3} | {Cells(lv.Frac, mapSize):N0} | {Cells(lv.Frac, 8192):N0} |"); + sb.AppendLine(); + sb.AppendLine($"Every field: coast shelf ON + offshore `{OffshoreSettings.Organic().Describe()}` + region labeling ON. The revert is the variable."); + sb.AppendLine("The revert is **origin-blind**: it removes small natural nubs as well as offshore-pass dots (fewer / bigger, intended). An offshore"); + sb.AppendLine("island it removes leaves its submerged skirt (not this component — component-only) as a shoal."); + sb.AppendLine(); + sb.AppendLine("## ⚠ The palette is PROVISIONAL"); + sb.AppendLine(); + sb.AppendLine("`ProvisionalEven`, flagged. The individually-coloured scheme is ONLY the `regions.png` overlay."); + sb.AppendLine(); + sb.AppendLine("## The oracle"); + sb.AppendLine(); + sb.AppendLine("Regressions (offshore OFF + revert OFF must be bit-identical to Phase 1, task 03 and the `terrain-curve-v1` gallery dump; labeling ON + revert OFF bit-identical to the task-06 dump):"); + sb.AppendLine(); + sb.AppendLine(hard.Count == 0 ? "*(skipped — probe run)*\n" : ShapingOracle.ToMarkdownTable(hard)); + sb.AppendLine("Per field (centre-is-land m · revert guards n · determinism o · moat i · mainland unmoved j · tag/coastline k · HMaxSeed l · classify b):"); + sb.AppendLine(); + sb.AppendLine(ShapingOracle.ToMarkdownTable(perField)); + sb.AppendLine($"**{(allOk ? "ALL HARD CHECKS PASS" : "⚠⚠ FAILURES — do not judge this batch")}**"); + sb.AppendLine(); + sb.AppendLine("## Disposability"); + sb.AppendLine(); + sb.AppendLine("| Artifact | Keep? |"); + sb.AppendLine("|---|---|"); + sb.AppendLine("| `regions.png`, `tags.png`, `relief.png`, `INDEX.md`, `count_size_table.md` / `.csv` | **keep** |"); + sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |"); + sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code — large, clear freely |"); + sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |"); + sb.AppendLine(); + sb.AppendLine($"Plates at {mapSize}, table at {tableSize}, curve calibrated at {calibSize} with offshore off. {WorldScale.Describe()}."); + WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString()); + } + + private static void WriteText(string path, string text) + { + using var f = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Write); + if (f == null) { GD.PrintErr($"could not write {path}"); return; } + f.StoreString(text); + } + + // ---- env helpers -------------------------------------------------------- + + private static string EnvStr(string k, string fallback) + { + string v = System.Environment.GetEnvironmentVariable(k); + return string.IsNullOrWhiteSpace(v) ? fallback : v; + } + + private static int EnvInt(string k, int fallback) + => int.TryParse(EnvStr(k, null) ?? "", out int v) ? v : fallback; + + private static float EnvFloat(string k, float fallback) + => float.TryParse(EnvStr(k, null) ?? "", System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out float v) ? v : fallback; + + private static int[] EnvSeeds(string k, int[] fallback) + { + string v = EnvStr(k, null); + if (v == null) return fallback; + var outp = new List(); + foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries)) + if (int.TryParse(part.Trim(), out int s) && s > 0) outp.Add(s); + return outp.Count > 0 ? outp.ToArray() : fallback; + } + } +} diff --git a/Tools/Scripts/RegionLabelingTool.cs.uid b/Tools/Scripts/RegionLabelingTool.cs.uid new file mode 100644 index 0000000..2e877df --- /dev/null +++ b/Tools/Scripts/RegionLabelingTool.cs.uid @@ -0,0 +1 @@ +uid://ceeyncymgtjpm diff --git a/Tools/Scripts/RegionOverlayRenderer.cs b/Tools/Scripts/RegionOverlayRenderer.cs new file mode 100644 index 0000000..71adea0 --- /dev/null +++ b/Tools/Scripts/RegionOverlayRenderer.cs @@ -0,0 +1,77 @@ +using System; +using Godot; +using IslaApocalypse.Core; + +namespace IslaApocalypse.Tools +{ + /// + /// THE LABELED-REGIONS OVERLAY (chat2/07): the mainland one tint, EACH island component an + /// individually distinct colour, the components the speck revert removed in a dim red, the midline + /// drawn — so the developer can SEE that components are identified correctly and catch an + /// 8-connectivity mislabel (two touching blobs coloured as one, one mass coloured as two). + /// + /// ⚠ A DIAGNOSTIC, NOT A MAP. It draws the region layer's id map, which is DATA. No hypsometry, no + /// hillshade — flat tints on purpose. The relief / grayscale plates stay on the provisional palette. + /// Presentation only: it is handed arrays and returns a PNG. + /// + public static class RegionOverlayRenderer + { + private static readonly Color Sea = new(0.055f, 0.110f, 0.235f); + private static readonly Color Mainland = new(0.340f, 0.380f, 0.330f); + private static readonly Color Reverted = new(0.420f, 0.080f, 0.080f); // where a reverted speck WAS (now sea) + private static readonly Color Midline = new(0.700f, 0.720f, 0.760f); + private static readonly Color Ink = new(0.941f, 0.949f, 0.961f); + + /// A distinct, saturated colour per island id — golden-angle hue walk, three value steps. + public static Color IslandColor(int id) + { + float hue = (id * 137.508f) % 360f / 360f; + float val = 0.70f + 0.15f * (id % 3); + float sat = 0.85f - 0.15f * ((id / 3) % 2); + return Color.FromHsv(hue, sat, val); + } + + /// The finished field's labeling (post-revert). + /// The pre-revert labeling, or null — its reverted components are painted . + public static void SavePng(RegionLabels labels, RegionLabels labelsPre, int mapSize, int revertedCount, long thresholdCells, + string absolutePath) + { + var img = Image.CreateEmpty(mapSize, mapSize, false, Image.Format.Rgb8); + int n = mapSize; + for (int x = 0; x < n; x++) + { + for (int y = 0; y < n; y++) + { + int id = labels.Id[x * n + y]; + Color c; + if (id == 0) + { + c = Sea; + if (labelsPre != null) + { + int pid = labelsPre.Id[x * n + y]; + if (pid != 0 && pid != labelsPre.MainlandId) c = Reverted; // was land, was not mainland, is sea now + } + } + else if (id == labels.MainlandId) c = Mainland; + else c = IslandColor(id); + img.SetPixel(x, y, c); + } + } + + int mid = n / 2; + for (int x = 0; x < n; x += 3) img.SetPixel(x, mid, Midline); + + var (north, south) = RegionLabeling.IslandsByHemisphere(labels); + int s = n >= 4096 ? 4 : 3; + int lh = TinyFont.Height(s) + 6; + TinyFont.Draw(img, "LABELED REGIONS - CLASSIFY FIELD, LAND 8-CONNECTED", 12, 12, s, Ink); + TinyFont.Draw(img, $"GREY: MAINLAND (CENTRE COMPONENT{(labels.CentreWasLand ? "" : " - FALLBACK, CENTRE NOT LAND")}) EACH ISLAND: ITS OWN COLOUR", 12, 12 + lh, s, Ink); + TinyFont.Draw(img, $"ISLANDS: {labels.IslandCount} ({north} N / {south} S BY CENTROID) DARK RED: {revertedCount} REVERTED < {thresholdCells} CELLS", 12, 12 + lh * 2, s, Ink); + TinyFont.Draw(img, "N ABOVE THE LINE - S BELOW - Y RUNS SOUTH", 12, 12 + lh * 3, s, Ink); + + Error err = img.SavePng(absolutePath); + if (err != Error.Ok) GD.PrintErr($"[RegionOverlayRenderer] SavePng failed ({err}) for {absolutePath}"); + } + } +} diff --git a/Tools/Scripts/RegionOverlayRenderer.cs.uid b/Tools/Scripts/RegionOverlayRenderer.cs.uid new file mode 100644 index 0000000..91edb93 --- /dev/null +++ b/Tools/Scripts/RegionOverlayRenderer.cs.uid @@ -0,0 +1 @@ +uid://c05blbudak0as diff --git a/Tools/Scripts/RegionPass.cs b/Tools/Scripts/RegionPass.cs new file mode 100644 index 0000000..b700823 --- /dev/null +++ b/Tools/Scripts/RegionPass.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using IslaApocalypse.Core; + +namespace IslaApocalypse.Tools +{ + /// The region pass's numbers, carried on Pass1Result for the report. + public sealed class RegionLedger + { + public bool RevertOn; + public long ThresholdCells; + public int PreIslands, PreNorth, PreSouth; // before the speck revert + public int PostIslands, PostNorth, PostSouth; // after + public int RevertedComponents; public long RevertedCells; + public bool CentreWasLand = true, CentreWasLandPre = true; + public long PreMin, PreMedian, PreMax, PostMin, PostMedian, PostMax; public double PreMean, PostMean; + public int[] PreHistogram, PostHistogram; + public List<(int id, long cells, byte hemi, float newHeight)> Reverted = new(); + } + + /// + /// ⭐ PASS 1c — REGION LABELING + THE SPECK REVERT + THE ISLAND TAG (chat2/07). Runs over the + /// finished pass-1/1b classify field, IN PLACE, after the shelf/islets and before HMaxSeed + /// is retaken and anything classifies. + /// + /// ═══ WHAT IT DOES, IN ORDER ═══ + /// + /// 1. LABEL over the classify field — the general layer. + /// Pure analysis: no height changes. (The pre-revert table is kept for the instrument.) + /// 2. REVERT (config-gated: TerrainGenConfig.SpeckRevert; threshold + /// MinLandComponentFrac × map area) — every NON-MAINLAND component below the + /// threshold is lowered to seabed. ORIGIN-BLIND: it judges components by size, not by + /// who made them — a small natural nub goes the same way as an offshore-pass dot. + /// Fewer, bigger. Two guards, ASSERTED per component, hard failure on violation: + /// LOWER-ONLY — every touched cell goes DOWN (land → below sea), never up; + /// COMPONENT-ONLY — only cells of the sub-threshold component are touched, never a + /// neighbour (the submerged skirt an offshore island leaves behind + /// is NOT this component and stays — a shoal, by the rule). + /// Together they make it impossible for "revert" to move the mainland coast. + /// MAINLAND IS NEVER A CANDIDATE (asserted), however small a pathological seed made it. + /// The seabed a cell is lowered to is the MEAN height of the component's adjacent sea + /// cells (its ring), held strictly below sea by BitDecrement — a flat shoal at the + /// local depth, not a pit and not a reef. + /// 3. RELABEL after a revert the layer is run again, so the exposed table and ids are those of + /// the finished field. + /// 4. TAG BY CONSTRUCTION: every cell of every non-mainland component is an island cell, + /// hemisphere from its component's centroid. The big organic detached masses are + /// tagged the same as an offshore-pass dot. Nothing here knows which pass made a cell. + /// + /// The classify field IS the pass-1 array; pass 2 derives the render field from it and the curve is + /// identity at and below sea, so a reverted cell is seabed in both — asserted downstream by oracle (k). + /// + public static class RegionPass + { + public sealed class Result + { + public RegionLabels LabelsPre; // before the revert (== Labels when the revert is off or reverted nothing) + public RegionLabels Labels; // the finished field's labeling + public bool[,] IsIsland; // the tag, by construction + public byte[,] IslandHemisphere; + public RegionLedger Ledger = new(); + public List Notes = new(); + } + + /// The three thresholds of the chat2/07 batch, fractions of the map's area; Mid is the config default. + public const float ThresholdLowFrac = 1e-5f; // 168 cells at 4096 — only the smallest natural specks + public const float ThresholdMidFrac = 3e-5f; // 503 cells at 4096 — the offshore pass's own speck guard, applied to all land + public const float ThresholdHighFrac = 1e-4f; // 1,678 cells at 4096 — "fewer, bigger": takes small offshore islands too + + public static Result Apply(float[,] height, int mapSize, float sea, TerrainGenConfig cfg) + { + var r = new Result(); + var pre = RegionLabeling.Label(height, mapSize, sea); + r.LabelsPre = pre; + var led = r.Ledger; + led.CentreWasLandPre = pre.CentreWasLand; + FillPre(led, pre); + r.Notes.Add($"[Regions] labeled {pre.Regions.Count} land components ({pre.LandCells:N0} land cells): mainland id {pre.MainlandId} " + + $"({(pre.Mainland == null ? 0 : pre.Mainland.SizeCells):N0} cells, centre {(pre.CentreWasLand ? "is land" : "⚠ NOT LAND — fell back to the largest component")}), " + + $"{pre.IslandCount} islands (N {led.PreNorth} / S {led.PreSouth}); island cells min {led.PreMin} median {led.PreMedian} mean {led.PreMean:F0} max {led.PreMax}."); + + RegionLabels final = pre; + led.RevertOn = cfg.SpeckRevert; + if (cfg.SpeckRevert) + { + long threshold = Math.Max(1L, (long)Math.Round(cfg.MinLandComponentFrac * (double)mapSize * mapSize)); + led.ThresholdCells = threshold; + float strictlyBelowSea = MathF.BitDecrement(sea); + int n = mapSize; + + // Which components go: non-mainland, below the threshold. Mainland is never a candidate. + var revert = new Dictionary(); + foreach (var c in pre.Regions) + if (!c.IsMainland && c.SizeCells < threshold) revert[c.Id] = c; + if (pre.Mainland != null && revert.ContainsKey(pre.MainlandId)) + throw new InvalidOperationException("[RegionPass] the mainland was selected for revert. Refusing."); + + if (revert.Count > 0) + { + // The ring: the mean height of each doomed component's adjacent SEA cells. + var ringSum = new Dictionary(); + var ringCnt = new Dictionary(); + foreach (int id in revert.Keys) { ringSum[id] = 0; ringCnt[id] = 0; } + for (int x = 0; x < n; x++) + { + for (int y = 0; y < n; y++) + { + int id = pre.Id[x * n + y]; + if (id == 0 || !revert.ContainsKey(id)) continue; + for (int dx = -1; dx <= 1; dx++) + { + int nx = x + dx; if (nx < 0 || nx >= n) continue; + for (int dy = -1; dy <= 1; dy++) + { + int ny = y + dy; if (ny < 0 || ny >= n || (dx == 0 && dy == 0)) continue; + if (pre.Id[nx * n + ny] != 0) continue; // land (this or another component) + ringSum[id] += height[nx, ny]; ringCnt[id]++; + } + } + } + } + + var target = new Dictionary(); + foreach (var (id, c) in revert) + { + float t = ringCnt[id] > 0 ? (float)(ringSum[id] / ringCnt[id]) : strictlyBelowSea; + target[id] = MathF.Min(t, strictlyBelowSea); // strictly below sea, whatever the ring says + } + + // The revert, with both guards asserted cell by cell. + var touched = new Dictionary(); + foreach (int id in revert.Keys) touched[id] = 0; + long cells = 0; + for (int x = 0; x < n; x++) + { + for (int y = 0; y < n; y++) + { + int id = pre.Id[x * n + y]; + if (id == 0 || !target.TryGetValue(id, out float t)) continue; // COMPONENT-ONLY: nothing else is ever touched + float h = height[x, y]; + if (h < sea) + throw new InvalidOperationException($"[RegionPass] component {id} cell ({x},{y}) is not land (h {h:G9} < sea {sea:G9}) — the labeling and the field disagree. Refusing."); + if (t >= h) + throw new InvalidOperationException($"[RegionPass] LOWER-ONLY violated at ({x},{y}): {h:G9} → {t:G9}. Refusing."); + height[x, y] = t; + touched[id]++; cells++; + } + } + foreach (var (id, c) in revert) + { + if (touched[id] != c.SizeCells) + throw new InvalidOperationException($"[RegionPass] COMPONENT-ONLY violated: component {id} has {c.SizeCells} cells, {touched[id]} touched. Refusing."); + led.Reverted.Add((id, c.SizeCells, c.Hemisphere, target[id])); + } + led.RevertedComponents = revert.Count; + led.RevertedCells = cells; + + final = RegionLabeling.Label(height, mapSize, sea); + if (final.Mainland == null || pre.Mainland == null || final.Mainland.SizeCells != pre.Mainland.SizeCells) + throw new InvalidOperationException("[RegionPass] the mainland's size changed across the revert. Refusing."); + } + r.Notes.Add($"[Regions] speck revert ON (threshold {threshold:N0} cells = {cfg.MinLandComponentFrac:G2} of the map): " + + $"{led.RevertedComponents} sub-threshold non-mainland components ({led.RevertedCells:N0} cells) lowered to their ring's mean seabed; " + + $"lower-only and component-only asserted; mainland untouched by definition."); + } + else r.Notes.Add("[Regions] speck revert OFF."); + + r.Labels = final; + led.CentreWasLand = final.CentreWasLand; + FillPost(led, final); + + // ═══ THE TAG, BY CONSTRUCTION ═══ + var tag = new bool[mapSize, mapSize]; + var hemi = new byte[mapSize, mapSize]; + for (int x = 0; x < mapSize; x++) + for (int y = 0; y < mapSize; y++) + { + int id = final.Id[x * mapSize + y]; + if (id == 0 || id == final.MainlandId) continue; + tag[x, y] = true; + hemi[x, y] = final.Regions[id - 1].Hemisphere; + } + r.IsIsland = tag; r.IslandHemisphere = hemi; + r.Notes.Add($"[Regions] tag by construction: {final.IslandCount} islands (N {led.PostNorth} / S {led.PostSouth}), " + + $"{final.LandCells - (final.Mainland?.SizeCells ?? 0):N0} island cells tagged; island cells min {led.PostMin} median {led.PostMedian} mean {led.PostMean:F0} max {led.PostMax}."); + return r; + } + + private static void FillPre(RegionLedger l, RegionLabels lab) + { + (l.PreNorth, l.PreSouth) = RegionLabeling.IslandsByHemisphere(lab); + var (c, mn, med, mean, mx, h) = RegionLabeling.IslandSizes(lab); + l.PreIslands = c; l.PreMin = mn; l.PreMedian = med; l.PreMean = mean; l.PreMax = mx; l.PreHistogram = h; + } + + private static void FillPost(RegionLedger l, RegionLabels lab) + { + (l.PostNorth, l.PostSouth) = RegionLabeling.IslandsByHemisphere(lab); + var (c, mn, med, mean, mx, h) = RegionLabeling.IslandSizes(lab); + l.PostIslands = c; l.PostMin = mn; l.PostMedian = med; l.PostMean = mean; l.PostMax = mx; l.PostHistogram = h; + } + } +} diff --git a/Tools/Scripts/RegionPass.cs.uid b/Tools/Scripts/RegionPass.cs.uid new file mode 100644 index 0000000..35b45d9 --- /dev/null +++ b/Tools/Scripts/RegionPass.cs.uid @@ -0,0 +1 @@ +uid://btgwrycrjllr4 diff --git a/Tools/Scripts/Shaping.cs b/Tools/Scripts/Shaping.cs index 8dbbc91..500f74f 100644 --- a/Tools/Scripts/Shaping.cs +++ b/Tools/Scripts/Shaping.cs @@ -75,7 +75,7 @@ namespace IslaApocalypse.Tools knots: null, anchors: null, hMaxSeed: p1.HMaxSeed, edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: p1.HMinSeed, hMax: p1.HMaxSeed, elapsedMs: Time.GetTicksMsec() - t0, notes: notes, - isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere); + isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere); } // ═══ WHICH CURVE (chat2/02) ═══ @@ -212,7 +212,7 @@ namespace IslaApocalypse.Tools knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed, edgeAmpRaw: edgeAmpRaw, maxEdgeShiftRaw: maxEdgeShiftRaw, hMin: hMin, hMax: hMax, elapsedMs: Time.GetTicksMsec() - t0, notes: notes, - isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere); + isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere); } /// @@ -283,7 +283,7 @@ namespace IslaApocalypse.Tools knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed, edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax, elapsedMs: Time.GetTicksMsec() - t0, notes: notes, - isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere); + isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere); } /// @@ -337,7 +337,7 @@ namespace IslaApocalypse.Tools knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed, edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax, elapsedMs: Time.GetTicksMsec() - t0, notes: notes, - isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere); + isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere); } } } diff --git a/Tools/Scripts/ShapingOracle.cs b/Tools/Scripts/ShapingOracle.cs index 9fa73df..8f9d88f 100644 --- a/Tools/Scripts/ShapingOracle.cs +++ b/Tools/Scripts/ShapingOracle.cs @@ -414,8 +414,8 @@ namespace IslaApocalypse.Tools { var c = new Check { Id = "i", Name = "moat intact — no island touches the mainland" }; int bridged = OffshoreAnalysis.BridgedCount(comps); - c.Passed = p1.HasOffshoreTag && bridged == 0; - c.Detail = !p1.HasOffshoreTag ? "no offshore tag — nothing to check" + c.Passed = p1.HasIslandTag && bridged == 0; + c.Detail = !p1.HasIslandTag ? "no island tag — nothing to check" : bridged == 0 ? $"all {comps.Count} islands are separated from mainland by water" : $"{bridged} island(s) BRIDGE to mainland land"; return c; @@ -476,7 +476,7 @@ namespace IslaApocalypse.Tools bool cl = p2.HeightClassify[x, y] >= sea; bool rl = p2.Height[x, y] >= sea; if (cl != rl) mismatch++; - if (p2.IsOffshoreIsland != null && p2.IsOffshoreIsland[x, y]) + if (p2.IsIsland != null && p2.IsIsland[x, y]) { tagged++; if (!cl || !rl) tagNotLand++; @@ -505,6 +505,80 @@ namespace IslaApocalypse.Tools }; } + // ═══ chat2/07 — the region checks ═══ + + /// (m) CENTRE IS LAND — the mainland definition held (the massif is centred); the fallback was not needed. + public static Check CentreIsLand(Pass1Result p1) + { + var c = new Check { Id = "m", Name = "mainland = centre component (centre cell is land, no fallback)" }; + if (p1.Regions == null) { c.Passed = false; c.Detail = "no region labeling on this field"; return c; } + var m = p1.Regions.Mainland; + c.Passed = p1.Regions.CentreWasLand && m != null && (p1.RegionLedger == null || p1.RegionLedger.CentreWasLandPre); + c.Detail = c.Passed + ? $"centre is land; mainland id {p1.Regions.MainlandId}, {m.SizeCells:N0} cells, centroid ({m.CentroidX:F0},{m.CentroidY:F0}); {p1.Regions.IslandCount} islands" + : "⚠ CENTRE CELL IS NOT LAND — fell back to the largest component"; + return c; + } + + /// + /// (n) ⭐ THE REVERT GUARDS, RE-PROVEN ON THE FIELDS — filter OFF vs ON: every cell of the OFF + /// field's MAINLAND component is bit-identical; every cell that changed was land in a + /// sub-threshold NON-MAINLAND component of the OFF labeling (component-only) and went DOWN, to + /// below sea (lower-only); nothing else moved. The pass asserted this as it ran; this is the + /// independent proof on the finished fields. + /// + public static Check RevertGuards(Pass1Result off, Pass1Result on, float sea, long thresholdCells) + { + var c = new Check { Id = "n", Name = "speck revert: mainland bit-identical, every change is in a sub-threshold island and lower-only" }; + if (off.Regions == null) { c.Passed = false; c.Detail = "the OFF field has no region labeling"; return c; } + int n = off.MapSize; + var lab = off.Regions; + long mainland = 0, mainlandDiff = 0, changed = 0, notIsland = 0, notSmall = 0, raised = 0, notSea = 0; + string first = null; + for (int x = 0; x < n; x++) + { + for (int y = 0; y < n; y++) + { + float a = off.Height[x, y], b = on.Height[x, y]; + int id = lab.Id[x * n + y]; + bool isMain = id != 0 && id == lab.MainlandId; + if (isMain) mainland++; + if (BitConverter.SingleToInt32Bits(a) == BitConverter.SingleToInt32Bits(b)) continue; + changed++; + if (isMain) { mainlandDiff++; first ??= $"mainland cell [{x},{y}] {a:G9} → {b:G9}"; continue; } + if (id == 0) { notIsland++; first ??= $"sea cell [{x},{y}] changed {a:G9} → {b:G9}"; continue; } + if (lab.Regions[id - 1].SizeCells >= thresholdCells) { notSmall++; first ??= $"cell [{x},{y}] of component {id} ({lab.Regions[id - 1].SizeCells} cells ≥ {thresholdCells}) changed"; } + if (b >= a) { raised++; first ??= $"cell [{x},{y}] RAISED {a:G9} → {b:G9}"; } + if (b >= sea) { notSea++; first ??= $"cell [{x},{y}] still land after revert ({b:G9})"; } + } + } + c.Passed = mainlandDiff == 0 && notIsland == 0 && notSmall == 0 && raised == 0 && notSea == 0; + c.Detail = c.Passed + ? $"all {mainland:N0} mainland cells bit-identical; {changed:N0} cells changed, every one in a sub-threshold island, lowered below sea" + : $"VIOLATION — mainland {mainlandDiff:N0} / non-island {notIsland:N0} / over-threshold {notSmall:N0} / raised {raised:N0} / still land {notSea:N0} — {first}"; + return c; + } + + /// (o) LABELS DETERMINISTIC — two generations of the same seed: id maps and component tables identical. + public static Check LabelsDeterministic(Pass1Result a, Pass1Result b) + { + var c = new Check { Id = "o", Name = "region ids deterministic per seed (two runs, id map + table identical)" }; + if (a.Regions == null || b.Regions == null) { c.Passed = false; c.Detail = "no region labeling"; return c; } + long diff = 0; int n = a.MapSize; + for (int i = 0; i < n * n; i++) if (a.Regions.Id[i] != b.Regions.Id[i]) diff++; + bool table = a.Regions.Regions.Count == b.Regions.Regions.Count && a.Regions.MainlandId == b.Regions.MainlandId; + if (table) + for (int i = 0; i < a.Regions.Regions.Count; i++) + { + var ra = a.Regions.Regions[i]; var rb = b.Regions.Regions[i]; + if (ra.SizeCells != rb.SizeCells || ra.CentroidX != rb.CentroidX || ra.CentroidY != rb.CentroidY || ra.Hemisphere != rb.Hemisphere || ra.IsMainland != rb.IsMainland) { table = false; break; } + } + c.Passed = diff == 0 && table; + c.Detail = c.Passed ? $"{a.Regions.Regions.Count} components, id map identical over {(long)n * n:N0} cells, tables identical" + : $"{diff:N0} id cells differ; tables {(table ? "identical" : "DIFFER")}"; + return c; + } + /// Render the whole oracle as a markdown table for the INDEX and the report. public static string ToMarkdownTable(IEnumerable checks) { diff --git a/Tools/Scripts/TerrainGenConfig.cs b/Tools/Scripts/TerrainGenConfig.cs index 03a3d2d..eb91120 100644 --- a/Tools/Scripts/TerrainGenConfig.cs +++ b/Tools/Scripts/TerrainGenConfig.cs @@ -274,6 +274,32 @@ namespace IslaApocalypse.Tools /// public OffshoreSettings Offshore = new OffshoreSettings(); + // ---- PASS 1c — region labeling + the speck revert (chat2/07) ---------- + + /// + /// ⭐ THE REGION-LABELING LAYER (Core.RegionLabeling, via RegionPass): 8-connected + /// land components on the classify field, mainland = the centre component, the island tag BY + /// CONSTRUCTION. Pure analysis — it changes no height — so it is ON by default without touching + /// any regression anchor. Off ⇒ no tag, no region table (the tag arrays are null). + /// + public bool RegionLabeling = true; + + /// + /// ⭐ THE SPECK REVERT — lower every non-mainland land component smaller than + /// of the map to seabed. Origin-blind; lower-only and + /// component-only, asserted; mainland never a candidate. + /// + /// ⚠ DEFAULT OFF IN THE BARE CONFIG, for exactly the reason the shelf and the islets are: the + /// raw field has small natural nubs, so with this ON the calibration pool's land histogram, the + /// curve knots and every Phase-1 / task-03 / task-04 regression dump would move at once. + /// The region batch turns it on explicitly (its preset is ON); flipping the bare default is + /// the act that re-baselines the oracles — own task, not a side effect. + /// + public bool SpeckRevert = false; + + /// The revert threshold, as a fraction of the map's AREA (scale-free). → . + public float MinLandComponentFrac = RegionPass.ThresholdMidFrac; + /// A short label for this variant, used in output filenames. E.g. "full", "base_only". public string VariantLabel = "full"; diff --git a/Tools/Scripts/Topography.cs b/Tools/Scripts/Topography.cs index 36166fe..edbcc2f 100644 --- a/Tools/Scripts/Topography.cs +++ b/Tools/Scripts/Topography.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Godot; using IslaApocalypse.Core; @@ -299,7 +300,18 @@ namespace IslaApocalypse.Tools // ~290 m). Both values are carried so the report states whether it moved, not guesses. float hMaxBeforeOffshore = hMax; OffshorePass.Result offshore = OffshorePass.Apply(height, preTrenchFalloff, mapSize, cfg.Seed, cfg.SeaLevel, cfg); - if (offshore != null) + + // ═══ PASS 1c — REGION LABELING + THE SPECK REVERT + THE ISLAND TAG (chat2/07) ═══ + // + // The general region layer over the classify field (this array), then the origin-blind + // speck revert (config-gated, lower-only, component-only), then the island tag BY + // CONSTRUCTION from the finished labeling. Labeling alone changes nothing; only the revert + // may, and only downward, and only inside a sub-threshold non-mainland component. → RegionPass. + RegionPass.Result regions = cfg.RegionLabeling + ? RegionPass.Apply(height, mapSize, cfg.SeaLevel, cfg) + : null; + + if (offshore != null || regions != null) { hMax = float.MinValue; hMin = float.MaxValue; @@ -312,14 +324,21 @@ namespace IslaApocalypse.Tools } } + var notes = new List(); + if (offshore != null) notes.AddRange(offshore.Notes); + if (regions != null) notes.AddRange(regions.Notes); + return new Pass1Result(mapSize, cfg.Seed, height, preTrenchFalloff, latitudeField, hMax, hMin, Time.GetTicksMsec() - t0, hMaxSeedBeforeOffshore: hMaxBeforeOffshore, - isOffshoreIsland: offshore?.Tag, - islandHemisphere: offshore?.Hemi, + isIsland: regions?.IsIsland, + islandHemisphere: regions?.IslandHemisphere, offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic - offshore.LiftedReverted, - notes: offshore?.Notes, - offshoreLedger: offshore?.ToLedger()); + notes: notes, + offshoreLedger: offshore?.ToLedger(), + regions: regions?.Labels, + regionLedger: regions?.Ledger, + regionsPre: regions?.LabelsPre); } } }