feat: tributary water (tapered), significant-lake targeting, finer steps + deeper beds (terrain-water task 24)
All three gate polish items — they interleave in the same files, so they land as one buildable commit rather than three that do not compile independently. 1. TRIBUTARY WATER. Tributaries carved in task 22/23 but stayed dry: they were never registered with the water stage. They now are (as CarvedRiver entries named 'trib', kept OUT of the per-river console table so it stays the 6 mains). RiverTribWaterMinFlow (40k px of along-course flow) sets where water starts; RiverTribTaperPx (120 px) makes the wet->dry transition a FADE, not a wall — across the taper the wet strip narrows (0.25x -> 1x half-width) AND its surface drops toward the bed, so a stream head thins out and vanishes. Along-course flow is modelled quadratically from headwater trickle to full drainage at the mouth (the plan records drainage per river, not per sample). RiverTribWaterMinFlow=0 waters them end to end — the documented fallback. 2. LAKE-ENDER TARGETING. The join routed to the nearest classify-water CELL, which a 322-px puddle satisfies; it stopped ~80 px short of the 197k-px lagoon beside it. It now routes to a SIGNIFICANT water mask (bodies of at least RiverLakeMinTargetPx = 20k cells, built from the water-body table), falling back to any classify water only if no significant body is reachable, so a seed with genuinely small ponds still connects. Proven by adjacency: task 23 had 63 river cells touching the puddle and 0 touching the lagoon; task 24 has 49 touching the lagoon and 0 touching the puddle. 3. FINER STEPS + DEEPER BEDS. RiverStepDropM 2.0 -> 0.6 and RiverWaterDepthM 1.2 -> 2.2, RiverDepthScale 1.0 -> 1.5. Reaches 298 -> 1034; the level gap between adjacent reaches drops from median 0.571 m / p95 2.08 m to median 0.124 m / p95 0.74 m — the pond-staircase reads as a graded descent. Reaches stay trivial bodies (median 44 px). Guards all held: flood guard 0 newly-below-sea and 0 below-sea cells modified, BIOME oracle md5-identical to the task-22 baseline (output-only unchanged), island top 457.65 m exact, crater core excluded, lowest carved cell exactly sea+margin (37.85 m). Cumulative max cut 25.98 m with 339 cells >20 m — identical to task 23, so deepening the WATER did not deepen the worst cuts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
645955d49c
commit
0e39538604
3 changed files with 113 additions and 15 deletions
|
|
@ -66,6 +66,14 @@ public static class RiverCarvePass
|
|||
public float WidthScale = 1.0f;
|
||||
public float DepthScale = 1.0f;
|
||||
public float SeaMarginM = 0.2f; // bed floor above sea, everywhere
|
||||
// Task 24: a tributary reach is WET where its along-course flow exceeds this;
|
||||
// upstream of that it TAPERS to dry over TribTaperPx rather than ending in a
|
||||
// wall of water. 0 = water tributaries end to end.
|
||||
public int TribWaterMinFlowPx = 40_000;
|
||||
public int TribTaperPx = 120;
|
||||
// Lake-enders route to the nearest water body of at least this size — the
|
||||
// nearest wet PIXEL was a puddle (task-23 gate finding).
|
||||
public int LakeMinTargetPx = 20_000;
|
||||
public DrainageAnalysis.Params PlanParams = new();
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +102,12 @@ public static class RiverCarvePass
|
|||
public float[] Bed; // raw units, monotone non-increasing
|
||||
public float[] HalfW; // px
|
||||
public bool ReachedWaterTerminal; // lake-enders: extension reached classify water
|
||||
// Task 24: along-course flow (px of drainage) per sample, and the first index
|
||||
// that carries water. Between WetFrom-TaperPx and WetFrom the water tapers
|
||||
// (narrowing and shallowing to the bed) so a stream head fades out.
|
||||
public float[] Flow;
|
||||
public int WetFrom;
|
||||
public int TaperPx;
|
||||
}
|
||||
|
||||
public class Stats
|
||||
|
|
@ -107,8 +121,13 @@ public static class RiverCarvePass
|
|||
public double AnalysisSeconds, RoutingSeconds, CarveSeconds;
|
||||
}
|
||||
|
||||
/// <param name="isSignificantWater">Row-major mask of classify water belonging to
|
||||
/// bodies of at least LakeMinTargetPx cells (task 24). Lake-enders route to THIS,
|
||||
/// not to any wet pixel: the task-23 build routed one into a puddle a few hundred
|
||||
/// px short of the obvious lagoon, because "nearest classify water" is satisfied
|
||||
/// by a 3-cell pond.</param>
|
||||
public static Stats Apply(float[,] height, int mapSize, bool[] isOcean,
|
||||
bool[] isClassifyWater, float southX, float southY,
|
||||
bool[] isClassifyWater, bool[] isSignificantWater, float southX, float southY,
|
||||
float[,] seaMap, float seaFlat,
|
||||
float craterCx, float craterCy, float craterCoreRadius,
|
||||
Func<double> secondsNow, Params p)
|
||||
|
|
@ -171,8 +190,14 @@ public static class RiverCarvePass
|
|||
bool reachedLake = false;
|
||||
if (g.Kind == "lake-ender")
|
||||
{
|
||||
var ext = RouteToOcean(height, n, isClassifyWater,
|
||||
// Target SIGNIFICANT water (task 24). Fall back to any classify water
|
||||
// only if no significant body is reachable, so a seed whose lake-ender
|
||||
// genuinely has only small ponds still connects rather than dead-ending.
|
||||
var ext = RouteToOcean(height, n, isSignificantWater,
|
||||
(int)g.Terminal.x, (int)g.Terminal.y, STYLE_LOWGROUND, SeaAt);
|
||||
if (ext.Count == 0)
|
||||
ext = RouteToOcean(height, n, isClassifyWater,
|
||||
(int)g.Terminal.x, (int)g.Terminal.y, STYLE_LOWGROUND, SeaAt);
|
||||
if (ext.Count > 0) { route = ext; reachedLake = true; }
|
||||
}
|
||||
|
||||
|
|
@ -304,7 +329,10 @@ public static class RiverCarvePass
|
|||
{
|
||||
var course = new List<(float x, float y)>(trib.Course);
|
||||
course.Reverse(); // head → confluence
|
||||
CarveRiver(null, "tributary", trib.DrainageAreaPx, course, null,
|
||||
// Task 24: tributaries are registered as carved rivers (name "trib") so the
|
||||
// water stage sees them — they carved but stayed dry in task 23. They are NOT
|
||||
// added to stats.Rivers, so the per-river console table stays the 6 mains.
|
||||
CarveRiver("trib", "tributary", trib.DrainageAreaPx, course, null,
|
||||
height, n, seaAt, coreSq, craterCx, craterCy, p, stats);
|
||||
}
|
||||
|
||||
|
|
@ -452,11 +480,29 @@ public static class RiverCarvePass
|
|||
|
||||
if (name != null)
|
||||
{
|
||||
stats.Rivers.Add(rs);
|
||||
if (kind != "tributary") stats.Rivers.Add(rs);
|
||||
// Along-course flow: drainage grows from a headwater trickle to the full
|
||||
// figure at the mouth. Quadratic in t so the substantial lower half
|
||||
// dominates — a first-order stand-in for real accumulation, which the
|
||||
// plan only records per-river.
|
||||
var flow = new float[m];
|
||||
for (int i = 0; i < m; i++)
|
||||
{
|
||||
float t = m > 1 ? (float)i / (m - 1) : 1f;
|
||||
flow[i] = drainagePx * (0.05f + 0.95f * t * t);
|
||||
}
|
||||
int wetFrom = 0;
|
||||
if (kind == "tributary" && p.TribWaterMinFlowPx > 0)
|
||||
{
|
||||
wetFrom = m; // dry unless the threshold is met
|
||||
for (int i = 0; i < m; i++)
|
||||
if (flow[i] >= p.TribWaterMinFlowPx) { wetFrom = i; break; }
|
||||
}
|
||||
stats.Carved.Add(new CarvedRiver
|
||||
{
|
||||
Name = name, Kind = kind, DrainagePx = drainagePx,
|
||||
Dense = dense, Bed = bed, HalfW = halfW
|
||||
Dense = dense, Bed = bed, HalfW = halfW,
|
||||
Flow = flow, WetFrom = wetFrom, TaperPx = p.TribTaperPx
|
||||
});
|
||||
}
|
||||
return rs;
|
||||
|
|
@ -501,7 +547,11 @@ public static class RiverCarvePass
|
|||
{
|
||||
int m = r.Dense.Count;
|
||||
if (m < 2) continue;
|
||||
int i = 0;
|
||||
// Task 24: start at the wet-from index (tributary threshold; 0 for mains),
|
||||
// but back up by the taper length so the transition is a FADE, not a wall.
|
||||
int i = Math.Max(0, r.WetFrom - r.TaperPx);
|
||||
if (i >= m) continue; // entirely below threshold: dry
|
||||
int taperStart = i, taperEnd = Math.Min(m - 1, r.WetFrom);
|
||||
float lastLevel = float.MaxValue;
|
||||
while (i < m)
|
||||
{
|
||||
|
|
@ -518,7 +568,14 @@ public static class RiverCarvePass
|
|||
var reach = new Reach { Id = nextId, River = r.Name, Level = level };
|
||||
for (int k2 = i; k2 < j; k2++)
|
||||
{
|
||||
float hw = r.HalfW[k2];
|
||||
// Taper: 0 at the dry end of the fade zone → 1 at full water. The
|
||||
// water narrows AND its surface drops to the bed, so a stream head
|
||||
// thins out and disappears instead of ending in a flat wall.
|
||||
float taper = 1f;
|
||||
if (taperEnd > taperStart && k2 < taperEnd)
|
||||
taper = (float)(k2 - taperStart) / (taperEnd - taperStart);
|
||||
if (taper <= 0.02f) continue;
|
||||
float hw = r.HalfW[k2] * (0.25f + 0.75f * taper);
|
||||
int x0 = (int)MathF.Floor(r.Dense[k2].x - hw), x1 = (int)MathF.Ceiling(r.Dense[k2].x + hw);
|
||||
int y0 = (int)MathF.Floor(r.Dense[k2].y - hw), y1 = (int)MathF.Ceiling(r.Dense[k2].y + hw);
|
||||
for (int x = x0; x <= x1; x++)
|
||||
|
|
@ -535,7 +592,12 @@ public static class RiverCarvePass
|
|||
float h = height[x, y];
|
||||
float sea = SeaAt(x, y);
|
||||
if (h < sea) continue; // ocean/seam territory
|
||||
if (h >= level) continue; // bank above the water line
|
||||
// The tapered surface sits between the bed and the reach
|
||||
// level, so the wet strip shallows as it narrows.
|
||||
float localLevel = taper >= 1f
|
||||
? level
|
||||
: r.Bed[k2] + (level - r.Bed[k2]) * taper;
|
||||
if (h >= localLevel) continue; // bank above the water line
|
||||
wbid[x, y] = nextId;
|
||||
reach.PixelCount++;
|
||||
reach.Cx += x; reach.Cy += y;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue