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; } } }