using System; using System.Collections.Generic; using System.IO; using System.Text; using Godot; using IslaApocalypse.Core; namespace IslaApocalypse.Tools { /// /// ⭐ THE COASTAL-FRAGMENTATION BATCH (chat2/09, exploration) — at a FIXED stretch, a perimeter-wide /// fragmentation-noise amplitude ladder: light → heavy, 4 levels × 2 seeds, options to pick from. /// /// ═══ TWO MODES ═══ /// /// ISLA_PROBE=1 numbers only at ISLA_CALIB_SIZE: a frequency × amplitude sweep on both seeds /// (island counts and sizes per hemisphere, mainland size) — used to fix the /// frequency and place the amplitude ladder. Written to scratch/frag_probe.md. /// (default) the BATCH: 4 amplitudes × 2 seeds at ISLA_MAPSIZE, lean render per field /// (labeled-regions overlay + relief + .f32), the hemisphere-split count/size table, /// the asymmetric oracle (interior locked, coast free). /// /// Every field: pass 1 + stretch (FIXED) + fragmentation (the axis), region labeling ON, speck revert /// at a LOW threshold (true 1–3-cell noise only), offshore OFF, shelf OFF. The curve is the tagged /// curve, unchanged. /// /// ═══ RUNNING IT ═══ /// /// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \ /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/CoastalFragmentTool.tscn /// /// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR /// ISLA_MAPSIZE / ISLA_CALIB_SIZE (default 4096 / 2048) /// ISLA_SEEDS the two seeds (default 1063685222, 999999937 — task 08's) /// ISLA_STRETCH the fixed stretch (default 2) /// ISLA_FRAG_LEVELS the 4 amplitudes (default: the probe-chosen ladder) /// ISLA_FRAG_FREQ the fixed frequency, periods per map width /// ISLA_SPECK_FRAC the speck-revert threshold, fraction of map area (default 2.5e-7 ≈ 4 cells at 4096) /// ISLA_PROBE=1 · ISLA_PROBE_FREQS · ISLA_PROBE_AMPS the probe sweep /// ISLA_FRAG_BITES=1 bites-only noise ([0,1]) instead of zero-mean ([-1,1]) /// ISLA_SKIP_8K=1 (no 8192 check this batch — the 08 dump at 4096 is the baseline) /// public partial class CoastalFragmentTool : Node { private static readonly int[] DefaultSeeds = { 1063685222, 999999937 }; /// ⚠ Task 01's pool, verbatim — the curve's identity. private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 }; /// ⭐ THE LADDER — set from the probe (chat2/09 report §1): light → heavy, amplitude the only axis. private static readonly float[] DefaultLadder = { 0.06f, 0.15f, 0.30f, 0.50f }; private static readonly float[] ProbeFreqs = { 12f, 20f, 32f }; private static readonly float[] ProbeAmps = { 0.03f, 0.06f, 0.12f, 0.20f, 0.32f, 0.5f }; private const float DefaultStretch = 2f; private const float DefaultSpeckFrac = 2.5e-7f; // ≈ 4 cells at 4096 — true noise only private const int DefaultMapSize = 4096; private const int DefaultCalibSize = 2048; public override void _Ready() { try { Run(); } catch (Exception e) { GD.PrintErr("=================================================================="); GD.PrintErr($" REFUSED: {e.Message}"); GD.PrintErr(e.StackTrace); GD.PrintErr("=================================================================="); GetTree().Quit(2); } } private sealed class HemiStats { public int All, Big; public long Min, Med, Max; public double Mean; public int[] Hist; public long[] Largest = Array.Empty(); } private sealed class Row { public int Level; public float Amp; public int Seed; public HemiStats N, S; public long MainlandCells; public int SpecksReverted; public bool Ok; public ulong Ms; } private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); int task = EnvInt("ISLA_TASK", 9); string descr = EnvStr("ISLA_BATCH", "coastal_fragment"); int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize); int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); float stretch = EnvFloat("ISLA_STRETCH", DefaultStretch); float[] ladder = EnvFloats("ISLA_FRAG_LEVELS", DefaultLadder); float freq = EnvFloat("ISLA_FRAG_FREQ", CoastalFragment.DefaultFreqPerMapWidth); float speckFrac = EnvFloat("ISLA_SPECK_FRAC", DefaultSpeckFrac); bool probe = EnvStr("ISLA_PROBE", "0") == "1"; bool bitesOnly = EnvStr("ISLA_FRAG_BITES", CoastalFragment.DefaultBitesOnly ? "1" : "0") == "1"; float[] probeFreqs = EnvFloats("ISLA_PROBE_FREQS", ProbeFreqs); float[] probeAmps = EnvFloats("ISLA_PROBE_AMPS", ProbeAmps); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string t08Source = EnvStr("ISLA_T08_SOURCE", "08_southern_stretch_explore"); string t08Level = EnvStr("ISLA_T08_LEVEL", "stretch_3"); // the 08 rung with stretch 2 string batchRoot = ToolingPaths.BatchRoot(task, descr); DirAccess.MakeDirRecursiveAbsolute(batchRoot); string scratch = ToolingPaths.BatchScratch(batchRoot); DirAccess.MakeDirRecursiveAbsolute(scratch); var anchors = CurveAnchors.Default; float sea = 0.15f; long big = Cells(RegionPass.ThresholdMidFrac, mapSize); long speckCells = Cells(speckFrac, mapSize); GD.Print("=================================================================="); GD.Print(" COASTAL FRAGMENTATION (chat2/09) — break more pieces off the edges, N + S"); GD.Print("=================================================================="); GD.Print($"MapSize : {mapSize} (plates) calibration / probe at {calibSize}"); GD.Print($"seeds : {string.Join(", ", seeds)}"); GD.Print($"fixed : stretch {stretch:G3} (band {SouthernStretch.DefaultBandStartFrac:F2}/{SouthernStretch.DefaultBandFeatherFrac:F2}, sinker stretched) · frag freq {freq:G3}/map · window {CoastalFragment.DefaultBandCentre:F2} ± {CoastalFragment.DefaultBandHalfWidth:F2} · speck revert < {speckCells} cells ({speckFrac:G2})"); GD.Print($"ladder : FragmentAmp {string.Join(", ", ladder)} noise {(bitesOnly ? "BITES ONLY [0,1]" : "zero-mean [-1,1]")} (\"big\" island = ≥ {big:N0} cells at {mapSize})"); GD.Print($"batch : {batchRoot}{(probe ? " ⚠ ISLA_PROBE — numbers only" : "")}"); GD.Print("=================================================================="); GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, offshore off) ---"); var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors); GD.Print($" {knots}"); float highRaw = knots.K2; // the top of the preserved lowland (30 m output) — "above the toe+red band" TerrainGenConfig Cfg(int size, int seed, string label, float amp, float fq, float st, bool revert) { return new TerrainGenConfig { MapSize = size, Seed = seed, VariantLabel = label, Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous, Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f, CoastShelf = false, Offshore = new OffshoreSettings(), RegionLabeling = true, SpeckRevert = revert, MinLandComponentFrac = speckFrac, SouthStretch = st, FragmentAmp = amp, FragmentFreqPerMapWidth = fq, FragmentBitesOnly = bitesOnly, }; } // ═══ PROBE ═══ if (probe) { GD.Print($"\n--- PROBE at {calibSize}: frequency × amplitude, stretch {stretch:G3} ---"); long bigC = Cells(RegionPass.ThresholdMidFrac, calibSize); var sb = new StringBuilder(); sb.AppendLine($"# chat2/09 probe — fragmentation frequency × amplitude at {calibSize}, stretch {stretch:G3}, noise {(bitesOnly ? "bites only" : "zero-mean")}"); sb.AppendLine(); sb.AppendLine($"\"big\" = ≥ {bigC} cells at {calibSize}. Speck revert < {Cells(speckFrac, calibSize)} cells. Offshore off."); sb.AppendLine(); sb.AppendLine("| seed | freq | amp | N islands all / big | N size med / max | S islands all / big | S size med / max | mainland cells | mainland Δ vs amp 0 |"); sb.AppendLine("|---|---|---|---|---|---|---|---|---|"); foreach (int seed in seeds) { var base0 = Topography.Generate(Cfg(calibSize, seed, "amp0", 0f, freq, stretch, true)); long main0 = base0.Regions.Mainland.SizeCells; var (n0, s0) = Stats(base0.Regions, bigC); sb.AppendLine($"| `{seed}` | — | 0 | {n0.All} / {n0.Big} | {n0.Med} / {n0.Max} | {s0.All} / {s0.Big} | {s0.Med} / {s0.Max} | {main0:N0} | 0 |"); GD.Print($" seed {seed} amp 0: N {n0.All}/{n0.Big} med {n0.Med} max {n0.Max} S {s0.All}/{s0.Big} med {s0.Med} max {s0.Max} mainland {main0:N0}"); foreach (float fq in probeFreqs) foreach (float amp in probeAmps) { var p = Topography.Generate(Cfg(calibSize, seed, "probe", amp, fq, stretch, true)); var (n, s) = Stats(p.Regions, bigC); long main = p.Regions.Mainland.SizeCells; sb.AppendLine($"| `{seed}` | {fq:G3} | {amp:G3} | {n.All} / {n.Big} | {n.Med} / {n.Max} | {s.All} / {s.Big} | {s.Med} / {s.Max} | {main:N0} | {main - main0:+#,0;-#,0;0} |"); GD.Print($" seed {seed} freq {fq,4:G3} amp {amp,5:G3}: N {n.All,3}/{n.Big,3} med {n.Med,6} max {n.Max,7} S {s.All,3}/{s.Big,3} med {s.Med,6} max {s.Max,7} mainland {main:N0} ({main - main0:+#,0;-#,0;0})"); } } WriteText(Path.Combine(scratch, bitesOnly ? "frag_probe_bites_only.md" : "frag_probe.md"), sb.ToString()); GD.Print($"\n probe written: {Path.Combine(scratch, "frag_probe.md")}"); GetTree().Quit(0); return; } // ═══ 1. REGRESSIONS ═══ GD.Print($"\n--- 1. REGRESSIONS ---"); var hard = new List(); { var offCfg = Cfg(calibSize, seeds[0], "off", 0f, freq, 0f, false); Pass1Result p1 = Topography.Generate(offCfg); var curveOff = offCfg.Clone(); curveOff.Curve = false; string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{seeds[0]}_full", "height.f32"); hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)", Shaping.Shape(p1, curveOff).Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump)); // ⭐ a8 — frag OFF at the fixed stretch, revert OFF == the task-08 stretch-2 field (its dump at the plate size). foreach (int seed in seeds) { string t08Dump = Path.Combine(ToolingPaths.BatchesRoot, t08Source, $"{seed}_{t08Level}", "height.f32"); if (File.Exists(t08Dump) && mapSize == 4096) { var c8 = Cfg(mapSize, seed, "t08", 0f, freq, stretch, false); Pass2Result q8 = Shaping.Shape(Topography.Generate(c8), c8); hard.Add(ShapingOracle.DumpRegression("a8", $"frag OFF, stretch {stretch:G3}, revert OFF == task-08 {t08Level} dump (the baseline) [{seed}]", q8.Height, HeightField.Load(t08Dump, mapSize), mapSize, t08Dump)); } else GD.Print($" a8 [{seed}]: ⚠ skipped — {(mapSize != 4096 ? "map size is not 4096" : $"no 08 dump at {t08Dump}")}"); } foreach (var c in hard) GD.Print(" " + c); } // ═══ 2. THE LADDER — 4 amplitudes × 2 seeds ═══ GD.Print($"\n--- 2. THE LADDER at {mapSize} ---"); var rows = new List(); var baseline = new Dictionary(); var perField = new List(); foreach (int seed in seeds) { var c0 = Cfg(mapSize, seed, "frag_0", 0f, freq, stretch, true); Pass1Result p0 = Topography.Generate(c0); baseline[seed] = MakeRow(0, 0f, seed, p0, big, true, p0.ElapsedMs); var b = baseline[seed]; GD.Print($" seed {seed} baseline (amp 0, stretch {stretch:G3}): N {b.N.All}/{b.N.Big} S {b.S.All}/{b.S.Big} mainland {b.MainlandCells:N0}"); for (int li = 0; li < ladder.Length; li++) { float amp = ladder[li]; string label = $"frag_{li + 1}"; var cfg = Cfg(mapSize, seed, label, amp, freq, stretch, true); Pass1Result p1 = Topography.Generate(cfg); Pass2Result p2 = Shaping.Shape(p1, cfg); var checks = new List { ShapingOracle.InteriorLocked(p0, p1, cfg.FragmentBandCentre, cfg.FragmentBandHalfWidth), ShapingOracle.HighGroundReport(p0, p1, highRaw, $"K2 ({highRaw:F3} raw, the top of the preserved lowland)"), ShapingOracle.CentreIsLand(p1), ShapingOracle.TagCoastlineConsistent(p2, sea), ShapingOracle.ClassifyFidelity(p1, p2), }; foreach (var c in checks) { c.Name += $" [{label} = {amp:G3}, {seed}]"; perField.Add(c); } bool ok = checks.TrueForAll(c => c.Passed); var row = MakeRow(li + 1, amp, seed, p1, big, ok, p1.ElapsedMs); rows.Add(row); WriteField(batchRoot, p1, p2, sea, anchors, skipRaw, amp); GD.Print($" {label,-7} amp {amp,5:G3} seed {seed,-11} N {row.N.All,3}/{row.N.Big,3} med {row.N.Med,6} max {row.N.Max,7} S {row.S.All,3}/{row.S.Big,3} med {row.S.Med,6} max {row.S.Max,7} mainland {row.MainlandCells:N0} ({row.MainlandCells - b.MainlandCells:+#,0;-#,0;0}) specks {row.SpecksReverted} {(ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms"); } } // determinism { float amp = ladder[1]; var a = Topography.Generate(Cfg(mapSize, seeds[0], "det", amp, freq, stretch, true)); var bb = Topography.Generate(Cfg(mapSize, seeds[0], "det", amp, freq, stretch, true)); var det = ShapingOracle.LabelsDeterministic(a, bb); det.Name += $" [amp {amp:G3}, {seeds[0]}]"; var bits = ShapingOracle.NorthLocked("o2", $"two generations bit-identical everywhere [amp {amp:G3}, {seeds[0]}]", a.Height, bb.Height, mapSize, mapSize); perField.Add(det); perField.Add(bits); GD.Print(" " + det); GD.Print(" " + bits); } bool allOk = hard.TrueForAll(c => c.Passed) && perField.TrueForAll(c => c.Passed); GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}"); foreach (var c in perField) if (!c.Passed) GD.PrintErr(" " + c); WriteTable(batchRoot, mapSize, ladder, seeds, rows, baseline, big, stretch, freq, speckCells); WriteIndex(batchRoot, mapSize, calibSize, ladder, seeds, rows, baseline, big, stretch, freq, speckCells, hard, perField, allOk); GD.Print("\n=================================================================="); GD.Print($" DONE — {batchRoot}"); GD.Print($" ORACLE {(allOk ? "HARD CHECKS ALL PASS" : "*** FAILURES — see the table ***")}"); GD.Print("=================================================================="); GetTree().Quit(allOk ? 0 : 3); } // ---- the instrument -------------------------------------------------- private static long Cells(float frac, int size) => Math.Max(1L, (long)Math.Round(frac * (double)size * size)); private static (HemiStats north, HemiStats south) Stats(RegionLabels l, long big) { var n = new List(); var s = new List(); foreach (var r in l.Regions) { if (r.IsMainland) continue; if (r.Hemisphere == RegionLabeling.HemiSouth) s.Add(r.SizeCells); else n.Add(r.SizeCells); } return (Make(n, big), Make(s, big)); } private static HemiStats Make(List sizes, long big) { sizes.Sort(); var h = new HemiStats { All = sizes.Count, Hist = new int[RegionLabeling.HistogramEdges.Length + 1] }; if (sizes.Count == 0) return h; double sum = 0; foreach (long v in sizes) { h.Hist[RegionLabeling.HistogramBin(v)]++; if (v >= big) h.Big++; sum += v; } h.Min = sizes[0]; h.Med = sizes[sizes.Count / 2]; h.Max = sizes[^1]; h.Mean = sum / sizes.Count; int k = Math.Min(3, sizes.Count); h.Largest = new long[k]; for (int i = 0; i < k; i++) h.Largest[i] = sizes[sizes.Count - 1 - i]; return h; } private static Row MakeRow(int level, float amp, int seed, Pass1Result p1, long big, bool ok, ulong ms) { var (n, s) = Stats(p1.Regions, big); return new Row { Level = level, Amp = amp, Seed = seed, N = n, S = s, MainlandCells = p1.Regions.Mainland.SizeCells, SpecksReverted = p1.RegionLedger?.RevertedComponents ?? 0, Ok = ok, Ms = ms, }; } // ---- the curve -------------------------------------------------------- private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors) { var rawPool = new LandHistogram(sea); var pass1 = new Dictionary(); foreach (int s in CalibrationSeeds) { var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); pass1[s] = p1; rawPool.Accumulate(p1.Height, calibSize); } 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])); float ceilingRaw = knots.K2; var rawAbove = new LandHistogram(sea); var outAbove = new LandHistogram(sea); foreach (int s in CalibrationSeeds) { var scfg = new TerrainGenConfig { MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true, CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase", }; Pass2Result st = Shaping.Shape(pass1[s], scfg); rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw); outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw); } var pcts = ClimbCalibration.DefaultPercentiles; var rawQ = new float[pcts.Length]; var outQ = new float[pcts.Length]; for (int i = 0; i < pcts.Length; i++) { rawQ[i] = rawAbove.Quantile(pcts[i]); outQ[i] = outAbove.Quantile(pcts[i]); } var cal = ClimbCalibration.FromPercentiles(pcts, rawQ, outQ, ceilingRaw, HeightCurve.EffectiveSpikeMax(pass1[CalibrationSeeds[0]].HMaxSeed, knots, anchors), anchors.RedCeil, anchors.PeakCap, mountainLift: 1.0f, peakSharpness: 1.0f); return (knots, cal); } // ---- output ----------------------------------------------------------- private static void WriteField(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw, float amp) { string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}"); DirAccess.MakeDirRecursiveAbsolute(dir); 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()} (AMP {amp:G3}) {p2.Seed}") .SavePng(Path.Combine(dir, "relief.png")); var led = p1.RegionLedger; RegionOverlayRenderer.SavePng(p1.Regions, led != null && led.RevertOn ? p1.RegionsPre : null, p1.MapSize, led?.RevertedComponents ?? 0, led?.ThresholdCells ?? 0, Path.Combine(dir, "regions.png")); } private static string HistRow(int[] h) { var sb = new StringBuilder(); for (int i = 0; i < h.Length; i++) { if (i > 0) sb.Append(" · "); sb.Append(h[i]); } return sb.ToString(); } private static string Largest(long[] l) => l.Length == 0 ? "—" : string.Join(" / ", Array.ConvertAll(l, v => v.ToString("N0"))); private static string TableMarkdown(float[] ladder, int[] seeds, List rows, Dictionary baseline, long big) { var sb = new StringBuilder(); var histHead = new StringBuilder(); for (int i = 0; i <= RegionLabeling.HistogramEdges.Length; i++) { if (i > 0) histHead.Append(" · "); histHead.Append(RegionLabeling.HistogramLabel(i)); } sb.AppendLine($"| Level | amp | Seed | **N islands all / ≥ {big:N0}** | N size med / mean / max | N largest three | N histogram ({histHead}) | **S islands all / ≥ {big:N0}** | S size med / mean / max | S largest three | S histogram | mainland cells (Δ vs amp 0) | specks reverted | oracle |"); sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|"); foreach (int seed in seeds) { var b = baseline[seed]; var all = new List { b }; all.AddRange(rows.FindAll(r => r.Seed == seed)); foreach (var r in all) sb.AppendLine($"| {(r.Level == 0 ? "*baseline*" : $"`frag_{r.Level}`")} | {r.Amp:G3} | `{seed}` | **{r.N.All} / {r.N.Big}** | {r.N.Med} / {r.N.Mean:F0} / {r.N.Max} | {Largest(r.N.Largest)} | {HistRow(r.N.Hist)} | " + $"**{r.S.All} / {r.S.Big}** | {r.S.Med} / {r.S.Mean:F0} / {r.S.Max} | {Largest(r.S.Largest)} | {HistRow(r.S.Hist)} | {r.MainlandCells:N0} ({r.MainlandCells - b.MainlandCells:+#,0;-#,0;0}) | {r.SpecksReverted} | {(r.Level == 0 ? "—" : r.Ok ? "pass" : "**FAIL**")} |"); } return sb.ToString(); } private static void WriteTable(string batchRoot, int mapSize, float[] ladder, int[] seeds, List rows, Dictionary baseline, long big, float stretch, float freq, long speckCells) { var sb = new StringBuilder(); sb.AppendLine($"# The hemisphere-split count/size table — {ladder.Length} amplitudes × {seeds.Length} seeds at {mapSize}"); sb.AppendLine(); sb.AppendLine($"Fixed: stretch {stretch:G3}, fragmentation frequency {freq:G3}/map, window {CoastalFragment.DefaultBandCentre:F2} ± {CoastalFragment.DefaultBandHalfWidth:F2}, speck revert < {speckCells} cells. Offshore / shelf OFF."); sb.AppendLine("Islands = non-mainland 8-connected land components of the classify field; hemisphere by centroid. BOTH hemispheres are signals now."); sb.AppendLine(); sb.Append(TableMarkdown(ladder, seeds, rows, baseline, big)); WriteText(Path.Combine(batchRoot, "count_size_table.md"), sb.ToString()); var csv = new StringBuilder(); csv.AppendLine("level,amp,seed,n_all,n_big,n_med,n_mean,n_max,n_largest,n_hist,s_all,s_big,s_med,s_mean,s_max,s_largest,s_hist,mainland_cells,specks_reverted,oracle,ms"); var ic = System.Globalization.CultureInfo.InvariantCulture; foreach (int seed in seeds) { var all = new List { baseline[seed] }; all.AddRange(rows.FindAll(r => r.Seed == seed)); foreach (var r in all) csv.AppendLine(string.Join(",", r.Level, r.Amp.ToString("G5", ic), r.Seed, r.N.All, r.N.Big, r.N.Med, r.N.Mean.ToString("F1", ic), r.N.Max, "\"" + Largest(r.N.Largest) + "\"", "\"" + HistRow(r.N.Hist) + "\"", r.S.All, r.S.Big, r.S.Med, r.S.Mean.ToString("F1", ic), r.S.Max, "\"" + Largest(r.S.Largest) + "\"", "\"" + HistRow(r.S.Hist) + "\"", r.MainlandCells, r.SpecksReverted, r.Ok ? "pass" : "FAIL", r.Ms)); } WriteText(Path.Combine(batchRoot, "count_size_table.csv"), csv.ToString()); } private static void WriteIndex(string batchRoot, int mapSize, int calibSize, float[] ladder, int[] seeds, List rows, Dictionary baseline, long big, float stretch, float freq, long speckCells, List hard, List perField, bool allOk) { int first = seeds.Length > 1 ? seeds[1] : seeds[0]; var sb = new StringBuilder(); sb.AppendLine("# Batch 09 — coastal fragmentation: break more pieces off the edges, N + S"); sb.AppendLine(); sb.AppendLine("**Options, not a setting.** At a FIXED stretch, a perimeter-wide band-limited fragmentation noise on the pre-power"); sb.AppendLine("falloff — only inside the coastal window, zero-mean — is swept light → heavy. Thin necks of barely-land flip first"); sb.AppendLine("(self-targeting: nothing detected, nothing stamped); the interior is bit-identical by construction (asserted). The"); sb.AppendLine("region layer is the instrument; both hemispheres are fragmentation signals now."); sb.AppendLine(); sb.AppendLine("## ⭐ Open this first"); sb.AppendLine(); sb.AppendLine($"1. **`{first}_frag_2/regions.png`** — a mid amplitude on the seed whose natural southern islands read fragmentation best; grey mainland, each island its own colour, dark red = a reverted speck (< {speckCells} cells)."); sb.AppendLine($"2. Walk the ladder on that seed: `{first}_frag_1/` … `_frag_{ladder.Length}/` (`regions.png` beside `relief.png`); then the same on `{seeds[0]}`."); sb.AppendLine("3. Then the table: N and S count + size side by side, down the rows as amplitude climbs — look for counts rising while the largest pieces stay healthy."); sb.AppendLine(); sb.AppendLine("## The fixed frame and the axis"); sb.AppendLine(); sb.AppendLine($"- **Fixed:** stretch `{stretch:G3}` (task 08's `stretch_3` rung; band {SouthernStretch.DefaultBandStartFrac:F2} / feather {SouthernStretch.DefaultBandFeatherFrac:F2}, sinker stretched — untouched this round) · fragmentation frequency `{freq:G3}` periods/map (the secondary dial, fixed) · window centre {CoastalFragment.DefaultBandCentre:F2} ± {CoastalFragment.DefaultBandHalfWidth:F2} (pre-power falloff) · speck revert < {speckCells} cells (true noise only) · offshore OFF · shelf OFF."); sb.AppendLine($"- **The axis — `FragmentAmp`:** {string.Join(" · ", Array.ConvertAll(ladder, v => v.ToString("G3")))} (levels 1–{ladder.Length}); baseline 0 = the task-08 stretch field, measured for Δ."); sb.AppendLine($"- \"big\" island = ≥ {big:N0} cells at {mapSize} (the 07 `threshold_mid`)."); sb.AppendLine(); sb.AppendLine($"## ⭐ The hemisphere-split count/size table — {ladder.Length} amplitudes × {seeds.Length} seeds at {mapSize}"); sb.AppendLine(); sb.Append(TableMarkdown(ladder, seeds, rows, baseline, big)); sb.AppendLine(); sb.AppendLine("Also as plain data: `count_size_table.md` / `.csv`. The probe that fixed the frequency and placed the ladder: `scratch/frag_probe.md`."); sb.AppendLine(); sb.AppendLine("## ⚠ The palette is PROVISIONAL"); sb.AppendLine(); sb.AppendLine("`ProvisionalEven`, flagged. The individually-coloured scheme is only the `regions.png` overlay."); sb.AppendLine(); sb.AppendLine("## The oracle (asymmetric: interior locked, coast free)"); sb.AppendLine(); sb.AppendLine("Regressions (the curve untouched; frag OFF at the fixed stretch bit-identical to the task-08 field):"); sb.AppendLine(); sb.AppendLine(ShapingOracle.ToMarkdownTable(hard)); sb.AppendLine("Per field (interior locked r · high-ground report s (informational) · centre-is-land m · tag/coastline k · classify b · determinism o / o2):"); sb.AppendLine(); sb.AppendLine(ShapingOracle.ToMarkdownTable(perField)); sb.AppendLine($"**{(allOk ? "ALL HARD CHECKS PASS" : "⚠⚠ FAILURES — do not judge this batch")}**"); sb.AppendLine(); sb.AppendLine("## Disposability"); sb.AppendLine(); sb.AppendLine("| Artifact | Keep? |"); sb.AppendLine("|---|---|"); sb.AppendLine("| `regions.png`, `relief.png`, `INDEX.md`, `count_size_table.md` / `.csv`, `scratch/frag_probe.md` | **keep** |"); sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code — large, clear freely |"); sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |"); sb.AppendLine(); sb.AppendLine($"Plates at {mapSize}, curve calibrated at {calibSize} with offshore off. {WorldScale.Describe()}."); WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString()); } private static void WriteText(string path, string text) { using var f = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Write); if (f == null) { GD.PrintErr($"could not write {path}"); return; } f.StoreString(text); } // ---- 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 float[] EnvFloats(string k, float[] fallback) { string v = EnvStr(k, null); if (v == null) return fallback; var outp = new List(); foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries)) if (float.TryParse(part.Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float f)) outp.Add(f); return outp.Count > 0 ? outp.ToArray() : 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; } } }