rev 3 of the curve redesign. The developer's verdict on the 01 baseline was that the LOWLANDS ARE GOOD; the fault is the terracing above them. So this adds a second curve mode that preserves the low plain bit-for-bit and replaces everything above the flood line with one smooth monotone climb. Core/ContinuousCurve — piecewise, and the pieces have different loyalties: - at/below sea: identity, as ever. - above sea to K2: DELEGATES to HeightCurve's own toe+red branches. Not "equivalent" — the same code path, so the same floats. Oracle (d) holds it to that. - above the ceiling: a Fritsch-Carlson (PCHIP) monotone spline to the 420 m cap, C1-joined to the red band's exit slope. Monotone by construction for any ordered control points, which retires the 24-corner sweep; a 10k strict-increase sample runs per seed anyway, because "cannot fail" is worth a millisecond. - Build() REFUSES rather than degrades: a ceiling near the old bench, a drama that folds the summit under its own onset, control-point secants that are not strictly increasing (the no-magnet rule, enforced rather than hoped for). Only BENCH_*/PLATEAU_* are dropped. SEA/ORANGE_CEIL/RED_CEIL survive because they are the storm-ladder FLOOD TIERS and they live inside the preserved lowland; PEAK_CAP and the per-seed spikeMax normalization survive as the summit. Shelf detail is forced off in continuous mode: the flat benches it de-slabbed no longer exist, and painting noise on the climb now would pre-judge what erosion should carve. Oracle, all hard checks passing: - (a1) curve off is bit-identical to Phase 1's dump. - (a2) staircase mode is bit-identical to TASK 01's dump — the control is provably the control, not a re-derivation. (CurveBaselineTool is pinned to Staircase so the config default moving to Continuous cannot drift it.) - (d) lowlands bit-identical to the staircase over 3.6M cells, every continuous variant, both seeds. The lifted_WRONG bookend fails it on 1.6M cells, as intended. - (f) sea identity per CELL, not per count, including 67M cells at 8192. The finding, measured and recorded in the batch scratch: the massif SHRANK. Land above 100 m goes 14.9% -> 4.8%, above 220 m 4.5% -> 0.6%. A feather sweep to the practical floor recovers ~1.3 points, so this is structural, not a tuning miss: the staircase's highland area was an artifact of the bench and plateau acting as magnets, and a curve with no magnets preserves the raw distribution's bottom-heavy shape. "No terraces" and "the same land up high" are not both available from curve work alone. Exploration batch, not convergence. A tuning pass follows once a direction is picked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
418 lines
19 KiB
C#
418 lines
19 KiB
C#
using System;
|
||
using System.Text;
|
||
|
||
namespace IslaApocalypse.Core
|
||
{
|
||
/// <summary>
|
||
/// ⭐⭐ THE CONTINUOUS-GRADE CURVE (chat2/02) — smooth the UPPER staircase, preserve the lowlands.
|
||
///
|
||
/// ═══ WHAT THIS IS, AND WHAT IT REFUSES TO BE ═══
|
||
///
|
||
/// The faithful v5 staircase (<see cref="HeightCurve"/>) terraces the island above the flood
|
||
/// tiers: foothill riser → bench → mid riser → plateau → summit needle. The developer's verdict
|
||
/// on the 01 baseline: the LOWLANDS ARE GOOD — the broad low plain, ~75 % of land below 30 m, is
|
||
/// the thing to keep. The fault is entirely ABOVE them: flat benches read as authored terraces
|
||
/// and the summit reads as a needle on a hump.
|
||
///
|
||
/// So this curve is PIECEWISE, and the pieces have different loyalties:
|
||
///
|
||
/// raw ≤ SEA IDENTITY. The coastline must not move. (Same line as v5.)
|
||
/// SEA < raw ≤ K2 ⭐ THE STAIRCASE'S OWN toe+red mapping, BY DELEGATION — the same
|
||
/// code path, so the lowland output is BIT-IDENTICAL to task 01's.
|
||
/// Not "equivalent": the same floats. Oracle (d) holds this.
|
||
/// K2 < raw ≤ ceilingRaw the red band's exit slope, CONTINUED LINEARLY — only non-empty
|
||
/// when the ceiling is raised above the default 30 m, extending the
|
||
/// current gentle low grade before the climb begins.
|
||
/// ceilingRaw < raw ≤ spikeMax
|
||
/// ⭐ THE NEW CLIMB — one smooth monotone Fritsch–Carlson (PCHIP)
|
||
/// spline from the lowland ceiling to PEAK_CAP. No bench, no
|
||
/// plateau, no needle: a coherent massif steepening to a peak.
|
||
/// raw > spikeMax the gentle tail, as v5: PEAK_CAP + (raw − spikeMax) · TAIL_SLOPE.
|
||
///
|
||
/// ═══ ⚠⚠ WHAT IS DELIBERATELY DROPPED, AND WHAT DELIBERATELY SURVIVES ═══
|
||
///
|
||
/// DROPPED: <c>BENCH_BASE/AMP</c>, <c>PLATEAU_BASE/AMP</c>, <c>SHELF_SPAN_*</c> and their three
|
||
/// modulation noise fields — the above-flood decorative terracing. That is the entire point of
|
||
/// this mode.
|
||
///
|
||
/// SURVIVES: <c>SEA</c>, <c>ORANGE_CEIL</c> (14 m) and <c>RED_CEIL</c> (30 m), because they are
|
||
/// the STORM-LADDER FLOOD TIERS and they live inside the preserved lowland — D-036's
|
||
/// terrain-shelves-at-flood-tiers is intact where it carries meaning. <c>PEAK_CAP</c> (420 m)
|
||
/// survives as the summit ceiling, with the per-seed <c>spikeMax</c> normalization unchanged.
|
||
///
|
||
/// ═══ ⚠ MONOTONE BY CONSTRUCTION — WHY THE 24-CORNER SWEEP RETIRES HERE ═══
|
||
///
|
||
/// Fritsch–Carlson tangent limiting guarantees a monotone interpolant for ANY monotone control
|
||
/// points: every tangent is clamped into the region where the Hermite cubic cannot overshoot.
|
||
/// The staircase needed a numeric sweep because its effective shape depended on three modulation
|
||
/// fields and a per-column warp; this curve has no per-column inputs at all — one spline per
|
||
/// seed. <see cref="AssertStrictlyIncreasing"/> still runs a cheap dense sample per seed,
|
||
/// because "cannot fail" is exactly the claim worth spending a millisecond checking.
|
||
///
|
||
/// ═══ THE TWO KNOBS (plus the ceiling) — ALL ACT ABOVE THE CEILING ONLY ═══
|
||
///
|
||
/// lowlandCeiling where the preserved low grade hands over to the climb (config, metres;
|
||
/// default 30 = RED_CEIL, i.e. the flood line — hand over exactly where the
|
||
/// staircase's lowland ends).
|
||
/// climbFeather how long the climb hugs the lowland's exit slope before steepening.
|
||
/// summitDrama how hard the top ~15 % steepens, so the peak reads pointy, not a ramp.
|
||
///
|
||
/// ⚠ NO CONTROL POINT MAY ACT AS A MAGNET. The generator enforces strictly INCREASING segment
|
||
/// secants below the summit: mass can never pile at an interior point the way it piled at the
|
||
/// bench, because no interval maps wide-in to narrow-out below the summit onset.
|
||
///
|
||
/// Engine-free (System.MathF), beside <see cref="HeightCurve"/> — the two modes are one seam.
|
||
/// </summary>
|
||
public sealed class ContinuousCurve
|
||
{
|
||
// ═══ shape constants (not config — the knobs above are the config surface) ═══
|
||
|
||
/// <summary>Where "the summit" begins, as a fraction of the climb's raw span. The top 15 %.</summary>
|
||
public const float SummitOnset = 0.85f;
|
||
|
||
/// <summary>
|
||
/// The ceiling knob's hard bound, metres. The bench sat at 100±12 m; a lowland ceiling at or
|
||
/// above it could preserve a flat bench, which is the one thing this mode exists to remove.
|
||
/// 80 m keeps clear air below the old bench's lowest excursion (88 m).
|
||
/// </summary>
|
||
public const float MaxLowlandCeilingM = 80f;
|
||
|
||
/// <summary>
|
||
/// Oracle (e) tripwires, in NORMALIZED climb slope (1 = the climb's average grade).
|
||
/// Floor: a slope this far below the join slope reads as a bench — the artifact this mode
|
||
/// removes. Ceiling: a slope this steep below the summit onset reads as a cliff.
|
||
/// Warn-and-report, not throw — this is an exploration batch.
|
||
/// </summary>
|
||
public const float NearFlatFactor = 0.25f; // × the normalized join slope
|
||
public const float CliffCeilingN = 3.5f;
|
||
|
||
// ═══ the built spline ═══
|
||
|
||
/// <summary>The knot set — only K1/K2 are consumed (the preserved toe+red).</summary>
|
||
public readonly CurveKnots Knots;
|
||
|
||
/// <summary>The anchors — Sea/Orange/Red/PeakCap/TailSlope consumed; bench/plateau ignored.</summary>
|
||
public readonly CurveAnchors Anchors;
|
||
|
||
/// <summary>Raw height where the preserved lowland hands over to the climb.</summary>
|
||
public readonly float CeilingRaw;
|
||
|
||
/// <summary>Output height at the handover — the top of the preserved lowland.</summary>
|
||
public readonly float CeilingOut;
|
||
|
||
/// <summary>This seed's raw summit: <c>EffectiveSpikeMax(hMaxSeed)</c>. The climb's right edge.</summary>
|
||
public readonly float SpikeMax;
|
||
|
||
/// <summary>The red band's exit slope — the climb's C¹ join tangent (raw out per raw in).</summary>
|
||
public readonly float JoinSlopeRaw;
|
||
|
||
/// <summary>The knob values this spline was built from, for the INDEX and the report.</summary>
|
||
public readonly float LowlandCeilingM, ClimbFeather, SummitDrama;
|
||
|
||
// Control points (raw x, out y) and the Fritsch–Carlson 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)
|
||
{
|
||
Knots = k; Anchors = a;
|
||
CeilingRaw = ceilingRaw; CeilingOut = ceilingOut; SpikeMax = spikeMax;
|
||
JoinSlopeRaw = joinSlopeRaw;
|
||
LowlandCeilingM = lowlandCeilingM; ClimbFeather = climbFeather; SummitDrama = summitDrama;
|
||
_x = x; _y = y; _m = m;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Build the per-seed spline. ⚠ PER SEED, because <paramref name="spikeMax"/> is per seed —
|
||
/// exactly the same reason the staircase's monotonicity sweep ran per seed.
|
||
///
|
||
/// Throws (refusing the generation) on any configuration that cannot produce the target
|
||
/// silhouette: a ceiling at bench height, a drama that would fold the summit under its own
|
||
/// onset, a ceiling above the seed's summit.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
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.");
|
||
if (climbFeather < 0f || climbFeather > 1f)
|
||
throw new InvalidOperationException($"[ContinuousCurve] climbFeather {climbFeather} is outside [0,1]. Refusing.");
|
||
if (summitDrama < 1f)
|
||
throw new InvalidOperationException($"[ContinuousCurve] summitDrama {summitDrama} < 1 would make the summit SHALLOWER than the climb's average — that is a ramp, not a peak. Refusing.");
|
||
|
||
// ---- control points, in normalized climb space ----
|
||
// u = (raw − ceilingRaw)/(spikeMax − ceilingRaw), v = (out − ceilingOut)/(PeakCap − ceilingOut).
|
||
float spanRaw = spikeMax - ceilingRaw;
|
||
float spanOut = a.PeakCap - ceilingOut;
|
||
float s0 = redSlope * spanRaw / spanOut; // the join slope, normalized
|
||
|
||
// The feather point: hug the join slope until u_f, then lift. Larger feather = longer hug.
|
||
float uF = 0.20f + 0.35f * climbFeather;
|
||
float vF = s0 * uF * 1.05f; // fractionally above the pure hug, so
|
||
// the secant already rises — no dip
|
||
|
||
// The summit onset: its secant to (1,1) IS the drama. v_s = 1 − drama·(1 − u_s).
|
||
float uS = SummitOnset;
|
||
float vS = 1f - summitDrama * (1f - uS);
|
||
if (vS <= vF + 0.02f)
|
||
throw new InvalidOperationException(
|
||
$"[ContinuousCurve] summitDrama {summitDrama:F2} folds the summit onset (v={vS:F3}) " +
|
||
$"under the feather point (v={vF:F3}) — the mid-climb would have to be flat or " +
|
||
"descending to compensate. Lower the drama or the feather. Refusing.");
|
||
|
||
// A mid point keeps the feather→onset transition smooth, on a gently convex path so the
|
||
// segment secants stay strictly INCREASING — the no-magnet guarantee.
|
||
float uM = (uF + uS) * 0.5f;
|
||
float vM = vF + (vS - vF) * MathF.Pow((uM - uF) / (uS - uF), 1.35f);
|
||
|
||
float[] u = { 0f, uF, uM, uS, 1f };
|
||
float[] v = { 0f, vF, vM, vS, 1f };
|
||
|
||
// ⚠ THE NO-MAGNET CHECK, enforced rather than assumed: every secant below the summit
|
||
// must be strictly greater than the one before it. A wide-in→narrow-out interval below
|
||
// the onset is a bench in the making.
|
||
float prevSecant = 0f;
|
||
for (int i = 1; i < u.Length; i++)
|
||
{
|
||
float sec = (v[i] - v[i - 1]) / (u[i] - u[i - 1]);
|
||
if (sec <= prevSecant)
|
||
throw new InvalidOperationException(
|
||
$"[ContinuousCurve] control-point secants are not strictly increasing at segment {i} " +
|
||
$"({sec:F4} after {prevSecant:F4}) with feather={climbFeather:F2}, drama={summitDrama:F2} — " +
|
||
"an interior point would act as a magnet. Refusing.");
|
||
prevSecant = sec;
|
||
}
|
||
|
||
// ---- denormalize and fit ----
|
||
int n = u.Length;
|
||
var x = new float[n];
|
||
var y = new float[n];
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
x[i] = ceilingRaw + u[i] * spanRaw;
|
||
y[i] = ceilingOut + v[i] * spanOut;
|
||
}
|
||
|
||
float[] m = FritschCarlsonTangents(x, y, startTangent: redSlope);
|
||
|
||
return new ContinuousCurve(k, a, ceilingRaw, ceilingOut, spikeMax, redSlope,
|
||
lowlandCeilingM, climbFeather, summitDrama, x, y, m);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Fritsch–Carlson (1980) monotone tangents, with a PRESCRIBED start tangent for the C¹
|
||
/// join. The weighted-harmonic-mean interior tangents already satisfy the monotonicity
|
||
/// region; the prescribed start is clamped into <c>[0, 3·Δ₀]</c>, which is the classical
|
||
/// sufficient bound — so the join is C¹ wherever the lowland's exit slope permits, and
|
||
/// safely limited where it does not (which is then reported by the slope sampler, not
|
||
/// hidden).
|
||
/// </summary>
|
||
private static float[] FritschCarlsonTangents(float[] x, float[] y, float startTangent)
|
||
{
|
||
int n = x.Length;
|
||
var h = new float[n - 1]; // interval widths
|
||
var d = new float[n - 1]; // secants
|
||
for (int i = 0; i < n - 1; i++)
|
||
{
|
||
h[i] = x[i + 1] - x[i];
|
||
d[i] = (y[i + 1] - y[i]) / h[i];
|
||
}
|
||
|
||
var m = new float[n];
|
||
|
||
// Start: the C¹ join, clamped into the monotone region.
|
||
m[0] = Math.Clamp(startTangent, 0f, 3f * d[0]);
|
||
|
||
// Interior: weighted harmonic mean — zero if the secants disagree in sign (they cannot
|
||
// here, both positive, but the guard is the algorithm's own and stays).
|
||
for (int i = 1; i < n - 1; i++)
|
||
{
|
||
if (d[i - 1] * d[i] <= 0f) { m[i] = 0f; continue; }
|
||
float w1 = 2f * h[i] + h[i - 1];
|
||
float w2 = h[i] + 2f * h[i - 1];
|
||
m[i] = (w1 + w2) / (w1 / d[i - 1] + w2 / d[i]);
|
||
}
|
||
|
||
// End: one-sided three-point estimate, clamped like the start. The summit's entry
|
||
// steepness comes from the last secant (the drama), not from an extrapolated spike.
|
||
float mEnd = ((2f * h[n - 2] + (n > 2 ? h[n - 3] : h[n - 2])) * d[n - 2]
|
||
- h[n - 2] * (n > 2 ? d[n - 3] : d[n - 2]))
|
||
/ (h[n - 2] + (n > 2 ? h[n - 3] : h[n - 2]));
|
||
if (mEnd < 0f) mEnd = 0f;
|
||
m[n - 1] = MathF.Min(mEnd, 3f * d[n - 2]);
|
||
|
||
return m;
|
||
}
|
||
|
||
/// <summary>
|
||
/// The curve, for one column. Handles every range: sea identity, the preserved lowland
|
||
/// (BY DELEGATION to <see cref="HeightCurve.Apply"/> — the same code path, hence the same
|
||
/// bits), the linear extension, the climb, the tail.
|
||
/// </summary>
|
||
public float Apply(float h)
|
||
{
|
||
// ⭐ IDENTITY AT AND BELOW SEA — the same load-bearing line as v5.
|
||
if (h <= Anchors.Sea) return h;
|
||
|
||
// ⭐ THE PRESERVED LOWLAND: delegate to the staircase's own toe+red branches. Below K2,
|
||
// HeightCurve.Apply never reads the bench/plateau/edge parameters, so any values pass —
|
||
// and the output is bit-identical to task 01's staircase, which oracle (d) asserts.
|
||
if (h < Knots.K2)
|
||
return HeightCurve.Apply(h, SpikeMax,
|
||
Anchors.BenchBase, Anchors.ShelfSpanMin, Anchors.PlateauBase, Anchors.ShelfSpanMin,
|
||
Knots, Anchors, edgeShift: 0f);
|
||
|
||
// The red band's grade, continued. Empty at the default ceiling (CeilingRaw == K2);
|
||
// at h == K2 exactly this is RED_CEIL + 0 — the same value the staircase's foothill
|
||
// riser produces at its own u = 0.
|
||
if (h <= CeilingRaw)
|
||
return Anchors.RedCeil + (h - Knots.K2) * JoinSlopeRaw;
|
||
|
||
// The gentle tail, as v5 — a slope, not a clip.
|
||
if (h >= SpikeMax)
|
||
return Anchors.PeakCap + (h - SpikeMax) * Anchors.TailSlope;
|
||
|
||
// ⭐ THE CLIMB: cubic Hermite on the Fritsch–Carlson tangents.
|
||
int i = FindInterval(h);
|
||
float dx = _x[i + 1] - _x[i];
|
||
float t = (h - _x[i]) / dx;
|
||
float t2 = t * t, t3 = t2 * t;
|
||
|
||
return (2f * t3 - 3f * t2 + 1f) * _y[i]
|
||
+ (t3 - 2f * t2 + t) * dx * _m[i]
|
||
+ (-2f * t3 + 3f * t2) * _y[i + 1]
|
||
+ (t3 - t2) * dx * _m[i + 1];
|
||
}
|
||
|
||
/// <summary>The climb's derivative at a raw height inside (CeilingRaw, SpikeMax).</summary>
|
||
public float SlopeAt(float h)
|
||
{
|
||
if (h <= CeilingRaw || h >= SpikeMax) return JoinSlopeRaw; // outside the spline proper
|
||
int i = FindInterval(h);
|
||
float dx = _x[i + 1] - _x[i];
|
||
float t = (h - _x[i]) / dx;
|
||
float t2 = t * t;
|
||
|
||
return (6f * t2 - 6f * t) * (_y[i] - _y[i + 1]) / dx
|
||
+ (3f * t2 - 4f * t + 1f) * _m[i]
|
||
+ (3f * t2 - 2f * t) * _m[i + 1];
|
||
}
|
||
|
||
private int FindInterval(float h)
|
||
{
|
||
// Four intervals — a linear scan beats a binary search at this size.
|
||
for (int i = _x.Length - 2; i > 0; i--)
|
||
if (h >= _x[i]) return i;
|
||
return 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// The cheap per-seed proof that "monotone by construction" held in float32 too: a dense
|
||
/// strict-increase sample over the whole range, sea to past the tail. Throws and refuses on
|
||
/// violation, exactly as the staircase's sweep did. ~10k samples, sub-millisecond.
|
||
/// </summary>
|
||
/// <returns>A one-line confirmation for the run log.</returns>
|
||
public string AssertStrictlyIncreasing()
|
||
{
|
||
float prevH = Anchors.Sea;
|
||
float prev = Apply(prevH);
|
||
double top = SpikeMax + 0.5;
|
||
double step = (top - Anchors.Sea) / 10000.0;
|
||
|
||
for (double hd = Anchors.Sea + step; hd <= top; hd += step)
|
||
{
|
||
float h = (float)hd;
|
||
if (h <= prevH) continue; // float32 dedupe, as the staircase's sweep
|
||
float v = Apply(h);
|
||
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.");
|
||
prev = v;
|
||
prevH = h;
|
||
}
|
||
|
||
return $"[ContinuousCurve] strict-increase sample passed (10k points, ceiling {LowlandCeilingM:F0} m, " +
|
||
$"feather {ClimbFeather:F2}, drama {SummitDrama:F2}, spikeMax {SpikeMax:F6}).";
|
||
}
|
||
|
||
/// <summary>
|
||
/// Oracle (e)'s instrument: sample the climb's slope densely and report it in NORMALIZED
|
||
/// units (1 = the climb's average grade). Returns the extremes and where they sit, plus the
|
||
/// tripwire verdicts — the caller decides how loudly to say it.
|
||
/// </summary>
|
||
public (float minN, float minAtRaw, float maxBelowOnsetN, float maxAtRaw, bool nearFlat, bool cliff)
|
||
SampleClimbSlopes(int samples = 2000)
|
||
{
|
||
float spanRaw = SpikeMax - CeilingRaw;
|
||
float spanOut = Anchors.PeakCap - CeilingOut;
|
||
float toN = spanRaw / spanOut; // raw slope → normalized
|
||
float onsetRaw = CeilingRaw + SummitOnset * spanRaw;
|
||
float s0N = JoinSlopeRaw * toN;
|
||
|
||
float minN = float.MaxValue, maxN = float.MinValue, minAt = 0f, maxAt = 0f;
|
||
for (int i = 1; i < samples; i++)
|
||
{
|
||
float h = CeilingRaw + spanRaw * i / samples;
|
||
float sN = SlopeAt(h) * toN;
|
||
if (sN < minN) { minN = sN; minAt = h; }
|
||
if (h < onsetRaw && sN > maxN) { maxN = sN; maxAt = h; }
|
||
}
|
||
|
||
bool nearFlat = minN < s0N * NearFlatFactor;
|
||
bool cliff = maxN > CliffCeilingN;
|
||
return (minN, minAt, maxN, maxAt, nearFlat, cliff);
|
||
}
|
||
|
||
/// <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);
|
||
return (r, Apply(r));
|
||
}
|
||
|
||
/// <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 ");
|
||
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");
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
}
|