islaApocalypse-v2/Core/Scripts/CurveKnots.cs
beezm 35b4818e9e Phase 2a: the faithful redistribution curve, re-measured against v2's own output
Ports the reference's v5 height curve and shelf-detail passes onto Phase 1's shape
and re-calibrates them against this repo's actual pass-1 distribution. This is the
BASELINE the reshape gets judged against, not the reshape.

Core (engine-free, D-060):
- WorldScale — THE vertical yardstick. One metres/raw number (251), replacing the
  prototype's three duplicate M_PER_UNIT constants and ~20 bare literals. The
  chunk-height coupling it had there is recorded as a DEFERRED vault decision, not
  inherited. RawFromMetres divides, matching the reference bit-for-bit.
- HeightCurve — the 7 bands, the frozen corner-fix blends, the per-seed spike
  normalization, the 24-corner monotonicity sweep that throws and refuses.
  Identity at and below sea, which everything downstream rests on.
- CurveKnots / CurveAnchors — input knots (measured percentiles) and output anchors
  (storm ladder) split apart and both made parameters, so the anchors are A/B-able
  without editing source. The reference's shipped knots are kept beside the measured
  ones as the fidelity yardstick.
- TerrainDetailPass — micro-relief skin plus the shelf-edge KNOT warp (which slides
  K3/K4/K5, not height — that is what keeps monotonicity structural). The crater
  exclusion is ported and inert until the carve lands.

Tools:
- Shaping — pass 2a, producing the two height fields. classify is bit-for-bit the
  raw pass-1 field; render is curved and detailed. Aliased when the curve is off,
  as the reference did. Pass1Result is left immutable so the oracle can compare.
- LandHistogram — the calibration engine AND the diagnostic. The reference shipped
  six knot literals and threw the measuring instrument away; this rebuilds it.
- ShapingOracle + CurveBaselineTool — four automatic checks before anything is
  looked at, and the batch that runs them.

Measured, not assumed:
- Knots re-measured over a 6-seed / 12.8M-sample pool. They differ from the
  reference's by at most 5.6 m of world height, against a 44.7 m per-seed spread —
  the pass-1 port is faithful.
- Oracle all pass, including pass 1 bit-identical to Phase 1's own .f32 dump.
- Band shares land on 60/13/10/5/8/3/1 to 0.00 pp.
- Knots hold across map size: the 8K delta (5.8 m) sits inside seed noise.

The finding the histograms deliver: 83% of land ends below 100 m and 96% below
220 m, with the median column at 13 m. That is the share targets doing exactly what
they say, not a bug — and it is the developer's call, which is why nothing here
reshapes it and the palette was deliberately left mis-fitted rather than recalibrated
to disguise it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
2026-08-20 01:38:10 -04:00

120 lines
6.1 KiB
C#
Raw 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.

namespace IslaApocalypse.Core
{
/// <summary>
/// The six INPUT knots of the redistribution curve — thresholds on the RAW pre-curve height that
/// cut the land distribution into the curve's seven bands.
///
/// ═══ ⚠⚠ THESE ARE A CALIBRATION ARTEFACT, NOT A DESIGN CONSTANT ═══
///
/// They are literal floats in source, but they were never CHOSEN as numbers. Each is a PERCENTILE
/// of the measured land-height distribution, baked down to a literal:
///
/// K1..K6 = P60 / P73 / P83 / P88 / P96 / P99 of the land CDF
///
/// which is what makes the band SHARES — 60/13/10/5/8/3/1 % of land — exact by construction.
/// The shares are the design decision; the knots are whatever percentiles land on THIS
/// generator's distribution.
///
/// > ### ⚠ A KNOT SET IS ONLY VALID FOR THE DISTRIBUTION IT WAS MEASURED ON.
/// > Copying knots across a change to pass 1 silently reallocates the bands. That is why
/// > <see cref="Reference"/> is kept beside <see cref="V2Baseline"/> rather than replaced by it:
/// > the DELTA between them is the port-fidelity check.
///
/// Monotonicity does not depend on the values: the curve is monotonic for ANY strictly ordered
/// knot set, and the shelf-edge warp's bound keeps the set ordered by construction. So a
/// recalibration cannot break the curve — it can only move where the bands sit.
/// </summary>
public sealed class CurveKnots
{
/// <summary>Preset id, carried into the blueprint's curve metadata when that lands.</summary>
public readonly byte PresetId;
/// <summary>Short name, for logs and batch folders.</summary>
public readonly string Name;
/// <summary>The six input knots, strictly ascending. K1..K6.</summary>
public readonly float K1, K2, K3, K4, K5, K6;
public CurveKnots(byte id, string name, float k1, float k2, float k3, float k4, float k5, float k6)
{
PresetId = id; Name = name;
K1 = k1; K2 = k2; K3 = k3; K4 = k4; K5 = k5; K6 = k6;
}
/// <summary>
/// The quantiles the knots ARE, in percent. The band shares follow by differencing:
/// 60 / 13 / 10 / 5 / 8 / 3 / 1.
///
/// ⚠ THE SHARE TARGETS ARE THE M3 VALUES AND ARE HELD FIXED BY THIS PORT. Reallocating them
/// is the reshape, and the reshape is a later task.
/// </summary>
public static readonly double[] Percentiles = { 60.0, 73.0, 83.0, 88.0, 96.0, 99.0 };
/// <summary>The land-share target per output band, in percent, in band order.</summary>
public static readonly double[] BandShareTargets = { 60.0, 13.0, 10.0, 5.0, 8.0, 3.0, 1.0 };
/// <summary>Band names, in curve order. For tables and plot overlays.</summary>
public static readonly string[] BandNames =
{ "toe/orange", "red", "foothill riser", "bench", "mid riser", "plateau", "summit spike" };
/// <summary>
/// ⛔ THE REFERENCE'S SHIPPED KNOTS, kept verbatim as the fidelity yardstick — NOT for use.
///
/// Read from <c>REFERENCE:Tools/Scripts/HeightCurve.cs:57-58</c> at tag
/// <c>pre-rewrite-reference</c> (<c>ab78883</c>): preset BALANCED (id 2), the task-09 taste
/// gate's winner. Calibrated 2026-08-08 from the pooled batch-04 flat-sea land CDF,
/// 340,618,126 samples. COMPACT (id 1) retired with that verdict.
///
/// ⚠ The CDF that produced these does not exist in the reference repo — no sampler, no
/// histogram, no percentile helper survives at the tag. Only the six outputs were committed,
/// which is precisely why v2 had to rebuild the measuring instrument rather than copy them.
/// </summary>
public static readonly CurveKnots Reference = new CurveKnots(2, "reference_balanced",
0.515899f, 0.612157f, 0.710472f, 0.784045f, 0.962922f, 1.119118f);
/// <summary>
/// ⭐ THE FAITHFUL v2 BASELINE — the same percentiles, re-measured on v2's own pass-1 output.
///
/// Measured by chat2/01 (<c>Tools/Scenes/CurveBaselineTool.tscn</c>) over a 6-seed pool at
/// MapSize 2048 — seeds 1063685222, 20260819, 777001, 424242, 90210, 31337 —
/// <b>12,854,486 land samples</b>, fine-histogram quantiles at 1e-4 raw resolution with
/// in-bin linear interpolation. Land range [0.1500 .. 1.4146] raw, zero overflow.
///
/// ⚠ These are DELIBERATELY not the reference literals. The delta against
/// <see cref="Reference"/> is the port's fidelity evidence, and it is SMALL — the knots agree
/// to within +5.6 / 4.2 metres of world height across all six:
///
/// K1 P60 0.529113 (ref 0.515899, +3.32 m)
/// K2 P73 0.634439 (ref 0.612157, +5.59 m)
/// K3 P83 0.732487 (ref 0.710472, +5.53 m)
/// K4 P88 0.796957 (ref 0.784045, +3.24 m)
/// K5 P96 0.960301 (ref 0.962922, 0.66 m)
/// K6 P99 1.102473 (ref 1.119118, 4.18 m)
///
/// v2's land distribution is very slightly FATTER in the middle and SHORTER in the tail than
/// the reference's — consistent with a faithful pass-1 port measured on six seeds rather than
/// the reference's own pooled batch, not with a divergence. → `output/chat2/01_*.report.md`.
///
/// ⚠ Quoted to six decimals, which is float32's honest precision; the stored values differ
/// from the raw measurement by &lt;1e-7 raw (2.5e-5 m). The batch tool always re-measures for
/// its own run, so this constant is the default for OTHER callers, never the batch's input.
///
/// Re-measure by running <c>Tools/Scenes/CurveBaselineTool.tscn</c>; it prints this table.
/// </summary>
public static readonly CurveKnots V2Baseline = new CurveKnots(2, "v2_balanced",
0.529113f, 0.634439f, 0.732487f, 0.796957f, 0.960301f, 1.102473f);
/// <summary>Strictly ascending? The precondition every other guarantee rests on.</summary>
public bool IsStrictlyOrdered => K1 < K2 && K2 < K3 && K3 < K4 && K4 < K5 && K5 < K6;
/// <summary>Indexed access, K1..K6 as [0..5]. For tables and sweeps.</summary>
public float this[int i] => i switch
{
0 => K1, 1 => K2, 2 => K3, 3 => K4, 4 => K5, 5 => K6,
_ => throw new System.IndexOutOfRangeException($"A curve has six knots; asked for {i}.")
};
public override string ToString() =>
$"{Name}(K1={K1:F6} K2={K2:F6} K3={K3:F6} K4={K4:F6} K5={K5:F6} K6={K6:F6})";
}
}