Skip to content

Commit 12910cf

Browse files
committed
Keep cheaper root distances and traversal when adding destinations
1 parent 2501da3 commit 12910cf

3 files changed

Lines changed: 45 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version
88

99
### Fixed
1010

11-
- Adding a root to an already resolved `tcod.path.Pathfinder` no longer leaves a stale predecessor which can cause cyclic paths after resolving again.
11+
- Adding a root to an already resolved `tcod.path.Pathfinder` resets its predecessor only when its value improves the current distance. This prevents cyclic paths while preserving routes to cheaper roots.
1212

1313
## [21.2.1] - 2026-06-04
1414

‎tcod/path.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,13 +1212,16 @@ def add_root(self, index: tuple[int, ...], value: int = 0) -> None:
12121212
12131213
`value` is the distance to use for this root. Zero is typical, but
12141214
if multiple roots are added they can be given different weights.
1215+
The root is ignored if `value` is not less than its current distance.
12151216
"""
12161217
index = tuple(index) # Check for bad input.
12171218
if self._order == "F": # Convert to ij indexing order.
12181219
index = index[::-1]
12191220
if len(index) != self._distance.ndim:
12201221
msg = f"Index must be {self._distance.ndim} items, got {index!r}"
12211222
raise TypeError(msg)
1223+
if value >= self._distance[index]:
1224+
return
12221225
self._distance[index] = value
12231226
self._travel[index] = index
12241227
self._update_heuristic(None)

‎tests/test_path.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,44 @@ def test_add_root_after_resolve(shape: tuple[int, ...], order: str, value: int)
4040
assert root in (origin, new_root)
4141
root_value = value if root == new_root else 0
4242
assert len(path) - 1 + root_value == expected[index]
43+
44+
45+
@pytest.mark.parametrize("shape", [(4, 7), (3, 4, 7)])
46+
@pytest.mark.parametrize("order", ["C", "F"])
47+
@pytest.mark.parametrize("first_value", [-5, 3])
48+
@pytest.mark.parametrize("relative_value", [-1, 0, 1, 20])
49+
@pytest.mark.parametrize("partial", [False, True])
50+
def test_add_root_preserves_better_distances(
51+
shape: tuple[int, ...], order: str, first_value: int, relative_value: int, partial: bool
52+
) -> None:
53+
graph = tcod.path.CustomGraph(shape, order=order)
54+
cost = np.ones(shape, dtype=np.int8)
55+
for axis in range(len(shape)):
56+
for direction in (-1, 1):
57+
edge = [0] * len(shape)
58+
edge[axis] = direction
59+
graph.add_edge(tuple(edge), cost=cost)
60+
pathfinder = tcod.path.Pathfinder(graph)
61+
origin = (0,) * len(shape)
62+
new_root = tuple(size // 2 for size in shape)
63+
pathfinder.add_root(origin, value=first_value)
64+
pathfinder.resolve(new_root if partial else None)
65+
old_distances = pathfinder.distance.copy()
66+
old_traversal = pathfinder.traversal.copy()
67+
old_path = pathfinder.path_from(new_root).copy()
68+
value = int(old_distances[new_root]) + relative_value
69+
pathfinder.add_root(new_root, value=value)
70+
if relative_value >= 0:
71+
np.testing.assert_array_equal(pathfinder.distance, old_distances)
72+
np.testing.assert_array_equal(pathfinder.traversal, old_traversal)
73+
np.testing.assert_array_equal(pathfinder.path_from(new_root), old_path)
74+
else:
75+
assert pathfinder.distance[new_root] == value
76+
np.testing.assert_array_equal(pathfinder.path_from(new_root), [new_root])
77+
pathfinder.resolve()
78+
coordinates = np.indices(shape)
79+
expected = np.minimum(
80+
first_value + coordinates.sum(axis=0),
81+
value + sum(abs(new_root[axis] - coordinates[axis]) for axis in range(len(shape))),
82+
)
83+
np.testing.assert_array_equal(pathfinder.distance, expected)

0 commit comments

Comments
 (0)