The bare TerrainGenConfig defaults did NOT reproduce the terrain the developer
locked, so "run the default generator" was not "the terrain in the gallery" —
the single most expensive fact in the codebase, and the reason a fresh chat
would spend an afternoon chasing differences that were CONFIGURATION, not
regression. This is the deliberate task that ends that, before any river work.
A1 — the defaults ARE the locked shape now. Five fields actually move:
SpeckRevert false->true, MinLandComponentFrac 3e-5->2.5e-7 (120x smaller; the
config default would have eaten real islands, not specks), SouthStretch 0->2,
FragmentAmp 0->0.5, Erosion false->true. Seven more were already correct via
SouthernStretch.Default* / CoastalFragment.Default* and are now pinned as
literals, because TerrainShapeV1 used to do that pinning and this default set
inherits the job. CoastShelf stays OFF — the locked shape has no shelf, and
evaluating it (D-041) is its own later task once water renders. Offshore stays
Off permanently (D-063): islands are organic-only, made by the stretch +
fragmentation and identified by the region layer, never placed.
A2 — the preserve mechanism. The curve knots are percentiles of the FAMILY-OFF
land distribution; flipping the defaults would have moved the pool, the knots,
and with them the render field of every batch including terrain-shape-v1
itself. So the pool is pinned family-off (TerrainGenConfig.WithFamilyOff /
CalibrationPool) rather than the knots being baked: calibration stays live, its
INPUT distribution is held still. The pin was a no-op by construction — it sets
the values the defaults carried the instant before the flip — and re-measuring
after confirms it: pool, all six knots, per-seed spread, shaped max,
monotonicity spikeMax and all seven band shares identical.
Applied wider than "in CalibrateCurve": OffshoreIslandsTool,
RegionLabelingTool and SouthernStretchTool generate their own family-off
field for the Phase-1 anchor, so the pool pin alone would NOT have covered
them and their a1 would have failed for a configuration reason. TerrainGenTool
too — it AUTHORED 02_pass1_port and must stay able to regenerate its own
anchor.
Recorded as a judged-and-parked property: knots measured family-off, applied
family-on. Deliberate, not an oversight. Same disposition as the mid-slope
feather.
A4 — no oracle may pass against a superseded baseline. Six anchors retired
(01/03/04/06/08/09) with their checks and ISLA_T0x_SOURCE defaults; three kept
(chat1/02_pass1_port as the family-off pass-1 guard, chat2/10 and chat2/11 as
the shape and erosion acceptance anchors). Two invariants were RE-POINTED
rather than lost — the southern stretch's north-lock and the coastal-fragment
interior-lock now compare against SAME-RUN fields, which is scale-free and
cannot be invalidated by a moved dump. The retired dumps are kept, not deleted,
and marked superseded in their INDEX.md.
A missing anchor is now LOUD. The old pattern skipped silently, so a moved
anchor did not make its oracle fail — it made it not RUN, and a batch with a
skipped check prints an all-PASS table that reads like a clean one. That is
the INVERSE of the hazard the re-baseline guards against, and the migration
below is exactly the event that would have triggered it, on nine anchors at
once. ShapingOracle.LoadAnchor now separates the two cases: absent -> throw;
present at another size -> loud INCONCLUSIVE, which is a fail, never a pass.
TerrainShapeV1 inverted from PRESET to GUARD and moved to its own file.
Apply() is gone — stamping the values on top of the defaults would MASK a
drift instead of catching it. Its constants are now the assertion target, and
Assert() refuses a run whose defaults have drifted off the locked shape.
B/C — batches are namespaced by chat: batches/<chat>/NN_slug/. Task numbers
restart at 00 per chat, so a flat root collided the moment a second chat
existed — four colliding prefixes across 25 batches, separable only by slug.
ToolingPaths.ChatSlug is REQUIRED (throws if unset) and defaults per tool to
its authoring chat, so re-running reproduces a batch in place while ISLA_CHAT
redirects — which is also what stops an acceptance run from overwriting the
very anchor it checks against. Writes go through BatchRoot; historical READS
compose against BatchesRoot and so carry the prefix in their own source string
("chat1/02_pass1_port"). The 25 existing batches were migrated moves-only.
ACCEPTANCE — 16 of 16 byte-identical, 0 failed. All 8 gallery seeds at 8192
from the bare defaults are byte-identical to chat2/10_frag4_seed_gallery
(= terrain-shape-v1, a59e52f); all 8 erosion fields byte-identical to
chat2/11_erosion (= ea291ea). Every gallery table row and every erosion
statistic reproduces its recorded value exactly. DrainageTool's a11 passes
bit-identical over 67,108,864 cells, and its analysis reproduces batch 12
exactly — so the whole chain rivers depends on (shape -> erosion -> drainage)
is unchanged. All 12 edited tools re-run clean; both new guards negative-tested.
The baseline moved in DEFAULTS, not in TERRAIN.
-> XX_Human/output/rivers/01_rebaseline_and_batch_namespace.report.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhkXBQh2tDmcWKpXYcj8vj
539 lines
26 KiB
C#
539 lines
26 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text;
|
||
using Godot;
|
||
using IslaApocalypse.Core;
|
||
|
||
namespace IslaApocalypse.Tools
|
||
{
|
||
/// <summary>
|
||
/// ⭐ THE MOUNTAIN-RESTORE BATCH (chat2/03) — put the mountain back, as a smooth slope.
|
||
///
|
||
/// ═══ THE STORY THIS BATCH TELLS, IN THREE HISTOGRAMS ═══
|
||
///
|
||
/// staircase the mass is there, but parked in two spikes (bench 100 m, plateau 220 m)
|
||
/// continuous_02 the spikes are gone — and so is the mass. It fell to 30–90 m.
|
||
/// continuous_restored ⭐ the same mass as the staircase, spread as one smooth grade.
|
||
///
|
||
/// That contrast is the point, so the three are named to sort adjacent.
|
||
///
|
||
/// ═══ HOW THE RESTORATION IS MEASURED ═══
|
||
///
|
||
/// The climb's control points are no longer invented from shape knobs. They are MEASURED off the
|
||
/// staircase itself, on the same 6-seed pool tasks 01/02 use:
|
||
///
|
||
/// for p in {10,30,50,70,85,95} of ABOVE-CEILING land:
|
||
/// u_p ← that percentile of the RAW height (normalized into the climb's span)
|
||
/// v_p ← that percentile of the OUTPUT height (staircase, normalized)
|
||
///
|
||
/// PCHIP through those points reproduces the staircase's elevation envelope; the flat bench and
|
||
/// plateau interiors become grade because <c>ClimbCalibration.MinNormalizedSecant</c> floors
|
||
/// every segment. → <see cref="ClimbCalibration"/>.
|
||
///
|
||
/// ⚠ The two quantile sets are paired by percentile across the SAME cell population, which is
|
||
/// exact only if the staircase were strictly monotone per column. It is monotone in raw, but the
|
||
/// per-column bench/plateau modulation (±12 / ±20 m) blurs the pairing by about that much. That
|
||
/// is well inside the envelope being targeted, and calibrating on the MEASURED output (rather
|
||
/// than a nominal unmodulated curve) is what makes oracle (g)'s land-above-100 m figure the thing
|
||
/// actually being aimed at.
|
||
///
|
||
/// ═══ RUNNING IT ═══
|
||
///
|
||
/// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
|
||
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/MountainRestoreTool.tscn
|
||
///
|
||
/// ISLA_TASK / ISLA_BATCH / ISLA_MAPSIZE / ISLA_SEEDS / ISLA_SHOWPIECE_SIZE / ISLA_SHOWPIECE
|
||
/// ISLA_PHASE1_SOURCE (default "chat1/02_pass1_port")
|
||
/// ISLA_SKIP_RAW
|
||
/// ISLA_LIFT_BIG probe: the `continuous_bigger` lift (default 1.35)
|
||
/// ISLA_SHARP probe: the `continuous_sharper_peak` knob (default 2.5)
|
||
/// </summary>
|
||
public partial class MountainRestoreTool : Node
|
||
{
|
||
private static readonly int[] DefaultSeeds = { 1063685222, 777001 };
|
||
|
||
/// <summary>⚠ Task 01's pool, verbatim — the knots' identity, and with it the staircase control's.</summary>
|
||
private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
|
||
|
||
private const int DefaultMapSize = 2048;
|
||
private const int DefaultShowpieceSize = 8192;
|
||
|
||
/// <summary>Oracle (g)'s PASS/NOTE threshold, percentage points of land above 100 m. Reported either way.</summary>
|
||
private const double MountainTolerancePp = 2.0;
|
||
|
||
public override void _Ready()
|
||
{
|
||
try { Run(); }
|
||
catch (Exception e)
|
||
{
|
||
GD.PrintErr("==================================================================");
|
||
GD.PrintErr($" REFUSED: {e.Message}");
|
||
GD.PrintErr("==================================================================");
|
||
GetTree().Quit(2);
|
||
}
|
||
}
|
||
|
||
private void Run()
|
||
{
|
||
ToolingPaths.Configure(OS.GetUserDataDir());
|
||
// ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat,
|
||
// so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another
|
||
// chat's namespace — which is what keeps an acceptance run from overwriting its own anchor.
|
||
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat2"));
|
||
|
||
int task = EnvInt("ISLA_TASK", 3);
|
||
string descr = EnvStr("ISLA_BATCH", "mountain_restore");
|
||
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", "chat1/02_pass1_port");
|
||
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
|
||
float liftBig = EnvFloat("ISLA_LIFT_BIG", 1.35f);
|
||
float sharpKnob = EnvFloat("ISLA_SHARP", 2.5f);
|
||
|
||
string batchRoot = ToolingPaths.BatchRoot(task, descr);
|
||
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
|
||
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
|
||
|
||
var anchors = CurveAnchors.Default;
|
||
float sea = 0.15f;
|
||
int primary = seeds[0];
|
||
|
||
GD.Print("==================================================================");
|
||
GD.Print(" MOUNTAIN RESTORE (chat2/03) — the staircase's mountain,");
|
||
GD.Print(" de-terraced. Calibrated, not invented.");
|
||
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("==================================================================");
|
||
|
||
// ═══ 0. KNOTS — task 01's pool, re-measured for bit-identity ═══
|
||
GD.Print("\n--- 0. KNOTS ---");
|
||
var rawPool = new LandHistogram(sea);
|
||
var pass1 = new Dictionary<int, Pass1Result>();
|
||
foreach (int seed in CalibrationSeeds)
|
||
{
|
||
var p1 = Topography.Generate(TerrainGenConfig.CalibrationPool(mapSize, seed));
|
||
pass1[seed] = p1;
|
||
rawPool.Accumulate(p1.Height, mapSize);
|
||
}
|
||
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($" {rawPool}");
|
||
GD.Print($" {knots}");
|
||
|
||
// ═══ 1. CALIBRATE — measure the staircase's above-ceiling elevation distribution ═══
|
||
//
|
||
// ⚠ The ceiling is the DEFAULT 30 m handover, which is exactly (K2, RED_CEIL). So
|
||
// "above-ceiling" is simply "raw > K2" — no derived float, and the same population the
|
||
// climb will later be responsible for.
|
||
GD.Print("\n--- 1. CALIBRATION (staircase above-ceiling distribution) ---");
|
||
float ceilingRaw = knots.K2;
|
||
|
||
var rawAbove = new LandHistogram(sea);
|
||
var outAbove = new LandHistogram(sea);
|
||
var stairPool = new Dictionary<int, Pass2Result>();
|
||
|
||
foreach (int seed in CalibrationSeeds)
|
||
{
|
||
var scfg = MakeConfig(mapSize, seed, knots, anchors, "staircase");
|
||
scfg.CurveMode = CurveModeKind.Staircase;
|
||
scfg.ShelfDetail = true;
|
||
Pass2Result st = Shaping.Shape(pass1[seed], scfg);
|
||
stairPool[seed] = st;
|
||
|
||
rawAbove.AccumulateWhere(pass1[seed].Height, pass1[seed].Height, mapSize, ceilingRaw);
|
||
outAbove.AccumulateWhere(st.Height, pass1[seed].Height, mapSize, ceilingRaw);
|
||
}
|
||
|
||
double shareAbove = 100.0 * rawAbove.TotalLand / rawPool.TotalLand;
|
||
GD.Print($" above-ceiling land: {rawAbove.TotalLand:N0} cells = {shareAbove:F1}% of all land");
|
||
|
||
var pcts = ClimbCalibration.DefaultPercentiles;
|
||
var rawQ = new float[pcts.Length];
|
||
var outQ = new float[pcts.Length];
|
||
GD.Print(" percentile → raw → staircase output");
|
||
for (int i = 0; i < pcts.Length; i++)
|
||
{
|
||
rawQ[i] = rawAbove.Quantile(pcts[i]);
|
||
outQ[i] = outAbove.Quantile(pcts[i]);
|
||
GD.Print($" P{pcts[i],-4:F0} raw {rawQ[i]:F4} → {WorldScale.MetresFromRaw(outQ[i] - sea),6:F1} m");
|
||
}
|
||
|
||
ClimbCalibration Calib(float lift, float sharp) => ClimbCalibration.FromPercentiles(
|
||
pcts, rawQ, outQ, ceilingRaw, HeightCurve.EffectiveSpikeMax(pass1[primary].HMaxSeed, knots, anchors),
|
||
anchors.RedCeil, anchors.PeakCap, lift, sharp);
|
||
|
||
// ⚠ ONE calibration object per knob pair, shared across seeds. spikeMax differs slightly
|
||
// per seed, but the calibration is NORMALIZED (u, v in [0,1]) — BuildCalibrated
|
||
// denormalizes against each seed's own spikeMax. So the shape is shared; the extent is
|
||
// per-seed, exactly as the per-seed peak normalization requires.
|
||
var calRestored = Calib(1.0f, 1.0f);
|
||
var calBigger = Calib(liftBig, 1.0f);
|
||
var calSharper = Calib(1.0f, sharpKnob);
|
||
GD.Print($" restored: {calRestored.Describe()}");
|
||
GD.Print($" bigger : {calBigger.Describe()}");
|
||
GD.Print($" sharper : {calSharper.Describe()}");
|
||
|
||
// ═══ 2. VARIANTS ═══
|
||
var variants = new List<(string label, Action<TerrainGenConfig> mutate)>
|
||
{
|
||
("staircase", c => { c.CurveMode = CurveModeKind.Staircase; c.ShelfDetail = true; }),
|
||
("continuous_02default", c => { c.CurveMode = CurveModeKind.Continuous;
|
||
c.ClimbCalibration = null; // the analytic 02 curve
|
||
c.ClimbFeather = 0.4f; c.SummitDrama = 2.5f; }),
|
||
("continuous_restored", c => { c.CurveMode = CurveModeKind.Continuous; c.ClimbCalibration = calRestored; }),
|
||
("continuous_bigger", c => { c.CurveMode = CurveModeKind.Continuous; c.ClimbCalibration = calBigger; }),
|
||
("continuous_sharper_peak", c => { c.CurveMode = CurveModeKind.Continuous; c.ClimbCalibration = calSharper; }),
|
||
};
|
||
|
||
GD.Print("\n--- 2. VARIANTS ---");
|
||
var results = new Dictionary<(int, string), Pass2Result>();
|
||
var offs = new Dictionary<int, Pass2Result>();
|
||
var rows = new List<string>();
|
||
bool notesPrinted = false;
|
||
|
||
foreach (int seed in seeds)
|
||
{
|
||
var offCfg = MakeConfig(mapSize, seed, knots, anchors, "curve_off");
|
||
offCfg.Curve = false;
|
||
offs[seed] = Shaping.Shape(pass1[seed], offCfg);
|
||
|
||
foreach (var (label, mutate) in variants)
|
||
{
|
||
var cfg = MakeConfig(mapSize, seed, knots, anchors, label);
|
||
mutate(cfg);
|
||
Pass2Result p2 = Shaping.Shape(pass1[seed], cfg);
|
||
results[(seed, label)] = p2;
|
||
if (!notesPrinted) foreach (string nt in p2.Notes) GD.Print(" " + nt);
|
||
rows.Add(WriteVariant(batchRoot, p2, sea, anchors, skipRaw));
|
||
}
|
||
notesPrinted = true;
|
||
}
|
||
|
||
// ═══ 3. ORACLE ═══
|
||
GD.Print("\n--- 3. ORACLE ---");
|
||
var hard = new List<ShapingOracle.Check>();
|
||
var soft = new List<ShapingOracle.Check>();
|
||
|
||
// ⭐ a1 KEPT at rivers/01 — the family-off pass-1 guard (config pinned family-off).
|
||
// ⚠ A missing dump now THROWS instead of skipping silently.
|
||
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, ShapingOracle.LoadAnchor("a1", "ISLA_PHASE1_SOURCE", p1Dump, mapSize), mapSize, p1Dump));
|
||
|
||
// ⚑ RETIRED at rivers/01 — a2, the staircase == `01_curve_baseline` control.
|
||
// Superseded by the continuous grade (→ D-062) — see CurveContinuousTool for the full note.
|
||
// The dump is NOT deleted (file-safety; it is regenerable and it is the record of what
|
||
// was judged); its `INDEX.md` is marked superseded. The check is gone so nothing can
|
||
// pass against a superseded baseline. → XX_Human/output/rivers/01_*.report.md §A4.
|
||
|
||
long bFail = 0;
|
||
foreach (int seed in seeds)
|
||
foreach (var (label, _) in variants)
|
||
if (!ShapingOracle.ClassifyFidelity(pass1[seed], results[(seed, label)]).Passed) bFail++;
|
||
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",
|
||
});
|
||
|
||
bool cOk = results[(primary, "continuous_restored")].Notes
|
||
.Exists(n => n.Contains("strict-increase sample passed"));
|
||
hard.Add(new ShapingOracle.Check
|
||
{
|
||
Id = "c", Name = "monotone — Fritsch–Carlson + per-seed sampled",
|
||
Passed = cOk,
|
||
Detail = cOk ? "confirmed on the calibrated climb (throws and refuses on violation)"
|
||
: "no strict-increase confirmation recorded",
|
||
});
|
||
|
||
string[] continuous = { "continuous_02default", "continuous_restored", "continuous_bigger", "continuous_sharper_peak" };
|
||
foreach (int seed in seeds)
|
||
foreach (string label in continuous)
|
||
{
|
||
var d = ShapingOracle.LowlandsPreserved(pass1[seed], results[(seed, "staircase")], results[(seed, label)]);
|
||
d.Name += $" [seed {seed}]";
|
||
hard.Add(d);
|
||
|
||
var e = ShapingOracle.UpperClimbProfile(results[(seed, label)]);
|
||
e.Name += $" [seed {seed}]";
|
||
soft.Add(e);
|
||
}
|
||
|
||
foreach (int seed in seeds)
|
||
foreach (var (label, _) in variants)
|
||
{
|
||
var f = ShapingOracle.SeaIdentity(offs[seed], results[(seed, label)], sea);
|
||
f.Name += $" [seed {seed}]";
|
||
hard.Add(f);
|
||
}
|
||
|
||
// (g) the restoration, measured — reported for every variant, gated for none.
|
||
var mountain = new List<ShapingOracle.Check>();
|
||
foreach (int seed in seeds)
|
||
foreach (string label in continuous)
|
||
{
|
||
var g = ShapingOracle.MountainRestored(results[(seed, label)], results[(seed, "staircase")],
|
||
sea, MountainTolerancePp);
|
||
g.Name += $" [seed {seed}]";
|
||
mountain.Add(g);
|
||
}
|
||
|
||
foreach (var c in hard) GD.Print(" " + c);
|
||
foreach (var c in soft) { if (c.Passed) GD.Print(" " + c); else GD.PrintErr(" ⚠ SLOPE: " + c); }
|
||
GD.Print(" --- (g) mountain, reported not gated ---");
|
||
foreach (var c in mountain) GD.Print(" " + c);
|
||
|
||
bool hardOk = hard.TrueForAll(c => c.Passed);
|
||
GD.Print($" ORACLE: {(hardOk ? "ALL HARD CHECKS PASS" : "*** HARD FAILURES ***")}");
|
||
|
||
// ═══ 4. HISTOGRAMS — the three-way contrast, adjacent by filename ═══
|
||
GD.Print("\n--- 4. HISTOGRAMS ---");
|
||
foreach (int seed in seeds)
|
||
{
|
||
var rawSeed = new LandHistogram(sea);
|
||
rawSeed.Accumulate(pass1[seed].Height, mapSize);
|
||
int order = 1;
|
||
foreach (var (label, _) in variants)
|
||
DrawShaped(results[(seed, label)], anchors, seed, mapSize, batchRoot, order++, sea);
|
||
}
|
||
|
||
// ═══ 5. SHOWPIECE ═══
|
||
string showNote = "skipped (ISLA_SHOWPIECE=0)";
|
||
if (showpiece)
|
||
{
|
||
GD.Print($"\n--- 5. SHOWPIECE at {showSize} (continuous_restored, seed {primary}) ---");
|
||
var cfg = MakeConfig(showSize, primary, knots, anchors, "continuous_restored_showpiece");
|
||
cfg.CurveMode = CurveModeKind.Continuous;
|
||
cfg.ClimbCalibration = calRestored;
|
||
|
||
Pass1Result p1 = Topography.Generate(cfg);
|
||
Pass2Result big = Shaping.Shape(p1, cfg);
|
||
foreach (string nt in big.Notes) GD.Print(" " + nt);
|
||
|
||
var cb = ShapingOracle.ClassifyFidelity(p1, big);
|
||
var offBigCfg = MakeConfig(showSize, primary, knots, anchors, "off"); offBigCfg.Curve = false;
|
||
var cf = ShapingOracle.SeaIdentity(Shaping.Shape(p1, offBigCfg), big, sea);
|
||
GD.Print($" {cb}");
|
||
GD.Print($" {cf}");
|
||
if (!cb.Passed || !cf.Passed) hardOk = false;
|
||
|
||
var (b100, b220) = ShapingOracle.LandAbove(big, sea);
|
||
GD.Print($" land >100 m {b100:F2}% >220 m {b220:F2}% (at {showSize})");
|
||
rows.Add(WriteVariant(batchRoot, big, sea, anchors, skipRaw));
|
||
showNote = $"seed {primary} at {showSize}; >100 m {b100:F2}%, >220 m {b220:F2}%";
|
||
}
|
||
|
||
WriteIndex(batchRoot, mapSize, showSize, seeds, primary, anchors, results, calRestored,
|
||
calBigger, calSharper, pcts, rawQ, outQ, hard, soft, mountain, rows, hardOk, showNote, sea);
|
||
|
||
GD.Print("\n==================================================================");
|
||
GD.Print($" DONE — {batchRoot}");
|
||
GD.Print($" ORACLE {(hardOk ? "HARD CHECKS ALL PASS" : "*** HARD FAILURES ***")}");
|
||
GD.Print("==================================================================");
|
||
GetTree().Quit(hardOk ? 0 : 3);
|
||
}
|
||
|
||
/// <remarks>
|
||
/// ⭐ rivers/01 — FAMILY-OFF PINNED, not defaulted. This is chat-2 CURVE development: authored
|
||
/// and judged before the shape family existed, on the family-off distribution the knots are
|
||
/// percentiles of. The re-baseline flipped the bare defaults family-ON, so the pin is what
|
||
/// keeps this tool measuring the thing it was written to measure.
|
||
/// → <see cref="TerrainGenConfig.WithFamilyOff"/>.
|
||
/// </remarks>
|
||
private static TerrainGenConfig MakeConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a, string label)
|
||
=> new TerrainGenConfig
|
||
{
|
||
MapSize = mapSize, Seed = seed, VariantLabel = label,
|
||
Curve = true, ShelfDetail = false, Knots = k, Anchors = a,
|
||
LowlandCeilingM = 30f,
|
||
}.WithFamilyOff();
|
||
|
||
// ---- output ---------------------------------------------------------
|
||
|
||
private static string WriteVariant(string batchRoot, Pass2Result p2, float sea,
|
||
CurveAnchors anchors, bool skipRaw)
|
||
{
|
||
string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}");
|
||
DirAccess.MakeDirRecursiveAbsolute(dir);
|
||
|
||
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"));
|
||
|
||
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);
|
||
LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap, p2.VariantLabel.ToUpperInvariant())
|
||
.SavePng(Path.Combine(dir, "relief.png"));
|
||
|
||
var (a100, a220) = ShapingOracle.LandAbove(p2, sea);
|
||
GD.Print($" {p2.VariantLabel,-30} seed {p2.Seed,-11} h[{p2.HMin,7:F3} .. {p2.HMax,6:F3}] " +
|
||
$" >100m {a100,5:F2}% >220m {a220,5:F2}% {p2.ElapsedMs,5} ms");
|
||
|
||
return $"| `{p2.Seed}_{p2.VariantLabel}` | {p2.Seed} | {p2.VariantLabel} | {p2.HMin:F3} | {p2.HMax:F3} | " +
|
||
$"{a100:F2}% | {a220:F2}% | {gMin:F3}..{gMax:F3} | {p2.ElapsedMs} ms |";
|
||
}
|
||
|
||
private static void DrawShaped(Pass2Result p2, CurveAnchors a, int seed, int mapSize,
|
||
string batchRoot, int order, float sea)
|
||
{
|
||
var shaped = new LandHistogram(sea);
|
||
shaped.Accumulate(p2.Height, mapSize);
|
||
|
||
float top = MathF.Ceiling(shaped.MaxLand * 20f) / 20f;
|
||
var display = shaped.Rebin((top - shaped.SeaLevel) / 360f);
|
||
var (a100, a220) = ShapingOracle.LandAbove(p2, sea);
|
||
|
||
var o = new HistogramRenderer.Options
|
||
{
|
||
Title = $"{p2.VariantLabel.ToUpperInvariant()} - SEED {seed}",
|
||
Subtitle = $"LAND ABOVE 100M {a100:F2} PCT - ABOVE 220M {a220:F2} PCT",
|
||
XAxisLabel = "RAW HEIGHT (POST-CURVE)",
|
||
XTop = top,
|
||
Footer = $"{shaped.TotalLand} LAND COLUMNS AT MAPSIZE {mapSize}",
|
||
};
|
||
|
||
// The two heights the restoration is measured at, on every plate, so the three-way
|
||
// contrast can be read off the same reference lines.
|
||
o.Markers.Add(new HistogramRenderer.Marker { Value = a.Sea + WorldScale.RawFromMetres(100f), Label = "100M" });
|
||
o.Markers.Add(new HistogramRenderer.Marker { Value = a.Sea + WorldScale.RawFromMetres(220f), Label = "220M" });
|
||
o.Markers.Add(new HistogramRenderer.Marker { Value = a.PeakCap, Label = "CAP 420M", Strong = false });
|
||
if (p2.Continuous != null)
|
||
o.Markers.Add(new HistogramRenderer.Marker { Value = p2.Continuous.CeilingOut, Label = "LOWLAND", Strong = false });
|
||
|
||
string file = $"hist_{seed}_{order}_{p2.VariantLabel}.png";
|
||
HistogramRenderer.SavePng(display, o, Path.Combine(batchRoot, file));
|
||
GD.Print($" {file}");
|
||
}
|
||
|
||
private static void WriteIndex(string batchRoot, int mapSize, int showSize, int[] seeds, int primary,
|
||
CurveAnchors a, Dictionary<(int, string), Pass2Result> results,
|
||
ClimbCalibration calRestored, ClimbCalibration calBigger, ClimbCalibration calSharper,
|
||
double[] pcts, float[] rawQ, float[] outQ,
|
||
List<ShapingOracle.Check> hard, List<ShapingOracle.Check> soft, List<ShapingOracle.Check> mountain,
|
||
List<string> rows, bool hardOk, string showNote, float sea)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("# Batch 03 — restore the mountain, as a smooth slope");
|
||
sb.AppendLine();
|
||
sb.AppendLine("The continuous climb's control points are now **measured off the staircase** instead of");
|
||
sb.AppendLine("invented from shape knobs. Same mountain mass, zero terraces. The lowlands are still");
|
||
sb.AppendLine("preserved bit-for-bit (oracle d).");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## ⭐ Open this first");
|
||
sb.AppendLine();
|
||
sb.AppendLine($"1. **`{primary}_continuous_restored_showpiece/relief.png`** — the centerpiece ({showNote}).");
|
||
sb.AppendLine("2. **The three-way histogram contrast**, adjacent by filename:");
|
||
sb.AppendLine($" - `hist_{primary}_1_staircase.png` — the mass, parked in two spikes");
|
||
sb.AppendLine($" - `hist_{primary}_2_continuous_02default.png` — spikes gone, **and so is the mass**");
|
||
sb.AppendLine($" - `hist_{primary}_3_continuous_restored.png` — ⭐ **the mass back, spread smooth**");
|
||
sb.AppendLine(" Every plate carries the same 100 m / 220 m reference lines.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## The restoration, measured");
|
||
sb.AppendLine();
|
||
sb.AppendLine("| Variant | land >100 m | land >220 m |");
|
||
sb.AppendLine("|---|---|---|");
|
||
foreach (string label in new[] { "staircase", "continuous_02default", "continuous_restored", "continuous_bigger", "continuous_sharper_peak" })
|
||
{
|
||
var (x100, x220) = ShapingOracle.LandAbove(results[(primary, label)], sea);
|
||
string star = label == "continuous_restored" ? " ⭐" : label == "staircase" ? " *(target)*" : "";
|
||
sb.AppendLine($"| `{label}`{star} | {x100:F2} % | {x220:F2} % |");
|
||
}
|
||
sb.AppendLine();
|
||
sb.AppendLine($"*(seed {primary} at {mapSize}; per-seed rows in Results below.)*");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## The calibration");
|
||
sb.AppendLine();
|
||
sb.AppendLine("Measured on the 6-seed pool, above-ceiling land only:");
|
||
sb.AppendLine();
|
||
sb.AppendLine("| percentile | raw | staircase output |");
|
||
sb.AppendLine("|---|---|---|");
|
||
for (int i = 0; i < pcts.Length; i++)
|
||
sb.AppendLine($"| P{pcts[i]:F0} | {rawQ[i]:F4} | **{WorldScale.MetresFromRaw(outQ[i] - sea):F0} m** |");
|
||
sb.AppendLine();
|
||
sb.AppendLine("| Variant | knobs | control points (u,v) |");
|
||
sb.AppendLine("|---|---|---|");
|
||
sb.AppendLine($"| `continuous_restored` | {calRestored.Describe().Split('·')[0].Trim()} | `{calRestored.Describe().Split('·')[1].Trim()}` |");
|
||
sb.AppendLine($"| `continuous_bigger` | {calBigger.Describe().Split('·')[0].Trim()} | `{calBigger.Describe().Split('·')[1].Trim()}` |");
|
||
sb.AppendLine($"| `continuous_sharper_peak` | {calSharper.Describe().Split('·')[0].Trim()} | `{calSharper.Describe().Split('·')[1].Trim()}` |");
|
||
sb.AppendLine();
|
||
sb.AppendLine($"`floored` counts segments the no-bench floor had to lift — i.e. where the staircase was flat.");
|
||
sb.AppendLine();
|
||
sb.AppendLine("## ⚠ The palette is PROVISIONAL");
|
||
sb.AppendLine();
|
||
sb.AppendLine("`ProvisionalEven` — the CostaRica colours re-spaced evenly SEA → 420 m. Final calibration");
|
||
sb.AppendLine("waits for the chosen 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 — upper climb slope profile (e):");
|
||
sb.AppendLine();
|
||
sb.AppendLine(ShapingOracle.ToMarkdownTable(soft));
|
||
sb.AppendLine("(g) mountain restored — **reported, not gated** (it is a taste target the developer tunes):");
|
||
sb.AppendLine();
|
||
sb.AppendLine(ShapingOracle.ToMarkdownTable(mountain));
|
||
sb.AppendLine("## Disposability");
|
||
sb.AppendLine();
|
||
sb.AppendLine("| Artifact | Keep? |");
|
||
sb.AppendLine("|---|---|");
|
||
sb.AppendLine("| `relief.png`, `hist_*.png`, `INDEX.md` | **keep** |");
|
||
sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
|
||
sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code (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 | >100 m | >220 m | grayscale range | time |");
|
||
sb.AppendLine("|---|---|---|---|---|---|---|---|---|");
|
||
foreach (string row in rows) sb.AppendLine(row);
|
||
sb.AppendLine();
|
||
sb.AppendLine($"MapSize {mapSize}, showpiece {showSize}, seeds {string.Join(", ", seeds)}. {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;
|
||
}
|
||
}
|
||
}
|