Fully functional orthogonal map tested
This commit is contained in:
parent
25a73aee60
commit
30fe0c4b5f
2 changed files with 136 additions and 43 deletions
|
|
@ -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
|
||||||
points.append((ix0, iy0))
|
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:
|
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
|
||||||
|
|
||||||
|
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:
|
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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue