Fully functional orthogonal map tested

This commit is contained in:
Griatch 2021-06-08 00:15:56 +02:00
parent 25a73aee60
commit 30fe0c4b5f
2 changed files with 136 additions and 43 deletions

View file

@ -175,6 +175,9 @@ class MapNode:
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})>"
def __repr__(self):
return str(self)
def build_links(self, xygrid): def build_links(self, xygrid):
""" """
Start tracking links in all cardinal directions to tie this to another node. All Start tracking links in all cardinal directions to tie this to another node. All
@ -303,6 +306,9 @@ class MapLink:
def __str__(self): def __str__(self):
return f"<LinkNode xy=({self.x},{self.y}) ({self.symbol})>" return f"<LinkNode xy=({self.x},{self.y}) ({self.symbol})>"
def __repr__(self):
return str(self)
def get_visually_connected(self, xygrid, directions=None): def get_visually_connected(self, xygrid, directions=None):
""" """
A helper to get all directions to which there appears to be a A helper to get all directions to which there appears to be a
@ -831,27 +837,33 @@ class Map:
# process the new(?) data # process the new(?) data
self._parse() self._parse()
def get_node_from_coord(self, X, Y): def get_node_from_coord(self, coords):
""" """
Get a MapNode from a coordinate. Get a MapNode from a coordinate.
Args: Args:
X (int): X-coordinate on XY (game) grid. coords (tuple): X,Y coordinates on XYgrid.
Y (int): Y-coordinate on XY (game) grid.
Returns: Returns:
MapNode: The node found at the given coordinates. MapNode: The node found at the given coordinates. Returns
`None` if there is no mapnode at the given coordinate.
Raises:
MapError: If trying to specify an iX,iY outside
of the grid's maximum bounds.
""" """
if not self.XYgrid: if not self.XYgrid:
self.parse() self.parse()
try: iX, iY = coords
return self.XYgrid[X][Y] if not ((0 <= iX <= self.max_X) and (0 <= iY <= self.max_Y)):
except IndexError: raise MapError("get_node_from_coord got coordinate {coords} which is "
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}).") "outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).")
try:
return self.XYgrid[coords[0]][coords[1]]
except KeyError:
return None
def get_shortest_path(self, startcoord, endcoord): def get_shortest_path(self, startcoord, endcoord):
""" """
@ -869,10 +881,10 @@ 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 self.pathfinding_routes is None:
self._calculate_path_matrix() self._calculate_path_matrix()
pathfinding_routes = self.pathfinding_routes pathfinding_routes = self.pathfinding_routes
@ -898,7 +910,7 @@ class Map:
return directions, path return directions, path
def get_map_display(self, coord, dist=2, only_nodes=False, def get_map_display(self, coord, dist=2, mode='scan',
character='@', max_size=None, return_str=True): character='@', max_size=None, return_str=True):
""" """
Display the map centered on a point and everything around it within a certain distance. Display the map centered on a point and everything around it within a certain distance.
@ -907,16 +919,14 @@ class Map:
coord (tuple): (X,Y) in-world coordinate location. coord (tuple): (X,Y) in-world coordinate location.
dist (int, optional): Number of gridpoints distance to show. Which dist (int, optional): Number of gridpoints distance to show. Which
grid to use depends on the setting of `only_nodes`. grid to use depends on the setting of `only_nodes`.
only_nodes (boolean): This determins if `dist` only counts the number of mode (str, optional): One of 'scan' or 'nodes'. In 'scan' mode, dist measure
full nodes or counts the number of actual visual map-grid-points number of xy grid points in all directions. If 'nodes', distance
(including links). If set, it's recommended to set `max_size` to avoid measure how many full nodes away to display.
too-large map displays.
character (str, optional): Place this symbol at the `coord` position character (str, optional): Place this symbol at the `coord` position
of the displayed map. Ignored if falsy. of the displayed map. Ignored if falsy.
max_size (tuple, optional): A max `(width, height)` of the resulting max_size (tuple, optional): A max `(width, height)` to crop the displayed
string or list. This can be useful together with `only_nodes` return to. Make both odd numbers to get a perfect center.
to avoid a map display growing unexpectedly. If unset, size If unset, display-size can grow up to the full size of the grid.
can grow up to the full size of the map.
return_str (bool, optional): Return result as an return_str (bool, optional): Return result as an
already formatted string. already formatted string.
@ -962,48 +972,77 @@ class Map:
# convert inputs to xygrid # convert inputs to xygrid
width, height = self.max_x + 1, self.max_y + 1 width, height = self.max_x + 1, self.max_y + 1
ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height)) ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height))
display_map = self.display_map
if only_nodes: if dist <= 0:
# dist measures only full, reachable nodes # show nothing but ourselves
return character if character else ' '
# we will build a list of coordinates (from the full if mode == 'nodes':
# map display) to actually include in the final # dist measures only full, reachable nodes.
# this requires a series of shortest-path
# Steps from on the pre-calulcated grid.
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)] points = [(ix, iy)]
xmax = 0
ymax = 0
node_index_map = self.node_index_map node_index_map = self.node_index_map
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:
continue continue
# 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((iX, iY), (node.X, node.Y))
# follow directions to figure out which map coords to display # follow directions to figure out which map coords to display
node0 = node node0 = node
ix0, iy0 = ix, iy ix0, iy0 = ix, iy
for path_element in path: for path_element in path:
# we don't need the start node since we know it already
if isinstance(path_element, str): if isinstance(path_element, str):
# a direction - this can lead to following # a direction - this can lead to following
# a longer link-chain chain # a longer link-chain chain
for dstep in node0.xy_steps_in_direction[path_element]: for dstep in node0.xy_steps_in_direction[path_element]:
dx, dy = _MAPSCAN[dstep] dx, dy = _MAPSCAN[dstep]
ix0, iy0 = ix0 + dx, iy0 + dy ix0, iy0 = ix0 + dx, iy0 + dy
xmax, ymax = max(xmax, ix0), max(ymax, iy0)
points.append((ix0, iy0)) points.append((ix0, iy0))
xmin, ymin = min(xmin, ix0), min(ymin, iy0)
xmax, ymax = max(xmax, ix0), max(ymax, iy0)
else: else:
# a Mapnode # a Mapnode
node0 = path_element node0 = path_element
ix0, iy0 = node0.ix, node0.iy ix0, iy0 = node0.x, node0.y
if (ix0, iy0) != (ix, iy):
points.append((ix0, iy0)) points.append((ix0, iy0))
xmin, ymin = min(xmin, ix0), min(ymin, iy0)
xmax, ymax = max(xmax, ix0), max(ymax, iy0)
# from evennia import set_trace;set_trace()
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: else:
# dist measures individual grid points # scan-mode (default) - dist measures individual grid points
if dist is None: if dist is None:
gridmap = self.display_map gridmap = self.display_map
ixc, iyc = ix, iy ixc, iyc = ix, iy
@ -1011,13 +1050,20 @@ class Map:
left, right = max(0, ix - dist), min(width, ix + dist + 1) left, right = max(0, ix - dist), min(width, ix + dist + 1)
bottom, top = max(0, iy - dist), min(height, iy + dist + 1) bottom, top = max(0, iy - dist), min(height, iy + dist + 1)
ixc, iyc = ix - left, iy - bottom ixc, iyc = ix - left, iy - bottom
gridmap = [line[left:right] for line in self.display_map[bottom:top]] gridmap = [line[left:right] for line in display_map[bottom:top]]
if character: if character:
gridmap[iyc][ixc] = character # correct indexing; it's a list of lines gridmap[iyc][ixc] = character # correct indexing; it's a list of lines
# we must flip the y-axis before returning if max_size:
# crop grid to make sure it doesn't grow too far
max_x, max_y = max_size
left, right = max(0, ixc - max_x // 2), min(width, ixc + max_x // 2 + 1)
bottom, top = max(0, iyc - max_y // 2), min(height, iyc + max_y // 2 + 1)
gridmap = [line[left:right] for line in gridmap[bottom:top]]
if return_str: if return_str:
# we must flip the y-axis before returning the string
return "\n".join("".join(line) for line in gridmap[::-1]) return "\n".join("".join(line) for line in gridmap[::-1])
else: else:
return gridmap return gridmap

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)
@ -130,6 +130,20 @@ class TestMap1(TestCase):
self.assertEqual(expectstr, mapstr) self.assertEqual(expectstr, mapstr)
self.assertEqual(expectlst, maplst[::-1]) # flip y-axis to match print direction self.assertEqual(expectlst, maplst[::-1]) # flip y-axis to match print direction
@parameterized.expand([
((0, 0), '# \n| \n@-#'),
((0, 1), '@-#\n| \n# '),
((1, 0), ' #\n |\n#-@'),
((1, 1), '#-@\n |\n #'),
])
def test_get_map_display__nodes__character(self, coord, expected):
"""
Get sub-part of map with node-mode.
"""
mapstr = self.map.get_map_display(coord, dist=1, mode='nodes', character='@')
self.assertEqual(expected, mapstr)
class TestMap2(TestCase): class TestMap2(TestCase):
""" """
@ -148,7 +162,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)
@ -177,7 +191,7 @@ class TestMap2(TestCase):
((4, 5), '#-#-@ \n| | \n#---# \n| | \n| #-#'), ((4, 5), '#-#-@ \n| | \n#---# \n| | \n| #-#'),
((5, 2), '--# \n | \n #-#\n |\n#---@\n \n--#-#\n | \n#-# '), ((5, 2), '--# \n | \n #-#\n |\n#---@\n \n--#-#\n | \n#-# '),
]) ])
def test_get_map_display__character(self, coord, expected): def test_get_map_display__scan__character(self, coord, expected):
""" """
Test showing smaller part of grid, showing @-character in the middle. Test showing smaller part of grid, showing @-character in the middle.
@ -186,7 +200,11 @@ class TestMap2(TestCase):
self.assertEqual(expected, mapstr) self.assertEqual(expected, mapstr)
def test_extended_path_tracking__horizontal(self): def test_extended_path_tracking__horizontal(self):
node = self.map.get_node_from_coord(4, 1) """
Crossing multi-gridpoint links should be tracked properly.
"""
node = self.map.get_node_from_coord((4, 1))
self.assertEqual( self.assertEqual(
node.xy_steps_in_direction, node.xy_steps_in_direction,
{'e': ['e'], {'e': ['e'],
@ -195,7 +213,11 @@ class TestMap2(TestCase):
) )
def test_extended_path_tracking__vertical(self): def test_extended_path_tracking__vertical(self):
node = self.map.get_node_from_coord(2, 2) """
Testing multi-gridpoint links in the vertical direction.
"""
node = self.map.get_node_from_coord((2, 2))
self.assertEqual( self.assertEqual(
node.xy_steps_in_direction, node.xy_steps_in_direction,
{'n': ['n', 'n', 'n'], {'n': ['n', 'n', 'n'],
@ -204,3 +226,28 @@ class TestMap2(TestCase):
'w': ['w']} 'w': ['w']}
) )
@parameterized.expand([
((0, 0), 2, None, '@'), # outside of any known node
((4, 5), 0, None, '@'), # 0 distance
((1, 0), 2, None,
'#-#-# \n | \n @-#-#'),
((0, 5), 1, None, '@-#'),
((0, 5), 4, None,
'@-#-#-#-#\n | \n #---#\n | \n | \n | \n # '),
((5, 1), 3, None, ' # \n | \n#-#---#-@\n | \n #-# '),
((2, 2), 2, None,
' # \n | \n #---# \n | \n | \n | \n'
'#-#-@-#---#\n | \n #-#---# '),
((2, 2), 2, (5, 5), # limit display size
' | \n | \n#-@-#\n | \n#-#--'),
((2, 2), 4, (3, 3), ' | \n-@-\n | '),
((2, 2), 4, (1, 1), '@')
])
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)
self.assertEqual(expected, mapstr)