Two paired detail passes on the locked v5 skeleton, output-heights only (classify path untouched): PASS A — shelf micro-relief: +-ShelfReliefAmp (default 3 m) Simplex skin (seed resolved+7409, ~40 undulations/island ~ 200 m features), weighted by shelf-ness (full mid-shelf, feathered to zero 30% of a band half-width into the risers) — both shelves get their rolling texture back; risers and peaks untouched. PASS B — drainage incision: D8 steepest-descent routing + height- ordered flow accumulation over the curved+relieved land (TerrainDetailPass — pure array machinery, named C++ candidate per D-035); depth = K*accum^p*slope (K=0.78, p=0.45 concave, cap 30 m), masked full on risers / 30% on shelves / zero on the toe, above the plateau top, and within 1.2x CraterRadius (feathered to 1.4x); hard clamp: carved height >= sea + 1 m. Ordering: curve -> relief -> incision -> crater carve (the carve stays the final authority; carve moved to its own pass 2c, bit-identical expression on both maps). The pass prints its own MEASURED depth distribution. Gate TerrainDetail "off"|"v1" (default v1; both passes one judged unit; no-op without the curve) + ShelfReliefAmp dial. New TDTL section (38 B: version + relief/incision params + seed offset) across writer/parser/harness — blueprints stay self-describing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
270 lines
10 KiB
C#
270 lines
10 KiB
C#
using Godot;
|
|
using System.Collections.Generic;
|
|
using IslaApocalypse.Core;
|
|
|
|
/// <summary>
|
|
/// The blueprint round-trip oracle (terrain-water task 02). Runs headless in seconds,
|
|
/// no generation, no road pass:
|
|
///
|
|
/// 1. load a known-good v1 blueprint (default: the preserved reference copy of seed
|
|
/// 1409879727) through the real parser,
|
|
/// 2. write it back out as v2 through the real writer (to a harness-only name — the
|
|
/// reference file is never overwritten),
|
|
/// 3. load the v2 file through the real parser,
|
|
/// 4. assert semantic equality: heights bitwise, biomes equal, towns equal in
|
|
/// position/tier/count, each road tier point-for-point.
|
|
///
|
|
/// Params and the per-town highway flag are EXCLUDED from equality — a v1 source
|
|
/// cannot supply them; they are verified in the full-generation acceptance run.
|
|
///
|
|
/// Run: Godot --headless --path <repo> res://Tools/Scenes/RoundTripHarness.tscn
|
|
/// Env overrides: HARNESS_V1_PATH (source file), HARNESS_V2_PATH (output file).
|
|
/// Exit code 0 = PASS, 1 = FAIL. Worth keeping permanently as a format regression test.
|
|
/// </summary>
|
|
public partial class RoundTripHarness : Node
|
|
{
|
|
public override void _Ready()
|
|
{
|
|
string v1Path = OS.GetEnvironment("HARNESS_V1_PATH");
|
|
if (string.IsNullOrEmpty(v1Path))
|
|
v1Path = ProjectSettings.GlobalizePath("user://reference_1409879727/MapData_Seed_1409879727.dat");
|
|
string v2Path = OS.GetEnvironment("HARNESS_V2_PATH");
|
|
if (string.IsNullOrEmpty(v2Path))
|
|
v2Path = ProjectSettings.GlobalizePath("user://Harness_RoundTrip_v2.dat");
|
|
|
|
GD.Print($"[Harness] v1 source: {v1Path}");
|
|
GD.Print($"[Harness] v2 output: {v2Path}");
|
|
|
|
bool pass = false;
|
|
try
|
|
{
|
|
pass = RunRoundTrip(v1Path, v2Path);
|
|
}
|
|
catch (System.Exception e)
|
|
{
|
|
GD.PrintErr($"[Harness] EXCEPTION: {e}");
|
|
}
|
|
|
|
GD.Print(pass ? "[Harness] RESULT: PASS" : "[Harness] RESULT: FAIL");
|
|
GetTree().Quit(pass ? 0 : 1);
|
|
}
|
|
|
|
private bool RunRoundTrip(string v1Path, string v2Path)
|
|
{
|
|
ulong t0 = Time.GetTicksMsec();
|
|
WorldBlueprint original = MapDataParser.LoadMapDataFromPath(v1Path);
|
|
ulong t1 = Time.GetTicksMsec();
|
|
if (original == null) { GD.PrintErr("[Harness] v1 load failed."); return false; }
|
|
GD.Print($"[Harness] v1 parse: {(t1 - t0) / 1000.0:F1}s (format v{original.FormatVersion})");
|
|
|
|
BlueprintWriter.WriteV2(v2Path, original);
|
|
ulong t2 = Time.GetTicksMsec();
|
|
GD.Print($"[Harness] v2 write: {(t2 - t1) / 1000.0:F1}s");
|
|
|
|
WorldBlueprint reread = MapDataParser.LoadMapDataFromPath(v2Path);
|
|
ulong t3 = Time.GetTicksMsec();
|
|
if (reread == null) { GD.PrintErr("[Harness] v2 load failed."); return false; }
|
|
GD.Print($"[Harness] v2 parse: {(t3 - t2) / 1000.0:F1}s (format v{reread.FormatVersion})");
|
|
|
|
return Compare(original, reread);
|
|
}
|
|
|
|
private bool Compare(WorldBlueprint a, WorldBlueprint b)
|
|
{
|
|
bool ok = true;
|
|
|
|
if (a.MapSize != b.MapSize)
|
|
{
|
|
GD.PrintErr($"[Harness] MapSize mismatch: {a.MapSize} vs {b.MapSize}");
|
|
return false; // nothing below is comparable
|
|
}
|
|
|
|
// Heights: bitwise-identical floats.
|
|
long heightDiffs = 0;
|
|
for (int x = 0; x < a.MapSize; x++)
|
|
for (int y = 0; y < a.MapSize; y++)
|
|
if (System.BitConverter.SingleToInt32Bits(a.HeightMap[x, y]) !=
|
|
System.BitConverter.SingleToInt32Bits(b.HeightMap[x, y]))
|
|
heightDiffs++;
|
|
if (heightDiffs > 0) { GD.PrintErr($"[Harness] {heightDiffs} height pixels differ bitwise."); ok = false; }
|
|
|
|
// Biomes: equal values (i32 -> u8 narrowing is documented and lossless for 0..255).
|
|
long biomeDiffs = 0;
|
|
for (int x = 0; x < a.MapSize; x++)
|
|
for (int y = 0; y < a.MapSize; y++)
|
|
if (a.BiomeMap[x, y] != b.BiomeMap[x, y])
|
|
biomeDiffs++;
|
|
if (biomeDiffs > 0) { GD.PrintErr($"[Harness] {biomeDiffs} biome pixels differ."); ok = false; }
|
|
|
|
// Towns: identical position/tier/count (highway flag deliberately not compared).
|
|
if (a.Towns.Count != b.Towns.Count)
|
|
{
|
|
GD.PrintErr($"[Harness] Town count mismatch: {a.Towns.Count} vs {b.Towns.Count}");
|
|
ok = false;
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < a.Towns.Count; i++)
|
|
{
|
|
if (a.Towns[i].Position != b.Towns[i].Position || a.Towns[i].Tier != b.Towns[i].Tier)
|
|
{
|
|
GD.PrintErr($"[Harness] Town {i} mismatch: " +
|
|
$"{a.Towns[i].Position}/{a.Towns[i].Tier} vs {b.Towns[i].Position}/{b.Towns[i].Tier}");
|
|
ok = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
ok &= CompareRoads("Highway", a.Highways, b.Highways);
|
|
ok &= CompareRoads("Branch", a.BranchRoads, b.BranchRoads);
|
|
ok &= CompareRoads("Rugged", a.RuggedRoads, b.RuggedRoads);
|
|
ok &= CompareRoads("Trail", a.TrailRoads, b.TrailRoads);
|
|
ok &= CompareWater(a, b);
|
|
ok &= CompareTerrainCurve(a, b);
|
|
ok &= CompareTerrainDetail(a, b);
|
|
|
|
if (ok)
|
|
GD.Print($"[Harness] Semantic equality holds: {a.MapSize}x{a.MapSize} grid, " +
|
|
$"{a.Towns.Count} towns, roads {a.Highways.Count}/{a.BranchRoads.Count}/" +
|
|
$"{a.RuggedRoads.Count}/{a.TrailRoads.Count} paths.");
|
|
return ok;
|
|
}
|
|
|
|
// Water sections (WBID/WBTB/WSRF) are compared whenever the SOURCE carries them —
|
|
// the regression test grows with the format. A source without water (legacy v1)
|
|
// must round-trip to a file without water.
|
|
private bool CompareWater(WorldBlueprint a, WorldBlueprint b)
|
|
{
|
|
if (a.WaterBodyIds == null && b.WaterBodyIds == null)
|
|
{
|
|
GD.Print("[Harness] Water sections: absent in source — nothing to compare (and none reappeared).");
|
|
return true;
|
|
}
|
|
if (a.WaterBodyIds == null || b.WaterBodyIds == null)
|
|
{
|
|
GD.PrintErr($"[Harness] Water sections presence mismatch: source {(a.WaterBodyIds != null ? "has" : "lacks")} them, reread {(b.WaterBodyIds != null ? "has" : "lacks")} them.");
|
|
return false;
|
|
}
|
|
|
|
bool ok = true;
|
|
long idDiffs = 0, surfDiffs = 0;
|
|
int n = a.MapSize;
|
|
for (int x = 0; x < n; x++)
|
|
{
|
|
for (int y = 0; y < n; y++)
|
|
{
|
|
if (a.WaterBodyIds[x, y] != b.WaterBodyIds[x, y]) idDiffs++;
|
|
ushort sa = a.WaterSurfaceQ != null ? a.WaterSurfaceQ[x, y] : (ushort)0;
|
|
ushort sb = b.WaterSurfaceQ != null ? b.WaterSurfaceQ[x, y] : (ushort)0;
|
|
if (sa != sb) surfDiffs++;
|
|
}
|
|
}
|
|
if (idDiffs > 0) { GD.PrintErr($"[Harness] {idDiffs} WBID pixels differ."); ok = false; }
|
|
if (surfDiffs > 0) { GD.PrintErr($"[Harness] {surfDiffs} WSRF pixels differ."); ok = false; }
|
|
|
|
if (a.WaterBodies.Count != b.WaterBodies.Count)
|
|
{
|
|
GD.PrintErr($"[Harness] Water body count mismatch: {a.WaterBodies.Count} vs {b.WaterBodies.Count}");
|
|
ok = false;
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < a.WaterBodies.Count; i++)
|
|
{
|
|
var wa = a.WaterBodies[i];
|
|
var wb = b.WaterBodies[i];
|
|
if (wa.Id != wb.Id || wa.Type != wb.Type || wa.Salinity != wb.Salinity ||
|
|
System.BitConverter.SingleToInt32Bits(wa.SurfaceLevel) != System.BitConverter.SingleToInt32Bits(wb.SurfaceLevel) ||
|
|
wa.PixelCount != wb.PixelCount || wa.Centroid != wb.Centroid)
|
|
{
|
|
GD.PrintErr($"[Harness] Water body {i} mismatch (id {wa.Id} vs {wb.Id}).");
|
|
ok = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ok) GD.Print($"[Harness] Water sections equal: {a.WaterBodies.Count} bodies, WBID+WSRF grids identical.");
|
|
return ok;
|
|
}
|
|
|
|
private bool CompareTerrainCurve(WorldBlueprint a, WorldBlueprint b)
|
|
{
|
|
if (a.TerrainCurve == null && b.TerrainCurve == null)
|
|
{
|
|
GD.Print("[Harness] TCRV: absent in source — nothing to compare (and none reappeared).");
|
|
return true;
|
|
}
|
|
if (a.TerrainCurve == null || b.TerrainCurve == null)
|
|
{
|
|
GD.PrintErr("[Harness] TCRV presence mismatch between source and reread.");
|
|
return false;
|
|
}
|
|
var ca = a.TerrainCurve; var cb = b.TerrainCurve;
|
|
bool same = ca.Version == cb.Version;
|
|
float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.SpikeMax, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope,
|
|
ca.BenchAmp, ca.PlateauAmp, ca.ShelfSpanMin, ca.ShelfSpanMax, ca.ElevFreqIslands, ca.StrengthFreqIslands,
|
|
ca.BenchSeedOffset, ca.PlateauSeedOffset, ca.StrengthSeedOffset,
|
|
ca.PresetId, ca.K5, ca.K6 };
|
|
float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.SpikeMax, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope,
|
|
cb.BenchAmp, cb.PlateauAmp, cb.ShelfSpanMin, cb.ShelfSpanMax, cb.ElevFreqIslands, cb.StrengthFreqIslands,
|
|
cb.BenchSeedOffset, cb.PlateauSeedOffset, cb.StrengthSeedOffset,
|
|
cb.PresetId, cb.K5, cb.K6 };
|
|
for (int i = 0; i < fa.Length; i++)
|
|
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
|
|
if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; }
|
|
GD.Print($"[Harness] TCRV equal (curve v{ca.Version}).");
|
|
return true;
|
|
}
|
|
|
|
private bool CompareTerrainDetail(WorldBlueprint a, WorldBlueprint b)
|
|
{
|
|
if (a.TerrainDetail == null && b.TerrainDetail == null)
|
|
{
|
|
GD.Print("[Harness] TDTL: absent in source — nothing to compare (and none reappeared).");
|
|
return true;
|
|
}
|
|
if (a.TerrainDetail == null || b.TerrainDetail == null)
|
|
{
|
|
GD.PrintErr("[Harness] TDTL presence mismatch between source and reread.");
|
|
return false;
|
|
}
|
|
var da = a.TerrainDetail; var db = b.TerrainDetail;
|
|
float[] fa = { da.Version, da.ReliefAmpM, da.ReliefFreqIslands, da.IncK, da.IncP, da.IncCapM, da.SeaClampRaw, da.CraterExclFactor, da.ShelfIncWeight, da.ReliefSeedOffset };
|
|
float[] fb = { db.Version, db.ReliefAmpM, db.ReliefFreqIslands, db.IncK, db.IncP, db.IncCapM, db.SeaClampRaw, db.CraterExclFactor, db.ShelfIncWeight, db.ReliefSeedOffset };
|
|
for (int i = 0; i < fa.Length; i++)
|
|
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i]))
|
|
{
|
|
GD.PrintErr("[Harness] TDTL fields differ.");
|
|
return false;
|
|
}
|
|
GD.Print($"[Harness] TDTL equal (detail v{da.Version}).");
|
|
return true;
|
|
}
|
|
|
|
private bool CompareRoads(string tier, List<Vector2[]> a, List<Vector2[]> b)
|
|
{
|
|
if (a.Count != b.Count)
|
|
{
|
|
GD.PrintErr($"[Harness] {tier} path count mismatch: {a.Count} vs {b.Count}");
|
|
return false;
|
|
}
|
|
for (int i = 0; i < a.Count; i++)
|
|
{
|
|
if (a[i].Length != b[i].Length)
|
|
{
|
|
GD.PrintErr($"[Harness] {tier} path {i} length mismatch: {a[i].Length} vs {b[i].Length}");
|
|
return false;
|
|
}
|
|
for (int p = 0; p < a[i].Length; p++)
|
|
{
|
|
if (System.BitConverter.SingleToInt32Bits(a[i][p].X) != System.BitConverter.SingleToInt32Bits(b[i][p].X) ||
|
|
System.BitConverter.SingleToInt32Bits(a[i][p].Y) != System.BitConverter.SingleToInt32Bits(b[i][p].Y))
|
|
{
|
|
GD.PrintErr($"[Harness] {tier} path {i} point {p} differs: {a[i][p]} vs {b[i][p]}");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|