Add extended-link tracking

This commit is contained in:
Griatch 2021-06-06 23:55:18 +02:00
parent 0458c9e308
commit 25a73aee60
2 changed files with 90 additions and 40 deletions

View file

@ -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

View file

@ -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']}
)