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(); } } }