diff --git a/evennia/contrib/map_and_pathfind/mapsystem.py b/evennia/contrib/map_and_pathfind/mapsystem.py index 576514d8e8..5ad60640f5 100644 --- a/evennia/contrib/map_and_pathfind/mapsystem.py +++ b/evennia/contrib/map_and_pathfind/mapsystem.py @@ -170,7 +170,7 @@ class MapNode: # 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 = {} + self.xy_steps_to_node = {} def __str__(self): return f"=1, this indicates how 'slow' it is to traverse that link. This is used by the Dijkstra algorithm to find the 'fastest' route to a point. By default this weight is 1 for every link, but a locked door, terrain etc could increase this - and have the algorithm prefer to use another route. - - It is usually bidirectional, but could also be one-directional. - It is also possible for a link to have some sort of blockage, like - a door. + and have the shortest-path algorithm prefer to use another route. """ # link setup @@ -312,7 +311,7 @@ class MapLink: def get_visually_connected(self, xygrid, directions=None): """ A helper to get all directions to which there appears to be a - visual link/node. This does not trace the link and check weights etc. + visual link/node. This does not trace the length of the link and check weights etc. Args: link (MapLink): Currently active link. @@ -620,7 +619,7 @@ class Map: """ mapcorner_symbol = '+' - max_pathfinding_length = 1000 + max_pathfinding_length = 500 empty_symbol = ' ' # we normally only accept one single character for the legend key legend_key_exceptions = ("\\") @@ -675,7 +674,57 @@ class Map: return "\n".join("".join(line) for line in self.display_map[::-1]) def __repr__(self): - return f"" + return f"" + + def _get_topology_around_coord(self, coord, dist=2): + """ + Get all links and nodes up to a certain distance from an XY coordinate. + + Args: + coord (tuple), the X,Y coordinate of the center point. + dist (int): How many nodes away from center point to find paths for. + + Returns: + tuple: A tuple of 5 elements `(coords, xmin, xmax, ymin, ymax)`, where the + first element is a list of xy-coordinates (on xygrid) for all linked nodes within + range. This is meant to be used with the xygrid for extracting a subset + for display purposes. The others are the minimum size of the rectangle + surrounding the area containing `coords`. + + Notes: + This performs a depth-first pass down the the given dist. + + """ + def _scan_neighbors(start_node, points, dist=2, + xmin=_BIG, ymin=_BIG, xmax=0, ymax=0, depth=0): + + x0, y0 = start_node.x, start_node.y + points.append((x0, y0)) + xmin, xmax = min(xmin, x0), max(xmax, x0) + ymin, ymax = min(ymin, y0), max(ymax, y0) + + if depth < dist: + # keep stepping + for direction, end_node in start_node.links.items(): + x, y = x0, y0 + for stepdirection in start_node.xy_steps_to_node[direction]: + dx, dy = _MAPSCAN[stepdirection] + + x, y = x + dx, y + dy + points.append((x, y)) + xmin, xmax = min(xmin, x), max(xmax, x) + ymin, ymax = min(ymin, y), max(ymax, y) + + points, xmin, xmax, ymin, ymax = _scan_neighbors( + end_node, points, dist=dist, + xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax, + depth=depth + 1) + + return points, xmin, xmax, ymin, ymax + + center_node = self.get_node_from_coord(coord) + points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist) + return list(set(points)), xmin, xmax, ymin, ymax def _calculate_path_matrix(self): """ @@ -695,7 +744,7 @@ class Map: # solve using Dijkstra's algorithm self.dist_matrix, self.pathfinding_routes = dijkstra( pathfinding_matrix, directed=True, - return_predecessors=True, limit=1000) + return_predecessors=True, limit=self.max_pathfinding_length) def _parse(self): """ @@ -958,14 +1007,17 @@ class Map: Display the map centered on a point and everything around it within a certain distance. Args: - coord (tuple): (X,Y) in-world coordinate location. + coord (tuple): (X,Y) in-world coordinate location. If this is not the location + of a node on the grid, the `character` or the empty-space symbol (by default + an empty space) will be shown. dist (int, optional): Number of gridpoints distance to show. Which grid to use depends on the setting of `only_nodes`. mode (str, optional): One of 'scan' or 'nodes'. In 'scan' mode, dist measure - number of xy grid points in all directions. If 'nodes', distance - measure how many full nodes away to display. + number of xy grid points in all directions and doesn't care about if visible + nodes are reachable or not. If 'nodes', distance measure how many linked nodes + away from the center coordinate to display. character (str, optional): Place this symbol at the `coord` position - of the displayed map. Ignored if falsy. + of the displayed map. The center node' symbol is shown if this is falsy. max_size (tuple, optional): A max `(width, height)` to crop the displayed return to. Make both odd numbers to get a perfect center. If unset, display-size can grow up to the full size of the grid. @@ -1013,63 +1065,18 @@ class Map: ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height)) display_map = self.display_map - if dist <= 0: - # show nothing but ourselves - return character if character else ' ' + if dist <= 0 or not self.get_node_from_coord(coord): + # There is no node at these coordinates. Show + # nothing but ourselves or emptiness + return character if character else self.empty_symbol if mode == 'nodes': # dist measures only full, reachable nodes. # this requires a series of shortest-path # Steps from on the pre-calulcated grid. + # from evennia import set_trace;set_trace() - if not self.dist_matrix: - self._calculate_path_matrix() - - xmin, ymin = width, height - xmax, ymax = 0, 0 - # adjusted center of map section - ixc, iyc = ix, iy - - center_node = self.get_node_from_coord((iX, iY)) - if not center_node: - # there is nothing at this grid location - return character if character else ' ' - - # the points list coordinates on the xygrid to show. - points = [(ix, iy)] - node_index_map = self.node_index_map - - # find all reachable nodes within a (weighted) distance of `dist` - for inode, node_dist in enumerate(self.dist_matrix[center_node.node_index]): - - if node_dist > dist: - continue - - # we have a node within 'dist' from us, get, the route to it - node = node_index_map[inode] - _, path = self.get_shortest_path((iX, iY), (node.X, node.Y)) - # follow directions to figure out which map coords to display - node0 = node - ix0, iy0 = ix, iy - for path_element in path: - # we don't need the start node since we know it already - if isinstance(path_element, str): - # a direction - this can lead to following - # a longer link-chain chain - for dstep in node0.xy_steps_in_direction[path_element]: - dx, dy = _MAPSCAN[dstep] - ix0, iy0 = ix0 + dx, iy0 + dy - points.append((ix0, iy0)) - xmin, ymin = min(xmin, ix0), min(ymin, iy0) - xmax, ymax = max(xmax, ix0), max(ymax, iy0) - else: - # a Mapnode - node0 = path_element - ix0, iy0 = node0.x, node0.y - if (ix0, iy0) != (ix, iy): - points.append((ix0, iy0)) - xmin, ymin = min(xmin, ix0), min(ymin, iy0) - xmax, ymax = max(xmax, ix0), max(ymax, iy0) + points, xmin, xmax, ymin, ymax = self._get_topology_around_coord(coord, dist=dist) ixc, iyc = ix - xmin, iy - ymin # note - override width/height here since our grid is @@ -1079,6 +1086,63 @@ class Map: for (ix0, iy0) in points: gridmap[iy0 - ymin][ix0 - xmin] = display_map[iy0][ix0] +# if not self.dist_matrix: +# self._calculate_path_matrix() +# +# xmin, ymin = width, height +# xmax, ymax = 0, 0 +# # adjusted center of map section +# ixc, iyc = ix, iy +# +# center_node = self.get_node_from_coord((iX, iY)) +# if not center_node: +# # there is nothing at this grid location +# return character if character else ' ' +# +# # the points list coordinates on the xygrid to show. +# points = [(ix, iy)] +# node_index_map = self.node_index_map +# +# # find all reachable nodes within a (weighted) distance of `dist` +# for inode, node_dist in enumerate(self.dist_matrix[center_node.node_index]): +# +# if node_dist > dist: +# continue +# +# # we have a node within 'dist' from us, get, the route to it +# node = node_index_map[inode] +# _, path = self.get_shortest_path((iX, iY), (node.X, node.Y)) +# # follow directions to figure out which map coords to display +# node0 = node +# ix0, iy0 = ix, iy +# for path_element in path: +# # we don't need the start node since we know it already +# if isinstance(path_element, str): +# # a direction - this can lead to following +# # a longer link-chain chain +# for dstep in node0.xy_steps_to_noden[path_element]: +# dx, dy = _MAPSCAN[dstep] +# ix0, iy0 = ix0 + dx, iy0 + dy +# points.append((ix0, iy0)) +# xmin, ymin = min(xmin, ix0), min(ymin, iy0) +# xmax, ymax = max(xmax, ix0), max(ymax, iy0) +# else: +# # a Mapnode +# node0 = path_element +# ix0, iy0 = node0.x, node0.y +# if (ix0, iy0) != (ix, iy): +# points.append((ix0, iy0)) +# xmin, ymin = min(xmin, ix0), min(ymin, iy0) +# xmax, ymax = max(xmax, ix0), max(ymax, iy0) +# +# ixc, iyc = ix - xmin, iy - ymin +# # note - override width/height here since our grid is +# # now different from the original for future cropping +# width, height = xmax - xmin + 1, ymax - ymin + 1 +# gridmap = [[" "] * width for _ in range(height)] +# for (ix0, iy0) in points: +# gridmap[iy0 - ymin][ix0 - xmin] = display_map[iy0][ix0] + else: # scan-mode (default) - dist measures individual grid points if dist is None: diff --git a/evennia/contrib/map_and_pathfind/tests.py b/evennia/contrib/map_and_pathfind/tests.py index 59239e22f4..98d1b53791 100644 --- a/evennia/contrib/map_and_pathfind/tests.py +++ b/evennia/contrib/map_and_pathfind/tests.py @@ -62,19 +62,7 @@ MAP2_DISPLAY = """ #-#-#-# """.strip() -MAP4 = r""" - - + 0 1 - - 1 #-# - |\| - 0 #-# - - + 0 1 - -""" - -MAP4 = r""" +MAP3 = r""" + 0 1 2 3 4 5 @@ -94,7 +82,7 @@ MAP4 = r""" """ -MAP4_DISPLAY = r""" +MAP3_DISPLAY = r""" #-#---# # | / \ / # / # @@ -108,7 +96,7 @@ MAP4_DISPLAY = r""" # #---#-# """.strip() -MAP5 = r""" +MAP4 = r""" + 0 1 2 3 4 @@ -126,7 +114,7 @@ MAP5 = r""" """ -MAP5_DISPLAY = r""" +MAP4_DISPLAY = r""" #-# #---# x / #-#-# @@ -138,7 +126,7 @@ MAP5_DISPLAY = r""" #---# """.strip() -MAP6 = r""" +MAP5 = r""" + 0 1 2 @@ -152,7 +140,7 @@ MAP6 = r""" """ -MAP6_DISPLAY = r""" +MAP5_DISPLAY = r""" #-# | | #># @@ -160,7 +148,7 @@ MAP6_DISPLAY = r""" #># """.strip() -MAP7 = r""" +MAP6 = r""" + 0 1 2 3 4 @@ -178,7 +166,7 @@ MAP7 = r""" """ -MAP7_DISPLAY = r""" +MAP6_DISPLAY = r""" #-#-#-# ^ | | #># @@ -191,7 +179,7 @@ MAP7_DISPLAY = r""" """.strip() -MAP8 = r""" +MAP7 = r""" + 0 1 2 2 #-# @@ -204,7 +192,7 @@ MAP8 = r""" """ -MAP8_DISPLAY = r""" +MAP7_DISPLAY = r""" #-# | #-o-# @@ -213,7 +201,7 @@ MAP8_DISPLAY = r""" """.strip() -MAP9 = r""" +MAP8 = r""" + 0 1 2 3 4 5 4 #-#-o o o-o @@ -230,7 +218,7 @@ MAP9 = r""" """ -MAP9_DISPLAY = r""" +MAP8_DISPLAY = r""" #-#-o o o-o | \|/| | | #-o-o-# o-# @@ -315,7 +303,7 @@ class TestMap1(TestCase): ((0, 1), 1, '@-#\n| \n# '), ((1, 0), 1, ' #\n |\n#-@'), ((1, 1), 1, '#-@\n |\n #'), - ((0, 0), 2, ''), + ((0, 0), 2, '#-#\n| |\n@-#'), ]) def test_get_map_display__nodes__character(self, coord, dist, expected): @@ -387,7 +375,7 @@ class TestMap2(TestCase): """ node = self.map.get_node_from_coord((4, 1)) self.assertEqual( - node.xy_steps_in_direction, + node.xy_steps_to_node, {'e': ['e'], 's': ['s'], 'w': ['w', 'w', 'w']} @@ -400,7 +388,7 @@ class TestMap2(TestCase): """ node = self.map.get_node_from_coord((2, 2)) self.assertEqual( - node.xy_steps_in_direction, + node.xy_steps_to_node, {'n': ['n', 'n', 'n'], 'e': ['e'], 's': ['s'], @@ -434,18 +422,18 @@ class TestMap2(TestCase): self.assertEqual(expected, mapstr) -class TestMap4(TestCase): +class TestMap3(TestCase): """ - Test Map4 - Map with diaginal links + Test Map3 - Map with diagonal links """ def setUp(self): - self.map = mapsystem.Map({"map": MAP4}) + self.map = mapsystem.Map({"map": MAP3}) def test_str_output(self): """Check the display_map""" stripped_map = "\n".join(line.rstrip() for line in str(self.map).split('\n')) - self.assertEqual(MAP4_DISPLAY, stripped_map) + self.assertEqual(MAP3_DISPLAY, stripped_map) @parameterized.expand([ ((0, 0), (1, 0), ()), # no node at (1, 0)! @@ -472,7 +460,7 @@ class TestMap4(TestCase): ((2, 2), 2, None, ' # \n / \n # / \n |/ \n # #\n \\ / ' '\n # @-# \n |/ \\ \n # #\n / \\ \n# # '), - ((5, 2), 2, None, '') + ((5, 2), 2, None, ' # \n | \n # \n / \\ \n# @\n \\ / \n # \n | \n # ') ]) def test_get_map_display__nodes__character(self, coord, dist, max_size, expected): """ @@ -481,21 +469,21 @@ class TestMap4(TestCase): """ mapstr = self.map.get_map_display(coord, dist=dist, mode='nodes', character='@', max_size=max_size) - # print(repr(mapstr)) + print(f"\n\n{expected}\n\n{mapstr}\n\n{repr(mapstr)}") self.assertEqual(expected, mapstr) -class TestMap5(TestCase): +class TestMap4(TestCase): """ - Test Map5 - Map with + and x crossing links + Test Map4 - Map with + and x crossing links """ def setUp(self): - self.map = mapsystem.Map({"map": MAP5}) + self.map = mapsystem.Map({"map": MAP4}) def test_str_output(self): """Check the display_map""" stripped_map = "\n".join(line.rstrip() for line in str(self.map).split('\n')) - self.assertEqual(MAP5_DISPLAY, stripped_map) + self.assertEqual(MAP4_DISPLAY, stripped_map) @parameterized.expand([ ((1, 0), (1, 2), ('n',)), # cross + vertically @@ -514,18 +502,18 @@ class TestMap5(TestCase): self.assertEqual(expected_directions, tuple(directions)) -class TestMap6(TestCase): +class TestMap5(TestCase): """ - Test Map6 - Small map with one-way links + Test Map5 - Small map with one-way links """ def setUp(self): - self.map = mapsystem.Map({"map": MAP6}) + self.map = mapsystem.Map({"map": MAP5}) def test_str_output(self): """Check the display_map""" stripped_map = "\n".join(line.rstrip() for line in str(self.map).split('\n')) - self.assertEqual(MAP6_DISPLAY, stripped_map) + self.assertEqual(MAP5_DISPLAY, stripped_map) @parameterized.expand([ ((0, 0), (1, 0), ('e',)), # cross one-way @@ -542,18 +530,18 @@ class TestMap6(TestCase): self.assertEqual(expected_directions, tuple(directions)) -class TestMap7(TestCase): +class TestMap6(TestCase): """ Test Map6 - Bigger map with one-way links in different directions """ def setUp(self): - self.map = mapsystem.Map({"map": MAP7}) + self.map = mapsystem.Map({"map": MAP6}) def test_str_output(self): """Check the display_map""" stripped_map = "\n".join(line.rstrip() for line in str(self.map).split('\n')) - self.assertEqual(MAP7_DISPLAY, stripped_map) + self.assertEqual(MAP6_DISPLAY, stripped_map) @parameterized.expand([ ((0, 0), (2, 0), ('e', 'e')), # cross one-way @@ -574,18 +562,18 @@ class TestMap7(TestCase): self.assertEqual(expected_directions, tuple(directions)) -class TestMap8(TestCase): +class TestMap7(TestCase): """ - Test Map6 - Small test of dynamic link node + Test Map7 - Small test of dynamic link node """ def setUp(self): - self.map = mapsystem.Map({"map": MAP8}) + self.map = mapsystem.Map({"map": MAP7}) def test_str_output(self): """Check the display_map""" stripped_map = "\n".join(line.rstrip() for line in str(self.map).split('\n')) - self.assertEqual(MAP8_DISPLAY, stripped_map) + self.assertEqual(MAP7_DISPLAY, stripped_map) @parameterized.expand([ ((1, 0), (1, 2), ('n', )), @@ -599,21 +587,21 @@ class TestMap8(TestCase): """ directions, _ = self.map.get_shortest_path(startcoord, endcoord) - self.assertequal(expected_directions, tuple(directions)) + self.assertEqual(expected_directions, tuple(directions)) -class TestMap9(TestCase): +class TestMap8(TestCase): """ - Test Map6 - Small test of dynamic link node + Test Map8 - Small test of dynamic link node """ def setUp(self): - self.map = mapsystem.Map({"map": MAP9}) + self.map = mapsystem.Map({"map": MAP8}) def test_str_output(self): """Check the display_map""" stripped_map = "\n".join(line.rstrip() for line in str(self.map).split('\n')) - self.assertEqual(MAP9_DISPLAY, stripped_map) + self.assertEqual(MAP8_DISPLAY, stripped_map) @parameterized.expand([ ((2, 0), (2, 2), ('n',)),