diff --git a/Tools/README.md b/Tools/README.md
index 0608121..94c3b77 100644
--- a/Tools/README.md
+++ b/Tools/README.md
@@ -31,11 +31,12 @@ constants, carried over verbatim — not re-derived from a design summary** (→
| `Scripts/TerrainNoise.cs` | ⭐ The FastNoiseLite config, **every fractal property pinned explicitly** |
| `Scripts/Topography.cs` | ⭐⭐ Pass 1 — the six elements, in the reference's execution order |
| `Scripts/IslandFalloff.cs` | The whole reference file now: `SmoothAbs`, the **coast shelf**, the **offshore islets** + the reshape helpers |
-| `Scripts/OffshorePass.cs` | ⭐ **Pass 1b** — shelf + islets over the finished pass-1 arrays, then `HMaxSeed` is retaken (chat2/05) |
-| `Scripts/OffshoreSettings.cs` | Every islet dial in one object; `Faithful()` (the reference) and `Hybrid()` (the reshape) |
-| `Scripts/OffshoreAnalysis.cs` | Island components, N/S counts, the moat check, **the hemisphere convention** |
+| `Scripts/OffshorePass.cs` | ⭐ **Pass 1b** — shelf + the **organic islet layer** (the one island mechanism) + the slop guards, over the finished pass-1 arrays, then `HMaxSeed` is retaken (chat2/05, retuned chat2/06) |
+| `Scripts/OffshoreSettings.cs` | Every islet dial in one object; `Faithful()` (the reference) and `Organic()` (the reshape, tuned: **density** + **south weight** + guards). ⚠ No floor / stamps / count guarantee — chat2/05's `Hybrid()` was reverted out in chat2/06 (git history has it) |
+| `Scripts/OffshoreAnalysis.cs` | Island components, N/S counts, the moat check, the separation-guard geometry, **the hemisphere convention** |
+| `Scripts/OffshoreDiagnosis.cs` | The per-hemisphere **measurement** — valid-zone area, binding gate, noise peaks over threshold (chat2/06 §2) |
| `Scripts/TagOverlayRenderer.cs` | The island-tag / hemisphere debug overlay |
-| `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The chat2/05 batch |
+| `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/TerrainGenConfig.cs` | Config + the per-element ablation toggles |
| `Scripts/HeightField.cs` | Raw `.f32` save/load — **the generation/presentation seam** |
@@ -207,8 +208,11 @@ loop with the submarine **coast shelf** and the **offshore islet** layer (~:621-
second sweep over the finished arrays — `OffshorePass` — with the same per-pixel arithmetic in the same
order, then **retakes `HMaxSeed` after them**, as the reference did (chat2/00 Drift §2, closed). The
islets exist in two presets: `OffshoreSettings.Faithful()` (the reference, verbatim — the control) and
-`OffshoreSettings.Hybrid()` (the reshape: small / low / flat / rigid, a seeded floor of ≥2 N / ≥4 S
-islands whose positions vary per seed, organic extras weighted south, corners allowed).
+`OffshoreSettings.Organic()` (the reshape: small / low / flat / crisp, corners allowed — **the organic
+noise-field layer is the only island mechanism**, tuned for coverage by **density** and a **south
+weight**, with speck / separation / blob guards; chat2/06). ⚠ **No forced count.** chat2/05's seeded
+floor (`Hybrid()`: stamps guaranteeing ≥2 N / ≥4 S) was tried and **reverted out on look** in chat2/06;
+the per-hemisphere counts are a statistical outcome of the tuning, read off the batch's count table.
> ### ⚠ Both default OFF — deliberately, and that is a decision to revisit.
>
diff --git a/Tools/Scripts/IslandFalloff.cs b/Tools/Scripts/IslandFalloff.cs
index e195084..0d4a802 100644
--- a/Tools/Scripts/IslandFalloff.cs
+++ b/Tools/Scripts/IslandFalloff.cs
@@ -10,7 +10,7 @@ namespace IslaApocalypse.Tools
/// 2. the submarine COAST SHELF () — chat2/05 stage 1
/// 3. the OFFSHORE ISLET layer (,
/// , ) — chat2/05 stage 1
- /// + the RESHAPE helpers (, ) — stage 2
+ /// + the RESHAPE helper () — stage 2
///
/// The faithful functions keep the reference's names, constants and arithmetic verbatim (D-050);
/// the parameterized overloads beside them exist so the reshape can move a dial without touching
@@ -222,7 +222,8 @@ namespace IslaApocalypse.Tools
}
// ═══════════════════════════════════════════════════════════════════════
- // 3b. THE RESHAPE HELPERS — chat2/05 stage 2. Not in the reference.
+ // 3b. THE RESHAPE HELPER — chat2/05 stage 2. Not in the reference. (The seeded-floor
+ // stamp that sat beside it was reverted out in chat2/06 — git history has it.)
// ═══════════════════════════════════════════════════════════════════════
///
@@ -238,20 +239,5 @@ namespace IslaApocalypse.Tools
if (w <= 0f || edgeSharpness <= 1f) return w;
return 1f - MathF.Pow(1f - w, edgeSharpness);
}
-
- ///
- /// The seeded-floor stamp: a flat disc of radius radius · coreFraction at full weight,
- /// then a smoothstep rim down to zero at . Higher
- /// ⇒ flatter top, crisper shore. This is the "rigid, defined
- /// coastline" the reshape asks for, in its simplest honest form.
- ///
- public static float StampWeight(float dist, float radius, float coreFraction)
- {
- if (dist >= radius) return 0f;
- float core = radius * coreFraction;
- if (dist <= core) return 1f;
- float t = (dist - core) / (radius - core);
- return 1f - t * t * (3f - 2f * t);
- }
}
}
diff --git a/Tools/Scripts/OffshoreAnalysis.cs b/Tools/Scripts/OffshoreAnalysis.cs
index 07be0e7..79e3de4 100644
--- a/Tools/Scripts/OffshoreAnalysis.cs
+++ b/Tools/Scripts/OffshoreAnalysis.cs
@@ -173,5 +173,64 @@ namespace IslaApocalypse.Tools
foreach (var c in comps) if (c.BridgedToMainland) b++;
return b;
}
+
+ // ═══ chat2/06 — the separation guard's geometry ═══
+
+ ///
+ /// Per component id, its BOUNDARY cells — tagged cells with at least one 8-neighbour that is
+ /// not tagged (or the map edge). The nearest approach between two islands is between
+ /// boundary cells, so the separation guard compares boundaries, not bodies: a few hundred
+ /// cells per island instead of thousands. is the pass's list of
+ /// every surfaced cell (the bodies), walked once.
+ ///
+ public static Dictionary> BoundaryCells(bool[,] tag, int[] compId, int mapSize,
+ IEnumerable<(int x, int y, float h0)> surfaced)
+ {
+ var result = new Dictionary>();
+ foreach (var (x, y, _) in surfaced)
+ {
+ if (!tag[x, y]) continue;
+ int id = compId[x * mapSize + y];
+ if (id == 0) continue;
+ bool edge = false;
+ for (int k = 0; k < 8 && !edge; k++)
+ {
+ int nx = x + DX[k], ny = y + DY[k];
+ if (nx < 0 || nx >= mapSize || ny < 0 || ny >= mapSize || !tag[nx, ny]) edge = true;
+ }
+ if (!edge) continue;
+ if (!result.TryGetValue(id, out var list)) result[id] = list = new List<(int, int)>();
+ list.Add((x, y));
+ }
+ return result;
+ }
+
+ /// Chebyshev gap between two components' bounding boxes (0 if they overlap). A lower bound on their true distance.
+ public static int BoxGap(IslandComponent a, IslandComponent b)
+ {
+ int gx = Math.Max(0, Math.Max(a.MinX - b.MaxX, b.MinX - a.MaxX));
+ int gy = Math.Max(0, Math.Max(a.MinY - b.MaxY, b.MinY - a.MaxY));
+ return Math.Max(gx, gy);
+ }
+
+ ///
+ /// The minimum Chebyshev distance between two boundary sets, early-exiting once it is
+ /// known to be below (the caller only needs "closer than the
+ /// minimum or not").
+ ///
+ public static int MinChebyshev(List<(int x, int y)> a, List<(int x, int y)> b, int below)
+ {
+ int best = int.MaxValue;
+ if (a == null || b == null) return best;
+ foreach (var (ax, ay) in a)
+ {
+ foreach (var (bx, by) in b)
+ {
+ int d = Math.Max(Math.Abs(ax - bx), Math.Abs(ay - by));
+ if (d < best) { best = d; if (best < below) return best; }
+ }
+ }
+ return best;
+ }
}
}
diff --git a/Tools/Scripts/OffshoreDiagnosis.cs b/Tools/Scripts/OffshoreDiagnosis.cs
new file mode 100644
index 0000000..9d02197
--- /dev/null
+++ b/Tools/Scripts/OffshoreDiagnosis.cs
@@ -0,0 +1,231 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Godot;
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ /// One hemisphere's share of the offshore zone, the gates, and the noise peaks.
+ public sealed class HemisphereDiagnosis
+ {
+ public string Name;
+
+ // ---- the valid offshore zone ----
+ public long Sea; // below-sea cells in this hemisphere (after the shelf)
+ public long Zone; // cells with zone weight > 0 (island-eligible)
+ public long ZoneFull; // cells with zone weight == 1 (clear of every feather)
+
+ // ---- which gate blocks (non-exclusive: a cell may fail several) ----
+ public long BlockDepth; // ambient depth < moat
+ public long BlockFalloff; // pre-Trench falloff < min (not "actually offshore")
+ public long BlockTrench; // at/past the outer bound
+ // ---- the SOLE blocker (a cell that fails exactly one gate — loosen that gate and it joins the zone) ----
+ public long SoleDepth, SoleFalloff, SoleTrench;
+
+ // ---- band geometry: per column, how many rows are ocean / zone in this hemisphere ----
+ public double OceanRowsPerColumn, ZoneRowsPerColumn;
+ public int ColumnsWithNoZone;
+
+ // ---- the noise peaks (strict 8-neighbour local maxima of the islet field, on sea cells) ----
+ public int PeaksSea; // all maxima over sea
+ public int PeaksInZone; // maxima inside the zone — the CAPACITY for islands at this frequency
+ public int PeaksInZoneOverThr; // inside the zone AND clearing the threshold — the island candidates
+ public int PeaksOverThrLost; // clearing the threshold but OUTSIDE the zone — killed by a gate:
+ public int LostDepth, LostFalloff, LostTrench; // …which one(s) (non-exclusive)
+
+ public double ZoneShareOfSea => Sea == 0 ? 0 : (double)Zone / Sea;
+ public double CandidatesPerMegacell => Zone == 0 ? 0 : PeaksInZoneOverThr * 1e6 / Zone;
+ }
+
+ ///
+ /// ⭐ THE SOUTH-SUPPRESSION DIAGNOSIS (chat2/06 §2) — MEASURE, DON'T GUESS. Before a knob moves,
+ /// answer per hemisphere: how much island-eligible ocean is there, how many noise peaks clear the
+ /// threshold in it, and which gate is the binding one. Read-only: it evaluates the islet noise
+ /// field and the zone mask exactly as does (same noise factory, same
+ /// calibration samples, same blended threshold, same gate arithmetic) over the post-shelf,
+ /// pre-islet field, and counts. It raises nothing.
+ ///
+ /// Hemisphere is the row midline (), as the tag.
+ ///
+ public static class OffshoreDiagnosis
+ {
+ public sealed class Report
+ {
+ public int Seed, MapSize;
+ public float ThresholdNorth, ThresholdSouth;
+ public HemisphereDiagnosis North = new() { Name = "north" };
+ public HemisphereDiagnosis South = new() { Name = "south" };
+ public HemisphereDiagnosis Of(byte hemi) => hemi == OffshoreAnalysis.HemiNorth ? North : South;
+ }
+
+ /// The post-shelf, pre-islet field (offshore OFF, shelf as the batch runs it).
+ public static Report Run(float[,] height, float[,] preTrench, int mapSize, int seed, float sea,
+ OffshoreSettings s, GenerationScale scale)
+ {
+ var rep = new Report { Seed = seed, MapSize = mapSize };
+ FastNoiseLite noise = TerrainNoise.CreateModulation(seed, s.SeedOffset, s.FreqPerMapWidth, scale);
+ float[] samples = OffshorePass.CalibrationSamples(noise, mapSize);
+ float thrN = IslandFalloff.CalibrateThreshold(samples, s.Density);
+ float thrS = s.Mode == OffshoreMode.Faithful ? thrN : IslandFalloff.CalibrateThreshold(samples, s.DensitySouth);
+ rep.ThresholdNorth = thrN; rep.ThresholdSouth = thrS;
+
+ float centerX = mapSize / 2.0f, centerY = mapSize / 2.0f, halfSpan = mapSize / 2.0f;
+ float mid = mapSize * 0.5f;
+ float band = MathF.Max(1f, s.HemisphereBlendHalfWidth * mapSize);
+
+ // The islet field over the whole map (a peak's neighbours may be land or out of zone).
+ var v = new float[mapSize, mapSize];
+ for (int x = 0; x < mapSize; x++)
+ for (int y = 0; y < mapSize; y++)
+ v[x, y] = (noise.GetNoise2D(x, y) + 1f) * 0.5f;
+
+ // Zone weight per cell, and the gate ledger. -1 = land.
+ var zone = new float[mapSize, mapSize];
+ int half = mapSize / 2;
+ long[] oceanRows = new long[2], zoneRows = new long[2];
+ int[] colsNoZone = new int[2];
+ for (int x = 0; x < mapSize; x++)
+ {
+ long[] colZone = new long[2];
+ for (int y = 0; y < mapSize; y++)
+ {
+ float h = height[x, y];
+ if (h >= sea) { zone[x, y] = -1f; continue; }
+ int hi = y < half ? 0 : 1;
+ var d = hi == 0 ? rep.North : rep.South;
+ d.Sea++; oceanRows[hi]++;
+
+ float depthM = WorldScale.MetresFromRaw(sea - h);
+ float dist = MathF.Max(MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan);
+ bool bDepth = depthM < s.MinDepthM;
+ bool bFall = preTrench[x, y] < s.MinFalloff;
+ bool bTr = dist >= s.TrenchOuter;
+ if (bDepth) d.BlockDepth++;
+ if (bFall) d.BlockFalloff++;
+ if (bTr) d.BlockTrench++;
+ int fails = (bDepth ? 1 : 0) + (bFall ? 1 : 0) + (bTr ? 1 : 0);
+ if (fails == 1)
+ {
+ if (bDepth) d.SoleDepth++; else if (bFall) d.SoleFalloff++; else d.SoleTrench++;
+ }
+
+ float z = IslandFalloff.OffshoreZoneWeight(depthM, preTrench[x, y],
+ MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan,
+ s.MinDepthM, s.DepthFeatherM, s.MinFalloff, s.FalloffFeather, s.TrenchInner, s.TrenchOuter);
+ zone[x, y] = z;
+ if (z > 0f) { d.Zone++; zoneRows[hi]++; colZone[hi]++; }
+ if (z >= 1f) d.ZoneFull++;
+ }
+ for (int hi = 0; hi < 2; hi++) if (colZone[hi] == 0) colsNoZone[hi]++;
+ }
+ rep.North.OceanRowsPerColumn = oceanRows[0] / (double)mapSize;
+ rep.South.OceanRowsPerColumn = oceanRows[1] / (double)mapSize;
+ rep.North.ZoneRowsPerColumn = zoneRows[0] / (double)mapSize;
+ rep.South.ZoneRowsPerColumn = zoneRows[1] / (double)mapSize;
+ rep.North.ColumnsWithNoZone = colsNoZone[0];
+ rep.South.ColumnsWithNoZone = colsNoZone[1];
+
+ // Peaks: strict local maxima of v over the 8-neighbourhood, on sea cells.
+ for (int x = 0; x < mapSize; x++)
+ {
+ for (int y = 0; y < mapSize; y++)
+ {
+ if (zone[x, y] < 0f) continue; // land
+ float c = v[x, y];
+ bool isMax = true;
+ for (int dx = -1; dx <= 1 && isMax; dx++)
+ {
+ int nx = x + dx; if (nx < 0 || nx >= mapSize) continue;
+ for (int dy = -1; dy <= 1; dy++)
+ {
+ if (dx == 0 && dy == 0) continue;
+ int ny = y + dy; if (ny < 0 || ny >= mapSize) continue;
+ if (v[nx, ny] >= c) { isMax = false; break; }
+ }
+ }
+ if (!isMax) continue;
+
+ var d = y < half ? rep.North : rep.South;
+ d.PeaksSea++;
+ float thr = s.Mode == OffshoreMode.Faithful ? thrN : OffshorePass.BlendedThreshold(y, mid, band, thrN, thrS);
+ bool over = c > thr;
+ if (zone[x, y] > 0f)
+ {
+ d.PeaksInZone++;
+ if (over) d.PeaksInZoneOverThr++;
+ }
+ else if (over)
+ {
+ d.PeaksOverThrLost++;
+ float h = height[x, y];
+ float depthM = WorldScale.MetresFromRaw(sea - h);
+ float dist = MathF.Max(MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan);
+ if (depthM < s.MinDepthM) d.LostDepth++;
+ if (preTrench[x, y] < s.MinFalloff) d.LostFalloff++;
+ if (dist >= s.TrenchOuter) d.LostTrench++;
+ }
+ }
+ }
+ return rep;
+ }
+
+ /// One markdown row per hemisphere for a report table (see ).
+ public static string TableHeader() =>
+ "| seed | hemi | sea cells | zone cells | zone / sea | zone == 1 | ocean rows/col | zone rows/col | cols w/o zone | " +
+ "blocked: depth / falloff / trench | sole blocker: depth / falloff / trench | peaks: sea / in zone / in zone > thr | lost > thr (depth / falloff / trench) | candidates per Mcell |\n" +
+ "|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
+
+ public static string TableRow(Report r, HemisphereDiagnosis d) =>
+ $"| `{r.Seed}` | **{d.Name}** | {d.Sea:N0} | {d.Zone:N0} | {d.ZoneShareOfSea:P1} | {d.ZoneFull:N0} | {d.OceanRowsPerColumn:F0} | {d.ZoneRowsPerColumn:F0} | {d.ColumnsWithNoZone} | " +
+ $"{d.BlockDepth:N0} / {d.BlockFalloff:N0} / {d.BlockTrench:N0} | {d.SoleDepth:N0} / {d.SoleFalloff:N0} / {d.SoleTrench:N0} | " +
+ $"{d.PeaksSea} / {d.PeaksInZone} / **{d.PeaksInZoneOverThr}** | {d.PeaksOverThrLost} ({d.LostDepth} / {d.LostFalloff} / {d.LostTrench}) | {d.CandidatesPerMegacell:F1} |";
+
+ /// Sum a pool of reports per hemisphere (means for the per-column numbers).
+ public static (HemisphereDiagnosis north, HemisphereDiagnosis south) Pool(IReadOnlyList reports)
+ {
+ var n = new HemisphereDiagnosis { Name = "north (pool)" };
+ var s = new HemisphereDiagnosis { Name = "south (pool)" };
+ foreach (var r in reports) { Add(n, r.North); Add(s, r.South); }
+ int k = Math.Max(1, reports.Count);
+ n.OceanRowsPerColumn /= k; s.OceanRowsPerColumn /= k;
+ n.ZoneRowsPerColumn /= k; s.ZoneRowsPerColumn /= k;
+ n.ColumnsWithNoZone /= k; s.ColumnsWithNoZone /= k;
+ return (n, s);
+ }
+
+ private static void Add(HemisphereDiagnosis a, HemisphereDiagnosis b)
+ {
+ a.Sea += b.Sea; a.Zone += b.Zone; a.ZoneFull += b.ZoneFull;
+ a.BlockDepth += b.BlockDepth; a.BlockFalloff += b.BlockFalloff; a.BlockTrench += b.BlockTrench;
+ a.SoleDepth += b.SoleDepth; a.SoleFalloff += b.SoleFalloff; a.SoleTrench += b.SoleTrench;
+ a.OceanRowsPerColumn += b.OceanRowsPerColumn; a.ZoneRowsPerColumn += b.ZoneRowsPerColumn; a.ColumnsWithNoZone += b.ColumnsWithNoZone;
+ a.PeaksSea += b.PeaksSea; a.PeaksInZone += b.PeaksInZone; a.PeaksInZoneOverThr += b.PeaksInZoneOverThr;
+ a.PeaksOverThrLost += b.PeaksOverThrLost; a.LostDepth += b.LostDepth; a.LostFalloff += b.LostFalloff; a.LostTrench += b.LostTrench;
+ }
+
+ /// A one-paragraph reading of the pooled numbers: which hemisphere has less eligible ocean, and which gate binds it.
+ public static string Interpret(HemisphereDiagnosis n, HemisphereDiagnosis s)
+ {
+ var sb = new StringBuilder();
+ string smaller = n.Zone < s.Zone ? "NORTH" : "SOUTH";
+ double ratio = n.Zone == 0 || s.Zone == 0 ? 0 : (double)Math.Max(n.Zone, s.Zone) / Math.Min(n.Zone, s.Zone);
+ sb.Append($"Valid offshore zone: north {n.Zone:N0} cells ({n.ZoneShareOfSea:P1} of its ocean, {n.ZoneRowsPerColumn:F0} rows/col), " +
+ $"south {s.Zone:N0} cells ({s.ZoneShareOfSea:P1} of its ocean, {s.ZoneRowsPerColumn:F0} rows/col) — the {smaller} has " +
+ $"{ratio:F2}× less island-eligible ocean. ");
+ sb.Append($"Island candidates (peaks in zone clearing the threshold): north {n.PeaksInZoneOverThr}, south {s.PeaksInZoneOverThr}; " +
+ $"capacity (all peaks in zone): north {n.PeaksInZone}, south {s.PeaksInZone}. ");
+ string Bind(HemisphereDiagnosis d)
+ {
+ long max = Math.Max(d.SoleDepth, Math.Max(d.SoleFalloff, d.SoleTrench));
+ string g = max == d.SoleFalloff ? "the falloff test" : max == d.SoleDepth ? "the moat (ambient depth)" : "the outer/trench bound";
+ long lostMax = Math.Max(d.LostDepth, Math.Max(d.LostFalloff, d.LostTrench));
+ string lg = d.PeaksOverThrLost == 0 ? "none" : lostMax == d.LostFalloff ? "falloff" : lostMax == d.LostDepth ? "moat" : "outer bound";
+ return $"{d.Name}: binding gate by sole-blocked cells = {g} (depth {d.SoleDepth:N0} / falloff {d.SoleFalloff:N0} / trench {d.SoleTrench:N0}); " +
+ $"over-threshold peaks lost to gates = {d.PeaksOverThrLost} (mostly {lg})";
+ }
+ sb.Append(Bind(n)).Append(". ").Append(Bind(s)).Append('.');
+ return sb.ToString();
+ }
+ }
+}
diff --git a/Tools/Scripts/OffshoreDiagnosis.cs.uid b/Tools/Scripts/OffshoreDiagnosis.cs.uid
new file mode 100644
index 0000000..40731a6
--- /dev/null
+++ b/Tools/Scripts/OffshoreDiagnosis.cs.uid
@@ -0,0 +1 @@
+uid://mriontjn1ihq
diff --git a/Tools/Scripts/OffshoreIslandsTool.cs b/Tools/Scripts/OffshoreIslandsTool.cs
index dac74fb..f84c755 100644
--- a/Tools/Scripts/OffshoreIslandsTool.cs
+++ b/Tools/Scripts/OffshoreIslandsTool.cs
@@ -8,53 +8,75 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
- /// ⭐ THE OFFSHORE-ISLANDS BATCH (chat2/05) — the faithful rejoin as the control, then the
- /// reshape: small / low / flat / rigid, loose-guaranteed (≥2 N, ≥4 S), organic extras weighted
- /// south, corners on. Judged on 2D maps, with the tag made visible.
+ /// ⭐ 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.)
///
- /// ═══ THE VARIANTS ═══
+ /// ═══ WHAT IT PRODUCES — a fixed budget: 4 plates + a count table + a diagnosis ═══
///
- /// faithful stage 1 — the reference's probabilistic islets, verbatim. Sparse, no corners,
- /// no floor. THE CONTROL.
- /// floor_only the reshape's seeded ≥2N/4S floor with the organic layer OFF — shows the
- /// guaranteed minimum and that positions vary per seed.
- /// hybrid ⭐ floor + organic extras, reshaped, corners on. THE DELIVERABLE. Six seeds.
- /// dense hybrid with ×3 organic density — a bookend for "how many is too many".
+ /// 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 shelf is ON for every variant (it is stage 1's other half and is invisible on these
- /// plates anyway); an offshore-OFF / shelf-OFF field is generated per seed for the oracle only.
+ /// 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). → .
///
/// ═══ 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. Their crest (24–34 m pre-curve) lands inside the curve's preserved toe, which squashes
- /// it to a few metres; because that toe is bit-preserved across curve tweaks, the islands' height
- /// is stable however the upper climb is tuned later.
+ /// 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_MAPSIZE render size (default 4096 — islands need pixels to read)
- /// ISLA_CALIB_SIZE curve calibration size (default 2048, task 01's)
- /// ISLA_SEEDS_HYBRID the six hybrid seeds
- /// ISLA_SEEDS_SMALL the three seeds for faithful / floor_only / dense
- /// ISLA_PHASE1_SOURCE Phase-1 .f32 batch (default "02_pass1_port")
- /// ISLA_T03_SOURCE task-03 .f32 batch (default "03_mountain_restore")
+ /// 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
///
public partial class OffshoreIslandsTool : Node
{
- private static readonly int[] DefaultHybridSeeds = { 1063685222, 20260821, 8675309, 123456789, 271828182, 999999937 };
- private static readonly int[] DefaultSmallSeeds = { 1063685222, 8675309, 999999937 };
+ ///
+ /// 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.
+ ///
+ private static readonly int[] DefaultTableSeeds =
+ {
+ 1063685222, 20260821, 8675309, 123456789, 271828182, 999999937,
+ 20260819, 777001, 424242, 90210, 31337, 55555,
+ };
- /// ⚠ Task 01's pool, verbatim — the curve's identity.
+ /// ⚠ Task 01's pool, verbatim — the curve's identity. Also the diagnosis pool.
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
+
+ /// The consistency targets the table is read against: "a couple north, a few south".
+ private const int TargetNorth = 2, TargetSouth = 3;
public override void _Ready()
{
@@ -63,6 +85,7 @@ namespace IslaApocalypse.Tools
{
GD.PrintErr("==================================================================");
GD.PrintErr($" REFUSED: {e.Message}");
+ GD.PrintErr(e.StackTrace);
GD.PrintErr("==================================================================");
GetTree().Quit(2);
}
@@ -70,26 +93,36 @@ namespace IslaApocalypse.Tools
private sealed class Row
{
- public string Variant; public int Seed;
- public int CountN, CountS; public long Lifted; public float HMaxBefore, HMaxAfter;
- public bool Ok;
- public string Centres; // the seeded floor's (x,y) list — the "positions vary per seed" evidence
+ 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", 5);
- string descr = EnvStr("ISLA_BATCH", "offshore_islands");
+ 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[] hybridSeeds = EnvSeeds("ISLA_SEEDS_HYBRID", DefaultHybridSeeds);
- int[] smallSeeds = EnvSeeds("ISLA_SEEDS_SMALL", DefaultSmallSeeds);
+ 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";
- string only = EnvStr("ISLA_ONLY", null); // probe: comma list of variant labels to run
+ 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);
@@ -97,39 +130,63 @@ namespace IslaApocalypse.Tools
var anchors = CurveAnchors.Default;
float sea = 0.15f;
- int primary = hybridSeeds[0];
+
+ // ═══ 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
+ {
+ 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/05) — faithful rejoin, then the reshape");
+ GD.Print(" OFFSHORE ISLANDS (chat2/06) — organic-only, tuned for coverage, south-weighted, no forced count");
GD.Print("==================================================================");
- GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize} (offshore OFF)");
- GD.Print($"hybrid : {string.Join(", ", hybridSeeds)}");
- GD.Print($"others : {string.Join(", ", smallSeeds)}");
+ 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}");
+ 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);
+ var (knots, calibration, _) = CalibrateCurve(calibSize, sea, anchors);
GD.Print($" {knots}");
GD.Print($" {calibration.Describe()}");
- // ═══ 1. REGRESSIONS at the calibration size — the things that must not have moved ═══
- GD.Print($"\n--- 1. REGRESSIONS at {calibSize}, seed {primary} ---");
+ // ═══ 1. REGRESSIONS — the things that must not have moved ═══
var hard = new List();
+ if (!tableOnly)
{
- var offCfg = BaseConfig(calibSize, primary, knots, anchors, calibration, "off");
+ 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, $"{primary}_full", "height.f32");
+ 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, $"{primary}_continuous_restored", "height.f32");
+ 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));
@@ -140,103 +197,132 @@ namespace IslaApocalypse.Tools
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);
+ foreach (var c in hard) GD.Print(" " + c);
- // ═══ 2. VARIANTS ═══
- // ⚠ PROBE OVERRIDES on the reshape's organic knobs, so the hybrid can be swept in scratch
- // without a rebuild. The defaults live in OffshoreSettings.Hybrid() — that is the
- // preset of record; these move it only when set. Printed into the INDEX either way.
- OffshoreSettings HybridTuned()
- {
- var h = OffshoreSettings.Hybrid();
- h.FreqPerMapWidth = EnvFloat("ISLA_OFF_FREQ", h.FreqPerMapWidth);
- h.DensityNorth = EnvFloat("ISLA_OFF_DENS_N", h.DensityNorth);
- h.DensitySouth = EnvFloat("ISLA_OFF_DENS_S", h.DensitySouth);
- h.CoreFraction = EnvFloat("ISLA_OFF_CORE", h.CoreFraction);
- h.EdgeSharpness = EnvFloat("ISLA_OFF_SHARP", h.EdgeSharpness);
- h.MinIslandAreaFrac = EnvFloat("ISLA_OFF_MINAREA", h.MinIslandAreaFrac);
- h.CrestM = EnvFloat("ISLA_OFF_CREST", h.CrestM);
- return h;
- }
-
- var variants = new List<(string label, int[] seeds, Action mutate)>
- {
- ("faithful", smallSeeds, c => { c.CoastShelf = true; c.Offshore = OffshoreSettings.Faithful(); }),
- ("floor_only", smallSeeds, c => { c.CoastShelf = true; c.Offshore = HybridTuned(); c.Offshore.Organic = false; }),
- ("hybrid", hybridSeeds, c => { c.CoastShelf = true; c.Offshore = HybridTuned(); }),
- ("dense", smallSeeds, c => { c.CoastShelf = true; c.Offshore = HybridTuned();
- c.Offshore.DensityNorth *= 3f; c.Offshore.DensitySouth *= 3f; }),
- };
- if (!string.IsNullOrWhiteSpace(only))
- {
- var keep = new HashSet(only.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
- variants.RemoveAll(v => !keep.Contains(v.label));
- GD.Print($" ⚠ ISLA_ONLY: running {string.Join(", ", keep)} only (a probe, not the batch of record)");
- }
-
- GD.Print($"\n--- 2. VARIANTS at {mapSize} ---");
- var offFields = new Dictionary(); // per seed, shelf off + offshore off
- var rows = new List();
- var perVariantChecks = new List();
- var knobs = new Dictionary();
- bool notesShown = false;
-
- foreach (var (label, seeds, mutate) in variants)
- {
- foreach (int seed in seeds)
+ // ⭐ 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)
{
- if (!offFields.TryGetValue(seed, out Pass1Result p1Off))
+ string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32");
+ if (File.Exists(t04Dump))
{
- p1Off = Topography.Generate(BaseConfig(mapSize, seed, knots, anchors, calibration, "off"));
- offFields[seed] = p1Off;
+ 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)");
+ }
- var cfg = BaseConfig(mapSize, seed, knots, anchors, calibration, label);
- mutate(cfg);
- knobs[label] = cfg.Offshore.Describe();
+ // ═══ 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();
+ 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();
+ var perFieldChecks = new List();
+ 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; }
- if (!notesShown || label == "hybrid" && seed == primary)
- foreach (string n in p1.Notes) GD.Print(" " + n);
- notesShown = true;
-
- // ---- oracle, per field ----
- var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, mapSize);
+ var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, tableSize);
var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps);
- var checks = new List();
- if (cfg.Offshore.FloorNorth > 0 || cfg.Offshore.FloorSouth > 0)
- checks.Add(ShapingOracle.OffshoreFloor(p1, cfg.Offshore.FloorNorth, cfg.Offshore.FloorSouth, sea, comps));
- checks.Add(ShapingOracle.MoatIntact(p1, comps));
- checks.Add(ShapingOracle.MainlandUnmoved(p1Off, p1, sea));
- checks.Add(ShapingOracle.TagCoastlineConsistent(p2, sea));
- checks.Add(ShapingOracle.HMaxAfterOffshore(p1));
- checks.Add(ShapingOracle.ClassifyFidelity(p1, p2));
- foreach (var c in checks) { c.Name += $" [{label} {seed}]"; perVariantChecks.Add(c); }
+ var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0);
+ var checks = new List
+ {
+ 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);
- WriteVariant(batchRoot, p1, p2, sea, anchors, skipRaw, cn, cs);
-
- var cl = new List();
- foreach (var (cx, cy, _) in p1.OffshoreCentres) cl.Add($"({cx},{cy})");
- rows.Add(new Row
+ var row = new Row
{
- Variant = label, Seed = seed, CountN = cn, CountS = cs,
- Lifted = p1.OffshoreLiftedCells, HMaxBefore = p1.HMaxSeedBeforeOffshore, HMaxAfter = p1.HMaxSeed, Ok = ok,
- Centres = cl.Count == 0 ? "—" : string.Join(" ", cl),
- });
- GD.Print($" {label,-11} seed {seed,-11} islands N {cn,2} S {cs,2} lifted {p1.OffshoreLiftedCells,8:N0} " +
- $"hMax {p1.HMaxSeedBeforeOffshore:F4}→{p1.HMaxSeed:F4} {(ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms");
+ 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();
+ 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.IsOffshoreIsland, 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) && perVariantChecks.TrueForAll(c => c.Passed);
+ 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 perVariantChecks) if (!c.Passed) GD.PrintErr(" " + c);
+ foreach (var c in perFieldChecks) if (!c.Passed) GD.PrintErr(" " + c);
- WriteIndex(batchRoot, mapSize, calibSize, primary, hybridSeeds, smallSeeds, rows, knobs, hard, perVariantChecks, allOk);
+ // ═══ 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}");
@@ -245,9 +331,68 @@ namespace IslaApocalypse.Tools
GetTree().Quit(allOk ? 0 : 3);
}
- // ---- the curve, measured exactly as tasks 03/04 did --------------------
+ // ---- the table's statistics -------------------------------------------
- private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
+ 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 LevelStats(List levels, List rows)
+ {
+ var outp = new List();
+ 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();
+ 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;
+ }
+
+ /// The table seed whose SOUTH count at the preset level is lowest (ties → lower total, then first in the pool), excluding the plate seed.
+ private static int PickBulgeSeed(List 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;
+ }
+
+ /// Copy the pass's guard ledger into a table row (the pass reports it; the tool does not recompute it).
+ 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) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
{
var rawPool = new LandHistogram(sea);
var pass1 = new Dictionary();
@@ -284,7 +429,7 @@ namespace IslaApocalypse.Tools
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);
+ return (knots, cal, pass1);
}
private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
@@ -298,7 +443,7 @@ namespace IslaApocalypse.Tools
// ---- output -----------------------------------------------------------
- private static void WriteVariant(string batchRoot, Pass1Result p1, Pass2Result p2, float sea,
+ 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}");
@@ -319,51 +464,154 @@ namespace IslaApocalypse.Tools
// ⭐ The tag overlay — the one artifact that shows the DATA this pass set.
TagOverlayRenderer.SavePng(p2.Height, p2.IsOffshoreIsland, p2.IslandHemisphere, p2.MapSize, sea,
- p1.OffshoreCentres, countN, countS, Path.Combine(dir, "tags.png"));
+ countN, countS, Path.Combine(dir, "tags.png"));
}
- private static void WriteIndex(string batchRoot, int mapSize, int calibSize, int primary,
- int[] hybridSeeds, int[] smallSeeds, List rows, Dictionary knobs,
- List hard, List perVariant, bool allOk)
+ private static string CountTableMarkdown(List levels, List rows, List stats)
{
var sb = new StringBuilder();
- sb.AppendLine("# Batch 05 — offshore islands: the faithful rejoin, then the reshape");
+ 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("Stage 1 rejoins the reference's coast shelf + islets verbatim (`faithful`, the control).");
- sb.AppendLine("Stage 2 reshapes: **small / low / flat / rigid**, a **seeded floor of ≥2 N / ≥4 S** islands");
- sb.AppendLine("(min-separated, positions varying per seed, never fixed zones) plus **organic extras weighted");
- sb.AppendLine("south**, corners and the outer edge allowed. Every tagged island cell carries a hemisphere.");
+ 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 levels, List rows, List 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 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 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 levels, List rows, List stats, List plateRows,
+ List diag, HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation,
+ List hard, List 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. **`{primary}_hybrid/tags.png`** — the tag overlay: grey mainland, cyan = island N, orange = island S,");
- sb.AppendLine(" rings = the seeded floor. Count, N/S split and tag correctness in one glance.");
- sb.AppendLine($"2. **`{primary}_hybrid/relief.png`** beside **`{primary}_faithful/relief.png`** — the reshape vs the reference.");
- sb.AppendLine($"3. Then the other hybrid seeds' `tags.png` — the floor holds everywhere; the extras vary.");
+ 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})`. The spine fades southward; the southern sinker bites the bottom 25 %;");
- sb.AppendLine("snow-town north, shipwreck south. Component hemisphere is by centroid; the tag per cell is by row.");
+ sb.AppendLine($"SOUTH = rows `[{mapSize / 2}, {mapSize})`. Component hemisphere is by centroid; the tag per cell is by row.");
sb.AppendLine();
- sb.AppendLine("## ⭐ The count table — the floor, and the spread above it");
+ sb.AppendLine("## The four plates");
sb.AppendLine();
- sb.AppendLine("| Variant | Seed | N | S | total | lifted cells | HMaxSeed before → after | oracle | seeded floor centres (x,y) |");
- sb.AppendLine("|---|---|---|---|---|---|---|---|---|");
- foreach (var r in rows)
- sb.AppendLine($"| `{r.Variant}` | `{r.Seed}` | **{r.CountN}** | **{r.CountS}** | {r.CountN + r.CountS} | {r.Lifted:N0} | " +
- $"{r.HMaxBefore:F4} → {r.HMaxAfter:F4}{(r.HMaxBefore != r.HMaxAfter ? " ⚠" : "")} | {(r.Ok ? "pass" : "**FAIL**")} | {r.Centres} |");
+ 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("`floor_only` / `hybrid` / `dense` carry a floor of **≥2 N / ≥4 S**; `faithful` has no floor (the");
- sb.AppendLine("reference's probabilistic layer) and is the control.");
+ sb.AppendLine($"## ⭐ The count table — {tableSeeds.Length} seeds × 3 levels at {tableSize} (the consistency evidence)");
sb.AppendLine();
- sb.AppendLine("## The knobs per variant");
+ sb.Append(CountTableMarkdown(levels, rows, stats));
sb.AppendLine();
- sb.AppendLine("| Variant | settings |");
+ 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 kv in knobs) sb.AppendLine($"| `{kv.Key}` | {kv.Value} |");
+ 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 variant (strength 0.775, scale 100 m, BitDecrement-clamped). It is");
- sb.AppendLine("invisible on these hypsometric plates — ported faithfully, judged when water renders.");
+ 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();
@@ -371,29 +619,32 @@ namespace IslaApocalypse.Tools
sb.AppendLine();
sb.AppendLine("## The oracle");
sb.AppendLine();
- sb.AppendLine("Regressions at the calibration size:");
+ 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(ShapingOracle.ToMarkdownTable(hard));
- sb.AppendLine("Per variant × seed (floor h · moat i · mainland unmoved j · tag/coastline k · HMaxSeed l · classify b):");
+ 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(perVariant));
+ 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` | **keep** |");
+ 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($"MapSize {mapSize}, curve calibrated at {calibSize} with offshore off. {WorldScale.Describe()}.");
+ 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());
+ }
- string index = Path.Combine(batchRoot, "INDEX.md");
- using var f = Godot.FileAccess.Open(index, Godot.FileAccess.ModeFlags.Write);
- if (f == null) { GD.PrintErr($"could not write {index}"); return; }
- f.StoreString(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 --------------------------------------------------------
diff --git a/Tools/Scripts/OffshorePass.cs b/Tools/Scripts/OffshorePass.cs
index 7d95c0b..3480d7c 100644
--- a/Tools/Scripts/OffshorePass.cs
+++ b/Tools/Scripts/OffshorePass.cs
@@ -6,8 +6,9 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
- /// ⭐⭐ PASS 1b — THE COAST SHELF AND THE OFFSHORE ISLETS (chat2/05). Runs over the finished
- /// pass-1 arrays, IN PLACE, before HMaxSeed is taken and before anything classifies.
+ /// ⭐⭐ PASS 1b — THE COAST SHELF AND THE OFFSHORE ISLETS (chat2/05, retuned chat2/06). Runs over
+ /// the finished pass-1 arrays, IN PLACE, before HMaxSeed is taken and before anything
+ /// classifies.
///
/// ═══ WHERE THIS SITS, AND WHY IT IS A SECOND SWEEP ═══
///
@@ -16,30 +17,30 @@ namespace IslaApocalypse.Tools
/// sweep over the arrays the first loop produced. Per pixel the inputs are identical — the raw
/// height, the sea level, the pre-Trench falloff, (x, y) — and the operations are applied in the
/// same order on the same floats, so the FAITHFUL mode reproduces the reference cell for cell.
- /// What the second sweep buys is the seeded floor: choosing island centres needs the whole
- /// depth/falloff field to exist first, which a single pixel loop cannot provide.
+ /// The second sweep is also what lets the slop guards see whole islands: a component rule needs
+ /// the finished field.
///
/// ⚠⚠ THE ORDERING THAT CLOSES chat2/00 DRIFT §2: the caller recomputes HMaxSeed AFTER this
/// pass, as the reference did, so the curve's per-seed peak normalization sees the same maximum
/// the reference saw. Expected to be unchanged (a ~34 m crest is far below any peak) — reported,
/// not assumed.
///
- /// ═══ THE THREE SUB-PASSES ═══
+ /// ═══ THE SUB-PASSES ═══
///
/// 1. SHELF every below-sea cell; depth-preserving; held strictly below sea by BitDecrement.
- /// 2. ORGANIC the reference's noise layer — faithful, or reshaped (smaller / lower / flatter /
- /// crisper, south-weighted) — every below-sea cell inside the zone mask.
- /// 3. FLOOR (Hybrid only) a seed-derived RNG places ≥N north / ≥S south stamps, each
- /// min-separated from the others AND clear of ALL existing land by a gap, so each
- /// stamp is its own connected component BY CONSTRUCTION. Positions vary per seed;
- /// there are no reserved zones. Refuses loudly if the floor cannot be placed.
+ /// 2. ORGANIC ⭐ THE ONE ISLAND MECHANISM — the reference's noise layer, faithful or reshaped
+ /// (smaller / lower / flatter / crisper, density + south weight), every below-sea
+ /// cell inside the zone mask. Nothing places an island from a centre; nothing
+ /// guarantees a count. (chat2/05's seeded floor was reverted out in chat2/06 — it
+ /// looked stamped. It lives in git history.)
+ /// 3. GUARDS (Organic only) specks, clusters and blobs reverted BY COMPONENT, then the
+ /// reference's submerged humps outside any surviving island's skirt.
///
- /// ═══ THE TWO PROTECTIONS ARE APPLIED TO EVERYTHING, INCLUDING THE FLOOR ═══
+ /// ═══ THE TWO PROTECTIONS ═══
///
/// The moat (min depth) and the "actually offshore" test (pre-Trench falloff) gate the organic
- /// layer per pixel — that is the reference. They ALSO gate every seeded stamp: a centre must sit
- /// fully inside the zone (weight exactly 1), and the stamp's every pixel is still multiplied by
- /// the zone weight. A guaranteed island cannot be guaranteed onto the mainland or into a lake.
+ /// layer per pixel — that is the reference. Nothing in this pass can raise a cell outside the
+ /// zone, so no island can bridge to shore or appear in a lake / the crater bay.
///
/// ═══ ⚠ THE ONE HONEST COUPLING ═══
///
@@ -58,15 +59,32 @@ namespace IslaApocalypse.Tools
{
public bool[,] Tag; // null when the islet layer is off (shelf only)
public byte[,] Hemi;
- public long ShelfCells, LiftedOrganic, LiftedSeeded;
+ public long ShelfCells, LiftedOrganic;
public float ThresholdNorth = float.NaN, ThresholdSouth = float.NaN;
- public List<(int x, int y, int r)> Centres = new();
public List Notes = new();
public int CountNorth, CountSouth, Bridged;
- public long LiftedReverted; // specks the guard put back
- public long RaisedReverted; // submerged debris bumps the guard put back
+
+ // ---- the guards' ledger (Organic only) ----
+ public int PreGuardNorth, PreGuardSouth; // islands before any guard
+ public int SpecksNorth, SpecksSouth; // reverted as specks
+ public int ClustersNorth, ClustersSouth; // reverted as too-close-to-a-larger-island
+ public int BlobsNorth, BlobsSouth; // reverted as oversize
+ public long LiftedReverted; // surfaced cells the guards put back
+ public long RaisedReverted; // submerged bump cells the guard put back
+
internal List<(int x, int y, float h0)> LiftedOrigin = new(); // every SURFACED lift
- internal List<(int x, int y, float h0)> RaisedOrigin = new(); // every organic raise, surfaced or not (guard on)
+ internal List<(int x, int y, float h0)> RaisedOrigin = new(); // every organic raise, surfaced or not (guards on)
+
+ public OffshoreLedger ToLedger() => Tag == null ? null : new OffshoreLedger
+ {
+ ThresholdNorth = ThresholdNorth, ThresholdSouth = ThresholdSouth,
+ PreGuardNorth = PreGuardNorth, PreGuardSouth = PreGuardSouth,
+ SpecksNorth = SpecksNorth, SpecksSouth = SpecksSouth,
+ ClustersNorth = ClustersNorth, ClustersSouth = ClustersSouth,
+ BlobsNorth = BlobsNorth, BlobsSouth = BlobsSouth,
+ LiftedOrganic = LiftedOrganic, LiftedReverted = LiftedReverted, RaisedReverted = RaisedReverted,
+ CountNorth = CountNorth, CountSouth = CountSouth,
+ };
}
///
@@ -121,12 +139,10 @@ namespace IslaApocalypse.Tools
r.Hemi = new byte[mapSize, mapSize];
bool faithful = s.Mode == OffshoreMode.Faithful;
- bool guardOn = !faithful && s.MinIslandAreaFrac > 0f;
+ bool guardsOn = !faithful && (s.MinIslandAreaFrac > 0f || s.MinSeparationFrac > 0f || s.MaxIslandAreaFrac > 0f);
float crest = sea + WorldScale.RawFromMetres(s.CrestM); // seaHere + OFFSHORE_ISLAND_H_M / 251f
- // ═══ 2. THE ORGANIC LAYER ═══
- var liftedOrigin = r.LiftedOrigin; // every lift, for the debris guard
- if (s.Organic)
+ // ═══ 2. THE ORGANIC LAYER — the one island mechanism ═══
{
FastNoiseLite noise = TerrainNoise.CreateModulation(seed, s.SeedOffset, s.FreqPerMapWidth, scale);
@@ -134,14 +150,8 @@ namespace IslaApocalypse.Tools
// sample on a stride grid and take the quantile. Deterministic from the seed, and it
// makes the density dial mean what it says whatever FastNoiseLite's range turns out
// to be. Verbatim: stride 8, (side)² samples, (noise + 1) * 0.5.
- const int stride = 8;
- int side = mapSize / stride;
- var samples = new float[side * side];
- for (int i = 0; i < side; i++)
- for (int j = 0; j < side; j++)
- samples[i * side + j] = (noise.GetNoise2D(i * stride, j * stride) + 1f) * 0.5f;
-
- float thrN = IslandFalloff.CalibrateThreshold(samples, s.DensityNorth);
+ float[] samples = CalibrationSamples(noise, mapSize);
+ float thrN = IslandFalloff.CalibrateThreshold(samples, s.Density);
float thrS = faithful ? thrN : IslandFalloff.CalibrateThreshold(samples, s.DensitySouth);
r.ThresholdNorth = thrN; r.ThresholdSouth = thrS;
@@ -167,14 +177,7 @@ namespace IslaApocalypse.Tools
// The threshold: one number in faithful mode; north/south blended smoothly
// across the midline in the reshape, so a straddling island is not sliced.
- float thr;
- if (faithful) thr = thrN;
- else
- {
- float t = Math.Clamp((y - mid) / band * 0.5f + 0.5f, 0f, 1f);
- t = t * t * (3f - 2f * t);
- thr = thrN + (thrS - thrN) * t;
- }
+ float thr = faithful ? thrN : BlendedThreshold(y, mid, band, thrN, thrS);
float blob = (faithful
? IslandFalloff.OffshoreBlob(v, thr)
@@ -185,14 +188,14 @@ namespace IslaApocalypse.Tools
float before = h;
h = h + (crest - h) * blob; // Mathf.Lerp, written out
height[x, y] = h;
- if (guardOn) r.RaisedOrigin.Add((x, y, before)); // for the debris guard (reshape only)
+ if (guardsOn) r.RaisedOrigin.Add((x, y, before)); // for the submerged-bump guard
if (before < sea && h >= sea)
{
r.LiftedOrganic++;
r.Tag[x, y] = true;
r.Hemi[x, y] = OffshoreAnalysis.HemisphereOfRow(y, mapSize);
- liftedOrigin.Add((x, y, before));
+ r.LiftedOrigin.Add((x, y, before));
}
}
}
@@ -202,159 +205,153 @@ namespace IslaApocalypse.Tools
$"(range {Min(samples):F3}..{Max(samples):F3}); lifted {r.LiftedOrganic:N0} cells above sea.");
}
- // ═══ 3. THE SEEDED FLOOR (Hybrid only) ═══
- if (s.Mode == OffshoreMode.Hybrid && (s.FloorNorth > 0 || s.FloorSouth > 0))
+ // ═══ 3. THE SLOP GUARDS — reshape only; over everything the pass raised ═══
+ //
+ // Four kinds of slop, three component rules and one location rule:
+ // (a) SPECKS a local maximum that barely clears the threshold surfaces a cap of a few
+ // cells — not an island. Reverted by MEMBERSHIP (a speck inside a real
+ // island's skirt is still a speck).
+ // (b) BLOBS a superlevel region that merged several maxima into one sprawling
+ // landmass. Reverted by membership. The cap sits well above the natural
+ // size so it is a net, not a sculptor; how often it bites is reported.
+ // (c) CLUSTERS two islands whose shores are closer than the minimum separation read as
+ // one; the SMALLER goes (greedy by size, so the largest of a cluster stays).
+ // (d) SUBMERGED BUMPS — every blob with any weight lerps the seabed toward the crest
+ // whether or not it surfaces, so the ocean fills with shallow humps (the
+ // reference's character; the water pass would draw a reef field nobody
+ // asked for). Attributed by HUMP: a hump (connected raised region) that
+ // holds a kept island is that island's own skirt and stays; one that holds
+ // none goes back to seabed; a dropped island's own cap goes too.
+ // None of these places, shapes or counts anything. (The faithful control keeps all four;
+ // that is the reference.)
+ if (guardsOn && (r.RaisedOrigin.Count > 0 || r.LiftedOrigin.Count > 0))
{
- var rng = new Pcg32(seed + s.PlacementSeedOffset);
- int radius = Math.Max(2, (int)MathF.Round(s.StampRadiusFrac * mapSize));
- // Rim jitter field: fine enough to put a few lobes around a stamp's rim (wavelength
- // ≈ radius / 1.5), stated per map width so the look holds at every size. Seeded off
- // the islet offset so it is decorrelated from the organic field but just as deterministic.
- FastNoiseLite jitterNoise = s.StampEdgeJitter > 0f
- ? TerrainNoise.CreateModulation(seed, s.SeedOffset + 1, 1.5f / MathF.Max(s.StampRadiusFrac, 1e-4f), scale)
- : null;
- int reachRadius = (int)MathF.Ceiling(radius * (1f + s.StampEdgeJitter));
- int gap = Math.Max(1, (int)MathF.Round(s.LandGapFrac * mapSize));
- float sepPx = s.SeparationFrac * mapSize;
- float zoneFloorDepth = s.MinDepthM + s.DepthFeatherM; // zone weight exactly 1
- float zoneFloorFalloff = s.MinFalloff + s.FalloffFeather;
- var placed = new List<(int x, int y)>();
+ double area = (double)mapSize * mapSize;
+ long minCells = s.MinIslandAreaFrac > 0f ? Math.Max(1L, (long)Math.Round(s.MinIslandAreaFrac * area)) : 0;
+ long maxCells = s.MaxIslandAreaFrac > 0f ? Math.Max(1L, (long)Math.Round(s.MaxIslandAreaFrac * area)) : long.MaxValue;
+ int minSep = s.MinSeparationFrac > 0f ? Math.Max(1, (int)Math.Round(s.MinSeparationFrac * mapSize)) : 0;
- foreach (byte hemi in new[] { OffshoreAnalysis.HemiNorth, OffshoreAnalysis.HemiSouth })
- {
- int need = hemi == OffshoreAnalysis.HemiNorth ? s.FloorNorth : s.FloorSouth;
- if (need <= 0) continue;
- string name = OffshoreAnalysis.HemisphereName(hemi);
- int y0 = hemi == OffshoreAnalysis.HemiNorth ? 0 : mapSize / 2;
- int y1 = hemi == OffshoreAnalysis.HemiNorth ? mapSize / 2 : mapSize;
-
- int got = 0, attempts = 0;
- while (got < need)
- {
- if (++attempts > s.MaxPlacementAttempts)
- throw new InvalidOperationException(
- $"[OffshorePass] could not place the seeded floor: {name} needed {need}, placed {got} " +
- $"after {attempts - 1} attempts. The valid ocean zone is too small or too crowded " +
- $"for sep {s.SeparationFrac:F3} / gap {s.LandGapFrac:F3} / trench ≤ {s.TrenchInner:F2}. " +
- "Refusing rather than under-delivering a guaranteed floor.");
-
- int cx = rng.NextInt(mapSize);
- int cy = y0 + rng.NextInt(y1 - y0);
-
- // Valid ocean: below sea, FULLY inside the zone (moat + falloff + trench),
- // so the centre's own weight is exactly 1 and the stamp surfaces.
- float hc = height[cx, cy];
- if (hc >= sea) continue;
- if (WorldScale.MetresFromRaw(sea - hc) < zoneFloorDepth) continue;
- if (preTrench[cx, cy] < zoneFloorFalloff) continue;
- float dTr = MathF.Max(MathF.Abs(cx - centerX) / halfSpan, MathF.Abs(cy - centerY) / halfSpan);
- if (dTr > s.TrenchInner) continue;
-
- // Separation from the other seeded centres.
- bool tooClose = false;
- foreach (var (px, py) in placed)
- {
- float ddx = px - cx, ddy = py - cy;
- if (ddx * ddx + ddy * ddy < sepPx * sepPx) { tooClose = true; break; }
- }
- if (tooClose) continue;
-
- // ⭐ THE LAND GAP — the guarantee's mechanism. No land of ANY kind (mainland or
- // organic) within radius + gap of the centre, so this stamp can touch nothing
- // and is its own connected component by construction.
- if (LandWithin(height, mapSize, cx, cy, reachRadius + gap, sea)) continue;
-
- long lifted = Stamp(height, preTrench, r, cx, cy, radius, s, sea, crest, mapSize, centerX, centerY, halfSpan, jitterNoise);
- if (lifted == 0)
- throw new InvalidOperationException(
- $"[OffshorePass] a seeded stamp at ({cx},{cy}) lifted no cells — the zone test and the stamp disagree. Refusing.");
-
- placed.Add((cx, cy));
- r.Centres.Add((cx, cy, radius));
- r.LiftedSeeded += lifted;
- got++;
- r.Notes.Add($"[Offshore] floor {name} #{got} at ({cx},{cy}) r{radius}: lifted {lifted:N0} cells (attempt {attempts})");
- }
- }
- }
-
- // ═══ 3b. THE DEBRIS GUARD — reshape only; runs LAST, over everything the pass raised ═══
- //
- // Two kinds of debris, one rule. (a) SPECKS: a local maximum that barely clears the
- // threshold surfaces a cap of a few cells — not an island. (b) SUBMERGED BUMPS: every
- // blob with any weight lerps the seabed toward the crest whether or not it surfaces, so
- // the ocean fills with shallow humps — which the first probe plate showed as a field of
- // pale speckles across the southern sea, and which the water pass would later draw as a
- // reef field nobody asked for. (The faithful control keeps both; that is the reference.)
- //
- // The rule: find the surviving islands (components at or above the minimum area), take
- // each one's bounding box with a margin as its KEEP region, and put every organic raise
- // OUTSIDE all keep regions back to the height it had — surfaced or not, tagged or not. A
- // real island keeps its own submerged skirt; everything else goes back to seabed.
- //
- // ⚠ Runs after the floor so a stamp's rim satellite (w·zone crossing the waterline
- // non-monotonically) is caught too; the stamp bodies are ~800+ cells and survive by
- // construction. The floor is still asserted below.
- if (guardOn && (r.RaisedOrigin.Count > 0 || r.LiftedOrigin.Count > 0))
- {
- long minCells = Math.Max(1L, (long)Math.Round(s.MinIslandAreaFrac * (double)mapSize * mapSize));
var pre = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize, out int[] compId);
+ (r.PreGuardNorth, r.PreGuardSouth) = OffshoreAnalysis.CountByHemisphere(pre);
- // Rule 1 — SPECKS go by MEMBERSHIP: any surfaced cell whose component is below the
- // minimum is reverted and untagged, wherever it sits (a speck inside a real island's
- // skirt is still a speck — the first keep-box cut let those survive as 1-cell islands).
- // Rule 2 — SUBMERGED BUMPS go by LOCATION: a raise outside every surviving island's
- // skirt (bbox + margin) goes back to seabed; inside, it is that island's own slope.
- var small = new HashSet();
- var keep = new List<(int x0, int y0, int x1, int y1)>();
+ var dropped = new HashSet();
+ var byId = new Dictionary();
+ foreach (var c in pre) byId[c.Id] = c;
+
+ // (a) specks and (b) blobs — by size.
foreach (var c in pre)
{
- if (c.Cells < minCells) { small.Add(c.Id); continue; }
- int w = c.MaxX - c.MinX + 1, hgt = c.MaxY - c.MinY + 1;
- int margin = Math.Max(4, Math.Max(w, hgt) / 2);
- keep.Add((c.MinX - margin, c.MinY - margin, c.MaxX + margin, c.MaxY + margin));
+ if (c.Cells < minCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.SpecksNorth, ref r.SpecksSouth); }
+ else if (c.Cells > maxCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.BlobsNorth, ref r.BlobsSouth); }
}
- bool Kept(int x, int y)
+ // (c) clusters — greedy by size among the survivors of (a)/(b).
+ if (minSep > 0)
{
- foreach (var (x0, y0, x1, y1) in keep)
+ var survivors = new List();
+ foreach (var c in pre) if (!dropped.Contains(c.Id)) survivors.Add(c);
+ survivors.Sort((a, b) => b.Cells.CompareTo(a.Cells)); // largest first
+ var boundary = OffshoreAnalysis.BoundaryCells(r.Tag, compId, mapSize, r.LiftedOrigin);
+ var kept = new List();
+ foreach (var c in survivors)
+ {
+ bool tooClose = false;
+ foreach (var k in kept)
+ {
+ if (OffshoreAnalysis.BoxGap(c, k) >= minSep) continue; // cannot be closer than the bbox gap
+ if (OffshoreAnalysis.MinChebyshev(boundary[c.Id], boundary[k.Id], minSep) < minSep) { tooClose = true; break; }
+ }
+ if (tooClose) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.ClustersNorth, ref r.ClustersSouth); }
+ else kept.Add(c);
+ }
+ }
+
+ // (d) THE SUBMERGED BUMPS — by HUMP, not by box. A hump is one 8-connected region of
+ // raised cells (the blob's footprint, surfaced or not). A hump that holds a kept island
+ // is that island's own skirt and stays whole; a hump that holds none is a reef nobody
+ // asked for and goes back to seabed whole. A DROPPED island's own cap — its bbox plus a
+ // margin — is reverted even inside a kept hump, or its rim would stay as a hollow ring
+ // beside its neighbour (the first probe plate showed exactly those ghost outlines).
+ var raised = new bool[mapSize, mapSize];
+ foreach (var (x, y, _) in r.RaisedOrigin) raised[x, y] = true;
+ var humpId = new int[mapSize * mapSize];
+ var humpKept = new List { false }; // index 0 unused
+ {
+ var stack = new Stack();
+ foreach (var (sx, sy, _) in r.RaisedOrigin)
+ {
+ if (humpId[sx * mapSize + sy] != 0) continue;
+ int id = humpKept.Count; humpKept.Add(false);
+ humpId[sx * mapSize + sy] = id; stack.Push(sx * mapSize + sy);
+ bool kept = false;
+ while (stack.Count > 0)
+ {
+ int cur = stack.Pop(); int cx = cur / mapSize, cy = cur % mapSize;
+ if (r.Tag[cx, cy] && !dropped.Contains(compId[cur])) kept = true;
+ for (int dx = -1; dx <= 1; dx++)
+ {
+ int nx = cx + dx; if (nx < 0 || nx >= mapSize) continue;
+ for (int dy = -1; dy <= 1; dy++)
+ {
+ int ny = cy + dy; if (ny < 0 || ny >= mapSize || (dx == 0 && dy == 0)) continue;
+ if (!raised[nx, ny]) continue;
+ int ni = nx * mapSize + ny;
+ if (humpId[ni] != 0) continue;
+ humpId[ni] = id; stack.Push(ni);
+ }
+ }
+ }
+ humpKept[id] = kept;
+ }
+ }
+ var dropBoxes = new List<(int x0, int y0, int x1, int y1)>();
+ foreach (var c in pre)
+ {
+ if (!dropped.Contains(c.Id)) continue;
+ int w = c.MaxX - c.MinX + 1, hgt = c.MaxY - c.MinY + 1;
+ int margin = Math.Max(4, Math.Max(w, hgt) / 2);
+ dropBoxes.Add((c.MinX - margin, c.MinY - margin, c.MaxX + margin, c.MaxY + margin));
+ }
+ bool InDropBox(int x, int y)
+ {
+ foreach (var (x0, y0, x1, y1) in dropBoxes)
if (x >= x0 && x <= x1 && y >= y0 && y <= y1) return true;
return false;
}
- long revertedRaised = 0, revertedLifted = 0;
+ long revertedLifted = 0, revertedRaised = 0;
foreach (var (x, y, h0) in r.LiftedOrigin)
{
- if (!small.Contains(compId[x * mapSize + y])) continue;
+ if (!dropped.Contains(compId[x * mapSize + y])) continue;
height[x, y] = h0; r.Tag[x, y] = false; r.Hemi[x, y] = OffshoreAnalysis.HemiNone;
revertedLifted++;
}
foreach (var (x, y, h0) in r.RaisedOrigin)
{
if (r.Tag[x, y]) continue; // a kept island's own surfaced cell
- if (Kept(x, y)) continue; // inside a kept island's skirt
+ bool keepIt = humpKept[humpId[x * mapSize + y]] && !InDropBox(x, y);
+ if (keepIt) continue;
if (height[x, y] > h0) { height[x, y] = h0; revertedRaised++; }
}
r.LiftedReverted = revertedLifted;
r.RaisedReverted = revertedRaised;
- r.Notes.Add($"[Offshore] debris guard: {small.Count} of {pre.Count} islands below {minCells:N0} cells reverted " +
- $"({revertedLifted:N0} surfaced cells), plus {revertedRaised:N0} submerged bump cells outside any kept island's skirt.");
+ r.Notes.Add($"[Offshore] guards: of {pre.Count} islands (N {r.PreGuardNorth} / S {r.PreGuardSouth}) reverted " +
+ $"{r.SpecksNorth + r.SpecksSouth} specks (< {minCells:N0} cells), " +
+ $"{r.ClustersNorth + r.ClustersSouth} clustered (< {minSep} px from a larger island), " +
+ $"{r.BlobsNorth + r.BlobsSouth} blobs (> {(maxCells == long.MaxValue ? "∞" : maxCells.ToString("N0"))} cells) " +
+ $"— {revertedLifted:N0} surfaced cells, plus {revertedRaised:N0} submerged bump cells outside any kept island's hump.");
}
- // ═══ PROVE THE FLOOR ON THE FINISHED FIELD — in the pass, before anyone looks ═══
+ // ═══ PROVE THE MOAT ON THE FINISHED FIELD — in the pass, before anyone looks ═══
var comps = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize);
(r.CountNorth, r.CountSouth) = OffshoreAnalysis.CountByHemisphere(comps);
r.Bridged = OffshoreAnalysis.BridgedCount(comps);
var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0);
r.Notes.Add($"[Offshore] islands: {r.CountNorth} north, {r.CountSouth} south ({comps.Count} components, " +
- $"{r.Bridged} bridged to mainland); lifted {r.LiftedOrganic + r.LiftedSeeded - r.LiftedReverted:N0} cells net" +
+ $"{r.Bridged} bridged to mainland); lifted {r.LiftedOrganic - r.LiftedReverted:N0} cells net" +
$"{(r.RaisedReverted > 0 ? $", {r.RaisedReverted:N0} submerged debris cells reverted" : "")}; " +
$"size cells min {szMin} median {szMed} mean {szMean:F0} max {szMax}.");
- if (s.Mode == OffshoreMode.Hybrid && (r.CountNorth < s.FloorNorth || r.CountSouth < s.FloorSouth))
- throw new InvalidOperationException(
- $"[OffshorePass] FLOOR VIOLATION after placement: {r.CountNorth} N / {r.CountSouth} S against " +
- $"{s.FloorNorth} N / {s.FloorSouth} S. The land gap should have made this impossible. Refusing.");
if (r.Bridged > 0)
throw new InvalidOperationException(
$"[OffshorePass] MOAT VIOLATION: {r.Bridged} island(s) touch mainland land. Refusing.");
@@ -362,93 +359,46 @@ namespace IslaApocalypse.Tools
return r;
}
- /// One seeded stamp: a flat-topped disc lerped toward the crest, zone-masked per pixel.
- private static long Stamp(float[,] height, float[,] preTrench, Result r, int cx, int cy, int radius,
- OffshoreSettings s, float sea, float crest, int mapSize, float centerX, float centerY, float halfSpan,
- FastNoiseLite jitterNoise)
+ /// The reference's calibration sample: stride 8, (side)² samples, (noise + 1) · 0.5. Shared with the diagnosis.
+ public static float[] CalibrationSamples(FastNoiseLite noise, int mapSize)
{
- long lifted = 0;
- int reach = (int)MathF.Ceiling(radius * (1f + s.StampEdgeJitter));
- for (int dx = -reach; dx <= reach; dx++)
- {
- int px = cx + dx;
- if (px < 0 || px >= mapSize) continue;
- for (int dy = -reach; dy <= reach; dy++)
- {
- int py = cy + dy;
- if (py < 0 || py >= mapSize) continue;
-
- // The rim wanders: the effective radius at this pixel is the nominal one scaled
- // by ±jitter from a fine noise field. Flat top and crisp shore are kept; the
- // compass-drawn outline is not.
- float rEff = radius;
- if (jitterNoise != null)
- rEff = radius * (1f + s.StampEdgeJitter * jitterNoise.GetNoise2D(px, py));
-
- float dist = MathF.Sqrt(dx * dx + dy * dy);
- float w = IslandFalloff.StampWeight(dist, rEff, s.StampCoreFrac);
- if (w <= 0f) continue;
-
- float h = height[px, py];
- if (h >= sea) continue; // never touches land
-
- // ⭐ The moat and the falloff test apply to the FLOOR too. Absolute.
- float zone = IslandFalloff.OffshoreZoneWeight(
- WorldScale.MetresFromRaw(sea - h), preTrench[px, py],
- MathF.Abs(px - centerX) / halfSpan, MathF.Abs(py - centerY) / halfSpan,
- s.MinDepthM, s.DepthFeatherM, s.MinFalloff, s.FalloffFeather,
- s.TrenchInner, s.TrenchOuter);
- float t = w * zone;
- if (t <= 0f) continue;
-
- float before = h;
- h = h + (crest - h) * t;
- height[px, py] = h;
-
- if (before < sea && h >= sea)
- {
- lifted++;
- r.Tag[px, py] = true;
- r.Hemi[px, py] = OffshoreAnalysis.HemisphereOfRow(py, mapSize);
- r.LiftedOrigin.Add((px, py, before));
- }
- }
- }
- return lifted;
+ const int stride = 8;
+ int side = mapSize / stride;
+ var samples = new float[side * side];
+ for (int i = 0; i < side; i++)
+ for (int j = 0; j < side; j++)
+ samples[i * side + j] = (noise.GetNoise2D(i * stride, j * stride) + 1f) * 0.5f;
+ return samples;
}
- /// Any land cell (at/above sea) within a Chebyshev box of ? Conservative by design.
- private static bool LandWithin(float[,] height, int mapSize, int cx, int cy, int reach, float sea)
+ /// The north/south threshold, smoothstep-blended across the midline. Shared with the diagnosis.
+ public static float BlendedThreshold(int y, float mid, float band, float thrN, float thrS)
{
- int x0 = Math.Max(0, cx - reach), x1 = Math.Min(mapSize - 1, cx + reach);
- int y0 = Math.Max(0, cy - reach), y1 = Math.Min(mapSize - 1, cy + reach);
- for (int x = x0; x <= x1; x++)
- for (int y = y0; y <= y1; y++)
- if (height[x, y] >= sea) return true;
- return false;
+ float t = Math.Clamp((y - mid) / band * 0.5f + 0.5f, 0f, 1f);
+ t = t * t * (3f - 2f * t);
+ return thrN + (thrS - thrN) * t;
+ }
+
+ private static void Bump(Result r, byte hemi, ref int north, ref int south)
+ {
+ if (hemi == OffshoreAnalysis.HemiNorth) north++; else south++;
}
private static float Min(float[] a) { float m = float.MaxValue; foreach (float v in a) if (v < m) m = v; return m; }
private static float Max(float[] a) { float m = float.MinValue; foreach (float v in a) if (v > m) m = v; return m; }
+ }
- ///
- /// PCG32 (O'Neill) — the same tiny deterministic generator the reference's erosion pass
- /// used. Seeded from the world seed + an offset, so island POSITIONS vary per world and
- /// reproduce exactly for one. Trivially portable to C++.
- ///
- private struct Pcg32
- {
- private ulong _state;
- public Pcg32(int seed) { _state = 0; NextU(); _state += (ulong)(uint)seed; NextU(); }
- public uint NextU()
- {
- ulong old = _state;
- _state = old * 6364136223846793005UL + 1442695040888963407UL;
- uint xorshifted = (uint)(((old >> 18) ^ old) >> 27);
- int rot = (int)(old >> 59);
- return (xorshifted >> rot) | (xorshifted << (-rot & 31));
- }
- public int NextInt(int n) => (int)(NextU() % (uint)n);
- }
+ ///
+ /// The islet layer's ledger, carried on Pass1Result for the report: thresholds, the
+ /// pre-guard island count, what each guard reverted, the final count. Numbers only — the tag
+ /// arrays are carried separately.
+ ///
+ public sealed class OffshoreLedger
+ {
+ public float ThresholdNorth, ThresholdSouth;
+ public int PreGuardNorth, PreGuardSouth;
+ public int SpecksNorth, SpecksSouth, ClustersNorth, ClustersSouth, BlobsNorth, BlobsSouth;
+ public long LiftedOrganic, LiftedReverted, RaisedReverted;
+ public int CountNorth, CountSouth;
}
}
diff --git a/Tools/Scripts/OffshoreSettings.cs b/Tools/Scripts/OffshoreSettings.cs
index 8129e43..5e57519 100644
--- a/Tools/Scripts/OffshoreSettings.cs
+++ b/Tools/Scripts/OffshoreSettings.cs
@@ -11,16 +11,19 @@ namespace IslaApocalypse.Tools
///
/// ⭐ chat2/05 stage 1 — the reference's probabilistic layer, verbatim: one noise field, one
/// calibrated threshold, the faithful blob, the faithful zone mask. Sparse, no corners, no
- /// floor. THE CONTROL.
+ /// guards. THE CONTROL.
///
Faithful,
///
- /// ⭐ chat2/05 stage 2 — the loose-guaranteed hybrid: a seeded floor of ≥N north / ≥S south
- /// islands (min-separated, positions varying per seed, never fixed zones) PLUS the organic
- /// layer on top, reshaped small / low / flat / rigid, south-weighted, corners allowed.
+ /// ⭐ chat2/06 — THE ONE ISLAND MECHANISM: the organic noise-field layer, reshaped small /
+ /// low / flat / crisp (chat2/05 stage 2's shape, unchanged), tuned for coverage by DENSITY
+ /// and a SOUTH WEIGHT, with the speck / separation / blob guards. No seeded floor, no
+ /// stamps, no count guarantee — every island is a noise outline, and the per-hemisphere
+ /// counts are a statistical outcome of the tuning (the chat2/05 forced floor was tried and
+ /// reverted on look; it is in git history, one checkout away).
///
- Hybrid,
+ Organic,
}
///
@@ -31,19 +34,24 @@ namespace IslaApocalypse.Tools
/// ═══ TWO PRESETS, AND WHY BOTH EXIST ═══
///
/// the reference's constants, verbatim. The control in every batch.
- /// the reshape — the deliverable.
+ /// the reshape + the chat2/06 coverage tuning — the deliverable.
///
/// The reshape does not EDIT the faithful constants; it sets different values on the same
- /// dials. So Faithful() stays bit-reproducible however far the hybrid is tuned.
+ /// dials. So Faithful() stays bit-reproducible however far the organic layer is tuned.
///
/// ═══ THE TWO PROTECTIONS THAT ARE NOT DIALS ═══
///
/// (the moat) and (the "actually offshore" test)
/// are exposed here because every knob is, but they are the MAIN-ISLAND AND LAKE PROTECTIONS and
/// the presets do not move them. They are what makes "no island can bridge to shore" and "no
- /// island in a lake or the crater bay" true by construction — and OffshorePass applies
- /// them to the SEEDED stamps as well, so a guaranteed island cannot be guaranteed into the wrong
- /// place.
+ /// island in a lake or the crater bay" true by construction.
+ ///
+ /// ═══ ⚠ WHAT IS DELIBERATELY NOT HERE (chat2/06) ═══
+ ///
+ /// No floor, no stamp radius/core/jitter, no separation-of-seeded-centres, no placement RNG —
+ /// nothing that guarantees a count or places an island from a centre. The forced mechanism
+ /// (chat2/05 Hybrid()) was reverted out whole; if a hard count ever comes back it is a
+ /// design decision, not a knob that was left lying around.
///
public sealed class OffshoreSettings
{
@@ -51,9 +59,6 @@ namespace IslaApocalypse.Tools
// ---- the organic (noise) layer ----------------------------------------
- /// Run the noise layer at all. Off ⇒ only the seeded floor (the `floor_only` contrast).
- public bool Organic = true;
-
/// Islet noise frequency, periods per map width. Higher ⇒ SMALLER blobs. Reference 14.
public float FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS;
@@ -61,12 +66,24 @@ namespace IslaApocalypse.Tools
public int SeedOffset = IslandFalloff.OFFSHORE_SEED_OFFSET;
///
- /// Organic density — the fraction of the noise field's ACTUAL sampled distribution that
- /// clears the threshold — per hemisphere. Equal ⇒ the reference's single dial. South >
- /// north ⇒ the developer's "weighted south". Reference 0.02 / 0.02.
+ /// ⭐ THE MAIN KNOB — organic density: the fraction of the noise field's ACTUAL sampled
+ /// distribution that clears the calibrated threshold. More ⇒ more of the field becomes
+ /// island. This is the NORTH density; the south is this × .
+ /// Reference 0.02.
///
- public float DensityNorth = 0.02f;
- public float DensitySouth = 0.02f;
+ public float Density = 0.02f;
+
+ ///
+ /// ⭐ THE SOUTH WEIGHT — a per-hemisphere density bias. South density =
+ /// × this. 1 ⇒ one dial (the reference); > 1 ⇒ "weighted south",
+ /// the developer's preference. It is applied to the DENSITY (i.e. the calibration quantile),
+ /// not to the blob shape, so a south island looks exactly like a north island — there are
+ /// simply more of them.
+ ///
+ public float SouthWeight = 1f;
+
+ /// The effective south density.
+ public float DensitySouth => Density * SouthWeight;
///
/// Half-width of the smooth threshold blend across the hemisphere midline, as a fraction of
@@ -84,16 +101,37 @@ namespace IslaApocalypse.Tools
/// Crest-to-sea edge sharpening exponent. 1 ⇒ the faithful smoothstep. Higher ⇒ crisper shore.
public float EdgeSharpness = 1f;
+ // ---- the slop guards (Organic only; all scale-free fractions of the map) --------------
+ //
+ // More density must not buy speck-debris or merged blobs. Three rules, each a revert BY
+ // COMPONENT MEMBERSHIP (the island goes back to seabed whole, and is untagged) — none of
+ // them places, shapes or counts anything. 0 ⇒ that rule is off. The faithful preset has none
+ // (the reference had no such rules).
+
///
- /// ⚠ THE DEBRIS GUARD. An organic island smaller than this fraction of the map's area is
+ /// ⚠ THE SPECK GUARD. An organic island smaller than this fraction of the map's AREA is
/// noise debris — a local maximum that barely cleared the threshold and surfaced a cap of a
- /// few cells — and is reverted to seabed and untagged. 0 ⇒ off (the reference had no such
- /// rule and its "~585 px blobs" comment was about the NOISE wavelength, not the caps).
- /// Stated as a fraction so the rule is scale-free: 3e-5 is ~126 cells at 2048, ~2,000 cells
- /// (a ~50 m islet) at 8192.
+ /// few cells — and is reverted to seabed and untagged. 3e-5 is ~126 cells at 2048, ~500 at
+ /// 4096, ~2,000 cells (a ~50 m islet) at 8192.
///
public float MinIslandAreaFrac = 0f;
+ ///
+ /// ⚠ THE SEPARATION GUARD — "enough separation that they read as distinct islands". Two
+ /// surviving islands whose nearest shores are closer than this fraction of the map WIDTH
+ /// read as one cluster; the SMALLER is reverted. Greedy by size, so the largest island in a
+ /// cluster always stays. Chebyshev distance between boundary cells.
+ ///
+ public float MinSeparationFrac = 0f;
+
+ ///
+ /// ⚠ THE BLOB GUARD. An organic island LARGER than this fraction of the map's area is a
+ /// superlevel region that merged several maxima into one sprawling landmass — the "blob"
+ /// the developer does not want — and is reverted whole. Set well above the natural size at
+ /// the preset density so it is a safety net, not a sculptor; how often it bites is reported.
+ ///
+ public float MaxIslandAreaFrac = 0f;
+
// ---- the zone mask: protections + the Trench bound --------------------
/// ⭐ THE MOAT. Reference 14 m. The presets do not move it.
@@ -115,95 +153,67 @@ namespace IslaApocalypse.Tools
public float TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER;
public float TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER;
- // ---- the seeded floor (Hybrid only) -----------------------------------
-
- /// ⭐ The guaranteed minimum island count per hemisphere. 0 / 0 ⇒ no floor (Faithful).
- public int FloorNorth = 0;
- public int FloorSouth = 0;
-
- /// Stamp radius, fraction of the map. Small and low is the target.
- public float StampRadiusFrac = 0.012f;
-
- /// Fraction of the stamp radius that is flat top. Higher ⇒ flatter, crisper shore.
- public float StampCoreFrac = 0.60f;
-
- ///
- /// Radial jitter on the stamp's rim, as a fraction of the radius, from a fine noise field:
- /// the outline wanders ±this much around the circle. 0 ⇒ a compass-drawn disc (the first
- /// probe plate: rigid, but visibly geometric). 0.25 keeps the crisp shore and the flat top
- /// while the coastline stops looking stamped.
- ///
- public float StampEdgeJitter = 0f;
-
- /// Minimum centre-to-centre separation between seeded islands, fraction of the map.
- public float SeparationFrac = 0.08f;
-
- ///
- /// Minimum clear water between a seeded stamp's rim and ANY existing land (mainland or
- /// organic), fraction of the map. This is what makes every seeded island its own connected
- /// component by construction — and therefore what makes the floor COUNT hold, not just the
- /// number of stamps.
- ///
- public float LandGapFrac = 0.010f;
-
- /// The placement RNG's seed offset. A SEED offset; positions vary per world seed.
- public int PlacementSeedOffset = 7703;
-
- /// Rejection-sampling budget per hemisphere before the pass REFUSES (loudly) rather than under-delivers.
- public int MaxPlacementAttempts = 40000;
-
// ---- presets ----------------------------------------------------------
- /// The reference, verbatim. No floor, single density, original footprint/crest/mask.
+ /// The reference, verbatim. Single density, no guards, original footprint/crest/mask.
public static OffshoreSettings Faithful() => new OffshoreSettings
{
Mode = OffshoreMode.Faithful,
- Organic = true,
FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS,
SeedOffset = IslandFalloff.OFFSHORE_SEED_OFFSET,
- DensityNorth = 0.02f, DensitySouth = 0.02f, // ConfigManager.OffshoreIslandDensity
+ Density = 0.02f, SouthWeight = 1f, // ConfigManager.OffshoreIslandDensity
CrestM = IslandFalloff.OFFSHORE_ISLAND_H_M,
CoreFraction = IslandFalloff.OFFSHORE_CORE,
EdgeSharpness = 1f,
TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER,
TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER,
- FloorNorth = 0, FloorSouth = 0,
};
///
- /// ⭐ The reshape: small / low / flatter / more rigid, loose-guaranteed (≥2 N, ≥4 S), organic
- /// extras weighted south, corners and the outer edge allowed.
+ /// ⭐ THE DELIVERABLE (chat2/06) — the organic layer, reshaped (chat2/05 stage 2's shape:
+ /// small / low / flat / crisp, corners on) and tuned for coverage: density up from 05's
+ /// 0.007, south-weighted, guarded against specks / clusters / blobs.
+ ///
+ /// ⚠ TUNED BY MEASUREMENT, not by feel (chat2/06 batch — the count table over 12 seeds ×
+ /// 3 levels and the per-hemisphere diagnosis). This preset is the batch's `density_mid`.
///
- public static OffshoreSettings Hybrid() => new OffshoreSettings
+ public static OffshoreSettings Organic() => new OffshoreSettings
{
- Mode = OffshoreMode.Hybrid,
- Organic = true,
- // ⚠ Chosen by SWEEP, not by feel (chat2/05 scratch): at freq 24 / 0.010 / 0.025 the field
- // produced 183 organic blobs of which 174 were debris. freq 16 with these densities gives
- // the floor plus ~+1–2 N / +6–8 S genuine extras of a few hundred cells each at 2048.
- FreqPerMapWidth = 16f, // a little smaller than the reference's 14
- DensityNorth = 0.007f, DensitySouth = 0.012f, // weighted south
- CrestM = 24f, // lower than the reference's 34 (pre-curve)
+ Mode = OffshoreMode.Organic,
+ FreqPerMapWidth = 16f, // a little smaller than the reference's 14 (chat2/05)
+ Density = OrganicDensityMid,
+ SouthWeight = OrganicSouthWeight,
+ CrestM = 24f, // lower than the reference's 34 (pre-curve) — lands in the curve's preserved toe
CoreFraction = 0.25f, // flatter than 0.45
EdgeSharpness = 2.5f, // crisper shore than the faithful smoothstep
- MinIslandAreaFrac = 3e-5f, // no noise debris
+ MinIslandAreaFrac = OrganicMinIslandAreaFrac,
+ MinSeparationFrac = OrganicMinSeparationFrac,
+ MaxIslandAreaFrac = OrganicMaxIslandAreaFrac,
TrenchInner = 0.90f, TrenchOuter = 0.97f, // corners + outer edge allowed; centre stays on-map
- FloorNorth = 2, FloorSouth = 4,
- StampRadiusFrac = 0.012f, StampCoreFrac = 0.60f, StampEdgeJitter = 0.25f,
- SeparationFrac = 0.08f, LandGapFrac = 0.010f,
};
+ ///
+ /// The three density levels of the chat2/06 batch, in one place so the tool and the preset
+ /// cannot disagree. Mid is the preset of record.
+ ///
+ public const float OrganicDensityLow = 0.016f; // the edge: every seed of the 12 still clears N ≥ 2 / S ≥ 3, but N's minimum IS 2
+ public const float OrganicDensityMid = 0.022f; // ⭐ the preset: N min 3 / mean 6.3, S min ~15 / mean ~20 over 12 seeds at 4096
+ public const float OrganicDensityHigh = 0.030f; // the "how many is too many" bookend
+ public const float OrganicSouthWeight = 1.25f; // preference, not a fix — the diagnosis found the south UN-suppressed (see the report)
+ public const float OrganicMinIslandAreaFrac = 3e-5f;
+ public const float OrganicMinSeparationFrac = 0.008f;
+ public const float OrganicMaxIslandAreaFrac = 6e-4f;
+
public OffshoreSettings Clone() => (OffshoreSettings)MemberwiseClone();
public string Describe()
{
if (Mode == OffshoreMode.Off) return "offshore OFF";
var sb = new StringBuilder();
- sb.Append($"{Mode}: organic {(Organic ? "on" : "OFF")} freq {FreqPerMapWidth:F0}/map density N {DensityNorth:F3} S {DensitySouth:F3} · ");
- sb.Append($"crest {CrestM:F0} m core {CoreFraction:F2} sharp {EdgeSharpness:F1} minArea {MinIslandAreaFrac:G2} · ");
+ sb.Append($"{Mode}: freq {FreqPerMapWidth:F0}/map density {Density:F4} × south {SouthWeight:F2} (= S {DensitySouth:F4}) · ");
+ sb.Append($"crest {CrestM:F0} m core {CoreFraction:F2} sharp {EdgeSharpness:F1} · ");
+ sb.Append($"guards minArea {MinIslandAreaFrac:G2} minSep {MinSeparationFrac:G2} maxArea {MaxIslandAreaFrac:G2} · ");
sb.Append($"moat {MinDepthM:F0}+{DepthFeatherM:F0} m falloff {MinFalloff:F2}+{FalloffFeather:F2} trench {TrenchInner:F2}→{TrenchOuter:F2}");
- if (FloorNorth > 0 || FloorSouth > 0)
- sb.Append($" · floor N{FloorNorth} S{FloorSouth} r {StampRadiusFrac:F3} core {StampCoreFrac:F2} sep {SeparationFrac:F2} gap {LandGapFrac:F3}");
return sb.ToString();
}
}
diff --git a/Tools/Scripts/Pass1Result.cs b/Tools/Scripts/Pass1Result.cs
index 03172ba..27647ec 100644
--- a/Tools/Scripts/Pass1Result.cs
+++ b/Tools/Scripts/Pass1Result.cs
@@ -91,13 +91,13 @@ namespace IslaApocalypse.Tools
///
public readonly byte[,] IslandHemisphere;
- /// The seeded-floor stamp centres and radii (Hybrid only; empty otherwise). For the overlay and the report.
- public readonly IReadOnlyList<(int x, int y, int r)> OffshoreCentres;
-
/// Cells the islet layer lifted above sea. The reference printed this too.
public readonly long OffshoreLiftedCells;
- /// Lines worth printing from the shelf/offshore pass: thresholds, floor placement, lifted counts.
+ /// The islet layer's numbers — thresholds, pre-guard count, guard ledger, final count. Null when offshore is off.
+ public readonly OffshoreLedger OffshoreLedger;
+
+ /// Lines worth printing from the shelf/offshore pass: thresholds, guard ledger, lifted counts.
public readonly IReadOnlyList Notes;
/// Wall-clock milliseconds the pass took.
@@ -107,8 +107,7 @@ namespace IslaApocalypse.Tools
float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs,
float hMaxSeedBeforeOffshore = float.NaN,
bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null,
- IReadOnlyList<(int x, int y, int r)> offshoreCentres = null,
- long offshoreLiftedCells = 0, IReadOnlyList notes = null)
+ long offshoreLiftedCells = 0, IReadOnlyList notes = null, OffshoreLedger offshoreLedger = null)
{
MapSize = mapSize;
Seed = seed;
@@ -121,8 +120,8 @@ namespace IslaApocalypse.Tools
HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore;
IsOffshoreIsland = isOffshoreIsland;
IslandHemisphere = islandHemisphere;
- OffshoreCentres = offshoreCentres ?? System.Array.Empty<(int, int, int)>();
OffshoreLiftedCells = offshoreLiftedCells;
+ OffshoreLedger = offshoreLedger;
Notes = notes ?? System.Array.Empty();
}
diff --git a/Tools/Scripts/ShapingOracle.cs b/Tools/Scripts/ShapingOracle.cs
index 9ea805c..9fa73df 100644
--- a/Tools/Scripts/ShapingOracle.cs
+++ b/Tools/Scripts/ShapingOracle.cs
@@ -402,28 +402,9 @@ namespace IslaApocalypse.Tools
// ═══ chat2/05 — the offshore checks ═══
- ///
- /// (h) ⭐ FLOOR HOLDS — at least /
- /// offshore islands per hemisphere, counted as 8-connected components of TAGGED land by
- /// centroid. The pass already refused to return without this; the oracle proves it again,
- /// independently, on the finished field — a guarantee checked once is a guarantee checked by
- /// the thing that might be wrong.
- ///
- public static Check OffshoreFloor(Pass1Result p1, int needNorth, int needSouth, float sea,
- List comps)
- {
- var c = new Check { Id = "h", Name = $"offshore floor ≥{needNorth} N / ≥{needSouth} S" };
- if (!p1.HasOffshoreTag)
- {
- c.Passed = false;
- c.Detail = "no offshore tag on this field — offshore was off.";
- return c;
- }
- var (n, s) = OffshoreAnalysis.CountByHemisphere(comps);
- c.Passed = n >= needNorth && s >= needSouth;
- c.Detail = $"{n} north, {s} south ({comps.Count} islands)";
- return c;
- }
+ // (h) was the chat2/05 seeded-floor check — reverted out with the floor in chat2/06. No
+ // count is guaranteed any more, so there is nothing for an oracle to assert; the count
+ // table is the evidence, and it is statistics, not a check.
///
/// (i) ⭐ MOAT INTACT — no offshore island is 8-connected to mainland land. The moat exists
diff --git a/Tools/Scripts/TagOverlayRenderer.cs b/Tools/Scripts/TagOverlayRenderer.cs
index 4b3d273..f5d388f 100644
--- a/Tools/Scripts/TagOverlayRenderer.cs
+++ b/Tools/Scripts/TagOverlayRenderer.cs
@@ -1,11 +1,10 @@
-using System.Collections.Generic;
using Godot;
namespace IslaApocalypse.Tools
{
///
/// The offshore TAG / HEMISPHERE debug overlay (chat2/05): mainland one tint, offshore-island
- /// land tinted by hemisphere, seeded centres ringed, the midline drawn — so the island count,
+ /// land tinted by hemisphere, the midline drawn — so the island count,
/// the N/S split and the tag's correctness are all visible at one glance.
///
/// ⚠ A DIAGNOSTIC, NOT A MAP. It draws the tag layer, which is DATA the shape pass set; it is
@@ -22,7 +21,6 @@ namespace IslaApocalypse.Tools
private static readonly Color IslandS = new(0.980f, 0.600f, 0.200f); // warm — south
private static readonly Color Untagged = new(0.950f, 0.150f, 0.800f); // ⚠ land that is neither — must never appear
private static readonly Color Midline = new(0.700f, 0.720f, 0.760f);
- private static readonly Color Ring = new(1.000f, 1.000f, 1.000f);
private static readonly Color Ink = new(0.941f, 0.949f, 0.961f);
///
@@ -30,7 +28,7 @@ namespace IslaApocalypse.Tools
/// by mis-tagging mainland). Null ⇒ derived as "land and not tagged", which is weaker.
///
public static void SavePng(float[,] height, bool[,] tag, byte[,] hemi, int mapSize, float sea,
- IReadOnlyList<(int x, int y, int r)> centres, int countN, int countS, string absolutePath)
+ int countN, int countS, string absolutePath)
{
var img = Image.CreateEmpty(mapSize, mapSize, false, Image.Format.Rgb8);
@@ -59,38 +57,16 @@ namespace IslaApocalypse.Tools
int mid = mapSize / 2;
for (int x = 0; x < mapSize; x += 3) img.SetPixel(x, mid, Midline);
- // Seeded centres: a ring at the stamp radius, so the floor islands can be told from the
- // organic ones by eye.
- if (centres != null)
- foreach (var (cx, cy, r) in centres)
- DrawRing(img, cx, cy, r, mapSize);
-
// A legend that cannot be separated from the picture.
int s = mapSize >= 4096 ? 4 : 3;
int lh = TinyFont.Height(s) + 6;
TinyFont.Draw(img, "OFFSHORE TAG OVERLAY", 12, 12, s, Ink);
TinyFont.Draw(img, "GREY: MAINLAND CYAN: ISLAND N ORANGE: ISLAND S", 12, 12 + lh, s, Ink);
- TinyFont.Draw(img, $"ISLANDS: {countN} NORTH {countS} SOUTH - RINGS: SEEDED FLOOR", 12, 12 + lh * 2, s, Ink);
+ TinyFont.Draw(img, $"ISLANDS: {countN} NORTH {countS} SOUTH - ALL ORGANIC, NONE FORCED", 12, 12 + lh * 2, s, Ink);
TinyFont.Draw(img, "N ABOVE THE LINE - S BELOW - Y RUNS SOUTH", 12, 12 + lh * 3, s, Ink);
Error err = img.SavePng(absolutePath);
if (err != Error.Ok) GD.PrintErr($"[TagOverlayRenderer] SavePng failed ({err}) for {absolutePath}");
}
-
- private static void DrawRing(Image img, int cx, int cy, int r, int n)
- {
- int rr = r + 3; // just outside the rim
- int steps = System.Math.Max(64, rr * 4);
- for (int i = 0; i < steps; i++)
- {
- double a = i * 2.0 * System.Math.PI / steps;
- for (int t = 0; t < 2; t++) // 2 px thick
- {
- int px = cx + (int)System.Math.Round((rr + t) * System.Math.Cos(a));
- int py = cy + (int)System.Math.Round((rr + t) * System.Math.Sin(a));
- if (px >= 0 && py >= 0 && px < n && py < n) img.SetPixel(px, py, Ring);
- }
- }
- }
}
}
diff --git a/Tools/Scripts/TerrainGenConfig.cs b/Tools/Scripts/TerrainGenConfig.cs
index f5ca742..03a3d2d 100644
--- a/Tools/Scripts/TerrainGenConfig.cs
+++ b/Tools/Scripts/TerrainGenConfig.cs
@@ -270,7 +270,7 @@ namespace IslaApocalypse.Tools
///
/// ⭐ The offshore islet system — every dial in one object. Mode = Off by default
/// (see the note above). is the reference verbatim;
- /// is the reshape.
+ /// is the reshape, tuned (chat2/06).
///
public OffshoreSettings Offshore = new OffshoreSettings();
diff --git a/Tools/Scripts/Topography.cs b/Tools/Scripts/Topography.cs
index 0adc131..36166fe 100644
--- a/Tools/Scripts/Topography.cs
+++ b/Tools/Scripts/Topography.cs
@@ -283,7 +283,7 @@ namespace IslaApocalypse.Tools
// The reference's pass 1 CONTINUED HERE with the coast shelf (~:621-640) and the
// offshore islets (~:641-664). v2 runs them as PASS 1b, a second sweep over these
// arrays, immediately below — same per-pixel arithmetic, same order, and the
- // seeded floor needs the whole depth/falloff field to exist first. → OffshorePass.
+ // slop guards need the whole field to see whole islands. → OffshorePass.
}
}
@@ -317,9 +317,9 @@ namespace IslaApocalypse.Tools
hMaxSeedBeforeOffshore: hMaxBeforeOffshore,
isOffshoreIsland: offshore?.Tag,
islandHemisphere: offshore?.Hemi,
- offshoreCentres: offshore?.Centres,
- offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic + offshore.LiftedSeeded - offshore.LiftedReverted,
- notes: offshore?.Notes);
+ offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic - offshore.LiftedReverted,
+ notes: offshore?.Notes,
+ offshoreLedger: offshore?.ToLedger());
}
}
}