chat2/06: offshore islands — revert the forced floor, tune the organic layer for coverage

The seeded floor from chat2/05 — FloorNorth/FloorSouth, the PCG32 placement, StampWeight,
StampRadiusFrac/StampCoreFrac/StampEdgeJitter, the centre separation and land gap, the
floor self-check and oracle (h), the centre rings on the tag overlay — is reverted out
whole, as a forward commit. It looked stamped. It is one checkout away at 3b96e06. What
remains is ONE island mechanism: the organic noise-field layer, with chat2/05 stage 2's
shape untouched (freq 16, crest 24 m pre-curve, core 0.25, sharpness 2.5, corners on,
moat + falloff test + outer bound unmoved), now OffshoreSettings.Organic(). The count per
hemisphere is a statistical outcome of the tuning, read off a table, never guaranteed.

The south was measured before anything moved (OffshoreDiagnosis, over task 01's pool at
2048): the premise that the south generates fewer islands is not what the field says.
The NORTH has 1.75x less island-eligible ocean (56 % of its ocean is zone vs 69 %; 233
zone rows per column vs 407) and under half the island candidates (1,183 over-threshold
peaks in zone vs 2,422). Per cell the south is richer too (484 vs 414 candidates per
Mcell). The gate that loses candidates is the moat in the north and the falloff test in
the south — the bulging south coast pushes the 0.72 contour out — but neither binds
enough to suppress it. So the fix is density, and the binding constraint for "a couple
north, consistently" is the north.

The knobs: Density (the main one, the calibration quantile) and SouthWeight (south density
= Density x weight — applied to the quantile, so a south island looks exactly like a north
one; there are just more). Three levels batched on 12 seeds at 4096, SouthWeight 1.25:
0.016 N 2/4.4/10 S 8/13.8/21 (every seed clears N >= 2, S >= 3, but N's minimum IS 2),
0.022 N 3/6.3/12 S 15/19.9/29 (the preset — the first level whose north minimum sits
comfortably above the target), 0.030 N 3/7.8/15 S 19/25.2/33 (the too-many bookend). The
south weight is the developer's stated preference, not a fix; the SouthWeight 1.0 tables
(S 6/10.9/16 at 0.022) are in the batch's scratch/ for the comparison.

The guards, so more density does not buy slop, each a revert by component membership:
specks (< 3e-5 of the map's area, ~500 cells at 4096 — 70-100 per field at these
densities, the noise caps the reference also surfaced), clusters (the smaller of two
islands whose shores are within 0.8 % of the map width — 0-7 per field), blobs (> 6e-4 —
never bit). And the reference's submerged humps, now attributed by HUMP rather than by
bounding box: the first 4096 plate probe showed hollow ghost rings beside islands — a
guard-dropped island's cap went back to seabed but its rim survived inside its
neighbour's bbox keep-region. A hump (one connected raised region) now stays only if it
holds a kept island, and a dropped island's own cap goes even inside a kept hump.

Oracle, all passing: a1 (Phase-1 dump), a3 (task-03 dump), a4 NEW — offshore OFF at 8192
bit-identical to the terrain-curve-v1 tag's own 04 gallery dump over 67,108,864 cells —
j0 shelf inert on land, and per field moat / mainland unmoved / tag-coastline / HMaxSeed
(unchanged, 1.289169) / classify on all 36 table fields and 4 plates. Batch:
BatchRoot(6, "offshore_organic_tune") — exactly 4 plates (three densities on 1063685222,
density_mid on 424242, the table's sparsest south) + count_table.md/.csv + diagnosis.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
This commit is contained in:
Stewart Howe 2026-08-21 06:22:54 -04:00
parent 3b96e06c1a
commit e8571b2778
13 changed files with 1021 additions and 573 deletions

View file

@ -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/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/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/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/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 `Hybrid()` (the reshape) | | `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 hemisphere convention** | | `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/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/Pass1Result.cs` | The height field **and the Phase-2 seams** |
| `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles | | `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles |
| `Scripts/HeightField.cs` | Raw `.f32` save/load — **the generation/presentation seam** | | `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 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 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 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 `OffshoreSettings.Organic()` (the reshape: small / low / flat / crisp, corners allowed — **the organic
islands whose positions vary per seed, organic extras weighted south, corners allowed). 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. > ### ⚠ Both default OFF — deliberately, and that is a decision to revisit.
> >

View file

@ -10,7 +10,7 @@ namespace IslaApocalypse.Tools
/// 2. the submarine COAST SHELF (<see cref="CoastShelf"/>) — chat2/05 stage 1 /// 2. the submarine COAST SHELF (<see cref="CoastShelf"/>) — chat2/05 stage 1
/// 3. the OFFSHORE ISLET layer (<see cref="OffshoreBlob"/>, /// 3. the OFFSHORE ISLET layer (<see cref="OffshoreBlob"/>,
/// <see cref="OffshoreZoneWeight"/>, <see cref="CalibrateThreshold"/>) — chat2/05 stage 1 /// <see cref="OffshoreZoneWeight"/>, <see cref="CalibrateThreshold"/>) — chat2/05 stage 1
/// + the RESHAPE helpers (<see cref="RigidBlob"/>, <see cref="StampWeight"/>) — stage 2 /// + the RESHAPE helper (<see cref="RigidBlob"/>) — stage 2
/// ///
/// The faithful functions keep the reference's names, constants and arithmetic verbatim (D-050); /// 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 /// 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.)
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
/// <summary> /// <summary>
@ -238,20 +239,5 @@ namespace IslaApocalypse.Tools
if (w <= 0f || edgeSharpness <= 1f) return w; if (w <= 0f || edgeSharpness <= 1f) return w;
return 1f - MathF.Pow(1f - w, edgeSharpness); return 1f - MathF.Pow(1f - w, edgeSharpness);
} }
/// <summary>
/// The seeded-floor stamp: a flat disc of radius <c>radius · coreFraction</c> at full weight,
/// then a smoothstep rim down to zero at <paramref name="radius"/>. Higher
/// <paramref name="coreFraction"/> ⇒ flatter top, crisper shore. This is the "rigid, defined
/// coastline" the reshape asks for, in its simplest honest form.
/// </summary>
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);
}
} }
} }

View file

@ -173,5 +173,64 @@ namespace IslaApocalypse.Tools
foreach (var c in comps) if (c.BridgedToMainland) b++; foreach (var c in comps) if (c.BridgedToMainland) b++;
return b; return b;
} }
// ═══ chat2/06 — the separation guard's geometry ═══
/// <summary>
/// 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. <paramref name="surfaced"/> is the pass's list of
/// every surfaced cell (the bodies), walked once.
/// </summary>
public static Dictionary<int, List<(int x, int y)>> BoundaryCells(bool[,] tag, int[] compId, int mapSize,
IEnumerable<(int x, int y, float h0)> surfaced)
{
var result = new Dictionary<int, List<(int x, int y)>>();
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;
}
/// <summary>Chebyshev gap between two components' bounding boxes (0 if they overlap). A lower bound on their true distance.</summary>
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);
}
/// <summary>
/// The minimum Chebyshev distance between two boundary sets, early-exiting once it is
/// known to be below <paramref name="below"/> (the caller only needs "closer than the
/// minimum or not").
/// </summary>
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;
}
} }
} }

View file

@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.Text;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>One hemisphere's share of the offshore zone, the gates, and the noise peaks.</summary>
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;
}
/// <summary>
/// ⭐ 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 <see cref="OffshorePass"/> 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 (<see cref="OffshoreAnalysis.HemisphereOfRow"/>), as the tag.
/// </summary>
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;
}
/// <param name="height">The post-shelf, pre-islet field (offshore OFF, shelf as the batch runs it).</param>
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;
}
/// <summary>One markdown row per hemisphere for a report table (see <see cref="TableHeader"/>).</summary>
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} |";
/// <summary>Sum a pool of reports per hemisphere (means for the per-column numbers).</summary>
public static (HemisphereDiagnosis north, HemisphereDiagnosis south) Pool(IReadOnlyList<Report> 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;
}
/// <summary>A one-paragraph reading of the pooled numbers: which hemisphere has less eligible ocean, and which gate binds it.</summary>
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();
}
}
}

View file

@ -0,0 +1 @@
uid://mriontjn1ihq

View file

@ -8,53 +8,75 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Tools namespace IslaApocalypse.Tools
{ {
/// <summary> /// <summary>
/// ⭐ THE OFFSHORE-ISLANDS BATCH (chat2/05) — the faithful rejoin as the control, then the /// ⭐ THE OFFSHORE-ISLANDS BATCH — chat2/06: ORGANIC-ONLY, TUNED FOR COVERAGE, SOUTH-WEIGHTED,
/// reshape: small / low / flat / rigid, loose-guaranteed (≥2 N, ≥4 S), organic extras weighted /// NO FORCED COUNT. (chat2/05's version of this tool — faithful / floor_only / hybrid / dense —
/// south, corners on. Judged on 2D maps, with the tag made visible. /// 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, /// PLATES (4, at ISLA_MAPSIZE, grayscale + .f32 + relief + tags overlay):
/// no floor. THE CONTROL. /// {plate}_density_low / _mid / _high three densities on ONE seed — the developer picks the
/// floor_only the reshape's seeded ≥2N/4S floor with the organic layer OFF — shows the /// look by eye (more islands vs slop), apples to apples.
/// guaranteed minimum and that positions vary per seed. /// {bulge}_density_mid the preset on the southern-bulge seed — the table seed
/// hybrid ⭐ floor + organic extras, reshaped, corners on. THE DELIVERABLE. Six seeds. /// whose south was SPARSEST at density_mid (auto-picked,
/// dense hybrid with ×3 organic density — a bookend for "how many is too many". /// 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 /// THE COUNT TABLE (data, not plates): per seed, north / south island counts across ~12 seeds for
/// plates anyway); an offshore-OFF / shelf-OFF field is generated per seed for the oracle only. /// each of the 3 density levels, with min / mean / max per hemisphere per level, the guard
/// ledger and the island sizes. This is how "a few south / a couple north, consistently" is
/// READ — as statistics of the tuning, never as a floor.
///
/// THE DIAGNOSIS (data): per hemisphere over the calibration seed pool — valid-zone area, which
/// gate binds, noise peaks clearing the threshold — measured BEFORE any knob moved (§2 of the
/// task). → <see cref="OffshoreDiagnosis"/>.
/// ///
/// ═══ THE CURVE IS THE TAGGED CURVE, UNCHANGED ═══ /// ═══ THE CURVE IS THE TAGGED CURVE, UNCHANGED ═══
/// ///
/// `continuous_restored` (tag terrain-curve-v1), calibrated on task 01's pool at the iteration /// `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 /// size WITH OFFSHORE OFF — islands are additive land on top of a curve that does not know they
/// exist. Their crest (2434 m pre-curve) lands inside the curve's preserved toe, which squashes /// exist. Oracle a1 / a3 / a4 prove offshore-off is bit-identical to Phase 1, task 03 and the
/// it to a few metres; because that toe is bit-preserved across curve tweaks, the islands' height /// tag's own 04 gallery dump.
/// is stable however the upper climb is tuned later.
/// ///
/// ═══ RUNNING IT ═══ /// ═══ RUNNING IT ═══
/// ///
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \ /// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/OffshoreIslandsTool.tscn /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/OffshoreIslandsTool.tscn
/// ///
/// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW /// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR
/// ISLA_MAPSIZE render size (default 4096 — islands need pixels to read) /// ISLA_MAPSIZE plate + table size (default 4096 — islands need pixels to read)
/// ISLA_CALIB_SIZE curve calibration size (default 2048, task 01's) /// ISLA_TABLE_SIZE count-table size (default = ISLA_MAPSIZE; a probe may drop it)
/// ISLA_SEEDS_HYBRID the six hybrid seeds /// ISLA_CALIB_SIZE curve calibration size (default 2048, task 01's) — also the diagnosis size
/// ISLA_SEEDS_SMALL the three seeds for faithful / floor_only / dense /// ISLA_TABLE_SEEDS the count-table seeds (default 12 below)
/// ISLA_PHASE1_SOURCE Phase-1 .f32 batch (default "02_pass1_port") /// ISLA_PLATE_SEED the three-density seed (default 1063685222)
/// ISLA_T03_SOURCE task-03 .f32 batch (default "03_mountain_restore") /// ISLA_BULGE_SEED the south-bulge seed (default 0 = auto: sparsest south at density_mid)
/// ISLA_DENS_LOW / ISLA_DENS_MID / ISLA_DENS_HIGH / ISLA_SOUTH_WEIGHT the tuning (probe overrides)
/// ISLA_OFF_MINAREA / ISLA_OFF_MINSEP / ISLA_OFF_MAXAREA the guards (probe overrides)
/// ISLA_OFF_FREQ / ISLA_OFF_CORE / ISLA_OFF_SHARP / ISLA_OFF_CREST the shape (probe overrides)
/// ISLA_TABLE_ONLY=1 probe: diagnosis + count table only (no regressions, no plates)
/// ISLA_SKIP_8K=1 skip the 8192 regression against the 04 gallery dump (a4)
/// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE the regression dumps' batches
/// </summary> /// </summary>
public partial class OffshoreIslandsTool : Node public partial class OffshoreIslandsTool : Node
{ {
private static readonly int[] DefaultHybridSeeds = { 1063685222, 20260821, 8675309, 123456789, 271828182, 999999937 }; /// <summary>
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.
/// </summary>
private static readonly int[] DefaultTableSeeds =
{
1063685222, 20260821, 8675309, 123456789, 271828182, 999999937,
20260819, 777001, 424242, 90210, 31337, 55555,
};
/// <summary>⚠ Task 01's pool, verbatim — the curve's identity.</summary> /// <summary>⚠ Task 01's pool, verbatim — the curve's identity. Also the diagnosis pool.</summary>
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 }; private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
private const int DefaultMapSize = 4096; private const int DefaultMapSize = 4096;
private const int DefaultCalibSize = 2048; private const int DefaultCalibSize = 2048;
private const int GallerySize = 8192; // the 04 gallery's render size
/// <summary>The consistency targets the table is read against: "a couple north, a few south".</summary>
private const int TargetNorth = 2, TargetSouth = 3;
public override void _Ready() public override void _Ready()
{ {
@ -63,6 +85,7 @@ namespace IslaApocalypse.Tools
{ {
GD.PrintErr("=================================================================="); GD.PrintErr("==================================================================");
GD.PrintErr($" REFUSED: {e.Message}"); GD.PrintErr($" REFUSED: {e.Message}");
GD.PrintErr(e.StackTrace);
GD.PrintErr("=================================================================="); GD.PrintErr("==================================================================");
GetTree().Quit(2); GetTree().Quit(2);
} }
@ -70,26 +93,36 @@ namespace IslaApocalypse.Tools
private sealed class Row private sealed class Row
{ {
public string Variant; public int Seed; public string Level; public int Seed;
public int CountN, CountS; public long Lifted; public float HMaxBefore, HMaxAfter; public int CountN, CountS, PreN, PreS, SpecksN, SpecksS, ClustersN, ClustersS, BlobsN, BlobsS;
public bool Ok; public long Lifted, SizeMin, SizeMed, SizeMax; public double SizeMean;
public string Centres; // the seeded floor's (x,y) list — the "positions vary per seed" evidence public float HMaxBefore, HMaxAfter;
public bool Ok; public ulong Ms;
}
private sealed class Level
{
public string Label; public OffshoreSettings Settings;
} }
private void Run() private void Run()
{ {
ToolingPaths.Configure(OS.GetUserDataDir()); ToolingPaths.Configure(OS.GetUserDataDir());
int task = EnvInt("ISLA_TASK", 5); int task = EnvInt("ISLA_TASK", 6);
string descr = EnvStr("ISLA_BATCH", "offshore_islands"); string descr = EnvStr("ISLA_BATCH", "offshore_organic_tune");
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize); int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
int tableSize = EnvInt("ISLA_TABLE_SIZE", mapSize);
int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize); int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
int[] hybridSeeds = EnvSeeds("ISLA_SEEDS_HYBRID", DefaultHybridSeeds); int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds);
int[] smallSeeds = EnvSeeds("ISLA_SEEDS_SMALL", DefaultSmallSeeds); int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222);
int bulgeSeedEnv = EnvInt("ISLA_BULGE_SEED", 0);
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port"); string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore"); string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool 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); string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot); DirAccess.MakeDirRecursiveAbsolute(batchRoot);
@ -97,39 +130,63 @@ namespace IslaApocalypse.Tools
var anchors = CurveAnchors.Default; var anchors = CurveAnchors.Default;
float sea = 0.15f; 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<Level>
{
new() { Label = "density_low", Settings = LevelSettings(EnvFloat("ISLA_DENS_LOW", OffshoreSettings.OrganicDensityLow)) },
new() { Label = "density_mid", Settings = LevelSettings(EnvFloat("ISLA_DENS_MID", OffshoreSettings.OrganicDensityMid)) },
new() { Label = "density_high", Settings = LevelSettings(EnvFloat("ISLA_DENS_HIGH", OffshoreSettings.OrganicDensityHigh)) },
};
Level mid = levels[1];
GD.Print("=================================================================="); GD.Print("==================================================================");
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("==================================================================");
GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize} (offshore OFF)"); GD.Print($"MapSize : {mapSize} (plates) table at {tableSize} curve calibrated at {calibSize} (offshore OFF) diagnosis at {calibSize}");
GD.Print($"hybrid : {string.Join(", ", hybridSeeds)}"); GD.Print($"table : {string.Join(", ", tableSeeds)}");
GD.Print($"others : {string.Join(", ", smallSeeds)}"); 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($"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("=================================================================="); GD.Print("==================================================================");
// ═══ 0. THE CURVE — continuous_restored, calibrated with offshore off ═══ // ═══ 0. THE CURVE — continuous_restored, calibrated with offshore off ═══
GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, 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($" {knots}");
GD.Print($" {calibration.Describe()}"); GD.Print($" {calibration.Describe()}");
// ═══ 1. REGRESSIONS at the calibration size — the things that must not have moved ═══ // ═══ 1. REGRESSIONS — the things that must not have moved ═══
GD.Print($"\n--- 1. REGRESSIONS at {calibSize}, seed {primary} ---");
var hard = new List<ShapingOracle.Check>(); var hard = new List<ShapingOracle.Check>();
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); Pass1Result p1 = Topography.Generate(offCfg);
var curveOff = offCfg.Clone(); curveOff.Curve = false; var curveOff = offCfg.Clone(); curveOff.Curve = false;
Pass2Result pOff = Shaping.Shape(p1, curveOff); 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", hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF == Phase-1 .f32 dump",
pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump)); pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
Pass2Result pRest = Shaping.Shape(p1, offCfg); 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)", hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF == task-03 .f32 dump (lowlands + curve untouched)",
pRest.Height, HeightField.Load(t03Dump, calibSize), calibSize, t03Dump)); 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)"; j0.Name = "shelf alone: every land cell bit-identical (shelf is below-sea only)";
hard.Add(j0); hard.Add(j0);
hard.Add(ShapingOracle.HMaxAfterOffshore(p1Shelf)); hard.Add(ShapingOracle.HMaxAfterOffshore(p1Shelf));
}
foreach (var c in hard) GD.Print(" " + c); foreach (var c in hard) GD.Print(" " + c);
// ═══ 2. VARIANTS ═══ // ⭐ a4 — offshore OFF at the 04 gallery's size == the terrain-curve-v1 tag's OWN output.
// ⚠ PROBE OVERRIDES on the reshape's organic knobs, so the hybrid can be swept in scratch // The literal "offshore-off is bit-identical to terrain-curve-v1", at full size.
// without a rebuild. The defaults live in OffshoreSettings.Hybrid() — that is the if (!skip8k)
// preset of record; these move it only when set. Printed into the INDEX either way.
OffshoreSettings HybridTuned()
{ {
var h = OffshoreSettings.Hybrid(); string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32");
h.FreqPerMapWidth = EnvFloat("ISLA_OFF_FREQ", h.FreqPerMapWidth); if (File.Exists(t04Dump))
h.DensityNorth = EnvFloat("ISLA_OFF_DENS_N", h.DensityNorth); {
h.DensitySouth = EnvFloat("ISLA_OFF_DENS_S", h.DensitySouth); GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, against {t04Dump} …");
h.CoreFraction = EnvFloat("ISLA_OFF_CORE", h.CoreFraction); var gCfg = BaseConfig(GallerySize, plateSeed, knots, anchors, calibration, "off");
h.EdgeSharpness = EnvFloat("ISLA_OFF_SHARP", h.EdgeSharpness); Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg);
h.MinIslandAreaFrac = EnvFloat("ISLA_OFF_MINAREA", h.MinIslandAreaFrac); var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump",
h.CrestM = EnvFloat("ISLA_OFF_CREST", h.CrestM); pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump);
return h; 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 variants = new List<(string label, int[] seeds, Action<TerrainGenConfig> mutate)> // ═══ 2. THE DIAGNOSIS — measure the south before touching a knob ═══
GD.Print($"\n--- 2. DIAGNOSIS (calibration pool at {calibSize}, shelf on, {mid.Label} thresholds) ---");
var diag = new List<OffshoreDiagnosis.Report>();
foreach (int s in CalibrationSeeds)
{ {
("faithful", smallSeeds, c => { c.CoastShelf = true; c.Offshore = OffshoreSettings.Faithful(); }), var dCfg = BaseConfig(calibSize, s, knots, anchors, calibration, "shelf_only");
("floor_only", smallSeeds, c => { c.CoastShelf = true; c.Offshore = HybridTuned(); c.Offshore.Organic = false; }), dCfg.CoastShelf = true;
("hybrid", hybridSeeds, c => { c.CoastShelf = true; c.Offshore = HybridTuned(); }), Pass1Result pShelf = Topography.Generate(dCfg);
("dense", smallSeeds, c => { c.CoastShelf = true; c.Offshore = HybridTuned(); var rep = OffshoreDiagnosis.Run(pShelf.Height, pShelf.PreTrenchFalloff, calibSize, s, sea, mid.Settings, dCfg.Scale);
c.Offshore.DensityNorth *= 3f; c.Offshore.DensitySouth *= 3f; }), 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} " +
if (!string.IsNullOrWhiteSpace(only)) $"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 keep = new HashSet<string>(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)");
} }
var (poolN, poolS) = OffshoreDiagnosis.Pool(diag);
string interpretation = OffshoreDiagnosis.Interpret(poolN, poolS);
GD.Print(" " + interpretation);
GD.Print($"\n--- 2. VARIANTS at {mapSize} ---"); // ═══ 3. THE COUNT TABLE — ~12 seeds × 3 levels ═══
var offFields = new Dictionary<int, Pass1Result>(); // per seed, shelf off + offshore off GD.Print($"\n--- 3. COUNT TABLE at {tableSize} ---");
var rows = new List<Row>(); var rows = new List<Row>();
var perVariantChecks = new List<ShapingOracle.Check>(); var perFieldChecks = new List<ShapingOracle.Check>();
var knobs = new Dictionary<string, string>();
bool notesShown = false; bool notesShown = false;
foreach (int seed in tableSeeds)
foreach (var (label, seeds, mutate) in variants)
{ {
foreach (int seed in seeds) Pass1Result p1Off = Topography.Generate(BaseConfig(tableSize, seed, knots, anchors, calibration, "off"));
foreach (var lv in levels)
{ {
if (!offFields.TryGetValue(seed, out Pass1Result p1Off)) var cfg = BaseConfig(tableSize, seed, knots, anchors, calibration, lv.Label);
{ cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone();
p1Off = Topography.Generate(BaseConfig(mapSize, seed, knots, anchors, calibration, "off"));
offFields[seed] = p1Off;
}
var cfg = BaseConfig(mapSize, seed, knots, anchors, calibration, label);
mutate(cfg);
knobs[label] = cfg.Offshore.Describe();
Pass1Result p1 = Topography.Generate(cfg); Pass1Result p1 = Topography.Generate(cfg);
Pass2Result p2 = Shaping.Shape(p1, 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) var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, tableSize);
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 (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps); var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps);
var checks = new List<ShapingOracle.Check>(); var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0);
if (cfg.Offshore.FloorNorth > 0 || cfg.Offshore.FloorSouth > 0) var checks = new List<ShapingOracle.Check>
checks.Add(ShapingOracle.OffshoreFloor(p1, cfg.Offshore.FloorNorth, cfg.Offshore.FloorSouth, sea, comps)); {
checks.Add(ShapingOracle.MoatIntact(p1, comps)); ShapingOracle.MoatIntact(p1, comps),
checks.Add(ShapingOracle.MainlandUnmoved(p1Off, p1, sea)); ShapingOracle.MainlandUnmoved(p1Off, p1, sea),
checks.Add(ShapingOracle.TagCoastlineConsistent(p2, sea)); ShapingOracle.TagCoastlineConsistent(p2, sea),
checks.Add(ShapingOracle.HMaxAfterOffshore(p1)); ShapingOracle.HMaxAfterOffshore(p1),
checks.Add(ShapingOracle.ClassifyFidelity(p1, p2)); ShapingOracle.ClassifyFidelity(p1, p2),
foreach (var c in checks) { c.Name += $" [{label} {seed}]"; perVariantChecks.Add(c); } };
foreach (var c in checks) { c.Name += $" [{lv.Label} {seed}]"; perFieldChecks.Add(c); }
bool ok = checks.TrueForAll(c => c.Passed); bool ok = checks.TrueForAll(c => c.Passed);
WriteVariant(batchRoot, p1, p2, sea, anchors, skipRaw, cn, cs); var row = new Row
var cl = new List<string>();
foreach (var (cx, cy, _) in p1.OffshoreCentres) cl.Add($"({cx},{cy})");
rows.Add(new Row
{ {
Variant = label, Seed = seed, CountN = cn, CountS = cs, Level = lv.Label, Seed = seed, CountN = cn, CountS = cs,
Lifted = p1.OffshoreLiftedCells, HMaxBefore = p1.HMaxSeedBeforeOffshore, HMaxAfter = p1.HMaxSeed, Ok = ok, Lifted = p1.OffshoreLiftedCells, HMaxBefore = p1.HMaxSeedBeforeOffshore, HMaxAfter = p1.HMaxSeed,
Centres = cl.Count == 0 ? "—" : string.Join(" ", cl), SizeMin = szMin, SizeMed = szMed, SizeMean = szMean, SizeMax = szMax, Ok = ok, Ms = p1.ElapsedMs,
}); };
GD.Print($" {label,-11} seed {seed,-11} islands N {cn,2} S {cs,2} lifted {p1.OffshoreLiftedCells,8:N0} " + ReadGuardLedger(p1, row);
$"hMax {p1.HMaxSeedBeforeOffshore:F4}→{p1.HMaxSeed:F4} {(ok ? "ok" : " CHECK FAILED")} {p1.ElapsedMs} ms"); rows.Add(row);
GD.Print($" {lv.Label,-13} seed {seed,-11} N {cn,2} S {cs,2} (pre-guard N {row.PreN,2} S {row.PreS,2}; specks {row.SpecksN + row.SpecksS,2} clusters {row.ClustersN + row.ClustersS,2} blobs {row.BlobsN + row.BlobsS,2}) " +
$"size med {szMed,5} max {szMax,6} {(ok ? "ok" : " CHECK FAILED")} {p1.ElapsedMs} ms");
}
}
var stats = LevelStats(levels, rows);
GD.Print("\n per level (min / mean / max):");
foreach (var st in stats)
GD.Print($" {st.Label,-13} N {st.MinN} / {st.MeanN:F1} / {st.MaxN} S {st.MinS} / {st.MeanS:F1} / {st.MaxS} " +
$"seeds with N≥{TargetNorth} & S≥{TargetSouth}: {st.MeetBoth}/{st.Seeds} S≥N: {st.SouthAtLeastNorth}/{st.Seeds} " +
$"guards: specks {st.Specks} clusters {st.Clusters} blobs {st.Blobs}");
// ═══ 4. THE PLATES — exactly four ═══
int bulgeSeed = bulgeSeedEnv > 0 ? bulgeSeedEnv : PickBulgeSeed(rows, mid.Label, plateSeed);
GD.Print($"\n southern-bulge seed: {bulgeSeed}{(bulgeSeedEnv > 0 ? " (ISLA_BULGE_SEED)" : " (auto: sparsest south at density_mid among the table seeds)")}");
var plates = new List<(int seed, Level level)> { (plateSeed, levels[0]), (plateSeed, levels[1]), (plateSeed, levels[2]), (bulgeSeed, mid) };
var plateRows = new List<Row>();
if (!tableOnly)
{
GD.Print($"\n--- 4. PLATES at {mapSize} ---");
foreach (var (seed, lv) in plates)
{
var cfg = BaseConfig(mapSize, seed, knots, anchors, calibration, lv.Label);
cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone();
Pass1Result p1 = Topography.Generate(cfg);
Pass2Result p2 = Shaping.Shape(p1, cfg);
var comps = OffshoreAnalysis.Components(p1.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 ***")}"); 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("\n==================================================================");
GD.Print($" DONE — {batchRoot}"); GD.Print($" DONE — {batchRoot}");
@ -245,9 +331,68 @@ namespace IslaApocalypse.Tools
GetTree().Quit(allOk ? 0 : 3); 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<LevelStat> LevelStats(List<Level> levels, List<Row> rows)
{
var outp = new List<LevelStat>();
foreach (var lv in levels)
{
var st = new LevelStat { Label = lv.Label, MinN = int.MaxValue, MinS = int.MaxValue };
double sumN = 0, sumS = 0; var meds = new List<long>();
foreach (var r in rows)
{
if (r.Level != lv.Label) continue;
st.Seeds++;
st.MinN = Math.Min(st.MinN, r.CountN); st.MaxN = Math.Max(st.MaxN, r.CountN); sumN += r.CountN;
st.MinS = Math.Min(st.MinS, r.CountS); st.MaxS = Math.Max(st.MaxS, r.CountS); sumS += r.CountS;
if (r.CountN >= TargetNorth && r.CountS >= TargetSouth) st.MeetBoth++;
if (r.CountS >= r.CountN) st.SouthAtLeastNorth++;
st.Specks += r.SpecksN + r.SpecksS; st.Clusters += r.ClustersN + r.ClustersS; st.Blobs += r.BlobsN + r.BlobsS;
meds.Add(r.SizeMed); st.SizeMax = Math.Max(st.SizeMax, r.SizeMax);
}
if (st.Seeds == 0) { st.MinN = st.MinS = 0; }
else { st.MeanN = sumN / st.Seeds; st.MeanS = sumS / st.Seeds; meds.Sort(); st.SizeMed = meds[meds.Count / 2]; }
outp.Add(st);
}
return outp;
}
/// <summary>The table seed whose SOUTH count at the preset level is lowest (ties → lower total, then first in the pool), excluding the plate seed.</summary>
private static int PickBulgeSeed(List<Row> rows, string midLabel, int plateSeed)
{
int best = 0, bestS = int.MaxValue, bestTotal = int.MaxValue;
foreach (var r in rows)
{
if (r.Level != midLabel || r.Seed == plateSeed) continue;
int total = r.CountN + r.CountS;
if (r.CountS < bestS || (r.CountS == bestS && total < bestTotal)) { best = r.Seed; bestS = r.CountS; bestTotal = total; }
}
return best == 0 ? plateSeed : best;
}
/// <summary>Copy the pass's guard ledger into a table row (the pass reports it; the tool does not recompute it).</summary>
private static void ReadGuardLedger(Pass1Result p1, Row row)
{
var l = p1.OffshoreLedger;
if (l == null) return;
row.PreN = l.PreGuardNorth; row.PreS = l.PreGuardSouth;
row.SpecksN = l.SpecksNorth; row.SpecksS = l.SpecksSouth;
row.ClustersN = l.ClustersNorth; row.ClustersS = l.ClustersSouth;
row.BlobsN = l.BlobsNorth; row.BlobsS = l.BlobsSouth;
}
// ---- the curve, measured exactly as tasks 03/04/05 did ----------------
private static (CurveKnots, ClimbCalibration, Dictionary<int, Pass1Result>) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
{ {
var rawPool = new LandHistogram(sea); var rawPool = new LandHistogram(sea);
var pass1 = new Dictionary<int, Pass1Result>(); var pass1 = new Dictionary<int, Pass1Result>();
@ -284,7 +429,7 @@ namespace IslaApocalypse.Tools
var cal = ClimbCalibration.FromPercentiles(pcts, rawQ, outQ, ceilingRaw, var cal = ClimbCalibration.FromPercentiles(pcts, rawQ, outQ, ceilingRaw,
HeightCurve.EffectiveSpikeMax(pass1[CalibrationSeeds[0]].HMaxSeed, knots, anchors), HeightCurve.EffectiveSpikeMax(pass1[CalibrationSeeds[0]].HMaxSeed, knots, anchors),
anchors.RedCeil, anchors.PeakCap, mountainLift: 1.0f, peakSharpness: 1.0f); 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, private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
@ -298,7 +443,7 @@ namespace IslaApocalypse.Tools
// ---- output ----------------------------------------------------------- // ---- 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) CurveAnchors anchors, bool skipRaw, int countN, int countS)
{ {
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}"); 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. // ⭐ The tag overlay — the one artifact that shows the DATA this pass set.
TagOverlayRenderer.SavePng(p2.Height, p2.IsOffshoreIsland, p2.IslandHemisphere, p2.MapSize, sea, 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, private static string CountTableMarkdown(List<Level> levels, List<Row> rows, List<LevelStat> stats)
int[] hybridSeeds, int[] smallSeeds, List<Row> rows, Dictionary<string, string> knobs,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perVariant, bool allOk)
{ {
var sb = new StringBuilder(); 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();
sb.AppendLine("Stage 1 rejoins the reference's coast shelf + islets verbatim (`faithful`, the control)."); sb.AppendLine("**Per level — the consistency read (min / mean / max over the seeds):**");
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(); sb.AppendLine();
sb.AppendLine($"| Level | density N / S | seeds | **N min / mean / max** | **S min / mean / max** | seeds with N ≥ {TargetNorth} & S ≥ {TargetSouth} | seeds with S ≥ N | specks / clusters / blobs reverted | median island (cells) | largest island (cells) |");
sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|");
foreach (var st in stats)
{
var lv = levels.Find(l => l.Label == st.Label);
sb.AppendLine($"| `{st.Label}` | {lv.Settings.Density:F4} / {lv.Settings.DensitySouth:F4} | {st.Seeds} | **{st.MinN} / {st.MeanN:F1} / {st.MaxN}** | **{st.MinS} / {st.MeanS:F1} / {st.MaxS}** | " +
$"**{st.MeetBoth} / {st.Seeds}** | {st.SouthAtLeastNorth} / {st.Seeds} | {st.Specks} / {st.Clusters} / {st.Blobs} | {st.SizeMed} | {st.SizeMax} |");
}
return sb.ToString();
}
private static void WriteCountTable(string batchRoot, int tableSize, List<Level> levels, List<Row> rows, List<LevelStat> stats)
{
var sb = new StringBuilder();
sb.AppendLine($"# The count table — {rows.Count / Math.Max(1, levels.Count)} seeds × {levels.Count} density levels at {tableSize}");
sb.AppendLine();
sb.AppendLine("Tagged 8-connected island components per hemisphere (centroid), after the guards. **No count is forced**;");
sb.AppendLine("this is the statistical outcome of the tuning. NORTH = rows [0, N/2), SOUTH = rows [N/2, N); y runs south.");
sb.AppendLine();
sb.Append(CountTableMarkdown(levels, rows, stats));
WriteText(Path.Combine(batchRoot, "count_table.md"), sb.ToString());
var csv = new StringBuilder();
csv.AppendLine("level,seed,density_n,density_s,north,south,total,preguard_n,preguard_s,specks,clusters,blobs,size_min,size_median,size_mean,size_max,lifted_cells,hmax_before,hmax_after,oracle,ms");
foreach (var r in rows)
{
var lv = levels.Find(l => l.Label == r.Level);
csv.AppendLine(string.Join(",", r.Level, r.Seed, lv.Settings.Density.ToString("F5", System.Globalization.CultureInfo.InvariantCulture),
lv.Settings.DensitySouth.ToString("F5", System.Globalization.CultureInfo.InvariantCulture),
r.CountN, r.CountS, r.CountN + r.CountS, r.PreN, r.PreS, r.SpecksN + r.SpecksS, r.ClustersN + r.ClustersS, r.BlobsN + r.BlobsS,
r.SizeMin, r.SizeMed, r.SizeMean.ToString("F1", System.Globalization.CultureInfo.InvariantCulture), r.SizeMax, r.Lifted,
r.HMaxBefore.ToString("G9", System.Globalization.CultureInfo.InvariantCulture), r.HMaxAfter.ToString("G9", System.Globalization.CultureInfo.InvariantCulture),
r.Ok ? "pass" : "FAIL", r.Ms));
}
WriteText(Path.Combine(batchRoot, "count_table.csv"), csv.ToString());
}
private static string DiagnosisMarkdown(int calibSize, Level mid, List<OffshoreDiagnosis.Report> diag,
HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation)
{
var sb = new StringBuilder();
sb.AppendLine($"Measured on the calibration pool at {calibSize} (shelf on, offshore off — the field the islet layer sees), with the");
sb.AppendLine($"`{mid.Label}` thresholds (density N {mid.Settings.Density:F4} / S {mid.Settings.DensitySouth:F4}). *Zone* = cells with zone weight > 0");
sb.AppendLine("(pass the falloff test + moat + outer bound); *peaks* = strict 8-neighbour local maxima of the islet noise field on sea cells;");
sb.AppendLine("*sole blocker* = sea cells that fail exactly one gate (loosen that gate and they join the zone); *lost > thr* = over-threshold");
sb.AppendLine("peaks outside the zone, with the gate(s) that excluded them.");
sb.AppendLine();
sb.AppendLine(OffshoreDiagnosis.TableHeader());
foreach (var r in diag)
{
sb.AppendLine(OffshoreDiagnosis.TableRow(r, r.North));
sb.AppendLine(OffshoreDiagnosis.TableRow(r, r.South));
}
var poolRep = new OffshoreDiagnosis.Report { Seed = 0 };
sb.AppendLine(OffshoreDiagnosis.TableRow(poolRep, poolN).Replace("| `0` |", "| **pool** |"));
sb.AppendLine(OffshoreDiagnosis.TableRow(poolRep, poolS).Replace("| `0` |", "| **pool** |"));
sb.AppendLine();
sb.AppendLine($"**Reading:** {interpretation}");
return sb.ToString();
}
private static void WriteDiagnosis(string batchRoot, int calibSize, Level mid, List<OffshoreDiagnosis.Report> diag,
HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation)
{
var sb = new StringBuilder();
sb.AppendLine("# The south diagnosis — zone area, gates and peaks per hemisphere");
sb.AppendLine();
sb.Append(DiagnosisMarkdown(calibSize, mid, diag, poolN, poolS, interpretation));
WriteText(Path.Combine(batchRoot, "diagnosis.md"), sb.ToString());
}
private static void WriteIndex(string batchRoot, int mapSize, int tableSize, int calibSize, int plateSeed, int bulgeSeed,
int[] tableSeeds, List<Level> levels, List<Row> rows, List<LevelStat> stats, List<Row> plateRows,
List<OffshoreDiagnosis.Report> diag, HemisphereDiagnosis poolN, HemisphereDiagnosis poolS, string interpretation,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> perField, bool allOk, bool tableOnly)
{
var mid = levels[1];
var sb = new StringBuilder();
sb.AppendLine("# Batch 06 — offshore islands: organic-only, tuned for coverage, south-weighted, no forced count");
sb.AppendLine();
sb.AppendLine("The chat2/05 forced floor (seeded stamps, guaranteed ≥2 N / ≥4 S) is **reverted out** — it looked stamped.");
sb.AppendLine("The **organic noise-field layer is the only island mechanism**; this batch tunes its **density** (the main");
sb.AppendLine("knob) and a **south weight** so it yields *a few south / a couple north* **consistently across seeds, as a");
sb.AppendLine("statistical outcome** — never a hard-coded count. Guards against slop: specks, clusters and blobs are");
sb.AppendLine("reverted whole; every surviving island is a noise outline, small, low, crisp.");
sb.AppendLine();
if (tableOnly) sb.AppendLine("> ⚠ **ISLA_TABLE_ONLY** — a probe run: diagnosis + count table only, no regressions, no plates. Not the batch of record.\n");
sb.AppendLine("## ⭐ Open this first"); sb.AppendLine("## ⭐ Open this first");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine($"1. **`{primary}_hybrid/tags.png`** — the tag overlay: grey mainland, cyan = island N, orange = island S,"); sb.AppendLine($"1. **`{plateSeed}_density_mid/tags.png`** — the preset of record: 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(" No rings any more — nothing is seeded. Then its `relief.png` for the shape.");
sb.AppendLine($"2. **`{primary}_hybrid/relief.png`** beside **`{primary}_faithful/relief.png`** — the reshape vs the reference."); sb.AppendLine($"2. **`{plateSeed}_density_low/`** and **`{plateSeed}_density_high/`** beside it — the same seed, less and more density;");
sb.AppendLine($"3. Then the other hybrid seeds' `tags.png` — the floor holds everywhere; the extras vary."); 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();
sb.AppendLine($"**Hemisphere convention (from the code, not invented):** y runs SOUTH. NORTH = rows `[0, {mapSize / 2})`,"); 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($"SOUTH = rows `[{mapSize / 2}, {mapSize})`. Component hemisphere is by centroid; the tag per cell is by row.");
sb.AppendLine("snow-town north, shipwreck south. Component hemisphere is by centroid; the tag per cell is by row.");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("## ⭐ The count table — the floor, and the spread above it"); sb.AppendLine("## The four plates");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("| Variant | Seed | N | S | total | lifted cells | HMaxSeed before → after | oracle | seeded floor centres (x,y) |"); sb.AppendLine("| Plate | N | S | total | pre-guard N / S | specks / clusters / blobs | lifted cells | oracle |");
sb.AppendLine("|---|---|---|---|---|---|---|---|---|"); sb.AppendLine("|---|---|---|---|---|---|---|---|");
foreach (var r in rows) foreach (var r in plateRows)
sb.AppendLine($"| `{r.Variant}` | `{r.Seed}` | **{r.CountN}** | **{r.CountS}** | {r.CountN + r.CountS} | {r.Lifted:N0} | " + 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**")} |");
$"{r.HMaxBefore:F4} → {r.HMaxAfter:F4}{(r.HMaxBefore != r.HMaxAfter ? " " : "")} | {(r.Ok ? "pass" : "**FAIL**")} | {r.Centres} |"); if (plateRows.Count == 0) sb.AppendLine("| *(no plates — probe run)* | | | | | | | |");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("`floor_only` / `hybrid` / `dense` carry a floor of **≥2 N / ≥4 S**; `faithful` has no floor (the"); sb.AppendLine($"## ⭐ The count table — {tableSeeds.Length} seeds × 3 levels at {tableSize} (the consistency evidence)");
sb.AppendLine("reference's probabilistic layer) and is the control.");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("## The knobs per variant"); sb.Append(CountTableMarkdown(levels, rows, stats));
sb.AppendLine(); 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("|---|---|"); 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();
sb.AppendLine("The coast shelf is ON for every variant (strength 0.775, scale 100 m, BitDecrement-clamped). It is"); sb.AppendLine("The coast shelf is ON for every field (strength 0.775, scale 100 m, BitDecrement-clamped); invisible on these");
sb.AppendLine("invisible on these hypsometric plates — ported faithfully, judged when water renders."); 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();
sb.AppendLine("## ⚠ The palette is PROVISIONAL"); sb.AppendLine("## ⚠ The palette is PROVISIONAL");
sb.AppendLine(); sb.AppendLine();
@ -371,29 +619,32 @@ namespace IslaApocalypse.Tools
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("## The oracle"); sb.AppendLine("## The oracle");
sb.AppendLine(); 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();
sb.AppendLine(ShapingOracle.ToMarkdownTable(hard)); sb.AppendLine(hard.Count == 0 ? "*(skipped — probe run)*\n" : ShapingOracle.ToMarkdownTable(hard));
sb.AppendLine("Per variant × seed (floor h · moat i · mainland unmoved j · tag/coastline k · HMaxSeed l · classify b):"); sb.AppendLine("Per field (moat i · mainland unmoved j · tag/coastline k · HMaxSeed l · classify b):");
sb.AppendLine(); 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($"**{(allOk ? "ALL HARD CHECKS PASS" : " FAILURES do not judge this batch")}**");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("## Disposability"); sb.AppendLine("## Disposability");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("| Artifact | Keep? |"); sb.AppendLine("| Artifact | Keep? |");
sb.AppendLine("|---|---|"); 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("| `grayscale.png` | ♻ regenerable from the `.f32` |");
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code — large, clear freely |"); sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code — large, clear freely |");
sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |"); sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
sb.AppendLine(); 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"); private static void WriteText(string path, string text)
using var f = Godot.FileAccess.Open(index, Godot.FileAccess.ModeFlags.Write); {
if (f == null) { GD.PrintErr($"could not write {index}"); return; } using var f = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Write);
f.StoreString(sb.ToString()); if (f == null) { GD.PrintErr($"could not write {path}"); return; }
f.StoreString(text);
} }
// ---- env helpers -------------------------------------------------------- // ---- env helpers --------------------------------------------------------

View file

@ -6,8 +6,9 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Tools namespace IslaApocalypse.Tools
{ {
/// <summary> /// <summary>
/// ⭐⭐ PASS 1b — THE COAST SHELF AND THE OFFSHORE ISLETS (chat2/05). Runs over the finished /// ⭐⭐ PASS 1b — THE COAST SHELF AND THE OFFSHORE ISLETS (chat2/05, retuned chat2/06). Runs over
/// pass-1 arrays, IN PLACE, before <c>HMaxSeed</c> is taken and before anything classifies. /// the finished pass-1 arrays, IN PLACE, before <c>HMaxSeed</c> is taken and before anything
/// classifies.
/// ///
/// ═══ WHERE THIS SITS, AND WHY IT IS A SECOND SWEEP ═══ /// ═══ 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 /// 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 /// 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. /// 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 /// The second sweep is also what lets the slop guards see whole islands: a component rule needs
/// depth/falloff field to exist first, which a single pixel loop cannot provide. /// the finished field.
/// ///
/// ⚠⚠ THE ORDERING THAT CLOSES chat2/00 DRIFT §2: the caller recomputes <c>HMaxSeed</c> AFTER this /// ⚠⚠ THE ORDERING THAT CLOSES chat2/00 DRIFT §2: the caller recomputes <c>HMaxSeed</c> AFTER this
/// pass, as the reference did, so the curve's per-seed peak normalization sees the same maximum /// 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, /// the reference saw. Expected to be unchanged (a ~34 m crest is far below any peak) — reported,
/// not assumed. /// not assumed.
/// ///
/// ═══ THE THREE SUB-PASSES ═══ /// ═══ THE SUB-PASSES ═══
/// ///
/// 1. SHELF every below-sea cell; depth-preserving; held strictly below sea by BitDecrement. /// 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 / /// 2. ORGANIC ⭐ THE ONE ISLAND MECHANISM — the reference's noise layer, faithful or reshaped
/// crisper, south-weighted) — every below-sea cell inside the zone mask. /// (smaller / lower / flatter / crisper, density + south weight), every below-sea
/// 3. FLOOR (Hybrid only) a seed-derived RNG places ≥N north / ≥S south stamps, each /// cell inside the zone mask. Nothing places an island from a centre; nothing
/// min-separated from the others AND clear of ALL existing land by a gap, so each /// guarantees a count. (chat2/05's seeded floor was reverted out in chat2/06 — it
/// stamp is its own connected component BY CONSTRUCTION. Positions vary per seed; /// looked stamped. It lives in git history.)
/// there are no reserved zones. Refuses loudly if the floor cannot be placed. /// 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 /// 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 /// layer per pixel — that is the reference. Nothing in this pass can raise a cell outside the
/// fully inside the zone (weight exactly 1), and the stamp's every pixel is still multiplied by /// zone, so no island can bridge to shore or appear in a lake / the crater bay.
/// the zone weight. A guaranteed island cannot be guaranteed onto the mainland or into a lake.
/// ///
/// ═══ ⚠ THE ONE HONEST COUPLING ═══ /// ═══ ⚠ THE ONE HONEST COUPLING ═══
/// ///
@ -58,15 +59,32 @@ namespace IslaApocalypse.Tools
{ {
public bool[,] Tag; // null when the islet layer is off (shelf only) public bool[,] Tag; // null when the islet layer is off (shelf only)
public byte[,] Hemi; public byte[,] Hemi;
public long ShelfCells, LiftedOrganic, LiftedSeeded; public long ShelfCells, LiftedOrganic;
public float ThresholdNorth = float.NaN, ThresholdSouth = float.NaN; public float ThresholdNorth = float.NaN, ThresholdSouth = float.NaN;
public List<(int x, int y, int r)> Centres = new();
public List<string> Notes = new(); public List<string> Notes = new();
public int CountNorth, CountSouth, Bridged; 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)> 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,
};
} }
/// <summary> /// <summary>
@ -121,12 +139,10 @@ namespace IslaApocalypse.Tools
r.Hemi = new byte[mapSize, mapSize]; r.Hemi = new byte[mapSize, mapSize];
bool faithful = s.Mode == OffshoreMode.Faithful; 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 float crest = sea + WorldScale.RawFromMetres(s.CrestM); // seaHere + OFFSHORE_ISLAND_H_M / 251f
// ═══ 2. THE ORGANIC LAYER ═══ // ═══ 2. THE ORGANIC LAYER — the one island mechanism ═══
var liftedOrigin = r.LiftedOrigin; // every lift, for the debris guard
if (s.Organic)
{ {
FastNoiseLite noise = TerrainNoise.CreateModulation(seed, s.SeedOffset, s.FreqPerMapWidth, scale); 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 // 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 // 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. // to be. Verbatim: stride 8, (side)² samples, (noise + 1) * 0.5.
const int stride = 8; float[] samples = CalibrationSamples(noise, mapSize);
int side = mapSize / stride; float thrN = IslandFalloff.CalibrateThreshold(samples, s.Density);
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 thrS = faithful ? thrN : IslandFalloff.CalibrateThreshold(samples, s.DensitySouth); float thrS = faithful ? thrN : IslandFalloff.CalibrateThreshold(samples, s.DensitySouth);
r.ThresholdNorth = thrN; r.ThresholdSouth = thrS; r.ThresholdNorth = thrN; r.ThresholdSouth = thrS;
@ -167,14 +177,7 @@ namespace IslaApocalypse.Tools
// The threshold: one number in faithful mode; north/south blended smoothly // The threshold: one number in faithful mode; north/south blended smoothly
// across the midline in the reshape, so a straddling island is not sliced. // across the midline in the reshape, so a straddling island is not sliced.
float thr; float thr = faithful ? thrN : BlendedThreshold(y, mid, band, thrN, thrS);
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 blob = (faithful float blob = (faithful
? IslandFalloff.OffshoreBlob(v, thr) ? IslandFalloff.OffshoreBlob(v, thr)
@ -185,14 +188,14 @@ namespace IslaApocalypse.Tools
float before = h; float before = h;
h = h + (crest - h) * blob; // Mathf.Lerp, written out h = h + (crest - h) * blob; // Mathf.Lerp, written out
height[x, y] = h; 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) if (before < sea && h >= sea)
{ {
r.LiftedOrganic++; r.LiftedOrganic++;
r.Tag[x, y] = true; r.Tag[x, y] = true;
r.Hemi[x, y] = OffshoreAnalysis.HemisphereOfRow(y, mapSize); 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."); $"(range {Min(samples):F3}..{Max(samples):F3}); lifted {r.LiftedOrganic:N0} cells above sea.");
} }
// ═══ 3. THE SEEDED FLOOR (Hybrid only) ═══ // ═══ 3. THE SLOP GUARDS — reshape only; over everything the pass raised ═══
if (s.Mode == OffshoreMode.Hybrid && (s.FloorNorth > 0 || s.FloorSouth > 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)>();
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 // Four kinds of slop, three component rules and one location rule:
// threshold surfaces a cap of a few cells — not an island. (b) SUBMERGED BUMPS: every // (a) SPECKS a local maximum that barely clears the threshold surfaces a cap of a few
// blob with any weight lerps the seabed toward the crest whether or not it surfaces, so // cells — not an island. Reverted by MEMBERSHIP (a speck inside a real
// the ocean fills with shallow humps — which the first probe plate showed as a field of // island's skirt is still a speck).
// pale speckles across the southern sea, and which the water pass would later draw as a // (b) BLOBS a superlevel region that merged several maxima into one sprawling
// reef field nobody asked for. (The faithful control keeps both; that is the reference.) // 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.
// The rule: find the surviving islands (components at or above the minimum area), take // (c) CLUSTERS two islands whose shores are closer than the minimum separation read as
// each one's bounding box with a margin as its KEEP region, and put every organic raise // one; the SMALLER goes (greedy by size, so the largest of a cluster stays).
// OUTSIDE all keep regions back to the height it had — surfaced or not, tagged or not. A // (d) SUBMERGED BUMPS — every blob with any weight lerps the seabed toward the crest
// real island keeps its own submerged skirt; everything else goes back to seabed. // 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
// ⚠ Runs after the floor so a stamp's rim satellite (w·zone crossing the waterline // asked for). Attributed by HUMP: a hump (connected raised region) that
// non-monotonically) is caught too; the stamp bodies are ~800+ cells and survive by // holds a kept island is that island's own skirt and stays; one that holds
// construction. The floor is still asserted below. // none goes back to seabed; a dropped island's own cap goes too.
if (guardOn && (r.RaisedOrigin.Count > 0 || r.LiftedOrigin.Count > 0)) // 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))
{ {
long minCells = Math.Max(1L, (long)Math.Round(s.MinIslandAreaFrac * (double)mapSize * mapSize)); 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;
var pre = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize, out int[] compId); 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 var dropped = new HashSet<int>();
// minimum is reverted and untagged, wherever it sits (a speck inside a real island's var byId = new Dictionary<int, IslandComponent>();
// skirt is still a speck — the first keep-box cut let those survive as 1-cell islands). foreach (var c in pre) byId[c.Id] = c;
// 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. // (a) specks and (b) blobs — by size.
var small = new HashSet<int>();
var keep = new List<(int x0, int y0, int x1, int y1)>();
foreach (var c in pre) foreach (var c in pre)
{ {
if (c.Cells < minCells) { small.Add(c.Id); continue; } if (c.Cells < minCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.SpecksNorth, ref r.SpecksSouth); }
int w = c.MaxX - c.MinX + 1, hgt = c.MaxY - c.MinY + 1; else if (c.Cells > maxCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.BlobsNorth, ref r.BlobsSouth); }
int margin = Math.Max(4, Math.Max(w, hgt) / 2);
keep.Add((c.MinX - margin, c.MinY - margin, c.MaxX + margin, c.MaxY + margin));
} }
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<IslandComponent>();
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<IslandComponent>();
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<bool> { false }; // index 0 unused
{
var stack = new Stack<int>();
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; if (x >= x0 && x <= x1 && y >= y0 && y <= y1) return true;
return false; return false;
} }
long revertedRaised = 0, revertedLifted = 0; long revertedLifted = 0, revertedRaised = 0;
foreach (var (x, y, h0) in r.LiftedOrigin) 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; height[x, y] = h0; r.Tag[x, y] = false; r.Hemi[x, y] = OffshoreAnalysis.HemiNone;
revertedLifted++; revertedLifted++;
} }
foreach (var (x, y, h0) in r.RaisedOrigin) foreach (var (x, y, h0) in r.RaisedOrigin)
{ {
if (r.Tag[x, y]) continue; // a kept island's own surfaced cell 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++; } if (height[x, y] > h0) { height[x, y] = h0; revertedRaised++; }
} }
r.LiftedReverted = revertedLifted; r.LiftedReverted = revertedLifted;
r.RaisedReverted = revertedRaised; r.RaisedReverted = revertedRaised;
r.Notes.Add($"[Offshore] debris guard: {small.Count} of {pre.Count} islands below {minCells:N0} cells reverted " + r.Notes.Add($"[Offshore] guards: of {pre.Count} islands (N {r.PreGuardNorth} / S {r.PreGuardSouth}) reverted " +
$"({revertedLifted:N0} surfaced cells), plus {revertedRaised:N0} submerged bump cells outside any kept island's skirt."); $"{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); var comps = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize);
(r.CountNorth, r.CountSouth) = OffshoreAnalysis.CountByHemisphere(comps); (r.CountNorth, r.CountSouth) = OffshoreAnalysis.CountByHemisphere(comps);
r.Bridged = OffshoreAnalysis.BridgedCount(comps); r.Bridged = OffshoreAnalysis.BridgedCount(comps);
var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0); var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0);
r.Notes.Add($"[Offshore] islands: {r.CountNorth} north, {r.CountSouth} south ({comps.Count} components, " + 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" : "")}; " + $"{(r.RaisedReverted > 0 ? $", {r.RaisedReverted:N0} submerged debris cells reverted" : "")}; " +
$"size cells min {szMin} median {szMed} mean {szMean:F0} max {szMax}."); $"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) if (r.Bridged > 0)
throw new InvalidOperationException( throw new InvalidOperationException(
$"[OffshorePass] MOAT VIOLATION: {r.Bridged} island(s) touch mainland land. Refusing."); $"[OffshorePass] MOAT VIOLATION: {r.Bridged} island(s) touch mainland land. Refusing.");
@ -362,93 +359,46 @@ namespace IslaApocalypse.Tools
return r; return r;
} }
/// <summary>One seeded stamp: a flat-topped disc lerped toward the crest, zone-masked per pixel.</summary> /// <summary>The reference's calibration sample: stride 8, (side)² samples, (noise + 1) · 0.5. Shared with the diagnosis.</summary>
private static long Stamp(float[,] height, float[,] preTrench, Result r, int cx, int cy, int radius, public static float[] CalibrationSamples(FastNoiseLite noise, int mapSize)
OffshoreSettings s, float sea, float crest, int mapSize, float centerX, float centerY, float halfSpan,
FastNoiseLite jitterNoise)
{ {
long lifted = 0; const int stride = 8;
int reach = (int)MathF.Ceiling(radius * (1f + s.StampEdgeJitter)); int side = mapSize / stride;
for (int dx = -reach; dx <= reach; dx++) var samples = new float[side * side];
{ for (int i = 0; i < side; i++)
int px = cx + dx; for (int j = 0; j < side; j++)
if (px < 0 || px >= mapSize) continue; samples[i * side + j] = (noise.GetNoise2D(i * stride, j * stride) + 1f) * 0.5f;
for (int dy = -reach; dy <= reach; dy++) return samples;
{
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;
} }
/// <summary>Any land cell (at/above sea) within a Chebyshev box of <paramref name="reach"/>? Conservative by design.</summary> /// <summary>The north/south threshold, smoothstep-blended across the midline. Shared with the diagnosis.</summary>
private static bool LandWithin(float[,] height, int mapSize, int cx, int cy, int reach, float sea) 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); float t = Math.Clamp((y - mid) / band * 0.5f + 0.5f, 0f, 1f);
int y0 = Math.Max(0, cy - reach), y1 = Math.Min(mapSize - 1, cy + reach); t = t * t * (3f - 2f * t);
for (int x = x0; x <= x1; x++) return thrN + (thrS - thrN) * t;
for (int y = y0; y <= y1; y++) }
if (height[x, y] >= sea) return true;
return false; 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 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; } private static float Max(float[] a) { float m = float.MinValue; foreach (float v in a) if (v > m) m = v; return m; }
}
/// <summary> /// <summary>
/// PCG32 (O'Neill) — the same tiny deterministic generator the reference's erosion pass /// The islet layer's ledger, carried on <c>Pass1Result</c> for the report: thresholds, the
/// used. Seeded from the world seed + an offset, so island POSITIONS vary per world and /// pre-guard island count, what each guard reverted, the final count. Numbers only — the tag
/// reproduce exactly for one. Trivially portable to C++. /// arrays are carried separately.
/// </summary> /// </summary>
private struct Pcg32 public sealed class OffshoreLedger
{ {
private ulong _state; public float ThresholdNorth, ThresholdSouth;
public Pcg32(int seed) { _state = 0; NextU(); _state += (ulong)(uint)seed; NextU(); } public int PreGuardNorth, PreGuardSouth;
public uint NextU() public int SpecksNorth, SpecksSouth, ClustersNorth, ClustersSouth, BlobsNorth, BlobsSouth;
{ public long LiftedOrganic, LiftedReverted, RaisedReverted;
ulong old = _state; public int CountNorth, CountSouth;
_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);
}
} }
} }

View file

@ -11,16 +11,19 @@ namespace IslaApocalypse.Tools
/// <summary> /// <summary>
/// ⭐ chat2/05 stage 1 — the reference's probabilistic layer, verbatim: one noise field, one /// ⭐ 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 /// calibrated threshold, the faithful blob, the faithful zone mask. Sparse, no corners, no
/// floor. THE CONTROL. /// guards. THE CONTROL.
/// </summary> /// </summary>
Faithful, Faithful,
/// <summary> /// <summary>
/// ⭐ chat2/05 stage 2 — the loose-guaranteed hybrid: a seeded floor of ≥N north / ≥S south /// ⭐ chat2/06 — THE ONE ISLAND MECHANISM: the organic noise-field layer, reshaped small /
/// islands (min-separated, positions varying per seed, never fixed zones) PLUS the organic /// low / flat / crisp (chat2/05 stage 2's shape, unchanged), tuned for coverage by DENSITY
/// layer on top, reshaped small / low / flat / rigid, south-weighted, corners allowed. /// 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).
/// </summary> /// </summary>
Hybrid, Organic,
} }
/// <summary> /// <summary>
@ -31,19 +34,24 @@ namespace IslaApocalypse.Tools
/// ═══ TWO PRESETS, AND WHY BOTH EXIST ═══ /// ═══ TWO PRESETS, AND WHY BOTH EXIST ═══
/// ///
/// <see cref="Faithful"/> the reference's constants, verbatim. The control in every batch. /// <see cref="Faithful"/> the reference's constants, verbatim. The control in every batch.
/// <see cref="Hybrid"/> the reshape — the deliverable. /// <see cref="Organic"/> the reshape + the chat2/06 coverage tuning — the deliverable.
/// ///
/// The reshape does not EDIT the faithful constants; it sets different values on the same /// The reshape does not EDIT the faithful constants; it sets different values on the same
/// dials. So <c>Faithful()</c> stays bit-reproducible however far the hybrid is tuned. /// dials. So <c>Faithful()</c> stays bit-reproducible however far the organic layer is tuned.
/// ///
/// ═══ THE TWO PROTECTIONS THAT ARE NOT DIALS ═══ /// ═══ THE TWO PROTECTIONS THAT ARE NOT DIALS ═══
/// ///
/// <see cref="MinDepthM"/> (the moat) and <see cref="MinFalloff"/> (the "actually offshore" test) /// <see cref="MinDepthM"/> (the moat) and <see cref="MinFalloff"/> (the "actually offshore" test)
/// are exposed here because every knob is, but they are the MAIN-ISLAND AND LAKE PROTECTIONS and /// 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 /// 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 <c>OffshorePass</c> applies /// island in a lake or the crater bay" true by construction.
/// them to the SEEDED stamps as well, so a guaranteed island cannot be guaranteed into the wrong ///
/// place. /// ═══ ⚠ 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 <c>Hybrid()</c>) was reverted out whole; if a hard count ever comes back it is a
/// design decision, not a knob that was left lying around.
/// </summary> /// </summary>
public sealed class OffshoreSettings public sealed class OffshoreSettings
{ {
@ -51,9 +59,6 @@ namespace IslaApocalypse.Tools
// ---- the organic (noise) layer ---------------------------------------- // ---- the organic (noise) layer ----------------------------------------
/// <summary>Run the noise layer at all. Off ⇒ only the seeded floor (the `floor_only` contrast).</summary>
public bool Organic = true;
/// <summary>Islet noise frequency, periods per map width. Higher ⇒ SMALLER blobs. Reference 14.</summary> /// <summary>Islet noise frequency, periods per map width. Higher ⇒ SMALLER blobs. Reference 14.</summary>
public float FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS; public float FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS;
@ -61,12 +66,24 @@ namespace IslaApocalypse.Tools
public int SeedOffset = IslandFalloff.OFFSHORE_SEED_OFFSET; public int SeedOffset = IslandFalloff.OFFSHORE_SEED_OFFSET;
/// <summary> /// <summary>
/// Organic density — the fraction of the noise field's ACTUAL sampled distribution that /// ⭐ THE MAIN KNOB — organic density: the fraction of the noise field's ACTUAL sampled
/// clears the threshold — per hemisphere. Equal ⇒ the reference's single dial. South &gt; /// distribution that clears the calibrated threshold. More ⇒ more of the field becomes
/// north ⇒ the developer's "weighted south". Reference 0.02 / 0.02. /// island. This is the NORTH density; the south is this × <see cref="SouthWeight"/>.
/// Reference 0.02.
/// </summary> /// </summary>
public float DensityNorth = 0.02f; public float Density = 0.02f;
public float DensitySouth = 0.02f;
/// <summary>
/// ⭐ THE SOUTH WEIGHT — a per-hemisphere density bias. South density =
/// <see cref="Density"/> × this. 1 ⇒ one dial (the reference); &gt; 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.
/// </summary>
public float SouthWeight = 1f;
/// <summary>The effective south density.</summary>
public float DensitySouth => Density * SouthWeight;
/// <summary> /// <summary>
/// Half-width of the smooth threshold blend across the hemisphere midline, as a fraction of /// Half-width of the smooth threshold blend across the hemisphere midline, as a fraction of
@ -84,16 +101,37 @@ namespace IslaApocalypse.Tools
/// <summary>Crest-to-sea edge sharpening exponent. 1 ⇒ the faithful smoothstep. Higher ⇒ crisper shore.</summary> /// <summary>Crest-to-sea edge sharpening exponent. 1 ⇒ the faithful smoothstep. Higher ⇒ crisper shore.</summary>
public float EdgeSharpness = 1f; 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).
/// <summary> /// <summary>
/// ⚠ 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 /// 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 /// few cells — and is reverted to seabed and untagged. 3e-5 is ~126 cells at 2048, ~500 at
/// rule and its "~585 px blobs" comment was about the NOISE wavelength, not the caps). /// 4096, ~2,000 cells (a ~50 m islet) at 8192.
/// 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.
/// </summary> /// </summary>
public float MinIslandAreaFrac = 0f; public float MinIslandAreaFrac = 0f;
/// <summary>
/// ⚠ 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.
/// </summary>
public float MinSeparationFrac = 0f;
/// <summary>
/// ⚠ 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.
/// </summary>
public float MaxIslandAreaFrac = 0f;
// ---- the zone mask: protections + the Trench bound -------------------- // ---- the zone mask: protections + the Trench bound --------------------
/// <summary>⭐ THE MOAT. Reference 14 m. The presets do not move it.</summary> /// <summary>⭐ THE MOAT. Reference 14 m. The presets do not move it.</summary>
@ -115,95 +153,67 @@ namespace IslaApocalypse.Tools
public float TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER; public float TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER;
public float TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER; public float TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER;
// ---- the seeded floor (Hybrid only) -----------------------------------
/// <summary>⭐ The guaranteed minimum island count per hemisphere. 0 / 0 ⇒ no floor (Faithful).</summary>
public int FloorNorth = 0;
public int FloorSouth = 0;
/// <summary>Stamp radius, fraction of the map. Small and low is the target.</summary>
public float StampRadiusFrac = 0.012f;
/// <summary>Fraction of the stamp radius that is flat top. Higher ⇒ flatter, crisper shore.</summary>
public float StampCoreFrac = 0.60f;
/// <summary>
/// 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.
/// </summary>
public float StampEdgeJitter = 0f;
/// <summary>Minimum centre-to-centre separation between seeded islands, fraction of the map.</summary>
public float SeparationFrac = 0.08f;
/// <summary>
/// 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.
/// </summary>
public float LandGapFrac = 0.010f;
/// <summary>The placement RNG's seed offset. A SEED offset; positions vary per world seed.</summary>
public int PlacementSeedOffset = 7703;
/// <summary>Rejection-sampling budget per hemisphere before the pass REFUSES (loudly) rather than under-delivers.</summary>
public int MaxPlacementAttempts = 40000;
// ---- presets ---------------------------------------------------------- // ---- presets ----------------------------------------------------------
/// <summary>The reference, verbatim. No floor, single density, original footprint/crest/mask.</summary> /// <summary>The reference, verbatim. Single density, no guards, original footprint/crest/mask.</summary>
public static OffshoreSettings Faithful() => new OffshoreSettings public static OffshoreSettings Faithful() => new OffshoreSettings
{ {
Mode = OffshoreMode.Faithful, Mode = OffshoreMode.Faithful,
Organic = true,
FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS, FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS,
SeedOffset = IslandFalloff.OFFSHORE_SEED_OFFSET, 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, CrestM = IslandFalloff.OFFSHORE_ISLAND_H_M,
CoreFraction = IslandFalloff.OFFSHORE_CORE, CoreFraction = IslandFalloff.OFFSHORE_CORE,
EdgeSharpness = 1f, EdgeSharpness = 1f,
TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER, TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER,
TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER, TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER,
FloorNorth = 0, FloorSouth = 0,
}; };
/// <summary> /// <summary>
/// ⭐ The reshape: small / low / flatter / more rigid, loose-guaranteed (≥2 N, ≥4 S), organic /// ⭐ THE DELIVERABLE (chat2/06) — the organic layer, reshaped (chat2/05 stage 2's shape:
/// extras weighted south, corners and the outer edge allowed. /// 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`.
/// </summary> /// </summary>
public static OffshoreSettings Hybrid() => new OffshoreSettings public static OffshoreSettings Organic() => new OffshoreSettings
{ {
Mode = OffshoreMode.Hybrid, Mode = OffshoreMode.Organic,
Organic = true, FreqPerMapWidth = 16f, // a little smaller than the reference's 14 (chat2/05)
// ⚠ Chosen by SWEEP, not by feel (chat2/05 scratch): at freq 24 / 0.010 / 0.025 the field Density = OrganicDensityMid,
// produced 183 organic blobs of which 174 were debris. freq 16 with these densities gives SouthWeight = OrganicSouthWeight,
// the floor plus ~+12 N / +68 S genuine extras of a few hundred cells each at 2048. CrestM = 24f, // lower than the reference's 34 (pre-curve) — lands in the curve's preserved toe
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)
CoreFraction = 0.25f, // flatter than 0.45 CoreFraction = 0.25f, // flatter than 0.45
EdgeSharpness = 2.5f, // crisper shore than the faithful smoothstep 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 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,
}; };
/// <summary>
/// The three density levels of the chat2/06 batch, in one place so the tool and the preset
/// cannot disagree. <c>Mid</c> is the preset of record.
/// </summary>
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 OffshoreSettings Clone() => (OffshoreSettings)MemberwiseClone();
public string Describe() public string Describe()
{ {
if (Mode == OffshoreMode.Off) return "offshore OFF"; if (Mode == OffshoreMode.Off) return "offshore OFF";
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append($"{Mode}: organic {(Organic ? "on" : "OFF")} freq {FreqPerMapWidth:F0}/map density N {DensityNorth:F3} S {DensitySouth:F3} · "); 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} minArea {MinIslandAreaFrac:G2} · "); 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}"); 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(); return sb.ToString();
} }
} }

View file

@ -91,13 +91,13 @@ namespace IslaApocalypse.Tools
/// </summary> /// </summary>
public readonly byte[,] IslandHemisphere; public readonly byte[,] IslandHemisphere;
/// <summary>The seeded-floor stamp centres and radii (Hybrid only; empty otherwise). For the overlay and the report.</summary>
public readonly IReadOnlyList<(int x, int y, int r)> OffshoreCentres;
/// <summary>Cells the islet layer lifted above sea. The reference printed this too.</summary> /// <summary>Cells the islet layer lifted above sea. The reference printed this too.</summary>
public readonly long OffshoreLiftedCells; public readonly long OffshoreLiftedCells;
/// <summary>Lines worth printing from the shelf/offshore pass: thresholds, floor placement, lifted counts.</summary> /// <summary>The islet layer's numbers — thresholds, pre-guard count, guard ledger, final count. Null when offshore is off.</summary>
public readonly OffshoreLedger OffshoreLedger;
/// <summary>Lines worth printing from the shelf/offshore pass: thresholds, guard ledger, lifted counts.</summary>
public readonly IReadOnlyList<string> Notes; public readonly IReadOnlyList<string> Notes;
/// <summary>Wall-clock milliseconds the pass took.</summary> /// <summary>Wall-clock milliseconds the pass took.</summary>
@ -107,8 +107,7 @@ namespace IslaApocalypse.Tools
float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs, float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs,
float hMaxSeedBeforeOffshore = float.NaN, float hMaxSeedBeforeOffshore = float.NaN,
bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null, bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null,
IReadOnlyList<(int x, int y, int r)> offshoreCentres = null, long offshoreLiftedCells = 0, IReadOnlyList<string> notes = null, OffshoreLedger offshoreLedger = null)
long offshoreLiftedCells = 0, IReadOnlyList<string> notes = null)
{ {
MapSize = mapSize; MapSize = mapSize;
Seed = seed; Seed = seed;
@ -121,8 +120,8 @@ namespace IslaApocalypse.Tools
HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore; HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore;
IsOffshoreIsland = isOffshoreIsland; IsOffshoreIsland = isOffshoreIsland;
IslandHemisphere = islandHemisphere; IslandHemisphere = islandHemisphere;
OffshoreCentres = offshoreCentres ?? System.Array.Empty<(int, int, int)>();
OffshoreLiftedCells = offshoreLiftedCells; OffshoreLiftedCells = offshoreLiftedCells;
OffshoreLedger = offshoreLedger;
Notes = notes ?? System.Array.Empty<string>(); Notes = notes ?? System.Array.Empty<string>();
} }

View file

@ -402,28 +402,9 @@ namespace IslaApocalypse.Tools
// ═══ chat2/05 — the offshore checks ═══ // ═══ chat2/05 — the offshore checks ═══
/// <summary> // (h) was the chat2/05 seeded-floor check — reverted out with the floor in chat2/06. No
/// (h) ⭐ FLOOR HOLDS — at least <paramref name="needNorth"/> / <paramref name="needSouth"/> // count is guaranteed any more, so there is nothing for an oracle to assert; the count
/// offshore islands per hemisphere, counted as 8-connected components of TAGGED land by // table is the evidence, and it is statistics, not a check.
/// 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.
/// </summary>
public static Check OffshoreFloor(Pass1Result p1, int needNorth, int needSouth, float sea,
List<IslandComponent> 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;
}
/// <summary> /// <summary>
/// (i) ⭐ MOAT INTACT — no offshore island is 8-connected to mainland land. The moat exists /// (i) ⭐ MOAT INTACT — no offshore island is 8-connected to mainland land. The moat exists

View file

@ -1,11 +1,10 @@
using System.Collections.Generic;
using Godot; using Godot;
namespace IslaApocalypse.Tools namespace IslaApocalypse.Tools
{ {
/// <summary> /// <summary>
/// The offshore TAG / HEMISPHERE debug overlay (chat2/05): mainland one tint, offshore-island /// 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. /// 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 /// ⚠ 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 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 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 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); private static readonly Color Ink = new(0.941f, 0.949f, 0.961f);
/// <param name="isMainlandLand"> /// <param name="isMainlandLand">
@ -30,7 +28,7 @@ namespace IslaApocalypse.Tools
/// by mis-tagging mainland). Null ⇒ derived as "land and not tagged", which is weaker. /// by mis-tagging mainland). Null ⇒ derived as "land and not tagged", which is weaker.
/// </param> /// </param>
public static void SavePng(float[,] height, bool[,] tag, byte[,] hemi, int mapSize, float sea, 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); var img = Image.CreateEmpty(mapSize, mapSize, false, Image.Format.Rgb8);
@ -59,38 +57,16 @@ namespace IslaApocalypse.Tools
int mid = mapSize / 2; int mid = mapSize / 2;
for (int x = 0; x < mapSize; x += 3) img.SetPixel(x, mid, Midline); 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. // A legend that cannot be separated from the picture.
int s = mapSize >= 4096 ? 4 : 3; int s = mapSize >= 4096 ? 4 : 3;
int lh = TinyFont.Height(s) + 6; int lh = TinyFont.Height(s) + 6;
TinyFont.Draw(img, "OFFSHORE TAG OVERLAY", 12, 12, s, Ink); 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, "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); TinyFont.Draw(img, "N ABOVE THE LINE - S BELOW - Y RUNS SOUTH", 12, 12 + lh * 3, s, Ink);
Error err = img.SavePng(absolutePath); Error err = img.SavePng(absolutePath);
if (err != Error.Ok) GD.PrintErr($"[TagOverlayRenderer] SavePng failed ({err}) for {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);
}
}
}
} }
} }

View file

@ -270,7 +270,7 @@ namespace IslaApocalypse.Tools
/// <summary> /// <summary>
/// ⭐ The offshore islet system — every dial in one object. <c>Mode = Off</c> by default /// ⭐ The offshore islet system — every dial in one object. <c>Mode = Off</c> by default
/// (see the note above). <see cref="OffshoreSettings.Faithful"/> is the reference verbatim; /// (see the note above). <see cref="OffshoreSettings.Faithful"/> is the reference verbatim;
/// <see cref="OffshoreSettings.Hybrid"/> is the reshape. /// <see cref="OffshoreSettings.Organic"/> is the reshape, tuned (chat2/06).
/// </summary> /// </summary>
public OffshoreSettings Offshore = new OffshoreSettings(); public OffshoreSettings Offshore = new OffshoreSettings();

View file

@ -283,7 +283,7 @@ namespace IslaApocalypse.Tools
// The reference's pass 1 CONTINUED HERE with the coast shelf (~:621-640) and the // 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 // 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 // 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, hMaxSeedBeforeOffshore: hMaxBeforeOffshore,
isOffshoreIsland: offshore?.Tag, isOffshoreIsland: offshore?.Tag,
islandHemisphere: offshore?.Hemi, islandHemisphere: offshore?.Hemi,
offshoreCentres: offshore?.Centres, offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic - offshore.LiftedReverted,
offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic + offshore.LiftedSeeded - offshore.LiftedReverted, notes: offshore?.Notes,
notes: offshore?.Notes); offshoreLedger: offshore?.ToLedger());
} }
} }
} }