Add extended-link tracking
This commit is contained in:
parent
0458c9e308
commit
25a73aee60
2 changed files with 90 additions and 40 deletions
|
|
@ -165,6 +165,12 @@ class MapNode:
|
||||||
self.weights = {}
|
self.weights = {}
|
||||||
# lowest direction to a given neighbor
|
# lowest direction to a given neighbor
|
||||||
self.cheapest_to_node = {}
|
self.cheapest_to_node = {}
|
||||||
|
# maps the directions (on the xygrid NOT on XYgrid!) taken if stepping
|
||||||
|
# out from this node in a given direction until you get to the end node.
|
||||||
|
# This catches eventual longer link chains that would otherwise be lost
|
||||||
|
# {startdirection: [direction, ...], ...}
|
||||||
|
# where the directional path-lists also include the start-direction
|
||||||
|
self.xy_steps_in_direction = {}
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"<MapNode {self.node_index} XY=({self.X},{self.Y}) ({self.symbol})>"
|
return f"<MapNode {self.node_index} XY=({self.X},{self.Y}) ({self.symbol})>"
|
||||||
|
|
@ -194,13 +200,16 @@ class MapNode:
|
||||||
# just because there is a link here, doesn't mean it's
|
# just because there is a link here, doesn't mean it's
|
||||||
# connected to this node. If so the `end_node` will be None.
|
# connected to this node. If so the `end_node` will be None.
|
||||||
|
|
||||||
end_node, weight = link.traverse(_REVERSE_DIRECTIONS[direction], xygrid)
|
end_node, weight, steps = link.traverse(_REVERSE_DIRECTIONS[direction], xygrid)
|
||||||
if end_node:
|
if end_node:
|
||||||
# the link could be followed to an end node!
|
# the link could be followed to an end node!
|
||||||
node_index = end_node.node_index
|
node_index = end_node.node_index
|
||||||
self.links[direction] = end_node
|
self.links[direction] = end_node
|
||||||
self.weights[node_index] = weight
|
self.weights[node_index] = weight
|
||||||
|
|
||||||
|
# this is useful for map building later
|
||||||
|
self.xy_steps_in_direction[direction] = steps
|
||||||
|
|
||||||
cheapest = self.cheapest_to_node.get(node_index, ("", _BIG))[1]
|
cheapest = self.cheapest_to_node.get(node_index, ("", _BIG))[1]
|
||||||
if weight < cheapest:
|
if weight < cheapest:
|
||||||
self.cheapest_to_node[node_index] = (direction, weight)
|
self.cheapest_to_node[node_index] = (direction, weight)
|
||||||
|
|
@ -352,9 +361,9 @@ class MapLink:
|
||||||
"""
|
"""
|
||||||
return self.weights
|
return self.weights
|
||||||
|
|
||||||
def traverse(self, start_direction, xygrid, _weight=0, _linklen=1):
|
def traverse(self, start_direction, xygrid, _weight=0, _linklen=1, _steps=None):
|
||||||
"""
|
"""
|
||||||
Recursively traverse a set of links.
|
Recursively traverse the links out of this Linknode..
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
start_direction (str): The direction (n, ne etc) from which
|
start_direction (str): The direction (n, ne etc) from which
|
||||||
|
|
@ -363,9 +372,12 @@ class MapLink:
|
||||||
Kwargs:
|
Kwargs:
|
||||||
_weight (int): Internal use.
|
_weight (int): Internal use.
|
||||||
_linklen (int): Internal use.
|
_linklen (int): Internal use.
|
||||||
|
_steps (list): Internal use.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: The (node, weight) result of the traversal.
|
tuple: The (node, weight, links) result of the traversal, where links
|
||||||
|
is a list of directions (n, ne etc) that describes how to to get
|
||||||
|
to the node on the grid. This includes the first direction.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
MapParserError: If a link lead to nowhere.
|
MapParserError: If a link lead to nowhere.
|
||||||
|
|
@ -388,17 +400,24 @@ class MapLink:
|
||||||
_weight += self.get_weights(
|
_weight += self.get_weights(
|
||||||
start_direction, xygrid, _weight).get(
|
start_direction, xygrid, _weight).get(
|
||||||
start_direction, self.default_weight)
|
start_direction, self.default_weight)
|
||||||
|
if _steps is None:
|
||||||
|
_steps = []
|
||||||
|
_steps.append(_REVERSE_DIRECTIONS[start_direction])
|
||||||
|
|
||||||
if hasattr(next_target, "node_index"):
|
if hasattr(next_target, "node_index"):
|
||||||
# we reached a node, this is the end of the link.
|
# we reached a node, this is the end of the link.
|
||||||
# we average the weight across all traversed link segments.
|
# we average the weight across all traversed link segments.
|
||||||
return next_target, (
|
return (
|
||||||
_weight / max(1, _linklen) if self.average_long_link_weights else _weight)
|
next_target,
|
||||||
|
_weight / max(1, _linklen) if self.average_long_link_weights else _weight,
|
||||||
|
_steps
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# we hit another link. Progress recursively.
|
# we hit another link. Progress recursively.
|
||||||
return next_target.traverse(
|
return next_target.traverse(
|
||||||
_REVERSE_DIRECTIONS[end_direction],
|
_REVERSE_DIRECTIONS[end_direction],
|
||||||
xygrid, _weight=_weight, _linklen=_linklen + 1)
|
xygrid, _weight=_weight, _linklen=_linklen + 1, _steps=_steps)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
|
|
@ -624,28 +643,6 @@ class Map:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Map {self.max_X}x{self.max_Y}, {len(self.node_index_map)} nodes>"
|
return f"<Map {self.max_X}x{self.max_Y}, {len(self.node_index_map)} nodes>"
|
||||||
|
|
||||||
def _get_node_from_coord(self, X, Y):
|
|
||||||
"""
|
|
||||||
Get a MapNode from a coordinate.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
X (int): X-coordinate on XY (game) grid.
|
|
||||||
Y (int): Y-coordinate on XY (game) grid.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MapNode: The node found at the given coordinates.
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not self.XYgrid:
|
|
||||||
self.parse()
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self.XYgrid[X][Y]
|
|
||||||
except IndexError:
|
|
||||||
raise MapError("_get_node_from_coord got coordinate ({x},{y}) which is "
|
|
||||||
"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).")
|
|
||||||
|
|
||||||
def _calculate_path_matrix(self):
|
def _calculate_path_matrix(self):
|
||||||
"""
|
"""
|
||||||
Solve the pathfinding problem using Dijkstra's algorithm.
|
Solve the pathfinding problem using Dijkstra's algorithm.
|
||||||
|
|
@ -834,6 +831,28 @@ class Map:
|
||||||
# process the new(?) data
|
# process the new(?) data
|
||||||
self._parse()
|
self._parse()
|
||||||
|
|
||||||
|
def get_node_from_coord(self, X, Y):
|
||||||
|
"""
|
||||||
|
Get a MapNode from a coordinate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
X (int): X-coordinate on XY (game) grid.
|
||||||
|
Y (int): Y-coordinate on XY (game) grid.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MapNode: The node found at the given coordinates.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.XYgrid:
|
||||||
|
self.parse()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.XYgrid[X][Y]
|
||||||
|
except IndexError:
|
||||||
|
raise MapError("get_node_from_coord got coordinate ({x},{y}) which is "
|
||||||
|
"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).")
|
||||||
|
|
||||||
def get_shortest_path(self, startcoord, endcoord):
|
def get_shortest_path(self, startcoord, endcoord):
|
||||||
"""
|
"""
|
||||||
Get the shortest route between two points on the grid.
|
Get the shortest route between two points on the grid.
|
||||||
|
|
@ -850,8 +869,8 @@ class Map:
|
||||||
the full path including the start- and end-node.
|
the full path including the start- and end-node.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
startnode = self._get_node_from_coord(*startcoord)
|
startnode = self.get_node_from_coord(*startcoord)
|
||||||
endnode = self._get_node_from_coord(*endcoord)
|
endnode = self.get_node_from_coord(*endcoord)
|
||||||
|
|
||||||
if not self.pathfinding_routes:
|
if not self.pathfinding_routes:
|
||||||
self._calculate_path_matrix()
|
self._calculate_path_matrix()
|
||||||
|
|
@ -955,7 +974,7 @@ class Map:
|
||||||
|
|
||||||
node_index_map = self.node_index_map
|
node_index_map = self.node_index_map
|
||||||
|
|
||||||
center_node = self._get_node_from_coord(iX, iY)
|
center_node = self.get_node_from_coord(iX, iY)
|
||||||
# find all reachable nodes within a (weighted) distance of `dist`
|
# find all reachable nodes within a (weighted) distance of `dist`
|
||||||
for inode, node_dist in enumerate(self.dist_matrix[center_node.node_index]):
|
for inode, node_dist in enumerate(self.dist_matrix[center_node.node_index]):
|
||||||
if node_dist > dist:
|
if node_dist > dist:
|
||||||
|
|
@ -963,14 +982,25 @@ class Map:
|
||||||
# we have a node within 'dist' from us, get, the route to it
|
# we have a node within 'dist' from us, get, the route to it
|
||||||
node = node_index_map[inode]
|
node = node_index_map[inode]
|
||||||
_, path = self.get_shortest_path(node.iX, node.iY)
|
_, path = self.get_shortest_path(node.iX, node.iY)
|
||||||
|
|
||||||
# follow directions to figure out which map coords to display
|
# follow directions to figure out which map coords to display
|
||||||
|
node0 = node
|
||||||
ix0, iy0 = ix, iy
|
ix0, iy0 = ix, iy
|
||||||
# for path_element in path:
|
for path_element in path:
|
||||||
# dx, dy = _MAPSCAN[direction]
|
if isinstance(path_element, str):
|
||||||
# ix0, iy0 = ix0 + dx, iy0 + dy
|
# a direction - this can lead to following
|
||||||
# xmax, ymax = max(xmax, ix0), max(ymax, iy0)
|
# a longer link-chain chain
|
||||||
# points.append((ix0, iy0))
|
for dstep in node0.xy_steps_in_direction[path_element]:
|
||||||
|
dx, dy = _MAPSCAN[dstep]
|
||||||
|
ix0, iy0 = ix0 + dx, iy0 + dy
|
||||||
|
xmax, ymax = max(xmax, ix0), max(ymax, iy0)
|
||||||
|
points.append((ix0, iy0))
|
||||||
|
else:
|
||||||
|
# a Mapnode
|
||||||
|
node0 = path_element
|
||||||
|
ix0, iy0 = node0.ix, node0.iy
|
||||||
|
points.append((ix0, iy0))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# dist measures individual grid points
|
# dist measures individual grid points
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ class TestMap1(TestCase):
|
||||||
self.assertEqual(str(self.map).strip(), MAP1_DISPLAY)
|
self.assertEqual(str(self.map).strip(), MAP1_DISPLAY)
|
||||||
|
|
||||||
def test_node_from_coord(self):
|
def test_node_from_coord(self):
|
||||||
node = self.map._get_node_from_coord(1, 1)
|
node = self.map.get_node_from_coord(1, 1)
|
||||||
self.assertEqual(node.X, 1)
|
self.assertEqual(node.X, 1)
|
||||||
self.assertEqual(node.x, 2)
|
self.assertEqual(node.x, 2)
|
||||||
self.assertEqual(node.X, 1)
|
self.assertEqual(node.X, 1)
|
||||||
|
|
@ -148,7 +148,7 @@ class TestMap2(TestCase):
|
||||||
|
|
||||||
def test_node_from_coord(self):
|
def test_node_from_coord(self):
|
||||||
for mapnode in self.map.node_index_map.values():
|
for mapnode in self.map.node_index_map.values():
|
||||||
node = self.map._get_node_from_coord(mapnode.X, mapnode.Y)
|
node = self.map.get_node_from_coord(mapnode.X, mapnode.Y)
|
||||||
self.assertEqual(node, mapnode)
|
self.assertEqual(node, mapnode)
|
||||||
self.assertEqual(node.x // 2, node.X)
|
self.assertEqual(node.x // 2, node.X)
|
||||||
self.assertEqual(node.y // 2, node.Y)
|
self.assertEqual(node.y // 2, node.Y)
|
||||||
|
|
@ -184,3 +184,23 @@ class TestMap2(TestCase):
|
||||||
"""
|
"""
|
||||||
mapstr = self.map.get_map_display(coord, dist=4, character='@')
|
mapstr = self.map.get_map_display(coord, dist=4, character='@')
|
||||||
self.assertEqual(expected, mapstr)
|
self.assertEqual(expected, mapstr)
|
||||||
|
|
||||||
|
def test_extended_path_tracking__horizontal(self):
|
||||||
|
node = self.map.get_node_from_coord(4, 1)
|
||||||
|
self.assertEqual(
|
||||||
|
node.xy_steps_in_direction,
|
||||||
|
{'e': ['e'],
|
||||||
|
's': ['s'],
|
||||||
|
'w': ['w', 'w', 'w']}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_extended_path_tracking__vertical(self):
|
||||||
|
node = self.map.get_node_from_coord(2, 2)
|
||||||
|
self.assertEqual(
|
||||||
|
node.xy_steps_in_direction,
|
||||||
|
{'n': ['n', 'n', 'n'],
|
||||||
|
'e': ['e'],
|
||||||
|
's': ['s'],
|
||||||
|
'w': ['w']}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue