Phase 2a: restore the mountain — calibrate the climb to the staircase, not to a guess

chat2/02 dissolved the terraces and lost two thirds of the mountain with them, then
concluded the loss was structural and needed a Phase-1 noise change. That conclusion
was wrong, and this commit is the refutation.

A monotone curve is a free reparametrization: it may be gentle in one place and steep
in another, and can lift bottom-heavy input into a substantial massif without ever
going flat. The area above a height is set by where the percentile->height mapping
crosses it, and that mapping is entirely ours to choose. The 02 sweep that "proved"
the loss structural varied climbFeather, which shapes the JOIN, not the mass
distribution — the wrong knob, and too strong a conclusion drawn from it.

Core/ClimbCalibration — the climb's control points are now MEASURED off the staircase
instead of invented from shape knobs. For p in {10,30,50,70,85,95} of above-ceiling
land, take that percentile's raw height and its staircase output height; PCHIP through
the pairs. That reproduces the staircase's elevation envelope, so the mountain mass
returns, while MinNormalizedSecant floors every grade so the flat bench and plateau
interiors become slope. The floor bites on exactly one segment — the plateau — which
is precisely where the staircase was flat.

ContinuousCurve.BuildCalibrated joins it to the same pinned lowland handover, the same
C1 join and the same per-seed spikeMax. The 02 analytic path survives unchanged as the
"before" contrast, and deliberately keeps its strictly-increasing-secant rule: a
calibrated curve is WAVY by design, so convexity is the wrong invariant for it and the
secant floor is the right one.

peakSharpness replaces summitDrama and fixes its bad trade. Drama steepened the peak by
pulling the summit ONSET down, dragging the whole massif with it (p99 199 -> 121 m).
Sharpness reshapes only above the last measured percentile, leaving that height fixed,
so peak and massif are independent: raising it leaves p90, >100 m and >220 m untouched
and only moves land within the summit.

Measured, both seeds, 2048:

  variant                >100 m   >220 m    p90
  staircase (target)     16.05%    4.53%  127.4 m
  continuous_02default    4.80%    0.63%   58.4 m
  continuous_restored    14.25%    3.45%  123.2 m
  continuous_bigger      19.70%    5.80%  163.5 m

Oracle all hard checks pass, including (a2) staircase still bit-identical to task 01's
dump and (d) lowlands bit-identical across every calibrated variant. New (g) reports
land above 100/220 m per variant and is deliberately NOT gated — it is a taste target
the developer tunes, and gating it would make mountainLift unusable. What it must never
do is stay silent, which is how 02 lost the mountain unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
This commit is contained in:
Stewart Howe 2026-08-20 04:14:47 -04:00
parent 8e55326a84
commit 639dc5f5a9
10 changed files with 1075 additions and 43 deletions

View file

@ -0,0 +1,300 @@
using System;
using System.Text;
namespace IslaApocalypse.Core
{
/// <summary>
/// ⭐⭐ THE CLIMB'S CONTROL POINTS, MEASURED FROM THE STAIRCASE (chat2/03) — "the staircase's
/// mountain with the terraces melted out".
///
/// ═══ THE MISTAKE THIS TYPE CORRECTS ═══
///
/// chat2/02 built the climb from ANALYTIC control points (a feather and a drama knob) and got a
/// bottom-heavy curve: land above 100 m fell from ~15 % to ~4.8 %. That report concluded the loss
/// was STRUCTURAL — that a no-magnet monotone curve must preserve the raw distribution's
/// bottom-heavy shape, so only a Phase-1 noise change could restore the mountain.
///
/// > ### ⚠ THAT CONCLUSION WAS WRONG, AND THIS TYPE IS THE PROOF.
/// >
/// > A monotone curve is a free reparametrization: it may be gentle in one place and steep in
/// > another, and can LIFT bottom-heavy input into a substantial mid-massif without ever going
/// > flat. **No-flats and lift-the-mass are compatible.** The area of land above a given height
/// > is set by where the percentile→height mapping CROSSES that height, and that mapping is
/// > entirely ours to choose.
/// >
/// > The 02 sweep that "proved" the loss structural varied <c>climbFeather</c>, which shapes the
/// > JOIN, not the mass distribution. It was the wrong knob, and the conclusion generalized from
/// > it was too strong.
///
/// ═══ WHAT THE STAIRCASE'S BENCHES ACTUALLY DID ═══
///
/// They did not CREATE highland. They LIFTED land to 100 m and 220 m. The same ~27 % of land
/// above the ceiling exists in both curves; 02's analytic climb simply placed it low. So the fix
/// is not to make more high land — it is to put the land that is already there back where the
/// staircase had it, as a smooth slope.
///
/// ═══ THE METHOD — the same percentile idea as task 01's knots, one level up ═══
///
/// Task 01 measured percentiles of the raw distribution to place the curve's INPUT knots. This
/// measures percentiles of the staircase's ABOVE-CEILING land to place the climb's OUTPUT
/// heights:
///
/// for each p in {10, 30, 50, 70, 85, 95}:
/// u_p = normalized RAW position of above-ceiling land at percentile p
/// v_p = normalized OUTPUT height of above-ceiling land at percentile p (staircase)
///
/// PCHIP through <c>(0,0), (u_p, v_p)…, (1,1)</c> reproduces the staircase's elevation envelope —
/// the same land ends up at the same heights, so the mountain mass returns — while the flat bench
/// and plateau INTERIORS become smooth grade.
///
/// ═══ ⚠ WHERE THE STAIRCASE WAS FLAT, WE MUST DEVIATE — AND THAT IS THE POINT ═══
///
/// A bench maps a wide input band onto a narrow output band, so two adjacent percentiles land at
/// nearly the same height and their secant is near zero. Reproducing THAT would rebuild the
/// bench. <see cref="MinNormalizedSecant"/> floors every segment's grade and renormalizes, so the
/// curve passes THROUGH the bench height with slope instead of running ALONG it. The floor bites
/// only where the staircase was flat; everywhere else the calibration is reproduced.
/// </summary>
public sealed class ClimbCalibration
{
/// <summary>
/// The above-ceiling land percentiles sampled. Six is a handful — enough to carry the
/// staircase's envelope, few enough that PCHIP interpolates smoothly between them rather
/// than tracing every wobble of the bench.
/// </summary>
public static readonly double[] DefaultPercentiles = { 10.0, 30.0, 50.0, 70.0, 85.0, 95.0 };
/// <summary>
/// The no-bench floor: no segment's grade may fall below this fraction of the climb's average
/// grade (1.0 = average). 0.35 is comfortably above <see cref="ContinuousCurve"/>'s own
/// near-flat tripwire and well below the grades the calibration produces outside the benches,
/// so it is a repair for the flats and a no-op everywhere else.
/// </summary>
public const float MinNormalizedSecant = 0.35f;
/// <summary>Iterations of floor-then-renormalize. It converges in a few; 24 is free insurance.</summary>
private const int RepairIterations = 24;
/// <summary>Normalized control points, strictly increasing in both. Includes (0,0) and (1,1).</summary>
public readonly float[] U, V;
/// <summary>The percentiles sampled, and the raw/output heights measured at each — for the report.</summary>
public readonly double[] Percentiles;
public readonly float[] RawAt, TargetHeightAt;
/// <summary>The knobs this calibration was shaped with.</summary>
public readonly float MountainLift, PeakSharpness;
/// <summary>
/// Normalized u of the summit onset — the LAST measured percentile. Above it,
/// <see cref="PeakSharpness"/> reshapes; below it, nothing does. That is the decoupling.
/// </summary>
public readonly float SummitOnsetU;
/// <summary>How many segments the no-bench floor had to lift. Zero means the staircase had no flats.</summary>
public readonly int SegmentsFloored;
private ClimbCalibration(float[] u, float[] v, double[] pcts, float[] rawAt, float[] targetAt,
float lift, float sharp, float onsetU, int floored)
{
U = u; V = v; Percentiles = pcts; RawAt = rawAt; TargetHeightAt = targetAt;
MountainLift = lift; PeakSharpness = sharp; SummitOnsetU = onsetU; SegmentsFloored = floored;
}
/// <summary>
/// Build the calibration from measured quantiles.
///
/// ⚠ Takes plain arrays, not a histogram: <c>LandHistogram</c> lives in <c>Tools/</c> and Core
/// depends on nothing above it. The caller measures; this shapes.
/// </summary>
/// <param name="percentiles">The percentiles sampled, ascending.</param>
/// <param name="rawQuantiles">Above-ceiling RAW height at each percentile.</param>
/// <param name="outQuantiles">Above-ceiling STAIRCASE OUTPUT height at each percentile.</param>
/// <param name="mountainLift">
/// 1.0 = reproduce the staircase's mountain. &gt;1 lifts the mid-massif higher; &lt;1 lowers it
/// toward chat2/02's bottom-heavy default. Applied as <c>v ← v^(1/lift)</c>, which is monotone
/// and fixes both endpoints, so it can move the massif without touching sea level or the cap.
/// </param>
/// <param name="peakSharpness">
/// ⭐ ACTS ONLY ABOVE THE LAST MEASURED PERCENTILE. 1.0 = a straight run to the cap; higher
/// defers the rise so the final approach steepens and the peak reads pointy.
/// ⚠ Unlike chat2/02's <c>summitDrama</c>, it CANNOT lower the massif — the onset's height is
/// fixed by the calibration before this is applied. That is the §3 fix.
/// </param>
public static ClimbCalibration FromPercentiles(
double[] percentiles, float[] rawQuantiles, float[] outQuantiles,
float ceilingRaw, float spikeMax, float ceilingOut, float peakCap,
float mountainLift, float peakSharpness)
{
int n = percentiles.Length;
if (rawQuantiles.Length != n || outQuantiles.Length != n)
throw new ArgumentException("[ClimbCalibration] percentile/raw/output arrays must be the same length.");
if (mountainLift <= 0f)
throw new ArgumentOutOfRangeException(nameof(mountainLift), mountainLift, "mountainLift must be positive.");
if (peakSharpness < 1f)
throw new ArgumentOutOfRangeException(nameof(peakSharpness), peakSharpness,
"peakSharpness < 1 would make the summit's final approach SHALLOWER than its own average — a ramp, not a peak.");
float spanRaw = spikeMax - ceilingRaw;
float spanOut = peakCap - ceilingOut;
if (spanRaw <= 0f || spanOut <= 0f)
throw new InvalidOperationException("[ClimbCalibration] the climb has no room — ceiling meets the summit.");
// ---- normalize the measured points, plus the two exact endpoints ----
var u = new float[n + 2];
var v = new float[n + 2];
u[0] = 0f; v[0] = 0f;
u[n + 1] = 1f; v[n + 1] = 1f;
for (int i = 0; i < n; i++)
{
u[i + 1] = Math.Clamp((rawQuantiles[i] - ceilingRaw) / spanRaw, 0f, 1f);
v[i + 1] = Math.Clamp((outQuantiles[i] - ceilingOut) / spanOut, 0f, 1f);
}
// ⚠ u must be STRICTLY increasing for PCHIP. Percentiles of a continuous distribution
// give that naturally; a degenerate seed (a plateau in the raw CDF) could not. Nudge
// rather than throw — a hair of u-spacing is not a shape decision.
const float minDu = 1e-4f;
for (int i = 1; i < u.Length; i++)
if (u[i] <= u[i - 1] + minDu) u[i] = u[i - 1] + minDu;
// Renormalize back onto [0,1] if the nudging pushed past the end.
if (u[u.Length - 1] > 1f)
{
float s = 1f / u[u.Length - 1];
for (int i = 1; i < u.Length; i++) u[i] *= s;
u[u.Length - 1] = 1f;
}
// ---- mountainLift: v ← v^(1/lift). Monotone, endpoints fixed. ----
if (Math.Abs(mountainLift - 1f) > 1e-6f)
{
float e = 1f / mountainLift;
for (int i = 1; i <= n; i++) v[i] = MathF.Pow(v[i], e);
}
// ---- the no-bench repair: floor every grade, renormalize to keep v(1) = 1 ----
float onsetU = u[n]; // the last measured percentile
int floored = RepairSecants(u, v, out _);
// ---- peakSharpness: reshape ONLY the segment above the onset ----
// Insert a midpoint whose height defers the rise, so the final approach steepens.
// v_mid = v_onset + (1 - v_onset) * 0.5^sharpness ⇒ sharpness 1 is exactly linear.
if (peakSharpness > 1f + 1e-6f)
{
float uS = u[n], vS = v[n];
float uMid = (uS + 1f) * 0.5f;
float vMid = vS + (1f - vS) * MathF.Pow(0.5f, peakSharpness);
var u2 = new float[u.Length + 1];
var v2 = new float[v.Length + 1];
Array.Copy(u, u2, n + 1); Array.Copy(v, v2, n + 1);
u2[n + 1] = uMid; v2[n + 1] = vMid;
u2[n + 2] = 1f; v2[n + 2] = 1f;
u = u2; v = v2;
// ⚠ The deferred first half must still not be a bench. Re-floor ONLY that segment,
// leaving the calibrated massif below the onset untouched — re-running the global
// repair here would renormalize the massif and undo the decoupling.
float du = uMid - uS;
float minDv = MinNormalizedSecant * du;
if (vMid - vS < minDv) v[n + 1] = vS + minDv;
}
var rawAt = (float[])rawQuantiles.Clone();
var outAt = (float[])outQuantiles.Clone();
var cal = new ClimbCalibration(u, v, (double[])percentiles.Clone(), rawAt, outAt,
mountainLift, peakSharpness, onsetU, floored);
cal.AssertUsable();
return cal;
}
/// <summary>
/// Floor every segment's normalized grade at <see cref="MinNormalizedSecant"/> and renormalize
/// so the last point still lands exactly on 1. Iterated, because renormalizing can push a
/// floored segment back under the floor; it converges as long as the un-floored segments have
/// room to absorb the excess.
/// </summary>
private static int RepairSecants(float[] u, float[] v, out float minSecant)
{
int m = u.Length;
var s = new float[m - 1];
var du = new float[m - 1];
for (int i = 0; i < m - 1; i++)
{
du[i] = u[i + 1] - u[i];
s[i] = (v[i + 1] - v[i]) / du[i];
}
int flooredCount = 0;
for (int it = 0; it < RepairIterations; it++)
{
int hit = 0;
for (int i = 0; i < s.Length; i++)
if (s[i] < MinNormalizedSecant) { s[i] = MinNormalizedSecant; hit++; }
flooredCount = hit;
float total = 0f;
for (int i = 0; i < s.Length; i++) total += s[i] * du[i];
if (Math.Abs(total - 1f) < 1e-6f) break;
for (int i = 0; i < s.Length; i++) s[i] /= total;
}
// Rebuild v from the repaired grades.
minSecant = float.MaxValue;
v[0] = 0f;
for (int i = 0; i < s.Length; i++)
{
if (s[i] < minSecant) minSecant = s[i];
v[i + 1] = v[i] + s[i] * du[i];
}
v[m - 1] = 1f; // exact, against accumulated float drift
return flooredCount;
}
/// <summary>
/// The invariants a calibration must satisfy before it is allowed to shape terrain. Throws
/// and refuses, rather than producing a curve nobody checked.
/// </summary>
private void AssertUsable()
{
for (int i = 1; i < U.Length; i++)
{
if (U[i] <= U[i - 1])
throw new InvalidOperationException(
$"[ClimbCalibration] control point {i} is not strictly right of its predecessor " +
$"(u {U[i - 1]} → {U[i]}). Refusing to generate.");
if (V[i] <= V[i - 1])
throw new InvalidOperationException(
$"[ClimbCalibration] control point {i} does not RISE (v {V[i - 1]} → {V[i]}) — that is a " +
$"bench, which is the artifact this mode exists to remove. Refusing to generate.");
}
if (Math.Abs(U[0]) > 1e-6f || Math.Abs(V[0]) > 1e-6f
|| Math.Abs(U[U.Length - 1] - 1f) > 1e-6f || Math.Abs(V[V.Length - 1] - 1f) > 1e-6f)
throw new InvalidOperationException(
"[ClimbCalibration] the endpoints must be exactly (0,0) and (1,1) — the lowland handover and " +
"the peak cap are not negotiable. Refusing to generate.");
}
/// <summary>The calibration as one line for the INDEX, the log and the report.</summary>
public string Describe()
{
var sb = new StringBuilder();
sb.Append($"lift {MountainLift:F2} sharp {PeakSharpness:F2} onsetU {SummitOnsetU:F3} " +
$"floored {SegmentsFloored} · uv ");
for (int i = 0; i < U.Length; i++) sb.Append($"({U[i]:F3},{V[i]:F3}) ");
return sb.ToString().TrimEnd();
}
/// <summary>The measured percentile table, for the report.</summary>
public string DescribeMeasured(float ceilingOut, float peakCap)
{
var sb = new StringBuilder();
for (int i = 0; i < Percentiles.Length; i++)
sb.Append($"P{Percentiles[i]:F0}→{WorldScale.MetresFromRaw(TargetHeightAt[i] - 0.15f):F0}m ");
return sb.ToString().TrimEnd();
}
}
}

View file

@ -0,0 +1 @@
uid://cl0hijnaw76jq

View file

@ -109,18 +109,106 @@ namespace IslaApocalypse.Core
/// <summary>The knob values this spline was built from, for the INDEX and the report.</summary>
public readonly float LowlandCeilingM, ClimbFeather, SummitDrama;
/// <summary>
/// ⭐ The measured calibration this climb was shaped from (chat2/03), or null when the climb
/// came from chat2/02's ANALYTIC feather/drama points.
///
/// Non-null is the current default: "the staircase's mountain with the terraces melted out".
/// Null survives so the 02 curve stays reproducible as a contrast variant — it is the "before"
/// in the three-way histogram story, not a fallback.
/// </summary>
public readonly ClimbCalibration Calibration;
/// <summary>Where the summit begins, normalized — the calibration's onset when calibrated, else the constant.</summary>
public float EffectiveSummitOnset => Calibration?.SummitOnsetU ?? SummitOnset;
// Control points (raw x, out y) and the FritschCarlson tangents. x strictly increasing.
private readonly float[] _x, _y, _m;
private ContinuousCurve(CurveKnots k, CurveAnchors a, float ceilingRaw, float ceilingOut,
float spikeMax, float joinSlopeRaw, float lowlandCeilingM, float climbFeather,
float summitDrama, float[] x, float[] y, float[] m)
float summitDrama, float[] x, float[] y, float[] m, ClimbCalibration calibration = null)
{
Knots = k; Anchors = a;
CeilingRaw = ceilingRaw; CeilingOut = ceilingOut; SpikeMax = spikeMax;
JoinSlopeRaw = joinSlopeRaw;
LowlandCeilingM = lowlandCeilingM; ClimbFeather = climbFeather; SummitDrama = summitDrama;
_x = x; _y = y; _m = m;
_x = x; _y = y; _m = m; Calibration = calibration;
}
/// <summary>
/// ⭐⭐ THE CALIBRATED CLIMB (chat2/03) — control points MEASURED from the staircase's
/// above-ceiling elevation distribution rather than invented from two shape knobs.
/// → <see cref="ClimbCalibration"/> for the method and for the chat2/02 mistake it corrects.
///
/// Everything outside the climb is identical to <see cref="Build"/>: the same lowland
/// handover pinned to the exact anchors, the same C¹ join to the red band's exit slope, the
/// same per-seed <paramref name="spikeMax"/>, the same tail. Only the interior shape changes.
///
/// ⚠ THIS PATH DOES NOT REQUIRE STRICTLY-INCREASING SECANTS, and that is deliberate. The 02
/// analytic path enforced a convex control polygon as its no-magnet rule. A curve calibrated
/// to real terrain is WAVY — gentler where the staircase had a bench, steeper through its
/// risers — so convexity is the wrong invariant here. The no-magnet guarantee instead comes
/// from <see cref="ClimbCalibration.MinNormalizedSecant"/>, which floors every grade: the
/// curve may slow down, but never to a bench.
/// </summary>
public static ContinuousCurve BuildCalibrated(CurveKnots k, CurveAnchors a, float spikeMax,
float lowlandCeilingM, ClimbCalibration calibration)
{
if (calibration == null) throw new ArgumentNullException(nameof(calibration));
var (ceilingRaw, ceilingOut, redSlope) = ResolveHandover(k, a, lowlandCeilingM);
if (ceilingRaw >= spikeMax - 1e-3f)
throw new InvalidOperationException(
$"[ContinuousCurve] lowland ceiling (raw {ceilingRaw:F4}) reaches this seed's summit " +
$"(spikeMax {spikeMax:F4}) — no room for a climb. Refusing.");
float spanRaw = spikeMax - ceilingRaw;
float spanOut = a.PeakCap - ceilingOut;
int n = calibration.U.Length;
var x = new float[n];
var y = new float[n];
for (int i = 0; i < n; i++)
{
x[i] = ceilingRaw + calibration.U[i] * spanRaw;
y[i] = ceilingOut + calibration.V[i] * spanOut;
}
float[] m = FritschCarlsonTangents(x, y, startTangent: redSlope);
return new ContinuousCurve(k, a, ceilingRaw, ceilingOut, spikeMax, redSlope,
lowlandCeilingM, climbFeather: float.NaN, summitDrama: float.NaN, x, y, m, calibration);
}
/// <summary>
/// Where the preserved lowland hands over to the climb, and the red band's exit slope.
///
/// ⚠ THE FLOOD LINE IS PINNED TO THE EXACT ANCHORS, and "30 m" is NOMINAL: RED_CEIL SEA is
/// 0.12 raw = 30.12 m. Any requested ceiling at or below the red ceiling hands over at
/// EXACTLY <c>(K2, RED_CEIL)</c> — no derived floats — so the linear extension is empty by
/// construction and the preserved toe+red band can never be cut by a rounding. (chat2/02's
/// first run refused its own default over that 0.12 m gap; pinning is the fix, not a wider
/// tolerance.)
/// </summary>
private static (float ceilingRaw, float ceilingOut, float redSlope) ResolveHandover(
CurveKnots k, CurveAnchors a, float lowlandCeilingM)
{
float redSlope = (a.RedCeil - a.OrangeCeil) / (k.K2 - k.K1);
if (lowlandCeilingM > MaxLowlandCeilingM)
throw new InvalidOperationException(
$"[ContinuousCurve] lowlandCeiling {lowlandCeilingM:F1} m is above the {MaxLowlandCeilingM:F0} m " +
"bound — close enough to the old bench (100±12 m) to preserve a flat one, which is the " +
"artifact this mode exists to remove. Refusing.");
float redCeilM = WorldScale.MetresFromRaw(a.RedCeil - a.Sea);
if (lowlandCeilingM <= redCeilM + 0.01f)
return (k.K2, a.RedCeil, redSlope);
float ceilingOut = a.Sea + WorldScale.RawFromMetres(lowlandCeilingM);
return (k.K2 + (ceilingOut - a.RedCeil) / redSlope, ceilingOut, redSlope);
}
/// <summary>
@ -134,35 +222,8 @@ namespace IslaApocalypse.Core
public static ContinuousCurve Build(CurveKnots k, CurveAnchors a, float spikeMax,
float lowlandCeilingM, float climbFeather, float summitDrama)
{
// ---- the preserved lowland's edge ----
float redSlope = (a.RedCeil - a.OrangeCeil) / (k.K2 - k.K1);
if (lowlandCeilingM > MaxLowlandCeilingM)
throw new InvalidOperationException(
$"[ContinuousCurve] lowlandCeiling {lowlandCeilingM:F1} m is above the {MaxLowlandCeilingM:F0} m " +
"bound — close enough to the old bench (100±12 m) to preserve a flat one, which is the " +
"artifact this mode exists to remove. Refusing.");
// ⚠ THE FLOOD LINE IS THE FLOOR, and "30 m" is NOMINAL: RED_CEIL SEA = 0.12 raw is
// actually 30.12 m through the yardstick. Any requested ceiling at or below the red
// ceiling means "hand over exactly where the preserved lowland ends", and that handover
// is pinned to THE EXACT ANCHORS — (K2, RED_CEIL), no derived floats — so the extension
// region is empty by construction and the toe+red band can never be cut. (The first
// probe run refused its own default over this 0.12 m nominal gap; pinning is the fix,
// not widening a tolerance.)
float redCeilM = WorldScale.MetresFromRaw(a.RedCeil - a.Sea);
float ceilingOut, ceilingRaw;
if (lowlandCeilingM <= redCeilM + 0.01f)
{
ceilingOut = a.RedCeil;
ceilingRaw = k.K2;
}
else
{
ceilingOut = a.Sea + WorldScale.RawFromMetres(lowlandCeilingM);
// Where the linear red-slope extension reaches that output.
ceilingRaw = k.K2 + (ceilingOut - a.RedCeil) / redSlope;
}
// ---- the preserved lowland's edge — shared with BuildCalibrated ----
var (ceilingRaw, ceilingOut, redSlope) = ResolveHandover(k, a, lowlandCeilingM);
if (ceilingRaw >= spikeMax - 1e-3f)
throw new InvalidOperationException(
@ -360,7 +421,7 @@ namespace IslaApocalypse.Core
if (v <= prev)
throw new InvalidOperationException(
$"[ContinuousCurve] MONOTONICITY VIOLATION at h={h}: {v} <= {prev} " +
$"(ceiling {LowlandCeilingM:F0} m, feather {ClimbFeather:F2}, drama {SummitDrama:F2}). Refusing to generate.");
$"(ceiling {LowlandCeilingM:F0} m, {KnobSummary()}). Refusing to generate.");
prev = v;
prevH = h;
}
@ -380,7 +441,7 @@ namespace IslaApocalypse.Core
float spanRaw = SpikeMax - CeilingRaw;
float spanOut = Anchors.PeakCap - CeilingOut;
float toN = spanRaw / spanOut; // raw slope → normalized
float onsetRaw = CeilingRaw + SummitOnset * spanRaw;
float onsetRaw = CeilingRaw + EffectiveSummitOnset * spanRaw;
float s0N = JoinSlopeRaw * toN;
float minN = float.MaxValue, maxN = float.MinValue, minAt = 0f, maxAt = 0f;
@ -400,15 +461,24 @@ namespace IslaApocalypse.Core
/// <summary>Raw height where the summit onset sits, and its output — for histogram overlays.</summary>
public (float raw, float outp) SummitOnsetPoint()
{
float r = CeilingRaw + SummitOnset * (SpikeMax - CeilingRaw);
float r = CeilingRaw + EffectiveSummitOnset * (SpikeMax - CeilingRaw);
return (r, Apply(r));
}
/// <summary>
/// The shaping knobs, named for whichever path built this curve — chat2/02's analytic
/// feather/drama or chat2/03's measured lift/sharpness. ⚠ The analytic fields are NaN on a
/// calibrated curve, so nothing may print them unconditionally.
/// </summary>
public string KnobSummary() => Calibration != null
? $"lift {Calibration.MountainLift:F2} sharp {Calibration.PeakSharpness:F2} (calibrated)"
: $"feather {ClimbFeather:F2} drama {SummitDrama:F2} (analytic 02)";
/// <summary>The control points as one line for the INDEX and the report.</summary>
public string DescribeControlPoints()
{
var sb = new StringBuilder();
sb.Append($"ceiling {LowlandCeilingM:F0}m feather {ClimbFeather:F2} drama {SummitDrama:F2} · points ");
sb.Append($"ceiling {LowlandCeilingM:F0}m {KnobSummary()} · points ");
for (int i = 0; i < _x.Length; i++)
sb.Append($"({_x[i]:F4},{_y[i]:F4}{(i == 0 ? " C1" : "")}) ");
sb.Append($"· join slope {JoinSlopeRaw:F4} raw");

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://cmtnrestore03isla"]
[ext_resource type="Script" path="res://Tools/Scripts/MountainRestoreTool.cs" id="1_mrt"]
[node name="MountainRestoreTool" type="Node"]
script = ExtResource("1_mrt")

View file

@ -134,6 +134,42 @@ namespace IslaApocalypse.Tools
FieldsPooled++;
}
/// <summary>
/// Pool one field's samples in, but only where a SECOND field clears a threshold — "the
/// output heights of the cells whose raw height is above the climb's ceiling".
///
/// ⚠ The gate is a different field from the values. That is the whole point: chat2/03
/// calibrates the climb against the staircase's OUTPUT distribution restricted to
/// ABOVE-CEILING land, and "above the ceiling" is a fact about the RAW height. Gating on the
/// values themselves would select a different population — output above the ceiling includes
/// nothing extra here, but only because the curve is monotone, and relying on that silently
/// would break the moment a caller gated a non-monotone pair.
///
/// The sea test still applies to the VALUES, so this stays a land histogram.
/// </summary>
public void AccumulateWhere(float[,] field, float[,] gate, int mapSize, float gateAbove)
{
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
if (gate[x, y] <= gateAbove) continue;
float h = field[x, y];
if (h <= SeaLevel) continue;
TotalLand++;
if (h < MinLand) MinLand = h;
if (h > MaxLand) MaxLand = h;
int bin = (int)((h - SeaLevel) / BinWidth);
if (bin >= _counts.Length) OverflowCount++;
else _counts[bin]++;
}
}
FieldsPooled++;
}
/// <summary>
/// The quantile at <paramref name="percent"/> (0..100) — a raw height, interpolated inside
/// its bin so the answer is not quantized to <see cref="BinWidth"/>.

View file

@ -0,0 +1,524 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐ THE MOUNTAIN-RESTORE BATCH (chat2/03) — put the mountain back, as a smooth slope.
///
/// ═══ THE STORY THIS BATCH TELLS, IN THREE HISTOGRAMS ═══
///
/// staircase the mass is there, but parked in two spikes (bench 100 m, plateau 220 m)
/// continuous_02 the spikes are gone — and so is the mass. It fell to 3090 m.
/// continuous_restored ⭐ the same mass as the staircase, spread as one smooth grade.
///
/// That contrast is the point, so the three are named to sort adjacent.
///
/// ═══ HOW THE RESTORATION IS MEASURED ═══
///
/// The climb's control points are no longer invented from shape knobs. They are MEASURED off the
/// staircase itself, on the same 6-seed pool tasks 01/02 use:
///
/// for p in {10,30,50,70,85,95} of ABOVE-CEILING land:
/// u_p ← that percentile of the RAW height (normalized into the climb's span)
/// v_p ← that percentile of the OUTPUT height (staircase, normalized)
///
/// PCHIP through those points reproduces the staircase's elevation envelope; the flat bench and
/// plateau interiors become grade because <c>ClimbCalibration.MinNormalizedSecant</c> floors
/// every segment. → <see cref="ClimbCalibration"/>.
///
/// ⚠ The two quantile sets are paired by percentile across the SAME cell population, which is
/// exact only if the staircase were strictly monotone per column. It is monotone in raw, but the
/// per-column bench/plateau modulation (±12 / ±20 m) blurs the pairing by about that much. That
/// is well inside the envelope being targeted, and calibrating on the MEASURED output (rather
/// than a nominal unmodulated curve) is what makes oracle (g)'s land-above-100 m figure the thing
/// actually being aimed at.
///
/// ═══ RUNNING IT ═══
///
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/MountainRestoreTool.tscn
///
/// ISLA_TASK / ISLA_BATCH / ISLA_MAPSIZE / ISLA_SEEDS / ISLA_SHOWPIECE_SIZE / ISLA_SHOWPIECE
/// ISLA_PHASE1_SOURCE (default "02_pass1_port") · ISLA_T01_SOURCE (default "01_curve_baseline")
/// ISLA_SKIP_RAW
/// ISLA_LIFT_BIG probe: the `continuous_bigger` lift (default 1.35)
/// ISLA_SHARP probe: the `continuous_sharper_peak` knob (default 2.5)
/// </summary>
public partial class MountainRestoreTool : Node
{
private static readonly int[] DefaultSeeds = { 1063685222, 777001 };
/// <summary>⚠ Task 01's pool, verbatim — the knots' identity, and with it the staircase control's.</summary>
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
private const int DefaultMapSize = 2048;
private const int DefaultShowpieceSize = 8192;
/// <summary>Oracle (g)'s PASS/NOTE threshold, percentage points of land above 100 m. Reported either way.</summary>
private const double MountainTolerancePp = 2.0;
public override void _Ready()
{
try { Run(); }
catch (Exception e)
{
GD.PrintErr("==================================================================");
GD.PrintErr($" REFUSED: {e.Message}");
GD.PrintErr("==================================================================");
GetTree().Quit(2);
}
}
private void Run()
{
ToolingPaths.Configure(OS.GetUserDataDir());
int task = EnvInt("ISLA_TASK", 3);
string descr = EnvStr("ISLA_BATCH", "mountain_restore");
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
int showSize = EnvInt("ISLA_SHOWPIECE_SIZE", DefaultShowpieceSize);
bool showpiece = EnvStr("ISLA_SHOWPIECE", "1") == "1";
string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
string t01Source = EnvStr("ISLA_T01_SOURCE", "01_curve_baseline");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
float liftBig = EnvFloat("ISLA_LIFT_BIG", 1.35f);
float sharpKnob = EnvFloat("ISLA_SHARP", 2.5f);
string batchRoot = ToolingPaths.BatchRoot(task, descr);
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
var anchors = CurveAnchors.Default;
float sea = 0.15f;
int primary = seeds[0];
GD.Print("==================================================================");
GD.Print(" MOUNTAIN RESTORE (chat2/03) — the staircase's mountain,");
GD.Print(" de-terraced. Calibrated, not invented.");
GD.Print("==================================================================");
GD.Print($"MapSize : {mapSize} showpiece {(showpiece ? showSize.ToString() : "off")}");
GD.Print($"yardstick : {WorldScale.Describe()}");
GD.Print($"seeds : {string.Join(", ", seeds)} (calibration pool: {string.Join(", ", CalibrationSeeds)})");
GD.Print($"batch : {batchRoot}");
GD.Print("==================================================================");
// ═══ 0. KNOTS — task 01's pool, re-measured for bit-identity ═══
GD.Print("\n--- 0. KNOTS ---");
var rawPool = new LandHistogram(sea);
var pass1 = new Dictionary<int, Pass1Result>();
foreach (int seed in CalibrationSeeds)
{
var p1 = Topography.Generate(new TerrainGenConfig { MapSize = mapSize, Seed = seed });
pass1[seed] = p1;
rawPool.Accumulate(p1.Height, mapSize);
}
var knots = new CurveKnots(2, "v2_balanced",
rawPool.Quantile(CurveKnots.Percentiles[0]), rawPool.Quantile(CurveKnots.Percentiles[1]),
rawPool.Quantile(CurveKnots.Percentiles[2]), rawPool.Quantile(CurveKnots.Percentiles[3]),
rawPool.Quantile(CurveKnots.Percentiles[4]), rawPool.Quantile(CurveKnots.Percentiles[5]));
GD.Print($" {rawPool}");
GD.Print($" {knots}");
// ═══ 1. CALIBRATE — measure the staircase's above-ceiling elevation distribution ═══
//
// ⚠ The ceiling is the DEFAULT 30 m handover, which is exactly (K2, RED_CEIL). So
// "above-ceiling" is simply "raw > K2" — no derived float, and the same population the
// climb will later be responsible for.
GD.Print("\n--- 1. CALIBRATION (staircase above-ceiling distribution) ---");
float ceilingRaw = knots.K2;
var rawAbove = new LandHistogram(sea);
var outAbove = new LandHistogram(sea);
var stairPool = new Dictionary<int, Pass2Result>();
foreach (int seed in CalibrationSeeds)
{
var scfg = MakeConfig(mapSize, seed, knots, anchors, "staircase");
scfg.CurveMode = CurveModeKind.Staircase;
scfg.ShelfDetail = true;
Pass2Result st = Shaping.Shape(pass1[seed], scfg);
stairPool[seed] = st;
rawAbove.AccumulateWhere(pass1[seed].Height, pass1[seed].Height, mapSize, ceilingRaw);
outAbove.AccumulateWhere(st.Height, pass1[seed].Height, mapSize, ceilingRaw);
}
double shareAbove = 100.0 * rawAbove.TotalLand / rawPool.TotalLand;
GD.Print($" above-ceiling land: {rawAbove.TotalLand:N0} cells = {shareAbove:F1}% of all land");
var pcts = ClimbCalibration.DefaultPercentiles;
var rawQ = new float[pcts.Length];
var outQ = new float[pcts.Length];
GD.Print(" percentile → raw → staircase output");
for (int i = 0; i < pcts.Length; i++)
{
rawQ[i] = rawAbove.Quantile(pcts[i]);
outQ[i] = outAbove.Quantile(pcts[i]);
GD.Print($" P{pcts[i],-4:F0} raw {rawQ[i]:F4} → {WorldScale.MetresFromRaw(outQ[i] - sea),6:F1} m");
}
ClimbCalibration Calib(float lift, float sharp) => ClimbCalibration.FromPercentiles(
pcts, rawQ, outQ, ceilingRaw, HeightCurve.EffectiveSpikeMax(pass1[primary].HMaxSeed, knots, anchors),
anchors.RedCeil, anchors.PeakCap, lift, sharp);
// ⚠ ONE calibration object per knob pair, shared across seeds. spikeMax differs slightly
// per seed, but the calibration is NORMALIZED (u, v in [0,1]) — BuildCalibrated
// denormalizes against each seed's own spikeMax. So the shape is shared; the extent is
// per-seed, exactly as the per-seed peak normalization requires.
var calRestored = Calib(1.0f, 1.0f);
var calBigger = Calib(liftBig, 1.0f);
var calSharper = Calib(1.0f, sharpKnob);
GD.Print($" restored: {calRestored.Describe()}");
GD.Print($" bigger : {calBigger.Describe()}");
GD.Print($" sharper : {calSharper.Describe()}");
// ═══ 2. VARIANTS ═══
var variants = new List<(string label, Action<TerrainGenConfig> mutate)>
{
("staircase", c => { c.CurveMode = CurveModeKind.Staircase; c.ShelfDetail = true; }),
("continuous_02default", c => { c.CurveMode = CurveModeKind.Continuous;
c.ClimbCalibration = null; // the analytic 02 curve
c.ClimbFeather = 0.4f; c.SummitDrama = 2.5f; }),
("continuous_restored", c => { c.CurveMode = CurveModeKind.Continuous; c.ClimbCalibration = calRestored; }),
("continuous_bigger", c => { c.CurveMode = CurveModeKind.Continuous; c.ClimbCalibration = calBigger; }),
("continuous_sharper_peak", c => { c.CurveMode = CurveModeKind.Continuous; c.ClimbCalibration = calSharper; }),
};
GD.Print("\n--- 2. VARIANTS ---");
var results = new Dictionary<(int, string), Pass2Result>();
var offs = new Dictionary<int, Pass2Result>();
var rows = new List<string>();
bool notesPrinted = false;
foreach (int seed in seeds)
{
var offCfg = MakeConfig(mapSize, seed, knots, anchors, "curve_off");
offCfg.Curve = false;
offs[seed] = Shaping.Shape(pass1[seed], offCfg);
foreach (var (label, mutate) in variants)
{
var cfg = MakeConfig(mapSize, seed, knots, anchors, label);
mutate(cfg);
Pass2Result p2 = Shaping.Shape(pass1[seed], cfg);
results[(seed, label)] = p2;
if (!notesPrinted) foreach (string nt in p2.Notes) GD.Print(" " + nt);
rows.Add(WriteVariant(batchRoot, p2, sea, anchors, skipRaw));
}
notesPrinted = true;
}
// ═══ 3. ORACLE ═══
GD.Print("\n--- 3. ORACLE ---");
var hard = new List<ShapingOracle.Check>();
var soft = new List<ShapingOracle.Check>();
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32");
string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump",
offs[primary].Height, HeightField.Load(p1Dump, mapSize), mapSize, p1Dump));
hard.Add(ShapingOracle.DumpRegression("a2", "staircase == task-01 curve_on .f32 dump",
results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump));
long bFail = 0;
foreach (int seed in seeds)
foreach (var (label, _) in variants)
if (!ShapingOracle.ClassifyFidelity(pass1[seed], results[(seed, label)]).Passed) bFail++;
hard.Add(new ShapingOracle.Check
{
Id = "b", Name = "classify == raw, all seeds × all variants",
Passed = bFail == 0,
Detail = bFail == 0 ? $"bit-identical on {seeds.Length} seeds × {variants.Count} variants"
: $"{bFail} (seed, variant) pairs drifted",
});
bool cOk = results[(primary, "continuous_restored")].Notes
.Exists(n => n.Contains("strict-increase sample passed"));
hard.Add(new ShapingOracle.Check
{
Id = "c", Name = "monotone — FritschCarlson + per-seed sampled",
Passed = cOk,
Detail = cOk ? "confirmed on the calibrated climb (throws and refuses on violation)"
: "no strict-increase confirmation recorded",
});
string[] continuous = { "continuous_02default", "continuous_restored", "continuous_bigger", "continuous_sharper_peak" };
foreach (int seed in seeds)
foreach (string label in continuous)
{
var d = ShapingOracle.LowlandsPreserved(pass1[seed], results[(seed, "staircase")], results[(seed, label)]);
d.Name += $" [seed {seed}]";
hard.Add(d);
var e = ShapingOracle.UpperClimbProfile(results[(seed, label)]);
e.Name += $" [seed {seed}]";
soft.Add(e);
}
foreach (int seed in seeds)
foreach (var (label, _) in variants)
{
var f = ShapingOracle.SeaIdentity(offs[seed], results[(seed, label)], sea);
f.Name += $" [seed {seed}]";
hard.Add(f);
}
// (g) the restoration, measured — reported for every variant, gated for none.
var mountain = new List<ShapingOracle.Check>();
foreach (int seed in seeds)
foreach (string label in continuous)
{
var g = ShapingOracle.MountainRestored(results[(seed, label)], results[(seed, "staircase")],
sea, MountainTolerancePp);
g.Name += $" [seed {seed}]";
mountain.Add(g);
}
foreach (var c in hard) GD.Print(" " + c);
foreach (var c in soft) { if (c.Passed) GD.Print(" " + c); else GD.PrintErr(" ⚠ SLOPE: " + c); }
GD.Print(" --- (g) mountain, reported not gated ---");
foreach (var c in mountain) GD.Print(" " + c);
bool hardOk = hard.TrueForAll(c => c.Passed);
GD.Print($" ORACLE: {(hardOk ? "ALL HARD CHECKS PASS" : "*** HARD FAILURES ***")}");
// ═══ 4. HISTOGRAMS — the three-way contrast, adjacent by filename ═══
GD.Print("\n--- 4. HISTOGRAMS ---");
foreach (int seed in seeds)
{
var rawSeed = new LandHistogram(sea);
rawSeed.Accumulate(pass1[seed].Height, mapSize);
int order = 1;
foreach (var (label, _) in variants)
DrawShaped(results[(seed, label)], anchors, seed, mapSize, batchRoot, order++, sea);
}
// ═══ 5. SHOWPIECE ═══
string showNote = "skipped (ISLA_SHOWPIECE=0)";
if (showpiece)
{
GD.Print($"\n--- 5. SHOWPIECE at {showSize} (continuous_restored, seed {primary}) ---");
var cfg = MakeConfig(showSize, primary, knots, anchors, "continuous_restored_showpiece");
cfg.CurveMode = CurveModeKind.Continuous;
cfg.ClimbCalibration = calRestored;
Pass1Result p1 = Topography.Generate(cfg);
Pass2Result big = Shaping.Shape(p1, cfg);
foreach (string nt in big.Notes) GD.Print(" " + nt);
var cb = ShapingOracle.ClassifyFidelity(p1, big);
var offBigCfg = MakeConfig(showSize, primary, knots, anchors, "off"); offBigCfg.Curve = false;
var cf = ShapingOracle.SeaIdentity(Shaping.Shape(p1, offBigCfg), big, sea);
GD.Print($" {cb}");
GD.Print($" {cf}");
if (!cb.Passed || !cf.Passed) hardOk = false;
var (b100, b220) = ShapingOracle.LandAbove(big, sea);
GD.Print($" land >100 m {b100:F2}% >220 m {b220:F2}% (at {showSize})");
rows.Add(WriteVariant(batchRoot, big, sea, anchors, skipRaw));
showNote = $"seed {primary} at {showSize}; >100 m {b100:F2}%, >220 m {b220:F2}%";
}
WriteIndex(batchRoot, mapSize, showSize, seeds, primary, anchors, results, calRestored,
calBigger, calSharper, pcts, rawQ, outQ, hard, soft, mountain, rows, hardOk, showNote, sea);
GD.Print("\n==================================================================");
GD.Print($" DONE — {batchRoot}");
GD.Print($" ORACLE {(hardOk ? "HARD CHECKS ALL PASS" : "*** HARD FAILURES ***")}");
GD.Print("==================================================================");
GetTree().Quit(hardOk ? 0 : 3);
}
private static TerrainGenConfig MakeConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label)
=> new TerrainGenConfig
{
MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = true, ShelfDetail = false, Knots = k, Anchors = a,
LowlandCeilingM = 30f,
};
// ---- output ---------------------------------------------------------
private static string WriteVariant(string batchRoot, Pass2Result p2, float sea,
CurveAnchors anchors, bool skipRaw)
{
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}");
DirAccess.MakeDirRecursiveAbsolute(dir);
var (gMin, gMax) = GrayscaleRenderer.SavePng(p2.Height, p2.MapSize, Path.Combine(dir, "grayscale.png"));
if (!skipRaw) HeightField.Save(p2.Height, p2.MapSize, Path.Combine(dir, "height.f32"));
var look = new LookConfig
{
Name = "hillshade_even", Palette = ReliefPalette.Kind.ProvisionalEven,
ZExaggeration = 18f, LightAzimuth = 315f, LightAltitude = 45f,
HillshadeStrength = 0.30f, SeaLevel = sea,
};
Image map = ReliefRenderer.Render(p2.Height, p2.MapSize, look);
LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap, p2.VariantLabel.ToUpperInvariant())
.SavePng(Path.Combine(dir, "relief.png"));
var (a100, a220) = ShapingOracle.LandAbove(p2, sea);
GD.Print($" {p2.VariantLabel,-30} seed {p2.Seed,-11} h[{p2.HMin,7:F3} .. {p2.HMax,6:F3}] " +
$" >100m {a100,5:F2}% >220m {a220,5:F2}% {p2.ElapsedMs,5} ms");
return $"| `{p2.Seed}_{p2.VariantLabel}` | {p2.Seed} | {p2.VariantLabel} | {p2.HMin:F3} | {p2.HMax:F3} | " +
$"{a100:F2}% | {a220:F2}% | {gMin:F3}..{gMax:F3} | {p2.ElapsedMs} ms |";
}
private static void DrawShaped(Pass2Result p2, CurveAnchors a, int seed, int mapSize,
string batchRoot, int order, float sea)
{
var shaped = new LandHistogram(sea);
shaped.Accumulate(p2.Height, mapSize);
float top = MathF.Ceiling(shaped.MaxLand * 20f) / 20f;
var display = shaped.Rebin((top - shaped.SeaLevel) / 360f);
var (a100, a220) = ShapingOracle.LandAbove(p2, sea);
var o = new HistogramRenderer.Options
{
Title = $"{p2.VariantLabel.ToUpperInvariant()} - SEED {seed}",
Subtitle = $"LAND ABOVE 100M {a100:F2} PCT - ABOVE 220M {a220:F2} PCT",
XAxisLabel = "RAW HEIGHT (POST-CURVE)",
XTop = top,
Footer = $"{shaped.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}",
};
// The two heights the restoration is measured at, on every plate, so the three-way
// contrast can be read off the same reference lines.
o.Markers.Add(new HistogramRenderer.Marker { Value = a.Sea + WorldScale.RawFromMetres(100f), Label = "100M" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.Sea + WorldScale.RawFromMetres(220f), Label = "220M" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M", Strong = false });
if (p2.Continuous != null)
o.Markers.Add(new HistogramRenderer.Marker { Value = p2.Continuous.CeilingOut, Label = "LOWLAND", Strong = false });
string file = $"hist_{seed}_{order}_{p2.VariantLabel}.png";
HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, file));
GD.Print($" {file}");
}
private static void WriteIndex(string batchRoot, int mapSize, int showSize, int[] seeds, int primary,
CurveAnchors a, Dictionary<(int, string), Pass2Result> results,
ClimbCalibration calRestored, ClimbCalibration calBigger, ClimbCalibration calSharper,
double[] pcts, float[] rawQ, float[] outQ,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> soft, List<ShapingOracle.Check> mountain,
List<string> rows, bool hardOk, string showNote, float sea)
{
var sb = new StringBuilder();
sb.AppendLine("# Batch 03 — restore the mountain, as a smooth slope");
sb.AppendLine();
sb.AppendLine("The continuous climb's control points are now **measured off the staircase** instead of");
sb.AppendLine("invented from shape knobs. Same mountain mass, zero terraces. The lowlands are still");
sb.AppendLine("preserved bit-for-bit (oracle d).");
sb.AppendLine();
sb.AppendLine("## ⭐ Open this first");
sb.AppendLine();
sb.AppendLine($"1. **`{primary}_continuous_restored_showpiece/relief.png`** — the centerpiece ({showNote}).");
sb.AppendLine("2. **The three-way histogram contrast**, adjacent by filename:");
sb.AppendLine($" - `hist_{primary}_1_staircase.png` — the mass, parked in two spikes");
sb.AppendLine($" - `hist_{primary}_2_continuous_02default.png` — spikes gone, **and so is the mass**");
sb.AppendLine($" - `hist_{primary}_3_continuous_restored.png` — ⭐ **the mass back, spread smooth**");
sb.AppendLine(" Every plate carries the same 100 m / 220 m reference lines.");
sb.AppendLine();
sb.AppendLine("## The restoration, measured");
sb.AppendLine();
sb.AppendLine("| Variant | land >100 m | land >220 m |");
sb.AppendLine("|---|---|---|");
foreach (string label in new[] { "staircase", "continuous_02default", "continuous_restored", "continuous_bigger", "continuous_sharper_peak" })
{
var (x100, x220) = ShapingOracle.LandAbove(results[(primary, label)], sea);
string star = label == "continuous_restored" ? " ⭐" : label == "staircase" ? " *(target)*" : "";
sb.AppendLine($"| `{label}`{star} | {x100:F2} % | {x220:F2} % |");
}
sb.AppendLine();
sb.AppendLine($"*(seed {primary} at {mapSize}; per-seed rows in Results below.)*");
sb.AppendLine();
sb.AppendLine("## The calibration");
sb.AppendLine();
sb.AppendLine("Measured on the 6-seed pool, above-ceiling land only:");
sb.AppendLine();
sb.AppendLine("| percentile | raw | staircase output |");
sb.AppendLine("|---|---|---|");
for (int i = 0; i < pcts.Length; i++)
sb.AppendLine($"| P{pcts[i]:F0} | {rawQ[i]:F4} | **{WorldScale.MetresFromRaw(outQ[i] - sea):F0} m** |");
sb.AppendLine();
sb.AppendLine("| Variant | knobs | control points (u,v) |");
sb.AppendLine("|---|---|---|");
sb.AppendLine($"| `continuous_restored` | {calRestored.Describe().Split('·')[0].Trim()} | `{calRestored.Describe().Split('·')[1].Trim()}` |");
sb.AppendLine($"| `continuous_bigger` | {calBigger.Describe().Split('·')[0].Trim()} | `{calBigger.Describe().Split('·')[1].Trim()}` |");
sb.AppendLine($"| `continuous_sharper_peak` | {calSharper.Describe().Split('·')[0].Trim()} | `{calSharper.Describe().Split('·')[1].Trim()}` |");
sb.AppendLine();
sb.AppendLine($"`floored` counts segments the no-bench floor had to lift — i.e. where the staircase was flat.");
sb.AppendLine();
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
sb.AppendLine();
sb.AppendLine("`ProvisionalEven` — the CostaRica colours re-spaced evenly SEA → 420 m. Final calibration");
sb.AppendLine("waits for the chosen profile. **Grayscale + the histograms are the honest instruments.**");
sb.AppendLine();
sb.AppendLine("## The oracle");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(hard));
sb.AppendLine($"**{(hardOk ? "ALL HARD CHECKS PASS" : " HARD FAILURES do not judge this batch")}**");
sb.AppendLine();
sb.AppendLine("Soft — upper climb slope profile (e):");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(soft));
sb.AppendLine("(g) mountain restored — **reported, not gated** (it is a taste target the developer tunes):");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(mountain));
sb.AppendLine("## Disposability");
sb.AppendLine();
sb.AppendLine("| Artifact | Keep? |");
sb.AppendLine("|---|---|");
sb.AppendLine("| `relief.png`, `hist_*.png`, `INDEX.md` | **keep** |");
sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code (the byte-level oracle) |");
sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
sb.AppendLine();
sb.AppendLine("## Results");
sb.AppendLine();
sb.AppendLine("| Folder | Seed | Variant | h min | h max | >100 m | >220 m | grayscale range | time |");
sb.AppendLine("|---|---|---|---|---|---|---|---|---|");
foreach (string row in rows) sb.AppendLine(row);
sb.AppendLine();
sb.AppendLine($"MapSize {mapSize}, showpiece {showSize}, seeds {string.Join(", ", seeds)}. {WorldScale.Describe()}.");
string index = Path.Combine(batchRoot, "INDEX.md");
using var f = Godot.FileAccess.Open(index, Godot.FileAccess.ModeFlags.Write);
if (f == null) { GD.PrintErr($"could not write {index}"); return; }
f.StoreString(sb.ToString());
}
// ---- env helpers ----------------------------------------------------
private static string EnvStr(string k, string fallback)
{
string v = System.Environment.GetEnvironmentVariable(k);
return string.IsNullOrWhiteSpace(v) ? fallback : v;
}
private static int EnvInt(string k, int fallback)
=> int.TryParse(EnvStr(k, null) ?? "", out int v) ? v : fallback;
private static float EnvFloat(string k, float fallback)
=> float.TryParse(EnvStr(k, null) ?? "", System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out float v) ? v : fallback;
private static int[] EnvSeeds(string k, int[] fallback)
{
string v = EnvStr(k, null);
if (v == null) return fallback;
var outp = new List<int>();
foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
if (int.TryParse(part.Trim(), out int s) && s > 0) outp.Add(s);
return outp.Count > 0 ? outp.ToArray() : fallback;
}
}
}

View file

@ -0,0 +1 @@
uid://dsg3y4a75vwmu

View file

@ -238,9 +238,14 @@ namespace IslaApocalypse.Tools
// stays hard for the same reason.
float spikeMax = HeightCurve.EffectiveSpikeMax(p1.HMaxSeed, knots, anchors);
// Build throws (refusing the generation) on any config that cannot hit the target
// silhouette; the tool's _Ready catches and Quit(2)s.
var curve = ContinuousCurve.Build(knots, anchors, spikeMax,
// ⭐ CALIBRATED when a measurement is supplied (chat2/03 — the staircase's mountain with
// the terraces melted out), ANALYTIC otherwise (chat2/02's feather/drama curve, kept as
// the "before" contrast). Both throw and refuse rather than degrade; the tool's _Ready
// catches and Quit(2)s.
var curve = cfg.ClimbCalibration != null
? ContinuousCurve.BuildCalibrated(knots, anchors, spikeMax,
cfg.LowlandCeilingM, cfg.ClimbCalibration)
: ContinuousCurve.Build(knots, anchors, spikeMax,
cfg.LowlandCeilingM, cfg.ClimbFeather, cfg.SummitDrama);
// Monotone by construction — and proven anyway, per seed, because "cannot fail" is

View file

@ -349,6 +349,57 @@ namespace IslaApocalypse.Tools
return c;
}
/// <summary>
/// (g) ⭐ MOUNTAIN RESTORED (chat2/03) — how much land ends up above 100 m and 220 m,
/// against the staircase's own figures.
///
/// ═══ ⚠ REPORTED, NOT GATED ═══
///
/// This is a taste target the developer tunes, so a miss is a FINDING, not a build failure —
/// gating it would make `mountainLift` unusable as a knob, since every value but one would
/// fail the run. What it must never do is stay silent: chat2/02 lost two thirds of the
/// mountain and only found out because someone went looking at the dumps afterwards. This
/// check is that look, made automatic.
///
/// <paramref name="tolerancePp"/> only decides whether the row reads PASS or NOTE; the
/// numbers are always printed.
/// </summary>
public static Check MountainRestored(Pass2Result variant, Pass2Result staircase,
float seaLevel, double tolerancePp)
{
var c = new Check { Id = "g", Name = $"mountain restored vs staircase [{variant.VariantLabel}]" };
var (v100, v220) = LandAbove(variant, seaLevel);
var (s100, s220) = LandAbove(staircase, seaLevel);
double d100 = v100 - s100;
c.Passed = Math.Abs(d100) <= tolerancePp;
c.Detail = $">100 m: {v100:F2} % vs staircase {s100:F2} % ({d100:+0.00;-0.00} pp) · " +
$">220 m: {v220:F2} % vs {s220:F2} % ({v220 - s220:+0.00;-0.00} pp)";
return c;
}
/// <summary>Percentage of LAND above 100 m and 220 m of world height. Land = at/above sea.</summary>
public static (double above100, double above220) LandAbove(Pass2Result p2, float seaLevel)
{
float t100 = seaLevel + WorldScale.RawFromMetres(100f);
float t220 = seaLevel + WorldScale.RawFromMetres(220f);
long land = 0, a100 = 0, a220 = 0;
for (int x = 0; x < p2.MapSize; x++)
{
for (int y = 0; y < p2.MapSize; y++)
{
float h = p2.Height[x, y];
if (h < seaLevel) continue;
land++;
if (h > t100) a100++;
if (h > t220) a220++;
}
}
return land == 0 ? (0.0, 0.0) : (100.0 * a100 / land, 100.0 * a220 / land);
}
/// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary>
public static string ToMarkdownTable(IEnumerable<Check> checks)
{

View file

@ -145,13 +145,51 @@ namespace IslaApocalypse.Tools
public float ClimbFeather = 0.4f;
/// <summary>
/// The summit's steepening, ≥ 1: the secant slope of the top 15 % of the climb, in units of
/// the climb's average grade. 1 = a ramp (refused); 2.5 = the default pointed peak; higher =
/// more dramatic. The peak reads pointy, never a needle-on-a-hump — there is no plateau
/// under it any more.
/// ⚠ chat2/02's ANALYTIC summit knob — SUPERSEDED by <see cref="PeakSharpness"/>.
///
/// It steepened the peak by pulling the summit ONSET DOWN, which lowered the whole mid-massif
/// with it: at 4.5 the p99 land height collapsed from 199 m to 121 m. A bad trade, and the
/// bug chat2/03 §3 exists to fix. It survives ONLY so the 02 curve stays reproducible as a
/// contrast variant; it is read only when <see cref="ClimbCalibration"/> is null.
/// </summary>
public float SummitDrama = 2.5f;
// ---- chat2/03: the CALIBRATED climb ---------------------------------
/// <summary>
/// ⭐ The measured climb calibration. Non-null ⇒ the climb reproduces the staircase's
/// above-ceiling elevation distribution as a smooth slope. Null ⇒ chat2/02's analytic
/// feather/drama curve (kept only as the "before" contrast).
///
/// ⚠ Not a value knob — it is MEASURED, per calibration pool, by the batch tool. Two configs
/// may share one instance safely: it is immutable.
/// </summary>
public ClimbCalibration ClimbCalibration = null;
/// <summary>
/// ⭐ How big the mountain is, relative to the staircase's.
///
/// 1.0 reproduce the staircase's mountain (the default — the least-surprising baseline)
/// &gt;1 lift the mid-massif higher: more land at 150300 m
/// &lt;1 a smaller mountain, toward chat2/02's bottom-heavy climb
///
/// Applied as <c>v ← v^(1/lift)</c> on the calibrated control points: monotone, and it fixes
/// both endpoints, so it moves the massif without touching the lowland handover or the cap.
/// ⚠ It scales the CLIMB only. It cannot move a lowland cell — oracle (d) proves that.
/// </summary>
public float MountainLift = 1.0f;
/// <summary>
/// ⭐ How pointy the summit is — and, unlike <see cref="SummitDrama"/>, <b>nothing else</b>.
///
/// It reshapes only the span above the last measured percentile, leaving that percentile's
/// height fixed. Raising it therefore cannot reduce the land below the onset: peak sharpness
/// and mountain mass are independent knobs. → chat2/03 §3.
///
/// 1.0 = a straight run to the cap; higher defers the rise so the final approach steepens.
/// </summary>
public float PeakSharpness = 1.0f;
/// <summary>
/// ⭐ Pass 2a rung 2: the shelf detail passes — micro-relief skin + shelf-edge knot warp.
/// ⚠ REQUIRES <see cref="Curve"/>: the edge warp slides the CURVE's knots, so with no curve