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

539 lines
26 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 MOUNTAIN-RESTORE BATCH (chat2/03) — put the mountain back, as a smooth slope.
///
/// ═══ THE STORY THIS BATCH TELLS, IN THREE HISTOGRAMS ═══
///
/// staircase the mass is there, but parked in two spikes (bench 100 m, plateau 220 m)
/// continuous_02 the spikes are gone — and so is the mass. It fell to 3090 m.
/// continuous_restored ⭐ the same mass as the staircase, spread as one smooth grade.
///
/// That contrast is the point, so the three are named to sort adjacent.
///
/// ═══ HOW THE RESTORATION IS MEASURED ═══
///
/// The climb's control points are no longer invented from shape knobs. They are MEASURED off the
/// staircase itself, on the same 6-seed pool tasks 01/02 use:
///
/// for p in {10,30,50,70,85,95} of ABOVE-CEILING land:
/// u_p ← that percentile of the RAW height (normalized into the climb's span)
/// v_p ← that percentile of the OUTPUT height (staircase, normalized)
///
/// PCHIP through those points reproduces the staircase's elevation envelope; the flat bench and
/// plateau interiors become grade because <c>ClimbCalibration.MinNormalizedSecant</c> floors
/// every segment. → <see cref="ClimbCalibration"/>.
///
/// ⚠ The two quantile sets are paired by percentile across the SAME cell population, which is
/// exact only if the staircase were strictly monotone per column. It is monotone in raw, but the
/// per-column bench/plateau modulation (±12 / ±20 m) blurs the pairing by about that much. That
/// is well inside the envelope being targeted, and calibrating on the MEASURED output (rather
/// than a nominal unmodulated curve) is what makes oracle (g)'s land-above-100 m figure the thing
/// actually being aimed at.
///
/// ═══ RUNNING IT ═══
///
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/MountainRestoreTool.tscn
///
/// ISLA_TASK / ISLA_BATCH / ISLA_MAPSIZE / ISLA_SEEDS / ISLA_SHOWPIECE_SIZE / ISLA_SHOWPIECE
/// ISLA_PHASE1_SOURCE (default "chat1/02_pass1_port")
/// ISLA_SKIP_RAW
/// ISLA_LIFT_BIG probe: the `continuous_bigger` lift (default 1.35)
/// ISLA_SHARP probe: the `continuous_sharper_peak` knob (default 2.5)
/// </summary>
public partial class MountainRestoreTool : Node
{
private static readonly int[] DefaultSeeds = { 1063685222, 777001 };
/// <summary>⚠ Task 01's pool, verbatim — the knots' identity, and with it the staircase control's.</summary>
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
private const int DefaultMapSize = 2048;
private const int DefaultShowpieceSize = 8192;
/// <summary>Oracle (g)'s PASS/NOTE threshold, percentage points of land above 100 m. Reported either way.</summary>
private const double MountainTolerancePp = 2.0;
public override void _Ready()
{
try { Run(); }
catch (Exception e)
{
GD.PrintErr("==================================================================");
GD.PrintErr($" REFUSED: {e.Message}");
GD.PrintErr("==================================================================");
GetTree().Quit(2);
}
}
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", 3);
string descr = EnvStr("ISLA_BATCH", "mountain_restore");
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";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
float liftBig = EnvFloat("ISLA_LIFT_BIG", 1.35f);
float sharpKnob = EnvFloat("ISLA_SHARP", 2.5f);
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
var anchors = CurveAnchors.Default;
float sea = 0.15f;
int primary = seeds[0];
GD.Print("==================================================================");
GD.Print(" MOUNTAIN RESTORE (chat2/03) — the staircase's mountain,");
GD.Print(" de-terraced. Calibrated, not invented.");
GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} showpiece {(showpiece ? showSize.ToString() : "off")}");
GD.Print($"yardstick : {WorldScale.Describe()}");
GD.Print($"seeds : {string.Join(", ", seeds)} (calibration pool: {string.Join(", ", CalibrationSeeds)})");
GD.Print($"batch : {batchRoot}");
GD.Print("==================================================================");
// ═══ 0. KNOTS — task 01's pool, re-measured for bit-identity ═══
GD.Print("\n--- 0. KNOTS ---");
var rawPool = new LandHistogram(sea);
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int seed in CalibrationSeeds)
{
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, seed));
pass1[seed] = p1;
rawPool.Accumulate(p1.Height, mapSize);
}
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]));
GD.Print($" {rawPool}");
GD.Print($" {knots}");
// ═══ 1. CALIBRATE — measure the staircase's above-ceiling elevation distribution ═══
//
// ⚠ The ceiling is the DEFAULT 30 m handover, which is exactly (K2, RED_CEIL). So
// "above-ceiling" is simply "raw > K2" — no derived float, and the same population the
// climb will later be responsible for.
GD.Print("\n--- 1. CALIBRATION (staircase above-ceiling distribution) ---");
float ceilingRaw = knots.K2;
var rawAbove = new LandHistogram(sea);
var outAbove = new LandHistogram(sea);
var stairPool = new Dictionary<int, Pass2Result>();
foreach (int seed in CalibrationSeeds)
{
var scfg = MakeConfig(mapSize, seed, knots, anchors, "staircase");
scfg.CurveMode = CurveModeKind.Staircase;
scfg.ShelfDetail = true;
Pass2Result st = Shaping.Shape(pass1[seed], scfg);
stairPool[seed] = st;
rawAbove.AccumulateWhere(pass1[seed].Height, pass1[seed].Height, mapSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[seed].Height, mapSize, ceilingRaw);
}
double shareAbove = 100.0 * rawAbove.TotalLand / rawPool.TotalLand;
GD.Print($" above-ceiling land: {rawAbove.TotalLand:N0} cells = {shareAbove:F1}% of all land");
var pcts = ClimbCalibration.DefaultPercentiles;
var rawQ = new float[pcts.Length];
var outQ = new float[pcts.Length];
GD.Print(" percentile → raw → staircase output");
for (int i = 0; i < pcts.Length; i++)
{
rawQ[i] = rawAbove.Quantile(pcts[i]);
outQ[i] = outAbove.Quantile(pcts[i]);
GD.Print($" P{pcts[i],-4:F0} raw {rawQ[i]:F4} → {WorldScale.MetresFromRaw(outQ[i] - sea),6:F1} m");
}
ClimbCalibration Calib(float lift, float sharp) => ClimbCalibration.FromPercentiles(
pcts, rawQ, outQ, ceilingRaw, HeightCurve.EffectiveSpikeMax(pass1[primary].HMaxSeed, knots, anchors),
anchors.RedCeil, anchors.PeakCap, lift, sharp);
// ⚠ ONE calibration object per knob pair, shared across seeds. spikeMax differs slightly
// per seed, but the calibration is NORMALIZED (u, v in [0,1]) — BuildCalibrated
// denormalizes against each seed's own spikeMax. So the shape is shared; the extent is
// per-seed, exactly as the per-seed peak normalization requires.
var calRestored = Calib(1.0f, 1.0f);
var calBigger = Calib(liftBig, 1.0f);
var calSharper = Calib(1.0f, sharpKnob);
GD.Print($" restored: {calRestored.Describe()}");
GD.Print($" bigger : {calBigger.Describe()}");
GD.Print($" sharper : {calSharper.Describe()}");
// ═══ 2. VARIANTS ═══
var variants = new List<(string label, Action<TerrainGenConfig> mutate)>
{
("staircase", c => { c.CurveMode = CurveModeKind.Staircase; c.ShelfDetail = true; }),
("continuous_02default", c => { c.CurveMode = CurveModeKind.Continuous;
c.ClimbCalibration = null; // the analytic 02 curve
c.ClimbFeather = 0.4f; c.SummitDrama = 2.5f; }),
("continuous_restored", c => { c.CurveMode = CurveModeKind.Continuous; c.ClimbCalibration = calRestored; }),
("continuous_bigger", c => { c.CurveMode = CurveModeKind.Continuous; c.ClimbCalibration = calBigger; }),
("continuous_sharper_peak", c => { c.CurveMode = CurveModeKind.Continuous; c.ClimbCalibration = calSharper; }),
};
GD.Print("\n--- 2. VARIANTS ---");
var results = new Dictionary<(int, string), Pass2Result>();
var offs = new Dictionary<int, Pass2Result>();
var rows = new List<string>();
bool notesPrinted = false;
foreach (int seed in seeds)
{
var offCfg = MakeConfig(mapSize, seed, knots, anchors, "curve_off");
offCfg.Curve = false;
offs[seed] = Shaping.Shape(pass1[seed], offCfg);
foreach (var (label, mutate) in variants)
{
var cfg = MakeConfig(mapSize, seed, knots, anchors, label);
mutate(cfg);
Pass2Result p2 = Shaping.Shape(pass1[seed], cfg);
results[(seed, label)] = p2;
if (!notesPrinted) foreach (string nt in p2.Notes) GD.Print(" " + nt);
rows.Add(WriteVariant(batchRoot, p2, sea, anchors, skipRaw));
}
notesPrinted = true;
}
// ═══ 3. ORACLE ═══
GD.Print("\n--- 3. ORACLE ---");
var hard = new List<ShapingOracle.Check>();
var soft = new List<ShapingOracle.Check>();
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off).
// ⚠ A missing dump now THROWS instead of skipping silently.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump",
offs[primary].Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, mapSize), mapSize, p1Dump));
// ⚑ RETIRED at rivers/01 — a2, the staircase == `01_curve_baseline` control.
// Superseded by the continuous grade (→ D-062) — see CurveContinuousTool for the full note.
// The dump is NOT deleted (file-safety; it is regenerable and it is the record of what
// was judged); its `INDEX.md` is marked superseded. The check is gone so nothing can
// pass against a superseded baseline. → XX_Human/output/rivers/01_*.report.md §A4.
long bFail = 0;
foreach (int seed in seeds)
foreach (var (label, _) in variants)
if (!ShapingOracle.ClassifyFidelity(pass1[seed], results[(seed, label)]).Passed) bFail++;
hard.Add(new ShapingOracle.Check
{
Id = "b", Name = "classify == raw, all seeds × all variants",
Passed = bFail == 0,
Detail = bFail == 0 ? $"bit-identical on {seeds.Length} seeds × {variants.Count} variants"
: $"{bFail} (seed, variant) pairs drifted",
});
bool cOk = results[(primary, "continuous_restored")].Notes
.Exists(n => n.Contains("strict-increase sample passed"));
hard.Add(new ShapingOracle.Check
{
Id = "c", Name = "monotone — FritschCarlson + per-seed sampled",
Passed = cOk,
Detail = cOk ? "confirmed on the calibrated climb (throws and refuses on violation)"
: "no strict-increase confirmation recorded",
});
string[] continuous = { "continuous_02default", "continuous_restored", "continuous_bigger", "continuous_sharper_peak" };
foreach (int seed in seeds)
foreach (string label in continuous)
{
var d = ShapingOracle.LowlandsPreserved(pass1[seed], results[(seed, "staircase")], results[(seed, label)]);
d.Name += $" [seed {seed}]";
hard.Add(d);
var e = ShapingOracle.UpperClimbProfile(results[(seed, label)]);
e.Name += $" [seed {seed}]";
soft.Add(e);
}
foreach (int seed in seeds)
foreach (var (label, _) in variants)
{
var f = ShapingOracle.SeaIdentity(offs[seed], results[(seed, label)], sea);
f.Name += $" [seed {seed}]";
hard.Add(f);
}
// (g) the restoration, measured — reported for every variant, gated for none.
var mountain = new List<ShapingOracle.Check>();
foreach (int seed in seeds)
foreach (string label in continuous)
{
var g = ShapingOracle.MountainRestored(results[(seed, label)], results[(seed, "staircase")],
sea, MountainTolerancePp);
g.Name += $" [seed {seed}]";
mountain.Add(g);
}
foreach (var c in hard) GD.Print(" " + c);
foreach (var c in soft) { if (c.Passed) GD.Print(" " + c); else GD.PrintErr(" ⚠ SLOPE: " + c); }
GD.Print(" --- (g) mountain, reported not gated ---");
foreach (var c in mountain) GD.Print(" " + c);
bool hardOk = hard.TrueForAll(c => c.Passed);
GD.Print($" ORACLE: {(hardOk ? "ALL HARD CHECKS PASS" : "*** HARD FAILURES ***")}");
// ═══ 4. HISTOGRAMS — the three-way contrast, adjacent by filename ═══
GD.Print("\n--- 4. HISTOGRAMS ---");
foreach (int seed in seeds)
{
var rawSeed = new LandHistogram(sea);
rawSeed.Accumulate(pass1[seed].Height, mapSize);
int order = 1;
foreach (var (label, _) in variants)
DrawShaped(results[(seed, label)], anchors, seed, mapSize, batchRoot, order++, sea);
}
// ═══ 5. SHOWPIECE ═══
string showNote = "skipped (ISLA_SHOWPIECE=0)";
if (showpiece)
{
GD.Print($"\n--- 5. SHOWPIECE at {showSize} (continuous_restored, seed {primary}) ---");
var cfg = MakeConfig(showSize, primary, knots, anchors, "continuous_restored_showpiece");
cfg.CurveMode = CurveModeKind.Continuous;
cfg.ClimbCalibration = calRestored;
Pass1Result p1 = Topography.Generate(cfg);
Pass2Result big = Shaping.Shape(p1, cfg);
foreach (string nt in big.Notes) GD.Print(" " + nt);
var cb = ShapingOracle.ClassifyFidelity(p1, big);
var offBigCfg = MakeConfig(showSize, primary, knots, anchors, "off"); offBigCfg.Curve = false;
var cf = ShapingOracle.SeaIdentity(Shaping.Shape(p1, offBigCfg), big, sea);
GD.Print($" {cb}");
GD.Print($" {cf}");
if (!cb.Passed || !cf.Passed) hardOk = false;
var (b100, b220) = ShapingOracle.LandAbove(big, sea);
GD.Print($" land >100 m {b100:F2}% >220 m {b220:F2}% (at {showSize})");
rows.Add(WriteVariant(batchRoot, big, sea, anchors, skipRaw));
showNote = $"seed {primary} at {showSize}; >100 m {b100:F2}%, >220 m {b220:F2}%";
}
WriteIndex(batchRoot, mapSize, showSize, seeds, primary, anchors, results, calRestored,
calBigger, calSharper, pcts, rawQ, outQ, hard, soft, mountain, rows, hardOk, showNote, sea);
GD.Print("\n==================================================================");
GD.Print($" DONE — {batchRoot}");
GD.Print($" ORACLE {(hardOk ? "HARD CHECKS ALL PASS" : "*** HARD FAILURES ***")}");
GD.Print("==================================================================");
GetTree().Quit(hardOk ? 0 : 3);
}
/// <remarks>
/// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This is chat-2 CURVE development: authored
/// and judged before the shape family existed, on the family-off distribution the knots are
/// percentiles of. The re-baseline flipped the bare defaults family-ON, so the pin is what
/// keeps this tool measuring the thing it was written to measure.
/// → <see cref="TerrainGenConfig.WithFamilyOff"/>.
/// </remarks>
private static TerrainGenConfig MakeConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label)
=> new TerrainGenConfig
{
MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, Knots = k, Anchors = a,
LowlandCeilingM = 30f,
}.WithFamilyOff();
// ---- output ---------------------------------------------------------
private static string WriteVariant(string batchRoot, Pass2Result p2, float sea,
CurveAnchors anchors, bool skipRaw)
{
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}");
DirAccess.MakeDirRecursiveAbsolute(dir);
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 = "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())
.SavePng(Path.Combine(dir, "relief.png"));
var (a100, a220) = ShapingOracle.LandAbove(p2, sea);
GD.Print($" {p2.VariantLabel,-30} seed {p2.Seed,-11} h[{p2.HMin,7:F3} .. {p2.HMax,6:F3}] " +
$" >100m {a100,5:F2}% >220m {a220,5:F2}% {p2.ElapsedMs,5} ms");
return $"| `{p2.Seed}_{p2.VariantLabel}` | {p2.Seed} | {p2.VariantLabel} | {p2.HMin:F3} | {p2.HMax:F3} | " +
$"{a100:F2}% | {a220:F2}% | {gMin:F3}..{gMax:F3} | {p2.ElapsedMs} ms |";
}
private static void DrawShaped(Pass2Result p2, CurveAnchors a, int seed, int mapSize,
string batchRoot, int order, float sea)
{
var shaped = new LandHistogram(sea);
shaped.Accumulate(p2.Height, mapSize);
float top = MathF.Ceiling(shaped.MaxLand * 20f) / 20f;
var display = shaped.Rebin((top - shaped.SeaLevel) / 360f);
var (a100, a220) = ShapingOracle.LandAbove(p2, sea);
var o = new HistogramRenderer.Options
{
Title = $"{p2.VariantLabel.ToUpperInvariant()} - SEED {seed}",
Subtitle = $"LAND ABOVE 100M {a100:F2} PCT - ABOVE 220M {a220:F2} PCT",
XAxisLabel = "RAW HEIGHT (POST-CURVE)",
XTop = top,
Footer = $"{shaped.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}",
};
// The two heights the restoration is measured at, on every plate, so the three-way
// contrast can be read off the same reference lines.
o.Markers.Add(new HistogramRenderer.Marker { Value = a.Sea + WorldScale.RawFromMetres(100f), Label = "100M" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.Sea + WorldScale.RawFromMetres(220f), Label = "220M" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M", Strong = false });
if (p2.Continuous != null)
o.Markers.Add(new HistogramRenderer.Marker { Value = p2.Continuous.CeilingOut, Label = "LOWLAND", Strong = false });
string file = $"hist_{seed}_{order}_{p2.VariantLabel}.png";
HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, file));
GD.Print($" {file}");
}
private static void WriteIndex(string batchRoot, int mapSize, int showSize, int[] seeds, int primary,
CurveAnchors a, Dictionary<(int, string), Pass2Result> results,
ClimbCalibration calRestored, ClimbCalibration calBigger, ClimbCalibration calSharper,
double[] pcts, float[] rawQ, float[] outQ,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> soft, List<ShapingOracle.Check> mountain,
List<string> rows, bool hardOk, string showNote, float sea)
{
var sb = new StringBuilder();
sb.AppendLine("# Batch 03 — restore the mountain, as a smooth slope");
sb.AppendLine();
sb.AppendLine("The continuous climb's control points are now **measured off the staircase** instead of");
sb.AppendLine("invented from shape knobs. Same mountain mass, zero terraces. The lowlands are still");
sb.AppendLine("preserved bit-for-bit (oracle d).");
sb.AppendLine();
sb.AppendLine("## ⭐ Open this first");
sb.AppendLine();
sb.AppendLine($"1. **`{primary}_continuous_restored_showpiece/relief.png`** — the centerpiece ({showNote}).");
sb.AppendLine("2. **The three-way histogram contrast**, adjacent by filename:");
sb.AppendLine($" - `hist_{primary}_1_staircase.png` — the mass, parked in two spikes");
sb.AppendLine($" - `hist_{primary}_2_continuous_02default.png` — spikes gone, **and so is the mass**");
sb.AppendLine($" - `hist_{primary}_3_continuous_restored.png` — ⭐ **the mass back, spread smooth**");
sb.AppendLine(" Every plate carries the same 100 m / 220 m reference lines.");
sb.AppendLine();
sb.AppendLine("## The restoration, measured");
sb.AppendLine();
sb.AppendLine("| Variant | land >100 m | land >220 m |");
sb.AppendLine("|---|---|---|");
foreach (string label in new[] { "staircase", "continuous_02default", "continuous_restored", "continuous_bigger", "continuous_sharper_peak" })
{
var (x100, x220) = ShapingOracle.LandAbove(results[(primary, label)], sea);
string star = label == "continuous_restored" ? " ⭐" : label == "staircase" ? " *(target)*" : "";
sb.AppendLine($"| `{label}`{star} | {x100:F2} % | {x220:F2} % |");
}
sb.AppendLine();
sb.AppendLine($"*(seed {primary} at {mapSize}; per-seed rows in Results below.)*");
sb.AppendLine();
sb.AppendLine("## The calibration");
sb.AppendLine();
sb.AppendLine("Measured on the 6-seed pool, above-ceiling land only:");
sb.AppendLine();
sb.AppendLine("| percentile | raw | staircase output |");
sb.AppendLine("|---|---|---|");
for (int i = 0; i < pcts.Length; i++)
sb.AppendLine($"| P{pcts[i]:F0} | {rawQ[i]:F4} | **{WorldScale.MetresFromRaw(outQ[i] - sea):F0} m** |");
sb.AppendLine();
sb.AppendLine("| Variant | knobs | control points (u,v) |");
sb.AppendLine("|---|---|---|");
sb.AppendLine($"| `continuous_restored` | {calRestored.Describe().Split('·')[0].Trim()} | `{calRestored.Describe().Split('·')[1].Trim()}` |");
sb.AppendLine($"| `continuous_bigger` | {calBigger.Describe().Split('·')[0].Trim()} | `{calBigger.Describe().Split('·')[1].Trim()}` |");
sb.AppendLine($"| `continuous_sharper_peak` | {calSharper.Describe().Split('·')[0].Trim()} | `{calSharper.Describe().Split('·')[1].Trim()}` |");
sb.AppendLine();
sb.AppendLine($"`floored` counts segments the no-bench floor had to lift — i.e. where the staircase was flat.");
sb.AppendLine();
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
sb.AppendLine();
sb.AppendLine("`ProvisionalEven` — the CostaRica colours re-spaced evenly SEA → 420 m. Final calibration");
sb.AppendLine("waits for the chosen profile. **Grayscale + the histograms are the honest instruments.**");
sb.AppendLine();
sb.AppendLine("## The oracle");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(hard));
sb.AppendLine($"**{(hardOk ? "ALL HARD CHECKS PASS" : " HARD FAILURES do not judge this batch")}**");
sb.AppendLine();
sb.AppendLine("Soft — upper climb slope profile (e):");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(soft));
sb.AppendLine("(g) mountain restored — **reported, not gated** (it is a taste target the developer tunes):");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(mountain));
sb.AppendLine("## Disposability");
sb.AppendLine();
sb.AppendLine("| Artifact | Keep? |");
sb.AppendLine("|---|---|");
sb.AppendLine("| `relief.png`, `hist_*.png`, `INDEX.md` | **keep** |");
sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code (the byte-level oracle) |");
sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
sb.AppendLine();
sb.AppendLine("## Results");
sb.AppendLine();
sb.AppendLine("| Folder | Seed | Variant | h min | h max | >100 m | >220 m | grayscale range | time |");
sb.AppendLine("|---|---|---|---|---|---|---|---|---|");
foreach (string row in rows) sb.AppendLine(row);
sb.AppendLine();
sb.AppendLine($"MapSize {mapSize}, showpiece {showSize}, seeds {string.Join(", ", seeds)}. {WorldScale.Describe()}.");
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 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;
}
}
}