using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// ⭐ THE FAITHFUL-CURVE BASELINE BATCH (chat2/01) — the control the reshape will be judged
/// against, plus the instrument that decides what the reshape should be.
///
/// ═══ WHAT IT DOES, IN ORDER ═══
///
/// 1. CALIBRATE — pool the LAND distribution across several seeds and take
/// P60/73/83/88/96/99. Those quantiles ARE the curve's knots, which is what makes the band
/// shares 60/13/10/5/8/3/1 exact by construction. Reports them against the reference's
/// shipped literals: that delta is the port-fidelity evidence.
/// 2. VARIANTS — per seed, `curve_off` (the control) and `curve_on` (the faithful baseline).
/// Grayscale + raw `.f32` + relief, because a pretty render alone cannot be argued with.
/// 3. HISTOGRAMS — raw and shaped land distributions for the showpiece seed, with the knots and
/// the output anchors overlaid, plus the per-band mass table.
/// 4. ORACLE — the four automatic checks, run before anything is looked at.
/// 5. SHOWPIECE — one render at a larger profile for the judging plate.
///
/// ═══ ⚠ CALIBRATE SMALL, CONFIRM BIG ═══
///
/// Knots are measured at the iteration profile (2K) and used at every profile. That rests on the
/// land distribution's SHAPE being scale-invariant — which Phase 1 established by construction
/// (every frequency and distance is a fraction of MapSize). The tool does not take that on trust:
/// the showpiece re-measures its own quantiles and reports the delta, so the assumption is
/// evidence rather than a footnote.
///
/// ═══ RUNNING IT ═══
///
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/CurveBaselineTool.tscn
///
/// (`--headless` also works — this tool awaits no render frames — but `xvfb-run` is correct in
/// both cases and costs nothing. → Tools/README.md §1.)
///
/// ISLA_TASK authoring task number (default 1)
/// ISLA_BATCH descriptor, NO prefix (default "curve_baseline")
/// ISLA_MAPSIZE calibration/variant profile (default 2048)
/// ISLA_SEEDS comma-separated positive (default: the 6 pinned below)
/// ISLA_SHOWPIECE_SIZE larger confirmation profile (default 8192)
/// ISLA_SHOWPIECE "0" to skip the big render
/// ISLA_VARIANTS "0" to skip the per-seed variants (calibration-only probe)
/// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "02_pass1_port")
/// ISLA_SKIP_RAW "1" to skip the .f32 dumps
///
public partial class CurveBaselineTool : Node
{
///
/// PINNED POSITIVE SEEDS — the four Phase-1 seeds plus two, for a six-seed calibration pool.
/// Pinned, not random: a calibration you cannot reproduce is a magic number with a good story.
///
private static readonly int[] DefaultSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
private const int DefaultMapSize = 2048;
private const int DefaultShowpieceSize = 8192;
///
/// Band-share tolerance, percentage points. The knots come from the SAME histogram the shares
/// are measured on, so the only error is in-bin interpolation — tenths, not units. A loose
/// tolerance here would make check (d) unfalsifiable.
///
private const double ShareTolerancePp = 0.25;
public override void _Ready()
{
// ⚠ An exception out of _Ready does NOT stop Godot — it logs and the process sits there
// with no main loop to end it, so a misconfigured run HANGS instead of failing. A hang
// looks like slow work, which is worse than a crash. Catch, say what was refused, exit 2.
try { Run(); }
catch (Exception e)
{
GD.PrintErr("==================================================================");
GD.PrintErr($" REFUSED: {e.Message}");
GD.PrintErr("==================================================================");
GetTree().Quit(2);
}
}
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
int task = EnvInt("ISLA_TASK", 1);
string descr = EnvStr("ISLA_BATCH", "curve_baseline");
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize);
bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1";
bool variants = EnvStr("ISLA_VARIANTS", "1") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
// ⚠ Composed by BatchRoot, never free-form — it refuses a descriptor carrying its own
// numeric prefix, which is how the counter drifted once already.
string batchRoot = ToolingPaths.BatchRoot(task, descr);
string scratch = ToolingPaths.BatchScratch(batchRoot);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
DirAccess.MakeDirRecursiveAbsolute(scratch);
var scale = new GenerationScale(mapSize);
var anchors = CurveAnchors.Default;
float sea = 0.15f;
GD.Print("==================================================================");
GD.Print(" CURVE BASELINE — faithful redistribution port + the histogram");
GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} (scaleFactor {scale.ScaleFactor:F3})");
GD.Print($"yardstick : {WorldScale.Describe()}");
GD.Print($"anchors : {anchors.DescribeMetres()}");
GD.Print($"seeds : {string.Join(", ", seeds)}");
GD.Print($"batch : {batchRoot}");
GD.Print("------------------------------------------------------------------");
GD.Print(ToolingPaths.Describe());
GD.Print("==================================================================");
// ═══════════════════════════════════════════════════════════════════
// 1. CALIBRATE — the knots ARE percentiles of the pooled land CDF
// ═══════════════════════════════════════════════════════════════════
GD.Print("\n--- 1. CALIBRATION POOL ---");
var rawPool = new LandHistogram(sea);
var pass1 = new Dictionary();
var perSeedKnots = new List();
foreach (int seed in seeds)
{
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed };
Pass1Result p1 = Topography.Generate(cfg);
pass1[seed] = p1;
rawPool.Accumulate(p1.Height, mapSize);
// Each seed's OWN knots too — the pool is the calibration, but the SPREAD across
// seeds is what tells us how much of any later delta is scale and how much is just
// which seed you happened to look at. Without it, the scale-invariance check below
// compares a 6-seed pool against one seed and calls the difference "scale".
var solo = new LandHistogram(sea);
solo.Accumulate(p1.Height, mapSize);
perSeedKnots.Add(MeasureKnots(solo, $"seed_{seed}"));
GD.Print($" pooled seed {seed,-11} h[{p1.HMinSeed,7:F3} .. {p1.HMaxSeed,6:F3}] {p1.ElapsedMs,5} ms");
}
GD.Print($" {rawPool}");
float seedSpread = KnotSpread(perSeedKnots);
GD.Print($" per-seed knot spread at {mapSize}: max range across the 6 seeds = " +
$"{seedSpread:F6} raw ({WorldScale.MetresFromRaw(seedSpread):F2} m)");
CurveKnots measured = MeasureKnots(rawPool, "v2_balanced");
GD.Print("\n MEASURED KNOTS vs THE REFERENCE'S SHIPPED LITERALS");
GD.Print(" knot pct v2 measured reference delta delta (m)");
for (int i = 0; i < 6; i++)
{
float v2 = measured[i], rf = CurveKnots.Reference[i];
GD.Print($" K{i + 1} P{CurveKnots.Percentiles[i],-5:F0} {v2,11:F6} {rf,11:F6} " +
$"{v2 - rf,+9:F6} {WorldScale.MetresFromRaw(v2 - rf),+8:F2}");
}
double[] shares = ShapingOracle.RealizedShares(rawPool, measured);
GD.Print("\n REALIZED LAND SHARES (target 60/13/10/5/8/3/1)");
for (int i = 0; i < 7; i++)
GD.Print($" {CurveKnots.BandNames[i],-16} {shares[i],6:F2} % (target {CurveKnots.BandShareTargets[i],4:F0} %)");
// ═══════════════════════════════════════════════════════════════════
// 2. THE SHAPED POOL — what the curve actually produces
// ═══════════════════════════════════════════════════════════════════
GD.Print("\n--- 2. SHAPED POOL (curve on, detail on) ---");
var shapedPool = new LandHistogram(sea);
var shapedResults = new Dictionary();
foreach (int seed in seeds)
{
var cfg = OnConfig(mapSize, seed, measured, anchors);
Pass2Result p2 = Shaping.Shape(pass1[seed], cfg);
shapedResults[seed] = p2;
shapedPool.Accumulate(p2.Height, mapSize);
if (seed == seeds[0]) foreach (string n in p2.Notes) GD.Print(" " + n);
GD.Print($" shaped seed {seed,-11} h[{p2.HMin,7:F3} .. {p2.HMax,6:F3}] {p2.ElapsedMs,5} ms");
}
GD.Print($" {shapedPool}");
GD.Print("\n SHAPED LAND PERCENTILES (what the palette would need to be placed on)");
double[] paletteP = { 10, 25, 50, 75, 90, 95, 99, 99.9 };
var sb2 = new StringBuilder(" ");
foreach (double p in paletteP) sb2.Append($" p{p:G}={shapedPool.Quantile(p):F3}");
GD.Print(sb2.ToString());
GD.Print($" shaped max {shapedPool.MaxLand:F4} (peak cap {anchors.PeakCap:F4})");
// ⚠ THE PALETTE WAS CALIBRATED ON THE *RAW* DISTRIBUTION (Phase 1, chat1/03) and the
// curve moves that distribution wholesale. This is reported, NOT fixed: re-placing the
// stops now would make the render look more dramatic while the terrain is genuinely
// lower — i.e. it would disguise the exact finding the histograms exist to deliver.
// Whether to recalibrate is a PRESENTATION call the developer makes AFTER deciding
// whether this elevation profile is the one they want.
double rawLow = raw3Stop(rawPool), shapedLow = raw3Stop(shapedPool);
double rawSat = 1.0 - rawPool.FractionBelow(1.450f), shapedSat = 1.0 - shapedPool.FractionBelow(1.450f);
GD.Print("\n PALETTE FIT (CostaRica stops were placed on the RAW distribution)");
GD.Print($" land below stop 3 (0.310 raw): raw {rawLow * 100:F1} % -> shaped {shapedLow * 100:F1} %");
GD.Print($" land above top stop (1.450): raw {rawSat * 100:F3} % -> shaped {shapedSat * 100:F3} % (clamps to white)");
// ═══════════════════════════════════════════════════════════════════
// 3. THE ORACLE — before a single render is looked at
// ═══════════════════════════════════════════════════════════════════
GD.Print("\n--- 3. ORACLE ---");
var checks = new List();
int primary = seeds[0];
Pass1Result pp1 = pass1[primary];
Pass2Result offPrimary = Shaping.Shape(pp1, OffConfig(mapSize, primary));
checks.Add(ShapingOracle.RegressionCurveOff(pp1, offPrimary));
string dumpPath = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32");
float[,] phase1Dump = HeightField.Load(dumpPath, mapSize);
checks.Add(ShapingOracle.RegressionAgainstDump(pp1.Height, phase1Dump, mapSize, dumpPath));
checks.Add(ShapingOracle.ClassifyFidelity(pp1, shapedResults[primary]));
checks.Add(ShapingOracle.Monotonicity(shapedResults[primary]));
checks.Add(ShapingOracle.BandShares(rawPool, measured, ShareTolerancePp));
// Every seed's classify field, not just the showpiece's — the invariant is per-seed.
long classifyDrift = 0;
foreach (int seed in seeds)
{
var c = ShapingOracle.ClassifyFidelity(pass1[seed], shapedResults[seed]);
if (!c.Passed) { classifyDrift++; GD.PrintErr($" classify drift on seed {seed}: {c.Detail}"); }
}
foreach (var c in checks) GD.Print(" " + c);
bool oracleOk = checks.TrueForAll(c => c.Passed) && classifyDrift == 0;
GD.Print($" ORACLE: {(oracleOk ? "ALL PASS" : "FAILURES PRESENT")} " +
$"(classify checked on all {seeds.Length} seeds: {seeds.Length - classifyDrift} clean)");
// ═══════════════════════════════════════════════════════════════════
// 4. VARIANTS — plain data beside the pretty render
// ═══════════════════════════════════════════════════════════════════
var rows = new List();
if (variants)
{
GD.Print("\n--- 4. VARIANTS (curve_off / curve_on) ---");
foreach (int seed in seeds)
{
rows.Add(WriteVariant(batchRoot, pass1[seed], OffConfig(mapSize, seed), skipRaw, sea, 1.45f));
rows.Add(WriteVariant(batchRoot, pass1[seed], OnConfig(mapSize, seed, measured, anchors),
skipRaw, sea, anchors.PeakCap));
}
}
else GD.Print("\n--- variants skipped (ISLA_VARIANTS=0) ---");
// ═══════════════════════════════════════════════════════════════════
// 5. HISTOGRAMS — the diagnostic, for the showpiece seed
// ═══════════════════════════════════════════════════════════════════
GD.Print("\n--- 5. HISTOGRAMS ---");
var rawSeed = new LandHistogram(sea);
rawSeed.Accumulate(pass1[primary].Height, mapSize);
var shapedSeed = new LandHistogram(sea);
shapedSeed.Accumulate(shapedResults[primary].Height, mapSize);
DrawRawHistogram(rawSeed, measured, primary, mapSize, batchRoot);
DrawShapedHistogram(shapedSeed, rawSeed, measured, anchors, primary, mapSize, batchRoot);
// ═══════════════════════════════════════════════════════════════════
// 6. SHOWPIECE — the judging plate, and the scale-invariance check
// ═══════════════════════════════════════════════════════════════════
string showNote = "skipped (ISLA_SHOWPIECE=0)";
if (showpiece)
{
GD.Print($"\n--- 6. SHOWPIECE at {showSize} ---");
showNote = WriteShowpiece(batchRoot, primary, showSize, measured, anchors, sea, skipRaw, rows, seedSpread);
}
else GD.Print("\n--- showpiece skipped (ISLA_SHOWPIECE=0) ---");
WriteIndex(batchRoot, mapSize, showSize, scale, seeds, primary, measured, anchors,
rawPool, shapedPool, shares, checks, rows, oracleOk, showNote, variants, seedSpread);
GD.Print("\n==================================================================");
GD.Print($" DONE — {batchRoot}");
GD.Print($" ORACLE {(oracleOk ? "ALL PASS" : "*** FAILURES — see the table ***")}");
GD.Print("==================================================================");
GetTree().Quit(oracleOk ? 0 : 3);
}
// ---- configs --------------------------------------------------------
private static TerrainGenConfig OffConfig(int mapSize, int seed) =>
new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "curve_off", Curve = false, ShelfDetail = false };
private static TerrainGenConfig OnConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a) =>
new TerrainGenConfig
{
MapSize = mapSize, Seed = seed, VariantLabel = "curve_on",
Curve = true, ShelfDetail = true, Knots = k, Anchors = a,
};
///
/// Fraction of land below the CostaRica palette's third stop (0.310 raw). A blunt
/// "how much of the island is painted with the first two colours" number — the palette's own
/// stops are at measured RAW percentiles, so this is how far the curve moved the picture.
///
private static double raw3Stop(LandHistogram h) => h.FractionBelow(0.310f);
///
/// The widest max−min range of any single knot across a set of measurements — "how much does
/// K_n move if you just change which seed you looked at". The yardstick every other knot
/// delta in this batch has to be judged against.
///
private static float KnotSpread(List sets)
{
float worst = 0f;
for (int i = 0; i < 6; i++)
{
float lo = float.MaxValue, hi = float.MinValue;
foreach (CurveKnots s in sets) { lo = MathF.Min(lo, s[i]); hi = MathF.Max(hi, s[i]); }
worst = MathF.Max(worst, hi - lo);
}
return worst;
}
/// The knots ARE the quantiles. This one method is the whole calibration.
private static CurveKnots MeasureKnots(LandHistogram pool, string name) => new CurveKnots(
2, name,
pool.Quantile(CurveKnots.Percentiles[0]), pool.Quantile(CurveKnots.Percentiles[1]),
pool.Quantile(CurveKnots.Percentiles[2]), pool.Quantile(CurveKnots.Percentiles[3]),
pool.Quantile(CurveKnots.Percentiles[4]), pool.Quantile(CurveKnots.Percentiles[5]));
// ---- output ---------------------------------------------------------
private static string WriteVariant(string batchRoot, Pass1Result p1, TerrainGenConfig cfg,
bool skipRaw, float sea, float legendTop)
{
Pass2Result p2 = Shaping.Shape(p1, cfg);
string dir = Path.Combine(batchRoot, $"{cfg.Seed}_{cfg.VariantLabel}");
DirAccess.MakeDirRecursiveAbsolute(dir);
// ⭐ PLAIN DATA BESIDE THE PRETTY RENDER, always — the point is judging the terrain, and
// you cannot judge a distribution through a palette.
var (gMin, gMax) = 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 = "gradient_flat", Palette = ReliefPalette.Kind.CostaRica,
HillshadeStrength = 0f, SeaLevel = sea,
};
Image map = ReliefRenderer.Render(p2.Height, p2.MapSize, look);
Image withLegend = LegendRenderer.WithLegend(map, look.Palette, sea, legendTop,
cfg.VariantLabel.ToUpperInvariant());
withLegend.SavePng(Path.Combine(dir, "relief.png"));
float land = p2.LandFraction(sea);
GD.Print($" {cfg.VariantLabel,-10} seed {cfg.Seed,-11} h[{p2.HMin,7:F3} .. {p2.HMax,6:F3}] " +
$" land {land * 100,5:F1}% {p2.ElapsedMs,5} ms");
return $"| `{cfg.Seed}_{cfg.VariantLabel}` | {cfg.Seed} | {cfg.VariantLabel} | " +
$"{p2.HMin:F3} | {p2.HMax:F3} | {land * 100:F1}% | {gMin:F3}..{gMax:F3} | {p2.ElapsedMs} ms |";
}
private static void DrawRawHistogram(LandHistogram raw, CurveKnots k, int seed, int mapSize, string batchRoot)
{
// Coarse enough to draw, fine enough to keep the shape: ~360 bins across the range.
float top = MathF.Ceiling(raw.MaxLand * 20f) / 20f;
var display = raw.Rebin((top - raw.SeaLevel) / 360f);
var o = new HistogramRenderer.Options
{
Title = $"RAW LAND HEIGHTS - SEED {seed}",
Subtitle = "PRE-CURVE. THE KNOTS ARE PERCENTILES OF THIS DISTRIBUTION.",
XAxisLabel = "RAW HEIGHT (PRE-CURVE)",
XTop = top,
Footer = $"{raw.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize} - BIN {display.BinWidth:F4} RAW",
};
float[] edges = { raw.SeaLevel, k.K1, k.K2, k.K3, k.K4, k.K5, k.K6, top };
for (int i = 0; i < 7; i++)
o.Bands.Add(new HistogramRenderer.Band
{
Lo = edges[i], Hi = edges[i + 1],
Label = CurveKnots.BandNames[i],
SharePercent = i < 6
? raw.FractionBetween(edges[i], edges[i + 1]) * 100.0
: Math.Max(0.0, (1.0 - raw.FractionBelow(k.K6)) * 100.0),
});
for (int i = 0; i < 6; i++)
o.Markers.Add(new HistogramRenderer.Marker
{
Value = k[i], Label = $"K{i + 1} P{CurveKnots.Percentiles[i]:F0}", Strong = true,
});
HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, "histogram_raw.png"));
GD.Print($" histogram_raw.png ({raw.TotalLand:N0} land columns, max {raw.MaxLand:F4})");
}
private static void DrawShapedHistogram(LandHistogram shaped, LandHistogram raw, CurveKnots k,
CurveAnchors a, int seed, int mapSize, string batchRoot)
{
float top = MathF.Ceiling(shaped.MaxLand * 20f) / 20f;
var display = shaped.Rebin((top - shaped.SeaLevel) / 360f);
var o = new HistogramRenderer.Options
{
Title = $"SHAPED LAND HEIGHTS - SEED {seed}",
Subtitle = "POST-CURVE. BANDS SIT AT THE STORM-LADDER OUTPUT ANCHORS.",
XAxisLabel = "RAW HEIGHT (POST-CURVE)",
XTop = top,
Footer = $"{shaped.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize} - BIN {display.BinWidth:F4} RAW",
};
// The OUTPUT bands: each input band's share, drawn where the curve puts it.
float benchTop = a.BenchBase + a.ShelfSpanMin;
float plateauTop = a.PlateauBase + a.ShelfSpanMin;
float[] outEdges = { a.Sea, a.OrangeCeil, a.RedCeil, a.BenchBase, benchTop, a.PlateauBase, plateauTop, top };
float[] inEdges = { raw.SeaLevel, k.K1, k.K2, k.K3, k.K4, k.K5, k.K6 };
for (int i = 0; i < 7; i++)
o.Bands.Add(new HistogramRenderer.Band
{
Lo = outEdges[i], Hi = outEdges[i + 1],
Label = CurveKnots.BandNames[i],
SharePercent = i < 6
? raw.FractionBetween(inEdges[i], inEdges[i + 1]) * 100.0
: Math.Max(0.0, (1.0 - raw.FractionBelow(k.K6)) * 100.0),
});
o.Markers.Add(new HistogramRenderer.Marker { Value = a.OrangeCeil, Label = "ORANGE" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.RedCeil, Label = "RED" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.BenchBase, Label = "BENCH" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PlateauBase, Label = "PLATEAU" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M" });
HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, "histogram_shaped.png"));
GD.Print($" histogram_shaped.png ({shaped.TotalLand:N0} land columns, max {shaped.MaxLand:F4})");
}
private static string WriteShowpiece(string batchRoot, int seed, int showSize, CurveKnots k,
CurveAnchors a, float sea, bool skipRaw, List rows, float seedSpread)
{
var cfg = OnConfig(showSize, seed, k, a);
cfg.VariantLabel = "curve_on_showpiece";
Pass1Result p1 = Topography.Generate(cfg);
GD.Print($" pass1 {showSize}: h[{p1.HMinSeed:F3} .. {p1.HMaxSeed:F3}] {p1.ElapsedMs} ms");
// ⭐ THE SCALE-INVARIANCE CHECK. Knots were measured at the iteration profile; if the
// land distribution's shape really is scale-free, this profile's own quantiles land on
// the same numbers. Measured, not assumed.
var bigPool = new LandHistogram(sea);
bigPool.Accumulate(p1.Height, showSize);
var bigKnots = MeasureKnots(bigPool, $"at_{showSize}");
var deltas = new StringBuilder();
float worst = 0f;
for (int i = 0; i < 6; i++)
{
float d = bigKnots[i] - k[i];
if (MathF.Abs(d) > MathF.Abs(worst)) worst = d;
deltas.Append($" K{i + 1}{d:+0.0000;-0.0000}");
}
// ⚠ THE COMPARISON IS CONFOUNDED, AND SAYING SO IS THE POINT. This is ONE seed at the
// big profile against a SIX-SEED POOL at the small one, so the delta mixes scale effects
// with seed-to-seed variation. `seedSpread` is how far a knot moves from seed choice
// alone at a fixed size — if the delta sits inside it, scale is not what moved.
bool withinSeedNoise = MathF.Abs(worst) <= seedSpread;
GD.Print($" scale-invariance: knots re-measured at {showSize} differ by{deltas}");
GD.Print($" worst {worst:+0.000000;-0.000000} raw = {WorldScale.MetresFromRaw(worst):+0.00;-0.00} m " +
$"vs per-seed spread {seedSpread:F6} raw ({WorldScale.MetresFromRaw(seedSpread):F2} m) " +
$"-> {(withinSeedNoise ? "WITHIN seed variation" : "EXCEEDS seed variation")}");
rows.Add(WriteVariant(batchRoot, p1, cfg, skipRaw, sea, a.PeakCap));
return $"seed {seed} at {showSize}; knots re-measured there differ by at most " +
$"{MathF.Abs(worst):F6} raw ({MathF.Abs(WorldScale.MetresFromRaw(worst)):F2} m), " +
$"{(withinSeedNoise ? "within" : "beyond")} the {WorldScale.MetresFromRaw(seedSpread):F2} m per-seed spread";
}
// ---- the index ------------------------------------------------------
private static void WriteIndex(string batchRoot, int mapSize, int showSize, GenerationScale scale,
int[] seeds, int primary, CurveKnots k, CurveAnchors a, LandHistogram rawPool,
LandHistogram shapedPool, double[] shares, List checks,
List rows, bool oracleOk, string showNote, bool variants, float seedSpread)
{
var sb = new StringBuilder();
sb.AppendLine("# Batch 01 — the faithful curve baseline");
sb.AppendLine();
sb.AppendLine("The redistribution curve and shelf detail, ported faithfully and **re-calibrated against");
sb.AppendLine("v2's own pass-1 output**. This is the CONTROL every later reshape is judged against —");
sb.AppendLine("not a taste gate. No erosion, no rivers, no water, no crater, no coast shelf, no islets.");
sb.AppendLine();
sb.AppendLine("## ⭐ Open this first");
sb.AppendLine();
sb.AppendLine($"1. **`{primary}_curve_on_showpiece/relief.png`** — the judging plate ({showNote}).");
sb.AppendLine($"2. **`histogram_raw.png`** and **`histogram_shaped.png`** — the diagnostic that says");
sb.AppendLine(" *why* the upper terrain looks the way it does. Read them before forming an opinion.");
sb.AppendLine($"3. **`{primary}_curve_off/relief.png`** vs **`{primary}_curve_on/relief.png`** — the A/B.");
sb.AppendLine();
sb.AppendLine("## Disposability");
sb.AppendLine();
sb.AppendLine("| Artifact | Keep? |");
sb.AppendLine("|---|---|");
sb.AppendLine("| `relief.png` | **keep** — the judging plates |");
sb.AppendLine("| `histogram_*.png` | **keep** — the finding |");
sb.AppendLine("| `INDEX.md` | **keep** |");
sb.AppendLine("| `grayscale.png` | ♻ **regenerable** from the `.f32` — safe to clear |");
sb.AppendLine("| `height.f32` | ♻ **regenerable** from seed + code — safe to clear, but it is the byte-level oracle |");
sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
sb.AppendLine();
sb.AppendLine("## Setup");
sb.AppendLine();
sb.AppendLine($"- **Calibration/variant profile:** {mapSize} (scaleFactor {scale.ScaleFactor:F3})");
sb.AppendLine($"- **Showpiece profile:** {showSize}");
sb.AppendLine($"- **Seeds (pooled as one calibration set):** {string.Join(", ", seeds)}");
sb.AppendLine($"- **Yardstick:** {WorldScale.Describe()}");
sb.AppendLine($"- **Output anchors:** {a.DescribeMetres()}");
sb.AppendLine($"- **Curve:** v{HeightCurve.Version} · **detail:** v{TerrainDetailPass.Version}");
sb.AppendLine();
sb.AppendLine("## ⭐ The re-measured knots");
sb.AppendLine();
sb.AppendLine($"Pooled land CDF: **{rawPool.TotalLand:N0} samples** from {rawPool.FieldsPooled} seeds, ");
sb.AppendLine($"range [{rawPool.MinLand:F4} .. {rawPool.MaxLand:F4}] raw, bin {rawPool.BinWidth:G3}.");
sb.AppendLine();
sb.AppendLine("| Knot | Percentile | v2 measured | reference literal | delta (raw) | delta (m) |");
sb.AppendLine("|---|---|---|---|---|---|");
for (int i = 0; i < 6; i++)
{
float v2 = k[i], rf = CurveKnots.Reference[i];
sb.AppendLine($"| K{i + 1} | P{CurveKnots.Percentiles[i]:F0} | `{v2:F6}` | `{rf:F6}` | " +
$"{v2 - rf:+0.000000;-0.000000} | {WorldScale.MetresFromRaw(v2 - rf):+0.00;-0.00} |");
}
sb.AppendLine();
sb.AppendLine("> **This delta is the port-fidelity check.** Near-identical means v2's pass-1 height");
sb.AppendLine("> distribution matches the reference's — the crown-jewel port is faithful. A large gap");
sb.AppendLine("> would mean pass 1 diverged, and would be the finding rather than a nuisance.");
sb.AppendLine();
sb.AppendLine($"**Scale for judging any knot delta:** across the six seeds at {mapSize}, a single knot");
sb.AppendLine($"moves by up to **{seedSpread:F6} raw ({WorldScale.MetresFromRaw(seedSpread):F2} m)** from seed choice alone.");
sb.AppendLine("Anything smaller than that is seed noise, not a difference.");
sb.AppendLine();
sb.AppendLine($"**Scale invariance:** {showNote}.");
sb.AppendLine();
sb.AppendLine("## Realized land shares");
sb.AppendLine();
sb.AppendLine("| Band | Realized | Target | Output lands at |");
sb.AppendLine("|---|---|---|---|");
float benchTop = a.BenchBase + a.ShelfSpanMin, plateauTop = a.PlateauBase + a.ShelfSpanMin;
string[] lands =
{
$"{WorldScale.MetresFromRaw(a.Sea - a.Sea):F0}–{WorldScale.MetresFromRaw(a.OrangeCeil - a.Sea):F0} m",
$"{WorldScale.MetresFromRaw(a.OrangeCeil - a.Sea):F0}–{WorldScale.MetresFromRaw(a.RedCeil - a.Sea):F0} m",
$"{WorldScale.MetresFromRaw(a.RedCeil - a.Sea):F0}–{WorldScale.MetresFromRaw(a.BenchBase - a.Sea):F0} m",
$"~{WorldScale.MetresFromRaw(a.BenchBase - a.Sea):F0} m (bench)",
$"{WorldScale.MetresFromRaw(benchTop - a.Sea):F0}–{WorldScale.MetresFromRaw(a.PlateauBase - a.Sea):F0} m",
$"~{WorldScale.MetresFromRaw(a.PlateauBase - a.Sea):F0} m (plateau)",
$"{WorldScale.MetresFromRaw(plateauTop - a.Sea):F0}–{WorldScale.MetresFromRaw(a.PeakCap - a.Sea):F0} m",
};
for (int i = 0; i < 7; i++)
sb.AppendLine($"| {CurveKnots.BandNames[i]} | {shares[i]:F2} % | {CurveKnots.BandShareTargets[i]:F0} % | {lands[i]} |");
sb.AppendLine();
sb.AppendLine($"Shaped land range: **[{shapedPool.MinLand:F4} .. {shapedPool.MaxLand:F4}] raw** " +
$"= {WorldScale.MetresFromRaw(shapedPool.MinLand - a.Sea):F0}–{WorldScale.MetresFromRaw(shapedPool.MaxLand - a.Sea):F0} m above sea.");
sb.AppendLine();
sb.AppendLine("## ⭐ The elevation profile this produces");
sb.AppendLine();
sb.AppendLine("Land height percentiles, before and after the curve — **this is the finding**:");
sb.AppendLine();
sb.AppendLine("| Percentile | raw | shaped | shaped, metres above sea |");
sb.AppendLine("|---|---|---|---|");
foreach (double p in new[] { 10.0, 25.0, 50.0, 75.0, 90.0, 95.0, 99.0, 99.9 })
{
float r = rawPool.Quantile(p), s = shapedPool.Quantile(p);
sb.AppendLine($"| p{p:G} | {r:F3} | {s:F3} | **{WorldScale.MetresFromRaw(s - a.Sea):F0} m** |");
}
sb.AppendLine();
double belowBench = shapedPool.FractionBelow(a.BenchBase) * 100.0;
double belowPlateau = shapedPool.FractionBelow(a.PlateauBase) * 100.0;
sb.AppendLine($"- **{belowBench:F1} %** of land sits below the bench " +
$"({WorldScale.MetresFromRaw(a.BenchBase - a.Sea):F0} m).");
sb.AppendLine($"- **{belowPlateau:F1} %** of land sits below the plateau " +
$"({WorldScale.MetresFromRaw(a.PlateauBase - a.Sea):F0} m).");
// ⚠ Do NOT state the RAW median in metres. Raw pre-curve height has no metre meaning —
// the yardstick applies to CURVED output, which is what the storm-ladder anchors define.
// Converting the raw median would invent a "before" elevation the world never had.
sb.AppendLine($"- The median land column ends up **{WorldScale.MetresFromRaw(shapedPool.Quantile(50) - a.Sea):F0} m** " +
$"above sea: the curve maps raw {rawPool.Quantile(50):F3} → {shapedPool.Quantile(50):F3}.");
sb.AppendLine();
sb.AppendLine("> ⚠ **Read `histogram_shaped.png` before concluding the terrain is broken.** The curve is");
sb.AppendLine("> doing exactly what its share targets say: 60 % of land into the bottom band, 73 % below");
sb.AppendLine("> the red ceiling, 4 % above the plateau. If the island reads flat, that is a decision");
sb.AppendLine("> showing up in a render — not a bug. Changing it is the RESHAPE, and the reshape is a");
sb.AppendLine("> later task with its own gate.");
sb.AppendLine();
sb.AppendLine("## ⚠ Palette fit — reported, not fixed");
sb.AppendLine();
sb.AppendLine("The CostaRica stops were placed on **measured percentiles of the RAW distribution**");
sb.AppendLine("(Phase 1). The curve moves that distribution, so the ramp no longer sits where the land is:");
sb.AppendLine();
sb.AppendLine("| | raw | shaped |");
sb.AppendLine("|---|---|---|");
sb.AppendLine($"| land below palette stop 3 (0.310 raw) | {raw3Stop(rawPool) * 100:F1} % | **{raw3Stop(shapedPool) * 100:F1} %** |");
sb.AppendLine($"| land above the top stop (1.450, clamps white) | {(1.0 - rawPool.FractionBelow(1.450f)) * 100:F3} % | {(1.0 - shapedPool.FractionBelow(1.450f)) * 100:F3} % |");
sb.AppendLine();
sb.AppendLine("**Deliberately left alone.** Re-placing the stops now would make the render look more");
sb.AppendLine("dramatic while the terrain is genuinely lower — it would disguise the very finding above.");
sb.AppendLine("Recalibrating the palette is a presentation call to make *after* the elevation profile is");
sb.AppendLine("settled, not before.");
sb.AppendLine();
sb.AppendLine("## The oracle");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(checks));
sb.AppendLine($"**{(oracleOk ? "ALL PASS" : "⚠⚠ FAILURES PRESENT — do not judge this batch until they are resolved")}**");
sb.AppendLine();
if (variants)
{
sb.AppendLine("## Results");
sb.AppendLine();
sb.AppendLine("| Folder | Seed | Variant | h min | h max | land % | grayscale range | time |");
sb.AppendLine("|---|---|---|---|---|---|---|---|");
foreach (string row in rows) sb.AppendLine(row);
sb.AppendLine();
}
sb.AppendLine("`scratch/` is persistent and is never cleaned.");
string index = Path.Combine(batchRoot, "INDEX.md");
using var f = Godot.FileAccess.Open(index, Godot.FileAccess.ModeFlags.Write);
if (f == null) { GD.PrintErr($"could not write {index}"); return; }
f.StoreString(sb.ToString());
}
// ---- 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 int[] EnvSeeds(string k, int[] fallback)
{
string v = EnvStr(k, null);
if (v == null) return fallback;
var outp = new List();
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;
}
}
}