islaApocalypse-v2/Tools/Scripts/Pass1Result.cs
beezm 3b96e06c1a chat2/05: offshore islands — rejoin the stub faithfully, then reshape to a guaranteed hybrid
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
2026-08-21 05:14:53 -04:00

142 lines
6.5 KiB
C#

using System.Collections.Generic;
namespace IslaApocalypse.Tools
{
/// <summary>
/// Everything pass 1 produces. → <see cref="Topography"/>.
///
/// ⭐ THREE OF THESE FIELDS ARE THE PHASE-2 SEAM. The reference computed them in pass 1 and
/// consumed them in pass 2; exposing them now is what keeps the deferred ports clean, and it
/// costs nothing to carry.
/// </summary>
public sealed class Pass1Result
{
/// <summary>Map side in columns.</summary>
public readonly int MapSize;
/// <summary>The resolved seed this was generated with. Always positive, always recorded.</summary>
public readonly int Seed;
/// <summary>
/// The raw pass-1 height field, <c>[x, y]</c>. Pre-curve, pre-erosion, pre-crater.
/// The reference's <c>finalH</c> (~:619) written to <c>_heightMap</c> (~:666).
/// </summary>
public readonly float[,] Height;
/// <summary>
/// ⭐ PHASE-2 SEAM — the falloff value captured BEFORE the <c>Pow(·, 2.5f)</c> and BEFORE the
/// Trench wall (reference ~:591, <c>preTrenchFalloff</c>).
///
/// It is the "how far past the island body are we" number, uncontaminated by the power's
/// curvature and by the Trench's additive ramp. The DEFERRED offshore-islet layer reads it
/// as its distance mask (<c>OffshoreZoneWeight</c>), which is the only reason it is captured
/// mid-computation rather than recomputed.
///
/// ⚠ A port that "tidies" the falloff chain into one expression destroys this capture point,
/// and the loss would not show up until Phase 2's offshore mask moved. It is exposed here so
/// that cannot happen quietly.
/// </summary>
public readonly float[,] PreTrenchFalloff;
/// <summary>
/// ⭐ PHASE-2 SEAM — the wobbled latitude scalar, <c>[x, y]</c>.
///
/// ⚠ THIS IS NOT A CLIMATE TEMPERATURE MAP. See <see cref="Topography"/> for the full note.
/// It is a stage-1-local latitude field that terrain GEOMETRY reads. Phase 2 needs it if the
/// latitude sea-level model is ever switched on (<c>SeaLevelModel = "field"</c>); under the
/// shipped flat model it has no consumer beyond the spine.
/// </summary>
public readonly float[,] LatitudeField;
/// <summary>
/// ⭐ PHASE-2 SEAM — the maximum height over the whole map (reference <c>_hMaxSeed</c>, ~:665).
///
/// The reference's v2+ redistribution curve is SEED-DEPENDENT: its spike maps
/// <c>[t4, hMaxSeed]</c> onto the peak band, so pass 2 cannot start until pass 1 has scanned
/// every pixel. That is precisely why the pass-1/pass-2 boundary is a hard one and not an
/// interleave. Carried here so Phase 2 inherits it rather than rediscovering the dependency.
/// </summary>
public readonly float HMaxSeed;
/// <summary>The minimum height. Not a reference field — carried for the renderer's ramp and the report.</summary>
public readonly float HMinSeed;
/// <summary>
/// ⚠ The map-wide max BEFORE the coast shelf and offshore islets ran (chat2/05). The
/// reference takes <c>_hMaxSeed</c> AFTER both, inside the same loop; v2 now does too —
/// <see cref="HMaxSeed"/> is the post-shelf/offshore value the curve normalizes against.
/// This one is carried so the report can state whether the two differed (expected: no, an
/// islet crest of ~34 m is far below any peak — but "expected" is measured, not assumed).
/// </summary>
public readonly float HMaxSeedBeforeOffshore;
// ═══ ⭐ THE OFFSHORE TAG — chat2/05's one forward-looking piece ═══
//
// DATA, set here, carried downstream, READ BY NOTHING IN THIS PHASE. It exists so a later
// pass (biome, fertility, placement) can find offshore-island land without re-deriving it
// from geometry. Downstream that ignores it is unaffected; downstream that reads it gets a
// clean flag. Both arrays are NULL when the offshore layer is off — a consumer checks for
// null, not for all-false.
/// <summary>
/// Per column: is this land an offshore island (as opposed to mainland)? Set for every cell
/// the islet layer lifted from below sea to at-or-above sea. Null when offshore is off.
/// </summary>
public readonly bool[,] IsOffshoreIsland;
/// <summary>
/// Per column: <see cref="OffshoreAnalysis.HemiNorth"/> / <see cref="OffshoreAnalysis.HemiSouth"/>
/// for tagged cells, <see cref="OffshoreAnalysis.HemiNone"/> otherwise. The convention is the
/// row midline — see <see cref="OffshoreAnalysis"/>. Null when offshore is off.
/// </summary>
public readonly byte[,] IslandHemisphere;
/// <summary>The seeded-floor stamp centres and radii (Hybrid only; empty otherwise). For the overlay and the report.</summary>
public readonly IReadOnlyList<(int x, int y, int r)> OffshoreCentres;
/// <summary>Cells the islet layer lifted above sea. The reference printed this too.</summary>
public readonly long OffshoreLiftedCells;
/// <summary>Lines worth printing from the shelf/offshore pass: thresholds, floor placement, lifted counts.</summary>
public readonly IReadOnlyList<string> Notes;
/// <summary>Wall-clock milliseconds the pass took.</summary>
public readonly ulong ElapsedMs;
public Pass1Result(int mapSize, int seed, float[,] height, float[,] preTrenchFalloff,
float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs,
float hMaxSeedBeforeOffshore = float.NaN,
bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null,
IReadOnlyList<(int x, int y, int r)> offshoreCentres = null,
long offshoreLiftedCells = 0, IReadOnlyList<string> notes = null)
{
MapSize = mapSize;
Seed = seed;
Height = height;
PreTrenchFalloff = preTrenchFalloff;
LatitudeField = latitudeField;
HMaxSeed = hMaxSeed;
HMinSeed = hMinSeed;
ElapsedMs = elapsedMs;
HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore;
IsOffshoreIsland = isOffshoreIsland;
IslandHemisphere = islandHemisphere;
OffshoreCentres = offshoreCentres ?? System.Array.Empty<(int, int, int)>();
OffshoreLiftedCells = offshoreLiftedCells;
Notes = notes ?? System.Array.Empty<string>();
}
/// <summary>Whether the offshore layer ran on this field (the tag arrays are present).</summary>
public bool HasOffshoreTag => IsOffshoreIsland != null;
/// <summary>Fraction of the map at or above the sea threshold. A cheap shape sanity number.</summary>
public float LandFraction(float seaLevel)
{
long land = 0;
for (int x = 0; x < MapSize; x++)
for (int y = 0; y < MapSize; y++)
if (Height[x, y] >= seaLevel) land++;
return land / (float)((long)MapSize * MapSize);
}
}
}