using System; using System.Collections.Generic; using System.IO; using System.Text; using Godot; using IslaApocalypse.Core; namespace IslaApocalypse.Tools { /// /// ⭐ 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 ClimbCalibration.MinNormalizedSecant floors /// every segment. → . /// /// ⚠ The two quantile sets are paired by percentile across the SAME cell population, which is /// exact only if the staircase were strictly monotone per column. It is monotone in raw, but the /// per-column bench/plateau modulation (±12 / ±20 m) blurs the pairing by about that much. That /// is well inside the envelope being targeted, and calibrating on the MEASURED output (rather /// than a nominal unmodulated curve) is what makes oracle (g)'s land-above-100 m figure the thing /// actually being aimed at. /// /// ═══ RUNNING IT ═══ /// /// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \ /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/MountainRestoreTool.tscn /// /// ISLA_TASK / ISLA_BATCH / ISLA_MAPSIZE / ISLA_SEEDS / ISLA_SHOWPIECE_SIZE / ISLA_SHOWPIECE /// ISLA_PHASE1_SOURCE (default "chat1/02_pass1_port") /// ISLA_SKIP_RAW /// ISLA_LIFT_BIG probe: the `continuous_bigger` lift (default 1.35) /// ISLA_SHARP probe: the `continuous_sharper_peak` knob (default 2.5) /// public partial class MountainRestoreTool : Node { private static readonly int[] DefaultSeeds = { 1063685222, 777001 }; /// ⚠ Task 01's pool, verbatim — the knots' identity, and with it the staircase control's. private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 }; private const int DefaultMapSize = 2048; private const int DefaultShowpieceSize = 8192; /// Oracle (g)'s PASS/NOTE threshold, percentage points of land above 100 m. Reported either way. private const double MountainTolerancePp = 2.0; public override void _Ready() { try { Run(); } catch (Exception e) { GD.PrintErr("=================================================================="); GD.PrintErr($" REFUSED: {e.Message}"); GD.PrintErr("=================================================================="); GetTree().Quit(2); } } private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2")); int task = EnvInt("ISLA_TASK", 3); string descr = EnvStr("ISLA_BATCH", "mountain_restore"); int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize); bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1"; string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "chat1/02_pass1_port"); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; float liftBig = EnvFloat("ISLA_LIFT_BIG", 1.35f); float sharpKnob = EnvFloat("ISLA_SHARP", 2.5f); string batchRoot = ToolingPaths.BatchRoot(task, descr); DirAccess.MakeDirRecursiveAbsolute(batchRoot); DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot)); var anchors = CurveAnchors.Default; float sea = 0.15f; int primary = seeds[0]; GD.Print("=================================================================="); GD.Print(" MOUNTAIN RESTORE (chat2/03) — the staircase's mountain,"); GD.Print(" de-terraced. Calibrated, not invented."); GD.Print("=================================================================="); GD.Print($"MapSize : {mapSize} showpiece {(showpiece ? showSize.ToString() : "off")}"); GD.Print($"yardstick : {WorldScale.Describe()}"); GD.Print($"seeds : {string.Join(", ", seeds)} (calibration pool: {string.Join(", ", CalibrationSeeds)})"); GD.Print($"batch : {batchRoot}"); GD.Print("=================================================================="); // ═══ 0. KNOTS — task 01's pool, re-measured for bit-identity ═══ GD.Print("\n--- 0. KNOTS ---"); var rawPool = new LandHistogram(sea); var pass1 = new Dictionary(); foreach (int seed in CalibrationSeeds) { var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, seed)); pass1[seed] = p1; rawPool.Accumulate(p1.Height, mapSize); } var knots = new CurveKnots(2, "v2_balanced", rawPool.Quantile(CurveKnots.Percentiles[0]), rawPool.Quantile(CurveKnots.Percentiles[1]), rawPool.Quantile(CurveKnots.Percentiles[2]), rawPool.Quantile(CurveKnots.Percentiles[3]), rawPool.Quantile(CurveKnots.Percentiles[4]), rawPool.Quantile(CurveKnots.Percentiles[5])); GD.Print($" {rawPool}"); GD.Print($" {knots}"); // ═══ 1. CALIBRATE — measure the staircase's above-ceiling elevation distribution ═══ // // ⚠ The ceiling is the DEFAULT 30 m handover, which is exactly (K2, RED_CEIL). So // "above-ceiling" is simply "raw > K2" — no derived float, and the same population the // climb will later be responsible for. GD.Print("\n--- 1. CALIBRATION (staircase above-ceiling distribution) ---"); float ceilingRaw = knots.K2; var rawAbove = new LandHistogram(sea); var outAbove = new LandHistogram(sea); var stairPool = new Dictionary(); 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 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(); var rows = new List(); 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(); var soft = new List(); // ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). // ⚠ A missing dump now THROWS instead of skipping silently. string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32"); hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump", offs[primary].Height, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, mapSize), mapSize, p1Dump)); // ⚑ RETIRED at rivers/01 — a2, the staircase == `01_curve_baseline` control. // Superseded by the continuous grade (→ D-062) — see CurveContinuousTool for the full note. // The dump is NOT deleted (file-safety; it is regenerable and it is the record of what // was judged); its `INDEX.md` is marked superseded. The check is gone so nothing can // pass against a superseded baseline. → XX_Human/output/rivers/01_*.report.md §A4. long bFail = 0; foreach (int seed in seeds) foreach (var (label, _) in variants) if (!ShapingOracle.ClassifyFidelity(pass1[seed], results[(seed, label)]).Passed) bFail++; hard.Add(new ShapingOracle.Check { Id = "b", Name = "classify == raw, all seeds × all variants", Passed = bFail == 0, Detail = bFail == 0 ? $"bit-identical on {seeds.Length} seeds × {variants.Count} variants" : $"{bFail} (seed, variant) pairs drifted", }); bool cOk = results[(primary, "continuous_restored")].Notes .Exists(n => n.Contains("strict-increase sample passed")); hard.Add(new ShapingOracle.Check { Id = "c", Name = "monotone — 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(); 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); } /// /// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This is chat-2 CURVE development: authored /// and judged before the shape family existed, on the family-off distribution the knots are /// percentiles of. The re-baseline flipped the bare defaults family-ON, so the pin is what /// keeps this tool measuring the thing it was written to measure. /// → . /// private static TerrainGenConfig MakeConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label) => new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = label, Curve = true, ShelfDetail = false, Knots = k, Anchors = a, LowlandCeilingM = 30f, }.WithFamilyOff(); // ---- output --------------------------------------------------------- private static string WriteVariant(string batchRoot, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw) { string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}"); DirAccess.MakeDirRecursiveAbsolute(dir); var (gMin, gMax) = GrayscaleRenderer.SavePng(p2.Height, p2.MapSize, Path.Combine(dir, "grayscale.png")); if (!skipRaw) HeightField.Save(p2.Height, p2.MapSize, Path.Combine(dir, "height.f32")); var look = new LookConfig { Name = "hillshade_even", Palette = ReliefPalette.Kind.ProvisionalEven, ZExaggeration = 18f, LightAzimuth = 315f, LightAltitude = 45f, HillshadeStrength = 0.30f, SeaLevel = sea, }; Image map = ReliefRenderer.Render(p2.Height, p2.MapSize, look); LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap, p2.VariantLabel.ToUpperInvariant()) .SavePng(Path.Combine(dir, "relief.png")); var (a100, a220) = ShapingOracle.LandAbove(p2, sea); GD.Print($" {p2.VariantLabel,-30} seed {p2.Seed,-11} h[{p2.HMin,7:F3} .. {p2.HMax,6:F3}] " + $" >100m {a100,5:F2}% >220m {a220,5:F2}% {p2.ElapsedMs,5} ms"); return $"| `{p2.Seed}_{p2.VariantLabel}` | {p2.Seed} | {p2.VariantLabel} | {p2.HMin:F3} | {p2.HMax:F3} | " + $"{a100:F2}% | {a220:F2}% | {gMin:F3}..{gMax:F3} | {p2.ElapsedMs} ms |"; } private static void DrawShaped(Pass2Result p2, CurveAnchors a, int seed, int mapSize, string batchRoot, int order, float sea) { var shaped = new LandHistogram(sea); shaped.Accumulate(p2.Height, mapSize); float top = MathF.Ceiling(shaped.MaxLand * 20f) / 20f; var display = shaped.Rebin((top - shaped.SeaLevel) / 360f); var (a100, a220) = ShapingOracle.LandAbove(p2, sea); var o = new HistogramRenderer.Options { Title = $"{p2.VariantLabel.ToUpperInvariant()} - SEED {seed}", Subtitle = $"LAND ABOVE 100M {a100:F2} PCT - ABOVE 220M {a220:F2} PCT", XAxisLabel = "RAW HEIGHT (POST-CURVE)", XTop = top, Footer = $"{shaped.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}", }; // The two heights the restoration is measured at, on every plate, so the three-way // contrast can be read off the same reference lines. o.Markers.Add(new HistogramRenderer.Marker { Value = a.Sea + WorldScale.RawFromMetres(100f), Label = "100M" }); o.Markers.Add(new HistogramRenderer.Marker { Value = a.Sea + WorldScale.RawFromMetres(220f), Label = "220M" }); o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M", Strong = false }); if (p2.Continuous != null) o.Markers.Add(new HistogramRenderer.Marker { Value = p2.Continuous.CeilingOut, Label = "LOWLAND", Strong = false }); string file = $"hist_{seed}_{order}_{p2.VariantLabel}.png"; HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, file)); GD.Print($" {file}"); } private static void WriteIndex(string batchRoot, int mapSize, int showSize, int[] seeds, int primary, CurveAnchors a, Dictionary<(int, string), Pass2Result> results, ClimbCalibration calRestored, ClimbCalibration calBigger, ClimbCalibration calSharper, double[] pcts, float[] rawQ, float[] outQ, List hard, List soft, List mountain, List 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(); 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; } } }