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; } /// /// ⭐⭐ SIGNIFICANT WATER (rivers/03) — the interim substitute for the reference's water-bodies /// table, built with this layer's own connected-component machinery. /// /// ═══ WHY THIS EXISTS ═══ /// /// The reference builds `isSignificantWater` from `_waterBodies` — cells of any body with /// `PixelCount >= RiverLakeMinTargetPx` — and a lake-ender routes to THAT rather than to any wet /// pixel. **v2 has no water-bodies table yet** (a known port gap, `00_ground` §D3 / /// carry-forward §5), so this labels 8-connected components of classify water directly and keeps /// the ones at least cells. Same semantics, same threshold, no table. /// /// ⚠ The size filter is the whole point and it is not a detail: routing a lake-ender to the /// NEAREST wet pixel put one into a three-cell puddle a few hundred px short of the obvious /// lagoon — the reference's own task-23 gate finding. "Nearest water" is satisfied by a puddle. /// /// ⚠ OCEAN IS EXCLUDED. A lake-ender that could reach the ocean is not a lake-ender; including /// ocean here would let one "terminate" at the coast and quietly become a sea river without ever /// passing the routed test. /// /// Pure: reads the mask, writes nothing, creates no water. Same 8-connectivity and same fixed /// neighbour order as , so component identity is deterministic. /// public static bool[] SignificantWaterMask(bool[] isClassifyWater, bool[] isOcean, int mapSize, int minPx, out int bodiesKept, out int bodiesTotal, out long cellsKept, out long largestPx) { int n = mapSize; var seen = new bool[n * n]; var mask = new bool[n * n]; var stack = new Stack(); var component = new List(); bodiesKept = 0; bodiesTotal = 0; cellsKept = 0; largestPx = 0; for (int s = 0; s < n * n; s++) { if (seen[s] || !isClassifyWater[s] || isOcean[s]) continue; component.Clear(); seen[s] = true; stack.Push(s); while (stack.Count > 0) { int cur = stack.Pop(); component.Add(cur); int cx = cur / n, cy = cur % n; 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 (seen[ni] || !isClassifyWater[ni] || isOcean[ni]) continue; seen[ni] = true; stack.Push(ni); } } bodiesTotal++; if (component.Count > largestPx) largestPx = component.Count; if (component.Count >= minPx) { bodiesKept++; cellsKept += component.Count; foreach (int c in component) mask[c] = true; } } return mask; } /// /// 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); } } }