islaApocalypse-v2/Tools/Scripts/RegionLabelingTool.cs
beezm c32a3b177c 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
2026-08-22 03:50:33 -04:00

611 lines
35 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐ THE REGION-LABELING BATCH (chat2/07) — the general region layer on the current terrain, the
/// island tag fixed by construction, and the tunable speck revert swept.
///
/// ═══ WHAT IT PRODUCES — a fixed budget: 4 plates + the count/size table ═══
///
/// PLATES (4 fields, each grayscale + .f32 + relief + the LABELED-REGIONS overlay + the tag overlay):
/// {plate}_threshold_low / _mid / _high three revert thresholds on ONE seed — the developer
/// dials "where too-small-to-keep sits" by eye.
/// {second}_threshold_mid the preset on a second seed — the table seed with the
/// most NATURAL islands (offshore off), auto-picked or
/// ISLA_SECOND_SEED — labeling + threshold are not
/// seed-specific; the big organic masses tag correctly.
///
/// THE COUNT/SIZE TABLE (data): per seed, natural islands (offshore off), pre-revert islands, and
/// post-revert islands at each threshold, with size min / median / mean / max and a log-spaced
/// size histogram — the instrument the later southern-stretch step tunes against.
///
/// Every "current terrain" field = shelf ON + the chat2/06 organic preset (`density_mid`) + region
/// labeling ON; the revert is the variable. The curve is the tagged curve, unchanged.
///
/// ═══ RUNNING IT ═══
///
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/RegionLabelingTool.tscn
///
/// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR
/// ISLA_MAPSIZE plate + table size (default 4096)
/// ISLA_TABLE_SIZE count-table size (default = ISLA_MAPSIZE; a probe may drop it)
/// ISLA_CALIB_SIZE curve calibration size (default 2048, task 01's)
/// ISLA_TABLE_SEEDS the table seeds (default 8 below)
/// ISLA_PLATE_SEED the three-threshold seed (default 1063685222)
/// ISLA_SECOND_SEED the second plate seed (default 0 = auto: most natural islands)
/// ISLA_THR_LOW / ISLA_THR_MID / ISLA_THR_HIGH thresholds, fraction of map area (probe overrides)
/// ISLA_TABLE_ONLY=1 probe: table only (no regressions, no plates)
/// ISLA_SKIP_8K=1 skip the 8192 regression (a4)
/// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE / ISLA_T06_SOURCE the regression dumps' batches
/// </summary>
public partial class RegionLabelingTool : Node
{
private static readonly int[] DefaultTableSeeds =
{
1063685222, 20260821, 8675309, 123456789, 271828182, 999999937, 90210, 424242,
};
/// <summary>⚠ Task 01's pool, verbatim — the curve's identity.</summary>
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
private const int DefaultMapSize = 4096;
private const int DefaultCalibSize = 2048;
private const int GallerySize = 8192;
public override void _Ready()
{
try { Run(); }
catch (Exception e)
{
GD.PrintErr("==================================================================");
GD.PrintErr($" REFUSED: {e.Message}");
GD.PrintErr(e.StackTrace);
GD.PrintErr("==================================================================");
GetTree().Quit(2);
}
}
private sealed class Level { public string Label; public float Frac; }
private sealed class Row
{
public string Level; public int Seed; public long ThresholdCells;
public int Natural, NaturalN, NaturalS; // offshore OFF, revert OFF
public int Pre, PreN, PreS; // offshore ON, revert OFF
public int Post, PostN, PostS; // offshore ON, revert at this level
public int RevertedComps; public long RevertedCells;
public long PreMin, PreMed, PreMax, PostMin, PostMed, PostMax; public double PreMean, PostMean;
public int[] PreHist, PostHist;
public long MainlandCells; public bool Ok; public ulong Ms;
}
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
int task = EnvInt("ISLA_TASK", 7);
string descr = EnvStr("ISLA_BATCH", "region_labeling");
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
int tableSize = EnvInt("ISLA_TABLE_SIZE", mapSize);
int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds);
int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222);
int secondEnv = EnvInt("ISLA_SECOND_SEED", 0);
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
string t06Source = EnvStr("ISLA_T06_SOURCE", "06_offshore_organic_tune");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1";
bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1";
var levels = new List<Level>
{
new() { Label = "threshold_low", Frac = EnvFloat("ISLA_THR_LOW", RegionPass.ThresholdLowFrac) },
new() { Label = "threshold_mid", Frac = EnvFloat("ISLA_THR_MID", RegionPass.ThresholdMidFrac) },
new() { Label = "threshold_high", Frac = EnvFloat("ISLA_THR_HIGH", RegionPass.ThresholdHighFrac) },
};
Level mid = levels[1];
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
var anchors = CurveAnchors.Default;
float sea = 0.15f;
GD.Print("==================================================================");
GD.Print(" REGION LABELING (chat2/07) — label all land, fix the tag, tunable speck revert");
GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} (plates) table at {tableSize} curve calibrated at {calibSize} (offshore OFF)");
GD.Print($"table : {string.Join(", ", tableSeeds)}");
GD.Print($"plate seed: {plateSeed} second seed: {(secondEnv > 0 ? secondEnv.ToString() : "auto (most natural islands)")}");
foreach (var l in levels) GD.Print($" {l.Label,-15} {l.Frac:G3} of map area = {Cells(l.Frac, mapSize):N0} cells at {mapSize} ({Cells(l.Frac, tableSize):N0} at {tableSize})");
GD.Print($"terrain : shelf ON + offshore {OffshoreSettings.Organic().Describe()}");
GD.Print($"contract : classify field · land 8-connected · mainland = centre component · id/size/centroid/hemisphere(centroid)/isMainland");
GD.Print($"batch : {batchRoot}{(tableOnly ? " ISLA_TABLE_ONLY a probe, not the batch of record" : "")}");
GD.Print("==================================================================");
// ═══ 0. THE CURVE ═══
GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, offshore off) ---");
var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors);
GD.Print($" {knots}");
GD.Print($" {calibration.Describe()}");
TerrainGenConfig Cfg(int size, int seed, string label, bool offshoreOn, bool revertOn, float frac)
{
var c = BaseConfig(size, seed, knots, anchors, calibration, label);
if (offshoreOn) { c.CoastShelf = true; c.Offshore = OffshoreSettings.Organic(); }
c.RegionLabeling = true;
c.SpeckRevert = revertOn;
c.MinLandComponentFrac = frac;
return c;
}
// ═══ 1. REGRESSIONS ═══
var hard = new List<ShapingOracle.Check>();
if (!tableOnly)
{
GD.Print($"\n--- 1. REGRESSIONS at {calibSize}, seed {plateSeed} ---");
var offCfg = Cfg(calibSize, plateSeed, "off", offshoreOn: false, revertOn: false, mid.Frac);
Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false;
Pass2Result pOff = Shaping.Shape(p1, curveOff);
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF, revert OFF (labeling on) == Phase-1 .f32 dump",
pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
Pass2Result pRest = Shaping.Shape(p1, offCfg);
string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32");
float[,] t03 = HeightField.Load(t03Dump, calibSize);
hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF, revert OFF (labeling on) == task-03 .f32 dump",
pRest.Height, t03, calibSize, t03Dump));
// Informational: offshore OFF, revert ON — how many NATURAL speck cells the revert removes
// from the bare field. Allowed to differ (the revert may change terrain); reported, not asserted.
var revCfg = Cfg(calibSize, plateSeed, "off_revert", offshoreOn: false, revertOn: true, mid.Frac);
Pass1Result p1Rev = Topography.Generate(revCfg);
Pass2Result pRev = Shaping.Shape(p1Rev, revCfg);
var info = ShapingOracle.DumpRegression("a3r", "(informational) offshore OFF, revert ON at threshold_mid vs task-03 dump — the natural specks removed", pRev.Height, t03, calibSize, t03Dump);
info.Detail = (info.Passed ? "no natural speck below the threshold on this seed — " : "") + info.Detail +
$" · reverted {p1Rev.RegionLedger.RevertedComponents} natural components / {p1Rev.RegionLedger.RevertedCells:N0} cells";
info.Passed = true;
hard.Add(info);
var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only";
Pass1Result p1Shelf = Topography.Generate(shelfCfg);
var j0 = ShapingOracle.MainlandUnmoved(p1, p1Shelf, sea);
j0.Name = "shelf alone: every land cell bit-identical (shelf is below-sea only)";
hard.Add(j0);
hard.Add(ShapingOracle.CentreIsLand(p1));
foreach (var c in hard) GD.Print(" " + c);
if (!skip8k)
{
string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32");
if (File.Exists(t04Dump))
{
GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, revert OFF …");
var gCfg = Cfg(GallerySize, plateSeed, "off", offshoreOn: false, revertOn: false, mid.Frac);
Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg);
var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF, revert OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump",
pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump);
hard.Add(a4); GD.Print(" " + a4);
}
else GD.Print($" a4: ⚠ skipped — no 04 gallery dump at {t04Dump}");
}
else GD.Print(" a4: skipped (ISLA_SKIP_8K)");
// ⭐ a6 — labeling ON, revert OFF, on the chat2/06 preset: bit-identical to the 06 batch's
// render field. Labeling is pure analysis; only the revert may change terrain.
string t06Dump = Path.Combine(ToolingPaths.BatchesRoot, t06Source, $"{plateSeed}_density_mid", "height.f32");
if (File.Exists(t06Dump) && mapSize == 4096)
{
var c6 = Cfg(mapSize, plateSeed, "density_mid", offshoreOn: true, revertOn: false, mid.Frac);
Pass2Result p6 = Shaping.Shape(Topography.Generate(c6), c6);
var a6 = ShapingOracle.DumpRegression("a6", "offshore density_mid ON, labeling ON, revert OFF == task-06 .f32 dump (labeling is pure analysis)",
p6.Height, HeightField.Load(t06Dump, mapSize), mapSize, t06Dump);
hard.Add(a6); GD.Print(" " + a6);
}
else GD.Print($" a6: ⚠ skipped — {(mapSize != 4096 ? "map size is not the 06 batch's 4096" : $"no 06 dump at {t06Dump}")}");
}
// ═══ 2. DETERMINISM ═══
GD.Print($"\n--- 2. DETERMINISM at {tableSize}, seed {plateSeed}, {mid.Label} ---");
var perFieldChecks = new List<ShapingOracle.Check>();
{
var cA = Cfg(tableSize, plateSeed, mid.Label, true, true, mid.Frac);
var cB = Cfg(tableSize, plateSeed, mid.Label, true, true, mid.Frac);
var det = ShapingOracle.LabelsDeterministic(Topography.Generate(cA), Topography.Generate(cB));
det.Name += $" [{plateSeed}]";
perFieldChecks.Add(det); GD.Print(" " + det);
}
// ═══ 3. THE COUNT/SIZE TABLE ═══
GD.Print($"\n--- 3. COUNT/SIZE TABLE at {tableSize} ---");
var rows = new List<Row>();
var naturalCount = new Dictionary<int, int>();
bool notesShown = false;
foreach (int seed in tableSeeds)
{
// natural: offshore OFF, revert OFF
Pass1Result pNat = Topography.Generate(Cfg(tableSize, seed, "natural", false, false, mid.Frac));
var (natN, natS) = RegionLabeling.IslandsByHemisphere(pNat.Regions);
naturalCount[seed] = pNat.Regions.IslandCount;
// pre: offshore ON, revert OFF
var cPre = Cfg(tableSize, seed, "pre", true, false, mid.Frac);
Pass1Result pPre = Topography.Generate(cPre);
{
var cj = ShapingOracle.MainlandUnmoved(pNat, pPre, sea); cj.Name += $" [offshore on vs off, {seed}]"; perFieldChecks.Add(cj);
var cm = ShapingOracle.CentreIsLand(pPre); cm.Name += $" [pre {seed}]"; perFieldChecks.Add(cm);
}
if (!notesShown) { foreach (string n in pPre.Notes) GD.Print(" " + n); }
GD.Print($" seed {seed,-11} natural islands {pNat.Regions.IslandCount,3} (N {natN} / S {natS}) pre-revert {pPre.Regions.IslandCount,3} (N {pPre.RegionLedger.PostNorth} / S {pPre.RegionLedger.PostSouth}) mainland {pPre.Regions.Mainland.SizeCells:N0} cells");
foreach (var lv in levels)
{
var cfg = Cfg(tableSize, seed, lv.Label, true, true, lv.Frac);
Pass1Result p1 = Topography.Generate(cfg);
Pass2Result p2 = Shaping.Shape(p1, cfg);
if (!notesShown) { foreach (string n in p1.Notes) if (n.StartsWith("[Regions]")) GD.Print(" " + n); notesShown = true; }
var led = p1.RegionLedger;
long thr = led.ThresholdCells;
var comps = OffshoreAnalysis.Components(p1.IsIsland, p1.Height, sea, tableSize);
var checks = new List<ShapingOracle.Check>
{
ShapingOracle.CentreIsLand(p1),
ShapingOracle.RevertGuards(pPre, p1, sea, thr),
ShapingOracle.MoatIntact(p1, comps),
ShapingOracle.TagCoastlineConsistent(p2, sea),
ShapingOracle.HMaxAfterOffshore(p1),
ShapingOracle.ClassifyFidelity(p1, p2),
};
foreach (var c in checks) { c.Name += $" [{lv.Label} {seed}]"; perFieldChecks.Add(c); }
bool ok = checks.TrueForAll(c => c.Passed);
var row = new Row
{
Level = lv.Label, Seed = seed, ThresholdCells = thr,
Natural = pNat.Regions.IslandCount, NaturalN = natN, NaturalS = natS,
Pre = led.PreIslands, PreN = led.PreNorth, PreS = led.PreSouth,
Post = led.PostIslands, PostN = led.PostNorth, PostS = led.PostSouth,
RevertedComps = led.RevertedComponents, RevertedCells = led.RevertedCells,
PreMin = led.PreMin, PreMed = led.PreMedian, PreMean = led.PreMean, PreMax = led.PreMax,
PostMin = led.PostMin, PostMed = led.PostMedian, PostMean = led.PostMean, PostMax = led.PostMax,
PreHist = led.PreHistogram, PostHist = led.PostHistogram,
MainlandCells = p1.Regions.Mainland.SizeCells, Ok = ok, Ms = p1.ElapsedMs,
};
rows.Add(row);
GD.Print($" {lv.Label,-15} seed {seed,-11} thr {thr,5} pre {row.Pre,3} → post {row.Post,3} (N {row.PostN,2} / S {row.PostS,2}) reverted {row.RevertedComps,3} comps / {row.RevertedCells,7:N0} cells " +
$"post size min {row.PostMin,5} med {row.PostMed,5} max {row.PostMax,6} {(ok ? "ok" : " CHECK FAILED")} {p1.ElapsedMs} ms");
}
}
// ═══ 4. THE PLATES ═══
int secondSeed = secondEnv > 0 ? secondEnv : PickSecondSeed(naturalCount, tableSeeds, plateSeed);
GD.Print($"\n second seed: {secondSeed}{(secondEnv > 0 ? " (ISLA_SECOND_SEED)" : $" (auto: most natural islands among the table seeds {naturalCount.GetValueOrDefault(secondSeed)})")}");
var plateRows = new List<Row>();
if (!tableOnly)
{
GD.Print($"\n--- 4. PLATES at {mapSize} ---");
var plates = new List<(int seed, Level lv)> { (plateSeed, levels[0]), (plateSeed, levels[1]), (plateSeed, levels[2]), (secondSeed, mid) };
foreach (var (seed, lv) in plates)
{
var cfg = Cfg(mapSize, seed, lv.Label, true, true, lv.Frac);
Pass1Result p1 = Topography.Generate(cfg);
Pass2Result p2 = Shaping.Shape(p1, cfg);
var led = p1.RegionLedger;
var cm = ShapingOracle.CentreIsLand(p1); cm.Name += $" [plate {lv.Label} {seed}]";
var ck = ShapingOracle.TagCoastlineConsistent(p2, sea); ck.Name += $" [plate {lv.Label} {seed}]";
perFieldChecks.Add(cm); perFieldChecks.Add(ck);
WritePlate(batchRoot, p1, p2, sea, anchors, skipRaw);
plateRows.Add(new Row
{
Level = lv.Label, Seed = seed, ThresholdCells = led.ThresholdCells,
Pre = led.PreIslands, PreN = led.PreNorth, PreS = led.PreSouth, Post = led.PostIslands, PostN = led.PostNorth, PostS = led.PostSouth,
RevertedComps = led.RevertedComponents, RevertedCells = led.RevertedCells,
PostMin = led.PostMin, PostMed = led.PostMedian, PostMean = led.PostMean, PostMax = led.PostMax,
MainlandCells = p1.Regions.Mainland.SizeCells, Ok = cm.Passed && ck.Passed, Ms = p1.ElapsedMs,
});
GD.Print($" plate {seed}_{lv.Label}: pre {led.PreIslands} → post {led.PostIslands} (N {led.PostNorth} / S {led.PostSouth}), reverted {led.RevertedComponents} comps {(cm.Passed && ck.Passed ? "ok" : " CHECK FAILED")} {p1.ElapsedMs} ms");
}
}
bool allOk = hard.TrueForAll(c => c.Passed) && perFieldChecks.TrueForAll(c => c.Passed);
GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}");
foreach (var c in perFieldChecks) if (!c.Passed) GD.PrintErr(" " + c);
WriteTable(batchRoot, tableSize, levels, rows);
WriteIndex(batchRoot, mapSize, tableSize, calibSize, plateSeed, secondSeed, tableSeeds, levels, rows, plateRows, hard, perFieldChecks, allOk, tableOnly);
GD.Print("\n==================================================================");
GD.Print($" DONE — {batchRoot}");
GD.Print($" ORACLE {(allOk ? "HARD CHECKS ALL PASS" : "*** FAILURES see the table ***")}");
GD.Print("==================================================================");
GetTree().Quit(allOk ? 0 : 3);
}
private static long Cells(float frac, int size) => Math.Max(1L, (long)Math.Round(frac * (double)size * size));
private static int PickSecondSeed(Dictionary<int, int> natural, int[] seeds, int plateSeed)
{
int best = 0, bestN = -1;
foreach (int s in seeds)
{
if (s == plateSeed) continue;
int n = natural.GetValueOrDefault(s);
if (n > bestN) { best = s; bestN = n; }
}
return best == 0 ? plateSeed : best;
}
// ---- the curve, measured exactly as tasks 0306 did --------------------
private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
{
var rawPool = new LandHistogram(sea);
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int s in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // offshore OFF, revert OFF by default
pass1[s] = p1;
rawPool.Accumulate(p1.Height, calibSize);
}
var knots = new CurveKnots(2, "v2_balanced",
rawPool.Quantile(CurveKnots.Percentiles[0]), rawPool.Quantile(CurveKnots.Percentiles[1]),
rawPool.Quantile(CurveKnots.Percentiles[2]), rawPool.Quantile(CurveKnots.Percentiles[3]),
rawPool.Quantile(CurveKnots.Percentiles[4]), rawPool.Quantile(CurveKnots.Percentiles[5]));
float ceilingRaw = knots.K2;
var rawAbove = new LandHistogram(sea);
var outAbove = new LandHistogram(sea);
foreach (int s in CalibrationSeeds)
{
var scfg = new TerrainGenConfig
{
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
};
Pass2Result st = Shaping.Shape(pass1[s], scfg);
rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);
}
var pcts = ClimbCalibration.DefaultPercentiles;
var rawQ = new float[pcts.Length];
var outQ = new float[pcts.Length];
for (int i = 0; i < pcts.Length; i++) { rawQ[i] = rawAbove.Quantile(pcts[i]); outQ[i] = outAbove.Quantile(pcts[i]); }
var cal = ClimbCalibration.FromPercentiles(pcts, rawQ, outQ, ceilingRaw,
HeightCurve.EffectiveSpikeMax(pass1[CalibrationSeeds[0]].HMaxSeed, knots, anchors),
anchors.RedCeil, anchors.PeakCap, mountainLift: 1.0f, peakSharpness: 1.0f);
return (knots, cal);
}
private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
ClimbCalibration cal, string label) => new TerrainGenConfig
{
MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = k, Anchors = a, ClimbCalibration = cal, LowlandCeilingM = 30f,
CoastShelf = false, Offshore = new OffshoreSettings(), // OFF unless the variant turns it on
};
// ---- output -----------------------------------------------------------
private static void WritePlate(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw)
{
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}");
DirAccess.MakeDirRecursiveAbsolute(dir);
GrayscaleRenderer.SavePng(p2.Height, p2.MapSize, Path.Combine(dir, "grayscale.png"));
if (!skipRaw) HeightField.Save(p2.Height, p2.MapSize, Path.Combine(dir, "height.f32"));
var look = new LookConfig
{
Name = "hillshade_even", Palette = ReliefPalette.Kind.ProvisionalEven,
ZExaggeration = 18f, LightAzimuth = 315f, LightAltitude = 45f,
HillshadeStrength = 0.30f, SeaLevel = sea,
};
Image map = ReliefRenderer.Render(p2.Height, p2.MapSize, look);
LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap, $"{p2.VariantLabel.ToUpperInvariant()} {p2.Seed}")
.SavePng(Path.Combine(dir, "relief.png"));
// ⭐ The labeled-regions overlay — the point of this task.
var led = p1.RegionLedger;
RegionOverlayRenderer.SavePng(p1.Regions, led.RevertOn ? p1.RegionsPre : null, p1.MapSize,
led.RevertedComponents, led.ThresholdCells, Path.Combine(dir, "regions.png"));
// The hemisphere tag overlay (chat2/05's), now showing the tag by construction.
var (n, s) = RegionLabeling.IslandsByHemisphere(p1.Regions);
TagOverlayRenderer.SavePng(p2.Height, p2.IsIsland, p2.IslandHemisphere, p2.MapSize, sea, n, s, Path.Combine(dir, "tags.png"));
}
private static string HistRow(int[] h)
{
if (h == null) return "—";
var sb = new StringBuilder();
for (int i = 0; i < h.Length; i++) { if (i > 0) sb.Append(" · "); sb.Append(h[i]); }
return sb.ToString();
}
private static string TableMarkdown(List<Level> levels, List<Row> rows, int tableSize)
{
var sb = new StringBuilder();
var histHead = new StringBuilder();
for (int i = 0; i <= RegionLabeling.HistogramEdges.Length; i++) { if (i > 0) histHead.Append(" · "); histHead.Append(RegionLabeling.HistogramLabel(i)); }
sb.AppendLine($"| Level | Seed | threshold (cells) | natural islands (offshore off) N / S | pre-revert islands N / S | **post-revert islands N / S** | reverted comps / cells | pre size min / med / mean / max | **post size min / med / mean / max** | post histogram ({histHead}) | mainland cells | oracle |");
sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|---|");
foreach (var lv in levels)
foreach (var r in rows)
{
if (r.Level != lv.Label) continue;
sb.AppendLine($"| `{r.Level}` | `{r.Seed}` | {r.ThresholdCells:N0} | {r.Natural} ({r.NaturalN} / {r.NaturalS}) | {r.Pre} ({r.PreN} / {r.PreS}) | **{r.Post} ({r.PostN} / {r.PostS})** | {r.RevertedComps} / {r.RevertedCells:N0} | " +
$"{r.PreMin} / {r.PreMed} / {r.PreMean:F0} / {r.PreMax} | **{r.PostMin} / {r.PostMed} / {r.PostMean:F0} / {r.PostMax}** | {HistRow(r.PostHist)} | {r.MainlandCells:N0} | {(r.Ok ? "pass" : "**FAIL**")} |");
}
sb.AppendLine();
sb.AppendLine("**Per level (over the seeds):**");
sb.AppendLine();
sb.AppendLine("| Level | threshold | post islands min / mean / max | post N min / mean / max | post S min / mean / max | reverted comps (total) | reverted cells (total) | post median island (median over seeds) | smallest surviving island |");
sb.AppendLine("|---|---|---|---|---|---|---|---|---|");
foreach (var lv in levels)
{
int cnt = 0, minP = int.MaxValue, maxP = 0, minN = int.MaxValue, maxN = 0, minS = int.MaxValue, maxS = 0; double sumP = 0, sumN = 0, sumS = 0;
long revC = 0, revCells = 0, smallest = long.MaxValue; var meds = new List<long>(); long thr = 0;
foreach (var r in rows)
{
if (r.Level != lv.Label) continue;
cnt++; thr = r.ThresholdCells;
minP = Math.Min(minP, r.Post); maxP = Math.Max(maxP, r.Post); sumP += r.Post;
minN = Math.Min(minN, r.PostN); maxN = Math.Max(maxN, r.PostN); sumN += r.PostN;
minS = Math.Min(minS, r.PostS); maxS = Math.Max(maxS, r.PostS); sumS += r.PostS;
revC += r.RevertedComps; revCells += r.RevertedCells; meds.Add(r.PostMed);
if (r.Post > 0) smallest = Math.Min(smallest, r.PostMin);
}
if (cnt == 0) continue;
meds.Sort();
sb.AppendLine($"| `{lv.Label}` | {lv.Frac:G3} = {thr:N0} cells | {minP} / {sumP / cnt:F1} / {maxP} | {minN} / {sumN / cnt:F1} / {maxN} | {minS} / {sumS / cnt:F1} / {maxS} | {revC} | {revCells:N0} | {meds[meds.Count / 2]} | {(smallest == long.MaxValue ? 0 : smallest)} |");
}
return sb.ToString();
}
private static void WriteTable(string batchRoot, int tableSize, List<Level> levels, List<Row> rows)
{
var sb = new StringBuilder();
sb.AppendLine($"# The count/size table — {rows.Count / Math.Max(1, levels.Count)} seeds × {levels.Count} revert thresholds at {tableSize}");
sb.AppendLine();
sb.AppendLine("Islands = non-mainland 8-connected land components of the CLASSIFY field (mainland = the centre component).");
sb.AppendLine("*natural* = offshore off, revert off; *pre-revert* = offshore `density_mid` on, revert off; *post-revert* = the same with");
sb.AppendLine("the speck revert on at the level's threshold. Sizes in cells. Histogram bins are cells, log-spaced.");
sb.AppendLine();
sb.Append(TableMarkdown(levels, rows, tableSize));
WriteText(Path.Combine(batchRoot, "count_size_table.md"), sb.ToString());
var csv = new StringBuilder();
csv.AppendLine("level,seed,threshold_cells,natural,natural_n,natural_s,pre,pre_n,pre_s,post,post_n,post_s,reverted_comps,reverted_cells,pre_min,pre_median,pre_mean,pre_max,post_min,post_median,post_mean,post_max,post_hist,mainland_cells,oracle,ms");
var ic = System.Globalization.CultureInfo.InvariantCulture;
foreach (var r in rows)
csv.AppendLine(string.Join(",", r.Level, r.Seed, r.ThresholdCells, r.Natural, r.NaturalN, r.NaturalS, r.Pre, r.PreN, r.PreS, r.Post, r.PostN, r.PostS,
r.RevertedComps, r.RevertedCells, r.PreMin, r.PreMed, r.PreMean.ToString("F1", ic), r.PreMax, r.PostMin, r.PostMed, r.PostMean.ToString("F1", ic), r.PostMax,
"\"" + HistRow(r.PostHist) + "\"", r.MainlandCells, r.Ok ? "pass" : "FAIL", r.Ms));
WriteText(Path.Combine(batchRoot, "count_size_table.csv"), csv.ToString());
}
private static void WriteIndex(string batchRoot, int mapSize, int tableSize, int calibSize, int plateSeed, int secondSeed,
int[] tableSeeds, List<Level> levels, List<Row> rows, List<Row> plateRows,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perField, bool allOk, bool tableOnly)
{
var sb = new StringBuilder();
sb.AppendLine("# Batch 07 — region labeling: label all land, fix the tag, tunable speck revert");
sb.AppendLine();
sb.AppendLine("The **region-labeling layer** (`Core.RegionLabeling`) flood-fills the CLASSIFY field's land into 8-connected");
sb.AppendLine("components, names the **centre component** the mainland, and exposes id / size / centroid / hemisphere (by");
sb.AppendLine("centroid) / isMainland. The **island tag is now a consequence of labeling** — every non-mainland component,");
sb.AppendLine("natural detached masses included. The **speck revert** (origin-blind, lower-only, component-only, mainland never)");
sb.AppendLine("lowers sub-threshold islands to their ring's seabed; the threshold is the dial swept here.");
sb.AppendLine();
if (tableOnly) sb.AppendLine("> ⚠ **ISLA_TABLE_ONLY** — a probe run: table only, no regressions, no plates. Not the batch of record.\n");
sb.AppendLine("## ⭐ Open this first");
sb.AppendLine();
sb.AppendLine($"1. **`{plateSeed}_threshold_mid/regions.png`** — the labeled-regions overlay: grey = mainland (the centre component),");
sb.AppendLine(" every island its own colour, dark red = where a reverted speck was. Then `relief.png` for the clean ocean.");
sb.AppendLine($"2. **`{plateSeed}_threshold_low/`** and **`{plateSeed}_threshold_high/`** beside it — same seed, lower / higher cutoff.");
sb.AppendLine($"3. **`{secondSeed}_threshold_mid/regions.png`** + `tags.png` — the second seed (most natural islands): the big organic");
sb.AppendLine(" detached masses are labeled and TAGGED (cyan / orange), which task 06's overlay left grey.");
sb.AppendLine("4. Then the count/size table — the instrument for the later southern-stretch step.");
sb.AppendLine();
sb.AppendLine("**The contract (verbatim):** field = classify (raw, uncurved) · land 8-connected (the complement of water's 4) ·");
sb.AppendLine("component = maximal 8-connected set of land cells (classify ≥ sea) · mainland = the component containing the map");
sb.AppendLine("centre (not merely the largest; the crater is NOT central) · per component: id, sizeCells, centroid, hemisphere");
sb.AppendLine($"(by centroid), isMainland. NORTH = rows `[0, {mapSize / 2})`, SOUTH = rows `[{mapSize / 2}, {mapSize})`; y runs south.");
sb.AppendLine();
sb.AppendLine("## The four plates");
sb.AppendLine();
sb.AppendLine("| Plate | threshold (cells) | pre-revert islands N / S | **post-revert islands N / S** | reverted comps / cells | post size min / med / mean / max | mainland cells | oracle |");
sb.AppendLine("|---|---|---|---|---|---|---|---|");
foreach (var r in plateRows)
sb.AppendLine($"| `{r.Seed}_{r.Level}/` | {r.ThresholdCells:N0} | {r.Pre} ({r.PreN} / {r.PreS}) | **{r.Post} ({r.PostN} / {r.PostS})** | {r.RevertedComps} / {r.RevertedCells:N0} | {r.PostMin} / {r.PostMed} / {r.PostMean:F0} / {r.PostMax} | {r.MainlandCells:N0} | {(r.Ok ? "pass" : "**FAIL**")} |");
if (plateRows.Count == 0) sb.AppendLine("| *(no plates — probe run)* | | | | | | | |");
sb.AppendLine();
sb.AppendLine($"## ⭐ The count/size table — {tableSeeds.Length} seeds × 3 thresholds at {tableSize}");
sb.AppendLine();
sb.Append(TableMarkdown(levels, rows, tableSize));
sb.AppendLine();
sb.AppendLine("Also as plain data: `count_size_table.md` / `.csv`.");
sb.AppendLine();
sb.AppendLine("## The levels");
sb.AppendLine();
sb.AppendLine("| Level | `MinLandComponentFrac` | cells at the plate size | cells at 8192 |");
sb.AppendLine("|---|---|---|---|");
foreach (var lv in levels) sb.AppendLine($"| `{lv.Label}`{(lv.Label == "threshold_mid" ? " config default" : "")} | {lv.Frac:G3} | {Cells(lv.Frac, mapSize):N0} | {Cells(lv.Frac, 8192):N0} |");
sb.AppendLine();
sb.AppendLine($"Every field: coast shelf ON + offshore `{OffshoreSettings.Organic().Describe()}` + region labeling ON. The revert is the variable.");
sb.AppendLine("The revert is **origin-blind**: it removes small natural nubs as well as offshore-pass dots (fewer / bigger, intended). An offshore");
sb.AppendLine("island it removes leaves its submerged skirt (not this component — component-only) as a shoal.");
sb.AppendLine();
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
sb.AppendLine();
sb.AppendLine("`ProvisionalEven`, flagged. The individually-coloured scheme is ONLY the `regions.png` overlay.");
sb.AppendLine();
sb.AppendLine("## The oracle");
sb.AppendLine();
sb.AppendLine("Regressions (offshore OFF + revert OFF must be bit-identical to Phase 1, task 03 and the `terrain-curve-v1` gallery dump; labeling ON + revert OFF bit-identical to the task-06 dump):");
sb.AppendLine();
sb.AppendLine(hard.Count == 0 ? "*(skipped — probe run)*\n" : ShapingOracle.ToMarkdownTable(hard));
sb.AppendLine("Per field (centre-is-land m · revert guards n · determinism o · moat i · mainland unmoved j · tag/coastline k · HMaxSeed l · classify b):");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(perField));
sb.AppendLine($"**{(allOk ? "ALL HARD CHECKS PASS" : " FAILURES do not judge this batch")}**");
sb.AppendLine();
sb.AppendLine("## Disposability");
sb.AppendLine();
sb.AppendLine("| Artifact | Keep? |");
sb.AppendLine("|---|---|");
sb.AppendLine("| `regions.png`, `tags.png`, `relief.png`, `INDEX.md`, `count_size_table.md` / `.csv` | **keep** |");
sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code — large, clear freely |");
sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
sb.AppendLine();
sb.AppendLine($"Plates at {mapSize}, table at {tableSize}, curve calibrated at {calibSize} with offshore off. {WorldScale.Describe()}.");
WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString());
}
private static void WriteText(string path, string text)
{
using var f = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Write);
if (f == null) { GD.PrintErr($"could not write {path}"); return; }
f.StoreString(text);
}
// ---- env helpers --------------------------------------------------------
private static string EnvStr(string k, string fallback)
{
string v = System.Environment.GetEnvironmentVariable(k);
return string.IsNullOrWhiteSpace(v) ? fallback : v;
}
private static int EnvInt(string k, int fallback)
=> int.TryParse(EnvStr(k, null) ?? "", out int v) ? v : fallback;
private static float EnvFloat(string k, float fallback)
=> float.TryParse(EnvStr(k, null) ?? "", System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out float v) ? v : fallback;
private static int[] EnvSeeds(string k, int[] fallback)
{
string v = EnvStr(k, null);
if (v == null) return fallback;
var outp = new List<int>();
foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
if (int.TryParse(part.Trim(), out int s) && s > 0) outp.Add(s);
return outp.Count > 0 ? outp.ToArray() : fallback;
}
}
}