using System;
using System.Collections.Generic;
using System.Text;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// One hemisphere's share of the offshore zone, the gates, and the noise peaks.
public sealed class HemisphereDiagnosis
{
public string Name;
// ---- the valid offshore zone ----
public long Sea; // below-sea cells in this hemisphere (after the shelf)
public long Zone; // cells with zone weight > 0 (island-eligible)
public long ZoneFull; // cells with zone weight == 1 (clear of every feather)
// ---- which gate blocks (non-exclusive: a cell may fail several) ----
public long BlockDepth; // ambient depth < moat
public long BlockFalloff; // pre-Trench falloff < min (not "actually offshore")
public long BlockTrench; // at/past the outer bound
// ---- the SOLE blocker (a cell that fails exactly one gate — loosen that gate and it joins the zone) ----
public long SoleDepth, SoleFalloff, SoleTrench;
// ---- band geometry: per column, how many rows are ocean / zone in this hemisphere ----
public double OceanRowsPerColumn, ZoneRowsPerColumn;
public int ColumnsWithNoZone;
// ---- the noise peaks (strict 8-neighbour local maxima of the islet field, on sea cells) ----
public int PeaksSea; // all maxima over sea
public int PeaksInZone; // maxima inside the zone — the CAPACITY for islands at this frequency
public int PeaksInZoneOverThr; // inside the zone AND clearing the threshold — the island candidates
public int PeaksOverThrLost; // clearing the threshold but OUTSIDE the zone — killed by a gate:
public int LostDepth, LostFalloff, LostTrench; // …which one(s) (non-exclusive)
public double ZoneShareOfSea => Sea == 0 ? 0 : (double)Zone / Sea;
public double CandidatesPerMegacell => Zone == 0 ? 0 : PeaksInZoneOverThr * 1e6 / Zone;
}
///
/// ⭐ THE SOUTH-SUPPRESSION DIAGNOSIS (chat2/06 §2) — MEASURE, DON'T GUESS. Before a knob moves,
/// answer per hemisphere: how much island-eligible ocean is there, how many noise peaks clear the
/// threshold in it, and which gate is the binding one. Read-only: it evaluates the islet noise
/// field and the zone mask exactly as does (same noise factory, same
/// calibration samples, same blended threshold, same gate arithmetic) over the post-shelf,
/// pre-islet field, and counts. It raises nothing.
///
/// Hemisphere is the row midline (), as the tag.
///
public static class OffshoreDiagnosis
{
public sealed class Report
{
public int Seed, MapSize;
public float ThresholdNorth, ThresholdSouth;
public HemisphereDiagnosis North = new() { Name = "north" };
public HemisphereDiagnosis South = new() { Name = "south" };
public HemisphereDiagnosis Of(byte hemi) => hemi == OffshoreAnalysis.HemiNorth ? North : South;
}
/// The post-shelf, pre-islet field (offshore OFF, shelf as the batch runs it).
public static Report Run(float[,] height, float[,] preTrench, int mapSize, int seed, float sea,
OffshoreSettings s, GenerationScale scale)
{
var rep = new Report { Seed = seed, MapSize = mapSize };
FastNoiseLite noise = TerrainNoise.CreateModulation(seed, s.SeedOffset, s.FreqPerMapWidth, scale);
float[] samples = OffshorePass.CalibrationSamples(noise, mapSize);
float thrN = IslandFalloff.CalibrateThreshold(samples, s.Density);
float thrS = s.Mode == OffshoreMode.Faithful ? thrN : IslandFalloff.CalibrateThreshold(samples, s.DensitySouth);
rep.ThresholdNorth = thrN; rep.ThresholdSouth = thrS;
float centerX = mapSize / 2.0f, centerY = mapSize / 2.0f, halfSpan = mapSize / 2.0f;
float mid = mapSize * 0.5f;
float band = MathF.Max(1f, s.HemisphereBlendHalfWidth * mapSize);
// The islet field over the whole map (a peak's neighbours may be land or out of zone).
var v = new float[mapSize, mapSize];
for (int x = 0; x < mapSize; x++)
for (int y = 0; y < mapSize; y++)
v[x, y] = (noise.GetNoise2D(x, y) + 1f) * 0.5f;
// Zone weight per cell, and the gate ledger. -1 = land.
var zone = new float[mapSize, mapSize];
int half = mapSize / 2;
long[] oceanRows = new long[2], zoneRows = new long[2];
int[] colsNoZone = new int[2];
for (int x = 0; x < mapSize; x++)
{
long[] colZone = new long[2];
for (int y = 0; y < mapSize; y++)
{
float h = height[x, y];
if (h >= sea) { zone[x, y] = -1f; continue; }
int hi = y < half ? 0 : 1;
var d = hi == 0 ? rep.North : rep.South;
d.Sea++; oceanRows[hi]++;
float depthM = WorldScale.MetresFromRaw(sea - h);
float dist = MathF.Max(MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan);
bool bDepth = depthM < s.MinDepthM;
bool bFall = preTrench[x, y] < s.MinFalloff;
bool bTr = dist >= s.TrenchOuter;
if (bDepth) d.BlockDepth++;
if (bFall) d.BlockFalloff++;
if (bTr) d.BlockTrench++;
int fails = (bDepth ? 1 : 0) + (bFall ? 1 : 0) + (bTr ? 1 : 0);
if (fails == 1)
{
if (bDepth) d.SoleDepth++; else if (bFall) d.SoleFalloff++; else d.SoleTrench++;
}
float z = IslandFalloff.OffshoreZoneWeight(depthM, preTrench[x, y],
MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan,
s.MinDepthM, s.DepthFeatherM, s.MinFalloff, s.FalloffFeather, s.TrenchInner, s.TrenchOuter);
zone[x, y] = z;
if (z > 0f) { d.Zone++; zoneRows[hi]++; colZone[hi]++; }
if (z >= 1f) d.ZoneFull++;
}
for (int hi = 0; hi < 2; hi++) if (colZone[hi] == 0) colsNoZone[hi]++;
}
rep.North.OceanRowsPerColumn = oceanRows[0] / (double)mapSize;
rep.South.OceanRowsPerColumn = oceanRows[1] / (double)mapSize;
rep.North.ZoneRowsPerColumn = zoneRows[0] / (double)mapSize;
rep.South.ZoneRowsPerColumn = zoneRows[1] / (double)mapSize;
rep.North.ColumnsWithNoZone = colsNoZone[0];
rep.South.ColumnsWithNoZone = colsNoZone[1];
// Peaks: strict local maxima of v over the 8-neighbourhood, on sea cells.
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
if (zone[x, y] < 0f) continue; // land
float c = v[x, y];
bool isMax = true;
for (int dx = -1; dx <= 1 && isMax; dx++)
{
int nx = x + dx; if (nx < 0 || nx >= mapSize) continue;
for (int dy = -1; dy <= 1; dy++)
{
if (dx == 0 && dy == 0) continue;
int ny = y + dy; if (ny < 0 || ny >= mapSize) continue;
if (v[nx, ny] >= c) { isMax = false; break; }
}
}
if (!isMax) continue;
var d = y < half ? rep.North : rep.South;
d.PeaksSea++;
float thr = s.Mode == OffshoreMode.Faithful ? thrN : OffshorePass.BlendedThreshold(y, mid, band, thrN, thrS);
bool over = c > thr;
if (zone[x, y] > 0f)
{
d.PeaksInZone++;
if (over) d.PeaksInZoneOverThr++;
}
else if (over)
{
d.PeaksOverThrLost++;
float h = height[x, y];
float depthM = WorldScale.MetresFromRaw(sea - h);
float dist = MathF.Max(MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan);
if (depthM < s.MinDepthM) d.LostDepth++;
if (preTrench[x, y] < s.MinFalloff) d.LostFalloff++;
if (dist >= s.TrenchOuter) d.LostTrench++;
}
}
}
return rep;
}
/// One markdown row per hemisphere for a report table (see ).
public static string TableHeader() =>
"| seed | hemi | sea cells | zone cells | zone / sea | zone == 1 | ocean rows/col | zone rows/col | cols w/o zone | " +
"blocked: depth / falloff / trench | sole blocker: depth / falloff / trench | peaks: sea / in zone / in zone > thr | lost > thr (depth / falloff / trench) | candidates per Mcell |\n" +
"|---|---|---|---|---|---|---|---|---|---|---|---|---|---|";
public static string TableRow(Report r, HemisphereDiagnosis d) =>
$"| `{r.Seed}` | **{d.Name}** | {d.Sea:N0} | {d.Zone:N0} | {d.ZoneShareOfSea:P1} | {d.ZoneFull:N0} | {d.OceanRowsPerColumn:F0} | {d.ZoneRowsPerColumn:F0} | {d.ColumnsWithNoZone} | " +
$"{d.BlockDepth:N0} / {d.BlockFalloff:N0} / {d.BlockTrench:N0} | {d.SoleDepth:N0} / {d.SoleFalloff:N0} / {d.SoleTrench:N0} | " +
$"{d.PeaksSea} / {d.PeaksInZone} / **{d.PeaksInZoneOverThr}** | {d.PeaksOverThrLost} ({d.LostDepth} / {d.LostFalloff} / {d.LostTrench}) | {d.CandidatesPerMegacell:F1} |";
/// Sum a pool of reports per hemisphere (means for the per-column numbers).
public static (HemisphereDiagnosis north, HemisphereDiagnosis south) Pool(IReadOnlyList reports)
{
var n = new HemisphereDiagnosis { Name = "north (pool)" };
var s = new HemisphereDiagnosis { Name = "south (pool)" };
foreach (var r in reports) { Add(n, r.North); Add(s, r.South); }
int k = Math.Max(1, reports.Count);
n.OceanRowsPerColumn /= k; s.OceanRowsPerColumn /= k;
n.ZoneRowsPerColumn /= k; s.ZoneRowsPerColumn /= k;
n.ColumnsWithNoZone /= k; s.ColumnsWithNoZone /= k;
return (n, s);
}
private static void Add(HemisphereDiagnosis a, HemisphereDiagnosis b)
{
a.Sea += b.Sea; a.Zone += b.Zone; a.ZoneFull += b.ZoneFull;
a.BlockDepth += b.BlockDepth; a.BlockFalloff += b.BlockFalloff; a.BlockTrench += b.BlockTrench;
a.SoleDepth += b.SoleDepth; a.SoleFalloff += b.SoleFalloff; a.SoleTrench += b.SoleTrench;
a.OceanRowsPerColumn += b.OceanRowsPerColumn; a.ZoneRowsPerColumn += b.ZoneRowsPerColumn; a.ColumnsWithNoZone += b.ColumnsWithNoZone;
a.PeaksSea += b.PeaksSea; a.PeaksInZone += b.PeaksInZone; a.PeaksInZoneOverThr += b.PeaksInZoneOverThr;
a.PeaksOverThrLost += b.PeaksOverThrLost; a.LostDepth += b.LostDepth; a.LostFalloff += b.LostFalloff; a.LostTrench += b.LostTrench;
}
/// A one-paragraph reading of the pooled numbers: which hemisphere has less eligible ocean, and which gate binds it.
public static string Interpret(HemisphereDiagnosis n, HemisphereDiagnosis s)
{
var sb = new StringBuilder();
string smaller = n.Zone < s.Zone ? "NORTH" : "SOUTH";
double ratio = n.Zone == 0 || s.Zone == 0 ? 0 : (double)Math.Max(n.Zone, s.Zone) / Math.Min(n.Zone, s.Zone);
sb.Append($"Valid offshore zone: north {n.Zone:N0} cells ({n.ZoneShareOfSea:P1} of its ocean, {n.ZoneRowsPerColumn:F0} rows/col), " +
$"south {s.Zone:N0} cells ({s.ZoneShareOfSea:P1} of its ocean, {s.ZoneRowsPerColumn:F0} rows/col) — the {smaller} has " +
$"{ratio:F2}× less island-eligible ocean. ");
sb.Append($"Island candidates (peaks in zone clearing the threshold): north {n.PeaksInZoneOverThr}, south {s.PeaksInZoneOverThr}; " +
$"capacity (all peaks in zone): north {n.PeaksInZone}, south {s.PeaksInZone}. ");
string Bind(HemisphereDiagnosis d)
{
long max = Math.Max(d.SoleDepth, Math.Max(d.SoleFalloff, d.SoleTrench));
string g = max == d.SoleFalloff ? "the falloff test" : max == d.SoleDepth ? "the moat (ambient depth)" : "the outer/trench bound";
long lostMax = Math.Max(d.LostDepth, Math.Max(d.LostFalloff, d.LostTrench));
string lg = d.PeaksOverThrLost == 0 ? "none" : lostMax == d.LostFalloff ? "falloff" : lostMax == d.LostDepth ? "moat" : "outer bound";
return $"{d.Name}: binding gate by sole-blocked cells = {g} (depth {d.SoleDepth:N0} / falloff {d.SoleFalloff:N0} / trench {d.SoleTrench:N0}); " +
$"over-threshold peaks lost to gates = {d.PeaksOverThrLost} (mostly {lg})";
}
sb.Append(Bind(n)).Append(". ").Append(Bind(s)).Append('.');
return sb.ToString();
}
}
}