The bare TerrainGenConfig defaults did NOT reproduce the terrain the developer
locked, so "run the default generator" was not "the terrain in the gallery" —
the single most expensive fact in the codebase, and the reason a fresh chat
would spend an afternoon chasing differences that were CONFIGURATION, not
regression. This is the deliberate task that ends that, before any river work.
A1 — the defaults ARE the locked shape now. Five fields actually move:
SpeckRevert false->true, MinLandComponentFrac 3e-5->2.5e-7 (120x smaller; the
config default would have eaten real islands, not specks), SouthStretch 0->2,
FragmentAmp 0->0.5, Erosion false->true. Seven more were already correct via
SouthernStretch.Default* / CoastalFragment.Default* and are now pinned as
literals, because TerrainShapeV1 used to do that pinning and this default set
inherits the job. CoastShelf stays OFF — the locked shape has no shelf, and
evaluating it (D-041) is its own later task once water renders. Offshore stays
Off permanently (D-063): islands are organic-only, made by the stretch +
fragmentation and identified by the region layer, never placed.
A2 — the preserve mechanism. The curve knots are percentiles of the FAMILY-OFF
land distribution; flipping the defaults would have moved the pool, the knots,
and with them the render field of every batch including terrain-shape-v1
itself. So the pool is pinned family-off (TerrainGenConfig.WithFamilyOff /
CalibrationPool) rather than the knots being baked: calibration stays live, its
INPUT distribution is held still. The pin was a no-op by construction — it sets
the values the defaults carried the instant before the flip — and re-measuring
after confirms it: pool, all six knots, per-seed spread, shaped max,
monotonicity spikeMax and all seven band shares identical.
Applied wider than "in CalibrateCurve": OffshoreIslandsTool,
RegionLabelingTool and SouthernStretchTool generate their own family-off
field for the Phase-1 anchor, so the pool pin alone would NOT have covered
them and their a1 would have failed for a configuration reason. TerrainGenTool
too — it AUTHORED 02_pass1_port and must stay able to regenerate its own
anchor.
Recorded as a judged-and-parked property: knots measured family-off, applied
family-on. Deliberate, not an oversight. Same disposition as the mid-slope
feather.
A4 — no oracle may pass against a superseded baseline. Six anchors retired
(01/03/04/06/08/09) with their checks and ISLA_T0x_SOURCE defaults; three kept
(chat1/02_pass1_port as the family-off pass-1 guard, chat2/10 and chat2/11 as
the shape and erosion acceptance anchors). Two invariants were RE-POINTED
rather than lost — the southern stretch's north-lock and the coastal-fragment
interior-lock now compare against SAME-RUN fields, which is scale-free and
cannot be invalidated by a moved dump. The retired dumps are kept, not deleted,
and marked superseded in their INDEX.md.
A missing anchor is now LOUD. The old pattern skipped silently, so a moved
anchor did not make its oracle fail — it made it not RUN, and a batch with a
skipped check prints an all-PASS table that reads like a clean one. That is
the INVERSE of the hazard the re-baseline guards against, and the migration
below is exactly the event that would have triggered it, on nine anchors at
once. ShapingOracle.LoadAnchor now separates the two cases: absent -> throw;
present at another size -> loud INCONCLUSIVE, which is a fail, never a pass.
TerrainShapeV1 inverted from PRESET to GUARD and moved to its own file.
Apply() is gone — stamping the values on top of the defaults would MASK a
drift instead of catching it. Its constants are now the assertion target, and
Assert() refuses a run whose defaults have drifted off the locked shape.
B/C — batches are namespaced by chat: batches/<chat>/NN_slug/. Task numbers
restart at 00 per chat, so a flat root collided the moment a second chat
existed — four colliding prefixes across 25 batches, separable only by slug.
ToolingPaths.ChatSlug is REQUIRED (throws if unset) and defaults per tool to
its authoring chat, so re-running reproduces a batch in place while ISLA_CHAT
redirects — which is also what stops an acceptance run from overwriting the
very anchor it checks against. Writes go through BatchRoot; historical READS
compose against BatchesRoot and so carry the prefix in their own source string
("chat1/02_pass1_port"). The 25 existing batches were migrated moves-only.
ACCEPTANCE — 16 of 16 byte-identical, 0 failed. All 8 gallery seeds at 8192
from the bare defaults are byte-identical to chat2/10_frag4_seed_gallery
(= terrain-shape-v1, a59e52f); all 8 erosion fields byte-identical to
chat2/11_erosion (= ea291ea). Every gallery table row and every erosion
statistic reproduces its recorded value exactly. DrainageTool's a11 passes
bit-identical over 67,108,864 cells, and its analysis reproduces batch 12
exactly — so the whole chain rivers depends on (shape -> erosion -> drainage)
is unchanged. All 12 edited tools re-run clean; both new guards negative-tested.
The baseline moved in DEFAULTS, not in TERRAIN.
-> XX_Human/output/rivers/01_rebaseline_and_batch_namespace.report.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhkXBQh2tDmcWKpXYcj8vj
489 lines
27 KiB
C#
489 lines
27 KiB
C#
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 interior-locked invariant check
|
||
/// </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 (chat2/09 batch, level 4) ═══
|
||
//
|
||
// ⭐ rivers/01: these were the ONLY home of the locked values. They now ALIAS
|
||
// `TerrainShapeV1`, which is itself the assertion target for `TerrainGenConfig`'s defaults —
|
||
// so the chain is: bare defaults → asserted against TerrainShapeV1 → printed here. One value,
|
||
// one place, and a throw if the generator ever stops agreeing with it.
|
||
private const float FrozenFragmentAmp = TerrainShapeV1.FragmentAmp;
|
||
private const float FrozenFragmentFreq = TerrainShapeV1.FragmentFreq;
|
||
private const float FrozenBandCentre = TerrainShapeV1.BandCentre;
|
||
private const float FrozenBandHalfWidth = TerrainShapeV1.BandHalfWidth;
|
||
private const bool FrozenBitesOnly = TerrainShapeV1.BitesOnly;
|
||
private const float FrozenStretch = TerrainShapeV1.Stretch;
|
||
private const float FrozenBandStart = TerrainShapeV1.BandStart;
|
||
private const float FrozenBandFeather = TerrainShapeV1.BandFeather;
|
||
private const bool FrozenStretchSinker = TerrainShapeV1.StretchSinker;
|
||
private const float FrozenSpeckFrac = TerrainShapeV1.SpeckFrac; // ≈ 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());
|
||
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
|
||
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
|
||
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
|
||
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
|
||
|
||
int task = EnvInt("ISLA_TASK", 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", "chat1/02_pass1_port");
|
||
|
||
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}");
|
||
|
||
// ⭐⭐ rivers/01: THE FROZEN SETTING IS NOW THE BARE DEFAULT.
|
||
//
|
||
// Every `Frozen*` constant above was re-homed into `TerrainGenConfig`'s defaults by the
|
||
// re-baseline, so this helper no longer SETS the shape — it only ABLATES it, for the
|
||
// family-off halves of the regression checks. That is the whole point: this batch is the
|
||
// `terrain-shape-v1` acceptance, and it can only prove the defaults reproduce the locked
|
||
// shape if it reads them instead of re-stating them.
|
||
//
|
||
// ⚠ The `frag` / `revert` / `stretch` flags are ABLATIONS ONLY. All three true = the bare
|
||
// default = the locked shape; `TerrainShapeV1.Assert` below is what keeps that claim
|
||
// honest if a default ever drifts.
|
||
TerrainGenConfig Frozen(int size, int seed, string label, bool frag = true, bool revert = true, bool stretch = true)
|
||
{
|
||
var c = new TerrainGenConfig
|
||
{
|
||
MapSize = size, Seed = seed, VariantLabel = label,
|
||
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
|
||
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
|
||
};
|
||
if (!stretch) c.SouthStretch = 0f;
|
||
if (!frag) c.FragmentAmp = 0f;
|
||
if (!revert) c.SpeckRevert = false;
|
||
// This batch is render-only shape: erosion is a later pass and never ran here.
|
||
c.Erosion = false;
|
||
return c;
|
||
}
|
||
|
||
// ⚠⚠ THE DEFAULT-DRIFT GUARD (rivers/01). The gallery above stopped STATING the locked shape
|
||
// and started READING it. If a default ever moves, every render silently moves with it and
|
||
// the batch still "passes" — so the claim is asserted, loudly, before a pixel is drawn.
|
||
// The `Frozen*` constants below are unchanged in value; their ROLE flipped from source to
|
||
// assertion target. → Tools/Scripts/TerrainShapeV1.cs
|
||
TerrainShapeV1.Assert("FragGallery");
|
||
GD.Print($" defaults : ✅ {TerrainShapeV1.Describe()}");
|
||
|
||
// ═══ 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;
|
||
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off). ⚠ loud.
|
||
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, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, calibSize), calibSize, p1Dump));
|
||
|
||
// ⚑ RETIRED at rivers/01 — a8 (`08_southern_stretch_explore`) and a9 (`09_coastal_fragment`).
|
||
// Both are EXPLORATION ladders this gallery was built to CONCLUDE: chat2/09 climbed the
|
||
// fragmentation ladder, chat2/10 froze rung 4 across 8 seeds, and the developer tagged the
|
||
// result `terrain-shape-v1`. Since rivers/01 that frozen setting IS the bare default, and
|
||
// `TerrainShapeV1.Assert` + a10 assert it directly — asserting it a third time through the
|
||
// rungs it was chosen from is circular, and 08's dump is at stretch 3 (off-shape) besides.
|
||
// The dump is NOT deleted (file-safety; regenerable, and the record of what was judged);
|
||
// its `INDEX.md` is marked superseded. → XX_Human/output/rivers/01_*.report.md §A4.
|
||
|
||
if (!skipAnchor)
|
||
{
|
||
foreach (int seed in AnchorSeeds)
|
||
{
|
||
// ⭐ r KEPT and SELF-ANCHORED — the interior-locked invariant is coastal fragmentation's
|
||
// load-bearing claim (it must touch the coastal window and NOTHING else), and it needs
|
||
// no external dump: both fields are generated here, from the bare defaults and from the
|
||
// same defaults with frag ablated off. That is what let 08 and 09 retire intact.
|
||
var c9 = Frozen(AnchorCheckSize, seed, "frag_4");
|
||
Pass1Result q9p = Topography.Generate(c9);
|
||
var c8 = Frozen(AnchorCheckSize, seed, "t08", frag: false, revert: false);
|
||
Pass1Result q8p = Topography.Generate(c8);
|
||
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, task, 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 23–27);
|
||
/// "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(TerrainGenConfig.CalibrationPool(calibSize, 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)
|
||
{
|
||
// ⭐ rivers/01: family-off PINNED, like the pool it shapes. (The family acts in pass 1 and
|
||
// `Shaping.Shape` never reads it, so this is inert today — pinned anyway so "the whole
|
||
// calibration is family-off" is a total claim rather than a field-by-field one.)
|
||
var scfg = new TerrainGenConfig
|
||
{
|
||
MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
|
||
CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
|
||
}.WithFamilyOff();
|
||
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 task, 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 {task:D2} — 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 + ").");
|
||
sb.AppendLine();
|
||
sb.AppendLine("> ### ⭐ Since rivers/01, this setting IS the bare `TerrainGenConfig` default — it is not stated here, it is READ.");
|
||
sb.AppendLine("> That is what makes this batch the acceptance for the re-baseline rather than a restatement of it: if a default");
|
||
sb.AppendLine("> ever drifts, `TerrainShapeV1.Assert` refuses the run instead of rendering a gallery that would look right and");
|
||
sb.AppendLine("> mean nothing. The curve calibration pool is pinned FAMILY-OFF (`TerrainGenConfig.WithFamilyOff`), which is what");
|
||
sb.AppendLine("> keeps the knots — and therefore these renders — bit-identical across the flip.");
|
||
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 23–27 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;
|
||
}
|
||
}
|
||
}
|