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:
parent
c60510beb7
commit
2de9502e4f
2 changed files with 262 additions and 4 deletions
|
|
@ -53,6 +53,7 @@ public static class DrainageAnalysis
|
|||
public int EndorheicMaxCount = 3;
|
||||
|
||||
public int TrunkCount = 3; // ~3 sea-reaching trunks (developer)
|
||||
public int GiantCount = 3; // 21b: top endorheic giants promoted
|
||||
public int MinOutletSeparationPx = 400; // don't pick 3 mouths of one delta
|
||||
|
||||
public int StemMinAccPx = 1000; // stem tracing stops below this
|
||||
|
|
@ -83,6 +84,32 @@ public static class DrainageAnalysis
|
|||
public List<Stream> Tributaries = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A promoted endorheic giant (task 21b): one of the island's biggest drainage
|
||||
/// systems, which pools inland because erosion could not cross the flats.
|
||||
/// Kind "routed" carries a PROVISIONAL route across the flats to the ocean —
|
||||
/// the path part 2 would carve, drawn for the gate, not water. Kind
|
||||
/// "lake-ender" keeps its lake/lagoon terminal (real geography, developer's
|
||||
/// call). Terminal is where the MAIN STEM actually pools (its sub-minimum),
|
||||
/// which on a flat basin floor is more truthful than the basin's deepest cell.
|
||||
/// </summary>
|
||||
public class Giant : Stream
|
||||
{
|
||||
public (float x, float y) Terminal;
|
||||
public (float x, float y) Spill; // where the basin overtops
|
||||
public float BasinDepthM;
|
||||
public long BasinAreaPx;
|
||||
public string Kind = "routed"; // "routed" | "lake-ender"
|
||||
public bool SouthernCandidate;
|
||||
public bool TerminalInClassifyWater;
|
||||
public List<(float x, float y)> ProvisionalRoute; // null for lake-enders
|
||||
public bool RouteReachedOcean;
|
||||
public (float x, float y) MountainExit;
|
||||
public float MountainExitElevM;
|
||||
public bool ExitFound;
|
||||
public List<Stream> Tributaries = new();
|
||||
}
|
||||
|
||||
public class EndorheicTerminal
|
||||
{
|
||||
public (float x, float y) Terminal; // basin minimum
|
||||
|
|
@ -95,6 +122,7 @@ public static class DrainageAnalysis
|
|||
{
|
||||
public List<Trunk> Trunks = new();
|
||||
public List<EndorheicTerminal> Endorheics = new();
|
||||
public List<Giant> Giants = new(); // 21b: the promoted giants
|
||||
public int TerminalBasinCount; // basins that qualified as sinks
|
||||
public long PitsFilledCount; // depressions filled through
|
||||
public long LandCells, SeaReachingCells, EndorheicCells, UnroutedCells;
|
||||
|
|
@ -110,7 +138,14 @@ public static class DrainageAnalysis
|
|||
/// such) or fill and spill onward to the true sea. Without this mask the first
|
||||
/// draft called two of its three "sea-reaching" trunks done at enclosed
|
||||
/// lagoons, which is exactly the overclaim the gate must not inherit.</param>
|
||||
public static Plan Run(float[,] height, int mapSize, bool[] isOcean, Params p)
|
||||
/// <param name="isClassifyWater">Row-major mask of ANY classify water (WBID != 0):
|
||||
/// a giant whose main stem pools inside classify water is a natural lake-ender;
|
||||
/// one pooling on dry ground is a route-to-sea candidate.</param>
|
||||
/// <param name="southX">Southernmost-town position (or -1 for none): the giant
|
||||
/// whose terminal lies closest is flagged the SOUTHERN CANDIDATE and always
|
||||
/// routed provisionally, per the 21b design — shown, not forced.</param>
|
||||
public static Plan Run(float[,] height, int mapSize, bool[] isOcean,
|
||||
bool[] isClassifyWater, float southX, float southY, Params p)
|
||||
{
|
||||
int n = mapSize;
|
||||
int total = n * n;
|
||||
|
|
@ -122,6 +157,8 @@ public static class DrainageAnalysis
|
|||
for (int y = 0; y < n; y++)
|
||||
original[x * n + y] = height[x, y];
|
||||
|
||||
float[] plan_fullFilled = null; // set inside step 2, used by 21b routing
|
||||
|
||||
// --- 1. Priority-flood with one-ulp epsilon (routing surface only) ---
|
||||
float[] filled = (float[])original.Clone();
|
||||
{
|
||||
|
|
@ -197,6 +234,11 @@ public static class DrainageAnalysis
|
|||
basinDepthM.Add(depth); basinAreaPx.Add(area); basinMinCell.Add(minCell);
|
||||
}
|
||||
|
||||
// 21b: the FULL fill (before terminal reversion) is the provisional-
|
||||
// routing surface — on it, every basin overtops at its spill and drains
|
||||
// to the border, which is exactly "where the water would continue".
|
||||
plan_fullFilled = (float[])filled.Clone();
|
||||
|
||||
bool[] terminal = new bool[nextId];
|
||||
for (int id = 1; id < nextId; id++)
|
||||
{
|
||||
|
|
@ -270,8 +312,8 @@ public static class DrainageAnalysis
|
|||
// badly when the basin floor is flat (a lagoon bed scatters inflow across
|
||||
// many sub-minima — measured: a 500k-px lagoon system reported under 50k).
|
||||
long[] basinInflow = new long[basinMinCell.Count];
|
||||
int[] dest = new int[total]; // 0 unknown, -1 sea, -2 stuck, >0 basin id
|
||||
{
|
||||
int[] dest = new int[total]; // 0 unknown, -1 sea, -2 stuck, >0 basin id
|
||||
var path = new List<int>(4096);
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
|
|
@ -455,6 +497,161 @@ public static class DrainageAnalysis
|
|||
}
|
||||
}
|
||||
|
||||
// --- 5d. The promoted GIANTS (21b): mixed set, provisional routes ---
|
||||
// Top GiantCount terminal basins by TOTAL inflow. Their upland stems are the
|
||||
// island's real big rivers; whether each continues to the sea is the gate's
|
||||
// decision, previewed here.
|
||||
{
|
||||
var giantsRanked = new List<(int id, long inflow)>();
|
||||
for (int id = 1; id < basinMinCell.Count; id++)
|
||||
{
|
||||
int mc = basinMinCell[id];
|
||||
if (mc < 0 || basinId[mc] != id) continue;
|
||||
if (basinInflow[id] >= p.EndorheicMinInflowPx) giantsRanked.Add((id, basinInflow[id]));
|
||||
}
|
||||
giantsRanked.Sort((a, b) => b.inflow.CompareTo(a.inflow));
|
||||
|
||||
// Does a terminal basin HOLD classify water? The lake-ender test must look
|
||||
// at the whole pool, 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.
|
||||
bool[] basinHasLake = new bool[basinMinCell.Count];
|
||||
for (int i = 0; i < total; i++)
|
||||
if (basinId[i] != 0 && isClassifyWater[i] && !isOcean[i])
|
||||
basinHasLake[basinId[i]] = true;
|
||||
|
||||
// The main stem's ENTRY into the basin: the highest-accumulation cell
|
||||
// whose flow terminates in this basin. On a flat basin floor the deepest
|
||||
// cell sees only local trickles (the task-21 lesson), so the stem is
|
||||
// anchored on the strongest feeder instead.
|
||||
var bestEntry = new Dictionary<int, int>();
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
if (dest[i] <= 0) continue;
|
||||
if (!bestEntry.TryGetValue(dest[i], out int cur) || acc[i] > acc[cur])
|
||||
bestEntry[dest[i]] = i;
|
||||
}
|
||||
|
||||
// The giant whose pooling point sits closest to the southernmost town is
|
||||
// the SOUTHERN CANDIDATE — always routed provisionally (shown, not forced).
|
||||
int southernPick = -1;
|
||||
if (southX >= 0f)
|
||||
{
|
||||
float bestD = float.MaxValue;
|
||||
foreach (var (id, _) in giantsRanked.GetRange(0, Math.Min(p.GiantCount, giantsRanked.Count)))
|
||||
{
|
||||
int mc = basinMinCell[id];
|
||||
float ddx = mc / n - southX, ddy = mc % n - southY;
|
||||
float d2 = ddx * ddx + ddy * ddy;
|
||||
if (d2 < bestD) { bestD = d2; southernPick = id; }
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (id, inflow) in giantsRanked.GetRange(0, Math.Min(p.GiantCount, giantsRanked.Count)))
|
||||
{
|
||||
var g = new Giant { DrainageAreaPx = inflow, BasinDepthM = basinDepthM[id], BasinAreaPx = basinAreaPx[id] };
|
||||
if (!bestEntry.TryGetValue(id, out int entry)) entry = basinMinCell[id];
|
||||
|
||||
// Downstream from the strongest feeder to where it actually pools…
|
||||
int t2 = entry;
|
||||
var down = new List<int> { t2 };
|
||||
while (dir[t2] >= 0) { t2 = Target(t2); down.Add(t2); }
|
||||
g.Terminal = (t2 / n, t2 % n);
|
||||
// …then the full main stem, traced upstream from that pooling point.
|
||||
var stem = TraceStem(t2, p.StemMinAccPx);
|
||||
g.Course = Decimate(stem);
|
||||
g.Head = (stem[^1] / n, stem[^1] % n);
|
||||
g.TerminalInClassifyWater = isClassifyWater[t2];
|
||||
|
||||
for (int i = 0; i + p.ExitWindowPx < stem.Count; i++)
|
||||
{
|
||||
float rise = (original[stem[i + p.ExitWindowPx]] - original[stem[i]]) * M_PER_UNIT;
|
||||
if (rise / p.ExitWindowPx >= p.ExitGradeMin)
|
||||
{
|
||||
g.ExitFound = true;
|
||||
g.MountainExit = (stem[i] / n, stem[i] % n);
|
||||
g.MountainExitElevM = original[stem[i]] * M_PER_UNIT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Lean tributaries on the giant's stem, same junction rule as trunks.
|
||||
var stemSet = new HashSet<int>(stem);
|
||||
var cands = new List<(int cell, long acc)>();
|
||||
foreach (int sc in stem)
|
||||
{
|
||||
int cx = sc / n, cy = sc % n;
|
||||
for (int k = 0; k < 8; k++)
|
||||
{
|
||||
int nx = cx + DX[k], ny = cy + DY[k];
|
||||
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
|
||||
int ni = nx * n + ny;
|
||||
if (stemSet.Contains(ni)) continue;
|
||||
if (dir[ni] >= 0 && Target(ni) == sc && acc[ni] >= p.TributaryMinAccPx)
|
||||
cands.Add((ni, acc[ni]));
|
||||
}
|
||||
}
|
||||
cands.Sort((a, b) => b.acc.CompareTo(a.acc));
|
||||
var takenT = new List<int>();
|
||||
foreach (var (cell, _) in cands)
|
||||
{
|
||||
if (takenT.Count >= p.TributaryMaxPerTrunk) break;
|
||||
int cx2 = cell / n, cy2 = cell % n;
|
||||
bool dup = false;
|
||||
foreach (int tc in takenT)
|
||||
{
|
||||
float ddx = cx2 - tc / n, ddy = cy2 - tc % n;
|
||||
if (ddx * ddx + ddy * ddy < 30f * 30f) { dup = true; break; }
|
||||
}
|
||||
if (!dup) takenT.Add(cell);
|
||||
}
|
||||
foreach (int cell in takenT)
|
||||
{
|
||||
var trib = new Stream { DrainageAreaPx = acc[cell] };
|
||||
var ts = TraceStem(cell, Math.Max(p.StemMinAccPx, (int)(acc[cell] / 20)));
|
||||
trib.Course = Decimate(ts);
|
||||
trib.Head = (ts[^1] / n, ts[^1] % n);
|
||||
g.Tributaries.Add(trib);
|
||||
}
|
||||
|
||||
// Kind: the terminal BASIN holds a classify lake → natural lake-ender;
|
||||
// dry pan → route to sea; the southern candidate is always routed.
|
||||
g.SouthernCandidate = id == southernPick;
|
||||
g.TerminalInClassifyWater = g.TerminalInClassifyWater || basinHasLake[id];
|
||||
g.Kind = (basinHasLake[id] && !g.SouthernCandidate) ? "lake-ender" : "routed";
|
||||
|
||||
// PROVISIONAL route (routed giants): walk steepest descent on the FULL
|
||||
// fill from the pooling point — the basin overtops at its spill and
|
||||
// the walk continues along the terrain's own drainage to the ocean.
|
||||
// DRAWN, not carved; part 2 carves along a route like this one.
|
||||
if (g.Kind == "routed")
|
||||
{
|
||||
var route = new List<int>();
|
||||
int c = t2;
|
||||
bool spillRecorded = false;
|
||||
for (int guard = 0; guard < 4 * n; guard++)
|
||||
{
|
||||
route.Add(c);
|
||||
if (isOcean[c]) { g.RouteReachedOcean = true; break; }
|
||||
if (!spillRecorded && basinId[c] != id)
|
||||
{ g.Spill = (c / n, c % n); spillRecorded = true; }
|
||||
int cx = c / n, cy = c % n;
|
||||
float best = float.MaxValue; int bestN = -1;
|
||||
for (int k = 0; k < 8; k++)
|
||||
{
|
||||
int nx = cx + DX[k], ny = cy + DY[k];
|
||||
if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
|
||||
int ni = nx * n + ny;
|
||||
if (plan_fullFilled[ni] < best) { best = plan_fullFilled[ni]; bestN = ni; }
|
||||
}
|
||||
if (bestN < 0 || plan_fullFilled[bestN] >= plan_fullFilled[c]) break; // stuck (report via flag)
|
||||
c = bestN;
|
||||
}
|
||||
g.ProvisionalRoute = Decimate(route);
|
||||
}
|
||||
plan.Giants.Add(g);
|
||||
}
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue