Fully functional orthogonal map tested

This commit is contained in:
Griatch 2021-06-08 00:15:56 +02:00
parent 48c7bb303c
commit 3c8003ee63
2 changed files with 136 additions and 43 deletions

View file

@ -175,6 +175,9 @@ class MapNode:
def __str__(self):
return f"<MapNode {self.node_index} XY=({self.X},{self.Y}) ({self.symbol})>"
def __repr__(self):
return str(self)
def build_links(self, xygrid):
"""
Start tracking links in all cardinal directions to tie this to another node. All
@ -303,6 +306,9 @@ class MapLink:
def __str__(self):
return f"<LinkNode xy=({self.x},{self.y}) ({self.symbol})>"
def __repr__(self):
return str(self)
def get_visually_connected(self, xygrid, directions=None):
"""
A helper to get all directions to which there appears to be a
@ -831,27 +837,33 @@ class Map:
# process the new(?) data
self._parse()
def get_node_from_coord(self, X, Y):
def get_node_from_coord(self, coords):
"""
Get a MapNode from a coordinate.
Args:
X (int): X-coordinate on XY (game) grid.
Y (int): Y-coordinate on XY (game) grid.
coords (tuple): X,Y coordinates on XYgrid.
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:
self.parse()
try:
return self.XYgrid[X][Y]
except IndexError:
raise MapError("get_node_from_coord got coordinate ({x},{y}) which is "
iX, iY = coords
if not ((0 <= iX <= self.max_X) and (0 <= iY <= self.max_Y)):
raise MapError("get_node_from_coord got coordinate {coords} which is "
"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):
"""
@ -869,10 +881,10 @@ class Map:
the full path including the start- and end-node.
"""
startnode = self.get_node_from_coord(*startcoord)
endnode = self.get_node_from_coord(*endcoord)
startnode = self.get_node_from_coord(startcoord)
endnode = self.get_node_from_coord(endcoord)
if not self.pathfinding_routes:
if self.pathfinding_routes is None:
self._calculate_path_matrix()
pathfinding_routes = self.pathfinding_routes
@ -898,7 +910,7 @@ class Map:
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):
"""
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.
dist (int, optional): Number of gridpoints distance to show. Which
grid to use depends on the setting of `only_nodes`.
only_nodes (boolean): This determins if `dist` only counts the number of
full nodes or counts the number of actual visual map-grid-points
(including links). If set, it's recommended to set `max_size` to avoid
too-large map displays.
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.
character (str, optional): Place this symbol at the `coord` position
of the displayed map. Ignored if falsy.
max_size (tuple, optional): A max `(width, height)` of the resulting
string or list. This can be useful together with `only_nodes`
to avoid a map display growing unexpectedly. If unset, size
can grow up to the full size of the map.
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.
return_str (bool, optional): Return result as an
already formatted string.
@ -962,48 +972,77 @@ class Map:
# convert inputs to xygrid
width, height = self.max_x + 1, self.max_y + 1
ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height))
display_map = self.display_map
if only_nodes:
# dist measures only full, reachable nodes
if dist <= 0:
# show nothing but ourselves
return character if character else ' '
# we will build a list of coordinates (from the full
# map display) to actually include in the final
if mode == 'nodes':
# 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)]
xmax = 0
ymax = 0
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`
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(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
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
xmax, ymax = max(xmax, ix0), max(ymax, iy0)
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.ix, node0.iy
points.append((ix0, iy0))
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)
# 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:
# dist measures individual grid points
# scan-mode (default) - dist measures individual grid points
if dist is None:
gridmap = self.display_map
ixc, iyc = ix, iy
@ -1011,13 +1050,20 @@ class Map:
left, right = max(0, ix - dist), min(width, ix + dist + 1)
bottom, top = max(0, iy - dist), min(height, iy + dist + 1)
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:
gridmap[iyc][ixc] = character # correct indexing; it's a list of lines
if character:
gridmap[iyc][ixc] = character # correct indexing; it's a list of lines
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]]
# we must flip the y-axis before returning
if return_str:
# we must flip the y-axis before returning the string
return "\n".join("".join(line) for line in gridmap[::-1])
else:
return gridmap

View file

@ -77,7 +77,7 @@ class TestMap1(TestCase):
self.assertEqual(str(self.map).strip(), MAP1_DISPLAY)
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, 2)
self.assertEqual(node.X, 1)
@ -130,6 +130,20 @@ class TestMap1(TestCase):
self.assertEqual(expectstr, mapstr)
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):
"""
@ -148,7 +162,7 @@ class TestMap2(TestCase):
def test_node_from_coord(self):
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.x // 2, node.X)
self.assertEqual(node.y // 2, node.Y)
@ -177,7 +191,7 @@ class TestMap2(TestCase):
((4, 5), '#-#-@ \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.
@ -186,7 +200,11 @@ class TestMap2(TestCase):
self.assertEqual(expected, mapstr)
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(
node.xy_steps_in_direction,
{'e': ['e'],
@ -195,7 +213,11 @@ class TestMap2(TestCase):
)
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(
node.xy_steps_in_direction,
{'n': ['n', 'n', 'n'],
@ -204,3 +226,28 @@ class TestMap2(TestCase):
'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)