islaApocalypse-v2/Tools/Scripts/Pass2Result.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

179 lines
7.9 KiB
C#

using System.Collections.Generic;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// Everything pass 2 produces: THE TWO HEIGHT FIELDS. → <see cref="Shaping"/>.
///
/// ═══ ⭐⭐ THE TWO-FIELD SPLIT (D-046) — THE DISCIPLINE THIS CLASS EXISTS TO HOLD ═══
///
/// <see cref="HeightClassify"/> RAW. Uncurved, un-detailed. The ORACLE.
/// <see cref="Height"/> RENDER. Curved, detailed, and later eroded and carved.
///
/// Everything that CLASSIFIES the world — biomes, water bodies, the ocean flood fill — reads the
/// classify field. Everything that DRAWS or MESHES it reads the render field. The reference's
/// hardest-won lesson is that this split is what made five rounds of taste-iteration safe: the
/// biome and water maps stayed md5-identical across every shaping change, so correctness was
/// never being judged by eye. → `Design - Tooling - Iteration and Batching.md`,
/// "build the oracle before the taste-iteration".
///
/// ⚠ NOTHING CONSUMES THE CLASSIFY FIELD YET. No water, no biomes exist in this phase. The split
/// is established HERE, at the curve, because the curve is where the second field is BORN — and
/// retrofitting a classify path after three passes already ran on one array is how the two
/// silently diverge. The field is produced and asserted now so that when water lands it has
/// something correct to read.
///
/// ═══ ⚠ WHEN THE TWO FIELDS ARE THE SAME ARRAY ═══
///
/// With the curve OFF there is nothing to separate, so both properties reference ONE array —
/// exactly as the reference did (<c>_heightMapClassify = (_curveOn || _erosionOn) ? new float[…]
/// : _heightMap</c>). <see cref="FieldsAreAliased"/> says so out loud, because a later pass that
/// writes through one reference while reading the other MUST know: the reference's crater carve
/// reads both into locals before writing either for precisely this reason, and that is the trap
/// this flag is here to keep visible until the carve lands.
/// </summary>
public sealed class Pass2Result
{
/// <summary>Map side in columns.</summary>
public readonly int MapSize;
/// <summary>The resolved seed. Same seed, same two fields.</summary>
public readonly int Seed;
/// <summary>
/// ⭐ THE RENDER FIELD, <c>[x, y]</c> — curved and detailed. What gets drawn, dumped and
/// (later) eroded, carved and meshed.
/// </summary>
public readonly float[,] Height;
/// <summary>
/// ⭐ THE CLASSIFY FIELD, <c>[x, y]</c> — bit-for-bit the raw pre-curve pass-1 height.
///
/// ⚠ Nothing may write to this after pass 2 except the crater carve, which is the one pass
/// that legitimately moves both fields. Erosion, rivers and detail are render-only.
/// </summary>
public readonly float[,] HeightClassify;
/// <summary>Was the curve applied? The primary A/B gate.</summary>
public readonly bool CurveOn;
/// <summary>
/// Which curve shaped the render field: "off", "staircase", "continuous" or "lifted_WRONG".
/// A label, not logic — the oracle and the INDEX read it so a result can never be mistaken
/// for the wrong mode's.
/// </summary>
public readonly string CurveModeLabel;
/// <summary>
/// The batch variant this result belongs to, carried straight from
/// <see cref="TerrainGenConfig.VariantLabel"/>.
///
/// ⚠ CARRIED, NOT INFERRED. The first cut of the chat2/02 tool reconstructed this from the
/// knob values ("ceiling &gt; 31 ⇒ hold_higher"), which works only for exactly today's
/// variant set: add a second variant sharing a knob value and two different runs silently
/// write to one folder. The label is an identity, so it travels with the result.
/// </summary>
public readonly string VariantLabel;
/// <summary>
/// The per-seed continuous spline, when <see cref="CurveModeLabel"/> is "continuous" — the
/// oracle samples its slopes (check e) and the INDEX prints its control points. Null for
/// every other mode.
/// </summary>
public readonly ContinuousCurve Continuous;
// ═══ ⭐ THE OFFSHORE TAG, CARRIED (chat2/05) ═══
//
// Pass 1 sets it; pass 2 carries it UNCHANGED beside the two height fields, because this is
// where the shaped-terrain result flows and where a downstream consumer would pick it up.
// The curve is identity at sea and monotone above, so a cell that was offshore-island LAND
// in pass 1 is still land in the render field — the tag stays valid for both fields without
// being recomputed (oracle: "classify/render coastline consistent").
//
// ⚠ NO LOGIC READS IT THIS PHASE. It is a data layer. A biome/fertility/placement pass reads
// it from here, checks for null (offshore off), and never re-derives island-land from
// geometry.
/// <summary>→ <see cref="Pass1Result.IsOffshoreIsland"/>, the same array. Null when offshore is off.</summary>
public readonly bool[,] IsOffshoreIsland;
/// <summary>→ <see cref="Pass1Result.IslandHemisphere"/>, the same array. Null when offshore is off.</summary>
public readonly byte[,] IslandHemisphere;
/// <summary>Was shelf detail applied? Requires <see cref="CurveOn"/> — it warps the curve's knots.</summary>
public readonly bool DetailOn;
/// <summary>The knot set used. Null when the curve is off.</summary>
public readonly CurveKnots Knots;
/// <summary>The output anchors used. Null when the curve is off.</summary>
public readonly CurveAnchors Anchors;
/// <summary>The per-seed spike input, carried through from pass 1.</summary>
public readonly float HMaxSeed;
/// <summary>The edge-warp amplitude actually APPLIED, raw units (post-clamp). Zero when detail is off.</summary>
public readonly float EdgeAmpRaw;
/// <summary>The knot set's safe warp bound, raw units — what <see cref="EdgeAmpRaw"/> was clamped to.</summary>
public readonly float MaxEdgeShiftRaw;
/// <summary>Render-field extremes after shaping. For the ramp and the report.</summary>
public readonly float HMin, HMax;
/// <summary>Wall-clock milliseconds pass 2 took.</summary>
public readonly ulong ElapsedMs;
/// <summary>
/// Lines worth printing: the monotonicity confirmation, any loud clamp. Collected rather than
/// printed inside the pass so the shaping code stays a pure function of its inputs and the
/// tool owns the console.
/// </summary>
public readonly List<string> Notes;
public Pass2Result(int mapSize, int seed, float[,] height, float[,] heightClassify,
bool curveOn, bool detailOn, string curveModeLabel, string variantLabel,
ContinuousCurve continuous, CurveKnots knots, CurveAnchors anchors, float hMaxSeed,
float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs,
List<string> notes, bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null)
{
IsOffshoreIsland = isOffshoreIsland;
IslandHemisphere = islandHemisphere;
MapSize = mapSize;
Seed = seed;
Height = height;
HeightClassify = heightClassify;
CurveOn = curveOn;
DetailOn = detailOn;
CurveModeLabel = curveModeLabel;
VariantLabel = variantLabel;
Continuous = continuous;
Knots = knots;
Anchors = anchors;
HMaxSeed = hMaxSeed;
EdgeAmpRaw = edgeAmpRaw;
MaxEdgeShiftRaw = maxEdgeShiftRaw;
HMin = hMin;
HMax = hMax;
ElapsedMs = elapsedMs;
Notes = notes;
}
/// <summary>
/// ⚠ True when the two fields ARE the same array (curve off). Any pass that writes one while
/// reading the other must read both into locals first. See the type header.
/// </summary>
public bool FieldsAreAliased => ReferenceEquals(Height, HeightClassify);
/// <summary>Fraction of the RENDER field at or above the sea threshold.</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);
}
}
}