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:
parent
e8571b2778
commit
c32a3b177c
19 changed files with 1336 additions and 56 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
229
Core/Scripts/RegionLabeling.cs
Normal file
229
Core/Scripts/RegionLabeling.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Core/Scripts/RegionLabeling.cs.uid
Normal file
1
Core/Scripts/RegionLabeling.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bsb27ghrajnhc
|
||||||
|
|
@ -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/05–06 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.
|
||||||
>
|
>
|
||||||
|
|
|
||||||
6
Tools/Scenes/RegionLabelingTool.tscn
Normal file
6
Tools/Scenes/RegionLabelingTool.tscn
Normal 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")
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/05–06 tagged only what the offshore pass raised; that was the bug). Both arrays
|
||||||
|
// are NULL when region labeling is off — a consumer checks for null, not for all-false.
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
611
Tools/Scripts/RegionLabelingTool.cs
Normal file
611
Tools/Scripts/RegionLabelingTool.cs
Normal 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 03–06 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/RegionLabelingTool.cs.uid
Normal file
1
Tools/Scripts/RegionLabelingTool.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ceeyncymgtjpm
|
||||||
77
Tools/Scripts/RegionOverlayRenderer.cs
Normal file
77
Tools/Scripts/RegionOverlayRenderer.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/RegionOverlayRenderer.cs.uid
Normal file
1
Tools/Scripts/RegionOverlayRenderer.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c05blbudak0as
|
||||||
203
Tools/Scripts/RegionPass.cs
Normal file
203
Tools/Scripts/RegionPass.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/RegionPass.cs.uid
Normal file
1
Tools/Scripts/RegionPass.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://btgwrycrjllr4
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue