chat2/02 dissolved the terraces and lost two thirds of the mountain with them, then
concluded the loss was structural and needed a Phase-1 noise change. That conclusion
was wrong, and this commit is the refutation.
A monotone curve is a free reparametrization: it may be gentle in one place and steep
in another, and can lift bottom-heavy input into a substantial massif without ever
going flat. The area above a height is set by where the percentile->height mapping
crosses it, and that mapping is entirely ours to choose. The 02 sweep that "proved"
the loss structural varied climbFeather, which shapes the JOIN, not the mass
distribution — the wrong knob, and too strong a conclusion drawn from it.
Core/ClimbCalibration — the climb's control points are now MEASURED off the staircase
instead of invented from shape knobs. For p in {10,30,50,70,85,95} of above-ceiling
land, take that percentile's raw height and its staircase output height; PCHIP through
the pairs. That reproduces the staircase's elevation envelope, so the mountain mass
returns, while MinNormalizedSecant floors every grade so the flat bench and plateau
interiors become slope. The floor bites on exactly one segment — the plateau — which
is precisely where the staircase was flat.
ContinuousCurve.BuildCalibrated joins it to the same pinned lowland handover, the same
C1 join and the same per-seed spikeMax. The 02 analytic path survives unchanged as the
"before" contrast, and deliberately keeps its strictly-increasing-secant rule: a
calibrated curve is WAVY by design, so convexity is the wrong invariant for it and the
secant floor is the right one.
peakSharpness replaces summitDrama and fixes its bad trade. Drama steepened the peak by
pulling the summit ONSET down, dragging the whole massif with it (p99 199 -> 121 m).
Sharpness reshapes only above the last measured percentile, leaving that height fixed,
so peak and massif are independent: raising it leaves p90, >100 m and >220 m untouched
and only moves land within the summit.
Measured, both seeds, 2048:
variant >100 m >220 m p90
staircase (target) 16.05% 4.53% 127.4 m
continuous_02default 4.80% 0.63% 58.4 m
continuous_restored 14.25% 3.45% 123.2 m
continuous_bigger 19.70% 5.80% 163.5 m
Oracle all hard checks pass, including (a2) staircase still bit-identical to task 01's
dump and (d) lowlands bit-identical across every calibrated variant. New (g) reports
land above 100/220 m per variant and is deliberately NOT gated — it is a taste target
the developer tunes, and gating it would make mountainLift unusable. What it must never
do is stay silent, which is how 02 lost the mountain unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
524 lines
24 KiB
C#
524 lines
24 KiB
C#
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 30–90 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 "02_pass1_port") · ISLA_T01_SOURCE (default "01_curve_baseline")
|
||
/// 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());
|
||
|
||
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", "02_pass1_port");
|
||
string t01Source = EnvStr("ISLA_T01_SOURCE", "01_curve_baseline");
|
||
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(new TerrainGenConfig { MapSize = mapSize, Seed = 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>();
|
||
|
||
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32");
|
||
string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32");
|
||
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump",
|
||
offs[primary].Height, HeightField.Load(p1Dump, mapSize), mapSize, p1Dump));
|
||
hard.Add(ShapingOracle.DumpRegression("a2", "staircase == task-01 curve_on .f32 dump",
|
||
results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump));
|
||
|
||
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 — Fritsch–Carlson + 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);
|
||
}
|
||
|
||
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,
|
||
};
|
||
|
||
// ---- 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;
|
||
}
|
||
}
|
||
}
|