chat2/02 dissolved the terraces and lost two thirds of the mountain with them, then
concluded the loss was structural and needed a Phase-1 noise change. That conclusion
was wrong, and this commit is the refutation.
A monotone curve is a free reparametrization: it may be gentle in one place and steep
in another, and can lift bottom-heavy input into a substantial massif without ever
going flat. The area above a height is set by where the percentile->height mapping
crosses it, and that mapping is entirely ours to choose. The 02 sweep that "proved"
the loss structural varied climbFeather, which shapes the JOIN, not the mass
distribution — the wrong knob, and too strong a conclusion drawn from it.
Core/ClimbCalibration — the climb's control points are now MEASURED off the staircase
instead of invented from shape knobs. For p in {10,30,50,70,85,95} of above-ceiling
land, take that percentile's raw height and its staircase output height; PCHIP through
the pairs. That reproduces the staircase's elevation envelope, so the mountain mass
returns, while MinNormalizedSecant floors every grade so the flat bench and plateau
interiors become slope. The floor bites on exactly one segment — the plateau — which
is precisely where the staircase was flat.
ContinuousCurve.BuildCalibrated joins it to the same pinned lowland handover, the same
C1 join and the same per-seed spikeMax. The 02 analytic path survives unchanged as the
"before" contrast, and deliberately keeps its strictly-increasing-secant rule: a
calibrated curve is WAVY by design, so convexity is the wrong invariant for it and the
secant floor is the right one.
peakSharpness replaces summitDrama and fixes its bad trade. Drama steepened the peak by
pulling the summit ONSET down, dragging the whole massif with it (p99 199 -> 121 m).
Sharpness reshapes only above the last measured percentile, leaving that height fixed,
so peak and massif are independent: raising it leaves p90, >100 m and >220 m untouched
and only moves land within the summit.
Measured, both seeds, 2048:
variant >100 m >220 m p90
staircase (target) 16.05% 4.53% 127.4 m
continuous_02default 4.80% 0.63% 58.4 m
continuous_restored 14.25% 3.45% 123.2 m
continuous_bigger 19.70% 5.80% 163.5 m
Oracle all hard checks pass, including (a2) staircase still bit-identical to task 01's
dump and (d) lowlands bit-identical across every calibrated variant. New (g) reports
land above 100/220 m per variant and is deliberately NOT gated — it is a taste target
the developer tunes, and gating it would make mountainLift unusable. What it must never
do is stay silent, which is how 02 lost the mountain unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
414 lines
16 KiB
C#
414 lines
16 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using IslaApocalypse.Core;
|
||
|
||
namespace IslaApocalypse.Tools
|
||
{
|
||
/// <summary>
|
||
/// ⭐⭐ THE ORACLE — the automatic correctness checks that let the developer's eye judge ONLY
|
||
/// relief.
|
||
///
|
||
/// ═══ WHY THIS EXISTS AT ALL ═══
|
||
///
|
||
/// The prototype's single most valuable terrain lesson was not about terrain:
|
||
///
|
||
/// > *"Five rounds of taste-iteration were safe BECAUSE correctness was not being judged by eye.
|
||
/// > Where a future phase has a subjective gate, ask first what the automatic invariant is."*
|
||
/// > — `Design - Tooling - Iteration and Batching.md`, "build the oracle before the
|
||
/// > taste-iteration, not after".
|
||
///
|
||
/// The curve is a subjective gate. So before a single render is looked at, four things are
|
||
/// proven mechanically:
|
||
///
|
||
/// (a) REGRESSION curve OFF is bit-identical to Phase 1's pass-1 output.
|
||
/// ⇒ the port disturbed nothing upstream.
|
||
/// (b) CLASSIFY FIDELITY the classify field is bit-identical to the raw pre-curve field,
|
||
/// curve on or off. ⇒ the oracle field is actually an oracle.
|
||
/// (c) MONOTONICITY the effective per-seed curve is strictly increasing everywhere.
|
||
/// ⇒ no peak has become a pit.
|
||
/// (d) BAND SHARES realized land shares match the 60/13/10/5/8/3/1 targets.
|
||
/// ⇒ the calibration did what it claimed.
|
||
///
|
||
/// ⚠ BIT-IDENTICAL MEANS BIT-IDENTICAL. These compare IEEE-754 bit patterns, not values within
|
||
/// an epsilon. "Close enough" is how a drift becomes a fact — and the whole point of the `.f32`
|
||
/// dump is that two generators agree or their dumps differ.
|
||
///
|
||
/// ⚠ Any failure fails the TASK, loudly. Nothing here papers over a mismatch: a check that
|
||
/// reports "mostly passed" is a check that has stopped working.
|
||
/// </summary>
|
||
public static class ShapingOracle
|
||
{
|
||
/// <summary>One check's verdict. <see cref="Detail"/> carries the evidence either way.</summary>
|
||
public sealed class Check
|
||
{
|
||
public string Id; // "a", "b", "c", "d"
|
||
public string Name;
|
||
public bool Passed;
|
||
public string Detail;
|
||
|
||
public override string ToString() => $"[{(Passed ? "PASS" : "FAIL")}] ({Id}) {Name} — {Detail}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// Compare two float fields for BIT equality. Returns the number of differing cells and the
|
||
/// first difference found, so a failure is actionable rather than just red.
|
||
/// </summary>
|
||
public static (long differing, string firstDiff) CompareBitwise(float[,] a, float[,] b, int mapSize)
|
||
{
|
||
long differing = 0;
|
||
string first = null;
|
||
|
||
for (int x = 0; x < mapSize; x++)
|
||
{
|
||
for (int y = 0; y < mapSize; y++)
|
||
{
|
||
int ba = BitConverter.SingleToInt32Bits(a[x, y]);
|
||
int bb = BitConverter.SingleToInt32Bits(b[x, y]);
|
||
if (ba == bb) continue;
|
||
|
||
differing++;
|
||
first ??= $"first at [{x},{y}]: {a[x, y]:G9} (0x{ba:X8}) vs {b[x, y]:G9} (0x{bb:X8})";
|
||
}
|
||
}
|
||
return (differing, first);
|
||
}
|
||
|
||
/// <summary>
|
||
/// (a) REGRESSION — with the curve off, shaping must return the pass-1 field untouched.
|
||
///
|
||
/// ⚠ This is the WEAKER, always-available half of check (a): it proves pass 2 is a no-op when
|
||
/// gated off. The stronger half — that pass 1 ITSELF still matches Phase 1 byte-for-byte — is
|
||
/// <see cref="RegressionAgainstDump"/>, which needs a Phase-1 `.f32` on disk.
|
||
/// </summary>
|
||
public static Check RegressionCurveOff(Pass1Result p1, Pass2Result offResult)
|
||
{
|
||
var c = new Check { Id = "a", Name = "regression: curve OFF == pass-1 output" };
|
||
|
||
if (offResult.CurveOn)
|
||
{
|
||
c.Passed = false;
|
||
c.Detail = "the result handed in was generated with the curve ON — wrong variant.";
|
||
return c;
|
||
}
|
||
|
||
var (differing, firstDiff) = CompareBitwise(p1.Height, offResult.Height, p1.MapSize);
|
||
bool aliased = offResult.FieldsAreAliased;
|
||
|
||
c.Passed = differing == 0;
|
||
c.Detail = c.Passed
|
||
? $"bit-identical over {(long)p1.MapSize * p1.MapSize:N0} cells" +
|
||
(aliased ? " (and the fields alias one array, as the reference did)" : "")
|
||
: $"{differing:N0} cells differ — {firstDiff}";
|
||
return c;
|
||
}
|
||
|
||
/// <summary>
|
||
/// (a′) REGRESSION against a Phase-1 `.f32` dump — the cross-run half.
|
||
///
|
||
/// ⚠ A MISSING DUMP IS NOT A PASS. It is reported as INCONCLUSIVE and the caller says so; a
|
||
/// check that silently succeeds when its input is absent is worse than no check, because it
|
||
/// buys confidence that was never earned.
|
||
/// </summary>
|
||
public static Check RegressionAgainstDump(float[,] current, float[,] phase1Dump, int mapSize, string dumpPath)
|
||
{
|
||
var c = new Check { Id = "a′", Name = "regression: pass-1 == Phase-1 .f32 dump" };
|
||
|
||
if (phase1Dump == null)
|
||
{
|
||
c.Passed = false;
|
||
c.Detail = $"INCONCLUSIVE — no readable Phase-1 dump at {dumpPath} for this seed/size. " +
|
||
"Not counted as a pass; set ISLA_PHASE1_SOURCE to a batch that has one.";
|
||
return c;
|
||
}
|
||
|
||
var (differing, firstDiff) = CompareBitwise(phase1Dump, 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>
|
||
/// (b) CLASSIFY FIDELITY — the classify field is the raw pre-curve field, bit-for-bit, with
|
||
/// the curve on or off.
|
||
///
|
||
/// This is the invariant every later phase's oracle rests on: biomes and water will classify
|
||
/// from this field, so if it has drifted by even one ulp the "md5-identical across shaping
|
||
/// changes" guarantee is gone before it is ever used.
|
||
/// </summary>
|
||
public static Check ClassifyFidelity(Pass1Result p1, Pass2Result p2)
|
||
{
|
||
var c = new Check { Id = "b", Name = "classify field == raw pass-1 field" };
|
||
|
||
var (differing, firstDiff) = CompareBitwise(p1.Height, p2.HeightClassify, p1.MapSize);
|
||
c.Passed = differing == 0;
|
||
c.Detail = c.Passed
|
||
? $"bit-identical over {(long)p1.MapSize * p1.MapSize:N0} cells (curve {(p2.CurveOn ? "ON" : "OFF")})"
|
||
: $"{differing:N0} cells differ — {firstDiff}";
|
||
return c;
|
||
}
|
||
|
||
/// <summary>
|
||
/// (c) MONOTONICITY — recorded rather than re-run.
|
||
///
|
||
/// <c>HeightCurve.AssertMonotonic</c> THROWS on violation and is called inside
|
||
/// <see cref="Shaping.Shape"/>, so reaching this code at all means the sweep passed. The
|
||
/// check exists so the oracle table states it explicitly instead of leaving the strongest
|
||
/// guarantee implicit in the absence of a crash.
|
||
/// </summary>
|
||
public static Check Monotonicity(Pass2Result p2)
|
||
{
|
||
var c = new Check { Id = "c", Name = "curve strictly monotonic (24-corner sweep)" };
|
||
|
||
if (!p2.CurveOn)
|
||
{
|
||
c.Passed = true;
|
||
c.Detail = "curve off — nothing to prove (identity is trivially monotonic).";
|
||
return c;
|
||
}
|
||
|
||
string note = p2.Notes.Find(n => n.Contains("Monotonicity assertion passed"));
|
||
c.Passed = note != null;
|
||
c.Detail = note ?? "no monotonicity confirmation recorded — AssertMonotonic did not run.";
|
||
return c;
|
||
}
|
||
|
||
/// <summary>
|
||
/// (d) BAND SHARES — the realized land shares against the M3 targets.
|
||
///
|
||
/// ⚠ MEASURED ON THE RAW (INPUT) DISTRIBUTION, because that is where the knots cut. The
|
||
/// shares are exact by construction IF the quantile machinery is right — so this check is
|
||
/// really a proof that <see cref="LandHistogram"/> measured what it claimed, which is the one
|
||
/// thing the reference could never verify about its own knots.
|
||
/// </summary>
|
||
/// <param name="tolerancePercentagePoints">
|
||
/// Allowed absolute deviation per band, in percentage points. The knots come from the SAME
|
||
/// histogram, so agreement is limited only by in-bin interpolation — tenths of a point, not
|
||
/// whole ones.
|
||
/// </param>
|
||
public static Check BandShares(LandHistogram raw, CurveKnots k, double tolerancePercentagePoints)
|
||
{
|
||
var c = new Check { Id = "d", Name = "realized land band shares == 60/13/10/5/8/3/1 targets" };
|
||
|
||
double[] realized = RealizedShares(raw, k);
|
||
double worst = 0.0;
|
||
int worstBand = -1;
|
||
|
||
for (int i = 0; i < realized.Length; i++)
|
||
{
|
||
double d = Math.Abs(realized[i] - CurveKnots.BandShareTargets[i]);
|
||
if (d > worst) { worst = d; worstBand = i; }
|
||
}
|
||
|
||
c.Passed = worst <= tolerancePercentagePoints;
|
||
c.Detail = $"worst band '{CurveKnots.BandNames[worstBand]}' off by {worst:F3} pp " +
|
||
$"(tolerance {tolerancePercentagePoints:F2} pp); realized " +
|
||
string.Join("/", Array.ConvertAll(realized, v => v.ToString("F2")));
|
||
return c;
|
||
}
|
||
|
||
/// <summary>
|
||
/// The fraction of land, in percent, falling in each of the curve's seven INPUT bands.
|
||
/// Band edges are sea, K1..K6, +∞.
|
||
/// </summary>
|
||
public static double[] RealizedShares(LandHistogram raw, CurveKnots k)
|
||
{
|
||
float[] edges = { raw.SeaLevel, k.K1, k.K2, k.K3, k.K4, k.K5, k.K6 };
|
||
var shares = new double[7];
|
||
|
||
for (int i = 0; i < 6; i++)
|
||
shares[i] = raw.FractionBetween(edges[i], edges[i + 1]) * 100.0;
|
||
|
||
shares[6] = Math.Max(0.0, (1.0 - raw.FractionBelow(k.K6)) * 100.0);
|
||
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>
|
||
/// (g) ⭐ MOUNTAIN RESTORED (chat2/03) — how much land ends up above 100 m and 220 m,
|
||
/// against the staircase's own figures.
|
||
///
|
||
/// ═══ ⚠ REPORTED, NOT GATED ═══
|
||
///
|
||
/// This is a taste target the developer tunes, so a miss is a FINDING, not a build failure —
|
||
/// gating it would make `mountainLift` unusable as a knob, since every value but one would
|
||
/// fail the run. What it must never do is stay silent: chat2/02 lost two thirds of the
|
||
/// mountain and only found out because someone went looking at the dumps afterwards. This
|
||
/// check is that look, made automatic.
|
||
///
|
||
/// <paramref name="tolerancePp"/> only decides whether the row reads PASS or NOTE; the
|
||
/// numbers are always printed.
|
||
/// </summary>
|
||
public static Check MountainRestored(Pass2Result variant, Pass2Result staircase,
|
||
float seaLevel, double tolerancePp)
|
||
{
|
||
var c = new Check { Id = "g", Name = $"mountain restored vs staircase [{variant.VariantLabel}]" };
|
||
|
||
var (v100, v220) = LandAbove(variant, seaLevel);
|
||
var (s100, s220) = LandAbove(staircase, seaLevel);
|
||
|
||
double d100 = v100 - s100;
|
||
c.Passed = Math.Abs(d100) <= tolerancePp;
|
||
c.Detail = $">100 m: {v100:F2} % vs staircase {s100:F2} % ({d100:+0.00;-0.00} pp) · " +
|
||
$">220 m: {v220:F2} % vs {s220:F2} % ({v220 - s220:+0.00;-0.00} pp)";
|
||
return c;
|
||
}
|
||
|
||
/// <summary>Percentage of LAND above 100 m and 220 m of world height. Land = at/above sea.</summary>
|
||
public static (double above100, double above220) LandAbove(Pass2Result p2, float seaLevel)
|
||
{
|
||
float t100 = seaLevel + WorldScale.RawFromMetres(100f);
|
||
float t220 = seaLevel + WorldScale.RawFromMetres(220f);
|
||
|
||
long land = 0, a100 = 0, a220 = 0;
|
||
for (int x = 0; x < p2.MapSize; x++)
|
||
{
|
||
for (int y = 0; y < p2.MapSize; y++)
|
||
{
|
||
float h = p2.Height[x, y];
|
||
if (h < seaLevel) continue;
|
||
land++;
|
||
if (h > t100) a100++;
|
||
if (h > t220) a220++;
|
||
}
|
||
}
|
||
return land == 0 ? (0.0, 0.0) : (100.0 * a100 / land, 100.0 * a220 / land);
|
||
}
|
||
|
||
/// <summary>Render the whole oracle as a markdown table for the INDEX and the report.</summary>
|
||
public static string ToMarkdownTable(IEnumerable<Check> checks)
|
||
{
|
||
var sb = new System.Text.StringBuilder();
|
||
sb.AppendLine("| | Check | Result | Evidence |");
|
||
sb.AppendLine("|---|---|---|---|");
|
||
foreach (Check c in checks)
|
||
sb.AppendLine($"| `{c.Id}` | {c.Name} | **{(c.Passed ? "PASS" : "FAIL")}** | {c.Detail} |");
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
}
|