islaApocalypse-v2/Tools/Scripts/RegionLabelingTool.cs
beezm 03fe75b378 rivers/01: re-baseline — bare defaults reproduce terrain-shape-v1 (calibration pinned family-off); retire stale oracles; namespace batch root by chat
The bare TerrainGenConfig defaults did NOT reproduce the terrain the developer
locked, so "run the default generator" was not "the terrain in the gallery" —
the single most expensive fact in the codebase, and the reason a fresh chat
would spend an afternoon chasing differences that were CONFIGURATION, not
regression. This is the deliberate task that ends that, before any river work.

A1 — the defaults ARE the locked shape now. Five fields actually move:
SpeckRevert false->true, MinLandComponentFrac 3e-5->2.5e-7 (120x smaller; the
config default would have eaten real islands, not specks), SouthStretch 0->2,
FragmentAmp 0->0.5, Erosion false->true. Seven more were already correct via
SouthernStretch.Default* / CoastalFragment.Default* and are now pinned as
literals, because TerrainShapeV1 used to do that pinning and this default set
inherits the job. CoastShelf stays OFF — the locked shape has no shelf, and
evaluating it (D-041) is its own later task once water renders. Offshore stays
Off permanently (D-063): islands are organic-only, made by the stretch +
fragmentation and identified by the region layer, never placed.

A2 — the preserve mechanism. The curve knots are percentiles of the FAMILY-OFF
land distribution; flipping the defaults would have moved the pool, the knots,
and with them the render field of every batch including terrain-shape-v1
itself. So the pool is pinned family-off (TerrainGenConfig.WithFamilyOff /
CalibrationPool) rather than the knots being baked: calibration stays live, its
INPUT distribution is held still. The pin was a no-op by construction — it sets
the values the defaults carried the instant before the flip — and re-measuring
after confirms it: pool, all six knots, per-seed spread, shaped max,
monotonicity spikeMax and all seven band shares identical.

  Applied wider than "in CalibrateCurve": OffshoreIslandsTool,
  RegionLabelingTool and SouthernStretchTool generate their own family-off
  field for the Phase-1 anchor, so the pool pin alone would NOT have covered
  them and their a1 would have failed for a configuration reason. TerrainGenTool
  too — it AUTHORED 02_pass1_port and must stay able to regenerate its own
  anchor.

  Recorded as a judged-and-parked property: knots measured family-off, applied
  family-on. Deliberate, not an oversight. Same disposition as the mid-slope
  feather.

A4 — no oracle may pass against a superseded baseline. Six anchors retired
(01/03/04/06/08/09) with their checks and ISLA_T0x_SOURCE defaults; three kept
(chat1/02_pass1_port as the family-off pass-1 guard, chat2/10 and chat2/11 as
the shape and erosion acceptance anchors). Two invariants were RE-POINTED
rather than lost — the southern stretch's north-lock and the coastal-fragment
interior-lock now compare against SAME-RUN fields, which is scale-free and
cannot be invalidated by a moved dump. The retired dumps are kept, not deleted,
and marked superseded in their INDEX.md.

  A missing anchor is now LOUD. The old pattern skipped silently, so a moved
  anchor did not make its oracle fail — it made it not RUN, and a batch with a
  skipped check prints an all-PASS table that reads like a clean one. That is
  the INVERSE of the hazard the re-baseline guards against, and the migration
  below is exactly the event that would have triggered it, on nine anchors at
  once. ShapingOracle.LoadAnchor now separates the two cases: absent -> throw;
  present at another size -> loud INCONCLUSIVE, which is a fail, never a pass.

  TerrainShapeV1 inverted from PRESET to GUARD and moved to its own file.
  Apply() is gone — stamping the values on top of the defaults would MASK a
  drift instead of catching it. Its constants are now the assertion target, and
  Assert() refuses a run whose defaults have drifted off the locked shape.

B/C — batches are namespaced by chat: batches/<chat>/NN_slug/. Task numbers
restart at 00 per chat, so a flat root collided the moment a second chat
existed — four colliding prefixes across 25 batches, separable only by slug.
ToolingPaths.ChatSlug is REQUIRED (throws if unset) and defaults per tool to
its authoring chat, so re-running reproduces a batch in place while ISLA_CHAT
redirects — which is also what stops an acceptance run from overwriting the
very anchor it checks against. Writes go through BatchRoot; historical READS
compose against BatchesRoot and so carry the prefix in their own source string
("chat1/02_pass1_port"). The 25 existing batches were migrated moves-only.

ACCEPTANCE — 16 of 16 byte-identical, 0 failed. All 8 gallery seeds at 8192
from the bare defaults are byte-identical to chat2/10_frag4_seed_gallery
(= terrain-shape-v1, a59e52f); all 8 erosion fields byte-identical to
chat2/11_erosion (= ea291ea). Every gallery table row and every erosion
statistic reproduces its recorded value exactly. DrainageTool's a11 passes
bit-identical over 67,108,864 cells, and its analysis reproduces batch 12
exactly — so the whole chain rivers depends on (shape -> erosion -> drainage)
is unchanged. All 12 edited tools re-run clean; both new guards negative-tested.

The baseline moved in DEFAULTS, not in TERRAIN.

-> XX_Human/output/rivers/01_rebaseline_and_batch_namespace.report.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhkXBQh2tDmcWKpXYcj8vj
2026-08-23 04:45:10 -04:00

589 lines
34 KiB
C#
Raw Permalink 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_PHASE1_SOURCE the Phase-1 regression dump's batch (default "chat1/02_pass1_port")
/// </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;
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());
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
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", "chat1/02_pass1_port");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "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)
{
// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This tool is chat-2 shaping DEVELOPMENT:
// it was authored and judged before the shape family existed, and its regression checks
// hold pass 1 against the FAMILY-OFF `02_pass1_port` dump. The re-baseline flipped the
// bare defaults family-ON, so without this pin every config here would silently acquire
// stretch + fragmentation and every anchor check would fail for a configuration reason.
// → TerrainGenConfig.WithFamilyOff().
var c = BaseConfig(size, seed, knots, anchors, calibration, label).WithFamilyOff();
// …then this tool's own axes, AFTER the pin (the pin would otherwise clear them).
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);
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud.
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, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump));
// ⚑ RETIRED at rivers/01 — a3 and a3r, both against `03_mountain_restore`.
// A curve-development intermediate, subsumed by the `terrain-shape-v1` acceptance (a10).
// a3r was informational only, and its subject — how many natural specks the revert takes —
// is now reported by the region ledger every run, with the revert ON by default.
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
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);
// ⚑ RETIRED at rivers/01 — a4 (`04_seed_gallery`) and a6 (`06_offshore_organic_tune`).
// a4 held the PRE-FAMILY committed curve; the locked shape is family-ON, and a10 is the
// 8192 acceptance now. a6 held the ORGANIC ISLET layer — the DROPPED mechanism (→ D-063):
// islands are organic-only, made by the stretch + fragmentation and identified here, never
// placed. An oracle pinning islet output is an oracle defending a design that was reversed.
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
}
// ═══ 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(TerrainGenConfig.CalibrationPool(calibSize, s)); // family-off PINNED (rivers/01), not defaulted
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)
{
// ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and
// `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole
// calibration is family-off" is a total claim rather than a field-by-field one.)
var scfg = new TerrainGenConfig
{
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
}.WithFamilyOff();
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;
}
}
}