islaApocalypse-v2/Tools/Scripts/FragGalleryTool.cs
beezm a59e52fcbc chat2/10: frag_4 seed gallery — render-only, the 09 setting frozen across 2 anchors + 6 fresh seeds at 8192
FragGalleryTool pins every frag_4 value explicitly (amp 0.5, freq 12, window 0.66 +- 0.18,
zero-mean, stretch 2 with band 0.70/0.05 and the sinker stretched, speck revert 2.5e-7, offshore
and shelf off, labeling on) and proves it is the 09 setting: at 4096 the frozen field is
bit-identical to the 09 frag_4 dump on both anchors (a9, 16.8 M cells each), the frag-off field
to the 08 stretch_3 dump (a8), the interior locked against it (r), ids deterministic (o), the
curve untouched (a1). No generation code changed.

Eight seeds at 8192 - the anchors 1063685222 and 999999937 plus 20260822, 31415926, 27182818,
16180339, 14142135, 17320508 (constants chosen before any render) - each with grayscale, .f32,
relief and the labeled-regions overlay, and the hemisphere-split count/size table with a stated
numeric read rule (too solid < 12 big islands across both hemispheres; shredded if big < 25 % of
all; else good spread). Seven read good spread; 14142135 trips the ratio rule (17 big of 69) and
is the most solid mainland of the eight (99.3 % of land). Graduation held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
2026-08-22 10:24:56 -04:00

443 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐ THE FRAG-4 SEED GALLERY (chat2/10) — RENDER-ONLY. Does the chat2/09 `frag_4` look generalize?
/// Every setting is FROZEN at frag_4 (pinned explicitly here, not left to a default), 2 anchor seeds +
/// 6 fresh seeds, at showpiece size, each with grayscale + .f32 + relief + the labeled-regions overlay,
/// and the hemisphere-split count/size table as the instrument. No knob, no ladder, no logic change.
///
/// ═══ RUNNING IT ═══
///
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/FragGalleryTool.tscn
///
/// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR
/// ISLA_MAPSIZE gallery size (default 8192)
/// ISLA_CALIB_SIZE curve calibration size (default 2048)
/// ISLA_SEEDS the gallery seeds (default: the 2 anchors + 6 fresh below)
/// ISLA_SKIP_ANCHOR_CHECK=1 skip the 4096 bit-identity check against the 09 frag_4 dumps
/// </summary>
public partial class FragGalleryTool : Node
{
/// <summary>The two seeds frag_4 was judged on (chat2/09).</summary>
private static readonly int[] AnchorSeeds = { 1063685222, 999999937 };
/// <summary>Six fresh seeds, chosen BEFORE any render — constants, not picks: the date and five famous digit strings.</summary>
private static readonly int[] FreshSeeds = { 20260822, 31415926, 27182818, 16180339, 14142135, 17320508 };
/// <summary>⚠ Task 01's pool, verbatim — the curve's identity.</summary>
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
// ═══ THE FROZEN frag_4 SETTING — every value pinned explicitly (chat2/09 batch, level 4) ═══
private const float FrozenFragmentAmp = 0.5f;
private const float FrozenFragmentFreq = 12f;
private const float FrozenBandCentre = 0.66f;
private const float FrozenBandHalfWidth = 0.18f;
private const bool FrozenBitesOnly = false;
private const float FrozenStretch = 2f;
private const float FrozenBandStart = 0.70f;
private const float FrozenBandFeather = 0.05f;
private const bool FrozenStretchSinker = true;
private const float FrozenSpeckFrac = 2.5e-7f; // 09's low speck revert (≈ 4 cells at 4096, ≈ 17 at 8192)
private const int DefaultMapSize = 8192;
private const int DefaultCalibSize = 2048;
private const int AnchorCheckSize = 4096; // the 09 batch's size
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<long>();
}
private sealed class Row
{
public int Seed; public bool Anchor;
public HemiStats N, S; public long MainlandCells; public double MainlandFrac; public int SpecksReverted;
public string Read; public bool Ok; public ulong Ms;
}
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
int task = EnvInt("ISLA_TASK", 10);
string descr = EnvStr("ISLA_BATCH", "frag4_seed_gallery");
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
int[] seedsEnv = EnvSeeds("ISLA_SEEDS", null);
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool skipAnchor = EnvStr("ISLA_SKIP_ANCHOR_CHECK", "0") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t08Source = EnvStr("ISLA_T08_SOURCE", "08_southern_stretch_explore");
string t09Source = EnvStr("ISLA_T09_SOURCE", "09_coastal_fragment");
var seeds = new List<int>(AnchorSeeds); if (seedsEnv == null) seeds.AddRange(FreshSeeds); else { seeds.Clear(); seeds.AddRange(seedsEnv); }
var anchorSet = new HashSet<int>(AnchorSeeds);
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
var anchors = CurveAnchors.Default;
float sea = 0.15f;
long big = Cells(RegionPass.ThresholdMidFrac, mapSize);
long speckCells = Cells(FrozenSpeckFrac, mapSize);
GD.Print("==================================================================");
GD.Print(" FRAG-4 SEED GALLERY (chat2/10) — render-only: does frag_4 generalize?");
GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize} (offshore off)");
GD.Print($"seeds : anchors {string.Join(", ", AnchorSeeds)} · fresh {string.Join(", ", seeds.FindAll(s => !anchorSet.Contains(s)))}");
GD.Print($"FROZEN : FragmentAmp {FrozenFragmentAmp} · freq {FrozenFragmentFreq}/map · window {FrozenBandCentre} ± {FrozenBandHalfWidth} · bitesOnly {FrozenBitesOnly} · " +
$"stretch {FrozenStretch} (band {FrozenBandStart}/{FrozenBandFeather}, sinker stretched {FrozenStretchSinker}) · speck revert < {speckCells} cells ({FrozenSpeckFrac:G2}) · offshore OFF · shelf OFF · labeling ON");
GD.Print($"\"big\" : ≥ {big:N0} cells at {mapSize} (the 07 mid threshold)");
GD.Print($"batch : {batchRoot}");
GD.Print("==================================================================");
GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, offshore off) ---");
var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors);
GD.Print($" {knots}");
TerrainGenConfig Frozen(int size, int seed, string label, bool frag = true, bool revert = true, bool stretch = true) => 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 = FrozenSpeckFrac,
SouthStretch = stretch ? FrozenStretch : 0f, SouthBandStartFrac = FrozenBandStart, SouthBandFeatherFrac = FrozenBandFeather, StretchSinker = FrozenStretchSinker,
FragmentAmp = frag ? FrozenFragmentAmp : 0f, FragmentFreqPerMapWidth = FrozenFragmentFreq,
FragmentBandCentre = FrozenBandCentre, FragmentBandHalfWidth = FrozenBandHalfWidth, FragmentBitesOnly = FrozenBitesOnly,
};
// ═══ 1. THE ORACLE — no code change, same setting ═══
GD.Print($"\n--- 1. ORACLE: the setting is the 09 frag_4 setting, and nothing upstream moved ---");
var hard = new List<ShapingOracle.Check>();
{
var offCfg = Frozen(calibSize, AnchorSeeds[0], "off", frag: false, revert: false, stretch: false);
Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false;
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{AnchorSeeds[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));
if (!skipAnchor)
{
foreach (int seed in AnchorSeeds)
{
// ⭐ a9 — the frozen setting at the 09 batch's size reproduces the 09 frag_4 field bit for bit.
string t09Dump = Path.Combine(ToolingPaths.BatchesRoot, t09Source, $"{seed}_frag_4", "height.f32");
var c9 = Frozen(AnchorCheckSize, seed, "frag_4");
Pass1Result q9p = Topography.Generate(c9);
Pass2Result q9 = Shaping.Shape(q9p, c9);
hard.Add(ShapingOracle.DumpRegression("a9", $"frozen frag_4 at {AnchorCheckSize} == task-09 frag_4 dump [{seed}] (no code change, same setting)", q9.Height, HeightField.Load(t09Dump, AnchorCheckSize), AnchorCheckSize, t09Dump));
// a8 — the stretch-2, frag-off baseline still equals the 08 field, and the interior is still locked against it.
string t08Dump = Path.Combine(ToolingPaths.BatchesRoot, t08Source, $"{seed}_stretch_3", "height.f32");
var c8 = Frozen(AnchorCheckSize, seed, "t08", frag: false, revert: false);
Pass1Result q8p = Topography.Generate(c8);
hard.Add(ShapingOracle.DumpRegression("a8", $"frag OFF, stretch 2 at {AnchorCheckSize} == task-08 stretch_3 dump [{seed}]", Shaping.Shape(q8p, c8).Height, HeightField.Load(t08Dump, AnchorCheckSize), AnchorCheckSize, t08Dump));
var r = ShapingOracle.InteriorLocked(q8p, q9p, FrozenBandCentre, FrozenBandHalfWidth); r.Name += $" [{seed}, {AnchorCheckSize}]"; hard.Add(r);
}
// determinism at the check size
var a = Topography.Generate(Frozen(AnchorCheckSize, AnchorSeeds[0], "det"));
var b = Topography.Generate(Frozen(AnchorCheckSize, AnchorSeeds[0], "det"));
var det = ShapingOracle.LabelsDeterministic(a, b); det.Name += $" [{AnchorSeeds[0]}, {AnchorCheckSize}]"; hard.Add(det);
}
foreach (var c in hard) GD.Print(" " + c);
}
// ═══ 2. THE GALLERY ═══
GD.Print($"\n--- 2. THE GALLERY at {mapSize} ---");
var rows = new List<Row>();
var perSeed = new List<ShapingOracle.Check>();
foreach (int seed in seeds)
{
var cfg = Frozen(mapSize, seed, "frag_4");
Pass1Result p1 = Topography.Generate(cfg);
Pass2Result p2 = Shaping.Shape(p1, cfg);
var checks = new List<ShapingOracle.Check>
{
ShapingOracle.CentreIsLand(p1),
ShapingOracle.TagCoastlineConsistent(p2, sea),
ShapingOracle.ClassifyFidelity(p1, p2),
};
foreach (var c in checks) { c.Name += $" [{seed}]"; perSeed.Add(c); }
bool ok = checks.TrueForAll(c => c.Passed);
var (n, s) = Stats(p1.Regions, big);
long landCells = p1.Regions.LandCells;
var row = new Row
{
Seed = seed, Anchor = anchorSet.Contains(seed), N = n, S = s,
MainlandCells = p1.Regions.Mainland.SizeCells, MainlandFrac = landCells == 0 ? 0 : p1.Regions.Mainland.SizeCells / (double)landCells,
SpecksReverted = p1.RegionLedger?.RevertedComponents ?? 0, Ok = ok, Ms = p1.ElapsedMs,
};
row.Read = AutoRead(row);
rows.Add(row);
WriteSeed(batchRoot, p1, p2, sea, anchors, skipRaw);
GD.Print($" seed {seed,-11}{(row.Anchor ? " " : " ")} N {n.All,3}/{n.Big,3} med {n.Med,6} largest {Largest(n.Largest)} S {s.All,3}/{s.Big,3} med {s.Med,6} largest {Largest(s.Largest)} mainland {row.MainlandCells:N0} ({row.MainlandFrac:P1} of land) → {row.Read} {(ok ? "ok" : " CHECK FAILED")} {p1.ElapsedMs} ms");
}
bool allOk = hard.TrueForAll(c => c.Passed) && perSeed.TrueForAll(c => c.Passed);
GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}");
foreach (var c in perSeed) if (!c.Passed) GD.PrintErr(" " + c);
WriteTable(batchRoot, mapSize, rows, big, speckCells);
WriteIndex(batchRoot, mapSize, calibSize, rows, big, speckCells, hard, perSeed, 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<long>(); var s = new List<long>();
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<long> 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;
}
/// <summary>
/// The one-word NUMERIC read — a stated rule, not a judgement: "too solid" if fewer than 12 big
/// (≥ the 07 mid threshold) islands detached across both hemispheres (the 09 anchors had 2327);
/// "shredded" if fewer than a quarter of the islands are big (all specks, no bigs); else "good
/// spread". The eye's read is in the report.
/// </summary>
private static string AutoRead(Row r)
{
int all = r.N.All + r.S.All, bigs = r.N.Big + r.S.Big;
if (bigs < 12) return "too solid";
if (all > 0 && bigs < all * 0.25) return "shredded";
return "good spread";
}
// ---- the curve --------------------------------------------------------
private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
{
var rawPool = new LandHistogram(sea);
var pass1 = new Dictionary<int, Pass1Result>();
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 WriteSeed(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw)
{
string dir = Path.Combine(batchRoot, $"{p2.Seed}");
DirAccess.MakeDirRecursiveAbsolute(dir);
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, $"FRAG_4 {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(List<Row> rows, 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($"| Seed | read | **N islands all / ≥ {big:N0}** | N median / largest three | N histogram ({histHead}) | **S islands all / ≥ {big:N0}** | S median / largest three | S histogram | mainland cells (% of land) | specks reverted | oracle |");
sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|");
foreach (var r in rows)
sb.AppendLine($"| `{r.Seed}`{(r.Anchor ? " anchor" : "")} | **{r.Read}** | **{r.N.All} / {r.N.Big}** | {r.N.Med} / {Largest(r.N.Largest)} | {HistRow(r.N.Hist)} | **{r.S.All} / {r.S.Big}** | {r.S.Med} / {Largest(r.S.Largest)} | {HistRow(r.S.Hist)} | {r.MainlandCells:N0} ({r.MainlandFrac:P1}) | {r.SpecksReverted} | {(r.Ok ? "pass" : "**FAIL**")} |");
return sb.ToString();
}
private static void WriteTable(string batchRoot, int mapSize, List<Row> rows, long big, long speckCells)
{
var sb = new StringBuilder();
sb.AppendLine($"# The count/size table — frag_4 frozen, {rows.Count} seeds at {mapSize}");
sb.AppendLine();
sb.AppendLine($"Islands = non-mainland 8-connected land components of the classify field; hemisphere by centroid; \"big\" = ≥ {big:N0} cells; speck revert < {speckCells} cells.");
sb.AppendLine("Read rule (numeric, stated): **too solid** if < 12 big islands across both hemispheres · **shredded** if big < 25 % of all · else **good spread**. The eye's read is in the report.");
sb.AppendLine();
sb.Append(TableMarkdown(rows, big));
WriteText(Path.Combine(batchRoot, "count_size_table.md"), sb.ToString());
var csv = new StringBuilder();
csv.AppendLine("seed,anchor,read,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,mainland_frac,specks_reverted,oracle,ms");
var ic = System.Globalization.CultureInfo.InvariantCulture;
foreach (var r in rows)
csv.AppendLine(string.Join(",", r.Seed, r.Anchor ? 1 : 0, r.Read,
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.MainlandFrac.ToString("F4", ic), 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, List<Row> rows, long big, long speckCells,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perSeed, bool allOk)
{
var sb = new StringBuilder();
sb.AppendLine($"# Batch 10 — frag_4 seed gallery: does the look generalize? (render-only, {mapSize})");
sb.AppendLine();
sb.AppendLine("**A contact sheet, not a tune.** Every plate is the SAME setting — chat2/09's `frag_4`, frozen — across the two");
sb.AppendLine("seeds it was judged on (⭐ anchors) and six fresh seeds chosen before any render. The question: does a coherent");
sb.AppendLine("mainland with medium/large lobes detaching all around generalize, or do some seeds come out too solid or shredded?");
sb.AppendLine();
sb.AppendLine("## The frozen setting");
sb.AppendLine();
sb.AppendLine($"`FragmentAmp {FrozenFragmentAmp}` · `FragmentFreqPerMapWidth {FrozenFragmentFreq}` · window `{FrozenBandCentre} ± {FrozenBandHalfWidth}` · bites-only `{FrozenBitesOnly}` · " +
$"`SouthStretch {FrozenStretch}` (band `{FrozenBandStart}` / feather `{FrozenBandFeather}`, sinker stretched `{FrozenStretchSinker}`) · speck revert < {speckCells} cells (`{FrozenSpeckFrac:G2}` of the map) · " +
"offshore OFF · shelf OFF · region labeling ON · the tagged curve (calibrated on task 01's pool at " + calibSize + "). Pinned explicitly in `FragGalleryTool` — nothing is left to a default.");
sb.AppendLine();
sb.AppendLine("## ⭐ The contact sheet");
sb.AppendLine();
sb.AppendLine("Open each seed's `regions.png` (the key view — detached pieces in colour) beside its `relief.png`.");
sb.AppendLine();
sb.AppendLine("| Seed | read | overlay | relief | N islands all / big | S islands all / big | largest N / S piece |");
sb.AppendLine("|---|---|---|---|---|---|---|");
foreach (var r in rows)
sb.AppendLine($"| `{r.Seed}`{(r.Anchor ? " anchor" : "")} | **{r.Read}** | [`{r.Seed}/regions.png`]({r.Seed}/regions.png) | [`{r.Seed}/relief.png`]({r.Seed}/relief.png) | {r.N.All} / {r.N.Big} | {r.S.All} / {r.S.Big} | {r.N.Max:N0} / {r.S.Max:N0} |");
sb.AppendLine();
sb.AppendLine($"## ⭐ The count/size table — hemisphere-split, with the size distribution");
sb.AppendLine();
sb.Append(TableMarkdown(rows, big));
sb.AppendLine();
sb.AppendLine("Read rule (numeric, stated): **too solid** if < 12 big islands across both hemispheres (the anchors had 2327 at 4096) · **shredded** if big < 25 % of all · else **good spread**. The eye's read is in the report. Also as `count_size_table.md` / `.csv`.");
sb.AppendLine();
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
sb.AppendLine();
sb.AppendLine("`ProvisionalEven`, flagged. The grayscale is the honest instrument; the overlay is the region layer's data.");
sb.AppendLine();
sb.AppendLine("## The oracle (render-only: the setting is the 09 setting, nothing upstream moved)");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(hard));
sb.AppendLine("Per seed (centre-is-land m · tag/coastline k · classify b):");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(perSeed));
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` | **keep** |");
sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + the frozen setting — large (256 MB each), clear freely |");
sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
sb.AppendLine();
sb.AppendLine($"Gallery 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 int[] EnvSeeds(string k, int[] fallback)
{
string v = EnvStr(k, null);
if (v == null) return fallback;
var outp = new List<int>();
foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
if (int.TryParse(part.Trim(), out int s) && s > 0) outp.Add(s);
return outp.Count > 0 ? outp.ToArray() : fallback;
}
}
}