Stage 1 fills the IslandFalloff.cs stub from the reference, verbatim: the coast shelf (below-sea only, depth-preserving, held strictly below sea by MathF.BitDecrement — the clamp that makes "cannot move the waterline" exact rather than statistical) and the islet layer (OffshoreBlob, CalibrateThreshold against the field's ACTUAL distribution, the OffshoreZoneWeight moat + pre-Trench-falloff test). Both run as pass 1b — OffshorePass, a second sweep over the finished pass-1 arrays with the same per-pixel arithmetic in the same order — and HMaxSeed is retaken AFTER them, as the reference did. That closes chat2/00 Drift §2. The value did not move on any of 15 runs; the order is now right by construction and oracle (l) prints it every time. Stage 2 is the reshape, OffshoreSettings.Hybrid(): a seeded floor of ≥2 N / ≥4 S islands placed by a PCG32 off the world seed — min-separated, clear of ALL existing land by a gap so each stamp is its own connected component by construction, fully inside the zone so the moat and falloff protections gate the floor exactly as they gate the organic layer — plus the noise layer on top, smaller (freq 16), lower (24 m pre-curve, which the preserved toe squashes to ~5 m and keeps there across curve tweaks), flatter (core 0.25), crisper (edge sharpness 2.5, stamp rim jittered so it is rigid without being a compass disc), south-weighted (0.007 N / 0.012 S, blended across the midline), corners allowed (trench mask 0.90→0.97). Every lifted cell is tagged with its hemisphere — NORTH is rows [0, N/2), y runs south, read off the spine's southern fade and the southern sinker, not invented — and carried through Pass1Result → Pass2Result for a consumer that does not exist yet. Two things the probes taught, both now in the code: the first organic densities produced 183 blobs of which 174 were noise debris (a six-config sweep fixed that), and the reference's lerp-to-crest leaves shallow humps across the seabed wherever a blob fails to surface (a guard reverts them outside any surviving island's skirt; specks by component membership, bumps by location). The faithful control keeps both, because it is the reference — its islets measure min 1 cell, median 28. Oracle, all hard checks passing: floor met on every hybrid seed (N 2–4, S 9–16); zero land bridges; every offshore-OFF land cell bit-identical with offshore ON; tag ↔ coastline consistent in both fields; curve-off still bit-identical to Phase 1's dump and continuous_restored to task 03's. Both gates default OFF, deliberately: flipping them is the act that retires the Phase-1 regression dumps, and that belongs in a task that re-baselines the oracles. The faithful control on seed 8675309 produced one north island. That is the gap the floor exists to close. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
177 lines
6.3 KiB
C#
177 lines
6.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace IslaApocalypse.Tools
|
|
{
|
|
/// <summary>One connected island of tagged offshore land, as the analysis sees it.</summary>
|
|
public sealed class IslandComponent
|
|
{
|
|
public int Id;
|
|
public long Cells;
|
|
public double CentroidX, CentroidY;
|
|
public int MinX, MinY, MaxX, MaxY;
|
|
|
|
/// <summary>Hemisphere by CENTROID (an island straddling the midline is counted once, where its mass is).</summary>
|
|
public byte Hemisphere;
|
|
|
|
/// <summary>
|
|
/// ⚠ True if any cell of this island is 8-adjacent to land that is NOT tagged offshore —
|
|
/// i.e. the island touches the mainland. The moat exists to make this impossible; this is
|
|
/// the check that it did.
|
|
/// </summary>
|
|
public bool BridgedToMainland;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ⭐ THE OFFSHORE ANALYSIS — counts islands, reads their hemisphere, and catches a land bridge.
|
|
/// Engine-free; used by the pass (to prove its own floor) and by the oracle (to prove it again,
|
|
/// independently, on the finished field).
|
|
///
|
|
/// ═══ THE HEMISPHERE CONVENTION — read from the code, not invented ═══
|
|
///
|
|
/// Pass 1's latitude scalar is <c>y / MapSize</c> (+ a ±0.1 wobble). The spine fades out where
|
|
/// that scalar exceeds 0.65 — "the southern fade" — and the "southern sinker" bites in the
|
|
/// BOTTOM 25 % of rows. So in this codebase, and in the lore it encodes (snow-town north,
|
|
/// shipwreck south): <b>y increases SOUTHWARD. North is the top half of the image.</b>
|
|
///
|
|
/// NORTH y ∈ [0, MapSize/2)
|
|
/// SOUTH y ∈ [MapSize/2, MapSize)
|
|
///
|
|
/// ⚠ The tag uses the clean row midline, NOT the wobbled latitude field. A hemisphere tag keyed
|
|
/// to a field that wanders ±10 % of the map would put the same island in different hemispheres
|
|
/// on different seeds for no geographic reason. The field's ORIENTATION is what is borrowed; its
|
|
/// wobble is not.
|
|
/// </summary>
|
|
public static class OffshoreAnalysis
|
|
{
|
|
public const byte HemiNone = 0;
|
|
public const byte HemiNorth = 1;
|
|
public const byte HemiSouth = 2;
|
|
|
|
/// <summary>The convention, in one place. Every consumer of the tag reads hemisphere through this.</summary>
|
|
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.
|
|
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 };
|
|
|
|
/// <summary>
|
|
/// Label the 8-connected components of tagged offshore land, and for each, whether it
|
|
/// touches untagged land (a bridge). <paramref name="height"/> + <paramref name="sea"/>
|
|
/// define "land"; <paramref name="tag"/> defines "offshore". Both are needed: the bridge test
|
|
/// is "tagged cell next to a land cell that is not tagged".
|
|
/// </summary>
|
|
public static List<IslandComponent> Components(bool[,] tag, float[,] height, float sea, int mapSize)
|
|
=> Components(tag, height, sea, mapSize, out _);
|
|
|
|
/// <summary>
|
|
/// As above, also returning the per-cell component id map (<c>x * mapSize + y</c>; 0 = not
|
|
/// tagged) — the debris guard needs membership, not just the list.
|
|
/// </summary>
|
|
public static List<IslandComponent> Components(bool[,] tag, float[,] height, float sea, int mapSize,
|
|
out int[] idMap)
|
|
{
|
|
var comps = new List<IslandComponent>();
|
|
int n = mapSize;
|
|
var id = new int[n * n]; // 0 = unvisited / not tagged
|
|
idMap = id;
|
|
if (tag == null) return comps;
|
|
|
|
var stack = new Stack<int>();
|
|
int next = 0;
|
|
|
|
for (int sx = 0; sx < n; sx++)
|
|
{
|
|
for (int sy = 0; sy < n; sy++)
|
|
{
|
|
if (!tag[sx, sy] || id[sx * n + sy] != 0) continue;
|
|
|
|
var c = new IslandComponent
|
|
{
|
|
Id = ++next, MinX = sx, MaxX = sx, MinY = sy, MaxY = sy,
|
|
};
|
|
double sumX = 0, sumY = 0;
|
|
|
|
id[sx * n + sy] = c.Id;
|
|
stack.Push(sx * n + sy);
|
|
|
|
while (stack.Count > 0)
|
|
{
|
|
int cur = stack.Pop();
|
|
int cx = cur / n, cy = cur % n;
|
|
c.Cells++; sumX += cx; sumY += cy;
|
|
if (cx < c.MinX) c.MinX = cx; if (cx > c.MaxX) c.MaxX = cx;
|
|
if (cy < c.MinY) c.MinY = cy; if (cy > c.MaxY) c.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;
|
|
|
|
if (tag[nx, ny])
|
|
{
|
|
int ni = nx * n + ny;
|
|
if (id[ni] != 0) continue;
|
|
id[ni] = c.Id;
|
|
stack.Push(ni);
|
|
}
|
|
else if (height[nx, ny] >= sea)
|
|
{
|
|
// Land, not tagged offshore ⇒ mainland (or a lake-shore) touching
|
|
// this island. The moat should have made this impossible.
|
|
c.BridgedToMainland = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
c.CentroidX = sumX / c.Cells;
|
|
c.CentroidY = sumY / c.Cells;
|
|
c.Hemisphere = HemisphereOfRow((int)Math.Round(c.CentroidY), mapSize);
|
|
comps.Add(c);
|
|
}
|
|
}
|
|
return comps;
|
|
}
|
|
|
|
/// <summary>Island counts per hemisphere, by component centroid.</summary>
|
|
public static (int north, int south) CountByHemisphere(List<IslandComponent> comps)
|
|
{
|
|
int nN = 0, nS = 0;
|
|
foreach (var c in comps)
|
|
{
|
|
if (c.Hemisphere == HemiNorth) nN++;
|
|
else if (c.Hemisphere == HemiSouth) nS++;
|
|
}
|
|
return (nN, nS);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Island SIZE statistics — the thing a count alone hides. 189 islands averaging 66 cells is
|
|
/// noise debris, not an archipelago; 12 islands averaging 900 cells is what the developer
|
|
/// asked for. Cells are map cells (1 column = 1 m at the target scale).
|
|
/// </summary>
|
|
public static (long min, long median, double mean, long max, int belowThreshold)
|
|
SizeSummary(List<IslandComponent> comps, long threshold)
|
|
{
|
|
if (comps.Count == 0) return (0, 0, 0.0, 0, 0);
|
|
var sizes = new List<long>(comps.Count);
|
|
double sum = 0; int below = 0;
|
|
foreach (var c in comps) { sizes.Add(c.Cells); sum += c.Cells; if (c.Cells < threshold) below++; }
|
|
sizes.Sort();
|
|
return (sizes[0], sizes[sizes.Count / 2], sum / sizes.Count, sizes[sizes.Count - 1], below);
|
|
}
|
|
|
|
/// <summary>How many components touch the mainland. Zero is the only acceptable answer.</summary>
|
|
public static int BridgedCount(List<IslandComponent> comps)
|
|
{
|
|
int b = 0;
|
|
foreach (var c in comps) if (c.BridgedToMainland) b++;
|
|
return b;
|
|
}
|
|
}
|
|
}
|