islaApocalypse-v2/Tools/Scripts/CurveContinuousTool.cs
beezm 8e55326a84 Phase 2a: continuous grade — smooth the upper staircase, preserve the lowlands
rev 3 of the curve redesign. The developer's verdict on the 01 baseline was that the
LOWLANDS ARE GOOD; the fault is the terracing above them. So this adds a second curve
mode that preserves the low plain bit-for-bit and replaces everything above the flood
line with one smooth monotone climb.

Core/ContinuousCurve — piecewise, and the pieces have different loyalties:
- at/below sea: identity, as ever.
- above sea to K2: DELEGATES to HeightCurve's own toe+red branches. Not "equivalent" —
  the same code path, so the same floats. Oracle (d) holds it to that.
- above the ceiling: a Fritsch-Carlson (PCHIP) monotone spline to the 420 m cap,
  C1-joined to the red band's exit slope. Monotone by construction for any ordered
  control points, which retires the 24-corner sweep; a 10k strict-increase sample runs
  per seed anyway, because "cannot fail" is worth a millisecond.
- Build() REFUSES rather than degrades: a ceiling near the old bench, a drama that folds
  the summit under its own onset, control-point secants that are not strictly increasing
  (the no-magnet rule, enforced rather than hoped for).

Only BENCH_*/PLATEAU_* are dropped. SEA/ORANGE_CEIL/RED_CEIL survive because they are
the storm-ladder FLOOD TIERS and they live inside the preserved lowland; PEAK_CAP and
the per-seed spikeMax normalization survive as the summit.

Shelf detail is forced off in continuous mode: the flat benches it de-slabbed no longer
exist, and painting noise on the climb now would pre-judge what erosion should carve.

Oracle, all hard checks passing:
- (a1) curve off is bit-identical to Phase 1's dump.
- (a2) staircase mode is bit-identical to TASK 01's dump — the control is provably the
  control, not a re-derivation. (CurveBaselineTool is pinned to Staircase so the config
  default moving to Continuous cannot drift it.)
- (d) lowlands bit-identical to the staircase over 3.6M cells, every continuous variant,
  both seeds. The lifted_WRONG bookend fails it on 1.6M cells, as intended.
- (f) sea identity per CELL, not per count, including 67M cells at 8192.

The finding, measured and recorded in the batch scratch: the massif SHRANK. Land above
100 m goes 14.9% -> 4.8%, above 220 m 4.5% -> 0.6%. A feather sweep to the practical
floor recovers ~1.3 points, so this is structural, not a tuning miss: the staircase's
highland area was an artifact of the bench and plateau acting as magnets, and a curve
with no magnets preserves the raw distribution's bottom-heavy shape. "No terraces" and
"the same land up high" are not both available from curve work alone.

Exploration batch, not convergence. A tuning pass follows once a direction is picked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
2026-08-20 03:41:27 -04:00

580 lines
28 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 CONTINUOUS-GRADE EXPLORATION BATCH (chat2/02, rev 3) — smooth the upper staircase,
/// preserve the lowlands, and prove both claims before anyone looks at a render.
///
/// ═══ THE TARGET SILHOUETTE (judge every shaped histogram against THIS) ═══
///
/// The bottom shoulder stays exactly where it is — the broad low pile, ~75 % of land below
/// 30 m, UNLIFTED. Above it, the bench and plateau spikes dissolve and the empty valleys fill
/// in: one smooth continuous falloff out to a thin dramatic peak at 420 m. Only the top ~25 %
/// of land moves.
///
/// ═══ THE VARIANTS — tight, one axis each ═══
///
/// staircase the faithful M3 control (task 01, bit-identical — oracle a2)
/// continuous_default lowland to 30 m, feather 0.40, drama 2.5 — the centerpiece
/// continuous_hold_higher lowland ceiling raised to 50 m — the primary axis explored up
/// continuous_dramatic_peak drama 4.5 — how pointy the peak gets
/// continuous_lifted_WRONG ⚠ the even whole-island lift — DELIBERATELY the wrong direction,
/// a contrast bookend. Its oracle-(d) failure is EXPECTED and is
/// reported as confirmation of why the direction is wrong.
///
/// ═══ ⚠ WHY THIS TOOL RE-MEASURES THE KNOTS INSTEAD OF USING CurveKnots.V2Baseline ═══
///
/// Task 01's batch was generated with the knots MEASURED IN THAT RUN, at full float precision.
/// The baked V2Baseline constants are those values quoted to six decimals — off by &lt;1e-7 raw,
/// which is invisible to every consumer EXCEPT a bit-identity oracle. Oracle (a2) demands the
/// staircase reproduce task 01's `.f32` byte-for-byte, so this tool re-runs the identical
/// 6-seed calibration (same seeds, same size, same instrument) and uses the measured knots.
/// It prints the measured-vs-baked deltas so the equivalence is evidence, not assumption.
///
/// ═══ RUNNING IT ═══
///
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/CurveContinuousTool.tscn
///
/// ISLA_TASK authoring task number (default 2)
/// ISLA_BATCH descriptor, NO prefix (default "curve_continuous")
/// ISLA_MAPSIZE variant profile (default 2048)
/// ISLA_SEEDS variant seeds, comma-separated (default: the 2 pinned below)
/// ISLA_SHOWPIECE_SIZE the big confirmation render (default 8192)
/// ISLA_SHOWPIECE "0" to skip it
/// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "02_pass1_port")
/// ISLA_T01_SOURCE batch holding task-01 .f32 (default "01_curve_baseline")
/// ISLA_SKIP_RAW "1" to skip the .f32 dumps
/// ISLA_CEILING_M probe override: lowland ceiling, metres (default 30)
/// ISLA_FEATHER probe override: climb feather, 0..1 (default 0.4)
/// ISLA_DRAMA probe override: summit drama, >= 1 (default 2.5)
///
/// ⚠ The three knob overrides move the BASE config only. Each batch variant sets its own axis
/// explicitly in its mutator, so a probe env can shift `continuous_default` but can never
/// silently move `hold_higher`'s ceiling or `dramatic_peak`'s drama out from under their names.
/// </summary>
public partial class CurveContinuousTool : Node
{
/// <summary>Two representative variant seeds — the primary Phase-1 seed and the tallest one.</summary>
private static readonly int[] DefaultSeeds = { 1063685222, 777001 };
/// <summary>
/// ⚠ THE TASK-01 CALIBRATION POOL, VERBATIM — same six seeds, so the measured knots (and
/// with them the staircase control) reproduce task 01 bit-for-bit. Do not "simplify" this
/// to the variant seeds; the pool is part of the knots' identity.
/// </summary>
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
private const int DefaultMapSize = 2048;
private const int DefaultShowpieceSize = 8192;
public override void _Ready()
{
// ⚠ An exception out of _Ready does not stop Godot — it logs and the process HANGS with
// no main loop to end it. Catch, say what was refused, exit 2. (The standing rule.)
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", 2);
string descr = EnvStr("ISLA_BATCH", "curve_continuous");
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";
string batchRoot = ToolingPaths.BatchRoot(task, descr); // composed, never free-form
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
var anchors = CurveAnchors.Default;
float sea = 0.15f;
int primary = seeds[0];
GD.Print("==================================================================");
GD.Print(" CURVE CONTINUOUS (rev 3) — smooth the upper staircase,");
GD.Print(" preserve the lowlands. The staircase rides along as the control.");
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("------------------------------------------------------------------");
GD.Print(ToolingPaths.Describe());
GD.Print("==================================================================");
// ═══ 0. RE-MEASURE THE KNOTS — task 01's calibration, verbatim (see the type header) ═══
GD.Print("\n--- 0. CALIBRATION (the task-01 pool, re-run for bit-identity) ---");
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);
GD.Print($" pooled seed {seed,-11} h[{p1.HMinSeed,7:F3} .. {p1.HMaxSeed,6:F3}] {p1.ElapsedMs,5} ms");
}
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(" measured vs baked V2Baseline (6-decimal roundings — expect <1e-6):");
for (int i = 0; i < 6; i++)
GD.Print($" K{i + 1}: measured {knots[i]:G9} baked {CurveKnots.V2Baseline[i]:G9} " +
$"delta {knots[i] - CurveKnots.V2Baseline[i]:E2}");
// ═══ 1. THE VARIANTS ═══
var variants = new List<(string label, Action<TerrainGenConfig> mutate)>
{
("staircase", c => { c.CurveMode = CurveModeKind.Staircase; c.ShelfDetail = true; }),
("continuous_default", c => { c.CurveMode = CurveModeKind.Continuous; }),
("continuous_hold_higher", c => { c.CurveMode = CurveModeKind.Continuous; c.LowlandCeilingM = 50f; }),
("continuous_dramatic_peak", c => { c.CurveMode = CurveModeKind.Continuous; c.SummitDrama = 4.5f; }),
("continuous_lifted_WRONG", c => { c.CurveMode = CurveModeKind.LiftedWrong; }),
};
GD.Print("\n--- 1. VARIANTS ---");
var results = new Dictionary<(int seed, string label), Pass2Result>();
var offs = new Dictionary<int, Pass2Result>();
var rows = new List<string>();
bool notesPrinted = false;
foreach (int seed in seeds)
{
Pass1Result p1 = pass1[seed];
offs[seed] = Shaping.Shape(p1, BaseConfig(mapSize, seed, knots, anchors, "curve_off", off: true));
foreach (var (label, mutate) in variants)
{
var cfg = BaseConfig(mapSize, seed, knots, anchors, label, off: false);
mutate(cfg);
Pass2Result p2 = Shaping.Shape(p1, cfg);
results[(seed, label)] = p2;
if (!notesPrinted) foreach (string n in p2.Notes) GD.Print(" " + n);
rows.Add(WriteVariant(batchRoot, p2, sea, anchors, skipRaw));
}
notesPrinted = true;
}
// ═══ 2. THE ORACLE — before anything is looked at ═══
GD.Print("\n--- 2. ORACLE ---");
var hard = new List<ShapingOracle.Check>();
var soft = new List<ShapingOracle.Check>();
ShapingOracle.Check bookend;
{
Pass1Result pp1 = pass1[primary];
// (a1) curve off == Phase 1's own dump.
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, HeightField.Load(p1Dump, mapSize), mapSize, p1Dump));
// (a2) staircase == task 01's own dump — the control is the control.
string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a2", "staircase mode == task-01 curve_on .f32 dump",
results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump));
// (b) classify == raw, every seed × every variant.
long bFail = 0;
foreach (int seed in seeds)
foreach (var (label, _) in variants)
{
var c = ShapingOracle.ClassifyFidelity(pass1[seed], results[(seed, label)]);
if (!c.Passed) { bFail++; GD.PrintErr($" classify drift: seed {seed} {label}: {c.Detail}"); }
}
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",
});
// (c) monotone — the staircase's sweep and the continuous strict-increase sample
// both throw on violation, so reaching here means they passed; state it explicitly.
bool cStair = results[(primary, "staircase")].Notes.Exists(n => n.Contains("Monotonicity assertion passed"));
bool cCont = results[(primary, "continuous_default")].Notes.Exists(n => n.Contains("strict-increase sample passed"));
hard.Add(new ShapingOracle.Check
{
Id = "c", Name = "monotone — staircase sweep + continuous strict-increase sample",
Passed = cStair && cCont,
Detail = $"staircase sweep {(cStair ? "confirmed" : "MISSING")}, " +
$"continuous sample {(cCont ? "confirmed" : "MISSING")} (both throw-and-refuse on violation)",
});
// (d) ⭐ lowlands preserved — every continuous variant, both seeds. HARD.
foreach (int seed in seeds)
foreach (string label in new[] { "continuous_default", "continuous_hold_higher", "continuous_dramatic_peak" })
{
var c = ShapingOracle.LowlandsPreserved(pass1[seed], results[(seed, "staircase")], results[(seed, label)]);
c.Name += $" [seed {seed}]";
hard.Add(c);
}
// (d, bookend) the WRONG lift is EXPECTED to fail this — its failure is the point.
bookend = ShapingOracle.LowlandsPreserved(pp1, results[(primary, "staircase")],
results[(primary, "continuous_lifted_WRONG")]);
// (e) upper climb profile — SOFT (exploration): warn loudly, do not gate the exit.
foreach (int seed in seeds)
foreach (string label in new[] { "continuous_default", "continuous_hold_higher", "continuous_dramatic_peak" })
{
var c = ShapingOracle.UpperClimbProfile(results[(seed, label)]);
c.Name += $" [seed {seed}]";
soft.Add(c);
}
// (f) sea identity — every variant, both seeds, per cell. HARD.
foreach (int seed in seeds)
foreach (var (label, _) in variants)
{
var c = ShapingOracle.SeaIdentity(offs[seed], results[(seed, label)], sea);
c.Name += $" [seed {seed}]";
hard.Add(c);
}
}
foreach (var c in hard) GD.Print(" " + c);
foreach (var c in soft)
{
if (c.Passed) GD.Print(" " + c);
else GD.PrintErr(" ⚠ SLOPE PROFILE: " + c);
}
GD.Print($" bookend (expected FAIL): {bookend}");
if (bookend.Passed)
GD.PrintErr(" ⚠⚠ the lifted_WRONG bookend PRESERVED the lowlands — it is not doing its job as a contrast.");
bool hardOk = hard.TrueForAll(c => c.Passed) && !bookend.Passed;
GD.Print($" ORACLE: {(hardOk ? "ALL HARD CHECKS PASS" : "*** HARD FAILURES ***")}" +
$"{(soft.TrueForAll(c => c.Passed) ? "" : " (soft slope warnings present see above)")}");
// ═══ 3. HISTOGRAMS — the contrast, adjacent by filename ═══
GD.Print("\n--- 3. HISTOGRAMS ---");
foreach (int seed in seeds)
{
var rawSeed = new LandHistogram(sea);
rawSeed.Accumulate(pass1[seed].Height, mapSize);
DrawRawHist(rawSeed, knots, seed, mapSize, batchRoot);
int order = 1;
foreach (var (label, _) in variants)
{
DrawShapedHist(results[(seed, label)], rawSeed, anchors, seed, mapSize, batchRoot, order++);
}
}
// ═══ 4. SHOWPIECE — continuous_default at the big profile ═══
string showNote = "skipped (ISLA_SHOWPIECE=0)";
if (showpiece)
{
GD.Print($"\n--- 4. SHOWPIECE at {showSize} (continuous_default, seed {primary}) ---");
var cfg = BaseConfig(showSize, primary, knots, anchors, "continuous_default_showpiece", off: false);
cfg.CurveMode = CurveModeKind.Continuous;
Pass1Result p1 = Topography.Generate(cfg);
GD.Print($" pass1 {showSize}: h[{p1.HMinSeed:F3} .. {p1.HMaxSeed:F3}] {p1.ElapsedMs} ms");
Pass2Result big = Shaping.Shape(p1, cfg);
foreach (string n in big.Notes) GD.Print(" " + n);
// The cheap checks that transfer to the big profile: classify fidelity + sea identity.
var cb = ShapingOracle.ClassifyFidelity(p1, big);
var offBig = Shaping.Shape(p1, BaseConfig(showSize, primary, knots, anchors, "off", off: true));
var cf = ShapingOracle.SeaIdentity(offBig, big, sea);
GD.Print($" {cb}");
GD.Print($" {cf}");
if (!cb.Passed || !cf.Passed) hardOk = false;
rows.Add(WriteVariant(batchRoot, big, sea, anchors, skipRaw));
showNote = $"seed {primary} at {showSize} — classify + sea identity re-verified there";
}
WriteIndex(batchRoot, mapSize, showSize, seeds, primary, knots, anchors,
results, hard, soft, bookend, rows, hardOk, showNote);
GD.Print("\n==================================================================");
GD.Print($" DONE — {batchRoot}");
GD.Print($" ORACLE {(hardOk ? "HARD CHECKS ALL PASS" : "*** HARD FAILURES see the table ***")}");
GD.Print("==================================================================");
GetTree().Quit(hardOk ? 0 : 3);
}
// ---- configs ----------------------------------------------------------
private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
string label, bool off) => new TerrainGenConfig
{
MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = !off, ShelfDetail = false, Knots = k, Anchors = a,
// Continuous knob defaults; variant mutators override per axis.
//
// ⚠ ENV-OVERRIDABLE so the knobs can be probed WITHOUT editing source and rebuilding —
// they are "eye-iteration knobs" per the task, and a knob you have to recompile to turn
// is not one. A probe run redirects ISLA_OUTPUT_DIR to scratch and sweeps these; the
// BATCH variants set them explicitly in their mutators, so a probe env cannot silently
// move the batch of record.
LowlandCeilingM = EnvFloat("ISLA_CEILING_M", 30f),
ClimbFeather = EnvFloat("ISLA_FEATHER", 0.4f),
SummitDrama = EnvFloat("ISLA_DRAMA", 2.5f),
};
// ---- output -----------------------------------------------------------
private static string WriteVariant(string batchRoot, Pass2Result p2, float sea,
CurveAnchors anchors, bool skipRaw)
{
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{LabelOf(p2)}");
DirAccess.MakeDirRecursiveAbsolute(dir);
// Plain data beside the pretty render, always.
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"));
// ⚠ HILLSHADED, deliberately — the continuous grade is the first terrain with coherent
// shape worth lighting (Phase 1's rule was "flat is the hero" BECAUSE raw noise fuzzes;
// that reasoning inverts once the climb is smooth). Subtle settings, and the palette is
// the PROVISIONAL even ramp — flagged as such in the palette, the INDEX and the report.
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);
Image withLegend = LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap,
LabelOf(p2).ToUpperInvariant());
withLegend.SavePng(Path.Combine(dir, "relief.png"));
float land = p2.LandFraction(sea);
GD.Print($" {LabelOf(p2),-26} seed {p2.Seed,-11} h[{p2.HMin,7:F3} .. {p2.HMax,6:F3}] " +
$" land {land * 100,5:F1}% {p2.ElapsedMs,5} ms");
return $"| `{p2.Seed}_{LabelOf(p2)}` | {p2.Seed} | {LabelOf(p2)} | {p2.HMin:F3} | {p2.HMax:F3} | " +
$"{land * 100:F1}% | {gMin:F3}..{gMax:F3} | {p2.ElapsedMs} ms |";
}
/// <summary>The variant folder name — read off the result, never re-derived.</summary>
private static string LabelOf(Pass2Result p2) => p2.VariantLabel;
private static void DrawRawHist(LandHistogram raw, CurveKnots k, int seed, int mapSize, string batchRoot)
{
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. K2 IS THE LOWLAND CEILING - EVERYTHING LEFT OF IT IS PRESERVED.",
XAxisLabel = "RAW HEIGHT (PRE-CURVE)",
XTop = top,
Footer = $"{raw.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}",
};
o.Markers.Add(new HistogramRenderer.Marker { Value = k.K2, Label = "K2 LOWLAND CEIL" });
o.Bands.Add(new HistogramRenderer.Band
{
Lo = raw.SeaLevel, Hi = k.K2, Label = "preserved lowland",
SharePercent = raw.FractionBelow(k.K2) * 100.0,
});
o.Bands.Add(new HistogramRenderer.Band
{
Lo = k.K2, Hi = top, Label = "the climb's input",
SharePercent = (1.0 - raw.FractionBelow(k.K2)) * 100.0,
});
HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, $"hist_{seed}_0_raw.png"));
GD.Print($" hist_{seed}_0_raw.png");
}
private static void DrawShapedHist(Pass2Result p2, LandHistogram rawSeed, CurveAnchors a,
int seed, int mapSize, string batchRoot, int order)
{
var shaped = new LandHistogram(rawSeed.SeaLevel);
shaped.Accumulate(p2.Height, mapSize);
float top = MathF.Ceiling(shaped.MaxLand * 20f) / 20f;
var display = shaped.Rebin((top - shaped.SeaLevel) / 360f);
string label = LabelOf(p2);
var o = new HistogramRenderer.Options
{
Title = $"SHAPED - {label.ToUpperInvariant()} - SEED {seed}",
XAxisLabel = "RAW HEIGHT (POST-CURVE)",
XTop = top,
Footer = $"{shaped.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}",
};
if (p2.Continuous != null)
{
var c = p2.Continuous;
var (onsetRaw, onsetOut) = c.SummitOnsetPoint();
o.Subtitle = "TARGET: BOTTOM PILE UNMOVED - ABOVE IT ONE SMOOTH FALLOFF TO A THIN PEAK.";
o.Markers.Add(new HistogramRenderer.Marker { Value = c.CeilingOut, Label = $"LOWLAND {c.LowlandCeilingM:F0}M" });
o.Markers.Add(new HistogramRenderer.Marker { Value = onsetOut, Label = "SUMMIT ONSET", Strong = false });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M" });
o.Bands.Add(new HistogramRenderer.Band
{
Lo = rawSeed.SeaLevel, Hi = c.CeilingOut, Label = "preserved lowland",
SharePercent = rawSeed.FractionBelow(c.CeilingRaw) * 100.0,
});
o.Bands.Add(new HistogramRenderer.Band
{
Lo = c.CeilingOut, Hi = onsetOut, Label = "the climb",
SharePercent = rawSeed.FractionBetween(c.CeilingRaw, onsetRaw) * 100.0,
});
o.Bands.Add(new HistogramRenderer.Band
{
Lo = onsetOut, Hi = top, Label = "summit",
SharePercent = (1.0 - rawSeed.FractionBelow(onsetRaw)) * 100.0,
});
}
else if (p2.CurveModeLabel == "staircase")
{
o.Subtitle = "THE CONTROL: THE M3 STAIRCASE - BENCH AND PLATEAU SPIKES BY DESIGN.";
o.Markers.Add(new HistogramRenderer.Marker { Value = a.OrangeCeil, Label = "ORANGE", Strong = false });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.RedCeil, Label = "RED", Strong = false });
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" });
}
else
{
o.Subtitle = "THE WRONG DIRECTION: THE WHOLE ISLAND LIFTED OFF ITS SHORELINE. A BOOKEND.";
o.Markers.Add(new HistogramRenderer.Marker { Value = a.RedCeil, Label = "OLD 30M LINE", Strong = false });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M" });
}
string file = $"hist_{seed}_{order}_{label}.png";
HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, file));
GD.Print($" {file}");
}
// ---- the index ----------------------------------------------------------
private static void WriteIndex(string batchRoot, int mapSize, int showSize, int[] seeds,
int primary, CurveKnots knots, CurveAnchors a,
Dictionary<(int, string), Pass2Result> results,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> soft,
ShapingOracle.Check bookend, List<string> rows, bool hardOk, string showNote)
{
var sb = new StringBuilder();
sb.AppendLine("# Batch 02 — continuous grade: smooth the upper staircase, preserve the lowlands");
sb.AppendLine();
sb.AppendLine("**rev 3.** The lowlands are GOOD and are preserved bit-for-bit (oracle d). Everything");
sb.AppendLine("above the 30 m flood line is replaced by one smooth monotone climb to the 420 m cap.");
sb.AppendLine("The staircase rides along as the control; the WRONG-direction whole-island lift rides");
sb.AppendLine("along as a contrast bookend. **Exploration, not convergence** — a tuning pass follows");
sb.AppendLine("once a direction is picked.");
sb.AppendLine();
sb.AppendLine("## ⭐ Open this first");
sb.AppendLine();
sb.AppendLine($"1. **`{primary}_continuous_default_showpiece/relief.png`** — the centerpiece ({showNote}).");
sb.AppendLine($"2. **`hist_{primary}_1_staircase.png` → `hist_{primary}_2_continuous_default.png`** — the");
sb.AppendLine(" contrast, adjacent by filename: bottom pile unchanged, the bench/plateau spikes above");
sb.AppendLine(" it dissolved into a smooth falloff.");
sb.AppendLine($"3. **`hist_{primary}_5_continuous_lifted_WRONG.png`** — the bookend: the whole pile shoved");
sb.AppendLine(" up off the shoreline. This is what \"make the island stand up\" would have done.");
sb.AppendLine($"4. `{primary}_staircase/relief.png` vs `{primary}_continuous_default/relief.png` — the A/B.");
sb.AppendLine();
sb.AppendLine("## The variants and their knobs");
sb.AppendLine();
sb.AppendLine("| Variant | Mode | Knobs | Control points |");
sb.AppendLine("|---|---|---|---|");
sb.AppendLine("| `staircase` | staircase | task-01 defaults (ShelfDetail on) | the v5 bands |");
foreach (string label in new[] { "continuous_default", "continuous_hold_higher", "continuous_dramatic_peak" })
{
var c = results[(primary, label)].Continuous;
sb.AppendLine($"| `{label}` | continuous | ceiling {c.LowlandCeilingM:F0} m · feather {c.ClimbFeather:F2} · " +
$"drama {c.SummitDrama:F2} | {c.DescribeControlPoints()} |");
}
sb.AppendLine("| `continuous_lifted_WRONG` | lifted | none — an even ×~1.47 lift of all land | n/a |");
sb.AppendLine();
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
sb.AppendLine();
sb.AppendLine("`ProvisionalEven` — the CostaRica colours re-spaced **evenly** SEA → 420 m, so equal");
sb.AppendLine("colour = equal height and nothing is emphasized. Final palette calibration waits for the");
sb.AppendLine("chosen curve 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 slope profile (e) — warn-and-report, exploration:");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(soft));
sb.AppendLine($"**The bookend, expected to FAIL (d):** {bookend.Detail}");
sb.AppendLine();
sb.AppendLine("## Disposability");
sb.AppendLine();
sb.AppendLine("| Artifact | Keep? |");
sb.AppendLine("|---|---|");
sb.AppendLine("| `relief.png`, `hist_*.png`, `INDEX.md` | **keep** — the judging plates and the finding |");
sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code (it is 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 | land % | grayscale range | time |");
sb.AppendLine("|---|---|---|---|---|---|---|---|");
foreach (string row in rows) sb.AppendLine(row);
sb.AppendLine();
sb.AppendLine($"Setup: MapSize {mapSize}, showpiece {showSize}, seeds {string.Join(", ", seeds)}, " +
$"knots re-measured on the task-01 pool. {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;
}
}
}