Phase 2a: continuous grade — smooth the upper staircase, preserve the lowlands

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
This commit is contained in:
Stewart Howe 2026-08-20 03:41:27 -04:00
parent 35b4818e9e
commit 8e55326a84
11 changed files with 1382 additions and 8 deletions

View file

@ -0,0 +1,418 @@
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 &lt; 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 &lt; 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 &lt; raw ≤ spikeMax
/// ⭐ THE NEW CLIMB — one smooth monotone FritschCarlson (PCHIP)
/// spline from the lowland ceiling to PEAK_CAP. No bench, no
/// plateau, no needle: a coherent massif steepening to a peak.
/// raw &gt; 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 ═══
///
/// FritschCarlson 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 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)
{
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>
/// FritschCarlson (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 FritschCarlson 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();
}
}
}

View file

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

View file

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

View file

@ -297,6 +297,10 @@ namespace IslaApocalypse.Tools
{
MapSize = mapSize, Seed = seed, VariantLabel = "curve_on",
Curve = true, ShelfDetail = true, Knots = k, Anchors = a,
// ⚠ PINNED, not defaulted: this tool exists to reproduce the task-01 staircase
// bit-for-bit. chat2/02 moved the config DEFAULT to Continuous for its exploration;
// a control batch must not move with a default. (chat2/02.)
CurveMode = CurveModeKind.Staircase,
};
/// <summary>

View file

@ -0,0 +1,580 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐ THE CONTINUOUS-GRADE EXPLORATION BATCH (chat2/02, rev 3) — smooth the upper staircase,
/// preserve the lowlands, and prove both claims before anyone looks at a render.
///
/// ═══ THE TARGET SILHOUETTE (judge every shaped histogram against THIS) ═══
///
/// The bottom shoulder stays exactly where it is — the broad low pile, ~75 % of land below
/// 30 m, UNLIFTED. Above it, the bench and plateau spikes dissolve and the empty valleys fill
/// in: one smooth continuous falloff out to a thin dramatic peak at 420 m. Only the top ~25 %
/// of land moves.
///
/// ═══ THE VARIANTS — tight, one axis each ═══
///
/// staircase the faithful M3 control (task 01, bit-identical — oracle a2)
/// continuous_default lowland to 30 m, feather 0.40, drama 2.5 — the centerpiece
/// continuous_hold_higher lowland ceiling raised to 50 m — the primary axis explored up
/// continuous_dramatic_peak drama 4.5 — how pointy the peak gets
/// continuous_lifted_WRONG ⚠ the even whole-island lift — DELIBERATELY the wrong direction,
/// a contrast bookend. Its oracle-(d) failure is EXPECTED and is
/// reported as confirmation of why the direction is wrong.
///
/// ═══ ⚠ WHY THIS TOOL RE-MEASURES THE KNOTS INSTEAD OF USING CurveKnots.V2Baseline ═══
///
/// Task 01's batch was generated with the knots MEASURED IN THAT RUN, at full float precision.
/// The baked V2Baseline constants are those values quoted to six decimals — off by &lt;1e-7 raw,
/// which is invisible to every consumer EXCEPT a bit-identity oracle. Oracle (a2) demands the
/// staircase reproduce task 01's `.f32` byte-for-byte, so this tool re-runs the identical
/// 6-seed calibration (same seeds, same size, same instrument) and uses the measured knots.
/// It prints the measured-vs-baked deltas so the equivalence is evidence, not assumption.
///
/// ═══ RUNNING IT ═══
///
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/CurveContinuousTool.tscn
///
/// ISLA_TASK authoring task number (default 2)
/// ISLA_BATCH descriptor, NO prefix (default "curve_continuous")
/// ISLA_MAPSIZE variant profile (default 2048)
/// ISLA_SEEDS variant seeds, comma-separated (default: the 2 pinned below)
/// ISLA_SHOWPIECE_SIZE the big confirmation render (default 8192)
/// ISLA_SHOWPIECE "0" to skip it
/// ISLA_PHASE1_SOURCE batch holding Phase-1 .f32 (default "02_pass1_port")
/// ISLA_T01_SOURCE batch holding task-01 .f32 (default "01_curve_baseline")
/// ISLA_SKIP_RAW "1" to skip the .f32 dumps
/// ISLA_CEILING_M probe override: lowland ceiling, metres (default 30)
/// ISLA_FEATHER probe override: climb feather, 0..1 (default 0.4)
/// ISLA_DRAMA probe override: summit drama, >= 1 (default 2.5)
///
/// ⚠ The three knob overrides move the BASE config only. Each batch variant sets its own axis
/// explicitly in its mutator, so a probe env can shift `continuous_default` but can never
/// silently move `hold_higher`'s ceiling or `dramatic_peak`'s drama out from under their names.
/// </summary>
public partial class CurveContinuousTool : Node
{
/// <summary>Two representative variant seeds — the primary Phase-1 seed and the tallest one.</summary>
private static readonly int[] DefaultSeeds = { 1063685222, 777001 };
/// <summary>
/// ⚠ THE TASK-01 CALIBRATION POOL, VERBATIM — same six seeds, so the measured knots (and
/// with them the staircase control) reproduce task 01 bit-for-bit. Do not "simplify" this
/// to the variant seeds; the pool is part of the knots' identity.
/// </summary>
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
private const int DefaultMapSize = 2048;
private const int DefaultShowpieceSize = 8192;
public override void _Ready()
{
// ⚠ An exception out of _Ready does not stop Godot — it logs and the process HANGS with
// no main loop to end it. Catch, say what was refused, exit 2. (The standing rule.)
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", 2);
string descr = EnvStr("ISLA_BATCH", "curve_continuous");
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";
string batchRoot = ToolingPaths.BatchRoot(task, descr); // composed, never free-form
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
var anchors = CurveAnchors.Default;
float sea = 0.15f;
int primary = seeds[0];
GD.Print("==================================================================");
GD.Print(" CURVE CONTINUOUS (rev 3) — smooth the upper staircase,");
GD.Print(" preserve the lowlands. The staircase rides along as the control.");
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("------------------------------------------------------------------");
GD.Print(ToolingPaths.Describe());
GD.Print("==================================================================");
// ═══ 0. RE-MEASURE THE KNOTS — task 01's calibration, verbatim (see the type header) ═══
GD.Print("\n--- 0. CALIBRATION (the task-01 pool, re-run for bit-identity) ---");
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);
GD.Print($" pooled seed {seed,-11} h[{p1.HMinSeed,7:F3} .. {p1.HMaxSeed,6:F3}] {p1.ElapsedMs,5} ms");
}
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(" measured vs baked V2Baseline (6-decimal roundings — expect <1e-6):");
for (int i = 0; i < 6; i++)
GD.Print($" K{i + 1}: measured {knots[i]:G9} baked {CurveKnots.V2Baseline[i]:G9} " +
$"delta {knots[i] - CurveKnots.V2Baseline[i]:E2}");
// ═══ 1. THE VARIANTS ═══
var variants = new List<(string label, Action<TerrainGenConfig> mutate)>
{
("staircase", c => { c.CurveMode = CurveModeKind.Staircase; c.ShelfDetail = true; }),
("continuous_default", c => { c.CurveMode = CurveModeKind.Continuous; }),
("continuous_hold_higher", c => { c.CurveMode = CurveModeKind.Continuous; c.LowlandCeilingM = 50f; }),
("continuous_dramatic_peak", c => { c.CurveMode = CurveModeKind.Continuous; c.SummitDrama = 4.5f; }),
("continuous_lifted_WRONG", c => { c.CurveMode = CurveModeKind.LiftedWrong; }),
};
GD.Print("\n--- 1. VARIANTS ---");
var results = new Dictionary<(int seed, string label), Pass2Result>();
var offs = new Dictionary<int, Pass2Result>();
var rows = new List<string>();
bool notesPrinted = false;
foreach (int seed in seeds)
{
Pass1Result p1 = pass1[seed];
offs[seed] = Shaping.Shape(p1, BaseConfig(mapSize, seed, knots, anchors, "curve_off", off: true));
foreach (var (label, mutate) in variants)
{
var cfg = BaseConfig(mapSize, seed, knots, anchors, label, off: false);
mutate(cfg);
Pass2Result p2 = Shaping.Shape(p1, cfg);
results[(seed, label)] = p2;
if (!notesPrinted) foreach (string n in p2.Notes) GD.Print(" " + n);
rows.Add(WriteVariant(batchRoot, p2, sea, anchors, skipRaw));
}
notesPrinted = true;
}
// ═══ 2. THE ORACLE — before anything is looked at ═══
GD.Print("\n--- 2. ORACLE ---");
var hard = new List<ShapingOracle.Check>();
var soft = new List<ShapingOracle.Check>();
ShapingOracle.Check bookend;
{
Pass1Result pp1 = pass1[primary];
// (a1) curve off == Phase 1's own dump.
string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{primary}_full", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF == Phase-1 .f32 dump",
offs[primary].Height, HeightField.Load(p1Dump, mapSize), mapSize, p1Dump));
// (a2) staircase == task 01's own dump — the control is the control.
string t01Dump = Path.Combine(ToolingPaths.BatchesRoot, t01Source, $"{primary}_curve_on", "height.f32");
hard.Add(ShapingOracle.DumpRegression("a2", "staircase mode == task-01 curve_on .f32 dump",
results[(primary, "staircase")].Height, HeightField.Load(t01Dump, mapSize), mapSize, t01Dump));
// (b) classify == raw, every seed × every variant.
long bFail = 0;
foreach (int seed in seeds)
foreach (var (label, _) in variants)
{
var c = ShapingOracle.ClassifyFidelity(pass1[seed], results[(seed, label)]);
if (!c.Passed) { bFail++; GD.PrintErr($" classify drift: seed {seed} {label}: {c.Detail}"); }
}
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",
});
// (c) monotone — the staircase's sweep and the continuous strict-increase sample
// both throw on violation, so reaching here means they passed; state it explicitly.
bool cStair = results[(primary, "staircase")].Notes.Exists(n => n.Contains("Monotonicity assertion passed"));
bool cCont = results[(primary, "continuous_default")].Notes.Exists(n => n.Contains("strict-increase sample passed"));
hard.Add(new ShapingOracle.Check
{
Id = "c", Name = "monotone — staircase sweep + continuous strict-increase sample",
Passed = cStair && cCont,
Detail = $"staircase sweep {(cStair ? "confirmed" : "MISSING")}, " +
$"continuous sample {(cCont ? "confirmed" : "MISSING")} (both throw-and-refuse on violation)",
});
// (d) ⭐ lowlands preserved — every continuous variant, both seeds. HARD.
foreach (int seed in seeds)
foreach (string label in new[] { "continuous_default", "continuous_hold_higher", "continuous_dramatic_peak" })
{
var c = ShapingOracle.LowlandsPreserved(pass1[seed], results[(seed, "staircase")], results[(seed, label)]);
c.Name += $" [seed {seed}]";
hard.Add(c);
}
// (d, bookend) the WRONG lift is EXPECTED to fail this — its failure is the point.
bookend = ShapingOracle.LowlandsPreserved(pp1, results[(primary, "staircase")],
results[(primary, "continuous_lifted_WRONG")]);
// (e) upper climb profile — SOFT (exploration): warn loudly, do not gate the exit.
foreach (int seed in seeds)
foreach (string label in new[] { "continuous_default", "continuous_hold_higher", "continuous_dramatic_peak" })
{
var c = ShapingOracle.UpperClimbProfile(results[(seed, label)]);
c.Name += $" [seed {seed}]";
soft.Add(c);
}
// (f) sea identity — every variant, both seeds, per cell. HARD.
foreach (int seed in seeds)
foreach (var (label, _) in variants)
{
var c = ShapingOracle.SeaIdentity(offs[seed], results[(seed, label)], sea);
c.Name += $" [seed {seed}]";
hard.Add(c);
}
}
foreach (var c in hard) GD.Print(" " + c);
foreach (var c in soft)
{
if (c.Passed) GD.Print(" " + c);
else GD.PrintErr(" ⚠ SLOPE PROFILE: " + c);
}
GD.Print($" bookend (expected FAIL): {bookend}");
if (bookend.Passed)
GD.PrintErr(" ⚠⚠ the lifted_WRONG bookend PRESERVED the lowlands — it is not doing its job as a contrast.");
bool hardOk = hard.TrueForAll(c => c.Passed) && !bookend.Passed;
GD.Print($" ORACLE: {(hardOk ? "ALL HARD CHECKS PASS" : "*** HARD FAILURES ***")}" +
$"{(soft.TrueForAll(c => c.Passed) ? "" : " (soft slope warnings present see above)")}");
// ═══ 3. HISTOGRAMS — the contrast, adjacent by filename ═══
GD.Print("\n--- 3. HISTOGRAMS ---");
foreach (int seed in seeds)
{
var rawSeed = new LandHistogram(sea);
rawSeed.Accumulate(pass1[seed].Height, mapSize);
DrawRawHist(rawSeed, knots, seed, mapSize, batchRoot);
int order = 1;
foreach (var (label, _) in variants)
{
DrawShapedHist(results[(seed, label)], rawSeed, anchors, seed, mapSize, batchRoot, order++);
}
}
// ═══ 4. SHOWPIECE — continuous_default at the big profile ═══
string showNote = "skipped (ISLA_SHOWPIECE=0)";
if (showpiece)
{
GD.Print($"\n--- 4. SHOWPIECE at {showSize} (continuous_default, seed {primary}) ---");
var cfg = BaseConfig(showSize, primary, knots, anchors, "continuous_default_showpiece", off: false);
cfg.CurveMode = CurveModeKind.Continuous;
Pass1Result p1 = Topography.Generate(cfg);
GD.Print($" pass1 {showSize}: h[{p1.HMinSeed:F3} .. {p1.HMaxSeed:F3}] {p1.ElapsedMs} ms");
Pass2Result big = Shaping.Shape(p1, cfg);
foreach (string n in big.Notes) GD.Print(" " + n);
// The cheap checks that transfer to the big profile: classify fidelity + sea identity.
var cb = ShapingOracle.ClassifyFidelity(p1, big);
var offBig = Shaping.Shape(p1, BaseConfig(showSize, primary, knots, anchors, "off", off: true));
var cf = ShapingOracle.SeaIdentity(offBig, big, sea);
GD.Print($" {cb}");
GD.Print($" {cf}");
if (!cb.Passed || !cf.Passed) hardOk = false;
rows.Add(WriteVariant(batchRoot, big, sea, anchors, skipRaw));
showNote = $"seed {primary} at {showSize} — classify + sea identity re-verified there";
}
WriteIndex(batchRoot, mapSize, showSize, seeds, primary, knots, anchors,
results, hard, soft, bookend, rows, hardOk, showNote);
GD.Print("\n==================================================================");
GD.Print($" DONE — {batchRoot}");
GD.Print($" ORACLE {(hardOk ? "HARD CHECKS ALL PASS" : "*** HARD FAILURES see the table ***")}");
GD.Print("==================================================================");
GetTree().Quit(hardOk ? 0 : 3);
}
// ---- configs ----------------------------------------------------------
private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
string label, bool off) => new TerrainGenConfig
{
MapSize = mapSize, Seed = seed, VariantLabel = label,
Curve = !off, ShelfDetail = false, Knots = k, Anchors = a,
// Continuous knob defaults; variant mutators override per axis.
//
// ⚠ ENV-OVERRIDABLE so the knobs can be probed WITHOUT editing source and rebuilding —
// they are "eye-iteration knobs" per the task, and a knob you have to recompile to turn
// is not one. A probe run redirects ISLA_OUTPUT_DIR to scratch and sweeps these; the
// BATCH variants set them explicitly in their mutators, so a probe env cannot silently
// move the batch of record.
LowlandCeilingM = EnvFloat("ISLA_CEILING_M", 30f),
ClimbFeather = EnvFloat("ISLA_FEATHER", 0.4f),
SummitDrama = EnvFloat("ISLA_DRAMA", 2.5f),
};
// ---- output -----------------------------------------------------------
private static string WriteVariant(string batchRoot, Pass2Result p2, float sea,
CurveAnchors anchors, bool skipRaw)
{
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{LabelOf(p2)}");
DirAccess.MakeDirRecursiveAbsolute(dir);
// Plain data beside the pretty render, always.
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"));
// ⚠ HILLSHADED, deliberately — the continuous grade is the first terrain with coherent
// shape worth lighting (Phase 1's rule was "flat is the hero" BECAUSE raw noise fuzzes;
// that reasoning inverts once the climb is smooth). Subtle settings, and the palette is
// the PROVISIONAL even ramp — flagged as such in the palette, the INDEX and the report.
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);
Image withLegend = LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap,
LabelOf(p2).ToUpperInvariant());
withLegend.SavePng(Path.Combine(dir, "relief.png"));
float land = p2.LandFraction(sea);
GD.Print($" {LabelOf(p2),-26} seed {p2.Seed,-11} h[{p2.HMin,7:F3} .. {p2.HMax,6:F3}] " +
$" land {land * 100,5:F1}% {p2.ElapsedMs,5} ms");
return $"| `{p2.Seed}_{LabelOf(p2)}` | {p2.Seed} | {LabelOf(p2)} | {p2.HMin:F3} | {p2.HMax:F3} | " +
$"{land * 100:F1}% | {gMin:F3}..{gMax:F3} | {p2.ElapsedMs} ms |";
}
/// <summary>The variant folder name — read off the result, never re-derived.</summary>
private static string LabelOf(Pass2Result p2) => p2.VariantLabel;
private static void DrawRawHist(LandHistogram raw, CurveKnots k, int seed, int mapSize, string batchRoot)
{
float top = MathF.Ceiling(raw.MaxLand * 20f) / 20f;
var display = raw.Rebin((top - raw.SeaLevel) / 360f);
var o = new HistogramRenderer.Options
{
Title = $"RAW LAND HEIGHTS - SEED {seed}",
Subtitle = "PRE-CURVE. K2 IS THE LOWLAND CEILING - EVERYTHING LEFT OF IT IS PRESERVED.",
XAxisLabel = "RAW HEIGHT (PRE-CURVE)",
XTop = top,
Footer = $"{raw.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}",
};
o.Markers.Add(new HistogramRenderer.Marker { Value = k.K2, Label = "K2 LOWLAND CEIL" });
o.Bands.Add(new HistogramRenderer.Band
{
Lo = raw.SeaLevel, Hi = k.K2, Label = "preserved lowland",
SharePercent = raw.FractionBelow(k.K2) * 100.0,
});
o.Bands.Add(new HistogramRenderer.Band
{
Lo = k.K2, Hi = top, Label = "the climb's input",
SharePercent = (1.0 - raw.FractionBelow(k.K2)) * 100.0,
});
HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, $"hist_{seed}_0_raw.png"));
GD.Print($" hist_{seed}_0_raw.png");
}
private static void DrawShapedHist(Pass2Result p2, LandHistogram rawSeed, CurveAnchors a,
int seed, int mapSize, string batchRoot, int order)
{
var shaped = new LandHistogram(rawSeed.SeaLevel);
shaped.Accumulate(p2.Height, mapSize);
float top = MathF.Ceiling(shaped.MaxLand * 20f) / 20f;
var display = shaped.Rebin((top - shaped.SeaLevel) / 360f);
string label = LabelOf(p2);
var o = new HistogramRenderer.Options
{
Title = $"SHAPED - {label.ToUpperInvariant()} - SEED {seed}",
XAxisLabel = "RAW HEIGHT (POST-CURVE)",
XTop = top,
Footer = $"{shaped.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}",
};
if (p2.Continuous != null)
{
var c = p2.Continuous;
var (onsetRaw, onsetOut) = c.SummitOnsetPoint();
o.Subtitle = "TARGET: BOTTOM PILE UNMOVED - ABOVE IT ONE SMOOTH FALLOFF TO A THIN PEAK.";
o.Markers.Add(new HistogramRenderer.Marker { Value = c.CeilingOut, Label = $"LOWLAND {c.LowlandCeilingM:F0}M" });
o.Markers.Add(new HistogramRenderer.Marker { Value = onsetOut, Label = "SUMMIT ONSET", Strong = false });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M" });
o.Bands.Add(new HistogramRenderer.Band
{
Lo = rawSeed.SeaLevel, Hi = c.CeilingOut, Label = "preserved lowland",
SharePercent = rawSeed.FractionBelow(c.CeilingRaw) * 100.0,
});
o.Bands.Add(new HistogramRenderer.Band
{
Lo = c.CeilingOut, Hi = onsetOut, Label = "the climb",
SharePercent = rawSeed.FractionBetween(c.CeilingRaw, onsetRaw) * 100.0,
});
o.Bands.Add(new HistogramRenderer.Band
{
Lo = onsetOut, Hi = top, Label = "summit",
SharePercent = (1.0 - rawSeed.FractionBelow(onsetRaw)) * 100.0,
});
}
else if (p2.CurveModeLabel == "staircase")
{
o.Subtitle = "THE CONTROL: THE M3 STAIRCASE - BENCH AND PLATEAU SPIKES BY DESIGN.";
o.Markers.Add(new HistogramRenderer.Marker { Value = a.OrangeCeil, Label = "ORANGE", Strong = false });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.RedCeil, Label = "RED", Strong = false });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.BenchBase, Label = "BENCH" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PlateauBase, Label = "PLATEAU" });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M" });
}
else
{
o.Subtitle = "THE WRONG DIRECTION: THE WHOLE ISLAND LIFTED OFF ITS SHORELINE. A BOOKEND.";
o.Markers.Add(new HistogramRenderer.Marker { Value = a.RedCeil, Label = "OLD 30M LINE", Strong = false });
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M" });
}
string file = $"hist_{seed}_{order}_{label}.png";
HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, file));
GD.Print($" {file}");
}
// ---- the index ----------------------------------------------------------
private static void WriteIndex(string batchRoot, int mapSize, int showSize, int[] seeds,
int primary, CurveKnots knots, CurveAnchors a,
Dictionary<(int, string), Pass2Result> results,
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> soft,
ShapingOracle.Check bookend, List<string> rows, bool hardOk, string showNote)
{
var sb = new StringBuilder();
sb.AppendLine("# Batch 02 — continuous grade: smooth the upper staircase, preserve the lowlands");
sb.AppendLine();
sb.AppendLine("**rev 3.** The lowlands are GOOD and are preserved bit-for-bit (oracle d). Everything");
sb.AppendLine("above the 30 m flood line is replaced by one smooth monotone climb to the 420 m cap.");
sb.AppendLine("The staircase rides along as the control; the WRONG-direction whole-island lift rides");
sb.AppendLine("along as a contrast bookend. **Exploration, not convergence** — a tuning pass follows");
sb.AppendLine("once a direction is picked.");
sb.AppendLine();
sb.AppendLine("## ⭐ Open this first");
sb.AppendLine();
sb.AppendLine($"1. **`{primary}_continuous_default_showpiece/relief.png`** — the centerpiece ({showNote}).");
sb.AppendLine($"2. **`hist_{primary}_1_staircase.png` → `hist_{primary}_2_continuous_default.png`** — the");
sb.AppendLine(" contrast, adjacent by filename: bottom pile unchanged, the bench/plateau spikes above");
sb.AppendLine(" it dissolved into a smooth falloff.");
sb.AppendLine($"3. **`hist_{primary}_5_continuous_lifted_WRONG.png`** — the bookend: the whole pile shoved");
sb.AppendLine(" up off the shoreline. This is what \"make the island stand up\" would have done.");
sb.AppendLine($"4. `{primary}_staircase/relief.png` vs `{primary}_continuous_default/relief.png` — the A/B.");
sb.AppendLine();
sb.AppendLine("## The variants and their knobs");
sb.AppendLine();
sb.AppendLine("| Variant | Mode | Knobs | Control points |");
sb.AppendLine("|---|---|---|---|");
sb.AppendLine("| `staircase` | staircase | task-01 defaults (ShelfDetail on) | the v5 bands |");
foreach (string label in new[] { "continuous_default", "continuous_hold_higher", "continuous_dramatic_peak" })
{
var c = results[(primary, label)].Continuous;
sb.AppendLine($"| `{label}` | continuous | ceiling {c.LowlandCeilingM:F0} m · feather {c.ClimbFeather:F2} · " +
$"drama {c.SummitDrama:F2} | {c.DescribeControlPoints()} |");
}
sb.AppendLine("| `continuous_lifted_WRONG` | lifted | none — an even ×~1.47 lift of all land | n/a |");
sb.AppendLine();
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
sb.AppendLine();
sb.AppendLine("`ProvisionalEven` — the CostaRica colours re-spaced **evenly** SEA → 420 m, so equal");
sb.AppendLine("colour = equal height and nothing is emphasized. Final palette calibration waits for the");
sb.AppendLine("chosen curve 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 slope profile (e) — warn-and-report, exploration:");
sb.AppendLine();
sb.AppendLine(ShapingOracle.ToMarkdownTable(soft));
sb.AppendLine($"**The bookend, expected to FAIL (d):** {bookend.Detail}");
sb.AppendLine();
sb.AppendLine("## Disposability");
sb.AppendLine();
sb.AppendLine("| Artifact | Keep? |");
sb.AppendLine("|---|---|");
sb.AppendLine("| `relief.png`, `hist_*.png`, `INDEX.md` | **keep** — the judging plates and the finding |");
sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code (it is 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 | land % | grayscale range | time |");
sb.AppendLine("|---|---|---|---|---|---|---|---|");
foreach (string row in rows) sb.AppendLine(row);
sb.AppendLine();
sb.AppendLine($"Setup: MapSize {mapSize}, showpiece {showSize}, seeds {string.Join(", ", seeds)}, " +
$"knots re-measured on the task-01 pool. {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://dmnl1sp8vyioe

View file

@ -58,6 +58,31 @@ namespace IslaApocalypse.Tools
/// <summary>Was the curve applied? The primary A/B gate.</summary>
public readonly bool CurveOn;
/// <summary>
/// Which curve shaped the render field: "off", "staircase", "continuous" or "lifted_WRONG".
/// A label, not logic — the oracle and the INDEX read it so a result can never be mistaken
/// for the wrong mode's.
/// </summary>
public readonly string CurveModeLabel;
/// <summary>
/// The batch variant this result belongs to, carried straight from
/// <see cref="TerrainGenConfig.VariantLabel"/>.
///
/// ⚠ CARRIED, NOT INFERRED. The first cut of the chat2/02 tool reconstructed this from the
/// knob values ("ceiling &gt; 31 ⇒ hold_higher"), which works only for exactly today's
/// variant set: add a second variant sharing a knob value and two different runs silently
/// write to one folder. The label is an identity, so it travels with the result.
/// </summary>
public readonly string VariantLabel;
/// <summary>
/// The per-seed continuous spline, when <see cref="CurveModeLabel"/> is "continuous" — the
/// oracle samples its slopes (check e) and the INDEX prints its control points. Null for
/// every other mode.
/// </summary>
public readonly ContinuousCurve Continuous;
/// <summary>Was shelf detail applied? Requires <see cref="CurveOn"/> — it warps the curve's knots.</summary>
public readonly bool DetailOn;
@ -90,7 +115,8 @@ namespace IslaApocalypse.Tools
public readonly List<string> Notes;
public Pass2Result(int mapSize, int seed, float[,] height, float[,] heightClassify,
bool curveOn, bool detailOn, CurveKnots knots, CurveAnchors anchors, float hMaxSeed,
bool curveOn, bool detailOn, string curveModeLabel, string variantLabel,
ContinuousCurve continuous, CurveKnots knots, CurveAnchors anchors, float hMaxSeed,
float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs,
List<string> notes)
{
@ -100,6 +126,9 @@ namespace IslaApocalypse.Tools
HeightClassify = heightClassify;
CurveOn = curveOn;
DetailOn = detailOn;
CurveModeLabel = curveModeLabel;
VariantLabel = variantLabel;
Continuous = continuous;
Knots = knots;
Anchors = anchors;
HMaxSeed = hMaxSeed;

View file

@ -46,8 +46,11 @@ namespace IslaApocalypse.Tools
/// </summary>
public static class ReliefPalette
{
/// <summary>Named palettes. Gated so the look can be A/B'd like any other change.</summary>
public enum Kind { Atlas, Dusk, CostaRica }
/// <summary>
/// Named palettes. Gated so the look can be A/B'd like any other change.
/// ⚠ ProvisionalEven is chat2/02's stopgap for the continuous curve — see its array's note.
/// </summary>
public enum Kind { Atlas, Dusk, CostaRica, ProvisionalEven }
/// <summary>
/// Bathymetric compression constant, in raw height units. The sea ramp is indexed by
@ -153,6 +156,29 @@ namespace IslaApocalypse.Tools
(1.000f, new Color(0.016f, 0.063f, 0.165f)), // abyss — flat
};
// ---- PROVISIONAL EVEN (chat2/02): the CostaRica colours, re-spaced evenly SEA → PEAK_CAP --
//
// ⚠⚠ PROVISIONAL, AND FLAGGED AS SUCH EVERYWHERE IT APPEARS. The continuous curve spreads
// land across the whole 0420 m band, so the raw-percentile CostaRica stops no longer sit
// where the land is. This ramp is deliberately UNOPINIONATED: the same colour sequence at
// EVEN height spacing, so equal colour distance = equal height distance and nothing is
// emphasized. Final palette calibration waits until a curve profile is CHOSEN — placing
// stops on a distribution still being explored would bake taste into the instrument.
// Grayscale + the histogram stay the honest instruments; this is only so the relief plates
// are readable meanwhile.
private static readonly (float h, Color c)[] ProvisionalEvenLand = BuildProvisionalEven();
private static (float h, Color c)[] BuildProvisionalEven()
{
// SEA → PEAK_CAP through the single yardstick — no new literal for the cap.
float lo = 0.15f;
float hi = 0.15f + IslaApocalypse.Core.WorldScale.RawFromMetres(420f);
var stops = new (float h, Color c)[CostaRicaLand.Length];
for (int i = 0; i < CostaRicaLand.Length; i++)
stops[i] = (lo + (hi - lo) * i / (CostaRicaLand.Length - 1), CostaRicaLand[i].c);
return stops;
}
/// <summary>The hypsometric tint for a height, before any relief shading.</summary>
public static Color Tint(Kind kind, float height, float seaLevel)
{
@ -169,9 +195,10 @@ namespace IslaApocalypse.Tools
private static ((float h, Color c)[] land, (float h, Color c)[] sea) Ramps(Kind kind) => kind switch
{
Kind.Dusk => (DuskLand, DuskSea),
Kind.CostaRica => (CostaRicaLand, CostaRicaSea),
_ => (AtlasLand, AtlasSea),
Kind.Dusk => (DuskLand, DuskSea),
Kind.CostaRica => (CostaRicaLand, CostaRicaSea),
Kind.ProvisionalEven => (ProvisionalEvenLand, CostaRicaSea), // same sea; land re-spaced
_ => (AtlasLand, AtlasSea),
};
/// <summary>The land stops of a palette, for drawing a legend.</summary>

View file

@ -71,11 +71,21 @@ namespace IslaApocalypse.Tools
if (cfg.ShelfDetail)
notes.Add("[Shaping] shelf detail requested but the curve is off — no-op (detail warps the curve's knots).");
return new Pass2Result(mapSize, p1.Seed, p1.Height, p1.Height,
curveOn: false, detailOn: false, knots: null, anchors: null, hMaxSeed: p1.HMaxSeed,
curveOn: false, detailOn: false, curveModeLabel: "off", variantLabel: cfg.VariantLabel, continuous: null,
knots: null, anchors: null, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: p1.HMinSeed, hMax: p1.HMaxSeed,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes);
}
// ═══ WHICH CURVE (chat2/02) ═══
//
// Staircase falls through to the faithful task-01 path below, UNTOUCHED — it is the
// control, and controls do not get refactored while they are being compared against.
if (cfg.CurveMode == CurveModeKind.Continuous)
return ShapeContinuous(p1, cfg, notes, t0);
if (cfg.CurveMode == CurveModeKind.LiftedWrong)
return ShapeLifted(p1, cfg, notes, t0);
CurveKnots knots = cfg.Knots;
CurveAnchors anchors = cfg.Anchors;
GenerationScale scale = cfg.Scale;
@ -197,9 +207,128 @@ namespace IslaApocalypse.Tools
// property that makes it an oracle.
return new Pass2Result(mapSize, p1.Seed, height, classify,
curveOn: true, detailOn: detailOn, knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
curveOn: true, detailOn: detailOn, curveModeLabel: "staircase", variantLabel: cfg.VariantLabel, continuous: null,
knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: edgeAmpRaw, maxEdgeShiftRaw: maxEdgeShiftRaw, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes);
}
/// <summary>
/// ⭐ THE CONTINUOUS MODE (chat2/02): the staircase's toe+red lowland preserved bit-for-bit,
/// everything above it replaced by one smooth monotone FritschCarlson climb.
/// → <see cref="ContinuousCurve"/> for the shape and its refusals.
///
/// ═══ ⚠ WHY THIS LOOP IS SO MUCH SIMPLER THAN THE STAIRCASE'S ═══
///
/// No bench/plateau/strength modulation fields — the anchors they modulated are dropped.
/// No shelf detail — the flat benches it existed to de-slab no longer exist above the
/// lowlands, and the preserved lowland never needed it. Micro-relief/roughness on the
/// continuous upper grade is DEFERRED: erosion is the real detail source, and painting
/// noise on the climb now would pre-judge what erosion should carve. So: one spline per
/// seed, one Apply per column, and the classify field untouched as always.
/// </summary>
private static Pass2Result ShapeContinuous(Pass1Result p1, TerrainGenConfig cfg,
List<string> notes, ulong t0)
{
int mapSize = p1.MapSize;
CurveKnots knots = cfg.Knots;
CurveAnchors anchors = cfg.Anchors;
// Same per-seed summit normalization as the staircase — the pass-1/pass-2 boundary
// 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,
cfg.LowlandCeilingM, cfg.ClimbFeather, cfg.SummitDrama);
// Monotone by construction — and proven anyway, per seed, because "cannot fail" is
// exactly the claim worth a millisecond of checking.
notes.Add(curve.AssertStrictlyIncreasing());
notes.Add($"[Shaping] continuous: {curve.DescribeControlPoints()}");
if (cfg.ShelfDetail)
notes.Add("[Shaping] ⚠ shelf detail requested but FORCED OFF in continuous mode — the flat " +
"benches it de-slabbed no longer exist, and detail on the climb is deferred to erosion.");
var height = new float[mapSize, mapSize];
var classify = new float[mapSize, mapSize];
float hMin = float.MaxValue, hMax = float.MinValue;
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
float raw = p1.Height[x, y];
classify[x, y] = raw; // ⭐ the oracle field, as always
float h = curve.Apply(raw); // sea identity + lowland delegation
// + climb + tail, all inside
if (h < hMin) hMin = h;
if (h > hMax) hMax = h;
height[x, y] = h;
}
}
return new Pass2Result(mapSize, p1.Seed, height, classify,
curveOn: true, detailOn: false, curveModeLabel: "continuous", variantLabel: cfg.VariantLabel, continuous: curve,
knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes);
}
/// <summary>
/// ⚠⚠ THE DELIBERATELY-WRONG BOOKEND (chat2/02): an even linear remap of ALL land —
/// <c>[SEA, spikeMax] → [SEA, PEAK_CAP]</c>. Every land column above sea is lifted by the
/// same ×~1.5 factor, which hoists the entire island off its shoreline and destroys the
/// broad low plain the developer likes.
///
/// It exists so the preserved-lowland variants can be seen AGAINST the mistake — the batch's
/// contrast anchor, per the rev-3 task. It is not a candidate, it takes no knobs, and its
/// oracle-(d) FAILURE is expected and reported as confirmation, not as a bug.
/// </summary>
private static Pass2Result ShapeLifted(Pass1Result p1, TerrainGenConfig cfg,
List<string> notes, ulong t0)
{
int mapSize = p1.MapSize;
CurveKnots knots = cfg.Knots;
CurveAnchors anchors = cfg.Anchors;
float spikeMax = HeightCurve.EffectiveSpikeMax(p1.HMaxSeed, knots, anchors);
float scale = (anchors.PeakCap - anchors.Sea) / (spikeMax - anchors.Sea);
notes.Add($"[Shaping] ⚠⚠ LIFTED_WRONG: even remap [sea, {spikeMax:F4}] → [sea, {anchors.PeakCap:F4}] " +
$"(×{scale:F3} on every land height) — the WRONG direction, kept as the contrast bookend.");
var height = new float[mapSize, mapSize];
var classify = new float[mapSize, mapSize];
float hMin = float.MaxValue, hMax = float.MinValue;
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
float raw = p1.Height[x, y];
classify[x, y] = raw;
float h;
if (raw <= anchors.Sea) h = raw; // even the wrong direction keeps the
// coastline — sea identity is not optional
else if (raw >= spikeMax) h = anchors.PeakCap + (raw - spikeMax) * anchors.TailSlope;
else h = anchors.Sea + (raw - anchors.Sea) * scale;
if (h < hMin) hMin = h;
if (h > hMax) hMax = h;
height[x, y] = h;
}
}
return new Pass2Result(mapSize, p1.Seed, height, classify,
curveOn: true, detailOn: false, curveModeLabel: "lifted_WRONG", variantLabel: cfg.VariantLabel, continuous: null,
knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes);
}
}
}

View file

@ -224,6 +224,131 @@ namespace IslaApocalypse.Tools
return shares;
}
/// <summary>
/// A bit-regression against any `.f32` dump, with the caller naming the check — the
/// chat2/02 generalization of <see cref="RegressionAgainstDump"/>, used to hold the
/// staircase mode against task 01's own batch output. The same rule applies: a missing dump
/// is INCONCLUSIVE and counted as a failure, never as a pass.
/// </summary>
public static Check DumpRegression(string id, string name, float[,] current, float[,] dump,
int mapSize, string dumpPath)
{
var c = new Check { Id = id, Name = name };
if (dump == null)
{
c.Passed = false;
c.Detail = $"INCONCLUSIVE — no readable dump at {dumpPath} for this seed/size. Not counted as a pass.";
return c;
}
var (differing, firstDiff) = CompareBitwise(dump, current, mapSize);
c.Passed = differing == 0;
c.Detail = c.Passed
? $"bit-identical to {dumpPath} over {(long)mapSize * mapSize:N0} cells"
: $"{differing:N0} cells differ from {dumpPath} — {firstDiff}";
return c;
}
/// <summary>
/// (d) ⭐ LOWLANDS PRESERVED — the rev-3 task's load-bearing check. For every cell whose RAW
/// height is at or below K2 (the toe+red band the developer likes), the continuous mode's
/// output must be BIT-IDENTICAL to the staircase's. This mechanically enforces "do not lift
/// the lowlands": the low pile cannot move if its every column is the same float.
///
/// The identity is by construction — the continuous curve DELEGATES to the staircase's own
/// toe+red code path below K2 — and this check is what keeps that construction honest
/// against refactoring, float re-association, or a ceiling knob bug.
///
/// ⚠ Run against the LIFTED_WRONG bookend this check is EXPECTED to fail — the caller
/// reports that failure as confirmation of the wrong direction, not as a defect.
/// </summary>
public static Check LowlandsPreserved(Pass1Result p1, Pass2Result staircase, Pass2Result variant)
{
var c = new Check { Id = "d", Name = $"lowlands (raw ≤ K2) bit-identical to staircase [{variant.VariantLabel}]" };
float k2 = staircase.Knots.K2;
long compared = 0, differing = 0;
string first = null;
for (int x = 0; x < p1.MapSize; x++)
{
for (int y = 0; y < p1.MapSize; y++)
{
if (p1.Height[x, y] > k2) continue;
compared++;
int ba = BitConverter.SingleToInt32Bits(staircase.Height[x, y]);
int bb = BitConverter.SingleToInt32Bits(variant.Height[x, y]);
if (ba == bb) continue;
differing++;
first ??= $"first at [{x},{y}] raw {p1.Height[x, y]:G9}: " +
$"staircase {staircase.Height[x, y]:G9} vs {variant.Height[x, y]:G9}";
}
}
c.Passed = differing == 0;
c.Detail = c.Passed
? $"bit-identical over all {compared:N0} lowland cells (raw ≤ K2 = {k2:F6})"
: $"{differing:N0} of {compared:N0} lowland cells differ — {first}";
return c;
}
/// <summary>
/// (e) UPPER CONTINUOUS — sample the climb's slope densely; no near-flat anywhere (a bench
/// reborn), no cliff below the summit onset (the summit itself may steepen — that is the
/// dramatic peak). ⚠ SOFT: this is an exploration batch, so a violation warns loudly and
/// lands in the report rather than failing the run — the tool decides the exit code.
/// </summary>
public static Check UpperClimbProfile(Pass2Result p2)
{
var c = new Check { Id = "e", Name = $"upper climb continuous — no flats, no low-mid cliffs [{p2.VariantLabel}]" };
if (p2.Continuous == null)
{
c.Passed = false;
c.Detail = "no continuous spline on this result — wrong mode handed in.";
return c;
}
var (minN, minAt, maxN, maxAt, nearFlat, cliff) = p2.Continuous.SampleClimbSlopes();
c.Passed = !nearFlat && !cliff;
c.Detail = $"slope (normalized, 1 = climb average): min {minN:F3} at raw {minAt:F4}" +
$"{(nearFlat ? " NEAR-FLAT (a bench reborn)" : "")}, " +
$"max below onset {maxN:F3} at raw {maxAt:F4}" +
$"{(cliff ? " CLIFF below the summit onset" : "")}";
return c;
}
/// <summary>
/// (f) SEA IDENTITY — per cell, not per count: a column is land in the variant exactly when
/// it is land with the curve off. Counting alone could hide two errors that cancel; this
/// cannot. The coastline is the one thing every mode, including the wrong one, must keep.
/// </summary>
public static Check SeaIdentity(Pass2Result off, Pass2Result variant, float seaLevel)
{
var c = new Check { Id = "f", Name = $"sea identity — per-cell landness unchanged [{variant.VariantLabel}]" };
long mismatches = 0;
string first = null;
for (int x = 0; x < off.MapSize; x++)
{
for (int y = 0; y < off.MapSize; y++)
{
bool a = off.Height[x, y] >= seaLevel;
bool b = variant.Height[x, y] >= seaLevel;
if (a == b) continue;
mismatches++;
first ??= $"first at [{x},{y}]: off {off.Height[x, y]:G9} vs {variant.Height[x, y]:G9}";
}
}
c.Passed = mismatches == 0;
c.Detail = c.Passed
? $"per-cell landness identical over {(long)off.MapSize * off.MapSize:N0} cells"
: $"{mismatches:N0} cells changed sides of the waterline — {first}";
return c;
}
/// <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

@ -2,6 +2,22 @@ using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// Which redistribution curve pass 2a applies (chat2/02). One seam, three occupants:
///
/// Staircase ⭐ the faithful v5 port (task 01) — toe/red/riser/bench/riser/plateau/spike.
/// THE CONTROL. Bit-identical to task 01's output, always present in a batch.
/// Continuous ⭐ the rev-3 redesign — the staircase's toe+red lowland PRESERVED bit-for-bit,
/// everything above it replaced by one smooth monotone climb to the 420 m cap.
/// → Core/ContinuousCurve.
/// LiftedWrong ⚠⚠ DELIBERATELY THE WRONG DIRECTION — an even linear remap of ALL land onto
/// [SEA, PEAK_CAP], which lifts the entire island off its shoreline and destroys
/// the low plain the developer likes. It exists as a CONTRAST BOOKEND so the
/// preserved-lowland variants can be judged against the mistake, and for no other
/// purpose. Do not ship it, do not tune it, do not "fix" it.
/// </summary>
public enum CurveModeKind { Staircase, Continuous, LiftedWrong }
/// <summary>
/// The generator's configuration, including the per-element ABLATION TOGGLES.
///
@ -98,6 +114,44 @@ namespace IslaApocalypse.Tools
/// </summary>
public bool Curve = true;
/// <summary>
/// ⭐ WHICH curve (chat2/02). → <see cref="CurveModeKind"/>.
///
/// Default CONTINUOUS per the rev-3 task — the exploration direction. Tools that exist to
/// reproduce the task-01 staircase (CurveBaselineTool) set Staircase EXPLICITLY, so the
/// default changing does not silently move a control batch.
/// </summary>
public CurveModeKind CurveMode = CurveModeKind.Continuous;
// ---- the continuous climb's knobs — ALL act above the lowland ceiling only ----
/// <summary>
/// How high the preserved lowland holds before the climb takes over, in METRES of output
/// height above sea. Default 30 = RED_CEIL, the flood line — the exact top of the
/// staircase's toe+red band, so nothing at all is re-mapped below it.
///
/// ⚠ Constrained to [30, 80] m by <see cref="ContinuousCurve.Build"/>: below 30 would cut
/// the preserved band; at ~100 it could preserve a flat bench, the artifact this mode
/// exists to remove. Raising it extends the red band's gentle grade linearly before the
/// climb begins. THE PRIMARY VARIANT AXIS.
/// </summary>
public float LowlandCeilingM = 30f;
/// <summary>
/// The shape of the climb's departure from the lowland, 0..1: how long it hugs the red
/// band's exit slope before steepening. Replaces the old ambiguous "bow". Acts only above
/// the ceiling; CANNOT touch the low band.
/// </summary>
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.
/// </summary>
public float SummitDrama = 2.5f;
/// <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