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
231 lines
11 KiB
C#
231 lines
11 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|