using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// ⭐ 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 <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 "chat1/02_pass1_port")
/// 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.
///
public partial class CurveContinuousTool : Node
{
/// Two representative variant seeds — the primary Phase-1 seed and the tallest one.
private static readonly int[] DefaultSeeds = { 1063685222, 777001 };
///
/// ⚠ 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.
///
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());
// ⭐ 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", 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", "chat1/02_pass1_port");
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();
foreach (int seed in CalibrationSeeds)
{
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, 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 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();
var rows = new List();
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();
var soft = new List();
ShapingOracle.Check bookend;
{
Pass1Result pp1 = pass1[primary];
// (a1) curve off == Phase 1's own dump. ⭐ KEPT at rivers/01: the family-off pass-1 guard,
// the last link between today's generator and the Phase-1 port. The config is pinned
// family-off so it still means what it says. ⚠ A missing dump now THROWS.
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.
// The staircase curve is SUPERSEDED by the continuous grade (→ D-062). A control that
// reproduces a curve nothing ships is scaffolding, and holding it green cost a
// 4-variant batch run to prove a mode no design doc describes any more.
// 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.
// (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),
}
// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. chat2/02 is CURVE development, measured on
// the family-off distribution the knots are percentiles of; the re-baseline flipped the bare
// defaults family-ON. → TerrainGenConfig.WithFamilyOff().
.WithFamilyOff();
// ---- 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 |";
}
/// The variant folder name — read off the result, never re-derived.
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 hard, List soft,
ShapingOracle.Check bookend, List 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();
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;
}
}
}