rivers/03c: routing polish — lake-termination as a preference, lake-enders come home
Two surgical refinements of rivers/03b's fix 3. Everything else (confluence,
the rim cap, the faithful default) unchanged. Courses only: no height mutated,
no water filled or created — asserted per seed by a raw-bit digest.
- FIX A (ISLA_LAKE_PREFER_RATIO, default 0.4): a dry-basin router goes to the SEA
unless cost_lake < ratio * cost_ocean, i.e. unless a lake is materially cheaper.
rivers/03b used "nearest of {ocean, lake}", which is exactly ratio < 1.0 and
captured 12 rivers, halving the sea mouths. Both leg costs are now recorded for
every router so the knob is readable off the table without a re-run.
- FIX B (OwnBasinLakeEnder): a natural lake-ender terminates at the water inside
its OWN terminal basin at any size. The 20,000 px significance threshold had
exiled 999999937 #3 from its sub-threshold home lake, sending it ~5,800 px along
the shoreline. The threshold still applies to routers choosing a distant lake.
As predicted, the rim cap re-arms: 31415926 #2 (66.7 m) now goes to the sea and
is refused, rather than being hidden in a lake.
Both fixes are off by default, so faithful mode still reproduces rivers/03 and
refined mode still reproduces rivers/03b.
Taste gate: nothing locked, nothing graduated.
This commit is contained in:
parent
b5ceca0419
commit
09d23127e7
2 changed files with 344 additions and 39 deletions
|
|
@ -297,6 +297,35 @@ namespace IslaApocalypse.Tools
|
||||||
/// False = the reference's behaviour (routers target ocean only).</summary>
|
/// False = the reference's behaviour (routers target ocean only).</summary>
|
||||||
public bool LakeTargetForRouters;
|
public bool LakeTargetForRouters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐⭐ rivers/03c FIX A — lake-termination becomes a PREFERENCE instead of an unconditional
|
||||||
|
/// capture. Set > 0 to enable; it then supersedes the plain nearest-of-union rule above.
|
||||||
|
///
|
||||||
|
/// ⚠⚠ WHY rivers/03b OVERSHOT. "Nearest of {ocean ∪ lake}" lets a lake that is merely a
|
||||||
|
/// *little* closer capture a river that had a clear shot at the coast — and it moved **12
|
||||||
|
/// rivers** to lake-fed, roughly halving the island's sea mouths (5/5/6/5 → 4/2/3/3). The
|
||||||
|
/// rule here is deliberately sea-biased instead:
|
||||||
|
/// <code>
|
||||||
|
/// lake-fed iff cost_lake < LakePreferRatio × cost_ocean
|
||||||
|
/// </code>
|
||||||
|
/// so a lake must be MATERIALLY cheaper to reach, not just nearer. **Lower ratio → more sea
|
||||||
|
/// rivers.** Both costs are recorded per router whether or not the lake wins, so the knob can
|
||||||
|
/// be read off the table without a re-run.
|
||||||
|
/// </summary>
|
||||||
|
public float LakePreferRatio;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐⭐ rivers/03c FIX B — a NATURAL lake-ender terminates at the water inside its OWN
|
||||||
|
/// terminal basin, at any size.
|
||||||
|
///
|
||||||
|
/// ⚠ The 20,000 px significance threshold is what exiled `999999937 #3` from its own home:
|
||||||
|
/// its basin's lake was sub-threshold, so it marched ~5,800 px along the shoreline hunting a
|
||||||
|
/// distant "significant" body. A basin's own water is where its flow goes regardless of how
|
||||||
|
/// big it is. **The threshold still applies to ROUTERS choosing a DISTANT lake** — a dry
|
||||||
|
/// basin still cannot connect itself to a three-cell puddle.
|
||||||
|
/// </summary>
|
||||||
|
public bool OwnBasinLakeEnder;
|
||||||
|
|
||||||
/// <summary>⭐ FIX 2 — the confluence post-pass: courses laid biggest-first join on true cell
|
/// <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.
|
/// intersection instead of running as parallel duplicates to the same mouth.
|
||||||
/// False = the reference's behaviour (no dedup, no join).</summary>
|
/// False = the reference's behaviour (no dedup, no join).</summary>
|
||||||
|
|
@ -328,6 +357,14 @@ namespace IslaApocalypse.Tools
|
||||||
/// <summary>⚠ The rim climb that was tested against the cap, and whether it was refused.</summary>
|
/// <summary>⚠ The rim climb that was tested against the cap, and whether it was refused.</summary>
|
||||||
public float CappedRimM;
|
public float CappedRimM;
|
||||||
public bool RefusedByCap;
|
public bool RefusedByCap;
|
||||||
|
|
||||||
|
/// <summary>⭐ rivers/03c FIX A's lever, recorded for EVERY router — lake-fed or not — so the
|
||||||
|
/// developer can read off which ratio value flips which river without a re-run.
|
||||||
|
/// <see cref="CostRatio"/> is cost_lake / cost_ocean; a river is lake-fed iff it is below
|
||||||
|
/// the configured ratio. NaN where the leg was not reachable.</summary>
|
||||||
|
public float CostOcean = float.NaN, CostLake = float.NaN, CostRatio = float.NaN;
|
||||||
|
/// <summary>Lake-enders (fix B): the route to its own basin's water, for the coast-hugger check.</summary>
|
||||||
|
public bool OwnBasinTargeted;
|
||||||
/// <summary>The class this river WOULD have had under the reference's rules — so every
|
/// <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>
|
/// reclassification the divergences caused is legible rather than silent.</summary>
|
||||||
public RiverClass FaithfulClass;
|
public RiverClass FaithfulClass;
|
||||||
|
|
@ -378,9 +415,13 @@ 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, Options opt = null)
|
Action<string> log, Options opt = null, int[] basinId = null)
|
||||||
{
|
{
|
||||||
opt ??= Options.Faithful;
|
opt ??= Options.Faithful;
|
||||||
|
if (opt.OwnBasinLakeEnder && basinId == null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"[RiverRouting] OwnBasinLakeEnder needs Plan.BasinId to know which water is a basin's OWN. " +
|
||||||
|
"Pass it; refusing to silently fall back to the distant-significant-body rule that produced the coast-hugger.");
|
||||||
|
|
||||||
// ⭐ FIX 3 — the router's target mask. With the divergence off this is the ocean alone, which
|
// ⭐ 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
|
// is the reference. With it on, a significant lake is an equally valid place for a river to
|
||||||
|
|
@ -422,18 +463,41 @@ namespace IslaApocalypse.Tools
|
||||||
bool refLakeEnder = c.AnalysisKind == "lake-ender";
|
bool refLakeEnder = c.AnalysisKind == "lake-ender";
|
||||||
if (!refLakeEnder)
|
if (!refLakeEnder)
|
||||||
{
|
{
|
||||||
// Route to the nearest of the target mask — {ocean} faithfully, {ocean ∪ lakes} refined.
|
|
||||||
var route = opt.LakeTargetForRouters
|
|
||||||
? RouteTo(height, n, routerTargets, c.TermX, c.TermY, style, sea)
|
|
||||||
: probe;
|
|
||||||
rr.FaithfulClass = RiverClass.RoutedGiant;
|
rr.FaithfulClass = RiverClass.RoutedGiant;
|
||||||
|
Route route;
|
||||||
|
bool stoppedAtLake;
|
||||||
|
|
||||||
|
if (opt.LakePreferRatio > 0f)
|
||||||
|
{
|
||||||
|
// ⭐⭐ rivers/03c FIX A — the two legs are costed SEPARATELY and compared, instead of
|
||||||
|
// racing in one search. That is the whole difference: a shared search returns
|
||||||
|
// whichever is nearer, this one returns the sea unless the lake is materially cheaper.
|
||||||
|
var lakeLeg = RouteTo(height, n, isSignificantWater, c.TermX, c.TermY, style, sea);
|
||||||
|
rr.CostOcean = probe.Reached ? probe.Cost : float.NaN;
|
||||||
|
rr.CostLake = lakeLeg.Reached ? lakeLeg.Cost : float.NaN;
|
||||||
|
rr.CostRatio = probe.Reached && lakeLeg.Reached && probe.Cost > 0f
|
||||||
|
? lakeLeg.Cost / probe.Cost : float.NaN;
|
||||||
|
|
||||||
|
// ⚠ No reachable lake → the sea, always. No reachable ocean → the lake if there is one.
|
||||||
|
stoppedAtLake = lakeLeg.Reached && probe.Reached
|
||||||
|
&& lakeLeg.Cost < opt.LakePreferRatio * probe.Cost;
|
||||||
|
if (lakeLeg.Reached && !probe.Reached) stoppedAtLake = true;
|
||||||
|
route = stoppedAtLake ? lakeLeg : probe;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// rivers/03b — the nearest of the union mask, whichever that turns out to be.
|
||||||
|
route = opt.LakeTargetForRouters
|
||||||
|
? RouteTo(height, n, routerTargets, c.TermX, c.TermY, style, sea)
|
||||||
|
: probe;
|
||||||
|
stoppedAtLake = route.Reached
|
||||||
|
&& isSignificantWater[(int)route.Target.x * n + (int)route.Target.y]
|
||||||
|
&& !isOcean[(int)route.Target.x * n + (int)route.Target.y];
|
||||||
|
}
|
||||||
|
|
||||||
rr.Lowland = route;
|
rr.Lowland = route;
|
||||||
rr.CappedRimM = route.Reached ? route.RimClimbM : 0f;
|
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)
|
if (!route.Reached)
|
||||||
{
|
{
|
||||||
rr.Class = RiverClass.RoutedGiant;
|
rr.Class = RiverClass.RoutedGiant;
|
||||||
|
|
@ -441,10 +505,12 @@ namespace IslaApocalypse.Tools
|
||||||
}
|
}
|
||||||
else if (stoppedAtLake)
|
else if (stoppedAtLake)
|
||||||
{
|
{
|
||||||
// ⭐ FIX 3 — it met a significant lake first. It terminates there. NO WATER CREATED:
|
// It ends at a significant lake — because that lake was nearer (03b) or materially
|
||||||
// the course simply ends at an existing body.
|
// cheaper (03c). NO WATER CREATED: the course simply ends at an existing body.
|
||||||
rr.Class = RiverClass.LakeFed;
|
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)";
|
rr.Why = opt.LakePreferRatio > 0f
|
||||||
|
? $"dry pan → LAKE-FED: reaching a significant lake costs {rr.CostLake:N0} vs {rr.CostOcean:N0} to the sea (ratio {rr.CostRatio:F3} < {opt.LakePreferRatio:F2}) — materially cheaper, so it ends at the lake"
|
||||||
|
: $"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)
|
else if (route.RimClimbM > opt.RimCapM)
|
||||||
{
|
{
|
||||||
|
|
@ -458,28 +524,56 @@ namespace IslaApocalypse.Tools
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
rr.Class = RiverClass.RoutedGiant;
|
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}";
|
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")}, cost {route.Cost:N0}" +
|
||||||
|
(opt.LakePreferRatio > 0f && !float.IsNaN(rr.CostRatio) ? $"; the nearest lake was not materially cheaper (ratio {rr.CostRatio:F3} ≥ {opt.LakePreferRatio:F2})" : "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
rr.Class = RiverClass.LakeEnder;
|
rr.Class = RiverClass.LakeEnder;
|
||||||
// The stem pools on dry ground short of its lake BECAUSE the pooling point is a local
|
// The stem pools on dry ground short of its lake BECAUSE the pooling point is a local
|
||||||
// minimum — a blind descent dead-ends there immediately. Route to the nearest
|
// minimum — a blind descent dead-ends there immediately. Route with the same lowground
|
||||||
// SIGNIFICANT body with the same lowground Dijkstra, so the course joins the lake.
|
// Dijkstra so the course actually joins the water.
|
||||||
// ⚠ Lake-enders route with LOWGROUND regardless of the style knob (the reference's rule).
|
// ⚠ Lake-enders route with LOWGROUND regardless of the style knob (the reference's rule).
|
||||||
var ext = RouteTo(height, n, isSignificantWater, c.TermX, c.TermY, StyleLowground, sea);
|
Route ext;
|
||||||
if (!ext.Reached)
|
if (opt.OwnBasinLakeEnder)
|
||||||
{
|
{
|
||||||
// Fall back to ANY classify water, so a seed whose lake-ender genuinely has only
|
// ⭐⭐ rivers/03c FIX B — its OWN basin's water, at any size. This is where its flow
|
||||||
// small ponds still connects rather than dead-ending.
|
// goes; it has no business hunting a distant body. Killing the coast-hugger outright.
|
||||||
var fb = RouteTo(height, n, isClassifyWater, c.TermX, c.TermY, StyleLowground, sea);
|
var ownWater = new bool[n * n];
|
||||||
if (fb.Reached) { ext = fb; rr.LakeWasFallback = true; }
|
int owned = 0;
|
||||||
|
for (int i = 0; i < ownWater.Length; i++)
|
||||||
|
if (basinId[i] == c.BasinId && isClassifyWater[i] && !isOcean[i]) { ownWater[i] = true; owned++; }
|
||||||
|
if (owned > 0)
|
||||||
|
{
|
||||||
|
ext = RouteTo(height, n, ownWater, c.TermX, c.TermY, StyleLowground, sea);
|
||||||
|
rr.OwnBasinTargeted = ext.Reached;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ⚠ Should not happen — basinHasLake is what put it in this branch — but a
|
||||||
|
// basin whose water is all ocean-masked would land here. Report, do not crash.
|
||||||
|
ext = new Route();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var far = RouteTo(height, n, isSignificantWater, c.TermX, c.TermY, StyleLowground, sea);
|
||||||
|
ext = far;
|
||||||
|
if (!ext.Reached)
|
||||||
|
{
|
||||||
|
// Fall back to ANY classify water, so a seed whose lake-ender genuinely has only
|
||||||
|
// small ponds still connects rather than dead-ending.
|
||||||
|
var fb = RouteTo(height, n, isClassifyWater, c.TermX, c.TermY, StyleLowground, sea);
|
||||||
|
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.FaithfulClass = RiverClass.LakeEnder;
|
||||||
rr.Why = rr.LakeReached
|
rr.Why = rr.LakeReached
|
||||||
? $"terminal basin holds classify water → natural lake-ender; joins {(rr.LakeWasFallback ? "a small body (fallback)" : "a significant body")} {ext.LenPx:F0} px away"
|
? (rr.OwnBasinTargeted
|
||||||
|
? $"terminal basin holds classify water → natural lake-ender; terminates at its OWN basin's water {ext.LenPx:F0} px away (fix B — no distant-body hunt, so no coast-hugging)"
|
||||||
|
: $"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 → natural 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,7 +583,9 @@ namespace IslaApocalypse.Tools
|
||||||
$"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")}" +
|
$"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.RefusedByCap ? " ⚠ REFUSED BY CAP" : "")}" +
|
||||||
$"{(rr.Class == RiverClass.LakeFed ? " ⭐ stopped at a lake, not the coast" : "")}");
|
$"{(rr.Class == RiverClass.LakeFed ? $" ⭐ LAKE-FED (ratio {rr.CostRatio:F3})" : "")}" +
|
||||||
|
$"{(!float.IsNaN(rr.CostRatio) && rr.Class == RiverClass.RoutedGiant ? $" → SEA (lake ratio {rr.CostRatio:F3})" : "")}" +
|
||||||
|
$"{(rr.OwnBasinTargeted ? $" ⭐ own-basin water, {rr.Lowland.LenPx:F0} px" : "")}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opt.Confluence) Confluence(outp, log);
|
if (opt.Confluence) Confluence(outp, log);
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,10 @@ namespace IslaApocalypse.Tools
|
||||||
public int SeaReaching;
|
public int SeaReaching;
|
||||||
/// <summary>⭐ Rivers whose water reaches the sea, counting tributaries through their trunk.</summary>
|
/// <summary>⭐ Rivers whose water reaches the sea, counting tributaries through their trunk.</summary>
|
||||||
public int SeaConnected;
|
public int SeaConnected;
|
||||||
|
/// <summary>⭐ rivers/03c: every router's lake/ocean cost ratio — the tuning lever, as data.</summary>
|
||||||
|
public List<(int rank, long px, bool lakeFed, float costOcean, float costLake, float ratio)> Ratios = new();
|
||||||
|
/// <summary>The coast-hugger check: the longest natural-lake-ender extension on this seed.</summary>
|
||||||
|
public float MaxLakeEnderLenPx; public int MaxLakeEnderRank;
|
||||||
public List<string> CapMoved = new();
|
public List<string> CapMoved = new();
|
||||||
public List<string> LakeMoved = new();
|
public List<string> LakeMoved = new();
|
||||||
public List<string> Joins = new();
|
public List<string> Joins = new();
|
||||||
|
|
@ -100,16 +104,26 @@ namespace IslaApocalypse.Tools
|
||||||
|
|
||||||
// ⭐ rivers/03b — the three approved DIVERGENCES. Default OFF, so this tool still reproduces
|
// ⭐ rivers/03b — the three approved DIVERGENCES. Default OFF, so this tool still reproduces
|
||||||
// rivers/03's faithful port bit-for-bit.
|
// rivers/03's faithful port bit-for-bit.
|
||||||
bool refined = EnvStr("ISLA_ROUTING_MODE", "faithful").Trim().ToLowerInvariant() == "refined";
|
string mode = EnvStr("ISLA_ROUTING_MODE", "faithful").Trim().ToLowerInvariant();
|
||||||
|
bool polished = mode == "polished"; // rivers/03c
|
||||||
|
bool refined = mode == "refined" || polished;
|
||||||
float rimCapM = float.TryParse(EnvStr("ISLA_RIM_CAP_M", "30"), out float rc) ? rc : 30f;
|
float rimCapM = float.TryParse(EnvStr("ISLA_RIM_CAP_M", "30"), out float rc) ? rc : 30f;
|
||||||
var opt = refined
|
// ⭐ rivers/03c FIX A — sea-aggressive by default, per the developer's lean.
|
||||||
? new RiverRouting.Options { RimCapM = rimCapM, LakeTargetForRouters = true, Confluence = true }
|
float lakeRatio = float.TryParse(EnvStr("ISLA_LAKE_PREFER_RATIO", "0.4"), out float lr) ? lr : 0.4f;
|
||||||
: RiverRouting.Options.Faithful;
|
var opt = polished
|
||||||
|
? new RiverRouting.Options
|
||||||
|
{
|
||||||
|
RimCapM = rimCapM, LakeTargetForRouters = true, Confluence = true,
|
||||||
|
LakePreferRatio = lakeRatio, OwnBasinLakeEnder = true,
|
||||||
|
}
|
||||||
|
: 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);
|
||||||
// ⭐ rivers/03b is a LETTERED SUB-TASK of 03 — same authoring task, three changed rules.
|
// ⭐ rivers/03b is a LETTERED SUB-TASK of 03 — same authoring task, three changed rules.
|
||||||
string taskSfx = EnvStr("ISLA_TASK_SUFFIX", refined ? "b" : "");
|
string taskSfx = EnvStr("ISLA_TASK_SUFFIX", polished ? "c" : refined ? "b" : "");
|
||||||
string descr = EnvStr("ISLA_BATCH", refined ? "routing_refinement" : "lowland_routing");
|
string descr = EnvStr("ISLA_BATCH", polished ? "routing_polish" : 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);
|
||||||
|
|
@ -153,9 +167,11 @@ namespace IslaApocalypse.Tools
|
||||||
var dpDefaults = new DrainageAnalysis.Params();
|
var dpDefaults = new DrainageAnalysis.Params();
|
||||||
|
|
||||||
GD.Print("==================================================================");
|
GD.Print("==================================================================");
|
||||||
GD.Print(refined
|
GD.Print(polished
|
||||||
? " ROUTING REFINEMENT (rivers/03b) — three DELIBERATE DIVERGENCES from the faithful port, COURSES ONLY"
|
? " ROUTING POLISH (rivers/03c) — lake-termination as a PREFERENCE + own-basin lake-enders, COURSES ONLY"
|
||||||
: " LOWLAND ROUTING (rivers/03) — the routed MIX on the pure top-N, COURSES ONLY");
|
: 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");
|
||||||
|
|
@ -177,6 +193,17 @@ namespace IslaApocalypse.Tools
|
||||||
GD.Print( " becomes a tributary. (Reference: no dedup, no join — parallel duplicates to one mouth.)");
|
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( " ⚠ KEPT: basinHasLake stays the sort — a basin that already holds a lake is a natural lake-ender.");
|
||||||
}
|
}
|
||||||
|
if (polished)
|
||||||
|
{
|
||||||
|
GD.Print("⭐⭐ rivers/03c — TWO SURGICAL REFINEMENTS OF FIX 3 (rivers/03b overshot: 12 rivers moved to");
|
||||||
|
GD.Print(" lake-fed and the sea mouths roughly halved, 5/5/6/5 → 4/2/3/3):");
|
||||||
|
GD.Print($" FIX A LAKE-TERMINATION IS NOW A PREFERENCE, ratio {lakeRatio:F2} — a router goes to the SEA unless");
|
||||||
|
GD.Print($" cost_lake < {lakeRatio:F2} x cost_ocean, i.e. unless the lake is MATERIALLY cheaper. Lower → more sea rivers.");
|
||||||
|
GD.Print( " Both costs are recorded for EVERY router so the knob is readable off the table without a re-run.");
|
||||||
|
GD.Print( " FIX B A NATURAL LAKE-ENDER ENDS AT ITS OWN BASIN'S WATER, at any size — the 20,000 px significance");
|
||||||
|
GD.Print( " threshold no longer exiles it from home. (It still applies to ROUTERS choosing a DISTANT lake.)");
|
||||||
|
GD.Print($" ⚠ EXPECT THE RIM CAP TO RE-ARM: fix A sends high-rim routers at the sea again, so {rimCapM:F0} m starts refusing.");
|
||||||
|
}
|
||||||
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}");
|
||||||
|
|
@ -239,7 +266,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), opt);
|
significant, sea, style, m => GD.Print(m), opt, plan.BasinId);
|
||||||
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 ═══
|
||||||
|
|
@ -278,7 +305,14 @@ namespace IslaApocalypse.Tools
|
||||||
if (rr.Joined) { r.Joined++; r.Joins.Add($"#{rr.Candidate.Rank}→#{rr.ConfluenceParentRank} at ({rr.JunctionCell.x},{rr.JunctionCell.y})"); }
|
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.
|
// ⭐ 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.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)");
|
if (rr.Class == RiverRouting.RiverClass.LakeFed) r.LakeMoved.Add($"#{rr.Candidate.Rank} ({rr.Candidate.DrainagePx:N0} px{(float.IsNaN(rr.CostRatio) ? "" : $", ratio {rr.CostRatio:F3}")})");
|
||||||
|
if (!float.IsNaN(rr.CostRatio) || rr.Class == RiverRouting.RiverClass.LakeFed || rr.Class == RiverRouting.RiverClass.RoutedGiant || rr.Class == RiverRouting.RiverClass.WalledOff)
|
||||||
|
if (!rr.Candidate.IsSea && rr.Candidate.AnalysisKind != "lake-ender")
|
||||||
|
r.Ratios.Add((rr.Candidate.Rank, rr.Candidate.DrainagePx, rr.Class == RiverRouting.RiverClass.LakeFed, rr.CostOcean, rr.CostLake, rr.CostRatio));
|
||||||
|
// ⭐ The coast-hugger check — how far a NATURAL lake-ender had to travel to find water.
|
||||||
|
if (rr.Class == RiverRouting.RiverClass.LakeEnder && rr.Lowland != null && rr.Lowland.Reached
|
||||||
|
&& rr.Lowland.LenPx > r.MaxLakeEnderLenPx)
|
||||||
|
{ r.MaxLakeEnderLenPx = rr.Lowland.LenPx; r.MaxLakeEnderRank = rr.Candidate.Rank; }
|
||||||
}
|
}
|
||||||
// ⚠ A tributary has NO mouth of its own — it reaches the sea through its trunk. So the two
|
// ⚠ 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
|
// numbers are different and both are reported: how many rivers END at the sea, and how many
|
||||||
|
|
@ -298,7 +332,10 @@ namespace IslaApocalypse.Tools
|
||||||
(r.SharedMouths.Count > 0 ? $" ⚠⚠ STILL SHARED: {string.Join(", ", r.SharedMouths)}" : " ✅ no two rivers share a mouth") +
|
(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.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)}");
|
if (r.LakeMoved.Count > 0) GD.Print($" ⭐ lake-fed: {string.Join(", ", r.LakeMoved)}");
|
||||||
|
GD.Print($" ⭐ coast-hugger check: longest NATURAL lake-ender extension = {r.MaxLakeEnderLenPx:F0} px" +
|
||||||
|
(r.MaxLakeEnderRank > 0 ? $" (#{r.MaxLakeEnderRank})" : "") +
|
||||||
|
(r.MaxLakeEnderLenPx > 2000f ? " ⚠⚠ STILL WANDERING" : " ✅"));
|
||||||
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);
|
||||||
|
|
@ -306,7 +343,8 @@ namespace IslaApocalypse.Tools
|
||||||
results.Add(r);
|
results.Add(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refined) WriteRefinedIndex(batchRoot, mapSize, seeds, results, promoteN, lakeMinPx, rimCapM, dpDefaults, skipRaw);
|
if (polished) WritePolishIndex(batchRoot, mapSize, seeds, results, promoteN, lakeMinPx, rimCapM, lakeRatio, dpDefaults, skipRaw);
|
||||||
|
else 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);
|
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}");
|
||||||
|
|
@ -417,7 +455,7 @@ namespace IslaApocalypse.Tools
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("rank,class,basin_has_lake,analysis_kind,faithful_class,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,cap_verdict,max_step_uphill_m,max_elev_m,total_uphill_m,uphill_steps," +
|
"route_cost,cost_ocean,cost_lake,cost_ratio,own_basin_target,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,joined,confluence_parent_rank," +
|
"rim_x,rim_y,cells_expanded,lake_reached,lake_was_fallback,joined,confluence_parent_rank," +
|
||||||
"junction_x,junction_y,stem_width_px,why");
|
"junction_x,junction_y,stem_width_px,why");
|
||||||
foreach (var rr in r.Rivers)
|
foreach (var rr in r.Rivers)
|
||||||
|
|
@ -431,7 +469,10 @@ namespace IslaApocalypse.Tools
|
||||||
$"{(c.IsSea ? "" : ClassName(rr.FaithfulClass))},{c.TerminusName},{c.DrainagePx},{c.TermX},{c.TermY},{rr.Course.Count}," +
|
$"{(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.Reached ? pr.Cost.ToString("F0") : "")}," +
|
||||||
|
$"{(float.IsNaN(rr.CostOcean) ? "" : rr.CostOcean.ToString("F0"))},{(float.IsNaN(rr.CostLake) ? "" : rr.CostLake.ToString("F0"))}," +
|
||||||
|
$"{(float.IsNaN(rr.CostRatio) ? "" : rr.CostRatio.ToString("F4"))},{(rr.OwnBasinTargeted ? "yes" : "")}," +
|
||||||
|
$"{(pr != null ? pr.RimClimbM.ToString("F2") : "")}," +
|
||||||
$"{(rr.RefusedByCap ? "REFUSED" : rr.Class == RiverRouting.RiverClass.RoutedGiant ? "under cap" : "")}," +
|
$"{(rr.RefusedByCap ? "REFUSED" : rr.Class == RiverRouting.RiverClass.RoutedGiant ? "under cap" : "")}," +
|
||||||
$"{(pr != null ? pr.MaxStepUphillM.ToString("F3") : "")}," +
|
$"{(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() : "")}," +
|
||||||
|
|
@ -454,7 +495,7 @@ namespace IslaApocalypse.Tools
|
||||||
|
|
||||||
if (refined)
|
if (refined)
|
||||||
DrainageRenderer.RefinedMix(r.Rivers, baseImg.Duplicate() as Image, n,
|
DrainageRenderer.RefinedMix(r.Rivers, baseImg.Duplicate() as Image, n,
|
||||||
$"SEED {r.Seed} - THE RESHAPED MIX ON THE PURE TOP {promoteN} (RIVERS/03B)",
|
$"SEED {r.Seed} - THE MIX ON THE PURE TOP {promoteN}",
|
||||||
$"{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()}",
|
$"{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()}",
|
||||||
$"CHEAPEST ROUTE TO THE SEA CLIMBS MORE THAN THE {rimCapM:F0} M RIM CAP, SO IT ENDS AT ITS OWN TERMINAL")
|
$"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"));
|
.SavePng(Path.Combine(dir, "refined_mix.png"));
|
||||||
|
|
@ -801,6 +842,174 @@ namespace IslaApocalypse.Tools
|
||||||
WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString());
|
WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ⭐ THE POLISH INDEX (rivers/03c) — the before/after/after, and the ratio table that IS the knob.
|
||||||
|
/// </summary>
|
||||||
|
private static void WritePolishIndex(string batchRoot, int mapSize, int[] seeds,
|
||||||
|
List<SeedResult> rows, int promoteN, int lakeMinPx, float rimCapM, float lakeRatio,
|
||||||
|
DrainageAnalysis.Params def, bool skipRaw)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
int primary = seeds.Length > 0 ? seeds[0] : 0;
|
||||||
|
|
||||||
|
sb.AppendLine($"# Batch 03c — routing polish: lake-termination as a PREFERENCE, and lake-enders come home");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**⛔ TASTE GATE. Nothing locked, nothing graduated.** The ratio and the cap are both knobs.");
|
||||||
|
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. Fix B *terminates a course");
|
||||||
|
sb.AppendLine("at* existing in-basin water; it creates nothing.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## 👉 The pick — a before / after / after");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"- **`999999937/refined_mix.png`** beside **`../03b_routing_refinement/999999937/refined_mix.png`**");
|
||||||
|
sb.AppendLine(" — the coast-hugger seed. `#3` marched ~5,800 px along the shoreline in 03b; it should now stop at home.");
|
||||||
|
sb.AppendLine($"- **`{primary}/refined_mix.png`** beside **`../03_lowland_routing/{primary}/routed_mix.png`**");
|
||||||
|
sb.AppendLine(" — the full arc, from the faithful port to here.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("> ### ⭐⭐ THE JUDGMENT, STATED");
|
||||||
|
sb.AppendLine("> **Does the island read good now — more sea rivers, no coast-hugger, no uphill river, a natural");
|
||||||
|
sb.AppendLine("> dendritic tree — and is the mouth count and coastal spread healthy?**");
|
||||||
|
sb.AppendLine(">");
|
||||||
|
sb.AppendLine($"> The tuning lever is the ratio table below. To add more sea rivers, **lower `ISLA_LAKE_PREFER_RATIO`**");
|
||||||
|
sb.AppendLine($"> from {lakeRatio:F2} — the table shows exactly which rivers flip at which value. If too many now skirt");
|
||||||
|
sb.AppendLine("> lakes to reach the sea, raise it. One value, one re-run.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## The two fixes");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| | rivers/03b did | rivers/03c does |");
|
||||||
|
sb.AppendLine("|---|---|---|");
|
||||||
|
sb.AppendLine($"| **A** | router goes to the **nearest** of {{ocean ∪ significant lake}} — a lake merely a bit closer captures it | router goes to the **sea** unless `cost_lake < {lakeRatio:F2} × cost_ocean`, i.e. unless the lake is **materially** cheaper |");
|
||||||
|
sb.AppendLine($"| **B** | a natural lake-ender hunts the nearest **significant** (≥ {lakeMinPx:N0} px) body — exiling it from a sub-threshold home lake | it terminates at **its own basin's** water, **any size** |");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"⚠ The {lakeMinPx:N0} px threshold **still applies to routers** choosing a *distant* lake — a dry basin still");
|
||||||
|
sb.AppendLine("cannot connect itself to a three-cell puddle. It simply no longer exiles a lake-ender from home.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**Unchanged from rivers/03b:** the confluence post-pass, the rim cap, the `basinHasLake` sort, and the");
|
||||||
|
sb.AppendLine("faithful default (which still reproduces rivers/03 bit-for-bit).");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## ⭐ The MIX");
|
||||||
|
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();
|
||||||
|
bool anyShared = false;
|
||||||
|
foreach (var r in rows) if (r.SharedMouths.Count > 0) anyShared = true;
|
||||||
|
sb.AppendLine(anyShared
|
||||||
|
? "> ⚠⚠ Some mouths are still shared — see the per-seed lines in the log."
|
||||||
|
: "> ✅ **No two rivers share a mouth on any seed** — the confluence pass still holds.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## ⭐⭐ The ratio table — THIS IS THE KNOB");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Every dry-basin router, with the cost of reaching a significant lake against the cost of reaching the");
|
||||||
|
sb.AppendLine($"sea. **A river is lake-fed iff its ratio is below `{lakeRatio:F2}`.** Read down the ratio column to see");
|
||||||
|
sb.AppendLine("exactly which rivers a different setting would flip — no re-run needed.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Seed | river | drainage px | cost to sea | cost to lake | ⭐ ratio | verdict |");
|
||||||
|
sb.AppendLine("|---|---|---|---|---|---|---|");
|
||||||
|
foreach (var r in rows)
|
||||||
|
{
|
||||||
|
var sorted = new List<(int rank, long px, bool lakeFed, float costOcean, float costLake, float ratio)>(r.Ratios);
|
||||||
|
sorted.Sort((a, b) => (float.IsNaN(a.ratio) ? float.MaxValue : a.ratio).CompareTo(float.IsNaN(b.ratio) ? float.MaxValue : b.ratio));
|
||||||
|
foreach (var t in sorted)
|
||||||
|
sb.AppendLine($"| `{r.Seed}` | #{t.rank} | {t.px:N0} | {(float.IsNaN(t.costOcean) ? "—" : t.costOcean.ToString("N0"))} | " +
|
||||||
|
$"{(float.IsNaN(t.costLake) ? "— (no lake)" : t.costLake.ToString("N0"))} | " +
|
||||||
|
$"{(float.IsNaN(t.ratio) ? "—" : $"**{t.ratio:F3}**")} | {(t.lakeFed ? "lake-fed" : "→ sea")} |");
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## What the cap now catches, and the min-rim re-check");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"Fix A sends high-rim routers at the sea again, so the **{rimCapM:F0} m rim cap re-arms** — in rivers/03b it");
|
||||||
|
sb.AppendLine("caught nothing, because fix 3 had already hidden those rivers in lakes.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Seed | ⚠ walled off by the cap | lake-fed (with ratio) | confluences |");
|
||||||
|
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 — check each walled-off river on the plate");
|
||||||
|
sb.AppendLine("> The cap tests the **least-cost route's** rim, not a theoretical minimum-rim path. LOWGROUND penalises");
|
||||||
|
sb.AppendLine("> uphill heavily so the chosen route is almost always the low-rim one — **but if a walled-off basin");
|
||||||
|
sb.AppendLine("> visibly should have had a low way out, that is the signal a bottleneck (min-rim) search is wanted.**");
|
||||||
|
sb.AppendLine("> Not built here; flag it if you see it.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("## ⭐ The coast-hugger check (fix B)");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("The longest **natural lake-ender** extension per seed. In rivers/03b `999999937 #3` ran **5,841 px**.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("| Seed | longest natural lake-ender extension | verdict |");
|
||||||
|
sb.AppendLine("|---|---|---|");
|
||||||
|
foreach (var r in rows)
|
||||||
|
sb.AppendLine($"| `{r.Seed}` | {r.MaxLakeEnderLenPx:F0} px{(r.MaxLakeEnderRank > 0 ? $" (#{r.MaxLakeEnderRank})" : "")} | " +
|
||||||
|
$"{(r.MaxLakeEnderLenPx > 2000f ? "⚠⚠ still wandering" : "✅ home")} |");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("> ### ⚠ One thing this task deliberately does NOT fix");
|
||||||
|
sb.AppendLine("> A **routed-to-sea** river still tracing a bit of coastline near its mouth is the **deferred**");
|
||||||
|
sb.AppendLine("> LOWGROUND-on-flat-lowland arc — the cheapest corridor across a flat plain runs near the shore. That");
|
||||||
|
sb.AppendLine("> is judged **after the bed carve gives rivers width and depth**, not corrected here. It is a different");
|
||||||
|
sb.AppendLine("> thing from the lake-ender wander this task kills outright.");
|
||||||
|
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 | ratio | rim climb m | cap | joins |");
|
||||||
|
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 ? $"own lake ({(int)lo.Target.x},{(int)lo.Target.y}), {lo.LenPx:F0} px" : $"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} | " +
|
||||||
|
$"{(float.IsNaN(rr.CostRatio) ? "—" : rr.CostRatio.ToString("F3"))} | " +
|
||||||
|
$"{(pr != null && pr.Reached ? pr.RimClimbM.ToString("F1") : "—")} | " +
|
||||||
|
$"{(rr.RefusedByCap ? "**REFUSED**" : rr.Class == RiverRouting.RiverClass.RoutedGiant ? "under" : "—")} | " +
|
||||||
|
$"{(rr.Joined ? $"#{rr.ConfluenceParentRank}" : "—")} |");
|
||||||
|
}
|
||||||
|
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($"Pure top {promoteN}, no quota. `ISLA_LAKE_PREFER_RATIO={lakeRatio:F2}`, `ISLA_RIM_CAP_M={rimCapM:F0}`, significance {lakeMinPx:N0} px.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine($"**⚠ NOT touched:** `DrainageAnalysis` (reused); `EndorheicMinDepthM` {def.EndorheicMinDepthM} m / `EndorheicMinAreaPx` {def.EndorheicMinAreaPx:N0} (they define");
|
||||||
|
sb.AppendLine($"the routing surface); `MinOutletSeparationPx` {def.MinOutletSeparationPx}; `StemMinAccPx` {def.StemMinAccPx}. Termini classified by `OceanMask`, the");
|
||||||
|
sb.AppendLine("significant-lake mask, and per-basin classify water only — no bare `h < sea`. `ProvisionalRoute` never drawn.");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("**⚠ No spatial term anywhere** — the southern-coast question stays a placement-era 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, incl. **`cost_ocean` / `cost_lake` / `cost_ratio`** (fix A's lever) and `own_basin_target` (fix B) |");
|
||||||
|
if (skipRaw)
|
||||||
|
sb.AppendLine("| ~~`<seed>/height.f32`~~ | **deliberately not written** — rivers/01 proved this field byte-identical to `chat2/11_erosion`. |");
|
||||||
|
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/03c_routing_polish.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