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:
parent
35b4818e9e
commit
8e55326a84
11 changed files with 1382 additions and 8 deletions
418
Core/Scripts/ContinuousCurve.cs
Normal file
418
Core/Scripts/ContinuousCurve.cs
Normal 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 < 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/ContinuousCurve.cs.uid
Normal file
1
Core/Scripts/ContinuousCurve.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cfyqb4kr3w1mu
|
||||
6
Tools/Scenes/CurveContinuousTool.tscn
Normal file
6
Tools/Scenes/CurveContinuousTool.tscn
Normal 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")
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
580
Tools/Scripts/CurveContinuousTool.cs
Normal file
580
Tools/Scripts/CurveContinuousTool.cs
Normal 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 <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Tools/Scripts/CurveContinuousTool.cs.uid
Normal file
1
Tools/Scripts/CurveContinuousTool.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dmnl1sp8vyioe
|
||||
|
|
@ -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 > 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;
|
||||
|
|
|
|||
|
|
@ -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 0–420 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>
|
||||
|
|
|
|||
|
|
@ -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 Fritsch–Carlson 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue