feat: mixed river plan with provisional routes (terrain-water task 21b, C0b part 1 revised)

Still PURE ANALYSIS — zero terrain change, zero water; the source blueprint is
never written (md5-verified). Extends task 21 per the developer's b+c decision:
promote ~5-6 rivers, the natural ocean trunks PLUS the top endorheic giants.

Giants (top GiantCount terminal basins by per-basin total inflow) get their main
stem anchored on the STRONGEST FEEDER into the basin, not the basin's deepest
cell — on a flat basin floor the deepest cell sees only local trickles (the
task-21 lesson applied to stems). Classification is by what the terminal BASIN
holds, not the stem's single pooling cell (a stem can pool on dry ground a few
hundred px short of its lagoon and still be a lagoon river): basin holds a
classify lake -> LAKE-ENDER; dry pan -> ROUTED; the giant pooling nearest the
southernmost town is the SOUTHERN CANDIDATE and always ROUTED (shown, not
forced). Routed giants carry a PROVISIONAL route: steepest descent on the FULL
(no-terminal) epsilon fill, so the basin overtops at its spill and the walk
follows the terrain's own drainage to the ocean — the technique part 2 carves
with, here only drawn and flagged provisionalRoute_NOT_WATER in the JSON.

Seed 1280587109 result (defaults, 6 rivers): 3 ocean trunks (unchanged from
task 21) + GIANT 1 [ROUTED - SOUTHERN CANDIDATE] 2.27M px pooling in the SE
lagoon, 932-px route via spill (6598,5866) to the ocean — the island's biggest
river serving the south; GIANT 2 [LAKE-ENDER] 1.82M px ending at the E lagoon;
GIANT 3 [ROUTED] 1.76M px SW dry-pan system, 1716-px route to the SW coast.
All provisional routes reach the ocean. Analysis 22 s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-11 18:33:41 -04:00
parent c60510beb7
commit 2de9502e4f
2 changed files with 262 additions and 4 deletions

View file

@ -47,6 +47,8 @@ public partial class RiverPlanTool : Node
p.EndorheicMinInflowPx = EnvI("RIVERPLAN_ENDO_MIN_INFLOW_PX", p.EndorheicMinInflowPx);
p.EndorheicMaxCount = EnvI("RIVERPLAN_ENDO_MAX_COUNT", p.EndorheicMaxCount);
p.TrunkCount = EnvI("RIVERPLAN_TRUNK_COUNT", p.TrunkCount);
p.TrunkCount = EnvI("RIVERPLAN_OCEAN_N", p.TrunkCount); // 21b alias
p.GiantCount = EnvI("RIVERPLAN_GIANT_N", p.GiantCount);
p.MinOutletSeparationPx = EnvI("RIVERPLAN_OUTLET_SEPARATION_PX", p.MinOutletSeparationPx);
p.StemMinAccPx = EnvI("RIVERPLAN_STEM_MIN_ACC_PX", p.StemMinAccPx);
p.TributaryMinAccPx = EnvI("RIVERPLAN_TRIB_MIN_ACC_PX", p.TributaryMinAccPx);
@ -81,12 +83,22 @@ public partial class RiverPlanTool : Node
// destinations that make a trunk "sea-reaching".
int nn = bp.MapSize;
bool[] isOcean = new bool[nn * nn];
bool[] isClassifyWater = new bool[nn * nn];
for (int x = 0; x < nn; x++)
for (int y = 0; y < nn; y++)
isOcean[x * nn + y] = bp.WaterBodyIds[x, y] == 1;
{
ushort wb = bp.WaterBodyIds[x, y];
isOcean[x * nn + y] = wb == 1;
isClassifyWater[x * nn + y] = wb != 0;
}
// Southernmost town — the 21b SOUTHERN CANDIDATE anchor (shown, not forced).
float southX = -1f, southY = -1f;
foreach (var t in bp.Towns)
if (t.Position.Y > southY) { southX = t.Position.X; southY = t.Position.Y; }
var p = ReadParams();
var plan = DrainageAnalysis.Run(bp.HeightMap, bp.MapSize, isOcean, p);
var plan = DrainageAnalysis.Run(bp.HeightMap, bp.MapSize, isOcean, isClassifyWater, southX, southY, p);
ulong t2 = Time.GetTicksMsec();
GD.Print($"[RiverPlan] analysis in {(t2 - t1) / 1000.0:F1}s.");
@ -123,6 +135,22 @@ public partial class RiverPlanTool : Node
(wb > 1 ? $" — terminates IN classify lake/lagoon WBID {wb} (river-feeds-lake)" : " — dry closed basin") + ".");
}
// ---- 21b: the promoted giants ----
int gi = 0;
foreach (var g in plan.Giants)
{
gi++;
GD.Print($"[RiverPlan] GIANT {gi} [{g.Kind.ToUpper()}{(g.SouthernCandidate ? " — SOUTHERN CANDIDATE" : "")}]: " +
$"drainage {g.DrainageAreaPx} px, pools at ({g.Terminal.x:F0},{g.Terminal.y:F0}) " +
$"({(g.TerminalInClassifyWater ? "in classify water" : "dry pan")}, basin {g.BasinDepthM:F1} m / {g.BasinAreaPx} px), " +
(g.ExitFound ? $"mountain-exit ({g.MountainExit.x:F0},{g.MountainExit.y:F0}) at {g.MountainExitElevM:F0} m, " : "") +
$"{g.Tributaries.Count} tributaries" +
(g.ProvisionalRoute != null
? $"; PROVISIONAL route {g.ProvisionalRoute.Count * 4} px via spill ({g.Spill.x:F0},{g.Spill.y:F0}) " +
(g.RouteReachedOcean ? "-> reaches the OCEAN" : "-> DID NOT reach the ocean (walk stuck — report)")
: "; ends at its lake") + ".");
}
// ---- the southern-town report (filed fact, not a constraint) ----
if (bp.Towns.Count > 0)
{
@ -209,6 +237,39 @@ public partial class RiverPlanTool : Node
if (i < plan.Trunks.Count - 1) sb.Append(',');
sb.Append('\n');
}
sb.Append("],\n\"giants\": [\n");
for (int i = 0; i < plan.Giants.Count; i++)
{
var g = plan.Giants[i];
sb.Append(" {\"kind\": \"").Append(g.Kind).Append("\", ");
sb.Append($"\"southernCandidate\": {(g.SouthernCandidate ? "true" : "false")}, ");
sb.Append($"\"drainageAreaPx\": {g.DrainageAreaPx}, ");
sb.Append("\"terminal\": "); Pt(sb, g.Terminal);
sb.Append($", \"terminalInClassifyWater\": {(g.TerminalInClassifyWater ? "true" : "false")}, ");
sb.Append($"\"basinDepthM\": {g.BasinDepthM.ToString("F2", ci)}, \"basinAreaPx\": {g.BasinAreaPx}, ");
sb.Append($"\"exitFound\": {(g.ExitFound ? "true" : "false")}, \"mountainExit\": "); Pt(sb, g.MountainExit);
sb.Append($", \"mountainExitElevM\": {g.MountainExitElevM.ToString("F1", ci)},\n \"course\": ");
Course(g.Course);
if (g.ProvisionalRoute != null)
{
sb.Append(",\n \"spill\": "); Pt(sb, g.Spill);
sb.Append($", \"routeReachedOcean\": {(g.RouteReachedOcean ? "true" : "false")}");
sb.Append(",\n \"provisionalRoute_NOT_WATER\": ");
Course(g.ProvisionalRoute);
}
sb.Append(",\n \"tributaries\": [");
for (int j = 0; j < g.Tributaries.Count; j++)
{
var tr = g.Tributaries[j];
if (j > 0) sb.Append(',');
sb.Append($"\n {{\"drainageAreaPx\": {tr.DrainageAreaPx}, \"course\": ");
Course(tr.Course);
sb.Append('}');
}
sb.Append("]\n }");
if (i < plan.Giants.Count - 1) sb.Append(',');
sb.Append('\n');
}
sb.Append("],\n\"endorheics\": [");
for (int i = 0; i < plan.Endorheics.Count; i++)
{