Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions tests/test_hydrology.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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()))}"
)
14 changes: 8 additions & 6 deletions worldgen/stages/hydrology.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,12 +717,14 @@ def _split_at_confluences(
acc: dict[HexCoord, float],
max_acc: float,
) -> list[River]:
"""Trim each River path to a source-to-confluence segment.
"""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, 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.
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.
Expand All @@ -743,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
cut = i + 1 # for an intersecting tributary, include the confluence hex
break
trimmed = path[:cut]
if len(trimmed) >= 2:
Expand Down