islaApocalypse-v2/Tools/Scripts/Pass1Result.cs
beezm c32a3b177c chat2/07: the region-labeling layer — label all land, tag by construction, tunable speck revert
Core/Scripts/RegionLabeling.cs is the shared-infra contract, built to the letter: it runs on
the CLASSIFY (raw) field; land is 8-connected, the deliberate complement of water's 4 (a
diagonal isthmus joins; the water either side stays separate); a component is a maximal
8-connected set of land cells; the MAINLAND is the component containing the map centre —
not merely the largest, which a later fragmentation step could flip — with a flagged
fallback to the largest if the centre were ever water (asserted, never needed: oracle m);
every other component is an island; per component id / sizeCells / centroid / hemisphere
(by centroid, one label per island) / isMainland. Ids come from a fixed scan order and are
proven stable across two generations (oracle o, 16.8M cells). It knows nothing about
offshore or stamped. Engine-free, in Core as C++-candidate math; the hemisphere convention
moved there with it, OffshoreAnalysis aliases it.

Tools/Scripts/RegionPass.cs is pass 1c: label, revert, relabel, tag. The island tag
(renamed IsIsland; IslandHemisphere from the component's centroid; Pass1Result.Regions
carries the whole table) is now a CONSEQUENCE of labeling — every non-mainland component.
That is the fix for the chat2/06 overlay, which tagged only what the offshore pass raised:
1063685222 has 11 natural islands including a 94,511-cell detached mass, 20260821 has 19,
all grey in 06's tags.png and all coloured now. The offshore pass itself is untouched; its
internal Tag stays for its own guards and is no longer exported.

The speck revert (TerrainGenConfig.SpeckRevert / MinLandComponentFrac) lowers every
non-mainland component below the threshold to the mean of its ring of adjacent sea cells,
held strictly below sea. Origin-blind: a natural nub goes the same way as an offshore dot
(6 natural components / 273 cells on the bare 1063685222 field at threshold_mid — reported
as a3r, informational). Lower-only and component-only are asserted cell by cell in the
pass and re-proven on the finished fields by oracle n (mainland bit-identical filter OFF
vs ON; every changed cell in a sub-threshold island, lowered below sea); the mainland is
never a candidate and its size is asserted unchanged across the revert. A reverted
offshore island leaves its submerged skirt as a shoal — not this component, by the rule.
Classify/render consistency is by construction (pass 1, curve identity at sea) and
asserted by oracle k. Deliberately OFF in the bare TerrainGenConfig for the reason the
shelf and islets are: the raw field has natural specks, so default-ON would move the
calibration pool and every regression dump; the batch turns it on.

Thresholds swept on 8 seeds at 4096 (1e-5 / 3e-5 / 1e-4 of the map = 168 / 503 / 1,678
cells): low removes 0–6 nubs per seed, mid (the config default, equal to the offshore
guard) 2–11, high 26–36 — most of the offshore islands, the "fewer, bigger" bookend. The
count/size table carries natural / pre / post counts per hemisphere, min/median/mean/max
and a log-spaced size histogram — the instrument for the southern-stretch step.

Oracle, all passing: a1, a3, a4 (8192, 67M cells) with labeling ON + revert OFF; a6 NEW —
labeling ON + revert OFF on the 06 preset bit-identical to the 06 batch's render field
(labeling is pure analysis); j0; m, n, o, i, j, k, l, b per field. Batch:
BatchRoot(7, "region_labeling") — exactly 4 plates (three thresholds on 1063685222,
threshold_mid on 20260821, the table's most-natural-islands seed), each with grayscale /
.f32 / relief / the labeled-regions overlay / the tag overlay, plus count_size_table.md/.csv.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
2026-08-22 03:50:33 -04:00

153 lines
7.2 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using IslaApocalypse.Core;
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 ISLAND TAG — BY CONSTRUCTION from the region layer (chat2/07; chat2/05's tag, fixed) ═══
//
// DATA, set here, carried downstream, READ BY NOTHING IN THIS PHASE. It exists so a later
// pass (biome, fertility, placement) can find island land without re-deriving it from
// geometry. Since chat2/07 it is a CONSEQUENCE OF LABELING: every cell of every non-mainland
// land component is tagged — the big organic detached masses the same as an offshore-pass
// dot (chat2/0506 tagged only what the offshore pass raised; that was the bug). Both arrays
// are NULL when region labeling is off — a consumer checks for null, not for all-false.
/// <summary>Per column: is this land an island (any non-mainland land component)? Null when labeling is off.</summary>
public readonly bool[,] IsIsland;
/// <summary>
/// Per column: <see cref="RegionLabeling.HemiNorth"/> / <see cref="RegionLabeling.HemiSouth"/>
/// for tagged cells (the COMPONENT's hemisphere, by centroid), <see cref="RegionLabeling.HemiNone"/>
/// otherwise. Null when labeling is off.
/// </summary>
public readonly byte[,] IslandHemisphere;
/// <summary>⭐ The region layer's output for this field: id map + per-component table (post-revert). Null when labeling is off.</summary>
public readonly RegionLabels Regions;
/// <summary>The labeling BEFORE the speck revert (== <see cref="Regions"/> when the revert is off or removed nothing). For the overlay's "where a speck was". Null when labeling is off.</summary>
public readonly RegionLabels RegionsPre;
/// <summary>The region pass's numbers — pre/post-revert counts and sizes, what was reverted. Null when labeling is off.</summary>
public readonly RegionLedger RegionLedger;
/// <summary>Cells the islet layer lifted above sea. The reference printed this too.</summary>
public readonly long OffshoreLiftedCells;
/// <summary>The islet layer's numbers — thresholds, pre-guard count, guard ledger, final count. Null when offshore is off.</summary>
public readonly OffshoreLedger OffshoreLedger;
/// <summary>Lines worth printing from the shelf/offshore pass: thresholds, guard ledger, 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[,] isIsland = null, byte[,] islandHemisphere = null,
long offshoreLiftedCells = 0, IReadOnlyList<string> notes = null, OffshoreLedger offshoreLedger = null,
RegionLabels regions = null, RegionLedger regionLedger = null, RegionLabels regionsPre = null)
{
MapSize = mapSize;
Seed = seed;
Height = height;
PreTrenchFalloff = preTrenchFalloff;
LatitudeField = latitudeField;
HMaxSeed = hMaxSeed;
HMinSeed = hMinSeed;
ElapsedMs = elapsedMs;
HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore;
IsIsland = isIsland;
IslandHemisphere = islandHemisphere;
Regions = regions;
RegionsPre = regionsPre;
RegionLedger = regionLedger;
OffshoreLiftedCells = offshoreLiftedCells;
OffshoreLedger = offshoreLedger;
Notes = notes ?? System.Array.Empty<string>();
}
/// <summary>Whether the region layer ran on this field (the tag arrays are present).</summary>
public bool HasIslandTag => IsIsland != 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);
}
}
}