rivers/03b: routing refinement — three deliberate divergences from the faithful port
NOT a port. Each fix corrects a faithful reference behaviour that produced a
physically-wrong result, on the developer's explicit call. basinHasLake is KEPT
as the sort. Courses only: no height mutated, no water filled or created —
asserted per seed by a raw-bit digest of both height fields.
- FIX 1 rim cap (ISLA_RIM_CAP_M, default 30 m): a route to the sea that must
climb higher than this above its terminal is refused; the river ends at its
own terminal. Reference routes at any cost (rivers/03 found a 66.7 m one).
- FIX 3 lake targets: a router stops at the nearer of {ocean, significant lake},
so it cannot skirt a lake to reach a distant coast. Reference targets ocean only.
- FIX 2 confluence: courses laid biggest-first join on TRUE cell intersection
(never proximity); the smaller becomes a tributary and adopts the bigger one's
downstream and terminus. Reference lays routes independently — rivers/03 found
two rivers at the identical ocean cell on every seed.
All three are off by default (RiverRouting.Options.Faithful), so rivers/03 still
reproduces bit-for-bit from the same tool.
Taste gate: nothing locked, nothing graduated.
This commit is contained in:
parent
4e4be6a83e
commit
b5ceca0419
3 changed files with 688 additions and 38 deletions
|
|
@ -520,6 +520,180 @@ namespace IslaApocalypse.Tools
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══ ⭐⭐ THE REFINED MIX (rivers/03b) — five classes, and the dendritic tree ═══════════════
|
||||||
|
//
|
||||||
|
// Same base, same colours where they carry over, and the SAME fixed width scale as rivers/02b
|
||||||
|
// and rivers/03, so this plate can be laid beside `03_lowland_routing/<seed>/routed_mix.png` and
|
||||||
|
// read as a before/after rather than as two different pictures.
|
||||||
|
//
|
||||||
|
// `RoutedMix` above is left exactly as rivers/03 produced it — that batch stays reproducible.
|
||||||
|
|
||||||
|
/// <summary>⭐ rivers/03b: a router that stopped at a significant lake instead of skirting it.</summary>
|
||||||
|
private static readonly Color LakeFedStem = new(0.520f, 0.380f, 0.780f);
|
||||||
|
private static readonly Color LakeFedReach = new(0.720f, 0.560f, 1.000f);
|
||||||
|
/// <summary>⚠ rivers/03b: refused by the rim cap — it would have been an uphill river.</summary>
|
||||||
|
private static readonly Color Walled = new(0.950f, 0.330f, 0.330f);
|
||||||
|
/// <summary>Where two courses actually meet.</summary>
|
||||||
|
private static readonly Color Junction = new(1.000f, 1.000f, 1.000f);
|
||||||
|
|
||||||
|
private static (Color stem, Color reach) ClassColours(RiverRouting.RiverClass c) => c switch
|
||||||
|
{
|
||||||
|
RiverRouting.RiverClass.OceanTrunk => (Trunk, Trunk),
|
||||||
|
RiverRouting.RiverClass.RoutedGiant => (RoutedStem, RoutedReach),
|
||||||
|
RiverRouting.RiverClass.LakeFed => (LakeFedStem, LakeFedReach),
|
||||||
|
RiverRouting.RiverClass.WalledOff => (Walled, Walled),
|
||||||
|
_ => (Giant, Giant),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐⭐ THE RESHAPED MIX — natural trunks, routed-through, lake-fed, natural lake-enders and
|
||||||
|
/// walled-off lake-enders, drawn as a dendritic TREE rather than as independent courses.
|
||||||
|
///
|
||||||
|
/// Each river draws only its OWN reach — truncated at its confluence junction if it joined one —
|
||||||
|
/// so tributaries merge into a single downstream line instead of running as parallel duplicates.
|
||||||
|
/// A white dot marks every junction. Within a river, the natural upland stem is drawn in the
|
||||||
|
/// muted tone and the lowland reach routing added in the bright one, exactly as rivers/03.
|
||||||
|
/// </summary>
|
||||||
|
public static Image RefinedMix(List<RiverRouting.RoutedRiver> rivers, Image img, int n,
|
||||||
|
string title, string subtitle, string capLine)
|
||||||
|
{
|
||||||
|
if (rivers.Count == 0) return img;
|
||||||
|
int mark = n >= 4096 ? 18 : 10;
|
||||||
|
|
||||||
|
var byArea = new List<RiverRouting.RoutedRiver>(rivers);
|
||||||
|
byArea.Sort((a, b) => b.Candidate.DrainagePx.CompareTo(a.Candidate.DrainagePx));
|
||||||
|
|
||||||
|
// Smallest first, so the biggest rivers finish on top.
|
||||||
|
for (int i = byArea.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var r = byArea[i];
|
||||||
|
int w = StemWidthFixed(r.Candidate.DrainagePx);
|
||||||
|
var (stemCol, reachCol) = ClassColours(r.Class);
|
||||||
|
var cells = r.CellPath;
|
||||||
|
if (cells == null || cells.Count == 0) continue;
|
||||||
|
|
||||||
|
// Its OWN reach: everything up to the junction, or the whole course if it kept its mouth.
|
||||||
|
int own = r.Joined ? OwnLength(r) : cells.Count;
|
||||||
|
int stemEnd = Math.Min(own, Math.Max(1, r.StemCells));
|
||||||
|
|
||||||
|
Polyline(img, Slice(cells, 0, stemEnd), n, stemCol, w);
|
||||||
|
if (own > stemEnd) Polyline(img, Slice(cells, stemEnd - 1, own), n, reachCol, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminus markers — read through the CONFLUENCE ROOT, because a tributary's mouth is its
|
||||||
|
// trunk's mouth and marking its own truncated end would invent a terminus it does not have.
|
||||||
|
foreach (var r in byArea)
|
||||||
|
{
|
||||||
|
var c = r.Candidate;
|
||||||
|
if (r.Joined)
|
||||||
|
{
|
||||||
|
Disc(img, r.JunctionCell.x, r.JunctionCell.y, Math.Max(4, mark / 2), n, Junction);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var (stemCol, reachCol) = ClassColours(r.Class);
|
||||||
|
switch (r.Class)
|
||||||
|
{
|
||||||
|
case RiverRouting.RiverClass.OceanTrunk:
|
||||||
|
Square(img, c.TermX, c.TermY, mark, n, Trunk);
|
||||||
|
break;
|
||||||
|
case RiverRouting.RiverClass.RoutedGiant:
|
||||||
|
if (r.Lowland != null && r.Lowland.Reached)
|
||||||
|
{
|
||||||
|
var t = r.Lowland.Target;
|
||||||
|
Square(img, (int)t.x, (int)t.y, mark, n, reachCol);
|
||||||
|
Ring(img, (int)t.x, (int)t.y, mark + 8, n, Ink, 3);
|
||||||
|
MarkRim(img, r.Lowland, n, mark);
|
||||||
|
}
|
||||||
|
Ring(img, c.TermX, c.TermY, mark, n, stemCol, 4);
|
||||||
|
break;
|
||||||
|
case RiverRouting.RiverClass.LakeFed:
|
||||||
|
if (r.Lowland != null && r.Lowland.Reached)
|
||||||
|
{
|
||||||
|
var t = r.Lowland.Target;
|
||||||
|
Disc(img, (int)t.x, (int)t.y, mark, n, reachCol);
|
||||||
|
Ring(img, (int)t.x, (int)t.y, mark + 8, n, Ink, 3);
|
||||||
|
}
|
||||||
|
Ring(img, c.TermX, c.TermY, mark, n, stemCol, 4);
|
||||||
|
break;
|
||||||
|
case RiverRouting.RiverClass.WalledOff:
|
||||||
|
// ⚠ It ends at its own terminal. A cross-less ring plus the rim it could not clear.
|
||||||
|
Disc(img, c.TermX, c.TermY, mark, n, Walled);
|
||||||
|
Ring(img, c.TermX, c.TermY, mark + 8, n, Ink, 3);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Disc(img, c.TermX, c.TermY, mark, n, Giant);
|
||||||
|
Ring(img, c.TermX, c.TermY, mark + 8, n, Ink, 3);
|
||||||
|
if (r.Lowland != null && r.Lowland.Reached)
|
||||||
|
Ring(img, (int)r.Lowland.Target.x, (int)r.Lowland.Target.y, mark, n, Giant, 4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- labels ----
|
||||||
|
int ls = n >= 4096 ? 4 : 3;
|
||||||
|
var placer = new LabelPlacer(n, ls, headerLines: 9);
|
||||||
|
int dropped = 0;
|
||||||
|
foreach (var r in byArea)
|
||||||
|
{
|
||||||
|
var c = r.Candidate;
|
||||||
|
var (stemCol, reachCol) = ClassColours(r.Class);
|
||||||
|
string tag = r.Class switch
|
||||||
|
{
|
||||||
|
RiverRouting.RiverClass.OceanTrunk => "TRUNK",
|
||||||
|
RiverRouting.RiverClass.RoutedGiant => $"SEA RIM {(r.Lowland != null ? r.Lowland.RimClimbM : 0f):F0}M",
|
||||||
|
RiverRouting.RiverClass.LakeFed => "LAKE-FED",
|
||||||
|
RiverRouting.RiverClass.WalledOff => $"WALLED {r.CappedRimM:F0}M",
|
||||||
|
_ => "LAKE",
|
||||||
|
};
|
||||||
|
if (r.Joined) tag += $" INTO R{r.ConfluenceParentRank}";
|
||||||
|
int lx = r.Joined ? r.JunctionCell.x : c.TermX;
|
||||||
|
int ly = r.Joined ? r.JunctionCell.y : c.TermY;
|
||||||
|
if (!placer.Place(img, $"{DrainageLabel(c.DrainagePx)} R{c.Rank} {tag}", lx, ly, mark,
|
||||||
|
r.Joined ? Junction : reachCol)) dropped++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int trunks = 0, routed = 0, lakeFed = 0, natural = 0, walled = 0, joined = 0;
|
||||||
|
foreach (var r in byArea)
|
||||||
|
{
|
||||||
|
switch (r.Class)
|
||||||
|
{
|
||||||
|
case RiverRouting.RiverClass.OceanTrunk: trunks++; break;
|
||||||
|
case RiverRouting.RiverClass.RoutedGiant: routed++; break;
|
||||||
|
case RiverRouting.RiverClass.LakeFed: lakeFed++; break;
|
||||||
|
case RiverRouting.RiverClass.WalledOff: walled++; break;
|
||||||
|
default: natural++; break;
|
||||||
|
}
|
||||||
|
if (r.Joined) joined++;
|
||||||
|
}
|
||||||
|
int s2 = n >= 4096 ? 4 : 3; int lh = TinyFont.Height(s2) + 6;
|
||||||
|
TinyFont.Draw(img, title, 12, 12, s2, Ink);
|
||||||
|
TinyFont.Draw(img, subtitle, 12, 12 + lh, s2, Ink);
|
||||||
|
TinyFont.Draw(img, $"CYAN: NATURAL OCEAN TRUNK ({trunks}) GREEN: ROUTED THROUGH TO THE SEA ({routed}) - DARK = NATURAL STEM, BRIGHT = THE REACH ROUTING ADDED", 12, 12 + lh * 2, s2, Trunk);
|
||||||
|
TinyFont.Draw(img, $"VIOLET: LAKE-FED ({lakeFed}) - A DRY BASIN THAT MET A SIGNIFICANT LAKE BEFORE THE SEA AND STOPS THERE (FIX 3)", 12, 12 + lh * 3, s2, LakeFedReach);
|
||||||
|
TinyFont.Draw(img, $"RED: WALLED OFF ({walled}) - {capLine} (FIX 1)", 12, 12 + lh * 4, s2, Walled);
|
||||||
|
TinyFont.Draw(img, $"ORANGE: NATURAL LAKE-ENDER ({natural}) - ITS BASIN ALREADY HOLDS A LAKE, SO ITS RIVER FEEDS IT", 12, 12 + lh * 5, s2, Giant);
|
||||||
|
TinyFont.Draw(img, $"WHITE DOT: CONFLUENCE ({joined} JOINED) - A TRIBUTARY MERGING INTO A BIGGER RIVER, NOT A PARALLEL DUPLICATE (FIX 2)", 12, 12 + lh * 6, s2, Junction);
|
||||||
|
TinyFont.Draw(img, $"YELLOW RING = THE RIM A ROUTED RIVER CLIMBED OVER. WIDTH: {StemWidthLaw()} - AS RIVERS/02B AND 03", 12, 12 + lh * 7, s2, RimMark);
|
||||||
|
TinyFont.Draw(img, "COURSES ONLY - NO HEIGHT MUTATED, NO WATER FILLED OR CREATED, NOTHING CARVED. PROVISIONALROUTE NOT DRAWN." +
|
||||||
|
(dropped > 0 ? $" ({dropped} LABEL(S) DROPPED)" : ""), 12, 12 + lh * 8, s2, Ink);
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>How many leading cells of a joined river's path are its own, up to the junction.</summary>
|
||||||
|
private static int OwnLength(RiverRouting.RoutedRiver r)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < r.CellPath.Count; i++)
|
||||||
|
if (r.CellPath[i].x == r.JunctionCell.x && r.CellPath[i].y == r.JunctionCell.y) return i + 1;
|
||||||
|
return r.CellPath.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<(float x, float y)> Slice(List<(int x, int y)> cells, int from, int to)
|
||||||
|
{
|
||||||
|
var outp = new List<(float x, float y)>();
|
||||||
|
for (int i = Math.Max(0, from); i < Math.Min(to, cells.Count); i++) outp.Add((cells[i].x, cells[i].y));
|
||||||
|
return outp;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Ring the route's high point — the rim the channel crosses.</summary>
|
/// <summary>Ring the route's high point — the rim the channel crosses.</summary>
|
||||||
private static void MarkRim(Image img, RiverRouting.Route route, int n, int mark)
|
private static void MarkRim(Image img, RiverRouting.Route route, int n, int mark)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,48 @@ namespace IslaApocalypse.Tools
|
||||||
RoutedGiant,
|
RoutedGiant,
|
||||||
/// <summary>Stays inland: terminates at a significant lake, or at its own terminal.</summary>
|
/// <summary>Stays inland: terminates at a significant lake, or at its own terminal.</summary>
|
||||||
LakeEnder,
|
LakeEnder,
|
||||||
|
|
||||||
|
// ═══ rivers/03b — two new termini, from the three approved DIVERGENCES ═══
|
||||||
|
|
||||||
|
/// <summary>⭐ rivers/03b (fix 3): a dry-basin router that reached a SIGNIFICANT LAKE before it
|
||||||
|
/// reached the sea, and terminates there. In the reference a router targets ocean only, so it
|
||||||
|
/// would skirt the lake and carry on — which is what this corrects. **No water is created.**</summary>
|
||||||
|
LakeFed,
|
||||||
|
|
||||||
|
/// <summary>⚠ rivers/03b (fix 1): a dry-basin router whose cheapest route to the sea had to
|
||||||
|
/// climb a rim HIGHER THAN THE CAP. The reference routes at any cost, which produced an
|
||||||
|
/// uphill river over a 66.7 m wall. Refused: the course ends at its own terminal — a real
|
||||||
|
/// terminal basin. **Nothing is filled; it just ends there.**</summary>
|
||||||
|
WalledOff,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⚠⚠ THE THREE DELIBERATE DIVERGENCES FROM THE REFERENCE (rivers/03b), off by default.
|
||||||
|
///
|
||||||
|
/// Defaults reproduce rivers/03's faithful port EXACTLY — no cap, ocean-only targets, no
|
||||||
|
/// confluence — so that batch stays re-runnable bit-for-bit. The refinement task turns them on.
|
||||||
|
/// **None of these is a port. Each is a motivated correction of a faithful behaviour that
|
||||||
|
/// produced a physically-wrong result**, on the developer's explicit call.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Options
|
||||||
|
{
|
||||||
|
/// <summary>⭐ FIX 1 — the rim cap, metres. A route to the sea that must climb higher than
|
||||||
|
/// this above its terminal is refused and the river becomes a walled-off lake-ender.
|
||||||
|
/// Infinity = the reference's behaviour (route at any cost).</summary>
|
||||||
|
public float RimCapM = float.PositiveInfinity;
|
||||||
|
|
||||||
|
/// <summary>⭐ FIX 3 — include significant lakes in a ROUTER's target mask, so a river stops
|
||||||
|
/// at the nearer of {ocean, significant lake} instead of skirting a lake to reach the sea.
|
||||||
|
/// False = the reference's behaviour (routers target ocean only).</summary>
|
||||||
|
public bool LakeTargetForRouters;
|
||||||
|
|
||||||
|
/// <summary>⭐ FIX 2 — the confluence post-pass: courses laid biggest-first join on true cell
|
||||||
|
/// intersection instead of running as parallel duplicates to the same mouth.
|
||||||
|
/// False = the reference's behaviour (no dedup, no join).</summary>
|
||||||
|
public bool Confluence;
|
||||||
|
|
||||||
|
/// <summary>rivers/03's faithful settings — every divergence off.</summary>
|
||||||
|
public static Options Faithful => new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>One promoted river, classified, routed and assembled.</summary>
|
/// <summary>One promoted river, classified, routed and assembled.</summary>
|
||||||
|
|
@ -281,6 +323,29 @@ namespace IslaApocalypse.Tools
|
||||||
public List<(float x, float y)> Course;
|
public List<(float x, float y)> Course;
|
||||||
public string Why = "";
|
public string Why = "";
|
||||||
|
|
||||||
|
// ═══ rivers/03b ═══
|
||||||
|
|
||||||
|
/// <summary>⚠ The rim climb that was tested against the cap, and whether it was refused.</summary>
|
||||||
|
public float CappedRimM;
|
||||||
|
public bool RefusedByCap;
|
||||||
|
/// <summary>The class this river WOULD have had under the reference's rules — so every
|
||||||
|
/// reclassification the divergences caused is legible rather than silent.</summary>
|
||||||
|
public RiverClass FaithfulClass;
|
||||||
|
|
||||||
|
/// <summary>The full course rasterised to cells — what the confluence test intersects on.</summary>
|
||||||
|
public List<(int x, int y)> CellPath;
|
||||||
|
/// <summary>⭐ What this river draws: its OWN reach, truncated at its junction if it joined.
|
||||||
|
/// The union of every river's own reach is the dendritic tree.</summary>
|
||||||
|
public List<(float x, float y)> OwnPath;
|
||||||
|
/// <summary>The rank of the river this one flows into, or 0 if it keeps its own terminus.</summary>
|
||||||
|
public int ConfluenceParentRank;
|
||||||
|
public bool Joined;
|
||||||
|
public (int x, int y) JunctionCell;
|
||||||
|
/// <summary>How many leading cells of <see cref="CellPath"/> are the NATURAL upland stem.
|
||||||
|
/// Everything after is the lowland reach routing added — the plate colours the two apart.</summary>
|
||||||
|
public int StemCells;
|
||||||
|
|
||||||
|
/// <summary>Does this river's own course end at the sea, before any confluence?</summary>
|
||||||
public bool ReachesSea => Class == RiverClass.OceanTrunk || Class == RiverClass.RoutedGiant;
|
public bool ReachesSea => Class == RiverClass.OceanTrunk || Class == RiverClass.RoutedGiant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,8 +378,22 @@ namespace IslaApocalypse.Tools
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static List<RoutedRiver> RouteAll(List<RiverCandidate> promoted, float[,] height, int n,
|
public static List<RoutedRiver> RouteAll(List<RiverCandidate> promoted, float[,] height, int n,
|
||||||
bool[] isOcean, bool[] isClassifyWater, bool[] isSignificantWater, float sea, byte style,
|
bool[] isOcean, bool[] isClassifyWater, bool[] isSignificantWater, float sea, byte style,
|
||||||
Action<string> log)
|
Action<string> log, Options opt = null)
|
||||||
{
|
{
|
||||||
|
opt ??= Options.Faithful;
|
||||||
|
|
||||||
|
// ⭐ FIX 3 — the router's target mask. With the divergence off this is the ocean alone, which
|
||||||
|
// is the reference. With it on, a significant lake is an equally valid place for a river to
|
||||||
|
// stop, so the Dijkstra halts at whichever it reaches first and a river can no longer skirt
|
||||||
|
// a lake on its way to a distant coast.
|
||||||
|
bool[] routerTargets = isOcean;
|
||||||
|
if (opt.LakeTargetForRouters)
|
||||||
|
{
|
||||||
|
routerTargets = new bool[n * n];
|
||||||
|
for (int i = 0; i < routerTargets.Length; i++)
|
||||||
|
routerTargets[i] = isOcean[i] || isSignificantWater[i];
|
||||||
|
}
|
||||||
|
|
||||||
var outp = new List<RoutedRiver>();
|
var outp = new List<RoutedRiver>();
|
||||||
foreach (var c in promoted)
|
foreach (var c in promoted)
|
||||||
{
|
{
|
||||||
|
|
@ -333,18 +412,54 @@ namespace IslaApocalypse.Tools
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ The ocean probe, for every giant — the affordability evidence.
|
// ⭐ The ocean probe, for every giant — the affordability evidence (rivers/03).
|
||||||
var probe = RouteTo(height, n, isOcean, c.TermX, c.TermY, style, sea);
|
var probe = RouteTo(height, n, isOcean, c.TermX, c.TermY, style, sea);
|
||||||
rr.OceanProbe = probe;
|
rr.OceanProbe = probe;
|
||||||
|
|
||||||
|
// ⚠ `basinHasLake` is KEPT as the sort (rivers/03's finding): a basin that already holds a
|
||||||
|
// visible lake is a natural lake-ender and its river feeds its own lake — it is not routed
|
||||||
|
// anywhere. Only DRY basins are candidate routers. None of the three divergences touches this.
|
||||||
bool refLakeEnder = c.AnalysisKind == "lake-ender";
|
bool refLakeEnder = c.AnalysisKind == "lake-ender";
|
||||||
if (!refLakeEnder)
|
if (!refLakeEnder)
|
||||||
{
|
{
|
||||||
rr.Class = RiverClass.RoutedGiant;
|
// Route to the nearest of the target mask — {ocean} faithfully, {ocean ∪ lakes} refined.
|
||||||
rr.Lowland = probe;
|
var route = opt.LakeTargetForRouters
|
||||||
rr.Why = probe.Reached
|
? RouteTo(height, n, routerTargets, c.TermX, c.TermY, style, sea)
|
||||||
? $"dry pan → routed; rim climb {probe.RimClimbM:F1} m, max step {probe.MaxStepUphillM:F2} m, cost {probe.Cost:N0}"
|
: probe;
|
||||||
: "dry pan → routed, but NO path to the ocean was found (unexpected — report)";
|
rr.FaithfulClass = RiverClass.RoutedGiant;
|
||||||
|
rr.Lowland = route;
|
||||||
|
rr.CappedRimM = route.Reached ? route.RimClimbM : 0f;
|
||||||
|
|
||||||
|
bool stoppedAtLake = route.Reached
|
||||||
|
&& isSignificantWater[(int)route.Target.x * n + (int)route.Target.y]
|
||||||
|
&& !isOcean[(int)route.Target.x * n + (int)route.Target.y];
|
||||||
|
|
||||||
|
if (!route.Reached)
|
||||||
|
{
|
||||||
|
rr.Class = RiverClass.RoutedGiant;
|
||||||
|
rr.Why = "dry pan → routed, but NO path to a target was found (unexpected — report)";
|
||||||
|
}
|
||||||
|
else if (stoppedAtLake)
|
||||||
|
{
|
||||||
|
// ⭐ FIX 3 — it met a significant lake first. It terminates there. NO WATER CREATED:
|
||||||
|
// the course simply ends at an existing body.
|
||||||
|
rr.Class = RiverClass.LakeFed;
|
||||||
|
rr.Why = $"dry pan → reached a SIGNIFICANT LAKE at ({(int)route.Target.x},{(int)route.Target.y}) before the sea, {route.LenPx:F0} px away — terminates there (faithful: would have skirted it for the coast)";
|
||||||
|
}
|
||||||
|
else if (route.RimClimbM > opt.RimCapM)
|
||||||
|
{
|
||||||
|
// ⭐ FIX 1 — the cheapest way to the sea still climbs a wall. Refuse it. The course
|
||||||
|
// ends at its own terminal, a real terminal basin. NOTHING IS FILLED.
|
||||||
|
rr.Class = RiverClass.WalledOff;
|
||||||
|
rr.RefusedByCap = true;
|
||||||
|
rr.Lowland = null;
|
||||||
|
rr.Why = $"dry pan → WALLED OFF: cheapest route to the sea climbs {route.RimClimbM:F1} m > cap {opt.RimCapM:F0} m (cost {route.Cost:N0}) — ends at its own terminal";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rr.Class = RiverClass.RoutedGiant;
|
||||||
|
rr.Why = $"dry pan → routed to the sea; rim climb {route.RimClimbM:F1} m ≤ cap {(float.IsInfinity(opt.RimCapM) ? "none" : opt.RimCapM.ToString("F0") + " m")}, max step {route.MaxStepUphillM:F2} m, cost {route.Cost:N0}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -362,17 +477,160 @@ namespace IslaApocalypse.Tools
|
||||||
if (fb.Reached) { ext = fb; rr.LakeWasFallback = true; }
|
if (fb.Reached) { ext = fb; rr.LakeWasFallback = true; }
|
||||||
}
|
}
|
||||||
if (ext.Reached) { rr.Lowland = ext; rr.LakeReached = true; }
|
if (ext.Reached) { rr.Lowland = ext; rr.LakeReached = true; }
|
||||||
|
rr.FaithfulClass = RiverClass.LakeEnder;
|
||||||
rr.Why = rr.LakeReached
|
rr.Why = rr.LakeReached
|
||||||
? $"terminal basin holds classify water → lake-ender; joins {(rr.LakeWasFallback ? "a small body (fallback)" : "a significant body")} {ext.LenPx:F0} px away"
|
? $"terminal basin holds classify water → natural lake-ender; joins {(rr.LakeWasFallback ? "a small body (fallback)" : "a significant body")} {ext.LenPx:F0} px away"
|
||||||
: "terminal basin holds classify water → lake-ender; no water body reachable, course ends at its terminal";
|
: "terminal basin holds classify water → natural lake-ender; no water body reachable, course ends at its terminal";
|
||||||
}
|
}
|
||||||
|
|
||||||
rr.Course = Assemble(c.Course, rr.Lowland);
|
rr.Course = Assemble(c.Course, rr.Lowland);
|
||||||
outp.Add(rr);
|
outp.Add(rr);
|
||||||
log($" #{c.Rank,-3} {c.DrainagePx,10:N0} px {(rr.Class == RiverClass.RoutedGiant ? "ROUTED " : "LAKE-ENDER")} " +
|
log($" #{c.Rank,-3} {c.DrainagePx,10:N0} px {ClassLabel(rr.Class),-11} " +
|
||||||
$"probe{(probe.Reached ? $" reached cost {probe.Cost,12:N0} rim {probe.RimClimbM,6:F1} m maxstep {probe.MaxStepUphillM,5:F2} m len {probe.LenPx,6:F0} px wander {probe.WanderRatio:F2} expanded {probe.Expanded:N0}" : " NO PATH")}" +
|
$"probe{(probe.Reached ? $" cost {probe.Cost,12:N0} rim {probe.RimClimbM,6:F1} m maxstep {probe.MaxStepUphillM,5:F2} m len {probe.LenPx,6:F0} px wander {probe.WanderRatio:F2}" : " NO PATH")}" +
|
||||||
$"{(rr.Class == RiverClass.LakeEnder ? $" | lake {(rr.LakeReached ? (rr.LakeWasFallback ? "fallback" : "significant") : "NONE")}" : "")}");
|
$"{(rr.Class == RiverClass.LakeEnder ? $" | lake {(rr.LakeReached ? (rr.LakeWasFallback ? "fallback" : "significant") : "NONE")}" : "")}" +
|
||||||
|
$"{(rr.RefusedByCap ? " ⚠ REFUSED BY CAP" : "")}" +
|
||||||
|
$"{(rr.Class == RiverClass.LakeFed ? " ⭐ stopped at a lake, not the coast" : "")}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opt.Confluence) Confluence(outp, log);
|
||||||
|
else foreach (var rr in outp) rr.OwnPath = rr.Course;
|
||||||
|
return outp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ClassLabel(RiverClass c) => c switch
|
||||||
|
{
|
||||||
|
RiverClass.OceanTrunk => "TRUNK",
|
||||||
|
RiverClass.RoutedGiant => "ROUTED",
|
||||||
|
RiverClass.LakeFed => "LAKE-FED",
|
||||||
|
RiverClass.WalledOff => "WALLED-OFF",
|
||||||
|
_ => "LAKE-ENDER",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═══ ⭐⭐ FIX 2 — THE CONFLUENCE POST-PASS (rivers/03b) ═══════════════════════════════════
|
||||||
|
//
|
||||||
|
// ⚠⚠ A DIVERGENCE, NOT A PORT. The reference lays every route independently and never dedups or
|
||||||
|
// joins them, which rivers/03 measured: on EVERY seed two routed rivers arrived at the identical
|
||||||
|
// ocean cell without ever having met. Two channels reaching the same mouth as parallel
|
||||||
|
// duplicates is not geography; two channels that meet and continue as one is.
|
||||||
|
//
|
||||||
|
// The rule, deliberately strict: courses are laid BIGGEST-FIRST by drainage, and a later course
|
||||||
|
// joins an earlier one only on TRUE CELL INTERSECTION — the later course's rasterised cell path
|
||||||
|
// actually reaching a cell an earlier one occupies. **Never proximity.** Two rivers running 3 px
|
||||||
|
// apart down the same valley stay two rivers; that is a question for the carve's channel width,
|
||||||
|
// not for routing to guess at.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Join intersecting courses into a dendritic tree. Biggest-first, so the largest drainage is
|
||||||
|
/// the trunk and smaller ones become its tributaries — the later river is truncated at the
|
||||||
|
/// FIRST (most-upstream) cell it shares with an already-laid course, and adopts that course's
|
||||||
|
/// downstream and terminus from there.
|
||||||
|
/// </summary>
|
||||||
|
private static void Confluence(List<RoutedRiver> rivers, Action<string> log)
|
||||||
|
{
|
||||||
|
var order = new List<RoutedRiver>(rivers);
|
||||||
|
order.Sort((a, b) => b.Candidate.DrainagePx.CompareTo(a.Candidate.DrainagePx));
|
||||||
|
|
||||||
|
// cell -> (the river occupying it, and how far along that river's cell path it sits)
|
||||||
|
var owner = new Dictionary<(int x, int y), (RoutedRiver river, int idx)>();
|
||||||
|
int joins = 0;
|
||||||
|
|
||||||
|
foreach (var r in order)
|
||||||
|
{
|
||||||
|
r.CellPath = Rasterise(r.Course);
|
||||||
|
var stemOnly = new List<(float x, float y)>(r.Candidate.Course);
|
||||||
|
stemOnly.Reverse();
|
||||||
|
r.StemCells = Rasterise(stemOnly).Count;
|
||||||
|
if (r.CellPath.Count == 0) { r.OwnPath = r.Course; continue; }
|
||||||
|
|
||||||
|
// The first cell of THIS course that someone bigger already occupies.
|
||||||
|
int hit = -1;
|
||||||
|
(RoutedRiver river, int idx) into = default;
|
||||||
|
for (int i = 0; i < r.CellPath.Count; i++)
|
||||||
|
if (owner.TryGetValue(r.CellPath[i], out into)) { hit = i; break; }
|
||||||
|
|
||||||
|
if (hit < 0)
|
||||||
|
{
|
||||||
|
// Keeps its own route and its own mouth.
|
||||||
|
r.OwnPath = r.Course;
|
||||||
|
for (int i = 0; i < r.CellPath.Count; i++)
|
||||||
|
if (!owner.ContainsKey(r.CellPath[i])) owner[r.CellPath[i]] = (r, i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ It joins. Truncate here and adopt the parent's downstream from the junction on.
|
||||||
|
var parent = into.river;
|
||||||
|
r.Joined = true;
|
||||||
|
r.ConfluenceParentRank = parent.Candidate.Rank;
|
||||||
|
r.JunctionCell = r.CellPath[hit];
|
||||||
|
joins++;
|
||||||
|
|
||||||
|
// What it DRAWS is its own reach only, up to the junction — the union of every river's
|
||||||
|
// own reach is the tree. Drawing the adopted downstream too would just overdraw the parent.
|
||||||
|
r.OwnPath = new List<(float x, float y)>();
|
||||||
|
for (int i = 0; i <= hit; i++) r.OwnPath.Add((r.CellPath[i].x, r.CellPath[i].y));
|
||||||
|
|
||||||
|
// The full course of record: its own reach, then the parent's from the junction to the sea.
|
||||||
|
var full = new List<(int x, int y)>();
|
||||||
|
for (int i = 0; i <= hit; i++) full.Add(r.CellPath[i]);
|
||||||
|
for (int i = into.idx + 1; i < parent.CellPath.Count; i++) full.Add(parent.CellPath[i]);
|
||||||
|
r.CellPath = full;
|
||||||
|
r.Course = new List<(float x, float y)>();
|
||||||
|
foreach (var cpt in full) r.Course.Add((cpt.x, cpt.y));
|
||||||
|
|
||||||
|
// Only its OWN reach becomes occupiable, so a third river can join this tributary.
|
||||||
|
for (int i = 0; i <= hit; i++)
|
||||||
|
if (!owner.ContainsKey(full[i])) owner[full[i]] = (r, i);
|
||||||
|
|
||||||
|
log($" ⭐ CONFLUENCE: #{r.Candidate.Rank} ({r.Candidate.DrainagePx:N0} px) joins #{parent.Candidate.Rank} " +
|
||||||
|
$"({parent.Candidate.DrainagePx:N0} px) at ({r.JunctionCell.x},{r.JunctionCell.y}) — " +
|
||||||
|
$"{hit} px of its own reach, then adopts #{parent.Candidate.Rank}'s downstream and terminus");
|
||||||
|
}
|
||||||
|
if (joins == 0) log(" (no confluences — every course keeps its own mouth)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ THE ROOT of a confluence chain — the river whose terminus this one actually ends at. A
|
||||||
|
/// tributary's mouth is its trunk's mouth, so this is what mouth counting and terminus class
|
||||||
|
/// must both be read through.
|
||||||
|
/// </summary>
|
||||||
|
public static RoutedRiver Root(RoutedRiver r, List<RoutedRiver> all)
|
||||||
|
{
|
||||||
|
var cur = r;
|
||||||
|
// The chain is finite and strictly increasing in drainage (biggest-first laying), so it
|
||||||
|
// cannot cycle; the guard is belt-and-braces against a future change to the ordering.
|
||||||
|
for (int guard = 0; guard < all.Count + 1 && cur.Joined; guard++)
|
||||||
|
{
|
||||||
|
RoutedRiver parent = null;
|
||||||
|
foreach (var o in all) if (o.Candidate.Rank == cur.ConfluenceParentRank) { parent = o; break; }
|
||||||
|
if (parent == null) break;
|
||||||
|
cur = parent;
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rasterise a polyline to a deduped 1-px cell path. ⚠ The confluence test is a TRUE CELL
|
||||||
|
/// intersection, so the courses must be compared as the cells they occupy, not as the sparse
|
||||||
|
/// vertices the analysis decimated them to (stems are decimated ×4, routes are Chaikin-smoothed).
|
||||||
|
/// </summary>
|
||||||
|
private static List<(int x, int y)> Rasterise(List<(float x, float y)> pts)
|
||||||
|
{
|
||||||
|
var outp = new List<(int x, int y)>();
|
||||||
|
if (pts == null || pts.Count == 0) return outp;
|
||||||
|
void Push(int x, int y)
|
||||||
|
{
|
||||||
|
if (outp.Count > 0 && outp[^1].x == x && outp[^1].y == y) return;
|
||||||
|
outp.Add((x, y));
|
||||||
|
}
|
||||||
|
for (int i = 0; i + 1 < pts.Count; i++)
|
||||||
|
{
|
||||||
|
var a = pts[i]; var b = pts[i + 1];
|
||||||
|
float dx = b.x - a.x, dy = b.y - a.y;
|
||||||
|
int steps = Math.Max(1, (int)MathF.Ceiling(MathF.Max(MathF.Abs(dx), MathF.Abs(dy))));
|
||||||
|
for (int s = 0; s < steps; s++)
|
||||||
|
Push((int)MathF.Round(a.x + dx * s / steps), (int)MathF.Round(a.y + dy * s / steps));
|
||||||
|
}
|
||||||
|
Push((int)MathF.Round(pts[^1].x), (int)MathF.Round(pts[^1].y));
|
||||||
return outp;
|
return outp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,15 @@ namespace IslaApocalypse.Tools
|
||||||
public int Seed;
|
public int Seed;
|
||||||
public List<RiverRouting.RoutedRiver> Rivers;
|
public List<RiverRouting.RoutedRiver> Rivers;
|
||||||
public int Trunks, Routed, Lakes;
|
public int Trunks, Routed, Lakes;
|
||||||
|
/// <summary>rivers/03b classes.</summary>
|
||||||
|
public int LakeFed, Walled, Joined;
|
||||||
|
/// <summary>Rivers whose OWN course ends at the sea (tributaries excluded — they have no mouth).</summary>
|
||||||
public int SeaReaching;
|
public int SeaReaching;
|
||||||
|
/// <summary>⭐ Rivers whose water reaches the sea, counting tributaries through their trunk.</summary>
|
||||||
|
public int SeaConnected;
|
||||||
|
public List<string> CapMoved = new();
|
||||||
|
public List<string> LakeMoved = new();
|
||||||
|
public List<string> Joins = new();
|
||||||
/// <summary>⭐ DISTINCT ocean mouth cells — routes are computed per giant with nothing
|
/// <summary>⭐ DISTINCT ocean mouth cells — routes are computed per giant with nothing
|
||||||
/// coordinating them, so two can land on the same cell. This is the honest river count.</summary>
|
/// coordinating them, so two can land on the same cell. This is the honest river count.</summary>
|
||||||
public int DistinctMouths;
|
public int DistinctMouths;
|
||||||
|
|
@ -90,8 +98,18 @@ namespace IslaApocalypse.Tools
|
||||||
ToolingPaths.Configure(OS.GetUserDataDir());
|
ToolingPaths.Configure(OS.GetUserDataDir());
|
||||||
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "rivers"));
|
ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "rivers"));
|
||||||
|
|
||||||
|
// ⭐ rivers/03b — the three approved DIVERGENCES. Default OFF, so this tool still reproduces
|
||||||
|
// rivers/03's faithful port bit-for-bit.
|
||||||
|
bool refined = EnvStr("ISLA_ROUTING_MODE", "faithful").Trim().ToLowerInvariant() == "refined";
|
||||||
|
float rimCapM = float.TryParse(EnvStr("ISLA_RIM_CAP_M", "30"), out float rc) ? rc : 30f;
|
||||||
|
var opt = refined
|
||||||
|
? new RiverRouting.Options { RimCapM = rimCapM, LakeTargetForRouters = true, Confluence = true }
|
||||||
|
: RiverRouting.Options.Faithful;
|
||||||
|
|
||||||
int task = EnvInt("ISLA_TASK", 3);
|
int task = EnvInt("ISLA_TASK", 3);
|
||||||
string descr = EnvStr("ISLA_BATCH", "lowland_routing");
|
// ⭐ rivers/03b is a LETTERED SUB-TASK of 03 — same authoring task, three changed rules.
|
||||||
|
string taskSfx = EnvStr("ISLA_TASK_SUFFIX", refined ? "b" : "");
|
||||||
|
string descr = EnvStr("ISLA_BATCH", refined ? "routing_refinement" : "lowland_routing");
|
||||||
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
|
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
|
||||||
int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
|
int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
|
||||||
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
|
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
|
||||||
|
|
@ -114,7 +132,7 @@ namespace IslaApocalypse.Tools
|
||||||
TerrainShapeV1.Assert("RiverRouting");
|
TerrainShapeV1.Assert("RiverRouting");
|
||||||
TerrainShapeV1.AssertErosionDefaultOn("RiverRouting");
|
TerrainShapeV1.AssertErosionDefaultOn("RiverRouting");
|
||||||
|
|
||||||
string batchRoot = ToolingPaths.BatchRoot(task, descr);
|
string batchRoot = ToolingPaths.BatchRoot(task, taskSfx, descr);
|
||||||
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
|
DirAccess.MakeDirRecursiveAbsolute(batchRoot);
|
||||||
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
|
DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
|
||||||
|
|
||||||
|
|
@ -135,7 +153,9 @@ namespace IslaApocalypse.Tools
|
||||||
var dpDefaults = new DrainageAnalysis.Params();
|
var dpDefaults = new DrainageAnalysis.Params();
|
||||||
|
|
||||||
GD.Print("==================================================================");
|
GD.Print("==================================================================");
|
||||||
GD.Print(" LOWLAND ROUTING (rivers/03) — the routed MIX on the pure top-N, COURSES ONLY");
|
GD.Print(refined
|
||||||
|
? " ROUTING REFINEMENT (rivers/03b) — three DELIBERATE DIVERGENCES from the faithful port, COURSES ONLY"
|
||||||
|
: " LOWLAND ROUTING (rivers/03) — the routed MIX on the pure top-N, COURSES ONLY");
|
||||||
GD.Print("==================================================================");
|
GD.Print("==================================================================");
|
||||||
GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}");
|
GD.Print($"MapSize : {mapSize} curve calibrated at {calibSize}");
|
||||||
GD.Print($"terrain : {TerrainShapeV1.Describe()} + erosion ON by default");
|
GD.Print($"terrain : {TerrainShapeV1.Describe()} + erosion ON by default");
|
||||||
|
|
@ -146,6 +166,17 @@ namespace IslaApocalypse.Tools
|
||||||
GD.Print($" ⚠ NOT a path test: on an 8-connected grid a path to the ocean ALWAYS exists, so");
|
GD.Print($" ⚠ NOT a path test: on an 8-connected grid a path to the ocean ALWAYS exists, so");
|
||||||
GD.Print($" \"a path exists → routed\" would classify everything as routed. The ocean route is");
|
GD.Print($" \"a path exists → routed\" would classify everything as routed. The ocean route is");
|
||||||
GD.Print($" still probed for EVERY giant so the missing affordability threshold is a number, not a guess.");
|
GD.Print($" still probed for EVERY giant so the missing affordability threshold is a number, not a guess.");
|
||||||
|
if (refined)
|
||||||
|
{
|
||||||
|
GD.Print("⚠⚠ THREE DELIBERATE DIVERGENCES FROM THE REFERENCE — approved, and NOT a port:");
|
||||||
|
GD.Print($" FIX 1 RIM CAP {rimCapM:F0} m — a route to the sea that must climb higher than this above its");
|
||||||
|
GD.Print( " terminal is REFUSED; the river becomes a walled-off inland lake-ender. (Reference: routes at ANY cost.)");
|
||||||
|
GD.Print( " FIX 3 LAKE TARGETS — a router stops at the nearer of {ocean, significant lake}, so it cannot");
|
||||||
|
GD.Print( " skirt a lake to reach a distant coast. (Reference: routers target ocean only.)");
|
||||||
|
GD.Print( " FIX 2 CONFLUENCE — courses laid biggest-first join on TRUE CELL INTERSECTION and the smaller");
|
||||||
|
GD.Print( " becomes a tributary. (Reference: no dedup, no join — parallel duplicates to one mouth.)");
|
||||||
|
GD.Print( " ⚠ KEPT: basinHasLake stays the sort — a basin that already holds a lake is a natural lake-ender.");
|
||||||
|
}
|
||||||
GD.Print($"lake target: significant water = 8-connected classify-water components >= {lakeMinPx:N0} px (interim for v2's missing water-bodies table)");
|
GD.Print($"lake target: significant water = 8-connected classify-water components >= {lakeMinPx:N0} px (interim for v2's missing water-bodies table)");
|
||||||
GD.Print($"⛔ RED LINE : courses only — no height mutated, no water filled, nothing carved. ASSERTED per seed.");
|
GD.Print($"⛔ RED LINE : courses only — no height mutated, no water filled, nothing carved. ASSERTED per seed.");
|
||||||
GD.Print($"batch : {batchRoot}");
|
GD.Print($"batch : {batchRoot}");
|
||||||
|
|
@ -208,7 +239,7 @@ namespace IslaApocalypse.Tools
|
||||||
GD.Print($" routing (style {styleS}) — probing the ocean for every giant:");
|
GD.Print($" routing (style {styleS}) — probing the ocean for every giant:");
|
||||||
ulong tr0 = Time.GetTicksMsec();
|
ulong tr0 = Time.GetTicksMsec();
|
||||||
var rivers = RiverRouting.RouteAll(promoted, p2.Height, mapSize, isOcean, isClassifyWater,
|
var rivers = RiverRouting.RouteAll(promoted, p2.Height, mapSize, isOcean, isClassifyWater,
|
||||||
significant, sea, style, m => GD.Print(m));
|
significant, sea, style, m => GD.Print(m), opt);
|
||||||
double routingSec = (Time.GetTicksMsec() - tr0) / 1000.0;
|
double routingSec = (Time.GetTicksMsec() - tr0) / 1000.0;
|
||||||
|
|
||||||
// ═══ ⛔ …and assert they are byte-identical after ═══
|
// ═══ ⛔ …and assert they are byte-identical after ═══
|
||||||
|
|
@ -235,28 +266,48 @@ namespace IslaApocalypse.Tools
|
||||||
};
|
};
|
||||||
foreach (var rr in rivers)
|
foreach (var rr in rivers)
|
||||||
{
|
{
|
||||||
if (rr.Class == RiverRouting.RiverClass.OceanTrunk) r.Trunks++;
|
switch (rr.Class)
|
||||||
else if (rr.Class == RiverRouting.RiverClass.RoutedGiant) r.Routed++;
|
{
|
||||||
else r.Lakes++;
|
case RiverRouting.RiverClass.OceanTrunk: r.Trunks++; break;
|
||||||
|
case RiverRouting.RiverClass.RoutedGiant: r.Routed++; break;
|
||||||
|
case RiverRouting.RiverClass.LakeFed: r.LakeFed++; break;
|
||||||
|
case RiverRouting.RiverClass.WalledOff: r.Walled++; break;
|
||||||
|
default: r.Lakes++; break;
|
||||||
|
}
|
||||||
if (rr.OceanProbe != null) r.TotalExpanded += rr.OceanProbe.Expanded;
|
if (rr.OceanProbe != null) r.TotalExpanded += rr.OceanProbe.Expanded;
|
||||||
|
if (rr.Joined) { r.Joined++; r.Joins.Add($"#{rr.Candidate.Rank}→#{rr.ConfluenceParentRank} at ({rr.JunctionCell.x},{rr.JunctionCell.y})"); }
|
||||||
|
// ⭐ Make every reclassification the divergences caused legible, not silent.
|
||||||
|
if (rr.RefusedByCap) r.CapMoved.Add($"#{rr.Candidate.Rank} ({rr.Candidate.DrainagePx:N0} px, rim {rr.CappedRimM:F1} m)");
|
||||||
|
if (rr.Class == RiverRouting.RiverClass.LakeFed) r.LakeMoved.Add($"#{rr.Candidate.Rank} ({rr.Candidate.DrainagePx:N0} px)");
|
||||||
|
}
|
||||||
|
// ⚠ A tributary has NO mouth of its own — it reaches the sea through its trunk. So the two
|
||||||
|
// numbers are different and both are reported: how many rivers END at the sea, and how many
|
||||||
|
// rivers' water GETS there.
|
||||||
|
foreach (var rr in rivers)
|
||||||
|
{
|
||||||
|
if (!rr.Joined && rr.ReachesSea) r.SeaReaching++;
|
||||||
|
if (RiverRouting.Root(rr, rivers).ReachesSea) r.SeaConnected++;
|
||||||
}
|
}
|
||||||
r.SeaReaching = r.Trunks + r.Routed;
|
|
||||||
MeasureMouths(r, rivers);
|
MeasureMouths(r, rivers);
|
||||||
r.Spread = Spread(rivers, mapSize);
|
r.Spread = Spread(rivers, mapSize);
|
||||||
r.Ms = Time.GetTicksMsec() - t0;
|
r.Ms = Time.GetTicksMsec() - t0;
|
||||||
|
|
||||||
GD.Print($" ⭐ MIX: {r.Trunks} natural trunks + {r.Routed} routed-through + {r.Lakes} lake-enders = {rivers.Count}");
|
GD.Print($" ⭐ MIX: {r.Trunks} trunks + {r.Routed} routed-to-sea + {r.LakeFed} lake-fed + {r.Lakes} natural lake-enders + {r.Walled} walled-off = {rivers.Count}" +
|
||||||
GD.Print($" → SEA-REACHING RIVERS: {r.SeaReaching}, at {r.DistinctMouths} DISTINCT mouths" +
|
(r.Joined > 0 ? $" ({r.Joined} joined as tributaries)" : ""));
|
||||||
(r.SharedMouths.Count > 0 ? $" ⚠ shared mouth: {string.Join(", ", r.SharedMouths)}" : "") +
|
GD.Print($" → {r.DistinctMouths} DISTINCT SEA MOUTHS from {r.SeaReaching} river(s) ending at the sea; {r.SeaConnected} rivers' water reaches the sea" +
|
||||||
|
(r.SharedMouths.Count > 0 ? $" ⚠⚠ STILL SHARED: {string.Join(", ", r.SharedMouths)}" : " ✅ no two rivers share a mouth") +
|
||||||
$" spread: {r.Spread}");
|
$" spread: {r.Spread}");
|
||||||
|
if (r.CapMoved.Count > 0) GD.Print($" ⚠ rim cap moved routed→walled-off: {string.Join(", ", r.CapMoved)}");
|
||||||
|
if (r.LakeMoved.Count > 0) GD.Print($" ⭐ lake-target moved routed→lake-fed: {string.Join(", ", r.LakeMoved)}");
|
||||||
GD.Print($" routing {routingSec:F1}s, {r.TotalExpanded:N0} cells settled across all probes");
|
GD.Print($" routing {routingSec:F1}s, {r.TotalExpanded:N0} cells settled across all probes");
|
||||||
|
|
||||||
WriteRiverCsv(batchRoot, r);
|
WriteRiverCsv(batchRoot, r);
|
||||||
RenderSeed(batchRoot, r, isOcean, p2, mapSize, sea, floorPx, promoteN, skipRaw);
|
RenderSeed(batchRoot, r, isOcean, p2, mapSize, sea, floorPx, promoteN, skipRaw, refined, rimCapM);
|
||||||
results.Add(r);
|
results.Add(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteIndex(batchRoot, mapSize, calibSize, seeds, results, promoteN, floorPx, promoteMax, lakeMinPx, styleS, dpDefaults, skipRaw);
|
if (refined) WriteRefinedIndex(batchRoot, mapSize, seeds, results, promoteN, lakeMinPx, rimCapM, dpDefaults, skipRaw);
|
||||||
|
else WriteIndex(batchRoot, mapSize, calibSize, seeds, results, promoteN, floorPx, promoteMax, lakeMinPx, styleS, dpDefaults, skipRaw);
|
||||||
GD.Print("\n==================================================================");
|
GD.Print("\n==================================================================");
|
||||||
GD.Print($" DONE — {batchRoot}");
|
GD.Print($" DONE — {batchRoot}");
|
||||||
GD.Print(" ⛔ TASTE GATE: the MIX is PRESENTED, not decided. No count, no K, no style, no default was set.");
|
GD.Print(" ⛔ TASTE GATE: the MIX is PRESENTED, not decided. No count, no K, no style, no default was set.");
|
||||||
|
|
@ -301,7 +352,8 @@ namespace IslaApocalypse.Tools
|
||||||
var at = new Dictionary<(int x, int y), List<int>>();
|
var at = new Dictionary<(int x, int y), List<int>>();
|
||||||
foreach (var rr in rivers)
|
foreach (var rr in rivers)
|
||||||
{
|
{
|
||||||
if (!rr.ReachesSea) continue;
|
// ⚠ A tributary adopted its trunk's terminus — it is not a separate mouth.
|
||||||
|
if (rr.Joined || !rr.ReachesSea) continue;
|
||||||
(int x, int y) key;
|
(int x, int y) key;
|
||||||
if (rr.Class == RiverRouting.RiverClass.OceanTrunk) key = (rr.Candidate.TermX, rr.Candidate.TermY);
|
if (rr.Class == RiverRouting.RiverClass.OceanTrunk) key = (rr.Candidate.TermX, rr.Candidate.TermY);
|
||||||
else if (rr.Lowland != null && rr.Lowland.Reached) key = ((int)rr.Lowland.Target.x, (int)rr.Lowland.Target.y);
|
else if (rr.Lowland != null && rr.Lowland.Reached) key = ((int)rr.Lowland.Target.x, (int)rr.Lowland.Target.y);
|
||||||
|
|
@ -325,7 +377,7 @@ namespace IslaApocalypse.Tools
|
||||||
int total = 0;
|
int total = 0;
|
||||||
foreach (var r in rivers)
|
foreach (var r in rivers)
|
||||||
{
|
{
|
||||||
if (!r.ReachesSea) continue;
|
if (r.Joined || !r.ReachesSea) continue;
|
||||||
float mx, my;
|
float mx, my;
|
||||||
if (r.Class == RiverRouting.RiverClass.OceanTrunk) { mx = r.Candidate.TermX; my = r.Candidate.TermY; }
|
if (r.Class == RiverRouting.RiverClass.OceanTrunk) { mx = r.Candidate.TermX; my = r.Candidate.TermY; }
|
||||||
else if (r.Lowland != null && r.Lowland.Reached) { mx = r.Lowland.Target.x; my = r.Lowland.Target.y; }
|
else if (r.Lowland != null && r.Lowland.Reached) { mx = r.Lowland.Target.x; my = r.Lowland.Target.y; }
|
||||||
|
|
@ -355,16 +407,19 @@ namespace IslaApocalypse.Tools
|
||||||
{
|
{
|
||||||
RiverRouting.RiverClass.OceanTrunk => "trunk",
|
RiverRouting.RiverClass.OceanTrunk => "trunk",
|
||||||
RiverRouting.RiverClass.RoutedGiant => "routed",
|
RiverRouting.RiverClass.RoutedGiant => "routed",
|
||||||
|
RiverRouting.RiverClass.LakeFed => "lake-fed",
|
||||||
|
RiverRouting.RiverClass.WalledOff => "walled-off",
|
||||||
_ => "lake-ender",
|
_ => "lake-ender",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static void WriteRiverCsv(string batchRoot, SeedResult r)
|
private static void WriteRiverCsv(string batchRoot, SeedResult r)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("rank,class,analysis_kind,terminus_type,drainage_px,term_x,term_y,course_pts," +
|
sb.AppendLine("rank,class,basin_has_lake,analysis_kind,faithful_class,terminus_type,drainage_px,term_x,term_y,course_pts," +
|
||||||
"route_reached,route_target_x,route_target_y,route_len_px,route_straight_px,wander," +
|
"route_reached,route_target_x,route_target_y,route_len_px,route_straight_px,wander," +
|
||||||
"route_cost,rim_climb_m,max_step_uphill_m,max_elev_m,total_uphill_m,uphill_steps," +
|
"route_cost,rim_climb_m,cap_verdict,max_step_uphill_m,max_elev_m,total_uphill_m,uphill_steps," +
|
||||||
"rim_x,rim_y,cells_expanded,lake_reached,lake_was_fallback,stem_width_px,why");
|
"rim_x,rim_y,cells_expanded,lake_reached,lake_was_fallback,joined,confluence_parent_rank," +
|
||||||
|
"junction_x,junction_y,stem_width_px,why");
|
||||||
foreach (var rr in r.Rivers)
|
foreach (var rr in r.Rivers)
|
||||||
{
|
{
|
||||||
var c = rr.Candidate;
|
var c = rr.Candidate;
|
||||||
|
|
@ -372,31 +427,43 @@ namespace IslaApocalypse.Tools
|
||||||
// ⚠ For a lake-ender the OCEAN PROBE is reported too (in the rim/cost columns of the
|
// ⚠ For a lake-ender the OCEAN PROBE is reported too (in the rim/cost columns of the
|
||||||
// probe row below) — those are what an affordability threshold would be set against.
|
// probe row below) — those are what an affordability threshold would be set against.
|
||||||
var pr = rr.OceanProbe;
|
var pr = rr.OceanProbe;
|
||||||
sb.AppendLine($"{c.Rank},{ClassName(rr.Class)},{(c.IsSea ? "" : c.AnalysisKind)},{c.TerminusName},{c.DrainagePx},{c.TermX},{c.TermY},{rr.Course.Count}," +
|
sb.AppendLine($"{c.Rank},{ClassName(rr.Class)},{(c.IsSea ? "" : (c.AnalysisKind == "lake-ender" ? "yes" : "no"))},{(c.IsSea ? "" : c.AnalysisKind)}," +
|
||||||
|
$"{(c.IsSea ? "" : ClassName(rr.FaithfulClass))},{c.TerminusName},{c.DrainagePx},{c.TermX},{c.TermY},{rr.Course.Count}," +
|
||||||
$"{(lo != null && lo.Reached ? "yes" : "no")},{(lo != null && lo.Reached ? ((int)lo.Target.x).ToString() : "")},{(lo != null && lo.Reached ? ((int)lo.Target.y).ToString() : "")}," +
|
$"{(lo != null && lo.Reached ? "yes" : "no")},{(lo != null && lo.Reached ? ((int)lo.Target.x).ToString() : "")},{(lo != null && lo.Reached ? ((int)lo.Target.y).ToString() : "")}," +
|
||||||
$"{(lo != null ? lo.LenPx.ToString("F1") : "")},{(lo != null ? lo.StraightPx.ToString("F1") : "")},{(lo != null ? lo.WanderRatio.ToString("F3") : "")}," +
|
$"{(lo != null ? lo.LenPx.ToString("F1") : "")},{(lo != null ? lo.StraightPx.ToString("F1") : "")},{(lo != null ? lo.WanderRatio.ToString("F3") : "")}," +
|
||||||
$"{(pr != null && pr.Reached ? pr.Cost.ToString("F0") : "")},{(pr != null ? pr.RimClimbM.ToString("F2") : "")},{(pr != null ? pr.MaxStepUphillM.ToString("F3") : "")}," +
|
$"{(pr != null && pr.Reached ? pr.Cost.ToString("F0") : "")},{(pr != null ? pr.RimClimbM.ToString("F2") : "")}," +
|
||||||
|
$"{(rr.RefusedByCap ? "REFUSED" : rr.Class == RiverRouting.RiverClass.RoutedGiant ? "under cap" : "")}," +
|
||||||
|
$"{(pr != null ? pr.MaxStepUphillM.ToString("F3") : "")}," +
|
||||||
$"{(pr != null ? pr.MaxElevM.ToString("F2") : "")},{(pr != null ? pr.TotalUphillM.ToString("F2") : "")},{(pr != null ? pr.UphillSteps.ToString() : "")}," +
|
$"{(pr != null ? pr.MaxElevM.ToString("F2") : "")},{(pr != null ? pr.TotalUphillM.ToString("F2") : "")},{(pr != null ? pr.UphillSteps.ToString() : "")}," +
|
||||||
$"{(pr != null && pr.Reached ? ((int)pr.RimPoint.x).ToString() : "")},{(pr != null && pr.Reached ? ((int)pr.RimPoint.y).ToString() : "")}," +
|
$"{(pr != null && pr.Reached ? ((int)pr.RimPoint.x).ToString() : "")},{(pr != null && pr.Reached ? ((int)pr.RimPoint.y).ToString() : "")}," +
|
||||||
$"{(pr != null ? pr.Expanded.ToString() : "")},{(rr.Class == RiverRouting.RiverClass.LakeEnder ? (rr.LakeReached ? "yes" : "no") : "")}," +
|
$"{(pr != null ? pr.Expanded.ToString() : "")},{(rr.Class == RiverRouting.RiverClass.LakeEnder ? (rr.LakeReached ? "yes" : "no") : "")}," +
|
||||||
$"{(rr.Class == RiverRouting.RiverClass.LakeEnder ? (rr.LakeWasFallback ? "yes" : "no") : "")}," +
|
$"{(rr.Class == RiverRouting.RiverClass.LakeEnder ? (rr.LakeWasFallback ? "yes" : "no") : "")}," +
|
||||||
|
$"{(rr.Joined ? "yes" : "no")},{(rr.Joined ? rr.ConfluenceParentRank.ToString() : "")}," +
|
||||||
|
$"{(rr.Joined ? rr.JunctionCell.x.ToString() : "")},{(rr.Joined ? rr.JunctionCell.y.ToString() : "")}," +
|
||||||
$"{DrainageRenderer.StemWidthFixed(c.DrainagePx)},\"{rr.Why}\"");
|
$"{DrainageRenderer.StemWidthFixed(c.DrainagePx)},\"{rr.Why}\"");
|
||||||
}
|
}
|
||||||
WriteText(Path.Combine(batchRoot, $"rivers_{r.Seed}.csv"), sb.ToString());
|
WriteText(Path.Combine(batchRoot, $"rivers_{r.Seed}.csv"), sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RenderSeed(string batchRoot, SeedResult r, bool[] isOcean, Pass2Result p2,
|
private static void RenderSeed(string batchRoot, SeedResult r, bool[] isOcean, Pass2Result p2,
|
||||||
int n, float sea, long floorPx, int promoteN, bool skipRaw)
|
int n, float sea, long floorPx, int promoteN, bool skipRaw, bool refined, float rimCapM)
|
||||||
{
|
{
|
||||||
string dir = Path.Combine(batchRoot, $"{r.Seed}");
|
string dir = Path.Combine(batchRoot, $"{r.Seed}");
|
||||||
DirAccess.MakeDirRecursiveAbsolute(dir);
|
DirAccess.MakeDirRecursiveAbsolute(dir);
|
||||||
Image baseImg = DrainageRenderer.TerrainBase(isOcean, p2.Height, n, sea, p2.HMax);
|
Image baseImg = DrainageRenderer.TerrainBase(isOcean, p2.Height, n, sea, p2.HMax);
|
||||||
|
|
||||||
DrainageRenderer.RoutedMix(r.Rivers, baseImg.Duplicate() as Image, n,
|
if (refined)
|
||||||
$"SEED {r.Seed} - THE ROUTED MIX ON THE PURE TOP {promoteN}",
|
DrainageRenderer.RefinedMix(r.Rivers, baseImg.Duplicate() as Image, n,
|
||||||
$"{r.Trunks} NATURAL TRUNKS + {r.Routed} ROUTED-THROUGH + {r.Lakes} LAKE-ENDERS = {r.SeaReaching} SEA-REACHING RIVERS. SPREAD: {r.Spread.ToUpperInvariant()}",
|
$"SEED {r.Seed} - THE RESHAPED MIX ON THE PURE TOP {promoteN} (RIVERS/03B)",
|
||||||
floorPx)
|
$"{r.DistinctMouths} DISTINCT SEA MOUTHS - {r.Trunks} TRUNK + {r.Routed} ROUTED + {r.LakeFed} LAKE-FED + {r.Lakes} NATURAL LAKE-ENDER + {r.Walled} WALLED-OFF, {r.Joined} JOINED. SPREAD: {r.Spread.ToUpperInvariant()}",
|
||||||
.SavePng(Path.Combine(dir, "routed_mix.png"));
|
$"CHEAPEST ROUTE TO THE SEA CLIMBS MORE THAN THE {rimCapM:F0} M RIM CAP, SO IT ENDS AT ITS OWN TERMINAL")
|
||||||
|
.SavePng(Path.Combine(dir, "refined_mix.png"));
|
||||||
|
else
|
||||||
|
DrainageRenderer.RoutedMix(r.Rivers, baseImg.Duplicate() as Image, n,
|
||||||
|
$"SEED {r.Seed} - THE ROUTED MIX ON THE PURE TOP {promoteN}",
|
||||||
|
$"{r.Trunks} NATURAL TRUNKS + {r.Routed} ROUTED-THROUGH + {r.Lakes} LAKE-ENDERS = {r.SeaReaching} SEA-REACHING RIVERS. SPREAD: {r.Spread.ToUpperInvariant()}",
|
||||||
|
floorPx)
|
||||||
|
.SavePng(Path.Combine(dir, "routed_mix.png"));
|
||||||
|
|
||||||
// Grayscale beside the pretty render — the field must be inspectable without the palette.
|
// Grayscale beside the pretty render — the field must be inspectable without the palette.
|
||||||
var (gmin, gmax) = GrayscaleRenderer.SavePng(p2.Height, n, Path.Combine(dir, "grayscale.png"));
|
var (gmin, gmax) = GrayscaleRenderer.SavePng(p2.Height, n, Path.Combine(dir, "grayscale.png"));
|
||||||
|
|
@ -583,6 +650,157 @@ namespace IslaApocalypse.Tools
|
||||||
WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString());
|
WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ THE REFINEMENT INDEX (rivers/03b) — the before/after, and the one judgment it feeds.
|
||||||
|
/// </summary>
|
||||||
|
private static void WriteRefinedIndex(string batchRoot, int mapSize, int[] seeds,
|
||||||
|
List<SeedResult> rows, int promoteN, int lakeMinPx, float rimCapM,
|
||||||
|
DrainageAnalysis.Params def, bool skipRaw)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
int primary = seeds.Length > 0 ? seeds[0] : 0;
|
||||||
|
|
||||||
|
sb.AppendLine($"# Batch 03b — routing refinement: the reshaped MIX on the pure top {promoteN}");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**⛔ TASTE GATE. Nothing is locked, and nothing graduates yet** — the cap is a knob, and the");
|
||||||
|
sb.AppendLine("developer has said not to graduate mid-refinement.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**⛔ COURSES ONLY. No height mutated, no water filled or created, nothing carved** — asserted per");
|
||||||
|
sb.AppendLine("seed by a raw-bit digest of both height fields before and after routing. Lake-termination *ends a");
|
||||||
|
sb.AppendLine("course at* an existing lake; it does not fill or create one.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## 👉 The pick — this is a BEFORE/AFTER");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Open **`{primary}/refined_mix.png`** beside rivers/03's");
|
||||||
|
sb.AppendLine($"**`../03_lowland_routing/{primary}/routed_mix.png`** (the before). Same base, same colours where");
|
||||||
|
sb.AppendLine("they carry over, same fixed width scale — the two are directly comparable.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("> ### ⭐⭐ THE JUDGMENT, STATED");
|
||||||
|
sb.AppendLine("> **Does the island now read as natural dendritic drainage — no uphill rivers, no parallel");
|
||||||
|
sb.AppendLine("> duplicate mouths, no skirting past a lake to reach the sea — and is the resulting sea-river count");
|
||||||
|
sb.AppendLine("> healthy, or is the island now too lake-locked?**");
|
||||||
|
sb.AppendLine(">");
|
||||||
|
sb.AppendLine($"> If too walled-off, the levers are the cap (`ISLA_RIM_CAP_M` up from {rimCapM:F0}) or — as its own");
|
||||||
|
sb.AppendLine("> future task — rim incision. If about right, routing character is settled and the whole routing");
|
||||||
|
sb.AppendLine("> unit graduates together.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## ⚠⚠ Three DELIBERATE DIVERGENCES from the reference — not a port");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Each corrects a faithful behaviour that produced a physically-wrong result, on the developer's call.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| # | The reference does | rivers/03 showed | rivers/03b does instead |");
|
||||||
|
sb.AppendLine("|---|---|---|---|");
|
||||||
|
sb.AppendLine($"| **1** | routes a dry basin to the sea **at any cost** | an uphill river over a **66.7 m** rim (`31415926 #2`) | **rim cap {rimCapM:F0} m** — over it, the river is refused and ends at its own terminal |");
|
||||||
|
sb.AppendLine("| **2** | lays every route independently, **no dedup, no join** | **two rivers at the identical ocean cell on every seed**, never having met | **confluence** — biggest-first, join on true cell intersection, the smaller becomes a tributary |");
|
||||||
|
sb.AppendLine("| **3** | a router targets **ocean only** | a router **skirts a lake** to reach the distant sea | **lake targets** — a router stops at the nearer of {ocean, significant lake} |");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**⚠ KEPT unchanged: `basinHasLake` is still the sort.** A basin that already holds a visible lake is");
|
||||||
|
sb.AppendLine("a natural lake-ender and its river feeds its own lake — that is rivers/03's finding and it stands.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## ⭐ The reshaped MIX, per seed");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Seed | trunk | routed→sea | lake-fed | natural lake-ender | ⚠ walled-off | joined | ⭐⭐ DISTINCT SEA MOUTHS | spread |");
|
||||||
|
sb.AppendLine("|---|---|---|---|---|---|---|---|---|");
|
||||||
|
foreach (var r in rows)
|
||||||
|
sb.AppendLine($"| `{r.Seed}` | {r.Trunks} | {r.Routed} | {r.LakeFed} | {r.Lakes} | {r.Walled} | {r.Joined} | " +
|
||||||
|
$"**{r.DistinctMouths}** | {r.Spread} |");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("⚠ *Rivers whose water reaches the sea, counting tributaries through their trunk:* " +
|
||||||
|
string.Join(", ", Array.ConvertAll(rows.ToArray(), r => $"`{r.Seed}` {r.SeaConnected}")) + ".");
|
||||||
|
sb.AppendLine("A tributary has no mouth of its own, so it is not a separate sea mouth — but its water still gets there.");
|
||||||
|
sb.AppendLine();
|
||||||
|
bool anyShared = false;
|
||||||
|
foreach (var r in rows) if (r.SharedMouths.Count > 0) anyShared = true;
|
||||||
|
if (anyShared)
|
||||||
|
{
|
||||||
|
sb.AppendLine("> ### ⚠⚠ SOME MOUTHS ARE STILL SHARED — fix 2 did not fully close it");
|
||||||
|
foreach (var r in rows)
|
||||||
|
if (r.SharedMouths.Count > 0) sb.AppendLine($"> - `{r.Seed}`: {string.Join("; ", r.SharedMouths)}");
|
||||||
|
sb.AppendLine("> Two courses can arrive at the same cell without their rasterised paths ever sharing one");
|
||||||
|
sb.AppendLine("> earlier — they approach from different sides. Reported, not papered over.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.AppendLine("> ### ✅ NO TWO RIVERS SHARE A MOUTH on any seed — fix 2 closed rivers/03's duplicate-mouth finding.");
|
||||||
|
sb.AppendLine("> Every distinct mouth is now a distinct river, and rivers that meet do so as a confluence.");
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## What the divergences actually moved");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Seed | ⚠ rim cap moved routed → walled-off | ⭐ lake target moved routed → lake-fed | confluences formed |");
|
||||||
|
sb.AppendLine("|---|---|---|---|");
|
||||||
|
foreach (var r in rows)
|
||||||
|
sb.AppendLine($"| `{r.Seed}` | {(r.CapMoved.Count > 0 ? string.Join("; ", r.CapMoved) : "— none")} | " +
|
||||||
|
$"{(r.LakeMoved.Count > 0 ? string.Join("; ", r.LakeMoved) : "— none")} | " +
|
||||||
|
$"{(r.Joins.Count > 0 ? string.Join("; ", r.Joins) : "— none")} |");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("> ### ⚠ The min-rim signal — what to watch for");
|
||||||
|
sb.AppendLine("> The cap is applied to the **least-cost route's** rim climb, not to a theoretical minimum-rim path.");
|
||||||
|
sb.AppendLine("> LOWGROUND penalises uphill heavily so the chosen route is almost always the low-rim one — **but if");
|
||||||
|
sb.AppendLine("> a basin is walled off that visibly should have had a low way out, that is the signal we need a");
|
||||||
|
sb.AppendLine("> bottleneck (min-rim) search.** Check each walled-off river on the plate against its surroundings.");
|
||||||
|
sb.AppendLine("> Not built here.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## The per-river diagnostic");
|
||||||
|
sb.AppendLine();
|
||||||
|
foreach (var r in rows)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"### `{r.Seed}`");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| rank | class | basin has lake | drainage px | terminus | rim climb m | cap | joins | route len px |");
|
||||||
|
sb.AppendLine("|---|---|---|---|---|---|---|---|---|");
|
||||||
|
foreach (var rr in r.Rivers)
|
||||||
|
{
|
||||||
|
var c = rr.Candidate; var lo = rr.Lowland; var pr = rr.OceanProbe;
|
||||||
|
string term = rr.Joined
|
||||||
|
? $"→ tributary of #{rr.ConfluenceParentRank}"
|
||||||
|
: rr.Class switch
|
||||||
|
{
|
||||||
|
RiverRouting.RiverClass.OceanTrunk => $"sea ({c.TermX},{c.TermY})",
|
||||||
|
RiverRouting.RiverClass.RoutedGiant => lo != null && lo.Reached ? $"sea ({(int)lo.Target.x},{(int)lo.Target.y})" : "⚠ no route",
|
||||||
|
RiverRouting.RiverClass.LakeFed => lo != null && lo.Reached ? $"**lake** ({(int)lo.Target.x},{(int)lo.Target.y})" : "⚠ no route",
|
||||||
|
RiverRouting.RiverClass.WalledOff => $"**its own terminal** ({c.TermX},{c.TermY})",
|
||||||
|
_ => rr.LakeReached ? $"lake ({(int)lo.Target.x},{(int)lo.Target.y})" : $"its own terminal ({c.TermX},{c.TermY})",
|
||||||
|
};
|
||||||
|
sb.AppendLine($"| #{c.Rank} | {ClassName(rr.Class)} | {(c.IsSea ? "—" : (c.AnalysisKind == "lake-ender" ? "**yes**" : "no"))} | {c.DrainagePx:N0} | {term} | " +
|
||||||
|
$"{(pr != null && pr.Reached ? pr.RimClimbM.ToString("F1") : "—")} | " +
|
||||||
|
$"{(rr.RefusedByCap ? "**REFUSED**" : rr.Class == RiverRouting.RiverClass.RoutedGiant ? "under" : "—")} | " +
|
||||||
|
$"{(rr.Joined ? $"#{rr.ConfluenceParentRank} at ({rr.JunctionCell.x},{rr.JunctionCell.y})" : "—")} | " +
|
||||||
|
$"{(lo != null && lo.Reached ? lo.LenPx.ToString("F0") : "—")} |");
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
sb.AppendLine("## What was run");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Chain + analysis + routing at **{mapSize}** on **{seeds.Length} seeds** (`{string.Join(", ", seeds)}`), all rendered.");
|
||||||
|
sb.AppendLine($"Promoted set: the **pure top {promoteN}** — no quota; rivers/02b's K stays parked and unlocked.");
|
||||||
|
sb.AppendLine($"Rim cap **{rimCapM:F0} m** (`ISLA_RIM_CAP_M`). Significant water: 8-connected classify-water components ≥ {lakeMinPx:N0} px,");
|
||||||
|
sb.AppendLine("ocean excluded (the interim for v2's missing water-bodies table). Lowland-only smoothing unchanged —");
|
||||||
|
sb.AppendLine("the lowland reach is smoothed, the erosion-carved upland stem never is.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"**⚠ NOT touched:** `DrainageAnalysis` (reused, not rebuilt); `EndorheicMinDepthM` {def.EndorheicMinDepthM} m and `EndorheicMinAreaPx` {def.EndorheicMinAreaPx:N0}");
|
||||||
|
sb.AppendLine($"(they define the routing surface); `MinOutletSeparationPx` {def.MinOutletSeparationPx}; `StemMinAccPx` {def.StemMinAccPx}. Termini are classified by");
|
||||||
|
sb.AppendLine("`RegionLabeling.OceanMask` and the significant-lake mask only — no bare `h < sea`. `Giant.ProvisionalRoute` never drawn.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**⚠ No spatial term anywhere.** The `1063685222` southern-coast gap is *not* addressed here — it stays a");
|
||||||
|
sb.AppendLine("placement-era question, not a routing one.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## Files");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| File | What it is |");
|
||||||
|
sb.AppendLine("|---|---|");
|
||||||
|
sb.AppendLine("| `<seed>/refined_mix.png` | the five classes as a dendritic tree; white dot = confluence, yellow ring = rim crossed |");
|
||||||
|
sb.AppendLine("| `<seed>/grayscale.png` | the eroded render field, no palette |");
|
||||||
|
sb.AppendLine("| `rivers_<seed>.csv` | per river: class, `basin_has_lake`, faithful class, rim + cap verdict, confluence parent, route geometry, why |");
|
||||||
|
if (skipRaw)
|
||||||
|
sb.AppendLine("| ~~`<seed>/height.f32`~~ | **deliberately not written** — rivers/01 proved this field byte-identical to `chat2/11_erosion`. `ISLA_SKIP_RAW=0` regenerates it. |");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Ranges: sea level `{def.SeaLevel}` raw = `{WorldScale.MetresFromRaw(def.SeaLevel):F2} m`; {WorldScale.Describe()}.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("→ `XX_Human/output/rivers/03b_routing_refinement.report.md`");
|
||||||
|
WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
// ---- the curve (the house pattern; pool pinned family-off per rivers/01) -------------------
|
// ---- the curve (the house pattern; pool pinned family-off per rivers/01) -------------------
|
||||||
|
|
||||||
private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
|
private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue