islaApocalypse-v2/Tools/Scripts/SouthernStretchTool.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

640 lines
36 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 SOUTHERN-STRETCH EXPLORATION BATCH (chat2/08) — map the fragmentation knob space: a
/// peninsula → few-big-pieces → gravel ladder, 5 stretch levels × 2 seeds, with the region layer
/// as the instrument. NOT a converged setting.
///
/// ═══ TWO MODES ═══
///
/// ISLA_DIAG_ONLY=1 the DIAGNOSTIC (numbers, no plates): the three southern forces along a
/// south-running profile (falloff blend / edge noise / sinker, each alone),
/// the reach table for the candidate seeds, and a stretch sweep under both
/// sinker modes — written to scratch/southern_diagnosis.md. Run first; it
/// sets the ladder.
/// (default) the BATCH: 5 levels × 2 seeds at ISLA_MAPSIZE, lean render per field
/// (labeled-regions overlay + relief + .f32), the hemisphere-split count/size
/// table, the asymmetric oracle.
///
/// Every field: pass 1 + the stretch, region labeling ON, offshore OFF, shelf OFF, speck revert OFF
/// — the pure fragmentation signal (the instrument counts "all islands" and "islands ≥ the 07
/// mid threshold" side by side). 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/SouthernStretchTool.tscn
///
/// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR
/// ISLA_MAPSIZE plate size (default 4096)
/// ISLA_CALIB_SIZE curve calibration + diagnostic size (default 2048)
/// ISLA_SEEDS the two batch seeds (default: auto — the two 07 seeds with the most southern mass)
/// ISLA_CANDIDATE_SEEDS the pool the auto-pick reads (default: the 07 table seeds)
/// ISLA_STRETCH_LEVELS the 5 stretch values (default: the diagnostic-chosen ladder below)
/// ISLA_BAND_START / ISLA_BAND_FEATHER the fixed band (fractions of the map; constants for the batch)
/// ISLA_STRETCH_SINKER 1 = the sinker rides the stretched distance (default), 0 = real y
/// ISLA_DIAG_ONLY=1 diagnostic only
/// </summary>
public partial class SouthernStretchTool : Node
{
private static readonly int[] DefaultCandidateSeeds =
{
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 };
/// <summary>
/// ⭐ THE LADDER — chosen by the diagnostic (chat2/08 report §1), not linear: the stretch
/// bites unevenly, so the steps are spaced where the southern count/size actually moves.
/// </summary>
private static readonly float[] DefaultLadder = { 0.5f, 1.0f, 2.0f, 3.0f, 5.0f };
/// <summary>The diagnostic's sweep (both sinker modes).</summary>
private static readonly float[] DiagSweep = { 0.25f, 0.5f, 1f, 1.5f, 2f, 3f, 5f, 8f, 16f };
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 Row
{
public int Level; public float Stretch; public int Seed;
public double ReachFrac, MedianCoastFrac; public long MainlandCells, MainlandSouthOfBand;
public int SouthAll, SouthBig, NorthAll, NorthBig;
public long SMin, SMed, SMax, NMax; public double SMean; public int[] SHist;
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", 8);
string descr = EnvStr("ISLA_BATCH", "southern_stretch_explore");
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
int[] candidates = EnvSeeds("ISLA_CANDIDATE_SEEDS", DefaultCandidateSeeds);
int[] seedsEnv = EnvSeeds("ISLA_SEEDS", null);
float[] ladder = EnvFloats("ISLA_STRETCH_LEVELS", DefaultLadder);
float bandStart = EnvFloat("ISLA_BAND_START", SouthernStretch.DefaultBandStartFrac);
float bandFeather = EnvFloat("ISLA_BAND_FEATHER", SouthernStretch.DefaultBandFeatherFrac);
bool stretchSinker = EnvStr("ISLA_STRETCH_SINKER", SouthernStretch.DefaultStretchSinker ? "1" : "0") == "1";
bool diagOnly = EnvStr("ISLA_DIAG_ONLY", "0") == "1";
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
string scratch = ToolingPaths.BatchScratch(batchRoot);
DirAccess.MakeDirRecursiveAbsolute(scratch);
var anchors = CurveAnchors.Default;
float sea = 0.15f;
long bigCells4k = Cells(RegionPass.ThresholdMidFrac, mapSize);
GD.Print("==================================================================");
GD.Print(" SOUTHERN STRETCH (chat2/08) — EXPLORATION: map the fragmentation knob space");
GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} (plates) calibration + diagnostic at {calibSize}");
GD.Print($"band : start {bandStart:F3} of the map (row {(int)(bandStart * mapSize)} at {mapSize}), feather {bandFeather:F3} — FIXED for the batch");
GD.Print($"sinker : {(stretchSinker ? "rides the stretched distance (held back inside the band)" : "real y (keeps pulling the extended mass down)")}");
GD.Print($"ladder : {string.Join(", ", ladder)}");
GD.Print($"fields : pass 1 + stretch · labeling ON · offshore OFF · shelf OFF · speck revert OFF (\"big\" island = ≥ {bigCells4k:N0} cells at {mapSize}, the 07 mid threshold)");
GD.Print($"batch : {batchRoot}{(diagOnly ? " ISLA_DIAG_ONLY the diagnostic, no plates" : "")}");
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}");
TerrainGenConfig Cfg(int size, int seed, string label, float stretch, bool sinkerStretched, bool sinkerOn = true, bool edgeOn = true)
{
// ⭐ 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 = new TerrainGenConfig
{
MapSize = size, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
RegionLabeling = true,
SouthernSinker = sinkerOn, EdgeNoise = edgeOn,
}.WithFamilyOff();
// …then the swept axis, AFTER the pin. Coastal fragmentation stays OFF here: chat2/08
// predates it, and this tool's ladder measures the stretch ALONE.
c.SouthStretch = stretch;
c.SouthBandStartFrac = bandStart;
c.SouthBandFeatherFrac = bandFeather;
c.StretchSinker = sinkerStretched;
return c;
}
// ═══ 1. THE SEEDS — southern mass, measured ═══
GD.Print($"\n--- 1. SOUTHERN REACH of the candidate seeds at {calibSize} (offshore off, stretch off) ---");
var reachRows = new List<(int seed, double reach, double median, long southCells, long mainland, int southIslands)>();
int bandRowC = (int)(bandStart * calibSize);
foreach (int s in candidates)
{
var p = Topography.Generate(Cfg(calibSize, s, "reach", 0f, stretchSinker));
var (reach, median, southCells) = Reach(p.Regions, calibSize, bandRowC);
var (n, so) = RegionLabeling.IslandsByHemisphere(p.Regions);
reachRows.Add((s, reach, median, southCells, p.Regions.Mainland.SizeCells, so));
GD.Print($" seed {s,-11} southernmost mainland row {reach:F3} of map, median coast {median:F3}, mainland cells south of band {southCells,9:N0} ({100.0 * southCells / p.Regions.Mainland.SizeCells:F1} % of mainland), natural S islands {so}");
}
int[] seeds = seedsEnv;
if (seeds == null)
{
reachRows.Sort((a, b) => b.southCells.CompareTo(a.southCells));
seeds = new[] { reachRows[0].seed, reachRows[1].seed };
}
GD.Print($" → batch seeds: {seeds[0]}, {seeds[1]}{(seedsEnv == null ? " (auto: the two with the most mainland south of the band)" : " (ISLA_SEEDS)")}");
// ═══ 2. THE DIAGNOSTIC ═══
var diag = new StringBuilder();
diag.AppendLine("# The southern diagnosis — chat2/08 (measured by SouthernStretchTool, ISLA_DIAG_ONLY)");
diag.AppendLine();
diag.AppendLine($"Size {calibSize}. Band start {bandStart:F3} (row {bandRowC}), feather {bandFeather:F3}. y runs south; fractions are y / MapSize.");
diag.AppendLine();
diag.AppendLine("## 1. Southern reach of the candidate seeds (offshore off, stretch off)");
diag.AppendLine();
diag.AppendLine("| seed | southernmost mainland row | median coast row (per column, central 60 %) | mainland cells south of band | % of mainland | natural S islands |");
diag.AppendLine("|---|---|---|---|---|---|");
foreach (var r in reachRows) diag.AppendLine($"| `{r.seed}` | {r.reach:F3} | {r.median:F3} | {r.southCells:N0} | {100.0 * r.southCells / r.mainland:F1} % | {r.southIslands} |");
diag.AppendLine();
diag.AppendLine($"**Batch seeds:** `{seeds[0]}`, `{seeds[1]}`.");
diag.AppendLine();
if (diagOnly)
{
GD.Print($"\n--- 2. THE THREE FORCES along a south-running profile (central 20 % of x, averaged) ---");
foreach (int s in seeds)
{
var full = Topography.Generate(Cfg(calibSize, s, "full", 0f, stretchSinker));
var noSink = Topography.Generate(Cfg(calibSize, s, "nosink", 0f, stretchSinker, sinkerOn: false));
var bare = Topography.Generate(Cfg(calibSize, s, "bare", 0f, stretchSinker, sinkerOn: false, edgeOn: false));
diag.AppendLine($"## 2. The three forces — seed `{s}` (central 20 % of x averaged; falloff terms are PRE-power)");
diag.AppendLine();
diag.AppendLine("| y / N | blend (ellipse+squircle) | edge noise | sinker | total pre-trench | total^2.5 | mean height | land fraction of row |");
diag.AppendLine("|---|---|---|---|---|---|---|---|");
int x0 = (int)(calibSize * 0.40), x1 = (int)(calibSize * 0.60);
for (int yi = 50; yi <= 100; yi += 2)
{
int y = Math.Min(calibSize - 1, yi * calibSize / 100);
double sb = 0, sn = 0, sf = 0, sh = 0; long land = 0; int cnt = 0;
for (int x = x0; x < x1; x++)
{
sb += bare.PreTrenchFalloff[x, y]; sn += noSink.PreTrenchFalloff[x, y]; sf += full.PreTrenchFalloff[x, y];
sh += full.Height[x, y]; if (full.Height[x, y] >= sea) land++; cnt++;
}
double blend = sb / cnt, edge = sn / cnt - blend, sink = sf / cnt - sn / cnt, total = sf / cnt;
diag.AppendLine($"| {y / (double)calibSize:F2} | {blend:F3} | {edge:F3} | {sink:F3} | {total:F3} | {Math.Pow(Math.Max(0, total), 2.5):F3} | {sh / cnt:F3} | {land / (double)cnt:P0} |");
GD.Print($" seed {s} y {y / (double)calibSize:F2}: blend {blend:F3} edge {edge:F3} sinker {sink:F3} total {total:F3} pow {Math.Pow(Math.Max(0, total), 2.5):F3} height {sh / cnt:F3} land {land / (double)cnt:P0}");
}
diag.AppendLine();
}
GD.Print($"\n--- 3. THE STRETCH SWEEP (both sinker modes) ---");
diag.AppendLine("## 3. The stretch sweep — reach, southern islands (all / ≥ mid threshold), sizes, and the northern control");
diag.AppendLine();
long bigC = Cells(RegionPass.ThresholdMidFrac, calibSize);
diag.AppendLine($"\"big\" = ≥ {bigC} cells at {calibSize} (the 07 `threshold_mid`). Reach = southernmost mainland row / N; median coast over the central 60 % of columns.");
diag.AppendLine();
foreach (bool mode in new[] { false, true })
{
diag.AppendLine($"### Sinker on {(mode ? "the STRETCHED distance (held back with the geometry)" : "the REAL y (unchanged)")}");
diag.AppendLine();
diag.AppendLine("| seed | stretch | reach | median coast | mainland cells south of band | S islands all / big | S size med / max | S largest island | N islands (control) |");
diag.AppendLine("|---|---|---|---|---|---|---|---|---|");
foreach (int s in seeds)
{
var sweep = new List<float> { 0f }; sweep.AddRange(DiagSweep);
foreach (float st in sweep)
{
var p = Topography.Generate(Cfg(calibSize, s, $"sweep_{st}", st, mode));
var (reach, median, southCells) = Reach(p.Regions, calibSize, bandRowC);
var stats = Hemi(p.Regions, bigC);
diag.AppendLine($"| `{s}` | {st:G3} | {reach:F3} | {median:F3} | {southCells:N0} | {stats.southAll} / {stats.southBig} | {stats.sMed} / {stats.sMax} | {stats.sMax} | {stats.northAll} |");
GD.Print($" sinker {(mode ? "stretched" : "real ")} seed {s,-11} stretch {st,5:G3} reach {reach:F3} median {median:F3} southCells {southCells,8:N0} S {stats.southAll,3}/{stats.southBig,3} med {stats.sMed,6} max {stats.sMax,7} N {stats.northAll}");
}
}
diag.AppendLine();
}
WriteText(Path.Combine(scratch, "southern_diagnosis.md"), diag.ToString());
GD.Print($"\n diagnosis written: {Path.Combine(scratch, "southern_diagnosis.md")}");
GD.Print(" ISLA_DIAG_ONLY — done; no plates.");
GetTree().Quit(0);
return;
}
// ═══ 3. REGRESSIONS — north bit-identical to terrain-curve-v1; stretch-off bit-identical everywhere ═══
GD.Print($"\n--- 3. REGRESSIONS at {calibSize}, seed {seeds[0]} ---");
var hard = new List<ShapingOracle.Check>();
int plate = seeds[0];
float maxStretch = ladder[ladder.Length - 1];
{
var offCfg = Cfg(calibSize, plate, "off", 0f, stretchSinker);
Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false;
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plate}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF == Phase-1 .f32 dump",
Shaping.Shape(p1, curveOff).Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump));
// ⭐ a3c KEPT and RE-POINTED — the north-locked invariant is the load-bearing claim of the
// southern stretch (→ D-065) and it does NOT need an external anchor: the unstretched field
// is generated right here. Re-pointing it off `03_mountain_restore` is what let that dump
// retire without losing the guarantee.
var topCfg = Cfg(calibSize, plate, "top", maxStretch, stretchSinker);
Pass2Result pTop = Shaping.Shape(Topography.Generate(topCfg), topCfg);
Pass2Result pUnstretched = Shaping.Shape(p1, offCfg);
hard.Add(ShapingOracle.NorthLocked("a3c",
$"stretch {maxStretch:G3} ON: north of the band bit-identical to the SAME-RUN unstretched field; changes only in/below the band",
pTop.Height, pUnstretched.Height, calibSize, bandRowC));
foreach (var c in hard) GD.Print(" " + c);
// ⚑ RETIRED at rivers/01 — a3 (`03_mountain_restore`) and a4b (`04_seed_gallery`).
// a3 was a curve-development intermediate, subsumed by the terrain-shape-v1 acceptance.
// a4b asserted the north-lock against the PRE-FAMILY 8192 gallery — but the north-lock is
// now proven scale-free against a same-run field (a3c above), so the external anchor bought
// nothing except an 8192 generation on every run.
// 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.
}
// ═══ 4. THE LADDER — 5 levels × 2 seeds ═══
GD.Print($"\n--- 4. THE LADDER at {mapSize} ---");
int bandRow = (int)(bandStart * mapSize);
var rows = new List<Row>();
var perField = new List<ShapingOracle.Check>();
var baselineRows = new Dictionary<int, Row>();
foreach (int seed in seeds)
{
var c0 = Cfg(mapSize, seed, "stretch_0", 0f, stretchSinker);
Pass1Result p0 = Topography.Generate(c0);
Pass2Result q0 = Shaping.Shape(p0, c0);
baselineRows[seed] = MakeRow(0, 0f, seed, p0, mapSize, bandRow, bigCells4k, true, p0.ElapsedMs);
var r0 = baselineRows[seed];
GD.Print($" seed {seed} baseline (stretch 0): reach {r0.ReachFrac:F3}, S islands {r0.SouthAll} / big {r0.SouthBig}, N islands {r0.NorthAll}");
for (int li = 0; li < ladder.Length; li++)
{
float st = ladder[li];
string label = $"stretch_{li + 1}";
var cfg = Cfg(mapSize, seed, label, st, stretchSinker);
Pass1Result p1 = Topography.Generate(cfg);
Pass2Result p2 = Shaping.Shape(p1, cfg);
var checks = new List<ShapingOracle.Check>
{
ShapingOracle.NorthLocked("p", "north of the band bit-locked (classify) vs stretch 0", p1.Height, p0.Height, mapSize, bandRow),
ShapingOracle.NorthLocked("p2", "north of the band bit-locked (render) vs stretch 0", p2.Height, q0.Height, mapSize, bandRow),
ShapingOracle.NorthIslandsInvariant("northern islands invariant vs stretch 0", p0.Regions, p1.Regions),
ShapingOracle.CentreIsLand(p1),
ShapingOracle.TagCoastlineConsistent(p2, sea),
ShapingOracle.ClassifyFidelity(p1, p2),
};
foreach (var c in checks) { c.Name += $" [{label} = {st:G3}, {seed}]"; perField.Add(c); }
bool ok = checks.TrueForAll(c => c.Passed);
var row = MakeRow(li + 1, st, seed, p1, mapSize, bandRow, bigCells4k, ok, p1.ElapsedMs);
rows.Add(row);
WriteField(batchRoot, p1, p2, sea, anchors, skipRaw, st);
GD.Print($" {label,-10} {st,5:G3} seed {seed,-11} reach {row.ReachFrac:F3} (median coast {row.MedianCoastFrac:F3}) mainland S-of-band {row.MainlandSouthOfBand,9:N0} " +
$"S islands {row.SouthAll,3} / big {row.SouthBig,3} S size med {row.SMed,6} max {row.SMax,7} N islands {row.NorthAll,3} {(ok ? "ok" : " CHECK FAILED")} {p1.ElapsedMs} ms");
}
}
// determinism: the middle level on the first seed, twice
{
float st = ladder[ladder.Length / 2];
var cA = Cfg(mapSize, plate, "det", st, stretchSinker); var cB = Cfg(mapSize, plate, "det", st, stretchSinker);
var a = Topography.Generate(cA); var b = Topography.Generate(cB);
var det = ShapingOracle.LabelsDeterministic(a, b); det.Name += $" [stretch {st:G3}, {plate}]";
var bits = ShapingOracle.NorthLocked("o2", $"two generations bit-identical everywhere [stretch {st:G3}, {plate}]", a.Height, b.Height, mapSize, mapSize);
perField.Add(det); perField.Add(bits);
GD.Print(" " + det); GD.Print(" " + bits);
}
bool allOk = hard.TrueForAll(c => c.Passed) && perField.TrueForAll(c => c.Passed);
GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}");
foreach (var c in perField) if (!c.Passed) GD.PrintErr(" " + c);
WriteTable(batchRoot, mapSize, ladder, seeds, rows, baselineRows, bigCells4k, bandStart, bandFeather, stretchSinker);
WriteIndex(batchRoot, mapSize, calibSize, ladder, seeds, rows, baselineRows, bigCells4k, bandStart, bandFeather, stretchSinker, hard, perField, allOk, diag.ToString());
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);
}
// ---- the instrument --------------------------------------------------
private static long Cells(float frac, int size) => Math.Max(1L, (long)Math.Round(frac * (double)size * size));
/// <summary>Southernmost mainland row / N, the median per-column coast row (central 60 % of columns) / N, and mainland cells at/below the band row.</summary>
private static (double reach, double median, long southCells) Reach(RegionLabels l, int n, int bandRow)
{
int main = l.MainlandId; int maxY = -1; long south = 0;
var coast = new List<int>();
int x0 = (int)(n * 0.2), x1 = (int)(n * 0.8);
for (int x = 0; x < n; x++)
{
int colMax = -1;
for (int y = 0; y < n; y++)
{
if (l.Id[x * n + y] != main) continue;
if (y > colMax) colMax = y;
if (y >= bandRow) south++;
}
if (colMax > maxY) maxY = colMax;
if (x >= x0 && x < x1 && colMax >= 0) coast.Add(colMax);
}
coast.Sort();
double median = coast.Count == 0 ? 0 : coast[coast.Count / 2] / (double)n;
return (maxY / (double)n, median, south);
}
private static (int southAll, int southBig, int northAll, int northBig, long sMin, long sMed, double sMean, long sMax, long nMax, int[] sHist)
Hemi(RegionLabels l, long big)
{
var south = new List<long>(); int northAll = 0, northBig = 0; long nMax = 0;
foreach (var r in l.Regions)
{
if (r.IsMainland) continue;
if (r.Hemisphere == RegionLabeling.HemiSouth) south.Add(r.SizeCells);
else { northAll++; if (r.SizeCells >= big) northBig++; nMax = Math.Max(nMax, r.SizeCells); }
}
south.Sort();
var hist = new int[RegionLabeling.HistogramEdges.Length + 1];
int sBig = 0; double sum = 0;
foreach (long s in south) { hist[RegionLabeling.HistogramBin(s)]++; if (s >= big) sBig++; sum += s; }
return (south.Count, sBig, northAll, northBig,
south.Count == 0 ? 0 : south[0], south.Count == 0 ? 0 : south[south.Count / 2], south.Count == 0 ? 0 : sum / south.Count,
south.Count == 0 ? 0 : south[south.Count - 1], nMax, hist);
}
private static Row MakeRow(int level, float st, int seed, Pass1Result p1, int mapSize, int bandRow, long big, bool ok, ulong ms)
{
var (reach, median, southCells) = Reach(p1.Regions, mapSize, bandRow);
var h = Hemi(p1.Regions, big);
return new Row
{
Level = level, Stretch = st, Seed = seed, ReachFrac = reach, MedianCoastFrac = median,
MainlandCells = p1.Regions.Mainland.SizeCells, MainlandSouthOfBand = southCells,
SouthAll = h.southAll, SouthBig = h.southBig, NorthAll = h.northAll, NorthBig = h.northBig,
SMin = h.sMin, SMed = h.sMed, SMean = h.sMean, SMax = h.sMax, NMax = h.nMax, SHist = h.sHist, Ok = ok, Ms = ms,
};
}
// ---- the curve --------------------------------------------------------
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);
}
// ---- output -----------------------------------------------------------
private static void WriteField(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw, float stretch)
{
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}");
DirAccess.MakeDirRecursiveAbsolute(dir);
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()} ({stretch:G3}) {p2.Seed}")
.SavePng(Path.Combine(dir, "relief.png"));
RegionOverlayRenderer.SavePng(p1.Regions, null, p1.MapSize, 0, 0, Path.Combine(dir, "regions.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(float[] ladder, int[] seeds, List<Row> rows, Dictionary<int, Row> baseline, long big)
{
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 | stretch | Seed | reach (southernmost mainland row / N) | median coast / N | mainland cells south of band | **SOUTH islands: all / ≥ {big:N0} cells** | **S size min / med / mean / max** | S histogram ({histHead}) | **NORTH islands (control)** | N largest | oracle |");
sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|---|");
foreach (int seed in seeds)
{
var b = baseline[seed];
sb.AppendLine($"| *baseline* | 0 | `{seed}` | {b.ReachFrac:F3} | {b.MedianCoastFrac:F3} | {b.MainlandSouthOfBand:N0} | **{b.SouthAll} / {b.SouthBig}** | **{b.SMin} / {b.SMed} / {b.SMean:F0} / {b.SMax}** | {HistRow(b.SHist)} | **{b.NorthAll}** | {b.NMax} | — |");
foreach (var r in rows)
{
if (r.Seed != seed) continue;
sb.AppendLine($"| `stretch_{r.Level}` | {r.Stretch:G3} | `{seed}` | {r.ReachFrac:F3} | {r.MedianCoastFrac:F3} | {r.MainlandSouthOfBand:N0} | **{r.SouthAll} / {r.SouthBig}** | **{r.SMin} / {r.SMed} / {r.SMean:F0} / {r.SMax}** | {HistRow(r.SHist)} | **{r.NorthAll}**{(r.NorthAll != b.NorthAll ? " MOVED" : "")} | {r.NMax} | {(r.Ok ? "pass" : "**FAIL**")} |");
}
}
return sb.ToString();
}
private static void WriteTable(string batchRoot, int mapSize, float[] ladder, int[] seeds, List<Row> rows, Dictionary<int, Row> baseline, long big,
float bandStart, float bandFeather, bool stretchSinker)
{
var sb = new StringBuilder();
sb.AppendLine($"# The hemisphere-split count/size table — {ladder.Length} stretch levels × {seeds.Length} seeds at {mapSize}");
sb.AppendLine();
sb.AppendLine($"Band start {bandStart:F3} (row {(int)(bandStart * mapSize)}), feather {bandFeather:F3} — FIXED. Sinker {(stretchSinker ? "rides the stretched distance" : "on the real y")}.");
sb.AppendLine("Islands = non-mainland 8-connected land components of the classify field (region layer); hemisphere by centroid. Offshore OFF, speck revert OFF.");
sb.AppendLine("SOUTH = the fragmentation signal; NORTH = the should-stay-flat control (flagged if it moves).");
sb.AppendLine();
sb.Append(TableMarkdown(ladder, seeds, rows, baseline, big));
WriteText(Path.Combine(batchRoot, "count_size_table.md"), sb.ToString());
var csv = new StringBuilder();
csv.AppendLine("level,stretch,seed,reach,median_coast,mainland_cells,mainland_south_of_band,south_all,south_big,s_min,s_median,s_mean,s_max,s_hist,north_all,north_big,n_max,oracle,ms");
var ic = System.Globalization.CultureInfo.InvariantCulture;
foreach (int seed in seeds)
{
var all = new List<Row> { baseline[seed] }; all.AddRange(rows.FindAll(r => r.Seed == seed));
foreach (var r in all)
csv.AppendLine(string.Join(",", r.Level, r.Stretch.ToString("G5", ic), r.Seed, r.ReachFrac.ToString("F4", ic), r.MedianCoastFrac.ToString("F4", ic), r.MainlandCells, r.MainlandSouthOfBand,
r.SouthAll, r.SouthBig, r.SMin, r.SMed, r.SMean.ToString("F1", ic), r.SMax, "\"" + HistRow(r.SHist) + "\"", r.NorthAll, r.NorthBig, r.NMax, 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 calibSize, float[] ladder, int[] seeds, List<Row> rows, Dictionary<int, Row> baseline, long big,
float bandStart, float bandFeather, bool stretchSinker, List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perField, bool allOk, string diagSummary)
{
int midLevel = ladder.Length / 2 + 1;
var sb = new StringBuilder();
sb.AppendLine("# Batch 08 — southern stretch, EXPLORATION: the fragmentation knob space");
sb.AppendLine();
sb.AppendLine("**A ladder, not a setting.** Inside a FIXED feathered latitude band the falloff's southward distance is compressed");
sb.AppendLine("(`y' = yB + (y yB) / (1 + stretch · ramp)`), so the mainland reaches further south with the elevation of the rows it came");
sb.AppendLine("from, and where the stretched thin edge thins below sea it fragments organically. North of the band the classify field is");
sb.AppendLine("bit-locked in both directions (asserted). Nothing is stamped. The region layer is the instrument: SOUTH island count/size");
sb.AppendLine("is the fragmentation signal, NORTH is the should-stay-flat control.");
sb.AppendLine();
sb.AppendLine("## ⭐ Open this first");
sb.AppendLine();
sb.AppendLine($"1. **`{seeds[0]}_stretch_{midLevel}/regions.png`** — the middle of the ladder on the first seed: grey mainland, each island its own colour.");
sb.AppendLine($"2. Walk the ladder on that seed: `{seeds[0]}_stretch_1/` … `_stretch_{ladder.Length}/` (`regions.png` beside `relief.png`).");
sb.AppendLine($"3. Then the same five on `{seeds[1]}` — what repeats is the knob; what does not is the seed.");
sb.AppendLine("4. Then the table: southern count/size down the rows, the northern control beside it.");
sb.AppendLine();
sb.AppendLine("## The fixed frame and the axis");
sb.AppendLine();
sb.AppendLine($"- **Band (constant for the batch):** start `{bandStart:F3}` of the map (row {(int)(bandStart * mapSize)} at {mapSize}), feather `{bandFeather:F3}` (smoothstep). Sea identity is hard above it; ramped across; extended below.");
sb.AppendLine($"- **Sinker:** {(stretchSinker ? "rides the stretched distance (pushed out with the geometry held back inside the band)" : "on the real y (keeps pulling the extended mass down where it always did)")}.");
sb.AppendLine($"- **The axis — stretch strength:** {string.Join(" · ", Array.ConvertAll(ladder, v => v.ToString("G3")))} (levels 1{ladder.Length}); baseline 0 measured for the control.");
sb.AppendLine($"- Every field: pass 1 + stretch, region labeling ON, offshore OFF, shelf OFF, speck revert OFF. \"big\" island = ≥ {big:N0} cells at {mapSize} (the 07 `threshold_mid`).");
sb.AppendLine();
sb.AppendLine($"## ⭐ The hemisphere-split count/size table — {ladder.Length} levels × {seeds.Length} seeds at {mapSize}");
sb.AppendLine();
sb.Append(TableMarkdown(ladder, seeds, rows, baseline, big));
sb.AppendLine();
sb.AppendLine("Also as plain data: `count_size_table.md` / `.csv`.");
sb.AppendLine();
sb.AppendLine("## The diagnosis (summary — full tables in `scratch/southern_diagnosis.md`)");
sb.AppendLine();
sb.Append(diagSummary);
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 (asymmetric)");
sb.AppendLine();
sb.AppendLine("Regressions (stretch OFF bit-identical everywhere; stretch ON at the ladder's top bit-identical NORTH OF THE BAND to the `terrain-curve-v1` dumps):");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(hard));
sb.AppendLine("Per field (north bit-locked classify p / render p2 · northern islands invariant q · centre-is-land m · tag/coastline k · classify b · determinism o):");
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`, `relief.png`, `INDEX.md`, `count_size_table.md` / `.csv`, `scratch/southern_diagnosis.md` | **keep** |");
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}, 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 float[] EnvFloats(string k, float[] fallback)
{
string v = EnvStr(k, null);
if (v == null) return fallback;
var outp = new List<float>();
foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
if (float.TryParse(part.Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float f)) outp.Add(f);
return outp.Count > 0 ? outp.ToArray() : 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;
}
}
}