From 4be7e52c563375fce734d35b026e67ae6919f8e2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 5 Jun 2021 18:59:34 +0200 Subject: [PATCH] Tested first unittests with map and pathfinding --- evennia/contrib/map_and_pathfind/mapsystem.py | 112 +++++++++--------- evennia/contrib/map_and_pathfind/tests.py | 43 ++++++- 2 files changed, 101 insertions(+), 54 deletions(-) diff --git a/evennia/contrib/map_and_pathfind/mapsystem.py b/evennia/contrib/map_and_pathfind/mapsystem.py index efbc696ae9..5480afef65 100644 --- a/evennia/contrib/map_and_pathfind/mapsystem.py +++ b/evennia/contrib/map_and_pathfind/mapsystem.py @@ -85,14 +85,14 @@ _REVERSE_DIRECTIONS = { } _MAPSCAN = { - "n": (0, 1), - "ne": (1, 1), + "n": (0, -1), + "ne": (1, -1), "e": (1, 0), - "se": (1, -1), - "s": (0, -1), - "sw": (-1, -1), + "se": (1, 1), + "s": (0, 1), + "sw": (-1, 1), "w": (-1, 0), - "nw": (-1, 1) + "nw": (-1, -1) } @@ -118,6 +118,9 @@ class MapNode: # to the empty string, use `symbol`. display_symbol = '' + # set during generation, but is also used for identification of the node + node_index = None + def __init__(self, x, y, node_index): """ Initialize the mapnode. @@ -170,15 +173,16 @@ class MapNode: # just because there is a link here, doesn't mean it's # connected to this node. If so the `end_node` will be None. - end_node, weight = link.traverse(self, _REVERSE_DIRECTIONS(direction), string_map) + end_node, weight = link.traverse(_REVERSE_DIRECTIONS[direction], string_map) if end_node: # the link could be followed to an end node! - self.links[direction] = end_node - self.weights[direction] = weight node_index = end_node.node_index + self.links[direction] = end_node + self.weights[node_index] = weight - if weight < self.cheapest_to_node.get(node_index, _BIG): - self.cheapest_to_node[node_index] = direction + cheapest = self.cheapest_to_node.get(node_index, ("", _BIG))[1] + if weight < cheapest: + self.cheapest_to_node[node_index] = (direction, weight) def linkweights(self, nnodes): """ @@ -211,7 +215,7 @@ class MapNode: str: The direction (nw, se etc) to get to that node in the cheapest way. """ - return self.cheapest_to_node[node.node_index] + return self.cheapest_to_node[node.node_index][0] class MapLink: @@ -286,7 +290,7 @@ class MapLink: directions = _REVERSE_DIRECTIONS links = {} for direction in directions: - dx, dy = _MAPSCAN(direction) + dx, dy = _MAPSCAN[direction] end_x, end_y = self.x + dx, self.y + dy if end_x in string_map and end_y in string_map[end_x]: links[direction] = string_map[end_x][end_y] @@ -344,6 +348,7 @@ class MapLink: MapParserError: If a link lead to nowhere. """ + # from evennia import set_trace;set_trace() end_direction = self.get_directions(start_direction, string_map).get(start_direction) if not end_direction: raise MapParserError(f"Link at ({self.x}, {self.y}) was connected to " @@ -369,7 +374,7 @@ class MapLink: else: # we hit another link. Progress recursively. return next_target.traverse( - _REVERSE_DIRECTIONS(end_direction), + _REVERSE_DIRECTIONS[end_direction], string_map, _weight=_weight, _linklen=_linklen + 1) @@ -546,8 +551,8 @@ class Map: Args: map_module_or_dict (str, module or dict): Path or module pointing to a map. If a dict, - this should be a dict with a key 'map' and optionally 'room_legend' and - 'link_legend' dicts to specify the map structure. + this should be a dict with a key 'map' and optionally a 'legend' + dicts to specify the map structure. """ # load data from dict or file @@ -562,7 +567,7 @@ class Map: mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND) self.mapstring = mapdata['map'] - self.node_legend = map_module_or_dict.get("legend", DEFAULT_LEGEND) + self.legend = map_module_or_dict.get("legend", DEFAULT_LEGEND) self.string_map = None self.node_map = None @@ -579,7 +584,7 @@ class Map: self.parse() def __str__(self): - return "\n".join(self.display_map) + return "\n".join("".join(line) for line in self.display_map) def parse(self): """ @@ -590,10 +595,12 @@ class Map: Notes: """ mapcorner_symbol = self.mapcorner_symbol - # this allows for [x][y] mapping with arbitrary objects + # this allows for string-based [x][y] mapping with arbitrary objects string_map = defaultdict(dict) # needed by pathfinder node_index_map = {} + # mapping nodes to real x,y positions + node_map = defaultdict(dict) mapstring = self.mapstring if mapcorner_symbol not in mapstring: @@ -603,7 +610,7 @@ class Map: # find the the (xstring, ystring) position where the corner symbol is maplines = mapstring.split("\n") mapcorner_x, mapcorner_y = 0, 0 - for mapcorner_y, line in maplines: + for mapcorner_y, line in enumerate(maplines): mapcorner_x = line.find(mapcorner_symbol) if mapcorner_x != -1: break @@ -619,7 +626,7 @@ class Map: # first pass: read string-grid and parse even (x,y) coordinates into nodes for iy, line in enumerate(maplines[origo_y:]): - maxheight = max(maxheight, iy) + maxheight = max(maxheight, iy + 1) even_iy = iy % 2 == 0 for ix, char in enumerate(line[origo_x:]): @@ -627,43 +634,37 @@ class Map: continue even_ix = ix % 2 == 0 - maxwidth = max(maxwidth, ix) + maxwidth = max(maxwidth, ix + 1) + + mapnode_or_link_class = self.legend.get(char) + if not mapnode_or_link_class: + raise MapParserError( + f"Symbol '{char}' on grid position ({ix,iy}) is not found in LEGEND.") if even_iy and even_ix: # a node position will only appear on even positions in the string grid. - - mapnode_class = self.node_legend.get(char) - if not mapnode_class: - raise MapParserError(f"Node symbol '{char}' not in NODE_LEGEND.") - - if hasattr(mapnode_class, "node_index"): + if hasattr(mapnode_or_link_class, "node_index"): # this is an actual node that represents an in-game location # - register it properly. # the x,y stored on the node is the 'actual' xy position in the game # world, not just the position in the string map (that is stored # in the string_map indices instead). - string_map[ix][iy] = self.node_map[ix][iy] = node_index_map[node_index] = \ - mapnode_class(node_index=node_index, x=ix // 2, y=iy // 2) + realx, realy = ix // 2, iy // 2 + string_map[ix][iy] = node_map[realx][realy] = node_index_map[node_index] = \ + mapnode_or_link_class(node_index=node_index, x=realx, y=realy) node_index += 1 continue - else: - # we have a link at a coordinate position. Store it but don't add - # it to the node_index_map since it doesn't have an in-game existence. - string_map[ix][iy] = mapnode_class() # actually a linknode class - else: - # an in-between coordinates link - linknode_class = self.link_legend(char) - if not linknode_class: - raise MapParserError(f"Link symbol '{char}' not in LINK_LEGEND.") - string_map[ix][iy] = linknode_class() - # second pass: Here we loop over all nodes and have them connect to each other - # via the detected linkages. - for node in node_index_map: - node.build_links(string_map) + # an in-between coordinates, or on-node position link + string_map[ix][iy] = mapnode_or_link_class(x=ix, y=iy) + + # second pass: Here we loop over all nodes and have them connect to each other + # via the detected linkages. + for node in node_index_map.values(): + node.build_links(string_map) # build display map - display_map = [" " * maxwidth for _ in range(maxheight)] + display_map = [[" "] * maxwidth for _ in range(maxheight)] for ix, ydct in string_map.items(): for iy, node_or_link in ydct.items(): display_map[iy][ix] = node_or_link.display_symbol @@ -672,23 +673,28 @@ class Map: self.width = maxwidth self.height = maxheight self.string_map = string_map - self.node_index_map = self.node_index_map + self.node_index_map = node_index_map self.display_map = display_map + self.node_map = node_map def _get_node_from_coord(self, x, y): """ Get a MapNode from a coordinate. Args: - x (int): X-coordinate on grid. - y (int): Y-coordinate on grid. + x (int): X-coordinate on game grid. + y (int): Y-coordinate on game grid. + + Returns: + MapNode: The node found at the given coordinates. + """ - if not self.node_2d_map: + if not self.node_map: self.parse() try: - self.node_2d_map[x][y] + return self.node_map[x][y] except IndexError: raise MapError("_get_node_from_coord got coordinate ({x},{y}) which is " "outside the grid size of (0,0) - ({self.width}, {self.height}).") @@ -729,8 +735,8 @@ class Map: """ - istartnode = self._get_node_from_coord(*endcoord).node_index - endnode = self._get_node_from_coord(*startcoord) + istartnode = self._get_node_from_coord(*startcoord).node_index + endnode = self._get_node_from_coord(*endcoord) if not self.pathfinding_routes: self._calculate_path_matrix() @@ -774,5 +780,5 @@ class Map: output = [] for line in self.display_map[top:bottom]: - output.append(line[left:right]) + output.append("".join(line[left:right])) return "\n".join(output) diff --git a/evennia/contrib/map_and_pathfind/tests.py b/evennia/contrib/map_and_pathfind/tests.py index e8a47d6721..ad47dbb035 100644 --- a/evennia/contrib/map_and_pathfind/tests.py +++ b/evennia/contrib/map_and_pathfind/tests.py @@ -4,5 +4,46 @@ Tests for the Mapsystem """ -from django.unittest import TestCase, mock +from unittest import TestCase, mock from . import mapsystem + + +MAP1 = """ + + + 0 1 2 + + 0 #-# + | | + 1 #-# + + +""" +MAP1_DISPLAY = """ +#-# +| | +#-# +""".strip() + + +class TestMap1(TestCase): + """ + Test the Map class with a simple map and default symbol legend. + + """ + + def setUp(self): + self.map = mapsystem.Map({"map": MAP1}) + + def test_str_output(self): + """Check the display_map""" + self.assertEqual(str(self.map).strip(), MAP1_DISPLAY) + + def test_node_from_coord(self): + node = self.map._get_node_from_coord(1, 1) + self.assertEqual(node.x, 1) + self.assertEqual(node.y, 1) + + def test_get_shortest_path(self): + nodepath, linkpath = self.map.get_shortest_path((0, 0), (1, 1)) + self.assertEqual([node.node_index for node in nodepath], [0, 1, 3]) + self.assertEqual(linkpath, ['e', 's'])