using System;
using System.Collections.Generic;
namespace IslaApocalypse.Core
{
/// One maximal 8-connected component of land, as the region layer exposes it.
public sealed class LandRegion
{
/// 1-based, assigned in deterministic scan order (x outer, y inner) — stable per seed across runs.
public int Id;
/// Cells in the component.
public long SizeCells;
/// Centroid in map cells.
public double CentroidX, CentroidY;
///
/// / , decided by the
/// CENTROID — one label per component; a straddler is decided by where its mass is, never per cell.
///
public byte Hemisphere;
/// True for exactly one component: the one containing the map centre (or the flagged fallback).
public bool IsMainland;
/// Bounding box, inclusive. Convenience for overlays and guards; not part of the contract.
public int MinX, MinY, MaxX, MaxY;
}
/// The result of one labeling: the per-cell id map and the per-component table.
public sealed class RegionLabels
{
public int MapSize;
/// Per cell, x * MapSize + y: the component id, or 0 for water.
public int[] Id;
/// Every component, indexed by Id - 1, in id order.
public List Regions;
/// The mainland's id (0 only if there is no land at all).
public int MainlandId;
///
/// ⚠ Whether the map-centre cell was land. Expected always true (the massif is centred and
/// stable). When false the mainland fell back to the LARGEST component and the caller must
/// report it loudly — the contract's mainland definition did not hold on this field.
///
public bool CentreWasLand;
public long LandCells;
public int IslandCount => Regions.Count - (MainlandId > 0 ? 1 : 0);
public LandRegion Mainland => MainlandId > 0 ? Regions[MainlandId - 1] : null;
public LandRegion Of(int id) => Regions[id - 1];
public int IdAt(int x, int y) => Id[x * MapSize + y];
}
///
/// ⭐⭐ THE REGION-LABELING LAYER — shared infrastructure (chat2/07). Flood-fills land into distinct
/// components, identifies mainland vs islands, and exposes per-component data. Islands are its first
/// consumer; later phases (biomes, placement, rivers, the crater) CONSUME this layer rather than
/// rebuild it. Engine-free; pure analysis over a float[,]; C++-candidate.
///
/// ═══ THE CONTRACT — build to it exactly (recorded at graduation as the shared-infra contract) ═══
///
/// FIELD It runs on the CLASSIFY (raw, uncurved) height — region identity partitions on the
/// same authoritative field as water bodies and biome regions (D-046), so islands /
/// water / biomes line up by construction. Raw is authoritative for region identity.
/// It does NOT run on the render field.
///
/// CONNECTIVITY Land is 8-CONNECTED. Deliberately the complement of water's 4-connectivity —
/// foreground/background using opposite connectivity is the topologically sound
/// pairing (a diagonal isthmus reads as JOINED; the water on either side of it reads
/// as SEPARATE), not a conflict with the water model.
///
/// COMPONENT A component = a maximal 8-connected set of land cells (land = classify height ≥ sea).
///
/// MAINLAND The component containing the MAP CENTRE (the mountain/massif is always centred and
/// stable) — NOT merely the largest component, because a later fragmentation step
/// could make "largest" flip seed to seed. Every OTHER land component is an island.
/// ⚠ The crater is NOT central — it is a northern-coastline feature, unrelated to the
/// centre or the mountain, and plays no part here.
/// Defensively: if the centre cell is not land, the layer reports it ( = false) and falls back to the largest
/// component, FLAGGED — the caller asserts rather than assumes.
///
/// PER COMPONENT id · sizeCells · centroid (x, y) · hemisphere (north / south, BY THE CENTROID —
/// one label per island; a straddler is decided by its centroid, never per cell) ·
/// isMainland.
///
/// Ids are assigned in deterministic scan order (x outer, y inner, first-seen), so they are stable
/// per seed across runs. Nothing here knows about "offshore" or "stamped" — it labels land.
///
/// ═══ THE HEMISPHERE CONVENTION — read from the code, not invented (chat2/05) ═══
///
/// Pass 1's latitude scalar is y / MapSize; the spine's "southern fade" and the "southern
/// sinker" bite at high y. So y increases SOUTHWARD: NORTH = rows [0, MapSize/2), SOUTH = rows
/// [MapSize/2, MapSize). The clean row midline, never the wobbled latitude field.
///
public static class RegionLabeling
{
public const byte HemiNone = 0;
public const byte HemiNorth = 1;
public const byte HemiSouth = 2;
/// The convention, in one place. Every consumer reads hemisphere through this.
public static byte HemisphereOfRow(int y, int mapSize) => y < mapSize / 2 ? HemiNorth : HemiSouth;
public static string HemisphereName(byte h) => h switch
{
HemiNorth => "north", HemiSouth => "south", _ => "none",
};
// 8-connectivity, fixed order (determinism: the fill order never changes).
private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 };
private static readonly int[] DY = { -1, 0, 1, -1, 1, -1, 0, 1 };
///
/// Label every 8-connected land component of (land = height ≥
/// ). Pure: the field is read, never written.
///
public static RegionLabels Label(float[,] classify, int mapSize, float sea)
{
int n = mapSize;
var id = new int[n * n];
var regions = new List();
var stack = new Stack();
long landCells = 0;
for (int sx = 0; sx < n; sx++)
{
for (int sy = 0; sy < n; sy++)
{
if (classify[sx, sy] < sea || id[sx * n + sy] != 0) continue;
var r = new LandRegion { Id = regions.Count + 1, MinX = sx, MaxX = sx, MinY = sy, MaxY = sy };
double sumX = 0, sumY = 0;
id[sx * n + sy] = r.Id;
stack.Push(sx * n + sy);
while (stack.Count > 0)
{
int cur = stack.Pop();
int cx = cur / n, cy = cur % n;
r.SizeCells++; sumX += cx; sumY += cy;
if (cx < r.MinX) r.MinX = cx; if (cx > r.MaxX) r.MaxX = cx;
if (cy < r.MinY) r.MinY = cy; if (cy > r.MaxY) r.MaxY = cy;
for (int k = 0; k < 8; k++)
{
int nx = cx + DX[k], ny = cy + DY[k];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
int ni = nx * n + ny;
if (id[ni] != 0 || classify[nx, ny] < sea) continue;
id[ni] = r.Id;
stack.Push(ni);
}
}
r.CentroidX = sumX / r.SizeCells;
r.CentroidY = sumY / r.SizeCells;
r.Hemisphere = HemisphereOfRow((int)Math.Round(r.CentroidY), n);
landCells += r.SizeCells;
regions.Add(r);
}
}
var labels = new RegionLabels { MapSize = n, Id = id, Regions = regions, LandCells = landCells };
// ⭐ MAINLAND = the component containing the map centre. Asserted by the caller; the
// fallback (largest) exists so a run can finish and REPORT the violation rather than crash.
int centre = (n / 2) * n + (n / 2);
labels.CentreWasLand = id[centre] != 0;
if (labels.CentreWasLand) labels.MainlandId = id[centre];
else
{
long best = -1;
foreach (var r in regions) if (r.SizeCells > best) { best = r.SizeCells; labels.MainlandId = r.Id; }
}
if (labels.MainlandId > 0) regions[labels.MainlandId - 1].IsMainland = true;
return labels;
}
///
/// Size statistics over the islands (non-mainland components): count, min / median / mean /
/// max cells, and a log-spaced histogram — the instrument that turns "nice pieces vs shattered
/// gravel" into numbers.
///
public static (int count, long min, long median, double mean, long max, int[] histogram)
IslandSizes(RegionLabels labels)
{
var sizes = new List();
foreach (var r in labels.Regions) if (!r.IsMainland) sizes.Add(r.SizeCells);
var hist = new int[HistogramEdges.Length + 1];
if (sizes.Count == 0) return (0, 0, 0, 0.0, 0, hist);
sizes.Sort();
double sum = 0;
foreach (long s in sizes) { sum += s; hist[HistogramBin(s)]++; }
return (sizes.Count, sizes[0], sizes[sizes.Count / 2], sum / sizes.Count, sizes[sizes.Count - 1], hist);
}
/// Histogram bin edges (cells): [0,64) [64,256) [256,1024) [1024,4096) [4096,16384) [16384,∞).
public static readonly long[] HistogramEdges = { 64, 256, 1024, 4096, 16384 };
public static int HistogramBin(long cells)
{
for (int i = 0; i < HistogramEdges.Length; i++) if (cells < HistogramEdges[i]) return i;
return HistogramEdges.Length;
}
public static string HistogramLabel(int bin) => bin == 0 ? $"<{HistogramEdges[0]}"
: bin < HistogramEdges.Length ? $"{HistogramEdges[bin - 1]}–{HistogramEdges[bin] - 1}"
: $"≥{HistogramEdges[^1]}";
// ═══ chat2/12 — THE OCEAN IDENTITY (the water-side complement of the land contract) ═══
//
// Water is 4-CONNECTED (the deliberate complement of land's 8 — a diagonal isthmus joins land and
// separates the water either side). THE OCEAN = the 4-connected water component that touches the
// map border (the Trench guarantees the border is water, so the corner is a safe seed). Every
// other below-sea cell — enclosed lagoons, lake beds, island-fringe pockets — is NOT ocean: to the
// drainage router it is ordinary terrain (a terminal basin or a fill-and-spill), and to a
// "sea-reaching" test it does not count as the sea. Computed on the CLASSIFY field (authoritative).
///
/// The ocean mask, row-major (x·n+y): true for every below-sea cell 4-connected to the map
/// border. Pure: reads , writes nothing.
///
public static bool[] OceanMask(float[,] classify, int mapSize, float sea, out long oceanCells, out long enclosedWaterCells)
{
int n = mapSize;
var ocean = new bool[n * n];
var q = new Queue();
void Seed(int x, int y) { if (classify[x, y] < sea && !ocean[x * n + y]) { ocean[x * n + y] = true; q.Enqueue(x * n + y); } }
for (int x = 0; x < n; x++) { Seed(x, 0); Seed(x, n - 1); }
for (int y = 0; y < n; y++) { Seed(0, y); Seed(n - 1, y); }
int[] dx4 = { -1, 1, 0, 0 }, dy4 = { 0, 0, -1, 1 };
while (q.Count > 0)
{
int c = q.Dequeue(); int cx = c / n, cy = c % n;
for (int k = 0; k < 4; k++)
{
int nx = cx + dx4[k], ny = cy + dy4[k];
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
int ni = nx * n + ny;
if (ocean[ni] || classify[nx, ny] >= sea) continue;
ocean[ni] = true; q.Enqueue(ni);
}
}
oceanCells = 0; enclosedWaterCells = 0;
for (int x = 0; x < n; x++)
for (int y = 0; y < n; y++)
{
if (classify[x, y] >= sea) continue;
if (ocean[x * n + y]) oceanCells++; else enclosedWaterCells++;
}
return ocean;
}
/// Island counts per hemisphere (non-mainland components, by centroid).
public static (int north, int south) IslandsByHemisphere(RegionLabels labels)
{
int nN = 0, nS = 0;
foreach (var r in labels.Regions)
{
if (r.IsMainland) continue;
if (r.Hemisphere == HemiNorth) nN++; else if (r.Hemisphere == HemiSouth) nS++;
}
return (nN, nS);
}
}
}