Map contrib working with all links and nodes

This commit is contained in:
Griatch 2021-06-09 20:19:56 +02:00
parent ea63071c64
commit 6c2722a3f2
2 changed files with 339 additions and 71 deletions

View file

@ -173,7 +173,7 @@ class MapNode:
self.xy_steps_in_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.symbol}' {self.node_index} XY=({round(self.X)},{round(self.Y)})"
def __repr__(self): def __repr__(self):
return str(self) return str(self)
@ -274,13 +274,13 @@ class MapLink:
# n,ne,e,se,s,sw,w,nw. A link is described as {startpos:endpoit}, like connecting # n,ne,e,se,s,sw,w,nw. A link is described as {startpos:endpoit}, like connecting
# the named corners with a line. If the inverse direction is also possible, it # the named corners with a line. If the inverse direction is also possible, it
# must also be specified. So a south-northward, two-way link would be described # must also be specified. So a south-northward, two-way link would be described
# as {"s": "n", "n": "s"}. The get_directions method can be customized to # as {"s": "n", "n": "s"}. The get_direction method can be customized to
# dynamically modify this during parsing. # return something else.
directions = {} directions = {}
# this is required for pathfinding. Each weight is defined as {startpos:weight}, where # this is required for pathfinding. Each weight is defined as {startpos:weight}, where
# the startpos is the direction of the cell (n,ne etc) where the link *starts*. The # the startpos is the direction of the cell (n,ne etc) where the link *starts*. The
# weight is a value > 0, smaller than _BIG. The get_directions method can be # weight is a value > 0, smaller than _BIG. The get_weight method can be
# customized to modify this during parsing. # customized to modify to return something else.
weights = {} weights = {}
default_weight = 1 default_weight = 1
# This setting only applies if this is the *first* link in a chain of multiple links. Usually, # This setting only applies if this is the *first* link in a chain of multiple links. Usually,
@ -304,7 +304,7 @@ class MapLink:
self.display_symbol = self.symbol self.display_symbol = self.symbol
def __str__(self): def __str__(self):
return f"<LinkNode xy=({self.x},{self.y}) ({self.symbol})>" return f"<LinkNode '{self.symbol}' XY=({round(self.x / 2)},{round(self.y / 2)})>"
def __repr__(self): def __repr__(self):
return str(self) return str(self)
@ -324,6 +324,8 @@ class MapLink:
dict: Mapping {direction: node_or_link} wherever such was found. dict: Mapping {direction: node_or_link} wherever such was found.
""" """
# if (self.x, self.y) == (4, 8):
# from evennia import set_trace;set_trace()
if not directions: if not directions:
directions = _REVERSE_DIRECTIONS directions = _REVERSE_DIRECTIONS
links = {} links = {}
@ -331,10 +333,15 @@ class MapLink:
dx, dy = _MAPSCAN[direction] dx, dy = _MAPSCAN[direction]
end_x, end_y = self.x + dx, self.y + dy end_x, end_y = self.x + dx, self.y + dy
if end_x in xygrid and end_y in xygrid[end_x]: if end_x in xygrid and end_y in xygrid[end_x]:
links[direction] = xygrid[end_x][end_y] # there is is something there, we need to check if it is either
# a map node or a link connecting in our direction
node_or_link = xygrid[end_x][end_y]
if (hasattr(node_or_link, "node_index")
or node_or_link.get_direction(direction, xygrid)):
links[direction] = node_or_link
return links return links
def get_directions(self, start_direction, xygrid): def get_direction(self, start_direction, xygrid):
""" """
Hook to override for customizing how the directions are Hook to override for customizing how the directions are
determined. determined.
@ -344,13 +351,18 @@ class MapLink:
xygrid (dict): 2D dict with x,y coordinates as keys. xygrid (dict): 2D dict with x,y coordinates as keys.
Returns: Returns:
dict: The directions map {start_direction:end_direction} of str: The 'out' direction side of the link - where the link
the link. By default this is just self.directions. leads to.
Example:
With the default legend, if the link is a straght vertical link
(`|`) and `start_direction` is `s` (link is approached from
from the south side), then this function will return `n'.
""" """
return self.directions return self.directions.get(start_direction)
def get_weights(self, start_direction, xygrid, current_weight): def get_weight(self, start_direction, xygrid, current_weight):
""" """
Hook to override for customizing how the weights are determined. Hook to override for customizing how the weights are determined.
@ -361,11 +373,10 @@ class MapLink:
we are progressing down a multi-step path. we are progressing down a multi-step path.
Returns: Returns:
dict: The directions map {start_direction:weight} of int: The weight to use for a link from `start_direction`.
the link. By default this is just self.weights
""" """
return self.weights return self.weights.get(start_direction, self.default_weight)
def traverse(self, start_direction, xygrid, _weight=0, _linklen=1, _steps=None): def traverse(self, start_direction, xygrid, _weight=0, _linklen=1, _steps=None):
""" """
@ -389,26 +400,26 @@ class MapLink:
MapParserError: If a link lead to nowhere. MapParserError: If a link lead to nowhere.
""" """
# from evennia import set_trace;set_trace() end_direction = self.get_direction(start_direction, xygrid)
end_direction = self.get_directions(start_direction, xygrid).get(start_direction)
if not end_direction: if not end_direction:
if _steps is None: if _steps is None:
# is perfectly okay to not be linking to a node # is perfectly okay to not be linking to a node
return None, 0, None return None, 0, None
raise MapParserError(f"Link at ({self.x}, {self.y}) was connected to " raise MapParserError(f"Link '{self.symbol}' at "
f"from {start_direction}, but does not link that way.") f"XY=({round(self.x / 2)},{round(self.y / 2)}) "
f"was connected to from the direction {start_direction}, but "
"is not set up to link in that direction.")
dx, dy = _MAPSCAN[end_direction] dx, dy = _MAPSCAN[end_direction]
end_x, end_y = self.x + dx, self.y + dy end_x, end_y = self.x + dx, self.y + dy
try: try:
next_target = xygrid[end_x][end_y] next_target = xygrid[end_x][end_y]
except KeyError: except KeyError:
raise MapParserError(f"Link at ({self.x}, {self.y}) points to " raise MapParserError(f"Link '{self.symbol}' at "
f"empty space in direction {end_direction}!") f"XY=({round(self.x / 2)},{round(self.y / 2)}) "
"points to empty space in the direction {end_direction}!")
_weight += self.get_weights( _weight += self.get_weight(start_direction, xygrid, _weight)
start_direction, xygrid, _weight).get(
start_direction, self.default_weight)
if _steps is None: if _steps is None:
_steps = [] _steps = []
_steps.append(_REVERSE_DIRECTIONS[start_direction]) _steps.append(_REVERSE_DIRECTIONS[start_direction])
@ -513,33 +524,46 @@ class DynamicMapLink(MapLink):
""" """
symbol = "o" symbol = "o"
def get_directions(self, start_direction, xygrid): def get_direction(self, start_direction, xygrid):
# get all visually connected links # get all visually connected links
directions = {} if not hasattr(self, '_cached_directions'):
links = list(self.get_visually_connected(xygrid).keys()) # try to get from cache where possible
loop_links = links.copy() directions = {}
# first get all cross-through links unhandled_links = list(self.get_visually_connected(xygrid).keys())
for direction in loop_links:
if _REVERSE_DIRECTIONS[direction] in loop_links:
directions[direction] = links.pop(direction)
# check if we have any non-cross-through paths to handle # get all straight lines (n-s, sw-ne etc) we can trace through
if len(links) != 2: # the dynamic link and remove them from the unhandled_links list
links = "-".join(links) unhandled_links_copy = unhandled_links.copy()
raise MapParserError( for direction in unhandled_links_copy:
f"dynamic link at grid ({self.x, self.y}) cannot determine " if _REVERSE_DIRECTIONS[direction] in unhandled_links_copy:
f"where how to connect links leading to/from {links}.") directions[direction] = _REVERSE_DIRECTIONS[
directions[links[0]] = links[1] unhandled_links.pop(unhandled_links.index(direction))]
directions[links[1]] = links[0]
return directions # check if we have any non-cross-through paths left to handle
n_unhandled = len(unhandled_links)
if n_unhandled:
# still remaining unhandled links. If there's not exactly
# one 'incoming' and one 'outgoing' we can't figure out
# where to go in a non-ambiguous way.
if n_unhandled != 2:
links = ", ".join(unhandled_links)
raise MapParserError(
f"Dynamic Link '{self.symbol}' at "
f"XY=({round(self.x / 2)},{round(self.y / 2)}) cannot determine "
f"how to connect in/out directions {links}.")
directions[unhandled_links[0]] = unhandled_links[1]
directions[unhandled_links[1]] = unhandled_links[0]
self._cached_directions = directions
return self._cached_directions.get(start_direction)
# these are all symbols used for x,y coordinate spots # these are all symbols used for x,y coordinate spots
# at (0,1) etc. # at (0,1) etc.
DEFAULT_LEGEND = { DEFAULT_LEGEND = {
"#": MapNode, "#": MapNode,
"o": MapLink,
"|": NSMapLink, "|": NSMapLink,
"-": EWMapLink, "-": EWMapLink,
"/": NESWMapLink, "/": NESWMapLink,
@ -550,6 +574,7 @@ DEFAULT_LEGEND = {
"^": SNOneWayMapLink, "^": SNOneWayMapLink,
"<": EWOneWayMapLink, "<": EWOneWayMapLink,
">": WEOneWayMapLink, ">": WEOneWayMapLink,
"o": DynamicMapLink,
} }
# -------------------------------------------- # --------------------------------------------
@ -662,7 +687,7 @@ class Map:
pathfinding_graph = zeros((nnodes, nnodes)) pathfinding_graph = zeros((nnodes, nnodes))
# build a matrix representing the map graph, with 0s as impassable areas # build a matrix representing the map graph, with 0s as impassable areas
for inode, node in self.node_index_map.items(): for inode, node in self.node_index_map.items():
pathfinding_graph[:, inode] = node.linkweights(nnodes) pathfinding_graph[inode, :] = node.linkweights(nnodes)
# create a sparse matrix to represent link relationships from each node # create a sparse matrix to represent link relationships from each node
pathfinding_matrix = csr_matrix(pathfinding_graph) pathfinding_matrix = csr_matrix(pathfinding_graph)
@ -699,6 +724,7 @@ class Map:
"symbols marking the upper- and bottom-left corners of the " "symbols marking the upper- and bottom-left corners of the "
"grid area.") "grid area.")
# from evennia import set_trace;set_trace()
# find the the position (in the string as a whole) of the top-left corner-marker # find the the position (in the string as a whole) of the top-left corner-marker
maplines = mapstring.split("\n") maplines = mapstring.split("\n")
topleft_marker_x, topleft_marker_y = -1, -1 topleft_marker_x, topleft_marker_y = -1, -1
@ -706,7 +732,7 @@ class Map:
topleft_marker_x = line.find(mapcorner_symbol) topleft_marker_x = line.find(mapcorner_symbol)
if topleft_marker_x != -1: if topleft_marker_x != -1:
break break
if topleft_marker_x == -1 or topleft_marker_y == -1: if -1 in (topleft_marker_x, topleft_marker_y):
raise MapParserError(f"No top-left corner-marker ({mapcorner_symbol}) found!") raise MapParserError(f"No top-left corner-marker ({mapcorner_symbol}) found!")
# find the position (in the string as a whole) of the bottom-left corner-marker # find the position (in the string as a whole) of the bottom-left corner-marker
@ -719,11 +745,13 @@ class Map:
raise MapParserError(f"No bottom-left corner-marker ({mapcorner_symbol}) found! " raise MapParserError(f"No bottom-left corner-marker ({mapcorner_symbol}) found! "
"Make sure it lines up with the top-left corner-marker " "Make sure it lines up with the top-left corner-marker "
f"(found at column {topleft_marker_x} of the string).") f"(found at column {topleft_marker_x} of the string).")
# the actual coordinate is dy below the topleft marker so we need to shift
botleft_marker_y += topleft_marker_y + 1
# in-string_position of the top- and bottom-left grid corners (2 steps in from marker) # in-string_position of the top- and bottom-left grid corners (2 steps in from marker)
# the bottom-left corner is also the origo (0,0) of the grid. # the bottom-left corner is also the origo (0,0) of the grid.
topleft_y = topleft_marker_y + 2 topleft_y = topleft_marker_y + 2
origo_x, origo_y = botleft_marker_x + 2, botleft_marker_y + 2 origo_x, origo_y = botleft_marker_x + 2, botleft_marker_y - 1
# highest actually filled grid points # highest actually filled grid points
max_x = 0 max_x = 0
@ -747,7 +775,8 @@ class Map:
mapnode_or_link_class = self.legend.get(char) mapnode_or_link_class = self.legend.get(char)
if not mapnode_or_link_class: if not mapnode_or_link_class:
raise MapParserError( raise MapParserError(
f"Symbol '{char}' on xygrid position ({ix},{iy}) is not found in LEGEND." f"Symbol '{char}' on XY=({round(ix / 2)},{round(iy / 2)}) "
"is not found in LEGEND."
) )
if hasattr(mapnode_or_link_class, "node_index"): if hasattr(mapnode_or_link_class, "node_index"):
# A mapnode. Mapnodes can only be placed on even grid positions, where # A mapnode. Mapnodes can only be placed on even grid positions, where
@ -755,8 +784,9 @@ class Map:
if not (even_iy and ix % 2 == 0): if not (even_iy and ix % 2 == 0):
raise MapParserError( raise MapParserError(
f"Symbol '{char}' (xygrid ({ix},{iy}) marks a Node but is located " f"Symbol '{char}' on XY=({round(ix / 2)},{round(iy / 2)}) marks a "
"between valid (X,Y) positions!") "MapNode but is located between integer (X,Y) positions (only "
"Links can be placed between coordinates)!")
# save the node to several different maps for different uses # save the node to several different maps for different uses
# in both coordinate systems # in both coordinate systems
@ -861,8 +891,8 @@ class Map:
iX, iY = coords iX, iY = coords
if not ((0 <= iX <= self.max_X) and (0 <= iY <= self.max_Y)): if not ((0 <= iX <= self.max_X) and (0 <= iY <= self.max_Y)):
raise MapError("get_node_from_coord got coordinate {coords} which is " raise MapError(f"get_node_from_coord got coordinate {coords} which is "
"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).") f"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).")
try: try:
return self.XYgrid[coords[0]][coords[1]] return self.XYgrid[coords[0]][coords[1]]
except KeyError: except KeyError:
@ -887,10 +917,17 @@ class Map:
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 endnode: if not (startnode and endnode):
# no node at given coordinate. No path is possible. # no node at given coordinate. No path is possible.
return [], [] return [], []
try:
istartnode = startnode.node_index
inextnode = endnode.node_index
except AttributeError:
raise MapError(f"Map.get_shortest_path received start/end nodes {startnode} and "
f"{endnode}. They must both be MapNodes (not Links)")
if self.pathfinding_routes is None: if self.pathfinding_routes is None:
self._calculate_path_matrix() self._calculate_path_matrix()
@ -899,8 +936,6 @@ class Map:
path = [endnode] path = [endnode]
directions = [] directions = []
istartnode = startnode.node_index
inextnode = endnode.node_index
while pathfinding_routes[istartnode, inextnode] != -9999: while pathfinding_routes[istartnode, inextnode] != -9999:
# the -9999 is set by algorithm for unreachable nodes or if trying # the -9999 is set by algorithm for unreachable nodes or if trying
@ -945,31 +980,28 @@ class Map:
indexing `outlist[iy][ix]` in that order. indexing `outlist[iy][ix]` in that order.
Notes: Notes:
If outputting an output list, the y-axis must first be If outputting a list, the y-axis must first be reversed before printing since printing
reversed since printing happens top-bottom and the y coordinate happens top-bottom and the y coordinate system goes bottom-up. This can be done simply
system goes bottom-up. This can be done simply with with this before building the final string to send/print.
reversed = outlist[::-1] printable_order_list = outlist[::-1]
before starting the printout loop. If mode='nodes', a `dist` of 2 will give the following result in a row of nodes:
If `only_nodes` is True, a dist of 2 will give the following
result in a row of nodes:
#-#-@----------#-# #-#-@----------#-#
This display may grow much bigger than expected (both horizontally This display may thus visually grow much bigger than expected (both horizontally and
and vertically). consider setting `max_size` if wanting to restrict the display size. vertically). consider setting `max_size` if wanting to restrict the display size. Also
also note that link 'weights' are *included* in this estimate, so note that link 'weights' are *included* in this estimate, so if links have weights > 1,
if links have weights > 1, fewer nodes will be found for a given `dist`. fewer nodes may be found for a given `dist`.
If `only_nodes` is False, dist of 2 would give If mode=`scan`, a dist of 2 on the above example would instead give
#-@-- #-@--
This is more of a 'moving' overview type of map that just displays a part of the grid This mode simply shows a cut-out subsection of the map you are on. The `dist` is
you are on. It does not consider links or weights and may also show nodes not measured on xygrid, so two steps per XY coordinate. It does not consider links or
actually reachable at the moment: weights and may also show nodes not actually reachable at the moment:
| | | |
# @-# # @-#
@ -1039,7 +1071,6 @@ class Map:
xmin, ymin = min(xmin, ix0), min(ymin, iy0) xmin, ymin = min(xmin, ix0), min(ymin, iy0)
xmax, ymax = max(xmax, ix0), max(ymax, iy0) xmax, ymax = max(xmax, ix0), max(ymax, iy0)
# from evennia import set_trace;set_trace()
ixc, iyc = ix - xmin, iy - ymin ixc, iyc = ix - xmin, iy - ymin
# note - override width/height here since our grid is # note - override width/height here since our grid is
# now different from the original for future cropping # now different from the original for future cropping

View file

@ -138,6 +138,110 @@ MAP5_DISPLAY = r"""
#---# #---#
""".strip() """.strip()
MAP6 = r"""
+ 0 1 2
2 #-#
| |
1 #>#
0 #>#
+ 0 1 2
"""
MAP6_DISPLAY = r"""
#-#
| |
#>#
#>#
""".strip()
MAP7 = r"""
+ 0 1 2 3 4
4 #-#-#-#
^ |
3 | #>#
| | |
2 #-#-#-#
^ v
1 #---#-#
| | |
0 #-#>#-#<#
+ 0 1 2 3 4
"""
MAP7_DISPLAY = r"""
#-#-#-#
^ |
| #>#
| | |
#-#-#-#
^ v
#---#-#
| | |
#-#>#-#<#
""".strip()
MAP8 = r"""
+ 0 1 2
2 #-#
|
1 #-o-#
|
0 #-#
+ 0 1 2
"""
MAP8_DISPLAY = r"""
#-#
|
#-o-#
|
#-#
""".strip()
MAP9 = r"""
+ 0 1 2 3 4 5
4 #-#-o o o-o
| \|/| | |
3 #-o-o-# o-#
| /|\ |
2 o-o-#-# o
| | /
1 #-o-#-o-#
| /
0 #---#-o
+ 0 1 2 3 4 5
"""
MAP9_DISPLAY = r"""
#-#-o o o-o
| \|/| | |
#-o-o-# o-#
| /|\ |
o-o-#-# o
| | /
#-o-#-o-#
| /
#---#-o
""".strip()
class TestMap1(TestCase): class TestMap1(TestCase):
""" """
@ -377,7 +481,7 @@ class TestMap4(TestCase):
""" """
mapstr = self.map.get_map_display(coord, dist=dist, mode='nodes', character='@', mapstr = self.map.get_map_display(coord, dist=dist, mode='nodes', character='@',
max_size=max_size) max_size=max_size)
print(repr(mapstr)) # print(repr(mapstr))
self.assertEqual(expected, mapstr) self.assertEqual(expected, mapstr)
class TestMap5(TestCase): class TestMap5(TestCase):
@ -408,3 +512,136 @@ class TestMap5(TestCase):
""" """
directions, _ = self.map.get_shortest_path(startcoord, endcoord) directions, _ = self.map.get_shortest_path(startcoord, endcoord)
self.assertEqual(expected_directions, tuple(directions)) self.assertEqual(expected_directions, tuple(directions))
class TestMap6(TestCase):
"""
Test Map6 - Small map with one-way links
"""
def setUp(self):
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(MAP6_DISPLAY, stripped_map)
@parameterized.expand([
((0, 0), (1, 0), ('e',)), # cross one-way
((1, 0), (0, 0), ()), # blocked
((0, 1), (1, 1), ('e',)), # should still take shortest
((1, 1), (0, 1), ('n', 'w', 's')), # take long way around
])
def test_shortest_path(self, startcoord, endcoord, expected_directions):
"""
Test shortest-path calculations throughout the grid.
"""
directions, _ = self.map.get_shortest_path(startcoord, endcoord)
self.assertEqual(expected_directions, tuple(directions))
class TestMap7(TestCase):
"""
Test Map6 - Bigger map with one-way links in different directions
"""
def setUp(self):
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(MAP7_DISPLAY, stripped_map)
@parameterized.expand([
((0, 0), (2, 0), ('e', 'e')), # cross one-way
((2, 0), (0, 0), ('e', 'n', 'w', 's', 'w')), # blocked, long way around
((4, 0), (3, 0), ('w',)),
((3, 0), (4, 0), ('n', 'e', 's')),
((1, 1), (1, 2), ('n',)),
((1, 2), (1, 1), ('e', 'e', 's', 'w')),
((3, 1), (1, 4), ('w', 'n', 'n')),
((0, 4), (0, 0), ('e', 'e', 'e', 's', 's', 's', 'w', 's', 'w')),
])
def test_shortest_path(self, startcoord, endcoord, expected_directions):
"""
Test shortest-path calculations throughout the grid.
"""
directions, _ = self.map.get_shortest_path(startcoord, endcoord)
self.assertEqual(expected_directions, tuple(directions))
class TestMap8(TestCase):
"""
Test Map6 - Small test of dynamic link node
"""
def setUp(self):
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(MAP8_DISPLAY, stripped_map)
@parameterized.expand([
((1, 0), (1, 2), ('n', )),
((1, 2), (1, 0), ('s', )),
((0, 1), (2, 1), ('e', )),
((2, 1), (0, 1), ('w', )),
])
def test_shortest_path(self, startcoord, endcoord, expected_directions):
"""
test shortest-path calculations throughout the grid.
"""
directions, _ = self.map.get_shortest_path(startcoord, endcoord)
self.assertequal(expected_directions, tuple(directions))
class TestMap9(TestCase):
"""
Test Map6 - Small test of dynamic link node
"""
def setUp(self):
self.map = mapsystem.Map({"map": MAP9})
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)
@parameterized.expand([
((2, 0), (2, 2), ('n',)),
((0, 0), (5, 3), ('e', 'e')),
((5, 1), (0, 3), ('w', 'w', 'n', 'w')),
((1, 1), (2, 2), ('n', 'w', 's')),
((5, 3), (5, 3), ()),
((5, 3), (0, 4), ('s', 'n', 'w', 'n')),
((1, 4), (3, 3), ('e', 'w', 'e')),
])
def test_shortest_path(self, startcoord, endcoord, expected_directions):
"""
test shortest-path calculations throughout the grid.
"""
directions, _ = self.map.get_shortest_path(startcoord, endcoord)
self.assertEqual(expected_directions, tuple(directions))
@parameterized.expand([
((2, 2), 1, None, ' #-o \n | \n# o \n| | \no-o-@-#\n '
'| \n o \n | \n # '),
])
def test_get_map_display__nodes__character(self, coord, dist, max_size, expected):
"""
Get sub-part of map with node-mode.
"""
mapstr = self.map.get_map_display(coord, dist=dist, mode='nodes', character='@',
max_size=max_size)
print(repr(mapstr))
self.assertEqual(expected, mapstr)