using System; using System.Text; namespace IslaApocalypse.Core { /// /// ⭐⭐ 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 () 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: BENCH_BASE/AMP, PLATEAU_BASE/AMP, SHELF_SPAN_* and their three /// modulation noise fields — the above-flood decorative terracing. That is the entire point of /// this mode. /// /// SURVIVES: SEA, ORANGE_CEIL (14 m) and RED_CEIL (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. PEAK_CAP (420 m) /// survives as the summit ceiling, with the per-seed spikeMax 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. 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 — the two modes are one seam. /// public sealed class ContinuousCurve { // ═══ shape constants (not config — the knobs above are the config surface) ═══ /// Where "the summit" begins, as a fraction of the climb's raw span. The top 15 %. public const float SummitOnset = 0.85f; /// /// 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). /// public const float MaxLowlandCeilingM = 80f; /// /// 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. /// public const float NearFlatFactor = 0.25f; // × the normalized join slope public const float CliffCeilingN = 3.5f; // ═══ the built spline ═══ /// The knot set — only K1/K2 are consumed (the preserved toe+red). public readonly CurveKnots Knots; /// The anchors — Sea/Orange/Red/PeakCap/TailSlope consumed; bench/plateau ignored. public readonly CurveAnchors Anchors; /// Raw height where the preserved lowland hands over to the climb. public readonly float CeilingRaw; /// Output height at the handover — the top of the preserved lowland. public readonly float CeilingOut; /// This seed's raw summit: EffectiveSpikeMax(hMaxSeed). The climb's right edge. public readonly float SpikeMax; /// The red band's exit slope — the climb's C¹ join tangent (raw out per raw in). public readonly float JoinSlopeRaw; /// The knob values this spline was built from, for the INDEX and the report. 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; } /// /// Build the per-seed spline. ⚠ PER SEED, because 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. /// 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); } /// /// 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 [0, 3·Δ₀], 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). /// 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; } /// /// The curve, for one column. Handles every range: sea identity, the preserved lowland /// (BY DELEGATION to — the same code path, hence the same /// bits), the linear extension, the climb, the tail. /// 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]; } /// The climb's derivative at a raw height inside (CeilingRaw, SpikeMax). 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; } /// /// 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. /// /// A one-line confirmation for the run log. 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})."; } /// /// 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. /// 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); } /// Raw height where the summit onset sits, and its output — for histogram overlays. public (float raw, float outp) SummitOnsetPoint() { float r = CeilingRaw + SummitOnset * (SpikeMax - CeilingRaw); return (r, Apply(r)); } /// The control points as one line for the INDEX and the report. 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(); } } }