feat: curve v4 — spatially modulated shelves (terrain-water task 08)
Per the task-07 gate: the terraces work but uniform anchors put a flat ring at exactly 100 m and 220 m on every mountain. v4 keeps v3's structure (frozen toe/rise, three risers, per-seed u4 spike to 420 m) and turns the shelf anchors into smooth spatial fields: bench 100+-12 m and plateau 220+-20 m via two decorrelated very-low-freq Simplex fields (~3 undulations per island width), plus a strength field (~5/island) blending each shelf's output span between ~2 m (pronounced flat) and ~25 m (barely a hint) — the optional strength modulation shipped, ordering-safe by construction (worst-case bench top 0.6962 < plateau min 0.9468). Field seeds derive from the RESOLVED noise seed + fixed offsets (7101/7207/7303) — no config knob. Apply is per-column with all modulated values as pure parameters (D-035); the classify path never sees them. Monotonicity assertion now sweeps all 8 modulation-extreme corners x per-seed spikeMax. TerrainCurve gate "off"|"v4" (default v4); v1-v3 retired loudly. TCRV extended (+36 B when version>=4: amplitudes, span range, frequencies, seed offsets — length-framed section makes the layout change safe; parser dispatches on the curve version byte; harness compares the extension). Note: MapGenerator.cs also carries the developer's editor whitespace normalization (spaces->tabs, git diff -w empty before this change). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a046486a58
commit
4d2e024b2f
6 changed files with 292 additions and 183 deletions
|
|
@ -1,75 +1,89 @@
|
|||
using Godot;
|
||||
|
||||
/// <summary>
|
||||
/// The height-redistribution curve, v3 — the TERRACED ASCENT (terrain-water task 07).
|
||||
/// Pure, static, monotonic piecewise map over raw blueprint heights (D-035: numbers
|
||||
/// in, numbers out; the per-seed spike maximum is an explicit PARAMETER).
|
||||
/// The height-redistribution curve, v4 — SPATIALLY MODULATED SHELVES (terrain-water
|
||||
/// task 08). Pure, static, monotonic piecewise map; every per-column input is an
|
||||
/// explicit PARAMETER (D-035): the seed spike max and the four modulated shelf
|
||||
/// values. The generator samples the modulation fields; this class never touches
|
||||
/// noise.
|
||||
///
|
||||
/// v3 (developer's task-06 ground-test verdict — floor and 420 m ceiling frozen, the
|
||||
/// ascent between them rebuilt): v2 spent ~87 % of the vertical budget in the last
|
||||
/// ~4 % of input, reading as flat-then-wall. v3 climbs in TERRACES — two benches,
|
||||
/// three risers — and relocates the white mountain-town plateau from 50 m to 220 m:
|
||||
/// v4 (developer's task-07 gate finding: the terraces work, but uniform anchors put
|
||||
/// a flat ring at exactly 100 m and exactly 220 m on every mountain — the bathtub
|
||||
/// rings): the bench and plateau OUTPUT anchors become smooth spatial fields —
|
||||
/// benchLo = 100 m ± 12 m and plateauLo = 220 m ± 20 m via two decorrelated
|
||||
/// very-low-frequency noise fields — and a third field modulates SHELF STRENGTH,
|
||||
/// blending each shelf between "pronounced flat" (output span ~2 m) and "barely a
|
||||
/// hint" (~25 m of gentle slope), so not every flank at shelf height develops the
|
||||
/// full terrace. Shelves stay locally flat; the two magic altitudes stop existing.
|
||||
///
|
||||
/// h ≤ sea (0.15) identity — water and the below-sea world untouched
|
||||
/// sea → K1 toe, ease-out — the lowlands (orange coverage 59 %)
|
||||
/// K1 → K2 linear rise into the red band (storm anchors frozen)
|
||||
/// K2 → K3 FOOTHILL RISER — smoothstep climb to the 100 m bench
|
||||
/// K3 → K4 BENCH 1 — near-flat walkable foothill shelf (~2 m rise)
|
||||
/// K4 → K5 MID RISER — the big middle climb to the white plateau
|
||||
/// K5 → K6 WHITE PLATEAU — near-flat shelf at 220 m (mountain
|
||||
/// towns / snow band, where it was always intended)
|
||||
/// K6 → spikeMax SUMMIT SPIKE — per-seed normalized (hMaxSeed), stiff
|
||||
/// ease-in kept from v2: spires off the plateau to 420 m
|
||||
/// h > spikeMax linear tail (strict monotonicity, no clamp)
|
||||
/// Structure otherwise v3's, unchanged: identity ≤ sea, frozen toe/rise (storm
|
||||
/// ladder), foothill riser → bench → mid riser → plateau → per-seed-normalized
|
||||
/// stiff spike (u⁴, 420 m cap) → tail. Input knots are v3's calibration.
|
||||
///
|
||||
/// Input knots recalibrated 2026-08-08 from the SAME pooled batch-04 flat-sea land
|
||||
/// CDF as v1/v2 (340,618,126 samples): P59/P72/P82/P87/P95/P98, giving pooled land
|
||||
/// fractions orange 59 / red 13 / foothill-riser 10 / bench 5 / mid-riser 8 /
|
||||
/// plateau 3 / spike 2 (exact by construction). Per-seed proportions vary — the
|
||||
/// wrinkle variety.
|
||||
///
|
||||
/// Strictly monotonic (every segment's normalized slope bounded below by a positive
|
||||
/// constant); asserted numerically per generation against the EFFECTIVE per-seed
|
||||
/// curve once hMaxSeed is known.
|
||||
/// ORDERING SAFETY BY CONSTRUCTION (asserted): with the amplitudes below, at every
|
||||
/// column: red ceiling 0.27 < benchLo−… (bench min 0.5006), bench top max 0.6962 <
|
||||
/// plateauLo min 0.9468, plateau top max 1.2062 < peak cap 1.8287. The numeric
|
||||
/// assertion additionally sweeps all 8 modulation-extreme corners per generation.
|
||||
/// </summary>
|
||||
public static class HeightCurve
|
||||
{
|
||||
public const ushort VERSION = 3;
|
||||
public const ushort VERSION = 4;
|
||||
|
||||
// Input knots — v3 calibration (see class header). K1..K4 are serialized in
|
||||
// TCRV's four knot slots; K5/K6 are version constants documented in
|
||||
// BLUEPRINT_FORMAT.md (the TCRV version byte selects the anchor set).
|
||||
public const float K1 = 0.509179f; // P59 — orange coverage boundary
|
||||
public const float K2 = 0.604081f; // P72 — red top
|
||||
public const float K3 = 0.698485f; // P82 — foothill riser top
|
||||
public const float K4 = 0.767213f; // P87 — bench 1 top
|
||||
public const float K5 = 0.930304f; // P95 — mid riser top
|
||||
public const float K6 = 1.050720f; // P98 — plateau top / spike foot
|
||||
// Input knots — v3 calibration (pooled batch-04 land CDF, P59/72/82/87/95/98).
|
||||
public const float K1 = 0.509179f;
|
||||
public const float K2 = 0.604081f;
|
||||
public const float K3 = 0.698485f;
|
||||
public const float K4 = 0.767213f;
|
||||
public const float K5 = 0.930304f;
|
||||
public const float K6 = 1.050720f;
|
||||
|
||||
// Output anchors — storm ladder (frozen) + the terraces.
|
||||
// Fixed output anchors — storm ladder + ceiling (frozen).
|
||||
public const float SEA = 0.15f;
|
||||
public const float ORANGE_CEIL = 0.206f; // 1000-yr storm ceiling (frozen)
|
||||
public const float RED_CEIL = 0.27f; // biblical ceiling (frozen)
|
||||
public const float FOOTHILL_BENCH = SEA + 100f / 251f; // ≈ 0.54841 (100 m above sea)
|
||||
public const float BENCH_STEP = 0.008f; // ~2 m rise across each bench
|
||||
public const float FOOTHILL_TOP = FOOTHILL_BENCH + BENCH_STEP;
|
||||
public const float PLATEAU = SEA + 220f / 251f; // ≈ 1.02649 (220 m — the white plateau)
|
||||
public const float PLATEAU_TOP = PLATEAU + BENCH_STEP;
|
||||
public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869 (frozen from v2)
|
||||
public const float ORANGE_CEIL = 0.206f;
|
||||
public const float RED_CEIL = 0.27f;
|
||||
public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869
|
||||
public const float TAIL_SLOPE = 0.25f;
|
||||
|
||||
// Degenerate/near-flat guard (unchanged from v2): spike domain floored at
|
||||
// K6 + SPIKE_MIN_SPAN so it stays positive and monotonic on any input.
|
||||
public const float SPIKE_MIN_SPAN = 0.01f;
|
||||
|
||||
// Modulated shelf anchors: base ± amplitude (raw units; 251 m per unit).
|
||||
public const float BENCH_BASE = SEA + 100f / 251f; // ≈ 0.54841 (100 m)
|
||||
public const float BENCH_AMP = 12f / 251f; // ± 12 m
|
||||
public const float PLATEAU_BASE = SEA + 220f / 251f; // ≈ 1.02649 (220 m)
|
||||
public const float PLATEAU_AMP = 20f / 251f; // ± 20 m
|
||||
|
||||
// Shelf strength: output span of each shelf segment, blended by the strength
|
||||
// field. Strong (t=1) → SPAN_MIN (~2 m, pronounced flat). Weak (t=0) →
|
||||
// SPAN_MAX (~25 m, barely a hint of a shelf).
|
||||
public const float SHELF_SPAN_MIN = 0.008f;
|
||||
public const float SHELF_SPAN_MAX = 0.10f;
|
||||
|
||||
// Modulation-field derivation (generator-side, recorded in TCRV): field seed =
|
||||
// RESOLVED WorldSeed + offset; frequency = (periods per island width) / MapSize.
|
||||
public const int BENCH_SEED_OFFSET = 7101;
|
||||
public const int PLATEAU_SEED_OFFSET = 7207;
|
||||
public const int STRENGTH_SEED_OFFSET = 7303;
|
||||
public const float ELEV_FREQ_ISLANDS = 3.0f; // ~3 undulations across the island
|
||||
public const float STRENGTH_FREQ_ISLANDS = 5.0f; // finer patchiness for shelf strength
|
||||
|
||||
public static float EffectiveSpikeMax(float hMaxSeed)
|
||||
{
|
||||
return Mathf.Max(hMaxSeed, K6 + SPIKE_MIN_SPAN);
|
||||
}
|
||||
|
||||
/// <summary>Shelf output span for a strength sample t ∈ [0,1].</summary>
|
||||
public static float ShelfSpan(float strength01)
|
||||
{
|
||||
return Mathf.Lerp(SHELF_SPAN_MAX, SHELF_SPAN_MIN, Mathf.Clamp(strength01, 0f, 1f));
|
||||
}
|
||||
|
||||
/// <param name="h">Raw pre-curve height.</param>
|
||||
/// <param name="hMaxSeed">The seed's raw pre-curve maximum — per-seed spike normalizer.</param>
|
||||
public static float Apply(float h, float hMaxSeed)
|
||||
/// <param name="hMaxSeed">Seed's raw pre-curve maximum (per-seed spike normalizer).</param>
|
||||
/// <param name="benchLo">This column's bench anchor (BENCH_BASE ± BENCH_AMP).</param>
|
||||
/// <param name="benchSpan">This column's bench output span (ShelfSpan of the strength field).</param>
|
||||
/// <param name="plateauLo">This column's plateau anchor (PLATEAU_BASE ± PLATEAU_AMP).</param>
|
||||
/// <param name="plateauSpan">This column's plateau output span.</param>
|
||||
public static float Apply(float h, float hMaxSeed,
|
||||
float benchLo, float benchSpan, float plateauLo, float plateauSpan)
|
||||
{
|
||||
if (h <= SEA) return h;
|
||||
|
||||
|
|
@ -77,72 +91,89 @@ public static class HeightCurve
|
|||
if (h < K1)
|
||||
{
|
||||
u = (h - SEA) / (K1 - SEA);
|
||||
s = 0.3f * u + 0.7f * (u * (2f - u)); // ease-out toe, slope ≥ 0.3
|
||||
s = 0.3f * u + 0.7f * (u * (2f - u)); // frozen ease-out toe
|
||||
return SEA + s * (ORANGE_CEIL - SEA);
|
||||
}
|
||||
if (h < K2)
|
||||
{
|
||||
u = (h - K1) / (K2 - K1);
|
||||
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // linear rise
|
||||
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise
|
||||
}
|
||||
if (h < K3)
|
||||
{
|
||||
u = (h - K2) / (K3 - K2);
|
||||
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // foothill riser, slope ≥ 0.2
|
||||
return RED_CEIL + s * (FOOTHILL_BENCH - RED_CEIL);
|
||||
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // foothill riser
|
||||
return RED_CEIL + s * (benchLo - RED_CEIL);
|
||||
}
|
||||
if (h < K4)
|
||||
{
|
||||
u = (h - K3) / (K4 - K3);
|
||||
return FOOTHILL_BENCH + u * BENCH_STEP; // bench 1, near-flat
|
||||
return benchLo + u * benchSpan; // bench — modulated
|
||||
}
|
||||
float benchTop = benchLo + benchSpan;
|
||||
if (h < K5)
|
||||
{
|
||||
u = (h - K4) / (K5 - K4);
|
||||
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // mid riser, slope ≥ 0.2
|
||||
return FOOTHILL_TOP + s * (PLATEAU - FOOTHILL_TOP);
|
||||
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // mid riser
|
||||
return benchTop + s * (plateauLo - benchTop);
|
||||
}
|
||||
if (h < K6)
|
||||
{
|
||||
u = (h - K5) / (K6 - K5);
|
||||
return PLATEAU + u * BENCH_STEP; // white plateau, near-flat
|
||||
return plateauLo + u * plateauSpan; // plateau — modulated
|
||||
}
|
||||
float plateauTop = plateauLo + plateauSpan;
|
||||
float spikeMax = EffectiveSpikeMax(hMaxSeed);
|
||||
if (h < spikeMax)
|
||||
{
|
||||
u = (h - K6) / (spikeMax - K6);
|
||||
s = 0.1f * u + 0.9f * (u * u * u * u); // summit spike (v2 shape kept), slope ≥ 0.1
|
||||
return PLATEAU_TOP + s * (PEAK_CAP - PLATEAU_TOP);
|
||||
s = 0.1f * u + 0.9f * (u * u * u * u); // summit spike (v2/v3 shape)
|
||||
return plateauTop + s * (PEAK_CAP - plateauTop);
|
||||
}
|
||||
return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numeric strict-monotonicity check of the EFFECTIVE per-seed curve — call once
|
||||
/// per generation after hMaxSeed is known. Loud throw, refuses to generate.
|
||||
/// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve:
|
||||
/// sweeps the full domain at every one of the 8 modulation-extreme corners
|
||||
/// (bench anchor ±, plateau anchor ±, strength min/max) with the per-seed
|
||||
/// spikeMax — the adversarial corner set for the ordering constraints. Loud
|
||||
/// throw, refuses to generate.
|
||||
/// </summary>
|
||||
public static void AssertMonotonic(float hMaxSeed)
|
||||
{
|
||||
float prevH = -7f;
|
||||
float prev = Apply(prevH, hMaxSeed);
|
||||
float[] benchLos = { BENCH_BASE - BENCH_AMP, BENCH_BASE + BENCH_AMP };
|
||||
float[] plateauLos = { PLATEAU_BASE - PLATEAU_AMP, PLATEAU_BASE + PLATEAU_AMP };
|
||||
float[] spans = { SHELF_SPAN_MIN, SHELF_SPAN_MAX };
|
||||
|
||||
// Only strictly increasing float32 samples are compared (task-05 incident fix).
|
||||
void Check(double hd)
|
||||
foreach (float bl in benchLos)
|
||||
{
|
||||
float h = (float)hd;
|
||||
if (h <= prevH) return;
|
||||
float v = Apply(h, hMaxSeed);
|
||||
if (v <= prev)
|
||||
throw new System.InvalidOperationException(
|
||||
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}): {v} <= {prev}. Refusing to generate.");
|
||||
prev = v;
|
||||
prevH = h;
|
||||
}
|
||||
foreach (float pl in plateauLos)
|
||||
{
|
||||
foreach (float sp in spans)
|
||||
{
|
||||
float prevH = -7f;
|
||||
float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp);
|
||||
|
||||
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5);
|
||||
for (double h = -7.0 + 0.01; h < 0.10; h += 0.01) Check(h);
|
||||
for (double h = 0.10; h <= top; h += 0.0001) Check(h);
|
||||
for (double h = top + 0.05; h <= top + 6.0; h += 0.05) Check(h);
|
||||
GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION}, effective spikeMax {EffectiveSpikeMax(hMaxSeed):F6}).");
|
||||
void Check(double hd)
|
||||
{
|
||||
float h = (float)hd;
|
||||
if (h <= prevH) return; // dedupe float32 samples (task-05 fix)
|
||||
float v = Apply(h, hMaxSeed, bl, sp, pl, sp);
|
||||
if (v <= prev)
|
||||
throw new System.InvalidOperationException(
|
||||
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}): {v} <= {prev}. Refusing to generate.");
|
||||
prev = v;
|
||||
prevH = h;
|
||||
}
|
||||
|
||||
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5);
|
||||
for (double hh = -7.0 + 0.01; hh < 0.10; hh += 0.01) Check(hh);
|
||||
for (double hh = 0.10; hh <= top; hh += 0.0001) Check(hh);
|
||||
for (double hh = top + 0.05; hh <= top + 6.0; hh += 0.05) Check(hh);
|
||||
}
|
||||
}
|
||||
}
|
||||
GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION}, 8 modulation corners, effective spikeMax {EffectiveSpikeMax(hMaxSeed):F6}).");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue