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]}"; /// 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); } } }