From 427647edaf84f4724f6972cf1fa4e6de7a013c80 Mon Sep 17 00:00:00 2001 From: countercheck Date: Wed, 13 May 2026 21:11:50 -0400 Subject: [PATCH 1/2] fix: tributaries now end at the confluence hex, not one hex before it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _split_at_confluences was using `cut = i` when trimming a lower-flow tributary, stopping the path at path[i-1] — the hex immediately before the confluence. Because that endpoint was not on the main trunk's polyline, tributaries appeared to peter out mid-map rather than flowing into the main river. Change to `cut = i + 1` so the confluence hex is included as the tributary's final point. It is already part of the trunk's path, so the two polylines share a vertex and visually connect. The shared hex is double-drawn in SVG but is invisible at normal stroke widths. Update test_no_shared_hexes_between_rivers to allow confluence hexes (tributary endpoints) to appear in multiple rivers, since that sharing is now intentional. Trunk duplication elsewhere is still caught. Co-Authored-By: Claude Sonnet 4.6 --- tests/test_hydrology.py | 18 ++++++++++++------ worldgen/stages/hydrology.py | 11 ++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/test_hydrology.py b/tests/test_hydrology.py index 649cbb4..452ec40 100644 --- a/tests/test_hydrology.py +++ b/tests/test_hydrology.py @@ -49,8 +49,8 @@ def test_river_paths_connected(hydro_state): def test_rivers_reach_ocean(hydro_state): # Each river must terminate at ocean/lake, a grid border, OR a confluence with - # another river (tributaries now stop at the first claimed hex rather than - # duplicating the downstream trunk). + # another river (tributaries end AT the confluence hex, which is also part of the + # higher-flow trunk, so the polylines visually connect). water_set = { coord for coord, h in hydro_state.hexes.items() @@ -232,8 +232,11 @@ def test_lake_drainage_merges_without_rewiring_existing_river(): def test_no_shared_hexes_between_rivers(hydro_state): - # Each land hex must appear in at most one River segment. Multiple rivers sharing - # the same hex meant the downstream trunk was duplicated, causing visual overdraw. + # Each land hex must appear in at most one River segment, EXCEPT confluence hexes. + # A confluence hex is the last hex of a tributary and simultaneously an interior + # hex of the higher-flow trunk — it is shared by design so the two polylines + # visually connect. Any shared hex that is NOT a tributary endpoint represents + # genuine trunk duplication and is a bug. from collections import defaultdict hex_to_rivers: dict[tuple[int, int], list[int]] = defaultdict(list) @@ -247,8 +250,11 @@ def test_no_shared_hexes_between_rivers(hydro_state): if coord in land_terrain: hex_to_rivers[coord].append(i) - shared = {k: v for k, v in hex_to_rivers.items() if len(v) > 1} + tributary_endpoints = { + river.hexes[-1] for river in hydro_state.rivers if river.hexes[-1] in land_terrain + } + shared = {k: v for k, v in hex_to_rivers.items() if len(v) > 1 and k not in tributary_endpoints} assert not shared, ( - f"{len(shared)} land hexes appear in multiple rivers; " + f"{len(shared)} land hexes appear in multiple rivers outside confluence endpoints; " f"first offender: {next(iter(shared))} in rivers {next(iter(shared.values()))}" ) diff --git a/worldgen/stages/hydrology.py b/worldgen/stages/hydrology.py index 9f883f2..19174ad 100644 --- a/worldgen/stages/hydrology.py +++ b/worldgen/stages/hydrology.py @@ -717,12 +717,13 @@ def _split_at_confluences( acc: dict[HexCoord, float], max_acc: float, ) -> list[River]: - """Trim each River path to a source-to-confluence segment. + """Trim each River path to end at the confluence hex. Higher-flow rivers process first and claim their land hexes. Each subsequent - river is cut at the first land hex already owned by a higher-flow river, converting - source-to-sea duplicates into distinct source-to-confluence segments. Rivers that - shrink below 2 hexes are dropped. Original list order is preserved in the output. + river is cut at the first land hex already owned by a higher-flow river; the + confluence hex itself is included as the tributary's endpoint so that it visually + connects to the trunk. Rivers that shrink below 2 hexes are dropped. Original + list order is preserved in the output. This runs after all hydrological computation is final so that per-hex river_flow, flow_dir, and lake drainage connectivity are unaffected. @@ -743,7 +744,7 @@ def _split_at_confluences( cut = len(path) for i, coord in enumerate(path[1:], 1): if coord in land and coord in claimed: - cut = i + cut = i + 1 # include confluence hex so tributary visually meets the trunk break trimmed = path[:cut] if len(trimmed) >= 2: From 97dca0640a5365468a9815ad602fcd53fcbc36f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 03:44:05 +0000 Subject: [PATCH 2/2] docs: clarify confluence trimming applies to intersecting tributaries Agent-Logs-Url: https://github.com/countercheck/worldgen/sessions/92667c92-f9d7-4814-8a3c-81a1e5fbd24d Co-authored-by: countercheck <4325443+countercheck@users.noreply.github.com> --- worldgen/stages/hydrology.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/worldgen/stages/hydrology.py b/worldgen/stages/hydrology.py index 19174ad..949a9ca 100644 --- a/worldgen/stages/hydrology.py +++ b/worldgen/stages/hydrology.py @@ -717,13 +717,14 @@ def _split_at_confluences( acc: dict[HexCoord, float], max_acc: float, ) -> list[River]: - """Trim each River path to end at the confluence hex. + """Trim intersecting tributaries to include their confluence hex. - Higher-flow rivers process first and claim their land hexes. Each subsequent - river is cut at the first land hex already owned by a higher-flow river; the - confluence hex itself is included as the tributary's endpoint so that it visually - connects to the trunk. Rivers that shrink below 2 hexes are dropped. Original - list order is preserved in the output. + Higher-flow rivers process first and claim their land hexes. A subsequent river + that intersects claimed land is cut at the first claimed land hex after its + headwater, and that confluence hex is kept so the tributary visually connects to + the trunk. Rivers that never intersect claimed land (typically highest-flow + trunks) keep their full source-to-mouth path. Rivers that shrink below 2 hexes + are dropped. Original list order is preserved in the output. This runs after all hydrological computation is final so that per-hex river_flow, flow_dir, and lake drainage connectivity are unaffected. @@ -744,7 +745,7 @@ def _split_at_confluences( cut = len(path) for i, coord in enumerate(path[1:], 1): if coord in land and coord in claimed: - cut = i + 1 # include confluence hex so tributary visually meets the trunk + cut = i + 1 # for an intersecting tributary, include the confluence hex break trimmed = path[:cut] if len(trimmed) >= 2: