chat2/09: coastal fragmentation — a perimeter-wide, band-limited noise, amplitude laddered at a fixed stretch
A dedicated term, not the edge noise scaled: pass 1's edge noise is positive-only, squircle- modulated, 2.5x the base frequency - the coastline's jitter; scaling it would roughen the whole rim and bias the coast inward. CoastalFragment adds, to the PRE-power falloff, amp * window(f) * noise(x + off, y + off) with its own deterministic field (seed offset 9109, coordinate offset 0.37 map widths - D-059), zero-mean, at 12 periods per map width (the lobe/neck scale, chosen by probe against 20 and 32: 20 climbs into smalls, 32 is fuzz), weighted by a smooth window on the falloff value itself, centred 0.66 (the coast's falloff for a median base noise, from the 08 diagnosis), half-width 0.18, EXACTLY ZERO beyond - so the interior, the massif and the deep sea are bit-identical by construction. Thin necks of barely-land flip first; nothing is detected, nothing is stamped. FragmentBitesOnly is exposed (probed: it erodes the coast inward - mainland -3/-8/-14 % at 0.06/0.15/0.30 - rather than detaching pieces; off by default). Batch: BatchRoot(9, "coastal_fragment") - 4 amplitudes (0.06, 0.15, 0.30, 0.50) x task 08's two seeds at 4096, stretch fixed at 2 (08's stretch_3 rung, whose dump is the bit-identical baseline, a8), sinker untouched, speck revert at < 4 cells, offshore/shelf off; lean render (regions overlay + relief + .f32), the hemisphere-split count/size table with largest-three and histograms. Oracle, all passing: a1 (the curve untouched), a8 x2 (frag OFF at stretch 2 == the 08 field, 16.8 M cells each), interior locked (r: every cell clear of the window bit-identical, 8/8), high-ground report (s, informational), centre is land, tag/coastline, classify == raw, ids and heights deterministic. The read: both hemispheres fragment; the north more readily (N 10 -> 28, 13 -> 40 islands across the ladder; S 4 -> 31, 14 -> 28) and the south's big stretch pieces are the zero-mean noise's other edge: on 999999937 the 83k-cell southern fragment is BRIDGED back onto the mainland at amp 0.50 (mainland +183k). The working range is amp 0.15-0.30; 0.50 is the bookend. Graduation held. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
This commit is contained in:
parent
537bd3e048
commit
bddd3636dd
9 changed files with 684 additions and 0 deletions
|
|
@ -41,6 +41,8 @@ constants, carried over verbatim — not re-derived from a design summary** (→
|
||||||
| `Scripts/RegionLabelingTool.cs` + `Scenes/RegionLabelingTool.tscn` | The chat2/07 batch — 3 revert thresholds + a second seed, the count/size instrument |
|
| `Scripts/RegionLabelingTool.cs` + `Scenes/RegionLabelingTool.tscn` | The chat2/07 batch — 3 revert thresholds + a second seed, the count/size instrument |
|
||||||
| `Scripts/SouthernStretch.cs` | ⭐ **The southern stretch** (chat2/08, exploration) — the one deliberate sea-identity relaxation, inside a fixed feathered latitude band: `y' = yB + (y − yB)/(1 + stretch·ramp)` for the mask geometry (and the sinker), texture untouched; north of the band bit-locked by construction |
|
| `Scripts/SouthernStretch.cs` | ⭐ **The southern stretch** (chat2/08, exploration) — the one deliberate sea-identity relaxation, inside a fixed feathered latitude band: `y' = yB + (y − yB)/(1 + stretch·ramp)` for the mask geometry (and the sinker), texture untouched; north of the band bit-locked by construction |
|
||||||
| `Scripts/SouthernStretchTool.cs` + `Scenes/SouthernStretchTool.tscn` | The chat2/08 batch — the diagnostic (`ISLA_DIAG_ONLY`) and the 5-level × 2-seed fragmentation ladder with the hemisphere-split instrument |
|
| `Scripts/SouthernStretchTool.cs` + `Scenes/SouthernStretchTool.tscn` | The chat2/08 batch — the diagnostic (`ISLA_DIAG_ONLY`) and the 5-level × 2-seed fragmentation ladder with the hemisphere-split instrument |
|
||||||
|
| `Scripts/CoastalFragment.cs` | ⭐ **Coastal fragmentation** (chat2/09, exploration) — a perimeter-wide, band-limited, zero-mean noise on the pre-power falloff inside the coastal window (≈ 0.66 ± 0.18); thin necks flip first; interior bit-identical by construction |
|
||||||
|
| `Scripts/CoastalFragmentTool.cs` + `Scenes/CoastalFragmentTool.tscn` | The chat2/09 batch — the frequency × amplitude probe (`ISLA_PROBE`) and the 4-amplitude × 2-seed ladder at a fixed stretch |
|
||||||
| `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The offshore batch — chat2/06: 4 plates + the count table + the diagnosis (the chat2/05 version is at `3b96e06`) |
|
| `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The offshore batch — chat2/06: 4 plates + the count table + the diagnosis (the chat2/05 version is at `3b96e06`) |
|
||||||
| `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** |
|
| `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** |
|
||||||
| `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles |
|
| `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles |
|
||||||
|
|
|
||||||
6
Tools/Scenes/CoastalFragmentTool.tscn
Normal file
6
Tools/Scenes/CoastalFragmentTool.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3 uid="uid://cfragment09isla"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://Tools/Scripts/CoastalFragmentTool.cs" id="1_cft"]
|
||||||
|
|
||||||
|
[node name="CoastalFragmentTool" type="Node"]
|
||||||
|
script = ExtResource("1_cft")
|
||||||
60
Tools/Scripts/CoastalFragment.cs
Normal file
60
Tools/Scripts/CoastalFragment.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ COASTAL FRAGMENTATION (chat2/09, EXPLORATION) — a perimeter-wide, band-limited, zero-mean noise
|
||||||
|
/// on the PRE-power falloff, applied only inside the coastal window.
|
||||||
|
///
|
||||||
|
/// ═══ WHY A DEDICATED TERM AND NOT THE EDGE NOISE SCALED ═══
|
||||||
|
///
|
||||||
|
/// Pass 1's edge noise is <c>(noise+1)/2 · 0.15 · squircle</c>: positive-only (it only ever PUSHES
|
||||||
|
/// the falloff up), modulated by the squircle (strongest at the rim, zero at the centre), sampled at
|
||||||
|
/// 2.5× the base frequency. Scaling it would roughen the whole rim and bias the coast inward; it is
|
||||||
|
/// the coastline's jitter, not a margin-targeted cutter. This term is separate: its own deterministic
|
||||||
|
/// field (seed offset + a coordinate offset in map widths), its own frequency (the neck/lobe scale),
|
||||||
|
/// zero-mean (it bites AND builds, so the coast is redrawn rather than eroded), and weighted by a
|
||||||
|
/// smooth WINDOW on the falloff value itself:
|
||||||
|
///
|
||||||
|
/// w(f) = 1 − smoothstep(|f − centre| / halfWidth) (exactly 0 beyond ± halfWidth)
|
||||||
|
/// falloff += amp · w(f) · noise(x + off, y + off) noise ∈ [−1, 1]
|
||||||
|
///
|
||||||
|
/// The coast sits where rawBase − falloff^2.5 crosses sea, i.e. near f ≈ 0.66 for a median rawBase
|
||||||
|
/// (chat2/08 diagnosis), so a window centred there covers the barely-land / barely-sea margin
|
||||||
|
/// around the whole perimeter, north and south. Cells whose falloff is clear of the window — the
|
||||||
|
/// interior, the massif, the deep sea — get w = 0 and are bit-identical by construction (asserted).
|
||||||
|
///
|
||||||
|
/// SELF-TARGETING: every lobe hangs off a neck of barely-land, cells whose height sits a hair above
|
||||||
|
/// sea. A bite of the same Δfalloff flips those first; solid land inside the window (a coastal hill)
|
||||||
|
/// moves in height but does not flip. No neck is detected; the margin selects itself. Amplitude is
|
||||||
|
/// the ladder; frequency is fixed and exposed as the secondary dial.
|
||||||
|
/// </summary>
|
||||||
|
public static class CoastalFragment
|
||||||
|
{
|
||||||
|
/// <summary>Periods per map width. The base noise is ≈ 4/map, the edge noise ≈ 10/map; lobes in the 08 plates are 2–7 % of the map. Chosen by the chat2/09 probe.</summary>
|
||||||
|
public const float DefaultFreqPerMapWidth = 12f;
|
||||||
|
|
||||||
|
/// <summary>The coastal window's centre in pre-power falloff units (the coast's falloff for a median base noise).</summary>
|
||||||
|
public const float DefaultBandCentre = 0.66f;
|
||||||
|
|
||||||
|
/// <summary>Half-width of the window. 0.18 spans f ∈ [0.48, 0.84] — the whole fringe, nothing of the interior.</summary>
|
||||||
|
public const float DefaultBandHalfWidth = 0.18f;
|
||||||
|
|
||||||
|
/// <summary>Bites only by default? Set by the chat2/09 probe (see the report): zero-mean redraws the margin and can bridge islands back; bites-only only cuts.</summary>
|
||||||
|
public const bool DefaultBitesOnly = false;
|
||||||
|
|
||||||
|
/// <summary>The field's seed offset (a SEED offset, like the islet layer's 7607).</summary>
|
||||||
|
public const int SeedOffset = 9109;
|
||||||
|
|
||||||
|
/// <summary>The field's coordinate offset, IN MAP WIDTHS (D-059) — decorrelates it from the base field's realization at every size.</summary>
|
||||||
|
public const float OffsetMapWidths = 0.37f;
|
||||||
|
|
||||||
|
/// <summary>The window weight for a pre-power falloff value.</summary>
|
||||||
|
public static float Window(float falloff, float centre, float halfWidth)
|
||||||
|
{
|
||||||
|
float t = MathF.Abs(falloff - centre) / halfWidth;
|
||||||
|
if (t >= 1f) return 0f;
|
||||||
|
return 1f - t * t * (3f - 2f * t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/CoastalFragment.cs.uid
Normal file
1
Tools/Scripts/CoastalFragment.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b4skxmovd0xpn
|
||||||
517
Tools/Scripts/CoastalFragmentTool.cs
Normal file
517
Tools/Scripts/CoastalFragmentTool.cs
Normal file
|
|
@ -0,0 +1,517 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Godot;
|
||||||
|
using IslaApocalypse.Core;
|
||||||
|
|
||||||
|
namespace IslaApocalypse.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ THE COASTAL-FRAGMENTATION BATCH (chat2/09, exploration) — at a FIXED stretch, a perimeter-wide
|
||||||
|
/// fragmentation-noise amplitude ladder: light → heavy, 4 levels × 2 seeds, options to pick from.
|
||||||
|
///
|
||||||
|
/// ═══ TWO MODES ═══
|
||||||
|
///
|
||||||
|
/// ISLA_PROBE=1 numbers only at ISLA_CALIB_SIZE: a frequency × amplitude sweep on both seeds
|
||||||
|
/// (island counts and sizes per hemisphere, mainland size) — used to fix the
|
||||||
|
/// frequency and place the amplitude ladder. Written to scratch/frag_probe.md.
|
||||||
|
/// (default) the BATCH: 4 amplitudes × 2 seeds at ISLA_MAPSIZE, lean render per field
|
||||||
|
/// (labeled-regions overlay + relief + .f32), the hemisphere-split count/size table,
|
||||||
|
/// the asymmetric oracle (interior locked, coast free).
|
||||||
|
///
|
||||||
|
/// Every field: pass 1 + stretch (FIXED) + fragmentation (the axis), region labeling ON, speck revert
|
||||||
|
/// at a LOW threshold (true 1–3-cell noise only), offshore OFF, shelf OFF. The curve is the tagged
|
||||||
|
/// curve, unchanged.
|
||||||
|
///
|
||||||
|
/// ═══ RUNNING IT ═══
|
||||||
|
///
|
||||||
|
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
|
||||||
|
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/CoastalFragmentTool.tscn
|
||||||
|
///
|
||||||
|
/// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR
|
||||||
|
/// ISLA_MAPSIZE / ISLA_CALIB_SIZE (default 4096 / 2048)
|
||||||
|
/// ISLA_SEEDS the two seeds (default 1063685222, 999999937 — task 08's)
|
||||||
|
/// ISLA_STRETCH the fixed stretch (default 2)
|
||||||
|
/// ISLA_FRAG_LEVELS the 4 amplitudes (default: the probe-chosen ladder)
|
||||||
|
/// ISLA_FRAG_FREQ the fixed frequency, periods per map width
|
||||||
|
/// ISLA_SPECK_FRAC the speck-revert threshold, fraction of map area (default 2.5e-7 ≈ 4 cells at 4096)
|
||||||
|
/// ISLA_PROBE=1 · ISLA_PROBE_FREQS · ISLA_PROBE_AMPS the probe sweep
|
||||||
|
/// ISLA_FRAG_BITES=1 bites-only noise ([0,1]) instead of zero-mean ([-1,1])
|
||||||
|
/// ISLA_SKIP_8K=1 (no 8192 check this batch — the 08 dump at 4096 is the baseline)
|
||||||
|
/// </summary>
|
||||||
|
public partial class CoastalFragmentTool : Node
|
||||||
|
{
|
||||||
|
private static readonly int[] DefaultSeeds = { 1063685222, 999999937 };
|
||||||
|
|
||||||
|
/// <summary>⚠ Task 01's pool, verbatim — the curve's identity.</summary>
|
||||||
|
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
|
||||||
|
|
||||||
|
/// <summary>⭐ THE LADDER — set from the probe (chat2/09 report §1): light → heavy, amplitude the only axis.</summary>
|
||||||
|
private static readonly float[] DefaultLadder = { 0.06f, 0.15f, 0.30f, 0.50f };
|
||||||
|
|
||||||
|
private static readonly float[] ProbeFreqs = { 12f, 20f, 32f };
|
||||||
|
private static readonly float[] ProbeAmps = { 0.03f, 0.06f, 0.12f, 0.20f, 0.32f, 0.5f };
|
||||||
|
|
||||||
|
private const float DefaultStretch = 2f;
|
||||||
|
private const float DefaultSpeckFrac = 2.5e-7f; // ≈ 4 cells at 4096 — true noise only
|
||||||
|
private const int DefaultMapSize = 4096;
|
||||||
|
private const int DefaultCalibSize = 2048;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
try { Run(); }
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
GD.PrintErr("==================================================================");
|
||||||
|
GD.PrintErr($" REFUSED: {e.Message}");
|
||||||
|
GD.PrintErr(e.StackTrace);
|
||||||
|
GD.PrintErr("==================================================================");
|
||||||
|
GetTree().Quit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class HemiStats
|
||||||
|
{
|
||||||
|
public int All, Big; public long Min, Med, Max; public double Mean; public int[] Hist; public long[] Largest = Array.Empty<long>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Row
|
||||||
|
{
|
||||||
|
public int Level; public float Amp; public int Seed;
|
||||||
|
public HemiStats N, S; public long MainlandCells; public int SpecksReverted;
|
||||||
|
public bool Ok; public ulong Ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Run()
|
||||||
|
{
|
||||||
|
ToolingPaths.Configure(OS.GetUserDataDir());
|
||||||
|
|
||||||
|
int task = EnvInt("ISLA_TASK", 9);
|
||||||
|
string descr = EnvStr("ISLA_BATCH", "coastal_fragment");
|
||||||
|
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
|
||||||
|
int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
|
||||||
|
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
|
||||||
|
float stretch = EnvFloat("ISLA_STRETCH", DefaultStretch);
|
||||||
|
float[] ladder = EnvFloats("ISLA_FRAG_LEVELS", DefaultLadder);
|
||||||
|
float freq = EnvFloat("ISLA_FRAG_FREQ", CoastalFragment.DefaultFreqPerMapWidth);
|
||||||
|
float speckFrac = EnvFloat("ISLA_SPECK_FRAC", DefaultSpeckFrac);
|
||||||
|
bool probe = EnvStr("ISLA_PROBE", "0") == "1";
|
||||||
|
bool bitesOnly = EnvStr("ISLA_FRAG_BITES", CoastalFragment.DefaultBitesOnly ? "1" : "0") == "1";
|
||||||
|
float[] probeFreqs = EnvFloats("ISLA_PROBE_FREQS", ProbeFreqs);
|
||||||
|
float[] probeAmps = EnvFloats("ISLA_PROBE_AMPS", ProbeAmps);
|
||||||
|
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
|
||||||
|
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
|
||||||
|
string t08Source = EnvStr("ISLA_T08_SOURCE", "08_southern_stretch_explore");
|
||||||
|
string t08Level = EnvStr("ISLA_T08_LEVEL", "stretch_3"); // the 08 rung with stretch 2
|
||||||
|
|
||||||
|
string batchRoot = ToolingPaths.BatchRoot(task, descr);
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
|
||||||
|
string scratch = ToolingPaths.BatchScratch(batchRoot);
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(scratch);
|
||||||
|
|
||||||
|
var anchors = CurveAnchors.Default;
|
||||||
|
float sea = 0.15f;
|
||||||
|
long big = Cells(RegionPass.ThresholdMidFrac, mapSize);
|
||||||
|
long speckCells = Cells(speckFrac, mapSize);
|
||||||
|
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GD.Print(" COASTAL FRAGMENTATION (chat2/09) — break more pieces off the edges, N + S");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GD.Print($"MapSize : {mapSize} (plates) calibration / probe at {calibSize}");
|
||||||
|
GD.Print($"seeds : {string.Join(", ", seeds)}");
|
||||||
|
GD.Print($"fixed : stretch {stretch:G3} (band {SouthernStretch.DefaultBandStartFrac:F2}/{SouthernStretch.DefaultBandFeatherFrac:F2}, sinker stretched) · frag freq {freq:G3}/map · window {CoastalFragment.DefaultBandCentre:F2} ± {CoastalFragment.DefaultBandHalfWidth:F2} · speck revert < {speckCells} cells ({speckFrac:G2})");
|
||||||
|
GD.Print($"ladder : FragmentAmp {string.Join(", ", ladder)} noise {(bitesOnly ? "BITES ONLY [0,1]" : "zero-mean [-1,1]")} (\"big\" island = ≥ {big:N0} cells at {mapSize})");
|
||||||
|
GD.Print($"batch : {batchRoot}{(probe ? " ⚠ ISLA_PROBE — numbers only" : "")}");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
|
||||||
|
GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, offshore off) ---");
|
||||||
|
var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors);
|
||||||
|
GD.Print($" {knots}");
|
||||||
|
float highRaw = knots.K2; // the top of the preserved lowland (30 m output) — "above the toe+red band"
|
||||||
|
|
||||||
|
TerrainGenConfig Cfg(int size, int seed, string label, float amp, float fq, float st, bool revert)
|
||||||
|
{
|
||||||
|
return new TerrainGenConfig
|
||||||
|
{
|
||||||
|
MapSize = size, Seed = seed, VariantLabel = label,
|
||||||
|
Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
|
||||||
|
Knots = knots, Anchors = anchors, ClimbCalibration = calibration, LowlandCeilingM = 30f,
|
||||||
|
CoastShelf = false, Offshore = new OffshoreSettings(),
|
||||||
|
RegionLabeling = true, SpeckRevert = revert, MinLandComponentFrac = speckFrac,
|
||||||
|
SouthStretch = st,
|
||||||
|
FragmentAmp = amp, FragmentFreqPerMapWidth = fq, FragmentBitesOnly = bitesOnly,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ PROBE ═══
|
||||||
|
if (probe)
|
||||||
|
{
|
||||||
|
GD.Print($"\n--- PROBE at {calibSize}: frequency × amplitude, stretch {stretch:G3} ---");
|
||||||
|
long bigC = Cells(RegionPass.ThresholdMidFrac, calibSize);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"# chat2/09 probe — fragmentation frequency × amplitude at {calibSize}, stretch {stretch:G3}, noise {(bitesOnly ? "bites only" : "zero-mean")}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"\"big\" = ≥ {bigC} cells at {calibSize}. Speck revert < {Cells(speckFrac, calibSize)} cells. Offshore off.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| seed | freq | amp | N islands all / big | N size med / max | S islands all / big | S size med / max | mainland cells | mainland Δ vs amp 0 |");
|
||||||
|
sb.AppendLine("|---|---|---|---|---|---|---|---|---|");
|
||||||
|
foreach (int seed in seeds)
|
||||||
|
{
|
||||||
|
var base0 = Topography.Generate(Cfg(calibSize, seed, "amp0", 0f, freq, stretch, true));
|
||||||
|
long main0 = base0.Regions.Mainland.SizeCells;
|
||||||
|
var (n0, s0) = Stats(base0.Regions, bigC);
|
||||||
|
sb.AppendLine($"| `{seed}` | — | 0 | {n0.All} / {n0.Big} | {n0.Med} / {n0.Max} | {s0.All} / {s0.Big} | {s0.Med} / {s0.Max} | {main0:N0} | 0 |");
|
||||||
|
GD.Print($" seed {seed} amp 0: N {n0.All}/{n0.Big} med {n0.Med} max {n0.Max} S {s0.All}/{s0.Big} med {s0.Med} max {s0.Max} mainland {main0:N0}");
|
||||||
|
foreach (float fq in probeFreqs)
|
||||||
|
foreach (float amp in probeAmps)
|
||||||
|
{
|
||||||
|
var p = Topography.Generate(Cfg(calibSize, seed, "probe", amp, fq, stretch, true));
|
||||||
|
var (n, s) = Stats(p.Regions, bigC);
|
||||||
|
long main = p.Regions.Mainland.SizeCells;
|
||||||
|
sb.AppendLine($"| `{seed}` | {fq:G3} | {amp:G3} | {n.All} / {n.Big} | {n.Med} / {n.Max} | {s.All} / {s.Big} | {s.Med} / {s.Max} | {main:N0} | {main - main0:+#,0;-#,0;0} |");
|
||||||
|
GD.Print($" seed {seed} freq {fq,4:G3} amp {amp,5:G3}: N {n.All,3}/{n.Big,3} med {n.Med,6} max {n.Max,7} S {s.All,3}/{s.Big,3} med {s.Med,6} max {s.Max,7} mainland {main:N0} ({main - main0:+#,0;-#,0;0})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WriteText(Path.Combine(scratch, bitesOnly ? "frag_probe_bites_only.md" : "frag_probe.md"), sb.ToString());
|
||||||
|
GD.Print($"\n probe written: {Path.Combine(scratch, "frag_probe.md")}");
|
||||||
|
GetTree().Quit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ 1. REGRESSIONS ═══
|
||||||
|
GD.Print($"\n--- 1. REGRESSIONS ---");
|
||||||
|
var hard = new List<ShapingOracle.Check>();
|
||||||
|
{
|
||||||
|
var offCfg = Cfg(calibSize, seeds[0], "off", 0f, freq, 0f, false);
|
||||||
|
Pass1Result p1 = Topography.Generate(offCfg);
|
||||||
|
var curveOff = offCfg.Clone(); curveOff.Curve = false;
|
||||||
|
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{seeds[0]}_full", "height.f32");
|
||||||
|
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, stretch OFF, frag OFF == Phase-1 .f32 dump (the curve is untouched)", Shaping.Shape(p1, curveOff).Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
|
||||||
|
|
||||||
|
// ⭐ a8 — frag OFF at the fixed stretch, revert OFF == the task-08 stretch-2 field (its dump at the plate size).
|
||||||
|
foreach (int seed in seeds)
|
||||||
|
{
|
||||||
|
string t08Dump = Path.Combine(ToolingPaths.BatchesRoot, t08Source, $"{seed}_{t08Level}", "height.f32");
|
||||||
|
if (File.Exists(t08Dump) && mapSize == 4096)
|
||||||
|
{
|
||||||
|
var c8 = Cfg(mapSize, seed, "t08", 0f, freq, stretch, false);
|
||||||
|
Pass2Result q8 = Shaping.Shape(Topography.Generate(c8), c8);
|
||||||
|
hard.Add(ShapingOracle.DumpRegression("a8", $"frag OFF, stretch {stretch:G3}, revert OFF == task-08 {t08Level} dump (the baseline) [{seed}]", q8.Height, HeightField.Load(t08Dump, mapSize), mapSize, t08Dump));
|
||||||
|
}
|
||||||
|
else GD.Print($" a8 [{seed}]: ⚠ skipped — {(mapSize != 4096 ? "map size is not 4096" : $"no 08 dump at {t08Dump}")}");
|
||||||
|
}
|
||||||
|
foreach (var c in hard) GD.Print(" " + c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ 2. THE LADDER — 4 amplitudes × 2 seeds ═══
|
||||||
|
GD.Print($"\n--- 2. THE LADDER at {mapSize} ---");
|
||||||
|
var rows = new List<Row>();
|
||||||
|
var baseline = new Dictionary<int, Row>();
|
||||||
|
var perField = new List<ShapingOracle.Check>();
|
||||||
|
foreach (int seed in seeds)
|
||||||
|
{
|
||||||
|
var c0 = Cfg(mapSize, seed, "frag_0", 0f, freq, stretch, true);
|
||||||
|
Pass1Result p0 = Topography.Generate(c0);
|
||||||
|
baseline[seed] = MakeRow(0, 0f, seed, p0, big, true, p0.ElapsedMs);
|
||||||
|
var b = baseline[seed];
|
||||||
|
GD.Print($" seed {seed} baseline (amp 0, stretch {stretch:G3}): N {b.N.All}/{b.N.Big} S {b.S.All}/{b.S.Big} mainland {b.MainlandCells:N0}");
|
||||||
|
|
||||||
|
for (int li = 0; li < ladder.Length; li++)
|
||||||
|
{
|
||||||
|
float amp = ladder[li];
|
||||||
|
string label = $"frag_{li + 1}";
|
||||||
|
var cfg = Cfg(mapSize, seed, label, amp, freq, stretch, true);
|
||||||
|
Pass1Result p1 = Topography.Generate(cfg);
|
||||||
|
Pass2Result p2 = Shaping.Shape(p1, cfg);
|
||||||
|
var checks = new List<ShapingOracle.Check>
|
||||||
|
{
|
||||||
|
ShapingOracle.InteriorLocked(p0, p1, cfg.FragmentBandCentre, cfg.FragmentBandHalfWidth),
|
||||||
|
ShapingOracle.HighGroundReport(p0, p1, highRaw, $"K2 ({highRaw:F3} raw, the top of the preserved lowland)"),
|
||||||
|
ShapingOracle.CentreIsLand(p1),
|
||||||
|
ShapingOracle.TagCoastlineConsistent(p2, sea),
|
||||||
|
ShapingOracle.ClassifyFidelity(p1, p2),
|
||||||
|
};
|
||||||
|
foreach (var c in checks) { c.Name += $" [{label} = {amp:G3}, {seed}]"; perField.Add(c); }
|
||||||
|
bool ok = checks.TrueForAll(c => c.Passed);
|
||||||
|
var row = MakeRow(li + 1, amp, seed, p1, big, ok, p1.ElapsedMs);
|
||||||
|
rows.Add(row);
|
||||||
|
WriteField(batchRoot, p1, p2, sea, anchors, skipRaw, amp);
|
||||||
|
GD.Print($" {label,-7} amp {amp,5:G3} seed {seed,-11} N {row.N.All,3}/{row.N.Big,3} med {row.N.Med,6} max {row.N.Max,7} S {row.S.All,3}/{row.S.Big,3} med {row.S.Med,6} max {row.S.Max,7} mainland {row.MainlandCells:N0} ({row.MainlandCells - b.MainlandCells:+#,0;-#,0;0}) specks {row.SpecksReverted} {(ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determinism
|
||||||
|
{
|
||||||
|
float amp = ladder[1];
|
||||||
|
var a = Topography.Generate(Cfg(mapSize, seeds[0], "det", amp, freq, stretch, true));
|
||||||
|
var bb = Topography.Generate(Cfg(mapSize, seeds[0], "det", amp, freq, stretch, true));
|
||||||
|
var det = ShapingOracle.LabelsDeterministic(a, bb); det.Name += $" [amp {amp:G3}, {seeds[0]}]";
|
||||||
|
var bits = ShapingOracle.NorthLocked("o2", $"two generations bit-identical everywhere [amp {amp:G3}, {seeds[0]}]", a.Height, bb.Height, mapSize, mapSize);
|
||||||
|
perField.Add(det); perField.Add(bits); GD.Print(" " + det); GD.Print(" " + bits);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allOk = hard.TrueForAll(c => c.Passed) && perField.TrueForAll(c => c.Passed);
|
||||||
|
GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}");
|
||||||
|
foreach (var c in perField) if (!c.Passed) GD.PrintErr(" " + c);
|
||||||
|
|
||||||
|
WriteTable(batchRoot, mapSize, ladder, seeds, rows, baseline, big, stretch, freq, speckCells);
|
||||||
|
WriteIndex(batchRoot, mapSize, calibSize, ladder, seeds, rows, baseline, big, stretch, freq, speckCells, hard, perField, allOk);
|
||||||
|
|
||||||
|
GD.Print("\n==================================================================");
|
||||||
|
GD.Print($" DONE — {batchRoot}");
|
||||||
|
GD.Print($" ORACLE {(allOk ? "HARD CHECKS ALL PASS" : "*** FAILURES — see the table ***")}");
|
||||||
|
GD.Print("==================================================================");
|
||||||
|
GetTree().Quit(allOk ? 0 : 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the instrument --------------------------------------------------
|
||||||
|
|
||||||
|
private static long Cells(float frac, int size) => Math.Max(1L, (long)Math.Round(frac * (double)size * size));
|
||||||
|
|
||||||
|
private static (HemiStats north, HemiStats south) Stats(RegionLabels l, long big)
|
||||||
|
{
|
||||||
|
var n = new List<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Row MakeRow(int level, float amp, int seed, Pass1Result p1, long big, bool ok, ulong ms)
|
||||||
|
{
|
||||||
|
var (n, s) = Stats(p1.Regions, big);
|
||||||
|
return new Row
|
||||||
|
{
|
||||||
|
Level = level, Amp = amp, Seed = seed, N = n, S = s, MainlandCells = p1.Regions.Mainland.SizeCells,
|
||||||
|
SpecksReverted = p1.RegionLedger?.RevertedComponents ?? 0, Ok = ok, Ms = ms,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the curve --------------------------------------------------------
|
||||||
|
|
||||||
|
private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
|
||||||
|
{
|
||||||
|
var rawPool = new LandHistogram(sea);
|
||||||
|
var pass1 = new Dictionary<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 WriteField(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw, float amp)
|
||||||
|
{
|
||||||
|
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}");
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(dir);
|
||||||
|
if (!skipRaw) HeightField.Save(p2.Height, p2.MapSize, Path.Combine(dir, "height.f32"));
|
||||||
|
var look = new LookConfig
|
||||||
|
{
|
||||||
|
Name = "hillshade_even", Palette = ReliefPalette.Kind.ProvisionalEven,
|
||||||
|
ZExaggeration = 18f, LightAzimuth = 315f, LightAltitude = 45f, HillshadeStrength = 0.30f, SeaLevel = sea,
|
||||||
|
};
|
||||||
|
Image map = ReliefRenderer.Render(p2.Height, p2.MapSize, look);
|
||||||
|
LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap, $"{p2.VariantLabel.ToUpperInvariant()} (AMP {amp:G3}) {p2.Seed}")
|
||||||
|
.SavePng(Path.Combine(dir, "relief.png"));
|
||||||
|
var led = p1.RegionLedger;
|
||||||
|
RegionOverlayRenderer.SavePng(p1.Regions, led != null && led.RevertOn ? p1.RegionsPre : null, p1.MapSize,
|
||||||
|
led?.RevertedComponents ?? 0, led?.ThresholdCells ?? 0, Path.Combine(dir, "regions.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string HistRow(int[] h)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < h.Length; i++) { if (i > 0) sb.Append(" · "); sb.Append(h[i]); }
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Largest(long[] l) => l.Length == 0 ? "—" : string.Join(" / ", Array.ConvertAll(l, v => v.ToString("N0")));
|
||||||
|
|
||||||
|
private static string TableMarkdown(float[] ladder, int[] seeds, List<Row> rows, Dictionary<int, Row> baseline, long big)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var histHead = new StringBuilder();
|
||||||
|
for (int i = 0; i <= RegionLabeling.HistogramEdges.Length; i++) { if (i > 0) histHead.Append(" · "); histHead.Append(RegionLabeling.HistogramLabel(i)); }
|
||||||
|
sb.AppendLine($"| Level | amp | Seed | **N islands all / ≥ {big:N0}** | N size med / mean / max | N largest three | N histogram ({histHead}) | **S islands all / ≥ {big:N0}** | S size med / mean / max | S largest three | S histogram | mainland cells (Δ vs amp 0) | specks reverted | oracle |");
|
||||||
|
sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|");
|
||||||
|
foreach (int seed in seeds)
|
||||||
|
{
|
||||||
|
var b = baseline[seed];
|
||||||
|
var all = new List<Row> { b }; all.AddRange(rows.FindAll(r => r.Seed == seed));
|
||||||
|
foreach (var r in all)
|
||||||
|
sb.AppendLine($"| {(r.Level == 0 ? "*baseline*" : $"`frag_{r.Level}`")} | {r.Amp:G3} | `{seed}` | **{r.N.All} / {r.N.Big}** | {r.N.Med} / {r.N.Mean:F0} / {r.N.Max} | {Largest(r.N.Largest)} | {HistRow(r.N.Hist)} | " +
|
||||||
|
$"**{r.S.All} / {r.S.Big}** | {r.S.Med} / {r.S.Mean:F0} / {r.S.Max} | {Largest(r.S.Largest)} | {HistRow(r.S.Hist)} | {r.MainlandCells:N0} ({r.MainlandCells - b.MainlandCells:+#,0;-#,0;0}) | {r.SpecksReverted} | {(r.Level == 0 ? "—" : r.Ok ? "pass" : "**FAIL**")} |");
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteTable(string batchRoot, int mapSize, float[] ladder, int[] seeds, List<Row> rows, Dictionary<int, Row> baseline, long big,
|
||||||
|
float stretch, float freq, long speckCells)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine($"# The hemisphere-split count/size table — {ladder.Length} amplitudes × {seeds.Length} seeds at {mapSize}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Fixed: stretch {stretch:G3}, fragmentation frequency {freq:G3}/map, window {CoastalFragment.DefaultBandCentre:F2} ± {CoastalFragment.DefaultBandHalfWidth:F2}, speck revert < {speckCells} cells. Offshore / shelf OFF.");
|
||||||
|
sb.AppendLine("Islands = non-mainland 8-connected land components of the classify field; hemisphere by centroid. BOTH hemispheres are signals now.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append(TableMarkdown(ladder, seeds, rows, baseline, big));
|
||||||
|
WriteText(Path.Combine(batchRoot, "count_size_table.md"), sb.ToString());
|
||||||
|
|
||||||
|
var csv = new StringBuilder();
|
||||||
|
csv.AppendLine("level,amp,seed,n_all,n_big,n_med,n_mean,n_max,n_largest,n_hist,s_all,s_big,s_med,s_mean,s_max,s_largest,s_hist,mainland_cells,specks_reverted,oracle,ms");
|
||||||
|
var ic = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
|
foreach (int seed in seeds)
|
||||||
|
{
|
||||||
|
var all = new List<Row> { baseline[seed] }; all.AddRange(rows.FindAll(r => r.Seed == seed));
|
||||||
|
foreach (var r in all)
|
||||||
|
csv.AppendLine(string.Join(",", r.Level, r.Amp.ToString("G5", ic), r.Seed,
|
||||||
|
r.N.All, r.N.Big, r.N.Med, r.N.Mean.ToString("F1", ic), r.N.Max, "\"" + Largest(r.N.Largest) + "\"", "\"" + HistRow(r.N.Hist) + "\"",
|
||||||
|
r.S.All, r.S.Big, r.S.Med, r.S.Mean.ToString("F1", ic), r.S.Max, "\"" + Largest(r.S.Largest) + "\"", "\"" + HistRow(r.S.Hist) + "\"",
|
||||||
|
r.MainlandCells, r.SpecksReverted, r.Ok ? "pass" : "FAIL", r.Ms));
|
||||||
|
}
|
||||||
|
WriteText(Path.Combine(batchRoot, "count_size_table.csv"), csv.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteIndex(string batchRoot, int mapSize, int calibSize, float[] ladder, int[] seeds, List<Row> rows, Dictionary<int, Row> baseline, long big,
|
||||||
|
float stretch, float freq, long speckCells, List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perField, bool allOk)
|
||||||
|
{
|
||||||
|
int first = seeds.Length > 1 ? seeds[1] : seeds[0];
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("# Batch 09 — coastal fragmentation: break more pieces off the edges, N + S");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**Options, not a setting.** At a FIXED stretch, a perimeter-wide band-limited fragmentation noise on the pre-power");
|
||||||
|
sb.AppendLine("falloff — only inside the coastal window, zero-mean — is swept light → heavy. Thin necks of barely-land flip first");
|
||||||
|
sb.AppendLine("(self-targeting: nothing detected, nothing stamped); the interior is bit-identical by construction (asserted). The");
|
||||||
|
sb.AppendLine("region layer is the instrument; both hemispheres are fragmentation signals now.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## ⭐ Open this first");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"1. **`{first}_frag_2/regions.png`** — a mid amplitude on the seed whose natural southern islands read fragmentation best; grey mainland, each island its own colour, dark red = a reverted speck (< {speckCells} cells).");
|
||||||
|
sb.AppendLine($"2. Walk the ladder on that seed: `{first}_frag_1/` … `_frag_{ladder.Length}/` (`regions.png` beside `relief.png`); then the same on `{seeds[0]}`.");
|
||||||
|
sb.AppendLine("3. Then the table: N and S count + size side by side, down the rows as amplitude climbs — look for counts rising while the largest pieces stay healthy.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## The fixed frame and the axis");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"- **Fixed:** stretch `{stretch:G3}` (task 08's `stretch_3` rung; band {SouthernStretch.DefaultBandStartFrac:F2} / feather {SouthernStretch.DefaultBandFeatherFrac:F2}, sinker stretched — untouched this round) · fragmentation frequency `{freq:G3}` periods/map (the secondary dial, fixed) · window centre {CoastalFragment.DefaultBandCentre:F2} ± {CoastalFragment.DefaultBandHalfWidth:F2} (pre-power falloff) · speck revert < {speckCells} cells (true noise only) · offshore OFF · shelf OFF.");
|
||||||
|
sb.AppendLine($"- **The axis — `FragmentAmp`:** {string.Join(" · ", Array.ConvertAll(ladder, v => v.ToString("G3")))} (levels 1–{ladder.Length}); baseline 0 = the task-08 stretch field, measured for Δ.");
|
||||||
|
sb.AppendLine($"- \"big\" island = ≥ {big:N0} cells at {mapSize} (the 07 `threshold_mid`).");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"## ⭐ The hemisphere-split count/size table — {ladder.Length} amplitudes × {seeds.Length} seeds at {mapSize}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append(TableMarkdown(ladder, seeds, rows, baseline, big));
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Also as plain data: `count_size_table.md` / `.csv`. The probe that fixed the frequency and placed the ladder: `scratch/frag_probe.md`.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("`ProvisionalEven`, flagged. The individually-coloured scheme is only the `regions.png` overlay.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## The oracle (asymmetric: interior locked, coast free)");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Regressions (the curve untouched; frag OFF at the fixed stretch bit-identical to the task-08 field):");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(ShapingOracle.ToMarkdownTable(hard));
|
||||||
|
sb.AppendLine("Per field (interior locked r · high-ground report s (informational) · centre-is-land m · tag/coastline k · classify b · determinism o / o2):");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(ShapingOracle.ToMarkdownTable(perField));
|
||||||
|
sb.AppendLine($"**{(allOk ? "ALL HARD CHECKS PASS" : "⚠⚠ FAILURES — do not judge this batch")}**");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## Disposability");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Artifact | Keep? |");
|
||||||
|
sb.AppendLine("|---|---|");
|
||||||
|
sb.AppendLine("| `regions.png`, `relief.png`, `INDEX.md`, `count_size_table.md` / `.csv`, `scratch/frag_probe.md` | **keep** |");
|
||||||
|
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code — large, clear freely |");
|
||||||
|
sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Plates at {mapSize}, curve calibrated at {calibSize} with offshore off. {WorldScale.Describe()}.");
|
||||||
|
WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteText(string path, string text)
|
||||||
|
{
|
||||||
|
using var f = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Write);
|
||||||
|
if (f == null) { GD.PrintErr($"could not write {path}"); return; }
|
||||||
|
f.StoreString(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- env helpers --------------------------------------------------------
|
||||||
|
|
||||||
|
private static string EnvStr(string k, string fallback)
|
||||||
|
{
|
||||||
|
string v = System.Environment.GetEnvironmentVariable(k);
|
||||||
|
return string.IsNullOrWhiteSpace(v) ? fallback : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int EnvInt(string k, int fallback)
|
||||||
|
=> int.TryParse(EnvStr(k, null) ?? "", out int v) ? v : fallback;
|
||||||
|
|
||||||
|
private static float EnvFloat(string k, float fallback)
|
||||||
|
=> float.TryParse(EnvStr(k, null) ?? "", System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out float v) ? v : fallback;
|
||||||
|
|
||||||
|
private static float[] EnvFloats(string k, float[] fallback)
|
||||||
|
{
|
||||||
|
string v = EnvStr(k, null);
|
||||||
|
if (v == null) return fallback;
|
||||||
|
var outp = new List<float>();
|
||||||
|
foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
if (float.TryParse(part.Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float f)) outp.Add(f);
|
||||||
|
return outp.Count > 0 ? outp.ToArray() : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] EnvSeeds(string k, int[] fallback)
|
||||||
|
{
|
||||||
|
string v = EnvStr(k, null);
|
||||||
|
if (v == null) return fallback;
|
||||||
|
var outp = new List<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Tools/Scripts/CoastalFragmentTool.cs.uid
Normal file
1
Tools/Scripts/CoastalFragmentTool.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cuwg87lli67l6
|
||||||
|
|
@ -627,6 +627,54 @@ namespace IslaApocalypse.Tools
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══ chat2/09 — the coastal-fragmentation checks ═══
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// (r) ⭐ INTERIOR LOCKED — every cell whose BASELINE pre-trench falloff is clear of the coastal
|
||||||
|
/// window (weight exactly 0: the interior, the massif, the deep sea) is bit-identical between the
|
||||||
|
/// baseline and the fragmented field. Reports how many cells changed inside the window (the
|
||||||
|
/// coast, allowed). The proof that fragmentation cannot reach inland.
|
||||||
|
/// </summary>
|
||||||
|
public static Check InteriorLocked(Pass1Result baseline, Pass1Result frag, float centre, float halfWidth)
|
||||||
|
{
|
||||||
|
var c = new Check { Id = "r", Name = "interior locked — every cell clear of the coastal window bit-identical (classify)" };
|
||||||
|
int n = baseline.MapSize; long outside = 0, outsideDiff = 0, inside = 0, insideDiff = 0; string first = null;
|
||||||
|
for (int x = 0; x < n; x++)
|
||||||
|
for (int y = 0; y < n; y++)
|
||||||
|
{
|
||||||
|
bool inWindow = MathF.Abs(baseline.PreTrenchFalloff[x, y] - centre) < halfWidth;
|
||||||
|
bool same = BitConverter.SingleToInt32Bits(baseline.Height[x, y]) == BitConverter.SingleToInt32Bits(frag.Height[x, y]);
|
||||||
|
if (inWindow) { inside++; if (!same) insideDiff++; }
|
||||||
|
else { outside++; if (!same) { outsideDiff++; first ??= $"[{x},{y}] f {baseline.PreTrenchFalloff[x, y]:F3}: {baseline.Height[x, y]:G9} → {frag.Height[x, y]:G9}"; } }
|
||||||
|
}
|
||||||
|
c.Passed = outsideDiff == 0;
|
||||||
|
c.Detail = c.Passed
|
||||||
|
? $"all {outside:N0} cells outside the window bit-identical; {insideDiff:N0} of {inside:N0} window cells changed (the coast)"
|
||||||
|
: $"{outsideDiff:N0} cells OUTSIDE the window changed — {first}";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// (s) informational — HIGH GROUND: of the cells whose BASELINE raw height is at or above
|
||||||
|
/// <paramref name="rawThreshold"/>, how many changed, and the largest change. A coastal hill
|
||||||
|
/// inside the window may legitimately move in height without flipping; this reports it.
|
||||||
|
/// </summary>
|
||||||
|
public static Check HighGroundReport(Pass1Result baseline, Pass1Result frag, float rawThreshold, string thresholdLabel)
|
||||||
|
{
|
||||||
|
var c = new Check { Id = "s", Name = $"(informational) high ground ≥ {thresholdLabel}: cells changed / largest |Δ|", Passed = true };
|
||||||
|
int n = baseline.MapSize; long high = 0, changed = 0; float maxAbs = 0f;
|
||||||
|
for (int x = 0; x < n; x++)
|
||||||
|
for (int y = 0; y < n; y++)
|
||||||
|
{
|
||||||
|
float a = baseline.Height[x, y]; if (a < rawThreshold) continue;
|
||||||
|
high++;
|
||||||
|
float b = frag.Height[x, y];
|
||||||
|
if (a != b) { changed++; float d = MathF.Abs(b - a); if (d > maxAbs) maxAbs = d; }
|
||||||
|
}
|
||||||
|
c.Detail = $"{changed:N0} of {high:N0} high cells changed; largest |Δ| {maxAbs:G4} raw ({Core.WorldScale.MetresFromRaw(maxAbs):F1} m)";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary>
|
/// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary>
|
||||||
public static string ToMarkdownTable(IEnumerable<Check> checks)
|
public static string ToMarkdownTable(IEnumerable<Check> checks)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,31 @@ namespace IslaApocalypse.Tools
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool StretchSinker = SouthernStretch.DefaultStretchSinker;
|
public bool StretchSinker = SouthernStretch.DefaultStretchSinker;
|
||||||
|
|
||||||
|
// ---- PASS 1 — COASTAL FRAGMENTATION (chat2/09, exploration) ------------------
|
||||||
|
//
|
||||||
|
// A band-limited, zero-mean noise added to the PRE-power falloff only where the falloff sits in
|
||||||
|
// the coastal window (≈ the barely-land / barely-sea margin, around the whole perimeter). It
|
||||||
|
// self-targets thin necks: the cells closest to the sea threshold flip first, so lobes pinch off
|
||||||
|
// into islands while the interior — window weight exactly zero — is bit-identical by
|
||||||
|
// construction. Nothing is detected, nothing is stamped. → CoastalFragment.
|
||||||
|
|
||||||
|
/// <summary>⭐ THE SWEPT AXIS. 0 = off (bit-identical everywhere). Peak |Δfalloff| (pre-power) at the window's centre.</summary>
|
||||||
|
public float FragmentAmp = 0f;
|
||||||
|
|
||||||
|
/// <summary>The fragmentation noise's frequency, periods per map width — the neck/lobe scale. The secondary dial (fixed this round). → <see cref="CoastalFragment.DefaultFreqPerMapWidth"/>.</summary>
|
||||||
|
public float FragmentFreqPerMapWidth = CoastalFragment.DefaultFreqPerMapWidth;
|
||||||
|
|
||||||
|
/// <summary>The coastal window's centre and half-width in PRE-power falloff units. Weight 1 at the centre, smooth to 0 at ± half-width; exactly 0 beyond.</summary>
|
||||||
|
public float FragmentBandCentre = CoastalFragment.DefaultBandCentre;
|
||||||
|
public float FragmentBandHalfWidth = CoastalFragment.DefaultBandHalfWidth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// false (default) ⇒ zero-mean noise: the margin is redrawn — bites AND builds (which can also
|
||||||
|
/// bridge an island back onto the mainland). true ⇒ bites only ((noise+1)/2 ≥ 0): land can only
|
||||||
|
/// recede, necks are cut, nothing is bridged, the coast net-recedes. → <see cref="CoastalFragment"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool FragmentBitesOnly = CoastalFragment.DefaultBitesOnly;
|
||||||
|
|
||||||
/// <summary>A short label for this variant, used in output filenames. E.g. "full", "base_only".</summary>
|
/// <summary>A short label for this variant, used in output filenames. E.g. "full", "base_only".</summary>
|
||||||
public string VariantLabel = "full";
|
public string VariantLabel = "full";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,13 @@ namespace IslaApocalypse.Tools
|
||||||
float hMax = float.MinValue;
|
float hMax = float.MinValue;
|
||||||
float hMin = float.MaxValue;
|
float hMin = float.MaxValue;
|
||||||
|
|
||||||
|
// ═══ COASTAL FRAGMENTATION (chat2/09) — its own deterministic field, precomputed ═══
|
||||||
|
bool fragOn = cfg.FragmentAmp > 0f && cfg.IslandFalloff;
|
||||||
|
FastNoiseLite fragNoise = fragOn
|
||||||
|
? TerrainNoise.CreateModulation(cfg.Seed, CoastalFragment.SeedOffset, cfg.FragmentFreqPerMapWidth, scale)
|
||||||
|
: null;
|
||||||
|
float fragOffset = scale.OffsetInMapWidths(CoastalFragment.OffsetMapWidths); // D-059: an offset in MAP WIDTHS, never raw pixels
|
||||||
|
|
||||||
// ═══ THE SOUTHERN STRETCH (chat2/08) — band constants, precomputed ═══
|
// ═══ THE SOUTHERN STRETCH (chat2/08) — band constants, precomputed ═══
|
||||||
bool stretchOn = cfg.SouthStretch > 0f;
|
bool stretchOn = cfg.SouthStretch > 0f;
|
||||||
float bandStart = cfg.SouthBandStartFrac * mapSize;
|
float bandStart = cfg.SouthBandStartFrac * mapSize;
|
||||||
|
|
@ -243,6 +250,23 @@ namespace IslaApocalypse.Tools
|
||||||
finalFalloff += southDepth * SouthSinkAmount;
|
finalFalloff += southDepth * SouthSinkAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══ COASTAL FRAGMENTATION (chat2/09) — in the coastal window only, pre-power ═══
|
||||||
|
//
|
||||||
|
// window(falloff) is exactly zero where the falloff is clear of the coastal margin,
|
||||||
|
// so the interior never sees this term (bit-identical by construction); inside the
|
||||||
|
// window a zero-mean noise bites or builds the margin, and the thinnest necks — the
|
||||||
|
// cells nearest the sea threshold — flip first. → CoastalFragment.
|
||||||
|
if (fragOn)
|
||||||
|
{
|
||||||
|
float w = CoastalFragment.Window(finalFalloff, cfg.FragmentBandCentre, cfg.FragmentBandHalfWidth);
|
||||||
|
if (w > 0f)
|
||||||
|
{
|
||||||
|
float nz = fragNoise.GetNoise2D(x + fragOffset, y + fragOffset); // [-1, 1]
|
||||||
|
if (cfg.FragmentBitesOnly) nz = (nz + 1f) * 0.5f; // [0, 1] — bites only
|
||||||
|
finalFalloff += cfg.FragmentAmp * w * nz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══ ⭐ THE PHASE-2 SEAM — captured BEFORE the power and BEFORE the Trench ═══
|
// ═══ ⭐ THE PHASE-2 SEAM — captured BEFORE the power and BEFORE the Trench ═══
|
||||||
// (ref ~:591). See Pass1Result.PreTrenchFalloff for why this exact point.
|
// (ref ~:591). See Pass1Result.PreTrenchFalloff for why this exact point.
|
||||||
preTrenchFalloff[x, y] = finalFalloff;
|
preTrenchFalloff[x, y] = finalFalloff;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue