Ports the reference's v5 height curve and shelf-detail passes onto Phase 1's shape and re-calibrates them against this repo's actual pass-1 distribution. This is the BASELINE the reshape gets judged against, not the reshape. Core (engine-free, D-060): - WorldScale — THE vertical yardstick. One metres/raw number (251), replacing the prototype's three duplicate M_PER_UNIT constants and ~20 bare literals. The chunk-height coupling it had there is recorded as a DEFERRED vault decision, not inherited. RawFromMetres divides, matching the reference bit-for-bit. - HeightCurve — the 7 bands, the frozen corner-fix blends, the per-seed spike normalization, the 24-corner monotonicity sweep that throws and refuses. Identity at and below sea, which everything downstream rests on. - CurveKnots / CurveAnchors — input knots (measured percentiles) and output anchors (storm ladder) split apart and both made parameters, so the anchors are A/B-able without editing source. The reference's shipped knots are kept beside the measured ones as the fidelity yardstick. - TerrainDetailPass — micro-relief skin plus the shelf-edge KNOT warp (which slides K3/K4/K5, not height — that is what keeps monotonicity structural). The crater exclusion is ported and inert until the carve lands. Tools: - Shaping — pass 2a, producing the two height fields. classify is bit-for-bit the raw pass-1 field; render is curved and detailed. Aliased when the curve is off, as the reference did. Pass1Result is left immutable so the oracle can compare. - LandHistogram — the calibration engine AND the diagnostic. The reference shipped six knot literals and threw the measuring instrument away; this rebuilds it. - ShapingOracle + CurveBaselineTool — four automatic checks before anything is looked at, and the batch that runs them. Measured, not assumed: - Knots re-measured over a 6-seed / 12.8M-sample pool. They differ from the reference's by at most 5.6 m of world height, against a 44.7 m per-seed spread — the pass-1 port is faithful. - Oracle all pass, including pass 1 bit-identical to Phase 1's own .f32 dump. - Band shares land on 60/13/10/5/8/3/1 to 0.00 pp. - Knots hold across map size: the 8K delta (5.8 m) sits inside seed noise. The finding the histograms deliver: 83% of land ends below 100 m and 96% below 220 m, with the median column at 13 m. That is the share targets doing exactly what they say, not a bug — and it is the developer's call, which is why nothing here reshapes it and the palette was deliberately left mis-fitted rather than recalibrated to disguise it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCWNaDZPfTiAy3meGNGgqt
238 lines
9.4 KiB
C#
238 lines
9.4 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>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();
|
||
}
|
||
}
|
||
}
|