Core/Scripts/RegionLabeling.cs is the shared-infra contract, built to the letter: it runs on the CLASSIFY (raw) field; land is 8-connected, the deliberate complement of water's 4 (a diagonal isthmus joins; the water either side stays separate); a component is a maximal 8-connected set of land cells; the MAINLAND is the component containing the map centre — not merely the largest, which a later fragmentation step could flip — with a flagged fallback to the largest if the centre were ever water (asserted, never needed: oracle m); every other component is an island; per component id / sizeCells / centroid / hemisphere (by centroid, one label per island) / isMainland. Ids come from a fixed scan order and are proven stable across two generations (oracle o, 16.8M cells). It knows nothing about offshore or stamped. Engine-free, in Core as C++-candidate math; the hemisphere convention moved there with it, OffshoreAnalysis aliases it. Tools/Scripts/RegionPass.cs is pass 1c: label, revert, relabel, tag. The island tag (renamed IsIsland; IslandHemisphere from the component's centroid; Pass1Result.Regions carries the whole table) is now a CONSEQUENCE of labeling — every non-mainland component. That is the fix for the chat2/06 overlay, which tagged only what the offshore pass raised: 1063685222 has 11 natural islands including a 94,511-cell detached mass, 20260821 has 19, all grey in 06's tags.png and all coloured now. The offshore pass itself is untouched; its internal Tag stays for its own guards and is no longer exported. The speck revert (TerrainGenConfig.SpeckRevert / MinLandComponentFrac) lowers every non-mainland component below the threshold to the mean of its ring of adjacent sea cells, held strictly below sea. Origin-blind: a natural nub goes the same way as an offshore dot (6 natural components / 273 cells on the bare 1063685222 field at threshold_mid — reported as a3r, informational). Lower-only and component-only are asserted cell by cell in the pass and re-proven on the finished fields by oracle n (mainland bit-identical filter OFF vs ON; every changed cell in a sub-threshold island, lowered below sea); the mainland is never a candidate and its size is asserted unchanged across the revert. A reverted offshore island leaves its submerged skirt as a shoal — not this component, by the rule. Classify/render consistency is by construction (pass 1, curve identity at sea) and asserted by oracle k. Deliberately OFF in the bare TerrainGenConfig for the reason the shelf and islets are: the raw field has natural specks, so default-ON would move the calibration pool and every regression dump; the batch turns it on. Thresholds swept on 8 seeds at 4096 (1e-5 / 3e-5 / 1e-4 of the map = 168 / 503 / 1,678 cells): low removes 0–6 nubs per seed, mid (the config default, equal to the offshore guard) 2–11, high 26–36 — most of the offshore islands, the "fewer, bigger" bookend. The count/size table carries natural / pre / post counts per hemisphere, min/median/mean/max and a log-spaced size histogram — the instrument for the southern-stretch step. Oracle, all passing: a1, a3, a4 (8192, 67M cells) with labeling ON + revert OFF; a6 NEW — labeling ON + revert OFF on the 06 preset bit-identical to the 06 batch's render field (labeling is pure analysis); j0; m, n, o, i, j, k, l, b per field. Batch: BatchRoot(7, "region_labeling") — exactly 4 plates (three thresholds on 1063685222, threshold_mid on 20260821, the table's most-natural-islands seed), each with grayscale / .f32 / relief / the labeled-regions overlay / the tag overlay, plus count_size_table.md/.csv. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
679 lines
37 KiB
C#
679 lines
37 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text;
|
||
using Godot;
|
||
using IslaApocalypse.Core;
|
||
|
||
namespace IslaApocalypse.Tools
|
||
{
|
||
/// <summary>
|
||
/// ⭐ THE OFFSHORE-ISLANDS BATCH — chat2/06: ORGANIC-ONLY, TUNED FOR COVERAGE, SOUTH-WEIGHTED,
|
||
/// NO FORCED COUNT. (chat2/05's version of this tool — faithful / floor_only / hybrid / dense —
|
||
/// is in git history at 3b96e06; the forced floor it batched was reverted out on look.)
|
||
///
|
||
/// ⚠ Since chat2/07 the island tag is set BY THE REGION LAYER (every non-mainland land component,
|
||
/// natural islets included), so this tool's counts now include natural islands; the chat2/06 batch
|
||
/// of record (offshore-pass islands only) was produced at e8571b2.
|
||
///
|
||
/// ═══ WHAT IT PRODUCES — a fixed budget: 4 plates + a count table + a diagnosis ═══
|
||
///
|
||
/// PLATES (4, at ISLA_MAPSIZE, grayscale + .f32 + relief + tags overlay):
|
||
/// {plate}_density_low / _mid / _high three densities on ONE seed — the developer picks the
|
||
/// look by eye (more islands vs slop), apples to apples.
|
||
/// {bulge}_density_mid the preset on the southern-bulge seed — the table seed
|
||
/// whose south was SPARSEST at density_mid (auto-picked,
|
||
/// or ISLA_BULGE_SEED) — the south fix is not seed-specific.
|
||
///
|
||
/// THE COUNT TABLE (data, not plates): per seed, north / south island counts across ~12 seeds for
|
||
/// each of the 3 density levels, with min / mean / max per hemisphere per level, the guard
|
||
/// ledger and the island sizes. This is how "a few south / a couple north, consistently" is
|
||
/// READ — as statistics of the tuning, never as a floor.
|
||
///
|
||
/// THE DIAGNOSIS (data): per hemisphere over the calibration seed pool — valid-zone area, which
|
||
/// gate binds, noise peaks clearing the threshold — measured BEFORE any knob moved (§2 of the
|
||
/// task). → <see cref="OffshoreDiagnosis"/>.
|
||
///
|
||
/// ═══ THE CURVE IS THE TAGGED CURVE, UNCHANGED ═══
|
||
///
|
||
/// `continuous_restored` (tag terrain-curve-v1), calibrated on task 01's pool at the iteration
|
||
/// size WITH OFFSHORE OFF — islands are additive land on top of a curve that does not know they
|
||
/// exist. Oracle a1 / a3 / a4 prove offshore-off is bit-identical to Phase 1, task 03 and the
|
||
/// tag's own 04 gallery dump.
|
||
///
|
||
/// ═══ RUNNING IT ═══
|
||
///
|
||
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
|
||
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/OffshoreIslandsTool.tscn
|
||
///
|
||
/// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR
|
||
/// ISLA_MAPSIZE plate + table size (default 4096 — islands need pixels to read)
|
||
/// ISLA_TABLE_SIZE count-table size (default = ISLA_MAPSIZE; a probe may drop it)
|
||
/// ISLA_CALIB_SIZE curve calibration size (default 2048, task 01's) — also the diagnosis size
|
||
/// ISLA_TABLE_SEEDS the count-table seeds (default 12 below)
|
||
/// ISLA_PLATE_SEED the three-density seed (default 1063685222)
|
||
/// ISLA_BULGE_SEED the south-bulge seed (default 0 = auto: sparsest south at density_mid)
|
||
/// ISLA_DENS_LOW / ISLA_DENS_MID / ISLA_DENS_HIGH / ISLA_SOUTH_WEIGHT the tuning (probe overrides)
|
||
/// ISLA_OFF_MINAREA / ISLA_OFF_MINSEP / ISLA_OFF_MAXAREA the guards (probe overrides)
|
||
/// ISLA_OFF_FREQ / ISLA_OFF_CORE / ISLA_OFF_SHARP / ISLA_OFF_CREST the shape (probe overrides)
|
||
/// ISLA_TABLE_ONLY=1 probe: diagnosis + count table only (no regressions, no plates)
|
||
/// ISLA_SKIP_8K=1 skip the 8192 regression against the 04 gallery dump (a4)
|
||
/// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE the regression dumps' batches
|
||
/// </summary>
|
||
public partial class OffshoreIslandsTool : Node
|
||
{
|
||
/// <summary>
|
||
/// The count-table pool: chat2/05's six hybrid seeds + task 01's calibration pool (minus the
|
||
/// shared anchor) + one more. Twelve draws; the anchor first.
|
||
/// </summary>
|
||
private static readonly int[] DefaultTableSeeds =
|
||
{
|
||
1063685222, 20260821, 8675309, 123456789, 271828182, 999999937,
|
||
20260819, 777001, 424242, 90210, 31337, 55555,
|
||
};
|
||
|
||
/// <summary>⚠ Task 01's pool, verbatim — the curve's identity. Also the diagnosis pool.</summary>
|
||
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
|
||
|
||
private const int DefaultMapSize = 4096;
|
||
private const int DefaultCalibSize = 2048;
|
||
private const int GallerySize = 8192; // the 04 gallery's render size
|
||
|
||
/// <summary>The consistency targets the table is read against: "a couple north, a few south".</summary>
|
||
private const int TargetNorth = 2, TargetSouth = 3;
|
||
|
||
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 Row
|
||
{
|
||
public string Level; public int Seed;
|
||
public int CountN, CountS, PreN, PreS, SpecksN, SpecksS, ClustersN, ClustersS, BlobsN, BlobsS;
|
||
public long Lifted, SizeMin, SizeMed, SizeMax; public double SizeMean;
|
||
public float HMaxBefore, HMaxAfter;
|
||
public bool Ok; public ulong Ms;
|
||
}
|
||
|
||
private sealed class Level
|
||
{
|
||
public string Label; public OffshoreSettings Settings;
|
||
}
|
||
|
||
private void Run()
|
||
{
|
||
ToolingPaths.Configure(OS.GetUserDataDir());
|
||
|
||
int task = EnvInt("ISLA_TASK", 6);
|
||
string descr = EnvStr("ISLA_BATCH", "offshore_organic_tune");
|
||
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
|
||
int tableSize = EnvInt("ISLA_TABLE_SIZE", mapSize);
|
||
int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
|
||
int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds);
|
||
int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222);
|
||
int bulgeSeedEnv = EnvInt("ISLA_BULGE_SEED", 0);
|
||
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
|
||
string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
|
||
string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
|
||
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
|
||
bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1";
|
||
bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1";
|
||
|
||
string batchRoot = ToolingPaths.BatchRoot(task, descr);
|
||
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
|
||
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
|
||
|
||
var anchors = CurveAnchors.Default;
|
||
float sea = 0.15f;
|
||
|
||
// ═══ THE THREE DENSITY LEVELS — the preset of record is `mid` ═══
|
||
OffshoreSettings LevelSettings(float density)
|
||
{
|
||
var o = OffshoreSettings.Organic();
|
||
o.Density = density;
|
||
o.SouthWeight = EnvFloat("ISLA_SOUTH_WEIGHT", o.SouthWeight);
|
||
o.MinIslandAreaFrac = EnvFloat("ISLA_OFF_MINAREA", o.MinIslandAreaFrac);
|
||
o.MinSeparationFrac = EnvFloat("ISLA_OFF_MINSEP", o.MinSeparationFrac);
|
||
o.MaxIslandAreaFrac = EnvFloat("ISLA_OFF_MAXAREA", o.MaxIslandAreaFrac);
|
||
o.FreqPerMapWidth = EnvFloat("ISLA_OFF_FREQ", o.FreqPerMapWidth);
|
||
o.CoreFraction = EnvFloat("ISLA_OFF_CORE", o.CoreFraction);
|
||
o.EdgeSharpness = EnvFloat("ISLA_OFF_SHARP", o.EdgeSharpness);
|
||
o.CrestM = EnvFloat("ISLA_OFF_CREST", o.CrestM);
|
||
return o;
|
||
}
|
||
var levels = new List<Level>
|
||
{
|
||
new() { Label = "density_low", Settings = LevelSettings(EnvFloat("ISLA_DENS_LOW", OffshoreSettings.OrganicDensityLow)) },
|
||
new() { Label = "density_mid", Settings = LevelSettings(EnvFloat("ISLA_DENS_MID", OffshoreSettings.OrganicDensityMid)) },
|
||
new() { Label = "density_high", Settings = LevelSettings(EnvFloat("ISLA_DENS_HIGH", OffshoreSettings.OrganicDensityHigh)) },
|
||
};
|
||
Level mid = levels[1];
|
||
|
||
GD.Print("==================================================================");
|
||
GD.Print(" OFFSHORE ISLANDS (chat2/06) — organic-only, tuned for coverage, south-weighted, no forced count");
|
||
GD.Print("==================================================================");
|
||
GD.Print($"MapSize : {mapSize} (plates) table at {tableSize} curve calibrated at {calibSize} (offshore OFF) diagnosis at {calibSize}");
|
||
GD.Print($"table : {string.Join(", ", tableSeeds)}");
|
||
GD.Print($"plate seed: {plateSeed} bulge seed: {(bulgeSeedEnv > 0 ? bulgeSeedEnv.ToString() : "auto (sparsest south at density_mid)")}");
|
||
foreach (var l in levels) GD.Print($" {l.Label,-13} {l.Settings.Describe()}");
|
||
GD.Print($"hemisphere: NORTH = rows [0, {mapSize / 2}) SOUTH = rows [{mapSize / 2}, {mapSize}) (y runs south)");
|
||
GD.Print($"batch : {batchRoot}{(tableOnly ? " ⚠ ISLA_TABLE_ONLY — a probe, not the batch of record" : "")}");
|
||
GD.Print("==================================================================");
|
||
|
||
// ═══ 0. THE CURVE — continuous_restored, calibrated with offshore off ═══
|
||
GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, offshore off) ---");
|
||
var (knots, calibration, _) = CalibrateCurve(calibSize, sea, anchors);
|
||
GD.Print($" {knots}");
|
||
GD.Print($" {calibration.Describe()}");
|
||
|
||
// ═══ 1. REGRESSIONS — the things that must not have moved ═══
|
||
var hard = new List<ShapingOracle.Check>();
|
||
if (!tableOnly)
|
||
{
|
||
GD.Print($"\n--- 1. REGRESSIONS at {calibSize}, seed {plateSeed} ---");
|
||
var offCfg = BaseConfig(calibSize, plateSeed, knots, anchors, calibration, "off");
|
||
Pass1Result p1 = Topography.Generate(offCfg);
|
||
|
||
var curveOff = offCfg.Clone(); curveOff.Curve = false;
|
||
Pass2Result pOff = Shaping.Shape(p1, curveOff);
|
||
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32");
|
||
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF == Phase-1 .f32 dump",
|
||
pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
|
||
|
||
Pass2Result pRest = Shaping.Shape(p1, offCfg);
|
||
string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32");
|
||
hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF == task-03 .f32 dump (lowlands + curve untouched)",
|
||
pRest.Height, HeightField.Load(t03Dump, calibSize), calibSize, t03Dump));
|
||
|
||
// Shelf ON, islets OFF: land must be bit-identical (the shelf touches only sea).
|
||
var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only";
|
||
Pass1Result p1Shelf = Topography.Generate(shelfCfg);
|
||
var j0 = ShapingOracle.MainlandUnmoved(p1, p1Shelf, sea);
|
||
j0.Name = "shelf alone: every land cell bit-identical (shelf is below-sea only)";
|
||
hard.Add(j0);
|
||
hard.Add(ShapingOracle.HMaxAfterOffshore(p1Shelf));
|
||
foreach (var c in hard) GD.Print(" " + c);
|
||
|
||
// ⭐ a4 — offshore OFF at the 04 gallery's size == the terrain-curve-v1 tag's OWN output.
|
||
// The literal "offshore-off is bit-identical to terrain-curve-v1", at full size.
|
||
if (!skip8k)
|
||
{
|
||
string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32");
|
||
if (File.Exists(t04Dump))
|
||
{
|
||
GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, against {t04Dump} …");
|
||
var gCfg = BaseConfig(GallerySize, plateSeed, knots, anchors, calibration, "off");
|
||
Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg);
|
||
var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump",
|
||
pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump);
|
||
hard.Add(a4);
|
||
GD.Print(" " + a4);
|
||
}
|
||
else GD.Print($" a4: ⚠ skipped — no 04 gallery dump at {t04Dump}");
|
||
}
|
||
else GD.Print(" a4: skipped (ISLA_SKIP_8K)");
|
||
}
|
||
|
||
// ═══ 2. THE DIAGNOSIS — measure the south before touching a knob ═══
|
||
GD.Print($"\n--- 2. DIAGNOSIS (calibration pool at {calibSize}, shelf on, {mid.Label} thresholds) ---");
|
||
var diag = new List<OffshoreDiagnosis.Report>();
|
||
foreach (int s in CalibrationSeeds)
|
||
{
|
||
var dCfg = BaseConfig(calibSize, s, knots, anchors, calibration, "shelf_only");
|
||
dCfg.CoastShelf = true;
|
||
Pass1Result pShelf = Topography.Generate(dCfg);
|
||
var rep = OffshoreDiagnosis.Run(pShelf.Height, pShelf.PreTrenchFalloff, calibSize, s, sea, mid.Settings, dCfg.Scale);
|
||
diag.Add(rep);
|
||
GD.Print($" seed {s,-11} N: zone {rep.North.Zone,9:N0} ({rep.North.ZoneShareOfSea,6:P1}) peaks in zone {rep.North.PeaksInZone,3} > thr {rep.North.PeaksInZoneOverThr,3} " +
|
||
$"S: zone {rep.South.Zone,9:N0} ({rep.South.ZoneShareOfSea,6:P1}) peaks in zone {rep.South.PeaksInZone,3} > thr {rep.South.PeaksInZoneOverThr,3} " +
|
||
$"sole-blocked N d/f/t {rep.North.SoleDepth:N0}/{rep.North.SoleFalloff:N0}/{rep.North.SoleTrench:N0} S {rep.South.SoleDepth:N0}/{rep.South.SoleFalloff:N0}/{rep.South.SoleTrench:N0}");
|
||
}
|
||
var (poolN, poolS) = OffshoreDiagnosis.Pool(diag);
|
||
string interpretation = OffshoreDiagnosis.Interpret(poolN, poolS);
|
||
GD.Print(" " + interpretation);
|
||
|
||
// ═══ 3. THE COUNT TABLE — ~12 seeds × 3 levels ═══
|
||
GD.Print($"\n--- 3. COUNT TABLE at {tableSize} ---");
|
||
var rows = new List<Row>();
|
||
var perFieldChecks = new List<ShapingOracle.Check>();
|
||
bool notesShown = false;
|
||
foreach (int seed in tableSeeds)
|
||
{
|
||
Pass1Result p1Off = Topography.Generate(BaseConfig(tableSize, seed, knots, anchors, calibration, "off"));
|
||
foreach (var lv in levels)
|
||
{
|
||
var cfg = BaseConfig(tableSize, seed, knots, anchors, calibration, lv.Label);
|
||
cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone();
|
||
Pass1Result p1 = Topography.Generate(cfg);
|
||
Pass2Result p2 = Shaping.Shape(p1, cfg);
|
||
if (!notesShown) { foreach (string n in p1.Notes) GD.Print(" " + n); notesShown = true; }
|
||
|
||
var comps = OffshoreAnalysis.Components(p1.IsIsland, p1.Height, sea, tableSize);
|
||
var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps);
|
||
var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0);
|
||
var checks = new List<ShapingOracle.Check>
|
||
{
|
||
ShapingOracle.MoatIntact(p1, comps),
|
||
ShapingOracle.MainlandUnmoved(p1Off, p1, sea),
|
||
ShapingOracle.TagCoastlineConsistent(p2, sea),
|
||
ShapingOracle.HMaxAfterOffshore(p1),
|
||
ShapingOracle.ClassifyFidelity(p1, p2),
|
||
};
|
||
foreach (var c in checks) { c.Name += $" [{lv.Label} {seed}]"; perFieldChecks.Add(c); }
|
||
bool ok = checks.TrueForAll(c => c.Passed);
|
||
|
||
var row = new Row
|
||
{
|
||
Level = lv.Label, Seed = seed, CountN = cn, CountS = cs,
|
||
Lifted = p1.OffshoreLiftedCells, HMaxBefore = p1.HMaxSeedBeforeOffshore, HMaxAfter = p1.HMaxSeed,
|
||
SizeMin = szMin, SizeMed = szMed, SizeMean = szMean, SizeMax = szMax, Ok = ok, Ms = p1.ElapsedMs,
|
||
};
|
||
ReadGuardLedger(p1, row);
|
||
rows.Add(row);
|
||
GD.Print($" {lv.Label,-13} seed {seed,-11} N {cn,2} S {cs,2} (pre-guard N {row.PreN,2} S {row.PreS,2}; specks {row.SpecksN + row.SpecksS,2} clusters {row.ClustersN + row.ClustersS,2} blobs {row.BlobsN + row.BlobsS,2}) " +
|
||
$"size med {szMed,5} max {szMax,6} {(ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms");
|
||
}
|
||
}
|
||
var stats = LevelStats(levels, rows);
|
||
GD.Print("\n per level (min / mean / max):");
|
||
foreach (var st in stats)
|
||
GD.Print($" {st.Label,-13} N {st.MinN} / {st.MeanN:F1} / {st.MaxN} S {st.MinS} / {st.MeanS:F1} / {st.MaxS} " +
|
||
$"seeds with N≥{TargetNorth} & S≥{TargetSouth}: {st.MeetBoth}/{st.Seeds} S≥N: {st.SouthAtLeastNorth}/{st.Seeds} " +
|
||
$"guards: specks {st.Specks} clusters {st.Clusters} blobs {st.Blobs}");
|
||
|
||
// ═══ 4. THE PLATES — exactly four ═══
|
||
int bulgeSeed = bulgeSeedEnv > 0 ? bulgeSeedEnv : PickBulgeSeed(rows, mid.Label, plateSeed);
|
||
GD.Print($"\n southern-bulge seed: {bulgeSeed}{(bulgeSeedEnv > 0 ? " (ISLA_BULGE_SEED)" : " (auto: sparsest south at density_mid among the table seeds)")}");
|
||
var plates = new List<(int seed, Level level)> { (plateSeed, levels[0]), (plateSeed, levels[1]), (plateSeed, levels[2]), (bulgeSeed, mid) };
|
||
var plateRows = new List<Row>();
|
||
if (!tableOnly)
|
||
{
|
||
GD.Print($"\n--- 4. PLATES at {mapSize} ---");
|
||
foreach (var (seed, lv) in plates)
|
||
{
|
||
var cfg = BaseConfig(mapSize, seed, knots, anchors, calibration, lv.Label);
|
||
cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone();
|
||
Pass1Result p1 = Topography.Generate(cfg);
|
||
Pass2Result p2 = Shaping.Shape(p1, cfg);
|
||
var comps = OffshoreAnalysis.Components(p1.IsIsland, p1.Height, sea, mapSize);
|
||
var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps);
|
||
var moat = ShapingOracle.MoatIntact(p1, comps); moat.Name += $" [plate {lv.Label} {seed}]";
|
||
var tag = ShapingOracle.TagCoastlineConsistent(p2, sea); tag.Name += $" [plate {lv.Label} {seed}]";
|
||
perFieldChecks.Add(moat); perFieldChecks.Add(tag);
|
||
WritePlate(batchRoot, p1, p2, sea, anchors, skipRaw, cn, cs);
|
||
var row = new Row { Level = lv.Label, Seed = seed, CountN = cn, CountS = cs, Lifted = p1.OffshoreLiftedCells, Ok = moat.Passed && tag.Passed };
|
||
ReadGuardLedger(p1, row);
|
||
plateRows.Add(row);
|
||
GD.Print($" plate {seed}_{lv.Label}: N {cn} S {cs} lifted {p1.OffshoreLiftedCells:N0} {(row.Ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms");
|
||
foreach (string n in p1.Notes) if (n.Contains("guards") || n.Contains("islands:")) GD.Print(" " + n);
|
||
}
|
||
}
|
||
|
||
bool allOk = hard.TrueForAll(c => c.Passed) && perFieldChecks.TrueForAll(c => c.Passed);
|
||
GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}");
|
||
foreach (var c in perFieldChecks) if (!c.Passed) GD.PrintErr(" " + c);
|
||
|
||
// ═══ 5. THE DATA FILES + INDEX ═══
|
||
WriteCountTable(batchRoot, tableSize, levels, rows, stats);
|
||
WriteDiagnosis(batchRoot, calibSize, mid, diag, poolN, poolS, interpretation);
|
||
WriteIndex(batchRoot, mapSize, tableSize, calibSize, plateSeed, bulgeSeed, tableSeeds, levels, rows, stats, plateRows,
|
||
diag, poolN, poolS, interpretation, hard, perFieldChecks, allOk, tableOnly);
|
||
|
||
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 table's statistics -------------------------------------------
|
||
|
||
private sealed class LevelStat
|
||
{
|
||
public string Label; public int Seeds;
|
||
public int MinN, MaxN, MinS, MaxS; public double MeanN, MeanS;
|
||
public int MeetBoth, SouthAtLeastNorth, Specks, Clusters, Blobs;
|
||
public long SizeMed, SizeMax;
|
||
}
|
||
|
||
private static List<LevelStat> LevelStats(List<Level> levels, List<Row> rows)
|
||
{
|
||
var outp = new List<LevelStat>();
|
||
foreach (var lv in levels)
|
||
{
|
||
var st = new LevelStat { Label = lv.Label, MinN = int.MaxValue, MinS = int.MaxValue };
|
||
double sumN = 0, sumS = 0; var meds = new List<long>();
|
||
foreach (var r in rows)
|
||
{
|
||
if (r.Level != lv.Label) continue;
|
||
st.Seeds++;
|
||
st.MinN = Math.Min(st.MinN, r.CountN); st.MaxN = Math.Max(st.MaxN, r.CountN); sumN += r.CountN;
|
||
st.MinS = Math.Min(st.MinS, r.CountS); st.MaxS = Math.Max(st.MaxS, r.CountS); sumS += r.CountS;
|
||
if (r.CountN >= TargetNorth && r.CountS >= TargetSouth) st.MeetBoth++;
|
||
if (r.CountS >= r.CountN) st.SouthAtLeastNorth++;
|
||
st.Specks += r.SpecksN + r.SpecksS; st.Clusters += r.ClustersN + r.ClustersS; st.Blobs += r.BlobsN + r.BlobsS;
|
||
meds.Add(r.SizeMed); st.SizeMax = Math.Max(st.SizeMax, r.SizeMax);
|
||
}
|
||
if (st.Seeds == 0) { st.MinN = st.MinS = 0; }
|
||
else { st.MeanN = sumN / st.Seeds; st.MeanS = sumS / st.Seeds; meds.Sort(); st.SizeMed = meds[meds.Count / 2]; }
|
||
outp.Add(st);
|
||
}
|
||
return outp;
|
||
}
|
||
|
||
/// <summary>The table seed whose SOUTH count at the preset level is lowest (ties → lower total, then first in the pool), excluding the plate seed.</summary>
|
||
private static int PickBulgeSeed(List<Row> rows, string midLabel, int plateSeed)
|
||
{
|
||
int best = 0, bestS = int.MaxValue, bestTotal = int.MaxValue;
|
||
foreach (var r in rows)
|
||
{
|
||
if (r.Level != midLabel || r.Seed == plateSeed) continue;
|
||
int total = r.CountN + r.CountS;
|
||
if (r.CountS < bestS || (r.CountS == bestS && total < bestTotal)) { best = r.Seed; bestS = r.CountS; bestTotal = total; }
|
||
}
|
||
return best == 0 ? plateSeed : best;
|
||
}
|
||
|
||
/// <summary>Copy the pass's guard ledger into a table row (the pass reports it; the tool does not recompute it).</summary>
|
||
private static void ReadGuardLedger(Pass1Result p1, Row row)
|
||
{
|
||
var l = p1.OffshoreLedger;
|
||
if (l == null) return;
|
||
row.PreN = l.PreGuardNorth; row.PreS = l.PreGuardSouth;
|
||
row.SpecksN = l.SpecksNorth; row.SpecksS = l.SpecksSouth;
|
||
row.ClustersN = l.ClustersNorth; row.ClustersS = l.ClustersSouth;
|
||
row.BlobsN = l.BlobsNorth; row.BlobsS = l.BlobsSouth;
|
||
}
|
||
|
||
// ---- the curve, measured exactly as tasks 03/04/05 did ----------------
|
||
|
||
private static (CurveKnots, ClimbCalibration, Dictionary<int, Pass1Result>) 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 }); // offshore OFF by default
|
||
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, pass1);
|
||
}
|
||
|
||
private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
|
||
ClimbCalibration cal, string label) => new TerrainGenConfig
|
||
{
|
||
MapSize = mapSize, Seed = seed, VariantLabel = label,
|
||
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
|
||
Knots = k, Anchors = a, ClimbCalibration = cal, LowlandCeilingM = 30f,
|
||
CoastShelf = false, Offshore = new OffshoreSettings(), // OFF unless the variant turns it on
|
||
};
|
||
|
||
// ---- output -----------------------------------------------------------
|
||
|
||
private static void WritePlate(string batchRoot, Pass1Result p1, Pass2Result p2, float sea,
|
||
CurveAnchors anchors, bool skipRaw, int countN, int countS)
|
||
{
|
||
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}");
|
||
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, $"{p2.VariantLabel.ToUpperInvariant()} {p2.Seed}")
|
||
.SavePng(Path.Combine(dir, "relief.png"));
|
||
|
||
// ⭐ The tag overlay — the one artifact that shows the DATA this pass set.
|
||
TagOverlayRenderer.SavePng(p2.Height, p2.IsIsland, p2.IslandHemisphere, p2.MapSize, sea,
|
||
countN, countS, Path.Combine(dir, "tags.png"));
|
||
}
|
||
|
||
private static string CountTableMarkdown(List<Level> levels, List<Row> rows, List<LevelStat> stats)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("| Level | Seed | **N** | **S** | total | pre-guard N / S | specks | clusters | blobs | size cells min / median / mean / max | lifted cells | HMaxSeed before → after | oracle | ms |");
|
||
sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|");
|
||
foreach (var lv in levels)
|
||
foreach (var r in rows)
|
||
{
|
||
if (r.Level != lv.Label) continue;
|
||
sb.AppendLine($"| `{r.Level}` | `{r.Seed}` | **{r.CountN}** | **{r.CountS}** | {r.CountN + r.CountS} | {r.PreN} / {r.PreS} | " +
|
||
$"{r.SpecksN + r.SpecksS} | {r.ClustersN + r.ClustersS} | {r.BlobsN + r.BlobsS} | " +
|
||
$"{r.SizeMin} / {r.SizeMed} / {r.SizeMean:F0} / {r.SizeMax} | {r.Lifted:N0} | " +
|
||
$"{r.HMaxBefore:F4} → {r.HMaxAfter:F4}{(r.HMaxBefore != r.HMaxAfter ? " ⚠" : "")} | {(r.Ok ? "pass" : "**FAIL**")} | {r.Ms} |");
|
||
}
|
||
sb.AppendLine();
|
||
sb.AppendLine("**Per level — the consistency read (min / mean / max over the seeds):**");
|
||
sb.AppendLine();
|
||
sb.AppendLine($"| Level | density N / S | seeds | **N min / mean / max** | **S min / mean / max** | seeds with N ≥ {TargetNorth} & S ≥ {TargetSouth} | seeds with S ≥ N | specks / clusters / blobs reverted | median island (cells) | largest island (cells) |");
|
||
sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|");
|
||
foreach (var st in stats)
|
||
{
|
||
var lv = levels.Find(l => l.Label == st.Label);
|
||
sb.AppendLine($"| `{st.Label}` | {lv.Settings.Density:F4} / {lv.Settings.DensitySouth:F4} | {st.Seeds} | **{st.MinN} / {st.MeanN:F1} / {st.MaxN}** | **{st.MinS} / {st.MeanS:F1} / {st.MaxS}** | " +
|
||
$"**{st.MeetBoth} / {st.Seeds}** | {st.SouthAtLeastNorth} / {st.Seeds} | {st.Specks} / {st.Clusters} / {st.Blobs} | {st.SizeMed} | {st.SizeMax} |");
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
private static void WriteCountTable(string batchRoot, int tableSize, List<Level> levels, List<Row> rows, List<LevelStat> stats)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine($"# The count table — {rows.Count / Math.Max(1, levels.Count)} seeds × {levels.Count} density levels at {tableSize}");
|
||
sb.AppendLine();
|
||
sb.AppendLine("Tagged 8-connected island components per hemisphere (centroid), after the guards. **No count is forced**;");
|
||
sb.AppendLine("this is the statistical outcome of the tuning. NORTH = rows [0, N/2), SOUTH = rows [N/2, N); y runs south.");
|
||
sb.AppendLine();
|
||
sb.Append(CountTableMarkdown(levels, rows, stats));
|
||
WriteText(Path.Combine(batchRoot, "count_table.md"), sb.ToString());
|
||
|
||
var csv = new StringBuilder();
|
||
csv.AppendLine("level,seed,density_n,density_s,north,south,total,preguard_n,preguard_s,specks,clusters,blobs,size_min,size_median,size_mean,size_max,lifted_cells,hmax_before,hmax_after,oracle,ms");
|
||
foreach (var r in rows)
|
||
{
|
||
var lv = levels.Find(l => l.Label == r.Level);
|
||
csv.AppendLine(string.Join(",", r.Level, r.Seed, lv.Settings.Density.ToString("F5", System.Globalization.CultureInfo.InvariantCulture),
|
||
lv.Settings.DensitySouth.ToString("F5", System.Globalization.CultureInfo.InvariantCulture),
|
||
r.CountN, r.CountS, r.CountN + r.CountS, r.PreN, r.PreS, r.SpecksN + r.SpecksS, r.ClustersN + r.ClustersS, r.BlobsN + r.BlobsS,
|
||
r.SizeMin, r.SizeMed, r.SizeMean.ToString("F1", System.Globalization.CultureInfo.InvariantCulture), r.SizeMax, r.Lifted,
|
||
r.HMaxBefore.ToString("G9", System.Globalization.CultureInfo.InvariantCulture), r.HMaxAfter.ToString("G9", System.Globalization.CultureInfo.InvariantCulture),
|
||
r.Ok ? "pass" : "FAIL", r.Ms));
|
||
}
|
||
WriteText(Path.Combine(batchRoot, "count_table.csv"), csv.ToString());
|
||
}
|
||
|
||
private static string DiagnosisMarkdown(int calibSize, Level mid, List<OffshoreDiagnosis.Report> diag,
|
||
HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine($"Measured on the calibration pool at {calibSize} (shelf on, offshore off — the field the islet layer sees), with the");
|
||
sb.AppendLine($"`{mid.Label}` thresholds (density N {mid.Settings.Density:F4} / S {mid.Settings.DensitySouth:F4}). *Zone* = cells with zone weight > 0");
|
||
sb.AppendLine("(pass the falloff test + moat + outer bound); *peaks* = strict 8-neighbour local maxima of the islet noise field on sea cells;");
|
||
sb.AppendLine("*sole blocker* = sea cells that fail exactly one gate (loosen that gate and they join the zone); *lost > thr* = over-threshold");
|
||
sb.AppendLine("peaks outside the zone, with the gate(s) that excluded them.");
|
||
sb.AppendLine();
|
||
sb.AppendLine(OffshoreDiagnosis.TableHeader());
|
||
foreach (var r in diag)
|
||
{
|
||
sb.AppendLine(OffshoreDiagnosis.TableRow(r, r.North));
|
||
sb.AppendLine(OffshoreDiagnosis.TableRow(r, r.South));
|
||
}
|
||
var poolRep = new OffshoreDiagnosis.Report { Seed = 0 };
|
||
sb.AppendLine(OffshoreDiagnosis.TableRow(poolRep, poolN).Replace("| `0` |", "| **pool** |"));
|
||
sb.AppendLine(OffshoreDiagnosis.TableRow(poolRep, poolS).Replace("| `0` |", "| **pool** |"));
|
||
sb.AppendLine();
|
||
sb.AppendLine($"**Reading:** {interpretation}");
|
||
return sb.ToString();
|
||
}
|
||
|
||
private static void WriteDiagnosis(string batchRoot, int calibSize, Level mid, List<OffshoreDiagnosis.Report> diag,
|
||
HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("# The south diagnosis — zone area, gates and peaks per hemisphere");
|
||
sb.AppendLine();
|
||
sb.Append(DiagnosisMarkdown(calibSize, mid, diag, poolN, poolS, interpretation));
|
||
WriteText(Path.Combine(batchRoot, "diagnosis.md"), sb.ToString());
|
||
}
|
||
|
||
private static void WriteIndex(string batchRoot, int mapSize, int tableSize, int calibSize, int plateSeed, int bulgeSeed,
|
||
int[] tableSeeds, List<Level> levels, List<Row> rows, List<LevelStat> stats, List<Row> plateRows,
|
||
List<OffshoreDiagnosis.Report> diag, HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation,
|
||
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perField, bool allOk, bool tableOnly)
|
||
{
|
||
var mid = levels[1];
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("# Batch 06 — offshore islands: organic-only, tuned for coverage, south-weighted, no forced count");
|
||
sb.AppendLine();
|
||
sb.AppendLine("The chat2/05 forced floor (seeded stamps, guaranteed ≥2 N / ≥4 S) is **reverted out** — it looked stamped.");
|
||
sb.AppendLine("The **organic noise-field layer is the only island mechanism**; this batch tunes its **density** (the main");
|
||
sb.AppendLine("knob) and a **south weight** so it yields *a few south / a couple north* **consistently across seeds, as a");
|
||
sb.AppendLine("statistical outcome** — never a hard-coded count. Guards against slop: specks, clusters and blobs are");
|
||
sb.AppendLine("reverted whole; every surviving island is a noise outline, small, low, crisp.");
|
||
sb.AppendLine();
|
||
if (tableOnly) sb.AppendLine("> ⚠ **ISLA_TABLE_ONLY** — a probe run: diagnosis + count table only, no regressions, no plates. Not the batch of record.\n");
|
||
sb.AppendLine("## ⭐ Open this first");
|
||
sb.AppendLine();
|
||
sb.AppendLine($"1. **`{plateSeed}_density_mid/tags.png`** — the preset of record: grey mainland, cyan = island N, orange = island S.");
|
||
sb.AppendLine(" No rings any more — nothing is seeded. Then its `relief.png` for the shape.");
|
||
sb.AppendLine($"2. **`{plateSeed}_density_low/`** and **`{plateSeed}_density_high/`** beside it — the same seed, less and more density;");
|
||
sb.AppendLine(" pick the look by eye (more islands vs slop).");
|
||
sb.AppendLine($"3. **`{bulgeSeed}_density_mid/tags.png`** — the preset on the southern-bulge seed (the table seed whose south was");
|
||
sb.AppendLine(" sparsest at `density_mid`): the south tuning is not seed-specific.");
|
||
sb.AppendLine("4. Then the count table below — *does it consistently give a few south / a couple north?*");
|
||
sb.AppendLine();
|
||
sb.AppendLine($"**Hemisphere convention (from the code, not invented):** y runs SOUTH. NORTH = rows `[0, {mapSize / 2})`,");
|
||
sb.AppendLine($"SOUTH = rows `[{mapSize / 2}, {mapSize})`. Component hemisphere is by centroid; the tag per cell is by row.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## The four plates");
|
||
sb.AppendLine();
|
||
sb.AppendLine("| Plate | N | S | total | pre-guard N / S | specks / clusters / blobs | lifted cells | oracle |");
|
||
sb.AppendLine("|---|---|---|---|---|---|---|---|");
|
||
foreach (var r in plateRows)
|
||
sb.AppendLine($"| `{r.Seed}_{r.Level}/` | **{r.CountN}** | **{r.CountS}** | {r.CountN + r.CountS} | {r.PreN} / {r.PreS} | {r.SpecksN + r.SpecksS} / {r.ClustersN + r.ClustersS} / {r.BlobsN + r.BlobsS} | {r.Lifted:N0} | {(r.Ok ? "pass" : "**FAIL**")} |");
|
||
if (plateRows.Count == 0) sb.AppendLine("| *(no plates — probe run)* | | | | | | | |");
|
||
sb.AppendLine();
|
||
sb.AppendLine($"## ⭐ The count table — {tableSeeds.Length} seeds × 3 levels at {tableSize} (the consistency evidence)");
|
||
sb.AppendLine();
|
||
sb.Append(CountTableMarkdown(levels, rows, stats));
|
||
sb.AppendLine();
|
||
sb.AppendLine("Also as plain data: `count_table.md` / `count_table.csv`.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## The levels — density and south weight");
|
||
sb.AppendLine();
|
||
sb.AppendLine("| Level | settings |");
|
||
sb.AppendLine("|---|---|");
|
||
foreach (var lv in levels) sb.AppendLine($"| `{lv.Label}`{(lv == mid ? " ⭐ preset of record" : "")} | {lv.Settings.Describe()} |");
|
||
sb.AppendLine();
|
||
sb.AppendLine("The coast shelf is ON for every field (strength 0.775, scale 100 m, BitDecrement-clamped); invisible on these");
|
||
sb.AppendLine("hypsometric plates — ported faithfully, judged when water renders. Moat / falloff test / outer bound unchanged from chat2/05.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## The south diagnosis — measured before tuning");
|
||
sb.AppendLine();
|
||
sb.Append(DiagnosisMarkdown(calibSize, mid, diag, poolN, poolS, interpretation));
|
||
sb.AppendLine();
|
||
sb.AppendLine("Also as `diagnosis.md`.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
|
||
sb.AppendLine();
|
||
sb.AppendLine("`ProvisionalEven`, flagged. Grayscale + `tags.png` are the honest instruments here.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## The oracle");
|
||
sb.AppendLine();
|
||
sb.AppendLine("Regressions (offshore OFF must be bit-identical to Phase 1, task 03 and the `terrain-curve-v1` gallery dump):");
|
||
sb.AppendLine();
|
||
sb.AppendLine(hard.Count == 0 ? "*(skipped — probe run)*\n" : ShapingOracle.ToMarkdownTable(hard));
|
||
sb.AppendLine("Per field (moat i · mainland unmoved j · tag/coastline k · HMaxSeed l · classify b):");
|
||
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("| `tags.png`, `relief.png`, `INDEX.md`, `count_table.md` / `.csv`, `diagnosis.md` | **keep** |");
|
||
sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
|
||
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}, table at {tableSize}, 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 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;
|
||
}
|
||
}
|
||
}
|