islaApocalypse-v2/Tools/Scripts/RiverCandidate.cs
beezm ea301f7a8c rivers/02: river promotion — unified ranking over both termini, and the count gate
Choose the river count against the terrain that actually exists. The M3 count of
3 was tuned on topography the southern stretch (D-065) and coastal fragmentation
(D-063) have since replaced, and TrunkCount/GiantCount/EndorheicMaxCount = 3 are
LEAN REPORTING CAPS, not a statement about this island.

UNIFIED RANKING. The reference promoted from two lists with two quotas — N sea
trunks, N endorheic giants — which encodes the assumption that reaching the sea
is what makes a drainage a river. This terrain does not satisfy it: 54-78% of
land drains inland across the gallery, 106-121 terminal basins per seed. So
selection is unified — every major drainage ranked by contributing-cell count in
ONE list, top N promoted, and the sea/endorheic split FALLS OUT. A quota would
have promoted small coastal drainages over far larger inland ones purely because
of where they end. Deliberate departure from the reference's structure (D-050
noted, approved in chat). Only SELECTION is unified: the per-terminus tag is
retained per river because rivers/03's routing branches on it, and Trunk/Giant
are untouched.

  The metric is the same unit on both sides, and that is now ASSERTED rather
  than argued: sea Acc and endorheic BasinInflow are both counts of contributing
  land cells on the same D8 field, and every land cell has exactly one
  destination, so Σ sea Acc + Σ BasinInflow + unrouted == LandCells exactly. The
  tool refuses to rank unless it holds, per seed. It held on all 8.

DrainageAnalysis.cs is NOT modified. The complete candidate set is re-derived
from state it already exposes (Dir/Acc/BasinId/BasinInflow/FullFilled); the
three internal per-basin values that are not exposed are exactly reconstructible
because terminal basins are reverted to the real surface while FullFilled keeps
the fill. Only REPORTING caps were raised, and only so the analysis's own
TraceStem produces a real upland course per promotable candidate.
EndorheicMinDepthM/MinAreaPx were left alone — they decide which depressions
BECOME terminal basins, i.e. they define the routing surface itself, and a count
chosen on a moved surface would be a count for terrain that does not exist.

TWO FINDINGS.

  1. THE TERRAIN HAS NO NATURAL COUNT. The knee (largest ratio between
     consecutive ranks) lands at rank 19/7/15/5/4/12/3/8 across the eight
     gallery seeds at ratios of 1.33-1.57x, with one uncorroborated 2.37x. A
     break that wanders across the whole plausible range at that strength is not
     a break — on a log axis the candidates fall on a near-straight line.
     N is a DESIGN choice, and the diagnostic says so instead of inventing one.

  2. THE ISLAND'S MAJOR RIVERS ARE INLAND RIVERS. Six of eight seeds have ZERO
     sea-reaching drainages in their top 8; the median at N=16 is one; one seed
     has none at 16. On the primary seed the largest sea drainage ranks 11th at
     736,088 px against 2,329,573 px for the largest endorheic — 3.2x. (That
     outlet is exactly batch 12's recorded top trunk, to the cell.)

A concern I raised and then CLOSED by measuring: MinOutletSeparationPx is a
plain Euclidean test with no notion of which landmass a coastline belongs to, so
on a fragmented archipelago it could suppress an island's only river. Measured:
0-6 cross-landmass suppressions per seed, and — decisively — the largest
suppressed above-floor outlet anywhere is 209,611 px against a smallest ladder
cutoff of 439,592 px, so ZERO suppressed outlets clear any rung on any seed. The
rule provably cannot have altered the ladder. Real but harmless; flagged for
rivers/03, not a blocker. The rule was NOT changed — it belongs to the analysis,
and moving it would move the candidate set the developer is being asked to judge.

Also found: the params are ABSOLUTE pixel counts, so this analysis is only valid
at the size they were tuned for. At 1024 nothing qualifies as endorheic (the
whole endorheic half cannot be exercised) and separation suppresses 15,043 of
15,048 outlets. The tool now refuses loudly to have a non-8192 run read as a
count decision. Flagged for rivers/03: these want to become scale-free fractions
as MinLandComponentFrac already is.

Giant.ProvisionalRoute is never rendered — the "comb" is rivers/03's to replace,
and drawing it would make a count judgment look like a finished network. The
plates draw the real erosion-carved upland stems. Endorheic rivers are marked at
where their stem POOLS, not at the basin's deepest cell: the analysis
distinguishes these deliberately and they sit up to 395 px apart, which drew
every stem detached from its own endpoint until it was fixed.

TASTE GATE: 8/12/16 are presented and nothing is decided. No count is chosen, no
default is set, and neither TerrainGenConfig nor DrainageAnalysis.Params changed.

-> XX_Human/output/rivers/02_promotion.report.md
-> batches/rivers/02_promotion/INDEX.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhkXBQh2tDmcWKpXYcj8vj
2026-08-23 06:13:03 -04:00

104 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐⭐ ONE CANDIDATE MAJOR DRAINAGE — the unit the river count is chosen over (rivers/02).
///
/// ═══ WHY THIS TYPE EXISTS: ONE LIST, NOT TWO ═══
///
/// The reference promoted rivers from TWO separate lists with TWO separate quotas — N sea-reaching
/// trunks and N endorheic giants (<c>DrainageAnalysis.Params.TrunkCount</c> / <c>GiantCount</c>,
/// both 3). That structure encodes an assumption this terrain does not satisfy: that reaching the
/// sea is what makes a drainage a river, and inland ones are a second category to be quota'd
/// separately.
///
/// **On the reshaped terrain ~68 % of land drains INLAND** (measured: 67.6 % on the primary seed,
/// 116 terminal basins). A separate quota would fight that — it would promote small coastal
/// drainages over far larger inland ones purely because of where they end.
///
/// > ### So selection is UNIFIED: rank every major drainage by contributing area, promote the top N,
/// > and let the sea-vs-endorheic split FALL OUT of which promoted rivers happen to reach the ocean.
/// > **An endorheic terminus is a PASS, not a fallback** — a river ending in a significant lake is
/// > as real as one reaching the coast, and is never forced to the coast.
///
/// ⚠ This is a DELIBERATE DEPARTURE from the reference's two-list structure (approved in chat;
/// D-050 port-discipline noted). Only the SELECTION is unified — <see cref="IsSea"/> is retained
/// per river because downstream routing branches on it, and <c>Trunk</c> / <c>Giant</c> are left
/// exactly as ported.
///
/// ═══ ⚠ THE METRIC IS THE SAME UNIT ON BOTH SIDES, AND THAT IS LOAD-BEARING ═══
///
/// <see cref="DrainagePx"/> is a COUNT OF CONTRIBUTING LAND CELLS in both cases, computed on the
/// same D8 field in the same pass:
///
/// SEA <c>Plan.Acc</c> at the outlet — every non-ocean cell is seeded 1 and accumulated
/// along <c>Plan.Dir</c>, so the outlet's value is the count of cells whose flow path
/// passes through it.
/// ENDORHEIC <c>Plan.BasinInflow[BasinId]</c> — the memoised downstream walk over the SAME
/// <c>Dir</c>, counting cells whose flow TERMINATES in that basin.
///
/// Every land cell has exactly one destination, so the two populations are disjoint and exhaustive:
/// <c>Σ sea-outlet Acc + Σ BasinInflow + UnroutedCells == LandCells</c>. `RiverPromotionTool`
/// ASSERTS that identity per seed — it is the mechanical proof that one ranking over both is sound.
/// </summary>
public sealed class RiverCandidate
{
/// <summary>⭐ The terminus. True = the outlet touches <c>RegionLabeling.OceanMask</c>; false = it pools in a terminal basin. Never a bare <c>h &lt; sea</c> test.</summary>
public bool IsSea;
/// <summary>Row-major cell: the sea outlet, or the terminal basin's MINIMUM (its deepest cell).</summary>
public int Cell;
public int X, Y;
/// <summary>
/// ⭐ Where the RIVER actually ends — the point its main stem pools at, i.e. the first point of
/// <see cref="Course"/>. Defaults to <see cref="X"/>/<see cref="Y"/> until bound.
///
/// ⚠⚠ FOR AN ENDORHEIC RIVER THIS IS NOT THE BASIN'S DEEPEST CELL, and the difference is
/// visible on a map. `DrainageAnalysis` is explicit about why: *"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."* On a wide flat lagoon bed those two points can sit far apart.
///
/// Both are real and both are kept: the basin minimum is the BASIN's identity (and is what the
/// CSV records), this is the RIVER's terminus (and is what the plates mark). Marking a river at
/// its basin's deepest cell draws the stem visibly detached from its own endpoint — which reads
/// as a broken river and would corrupt a count judgment.
/// </summary>
public int TermX, TermY;
/// <summary>⭐ THE RANKING METRIC — contributing land cells. Same unit for both termini (see the class note).</summary>
public long DrainagePx;
/// <summary>Terminal-basin id (endorheic only; 0 for sea). The stable key for binding a candidate to its <c>Giant</c>.</summary>
public int BasinId;
/// <summary>Endorheic only: the basin's max fill depth, metres.</summary>
public float BasinDepthM;
/// <summary>Endorheic only: the basin's area in cells.</summary>
public long BasinAreaPx;
/// <summary>
/// ⚠ Sea only. True when this outlet was DROPPED by the <c>MinOutletSeparationPx</c> rule
/// because a larger outlet sits within that radius. Kept in the distribution (it is a real
/// drainage) but excluded from ranking — see the tool's note on what separation discards.
/// </summary>
public bool SuppressedBySeparation;
/// <summary>
/// The REAL upland stem — the max-accumulation traced course from <c>DrainageAnalysis</c>'s own
/// <c>TraceStem</c>, bound after selection. Null for candidates outside the promoted set.
/// ⚠ Downstream-first and decimated ×4, as the analysis produces it.
/// ⚠⚠ This is the erosion-carved course, NOT <c>Giant.ProvisionalRoute</c> — the steepest-descent
/// placeholder ("the comb") is deliberately never drawn here; replacing it is rivers/03's job,
/// and drawing it would mislead a count judgment.
/// </summary>
public List<(float x, float y)> Course;
/// <summary>1-based rank in the unified descending ranking. 0 until ranked.</summary>
public int Rank;
public string TerminusName => IsSea ? "sea" : "endorheic";
}
}