chat2/07: the region-labeling layer — label all land, tag by construction, tunable speck revert

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
This commit is contained in:
Stewart Howe 2026-08-22 03:50:33 -04:00
parent e8571b2778
commit c32a3b177c
19 changed files with 1336 additions and 56 deletions

View file

@ -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/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/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/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/ToolingPaths.cs` | Every tooling path, env-overridable, resolved in one place. |
| `Scripts/FileSafety.cs` | The permanent file-safety rules, as throws rather than sentences. | | `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 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 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 `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. and run-splitting on dig are still later phases.
- **No erosion, rivers, water bodies, crater carve, coast shelf or offshore islets.** Later chat2 - **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. tasks; the curve is deliberately the only pass-2 element present.

View file

@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
namespace IslaApocalypse.Core
{
/// <summary>One maximal 8-connected component of land, as the region layer exposes it.</summary>
public sealed class LandRegion
{
/// <summary>1-based, assigned in deterministic scan order (x outer, y inner) — stable per seed across runs.</summary>
public int Id;
/// <summary>Cells in the component.</summary>
public long SizeCells;
/// <summary>Centroid in map cells.</summary>
public double CentroidX, CentroidY;
/// <summary>
/// <see cref="RegionLabeling.HemiNorth"/> / <see cref="RegionLabeling.HemiSouth"/>, decided by the
/// CENTROID — one label per component; a straddler is decided by where its mass is, never per cell.
/// </summary>
public byte Hemisphere;
/// <summary>True for exactly one component: the one containing the map centre (or the flagged fallback).</summary>
public bool IsMainland;
/// <summary>Bounding box, inclusive. Convenience for overlays and guards; not part of the contract.</summary>
public int MinX, MinY, MaxX, MaxY;
}
/// <summary>The result of one labeling: the per-cell id map and the per-component table.</summary>
public sealed class RegionLabels
{
public int MapSize;
/// <summary>Per cell, <c>x * MapSize + y</c>: the component id, or 0 for water.</summary>
public int[] Id;
/// <summary>Every component, indexed by <c>Id - 1</c>, in id order.</summary>
public List<LandRegion> Regions;
/// <summary>The mainland's id (0 only if there is no land at all).</summary>
public int MainlandId;
/// <summary>
/// ⚠ 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.
/// </summary>
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];
}
/// <summary>
/// ⭐⭐ 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 <c>float[,]</c>; 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 (<see
/// cref="RegionLabels.CentreWasLand"/> = 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 <c>y / MapSize</c>; 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.
/// </summary>
public static class RegionLabeling
{
public const byte HemiNone = 0;
public const byte HemiNorth = 1;
public const byte HemiSouth = 2;
/// <summary>The convention, in one place. Every consumer reads hemisphere through this.</summary>
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 };
/// <summary>
/// Label every 8-connected land component of <paramref name="classify"/> (land = height ≥
/// <paramref name="sea"/>). Pure: the field is read, never written.
/// </summary>
public static RegionLabels Label(float[,] classify, int mapSize, float sea)
{
int n = mapSize;
var id = new int[n * n];
var regions = new List<LandRegion>();
var stack = new Stack<int>();
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;
}
/// <summary>
/// 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.
/// </summary>
public static (int count, long min, long median, double mean, long max, int[] histogram)
IslandSizes(RegionLabels labels)
{
var sizes = new List<long>();
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);
}
/// <summary>Histogram bin edges (cells): [0,64) [64,256) [256,1024) [1024,4096) [4096,16384) [16384,∞).</summary>
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]}";
/// <summary>Island counts per hemisphere (non-mainland components, by centroid).</summary>
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);
}
}
}

View file

@ -0,0 +1 @@
uid://bsb27ghrajnhc

View file

@ -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/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/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/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/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/Pass1Result.cs` | The height field **and the Phase-2 seams** |
| `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles | | `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 > 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`. > 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 > `Pass1Result.IsIsland` / `IslandHemisphere` (carried through `Pass2Result`) mark every cell of every
> the islet layer lifted above sea, with its hemisphere. **NORTH = rows `[0, MapSize/2)`, SOUTH = > non-mainland land component — natural detached masses and offshore-pass islands alike — with the
> `[MapSize/2, MapSize)`** — y runs south, as the spine's southern fade and the southern sinker > COMPONENT's hemisphere (by centroid). **NORTH = rows `[0, MapSize/2)`, SOUTH = `[MapSize/2,
> already encode. A biome / fertility / placement pass reads it from there and never re-derives > MapSize)`** — y runs south. `Pass1Result.Regions` carries the full per-component table. A biome /
> island-land from geometry. Null when offshore is off. > fertility / placement pass reads these and never re-derives island-land from geometry. Null when
> region labeling is off. (chat2/0506 tagged only what the offshore pass raised — fixed in 07.)
> ### ⚠ The one honest coupling: islets TURN WATER INTO LAND. > ### ⚠ The one honest coupling: islets TURN WATER INTO LAND.
> >

View file

@ -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")

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools namespace IslaApocalypse.Tools
{ {
@ -44,17 +45,15 @@ namespace IslaApocalypse.Tools
/// </summary> /// </summary>
public static class OffshoreAnalysis public static class OffshoreAnalysis
{ {
public const byte HemiNone = 0; // The convention now lives in Core (RegionLabeling, chat2/07) — one definition; these are aliases.
public const byte HemiNorth = 1; public const byte HemiNone = RegionLabeling.HemiNone;
public const byte HemiSouth = 2; public const byte HemiNorth = RegionLabeling.HemiNorth;
public const byte HemiSouth = RegionLabeling.HemiSouth;
/// <summary>The convention, in one place. Every consumer of the tag reads hemisphere through this.</summary> /// <summary>→ <see cref="RegionLabeling.HemisphereOfRow"/>.</summary>
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 public static string HemisphereName(byte h) => RegionLabeling.HemisphereName(h);
{
HemiNorth => "north", HemiSouth => "south", _ => "none",
};
// 8-connectivity, fixed order. // 8-connectivity, fixed order.
private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 }; private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 };

View file

@ -12,6 +12,10 @@ namespace IslaApocalypse.Tools
/// NO FORCED COUNT. (chat2/05's version of this tool — faithful / floor_only / hybrid / dense — /// 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.) /// 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 ═══ /// ═══ WHAT IT PRODUCES — a fixed budget: 4 plates + a count table + a diagnosis ═══
/// ///
/// PLATES (4, at ISLA_MAPSIZE, grayscale + .f32 + relief + tags overlay): /// PLATES (4, at ISLA_MAPSIZE, grayscale + .f32 + relief + tags overlay):
@ -253,7 +257,7 @@ namespace IslaApocalypse.Tools
Pass2Result p2 = Shaping.Shape(p1, cfg); Pass2Result p2 = Shaping.Shape(p1, cfg);
if (!notesShown) { foreach (string n in p1.Notes) GD.Print(" " + n); notesShown = true; } 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 (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps);
var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0); var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0);
var checks = new List<ShapingOracle.Check> var checks = new List<ShapingOracle.Check>
@ -300,7 +304,7 @@ namespace IslaApocalypse.Tools
cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone(); cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone();
Pass1Result p1 = Topography.Generate(cfg); Pass1Result p1 = Topography.Generate(cfg);
Pass2Result p2 = Shaping.Shape(p1, 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 (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps);
var moat = ShapingOracle.MoatIntact(p1, comps); moat.Name += $" [plate {lv.Label} {seed}]"; var moat = ShapingOracle.MoatIntact(p1, comps); moat.Name += $" [plate {lv.Label} {seed}]";
var tag = ShapingOracle.TagCoastlineConsistent(p2, sea); tag.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")); .SavePng(Path.Combine(dir, "relief.png"));
// ⭐ The tag overlay — the one artifact that shows the DATA this pass set. // ⭐ 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")); countN, countS, Path.Combine(dir, "tags.png"));
} }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools namespace IslaApocalypse.Tools
{ {
@ -70,27 +71,34 @@ namespace IslaApocalypse.Tools
/// </summary> /// </summary>
public readonly float HMaxSeedBeforeOffshore; 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 // 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 // pass (biome, fertility, placement) can find island land without re-deriving it from
// from geometry. Downstream that ignores it is unaffected; downstream that reads it gets a // geometry. Since chat2/07 it is a CONSEQUENCE OF LABELING: every cell of every non-mainland
// clean flag. Both arrays are NULL when the offshore layer is off — a consumer checks for // land component is tagged — the big organic detached masses the same as an offshore-pass
// null, not for all-false. // dot (chat2/0506 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.
/// <summary>Per column: is this land an island (any non-mainland land component)? Null when labeling is off.</summary>
public readonly bool[,] IsIsland;
/// <summary> /// <summary>
/// Per column: is this land an offshore island (as opposed to mainland)? Set for every cell /// Per column: <see cref="RegionLabeling.HemiNorth"/> / <see cref="RegionLabeling.HemiSouth"/>
/// the islet layer lifted from below sea to at-or-above sea. Null when offshore is off. /// for tagged cells (the COMPONENT's hemisphere, by centroid), <see cref="RegionLabeling.HemiNone"/>
/// </summary> /// otherwise. Null when labeling is off.
public readonly bool[,] IsOffshoreIsland;
/// <summary>
/// Per column: <see cref="OffshoreAnalysis.HemiNorth"/> / <see cref="OffshoreAnalysis.HemiSouth"/>
/// for tagged cells, <see cref="OffshoreAnalysis.HemiNone"/> otherwise. The convention is the
/// row midline — see <see cref="OffshoreAnalysis"/>. Null when offshore is off.
/// </summary> /// </summary>
public readonly byte[,] IslandHemisphere; public readonly byte[,] IslandHemisphere;
/// <summary>⭐ The region layer's output for this field: id map + per-component table (post-revert). Null when labeling is off.</summary>
public readonly RegionLabels Regions;
/// <summary>The labeling BEFORE the speck revert (== <see cref="Regions"/> when the revert is off or removed nothing). For the overlay's "where a speck was". Null when labeling is off.</summary>
public readonly RegionLabels RegionsPre;
/// <summary>The region pass's numbers — pre/post-revert counts and sizes, what was reverted. Null when labeling is off.</summary>
public readonly RegionLedger RegionLedger;
/// <summary>Cells the islet layer lifted above sea. The reference printed this too.</summary> /// <summary>Cells the islet layer lifted above sea. The reference printed this too.</summary>
public readonly long OffshoreLiftedCells; public readonly long OffshoreLiftedCells;
@ -106,8 +114,9 @@ namespace IslaApocalypse.Tools
public Pass1Result(int mapSize, int seed, float[,] height, float[,] preTrenchFalloff, public Pass1Result(int mapSize, int seed, float[,] height, float[,] preTrenchFalloff,
float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs, float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs,
float hMaxSeedBeforeOffshore = float.NaN, float hMaxSeedBeforeOffshore = float.NaN,
bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null, bool[,] isIsland = null, byte[,] islandHemisphere = null,
long offshoreLiftedCells = 0, IReadOnlyList<string> notes = null, OffshoreLedger offshoreLedger = null) long offshoreLiftedCells = 0, IReadOnlyList<string> notes = null, OffshoreLedger offshoreLedger = null,
RegionLabels regions = null, RegionLedger regionLedger = null, RegionLabels regionsPre = null)
{ {
MapSize = mapSize; MapSize = mapSize;
Seed = seed; Seed = seed;
@ -118,15 +127,18 @@ namespace IslaApocalypse.Tools
HMinSeed = hMinSeed; HMinSeed = hMinSeed;
ElapsedMs = elapsedMs; ElapsedMs = elapsedMs;
HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore; HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore;
IsOffshoreIsland = isOffshoreIsland; IsIsland = isIsland;
IslandHemisphere = islandHemisphere; IslandHemisphere = islandHemisphere;
Regions = regions;
RegionsPre = regionsPre;
RegionLedger = regionLedger;
OffshoreLiftedCells = offshoreLiftedCells; OffshoreLiftedCells = offshoreLiftedCells;
OffshoreLedger = offshoreLedger; OffshoreLedger = offshoreLedger;
Notes = notes ?? System.Array.Empty<string>(); Notes = notes ?? System.Array.Empty<string>();
} }
/// <summary>Whether the offshore layer ran on this field (the tag arrays are present).</summary> /// <summary>Whether the region layer ran on this field (the tag arrays are present).</summary>
public bool HasOffshoreTag => IsOffshoreIsland != null; public bool HasIslandTag => IsIsland != null;
/// <summary>Fraction of the map at or above the sea threshold. A cheap shape sanity number.</summary> /// <summary>Fraction of the map at or above the sea threshold. A cheap shape sanity number.</summary>
public float LandFraction(float seaLevel) public float LandFraction(float seaLevel)

View file

@ -95,10 +95,10 @@ namespace IslaApocalypse.Tools
// it from here, checks for null (offshore off), and never re-derives island-land from // it from here, checks for null (offshore off), and never re-derives island-land from
// geometry. // geometry.
/// <summary>→ <see cref="Pass1Result.IsOffshoreIsland"/>, the same array. Null when offshore is off.</summary> /// <summary>→ <see cref="Pass1Result.IsIsland"/>, the same array. Null when region labeling is off.</summary>
public readonly bool[,] IsOffshoreIsland; public readonly bool[,] IsIsland;
/// <summary>→ <see cref="Pass1Result.IslandHemisphere"/>, the same array. Null when offshore is off.</summary> /// <summary>→ <see cref="Pass1Result.IslandHemisphere"/>, the same array. Null when region labeling is off.</summary>
public readonly byte[,] IslandHemisphere; public readonly byte[,] IslandHemisphere;
/// <summary>Was shelf detail applied? Requires <see cref="CurveOn"/> — it warps the curve's knots.</summary> /// <summary>Was shelf detail applied? Requires <see cref="CurveOn"/> — it warps the curve's knots.</summary>
@ -136,9 +136,9 @@ namespace IslaApocalypse.Tools
bool curveOn, bool detailOn, string curveModeLabel, string variantLabel, bool curveOn, bool detailOn, string curveModeLabel, string variantLabel,
ContinuousCurve continuous, CurveKnots knots, CurveAnchors anchors, float hMaxSeed, ContinuousCurve continuous, CurveKnots knots, CurveAnchors anchors, float hMaxSeed,
float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs, float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs,
List<string> notes, bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null) List<string> notes, bool[,] isIsland = null, byte[,] islandHemisphere = null)
{ {
IsOffshoreIsland = isOffshoreIsland; IsIsland = isIsland;
IslandHemisphere = islandHemisphere; IslandHemisphere = islandHemisphere;
MapSize = mapSize; MapSize = mapSize;
Seed = seed; Seed = seed;

View file

@ -0,0 +1,611 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐ 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
/// </summary>
public partial class RegionLabelingTool : Node
{
private static readonly int[] DefaultTableSeeds =
{
1063685222, 20260821, 8675309, 123456789, 271828182, 999999937, 90210, 424242,
};
/// <summary>⚠ Task 01's pool, verbatim — the curve's identity.</summary>
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<Level>
{
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<ShapingOracle.Check>();
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<ShapingOracle.Check>();
{
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<Row>();
var naturalCount = new Dictionary<int, int>();
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.Check>
{
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<Row>();
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<int, int> 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 0306 did --------------------
private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
{
var rawPool = new LandHistogram(sea);
var pass1 = new Dictionary<int, Pass1Result>();
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<Level> levels, List<Row> 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>(); 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<Level> levels, List<Row> 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<Level> levels, List<Row> rows, List<Row> plateRows,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> 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<int>();
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;
}
}
}

View file

@ -0,0 +1 @@
uid://ceeyncymgtjpm

View file

@ -0,0 +1,77 @@
using System;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>A distinct, saturated colour per island id — golden-angle hue walk, three value steps.</summary>
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);
}
/// <param name="labels">The finished field's labeling (post-revert).</param>
/// <param name="labelsPre">The pre-revert labeling, or null — its reverted components are painted <see cref="Reverted"/>.</param>
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}");
}
}
}

View file

@ -0,0 +1 @@
uid://c05blbudak0as

203
Tools/Scripts/RegionPass.cs Normal file
View file

@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>The region pass's numbers, carried on <c>Pass1Result</c> for the report.</summary>
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();
}
/// <summary>
/// ⭐ 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 <c>HMaxSeed</c>
/// is retaken and anything classifies.
///
/// ═══ WHAT IT DOES, IN ORDER ═══
///
/// 1. LABEL <see cref="RegionLabeling.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: <c>TerrainGenConfig.SpeckRevert</c>; threshold
/// <c>MinLandComponentFrac</c> × 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).
/// </summary>
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<string> Notes = new();
}
/// <summary>The three thresholds of the chat2/07 batch, fractions of the map's area; Mid is the config default.</summary>
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<int, LandRegion>();
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<int, double>();
var ringCnt = new Dictionary<int, long>();
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<int, float>();
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<int, long>();
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;
}
}
}

View file

@ -0,0 +1 @@
uid://btgwrycrjllr4

View file

@ -75,7 +75,7 @@ namespace IslaApocalypse.Tools
knots: null, anchors: null, hMaxSeed: p1.HMaxSeed, knots: null, anchors: null, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: p1.HMinSeed, hMax: p1.HMaxSeed, edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: p1.HMinSeed, hMax: p1.HMaxSeed,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes, elapsedMs: Time.GetTicksMsec() - t0, notes: notes,
isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere); isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere);
} }
// ═══ WHICH CURVE (chat2/02) ═══ // ═══ WHICH CURVE (chat2/02) ═══
@ -212,7 +212,7 @@ namespace IslaApocalypse.Tools
knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed, knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: edgeAmpRaw, maxEdgeShiftRaw: maxEdgeShiftRaw, hMin: hMin, hMax: hMax, edgeAmpRaw: edgeAmpRaw, maxEdgeShiftRaw: maxEdgeShiftRaw, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes, elapsedMs: Time.GetTicksMsec() - t0, notes: notes,
isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere); isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere);
} }
/// <summary> /// <summary>
@ -283,7 +283,7 @@ namespace IslaApocalypse.Tools
knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed, knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax, edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes, elapsedMs: Time.GetTicksMsec() - t0, notes: notes,
isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere); isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere);
} }
/// <summary> /// <summary>
@ -337,7 +337,7 @@ namespace IslaApocalypse.Tools
knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed, knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax, edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes, elapsedMs: Time.GetTicksMsec() - t0, notes: notes,
isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere); isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere);
} }
} }
} }

View file

@ -414,8 +414,8 @@ namespace IslaApocalypse.Tools
{ {
var c = new Check { Id = "i", Name = "moat intact — no island touches the mainland" }; var c = new Check { Id = "i", Name = "moat intact — no island touches the mainland" };
int bridged = OffshoreAnalysis.BridgedCount(comps); int bridged = OffshoreAnalysis.BridgedCount(comps);
c.Passed = p1.HasOffshoreTag && bridged == 0; c.Passed = p1.HasIslandTag && bridged == 0;
c.Detail = !p1.HasOffshoreTag ? "no offshore tag — nothing to check" c.Detail = !p1.HasIslandTag ? "no island tag — nothing to check"
: bridged == 0 ? $"all {comps.Count} islands are separated from mainland by water" : bridged == 0 ? $"all {comps.Count} islands are separated from mainland by water"
: $"{bridged} island(s) BRIDGE to mainland land"; : $"{bridged} island(s) BRIDGE to mainland land";
return c; return c;
@ -476,7 +476,7 @@ namespace IslaApocalypse.Tools
bool cl = p2.HeightClassify[x, y] >= sea; bool cl = p2.HeightClassify[x, y] >= sea;
bool rl = p2.Height[x, y] >= sea; bool rl = p2.Height[x, y] >= sea;
if (cl != rl) mismatch++; if (cl != rl) mismatch++;
if (p2.IsOffshoreIsland != null && p2.IsOffshoreIsland[x, y]) if (p2.IsIsland != null && p2.IsIsland[x, y])
{ {
tagged++; tagged++;
if (!cl || !rl) tagNotLand++; if (!cl || !rl) tagNotLand++;
@ -505,6 +505,80 @@ namespace IslaApocalypse.Tools
}; };
} }
// ═══ chat2/07 — the region checks ═══
/// <summary>(m) CENTRE IS LAND — the mainland definition held (the massif is centred); the fallback was not needed.</summary>
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;
}
/// <summary>
/// (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.
/// </summary>
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;
}
/// <summary>(o) LABELS DETERMINISTIC — two generations of the same seed: id maps and component tables identical.</summary>
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;
}
/// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary> /// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary>
public static string ToMarkdownTable(IEnumerable<Check> checks) public static string ToMarkdownTable(IEnumerable<Check> checks)
{ {

View file

@ -274,6 +274,32 @@ namespace IslaApocalypse.Tools
/// </summary> /// </summary>
public OffshoreSettings Offshore = new OffshoreSettings(); public OffshoreSettings Offshore = new OffshoreSettings();
// ---- PASS 1c — region labeling + the speck revert (chat2/07) ----------
/// <summary>
/// ⭐ THE REGION-LABELING LAYER (<c>Core.RegionLabeling</c>, via <c>RegionPass</c>): 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).
/// </summary>
public bool RegionLabeling = true;
/// <summary>
/// ⭐ THE SPECK REVERT — lower every non-mainland land component smaller than
/// <see cref="MinLandComponentFrac"/> 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.
/// </summary>
public bool SpeckRevert = false;
/// <summary>The revert threshold, as a fraction of the map's AREA (scale-free). → <see cref="RegionPass.ThresholdMidFrac"/>.</summary>
public float MinLandComponentFrac = RegionPass.ThresholdMidFrac;
/// <summary>A short label for this variant, used in output filenames. E.g. "full", "base_only".</summary> /// <summary>A short label for this variant, used in output filenames. E.g. "full", "base_only".</summary>
public string VariantLabel = "full"; public string VariantLabel = "full";

View file

@ -1,3 +1,4 @@
using System.Collections.Generic;
using Godot; using Godot;
using IslaApocalypse.Core; 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. // ~290 m). Both values are carried so the report states whether it moved, not guesses.
float hMaxBeforeOffshore = hMax; float hMaxBeforeOffshore = hMax;
OffshorePass.Result offshore = OffshorePass.Apply(height, preTrenchFalloff, mapSize, cfg.Seed, cfg.SeaLevel, cfg); 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; hMax = float.MinValue;
hMin = float.MaxValue; hMin = float.MaxValue;
@ -312,14 +324,21 @@ namespace IslaApocalypse.Tools
} }
} }
var notes = new List<string>();
if (offshore != null) notes.AddRange(offshore.Notes);
if (regions != null) notes.AddRange(regions.Notes);
return new Pass1Result(mapSize, cfg.Seed, height, preTrenchFalloff, latitudeField, return new Pass1Result(mapSize, cfg.Seed, height, preTrenchFalloff, latitudeField,
hMax, hMin, Time.GetTicksMsec() - t0, hMax, hMin, Time.GetTicksMsec() - t0,
hMaxSeedBeforeOffshore: hMaxBeforeOffshore, hMaxSeedBeforeOffshore: hMaxBeforeOffshore,
isOffshoreIsland: offshore?.Tag, isIsland: regions?.IsIsland,
islandHemisphere: offshore?.Hemi, islandHemisphere: regions?.IslandHemisphere,
offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic - offshore.LiftedReverted, offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic - offshore.LiftedReverted,
notes: offshore?.Notes, notes: notes,
offshoreLedger: offshore?.ToLedger()); offshoreLedger: offshore?.ToLedger(),
regions: regions?.Labels,
regionLedger: regions?.Ledger,
regionsPre: regions?.LabelsPre);
} }
} }
} }