islaApocalypse-v2/Tools/Scripts/OffshoreSettings.cs
beezm e8571b2778 chat2/06: offshore islands — revert the forced floor, tune the organic layer for coverage
The seeded floor from chat2/05 — FloorNorth/FloorSouth, the PCG32 placement, StampWeight,
StampRadiusFrac/StampCoreFrac/StampEdgeJitter, the centre separation and land gap, the
floor self-check and oracle (h), the centre rings on the tag overlay — is reverted out
whole, as a forward commit. It looked stamped. It is one checkout away at 3b96e06. What
remains is ONE island mechanism: the organic noise-field layer, with chat2/05 stage 2's
shape untouched (freq 16, crest 24 m pre-curve, core 0.25, sharpness 2.5, corners on,
moat + falloff test + outer bound unmoved), now OffshoreSettings.Organic(). The count per
hemisphere is a statistical outcome of the tuning, read off a table, never guaranteed.

The south was measured before anything moved (OffshoreDiagnosis, over task 01's pool at
2048): the premise that the south generates fewer islands is not what the field says.
The NORTH has 1.75x less island-eligible ocean (56 % of its ocean is zone vs 69 %; 233
zone rows per column vs 407) and under half the island candidates (1,183 over-threshold
peaks in zone vs 2,422). Per cell the south is richer too (484 vs 414 candidates per
Mcell). The gate that loses candidates is the moat in the north and the falloff test in
the south — the bulging south coast pushes the 0.72 contour out — but neither binds
enough to suppress it. So the fix is density, and the binding constraint for "a couple
north, consistently" is the north.

The knobs: Density (the main one, the calibration quantile) and SouthWeight (south density
= Density x weight — applied to the quantile, so a south island looks exactly like a north
one; there are just more). Three levels batched on 12 seeds at 4096, SouthWeight 1.25:
0.016 N 2/4.4/10 S 8/13.8/21 (every seed clears N >= 2, S >= 3, but N's minimum IS 2),
0.022 N 3/6.3/12 S 15/19.9/29 (the preset — the first level whose north minimum sits
comfortably above the target), 0.030 N 3/7.8/15 S 19/25.2/33 (the too-many bookend). The
south weight is the developer's stated preference, not a fix; the SouthWeight 1.0 tables
(S 6/10.9/16 at 0.022) are in the batch's scratch/ for the comparison.

The guards, so more density does not buy slop, each a revert by component membership:
specks (< 3e-5 of the map's area, ~500 cells at 4096 — 70-100 per field at these
densities, the noise caps the reference also surfaced), clusters (the smaller of two
islands whose shores are within 0.8 % of the map width — 0-7 per field), blobs (> 6e-4 —
never bit). And the reference's submerged humps, now attributed by HUMP rather than by
bounding box: the first 4096 plate probe showed hollow ghost rings beside islands — a
guard-dropped island's cap went back to seabed but its rim survived inside its
neighbour's bbox keep-region. A hump (one connected raised region) now stays only if it
holds a kept island, and a dropped island's own cap goes even inside a kept hump.

Oracle, all passing: a1 (Phase-1 dump), a3 (task-03 dump), a4 NEW — offshore OFF at 8192
bit-identical to the terrain-curve-v1 tag's own 04 gallery dump over 67,108,864 cells —
j0 shelf inert on land, and per field moat / mainland unmoved / tag-coastline / HMaxSeed
(unchanged, 1.289169) / classify on all 36 table fields and 4 plates. Batch:
BatchRoot(6, "offshore_organic_tune") — exactly 4 plates (three densities on 1063685222,
density_mid on 424242, the table's sparsest south) + count_table.md/.csv + diagnosis.md.

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

220 lines
11 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.Text;
namespace IslaApocalypse.Tools
{
/// <summary>Which offshore-islet system pass 1 runs. → <see cref="OffshoreSettings"/>.</summary>
public enum OffshoreMode
{
/// <summary>No islets. The shelf is gated separately (<c>TerrainGenConfig.CoastShelf</c>).</summary>
Off,
/// <summary>
/// ⭐ chat2/05 stage 1 — the reference's probabilistic layer, verbatim: one noise field, one
/// calibrated threshold, the faithful blob, the faithful zone mask. Sparse, no corners, no
/// guards. THE CONTROL.
/// </summary>
Faithful,
/// <summary>
/// ⭐ chat2/06 — THE ONE ISLAND MECHANISM: the organic noise-field layer, reshaped small /
/// low / flat / crisp (chat2/05 stage 2's shape, unchanged), tuned for coverage by DENSITY
/// and a SOUTH WEIGHT, with the speck / separation / blob guards. No seeded floor, no
/// stamps, no count guarantee — every island is a noise outline, and the per-hemisphere
/// counts are a statistical outcome of the tuning (the chat2/05 forced floor was tried and
/// reverted on look; it is in git history, one checkout away).
/// </summary>
Organic,
}
/// <summary>
/// ⭐ EVERY OFFSHORE-ISLET DIAL, IN ONE OBJECT — config-gated and isolated, per the developer's
/// standing modularity concern. Nothing above <see cref="OffshorePass"/> hardcodes an island
/// specific; the whole system is this object + <c>IslandFalloff</c> + <c>OffshorePass</c>.
///
/// ═══ TWO PRESETS, AND WHY BOTH EXIST ═══
///
/// <see cref="Faithful"/> the reference's constants, verbatim. The control in every batch.
/// <see cref="Organic"/> the reshape + the chat2/06 coverage tuning — the deliverable.
///
/// The reshape does not EDIT the faithful constants; it sets different values on the same
/// dials. So <c>Faithful()</c> stays bit-reproducible however far the organic layer is tuned.
///
/// ═══ THE TWO PROTECTIONS THAT ARE NOT DIALS ═══
///
/// <see cref="MinDepthM"/> (the moat) and <see cref="MinFalloff"/> (the "actually offshore" test)
/// are exposed here because every knob is, but they are the MAIN-ISLAND AND LAKE PROTECTIONS and
/// the presets do not move them. They are what makes "no island can bridge to shore" and "no
/// island in a lake or the crater bay" true by construction.
///
/// ═══ ⚠ WHAT IS DELIBERATELY NOT HERE (chat2/06) ═══
///
/// No floor, no stamp radius/core/jitter, no separation-of-seeded-centres, no placement RNG —
/// nothing that guarantees a count or places an island from a centre. The forced mechanism
/// (chat2/05 <c>Hybrid()</c>) was reverted out whole; if a hard count ever comes back it is a
/// design decision, not a knob that was left lying around.
/// </summary>
public sealed class OffshoreSettings
{
public OffshoreMode Mode = OffshoreMode.Off;
// ---- the organic (noise) layer ----------------------------------------
/// <summary>Islet noise frequency, periods per map width. Higher ⇒ SMALLER blobs. Reference 14.</summary>
public float FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS;
/// <summary>Islet noise seed offset. Reference 7607.</summary>
public int SeedOffset = IslandFalloff.OFFSHORE_SEED_OFFSET;
/// <summary>
/// ⭐ THE MAIN KNOB — organic density: the fraction of the noise field's ACTUAL sampled
/// distribution that clears the calibrated threshold. More ⇒ more of the field becomes
/// island. This is the NORTH density; the south is this × <see cref="SouthWeight"/>.
/// Reference 0.02.
/// </summary>
public float Density = 0.02f;
/// <summary>
/// ⭐ THE SOUTH WEIGHT — a per-hemisphere density bias. South density =
/// <see cref="Density"/> × this. 1 ⇒ one dial (the reference); &gt; 1 ⇒ "weighted south",
/// the developer's preference. It is applied to the DENSITY (i.e. the calibration quantile),
/// not to the blob shape, so a south island looks exactly like a north island — there are
/// simply more of them.
/// </summary>
public float SouthWeight = 1f;
/// <summary>The effective south density.</summary>
public float DensitySouth => Density * SouthWeight;
/// <summary>
/// Half-width of the smooth threshold blend across the hemisphere midline, as a fraction of
/// the map. ⚠ Without it a north/south density difference would cut any organic island that
/// straddles the midline along a dead-straight line. 0 ⇒ hard step (never wanted).
/// </summary>
public float HemisphereBlendHalfWidth = 0.05f;
/// <summary>Islet crest, metres above sea, PRE-CURVE. Reference 34. The curve's toe squashes it lower.</summary>
public float CrestM = IslandFalloff.OFFSHORE_ISLAND_H_M;
/// <summary>Fraction of a blob's excess over threshold that saturates. LOWER ⇒ flatter. Reference 0.45.</summary>
public float CoreFraction = IslandFalloff.OFFSHORE_CORE;
/// <summary>Crest-to-sea edge sharpening exponent. 1 ⇒ the faithful smoothstep. Higher ⇒ crisper shore.</summary>
public float EdgeSharpness = 1f;
// ---- the slop guards (Organic only; all scale-free fractions of the map) --------------
//
// More density must not buy speck-debris or merged blobs. Three rules, each a revert BY
// COMPONENT MEMBERSHIP (the island goes back to seabed whole, and is untagged) — none of
// them places, shapes or counts anything. 0 ⇒ that rule is off. The faithful preset has none
// (the reference had no such rules).
/// <summary>
/// ⚠ THE SPECK GUARD. An organic island smaller than this fraction of the map's AREA is
/// noise debris — a local maximum that barely cleared the threshold and surfaced a cap of a
/// few cells — and is reverted to seabed and untagged. 3e-5 is ~126 cells at 2048, ~500 at
/// 4096, ~2,000 cells (a ~50 m islet) at 8192.
/// </summary>
public float MinIslandAreaFrac = 0f;
/// <summary>
/// ⚠ THE SEPARATION GUARD — "enough separation that they read as distinct islands". Two
/// surviving islands whose nearest shores are closer than this fraction of the map WIDTH
/// read as one cluster; the SMALLER is reverted. Greedy by size, so the largest island in a
/// cluster always stays. Chebyshev distance between boundary cells.
/// </summary>
public float MinSeparationFrac = 0f;
/// <summary>
/// ⚠ THE BLOB GUARD. An organic island LARGER than this fraction of the map's area is a
/// superlevel region that merged several maxima into one sprawling landmass — the "blob"
/// the developer does not want — and is reverted whole. Set well above the natural size at
/// the preset density so it is a safety net, not a sculptor; how often it bites is reported.
/// </summary>
public float MaxIslandAreaFrac = 0f;
// ---- the zone mask: protections + the Trench bound --------------------
/// <summary>⭐ THE MOAT. Reference 14 m. The presets do not move it.</summary>
public float MinDepthM = IslandFalloff.OFFSHORE_MIN_DEPTH_M;
public float DepthFeatherM = IslandFalloff.OFFSHORE_DEPTH_FEATHER_M;
/// <summary>⭐ THE "ACTUALLY OFFSHORE" TEST on the pre-Trench falloff. Reference 0.72. The presets do not move it.</summary>
public float MinFalloff = IslandFalloff.OFFSHORE_MIN_FALLOFF;
public float FalloffFeather = IslandFalloff.OFFSHORE_FALLOFF_FEATHER;
/// <summary>
/// The outer bound, as a fraction of the half-span (map-anchored, like the Trench). Zone
/// fades from INNER to zero at OUTER. Reference 0.78 / 0.86 keeps islets well off the
/// Trench ramp (which starts at 0.90) and out of the corners. The reshape pushes both OUT so
/// islands populate the corners and may sit over the outer edge — the developer accepts
/// islands clipped by the map edge. OUTER &lt; 1 keeps every island's centre on the playable
/// map.
/// </summary>
public float TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER;
public float TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER;
// ---- presets ----------------------------------------------------------
/// <summary>The reference, verbatim. Single density, no guards, original footprint/crest/mask.</summary>
public static OffshoreSettings Faithful() => new OffshoreSettings
{
Mode = OffshoreMode.Faithful,
FreqPerMapWidth = IslandFalloff.OFFSHORE_FREQ_ISLANDS,
SeedOffset = IslandFalloff.OFFSHORE_SEED_OFFSET,
Density = 0.02f, SouthWeight = 1f, // ConfigManager.OffshoreIslandDensity
CrestM = IslandFalloff.OFFSHORE_ISLAND_H_M,
CoreFraction = IslandFalloff.OFFSHORE_CORE,
EdgeSharpness = 1f,
TrenchInner = IslandFalloff.OFFSHORE_TRENCH_INNER,
TrenchOuter = IslandFalloff.OFFSHORE_TRENCH_OUTER,
};
/// <summary>
/// ⭐ THE DELIVERABLE (chat2/06) — the organic layer, reshaped (chat2/05 stage 2's shape:
/// small / low / flat / crisp, corners on) and tuned for coverage: density up from 05's
/// 0.007, south-weighted, guarded against specks / clusters / blobs.
///
/// ⚠ TUNED BY MEASUREMENT, not by feel (chat2/06 batch — the count table over 12 seeds ×
/// 3 levels and the per-hemisphere diagnosis). This preset is the batch's `density_mid`.
/// </summary>
public static OffshoreSettings Organic() => new OffshoreSettings
{
Mode = OffshoreMode.Organic,
FreqPerMapWidth = 16f, // a little smaller than the reference's 14 (chat2/05)
Density = OrganicDensityMid,
SouthWeight = OrganicSouthWeight,
CrestM = 24f, // lower than the reference's 34 (pre-curve) — lands in the curve's preserved toe
CoreFraction = 0.25f, // flatter than 0.45
EdgeSharpness = 2.5f, // crisper shore than the faithful smoothstep
MinIslandAreaFrac = OrganicMinIslandAreaFrac,
MinSeparationFrac = OrganicMinSeparationFrac,
MaxIslandAreaFrac = OrganicMaxIslandAreaFrac,
TrenchInner = 0.90f, TrenchOuter = 0.97f, // corners + outer edge allowed; centre stays on-map
};
/// <summary>
/// The three density levels of the chat2/06 batch, in one place so the tool and the preset
/// cannot disagree. <c>Mid</c> is the preset of record.
/// </summary>
public const float OrganicDensityLow = 0.016f; // the edge: every seed of the 12 still clears N ≥ 2 / S ≥ 3, but N's minimum IS 2
public const float OrganicDensityMid = 0.022f; // ⭐ the preset: N min 3 / mean 6.3, S min ~15 / mean ~20 over 12 seeds at 4096
public const float OrganicDensityHigh = 0.030f; // the "how many is too many" bookend
public const float OrganicSouthWeight = 1.25f; // preference, not a fix — the diagnosis found the south UN-suppressed (see the report)
public const float OrganicMinIslandAreaFrac = 3e-5f;
public const float OrganicMinSeparationFrac = 0.008f;
public const float OrganicMaxIslandAreaFrac = 6e-4f;
public OffshoreSettings Clone() => (OffshoreSettings)MemberwiseClone();
public string Describe()
{
if (Mode == OffshoreMode.Off) return "offshore OFF";
var sb = new StringBuilder();
sb.Append($"{Mode}: freq {FreqPerMapWidth:F0}/map density {Density:F4} × south {SouthWeight:F2} (= S {DensitySouth:F4}) · ");
sb.Append($"crest {CrestM:F0} m core {CoreFraction:F2} sharp {EdgeSharpness:F1} · ");
sb.Append($"guards minArea {MinIslandAreaFrac:G2} minSep {MinSeparationFrac:G2} maxArea {MaxIslandAreaFrac:G2} · ");
sb.Append($"moat {MinDepthM:F0}+{DepthFeatherM:F0} m falloff {MinFalloff:F2}+{FalloffFeather:F2} trench {TrenchInner:F2}→{TrenchOuter:F2}");
return sb.ToString();
}
}
}