Fix tests, looking at expanding map-area display
This commit is contained in:
parent
0893b7b80b
commit
0458c9e308
2 changed files with 210 additions and 61 deletions
|
|
@ -74,14 +74,18 @@ See `./example_maps.py` for some empty grid areas to start from.
|
||||||
----
|
----
|
||||||
"""
|
"""
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from scipy.sparse.csgraph import dijkstra
|
|
||||||
from scipy.sparse import csr_matrix
|
try:
|
||||||
from scipy import zeros
|
from scipy.sparse.csgraph import dijkstra
|
||||||
|
from scipy.sparse import csr_matrix
|
||||||
|
from scipy import zeros
|
||||||
|
except ImportError as err:
|
||||||
|
raise ImportError(
|
||||||
|
f"{err}\nThe MapSystem contrib requires "
|
||||||
|
"the SciPy package. Install with `pip install scipy'.")
|
||||||
from evennia.utils.utils import variable_from_module, mod_import
|
from evennia.utils.utils import variable_from_module, mod_import
|
||||||
|
|
||||||
|
|
||||||
_BIG = 999999999999
|
|
||||||
|
|
||||||
_REVERSE_DIRECTIONS = {
|
_REVERSE_DIRECTIONS = {
|
||||||
"n": "s",
|
"n": "s",
|
||||||
"ne": "sw",
|
"ne": "sw",
|
||||||
|
|
@ -104,6 +108,8 @@ _MAPSCAN = {
|
||||||
"nw": (1, -1)
|
"nw": (1, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_BIG = 999999999999
|
||||||
|
|
||||||
|
|
||||||
class MapError(RuntimeError):
|
class MapError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
@ -114,11 +120,9 @@ class MapParserError(MapError):
|
||||||
|
|
||||||
class MapNode:
|
class MapNode:
|
||||||
"""
|
"""
|
||||||
This represents a 'room' node on the map.
|
This represents a 'room' node on the map. MapNodes are always located
|
||||||
|
on even x,y coordinates on the on-map xygrid and represents specific coordinates
|
||||||
A node is always located at an (int, int) location
|
on the in-game XYgrid.
|
||||||
on the map, even if it actually represents a throughput
|
|
||||||
to another node.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# symbol used in map definition
|
# symbol used in map definition
|
||||||
|
|
@ -162,10 +166,13 @@ class MapNode:
|
||||||
# lowest direction to a given neighbor
|
# lowest direction to a given neighbor
|
||||||
self.cheapest_to_node = {}
|
self.cheapest_to_node = {}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"<MapNode {self.node_index} XY=({self.X},{self.Y}) ({self.symbol})>"
|
||||||
|
|
||||||
def build_links(self, xygrid):
|
def build_links(self, xygrid):
|
||||||
"""
|
"""
|
||||||
Start tracking links in all cardinal directions to
|
Start tracking links in all cardinal directions to tie this to another node. All
|
||||||
tie this to another node.
|
links are placed on the xygrid since they never have an in-game representation.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
xygrid (dict): A 2d dict-of-dicts with x,y coordinates as keys and nodes as values.
|
xygrid (dict): A 2d dict-of-dicts with x,y coordinates as keys and nodes as values.
|
||||||
|
|
@ -284,6 +291,9 @@ class MapLink:
|
||||||
if not self.display_symbol:
|
if not self.display_symbol:
|
||||||
self.display_symbol = self.symbol
|
self.display_symbol = self.symbol
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"<LinkNode xy=({self.x},{self.y}) ({self.symbol})>"
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -596,7 +606,6 @@ class Map:
|
||||||
|
|
||||||
# Dijkstra algorithm variables
|
# Dijkstra algorithm variables
|
||||||
self.node_index_map = None
|
self.node_index_map = None
|
||||||
self.pathfinding_matrix = None
|
|
||||||
self.dist_matrix = None
|
self.dist_matrix = None
|
||||||
self.pathfinding_routes = None
|
self.pathfinding_routes = None
|
||||||
|
|
||||||
|
|
@ -604,7 +613,16 @@ class Map:
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "\n".join("".join(line) for line in self.display_map)
|
"""
|
||||||
|
Print the string representation of the map.
|
||||||
|
Since the y-axes origo is at the bottom, we must flip the
|
||||||
|
y-axis before printing (since printing is always top-to-bottom).
|
||||||
|
|
||||||
|
"""
|
||||||
|
return "\n".join("".join(line) for line in self.display_map[::-1])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Map {self.max_X}x{self.max_Y}, {len(self.node_index_map)} nodes>"
|
||||||
|
|
||||||
def _get_node_from_coord(self, X, Y):
|
def _get_node_from_coord(self, X, Y):
|
||||||
"""
|
"""
|
||||||
|
|
@ -626,7 +644,7 @@ class Map:
|
||||||
return self.XYgrid[X][Y]
|
return self.XYgrid[X][Y]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise MapError("_get_node_from_coord got coordinate ({x},{y}) which is "
|
raise MapError("_get_node_from_coord got coordinate ({x},{y}) which is "
|
||||||
"outside the grid size of (0,0) - ({self.width}, {self.height}).")
|
"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).")
|
||||||
|
|
||||||
def _calculate_path_matrix(self):
|
def _calculate_path_matrix(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -827,11 +845,12 @@ class Map:
|
||||||
we want to find the shortest route to.
|
we want to find the shortest route to.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: Two lists, first one containing the shortest sequence of map nodes to
|
tuple: Two lists, first containing the list of directions as strings (n, ne etc) and
|
||||||
traverse and the second a list of directions (n, se etc) describing the path.
|
the second is a mixed list of MapNodes and string-directions in a sequence describing
|
||||||
|
the full path including the start- and end-node.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
istartnode = self._get_node_from_coord(*startcoord).node_index
|
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 not self.pathfinding_routes:
|
||||||
|
|
@ -840,34 +859,45 @@ class Map:
|
||||||
pathfinding_routes = self.pathfinding_routes
|
pathfinding_routes = self.pathfinding_routes
|
||||||
node_index_map = self.node_index_map
|
node_index_map = self.node_index_map
|
||||||
|
|
||||||
nodepath = [endnode]
|
path = [endnode]
|
||||||
linkpath = []
|
directions = []
|
||||||
|
istartnode = startnode.node_index
|
||||||
inextnode = endnode.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 end-node
|
# the -9999 is set by algorithm for unreachable nodes or if trying
|
||||||
|
# to go a node we are already at (the start node in this case since
|
||||||
|
# we are working backwards).
|
||||||
inextnode = pathfinding_routes[istartnode, inextnode]
|
inextnode = pathfinding_routes[istartnode, inextnode]
|
||||||
nodepath.append(node_index_map[inextnode])
|
nextnode = node_index_map[inextnode]
|
||||||
linkpath.append(nodepath[-1].get_cheapest_link_to(nodepath[-2]))
|
directions.append(nextnode.get_cheapest_link_to(path[-1]))
|
||||||
|
path.extend((directions[-1], nextnode))
|
||||||
|
|
||||||
# we have the path - reverse to get the correct order
|
# we have the path - reverse to get the correct order
|
||||||
nodepath = nodepath[::-1]
|
path = path[::-1]
|
||||||
linkpath = linkpath[::-1]
|
directions = directions[::-1]
|
||||||
|
|
||||||
return nodepath, linkpath
|
return directions, path
|
||||||
|
|
||||||
def get_map_display(self, coord, dist=2, character='@', return_str=True):
|
def get_map_display(self, coord, dist=2, only_nodes=False,
|
||||||
|
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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
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.
|
dist (int, optional): Number of gridpoints distance to show. Which
|
||||||
A value of 2 will show adjacent nodes, a value
|
grid to use depends on the setting of `only_nodes`.
|
||||||
of 1 will only show links from current node. If this is None,
|
only_nodes (boolean): This determins if `dist` only counts the number of
|
||||||
show entire map centered on iX,iY.
|
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.
|
||||||
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
|
||||||
|
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.
|
||||||
return_str (bool, optional): Return result as an
|
return_str (bool, optional): Return result as an
|
||||||
already formatted string.
|
already formatted string.
|
||||||
|
|
||||||
|
|
@ -878,12 +908,72 @@ class Map:
|
||||||
extract a character at (ix,iy) coordinate from it, use
|
extract a character at (ix,iy) coordinate from it, use
|
||||||
indexing `outlist[iy][ix]` in that order.
|
indexing `outlist[iy][ix]` in that order.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If outputting an output list, the y-axis must first be
|
||||||
|
reversed since printing happens top-bottom and the y coordinate
|
||||||
|
system goes bottom-up. This can be done simply with
|
||||||
|
|
||||||
|
reversed = outlist[::-1]
|
||||||
|
|
||||||
|
before starting the printout loop.
|
||||||
|
|
||||||
|
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
|
||||||
|
and vertically). consider setting `max_size` if wanting to restrict the display size.
|
||||||
|
also note that link 'weights' are *included* in this estimate, so
|
||||||
|
if links have weights > 1, fewer nodes will be found for a given `dist`.
|
||||||
|
|
||||||
|
If `only_nodes` is False, dist of 2 would give
|
||||||
|
|
||||||
|
#-@--
|
||||||
|
|
||||||
|
This is more of a 'moving' overview type of map that just displays a part of the grid
|
||||||
|
you are on. It does not consider links or weights and may also show nodes not
|
||||||
|
actually reachable at the moment:
|
||||||
|
|
||||||
|
| |
|
||||||
|
# @-#
|
||||||
|
|
||||||
"""
|
"""
|
||||||
iX, iY = coord
|
iX, iY = coord
|
||||||
# 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))
|
||||||
|
|
||||||
|
if only_nodes:
|
||||||
|
# dist measures only full, reachable nodes
|
||||||
|
|
||||||
|
# we will build a list of coordinates (from the full
|
||||||
|
# map display) to actually include in the final
|
||||||
|
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)
|
||||||
|
|
||||||
|
# follow directions to figure out which map coords to display
|
||||||
|
ix0, iy0 = ix, iy
|
||||||
|
# for path_element in path:
|
||||||
|
# dx, dy = _MAPSCAN[direction]
|
||||||
|
# ix0, iy0 = ix0 + dx, iy0 + dy
|
||||||
|
# xmax, ymax = max(xmax, ix0), max(ymax, iy0)
|
||||||
|
# points.append((ix0, iy0))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 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
|
||||||
|
|
@ -896,7 +986,8 @@ class Map:
|
||||||
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 return_str:
|
if return_str:
|
||||||
return "\n".join("".join(line) for line in gridmap)
|
return "\n".join("".join(line) for line in gridmap[::-1])
|
||||||
else:
|
else:
|
||||||
return gridmap
|
return gridmap
|
||||||
|
|
|
||||||
|
|
@ -84,35 +84,51 @@ class TestMap1(TestCase):
|
||||||
self.assertEqual(node.y, 2)
|
self.assertEqual(node.y, 2)
|
||||||
|
|
||||||
def test_get_shortest_path(self):
|
def test_get_shortest_path(self):
|
||||||
nodepath, linkpath = self.map.get_shortest_path((0, 0), (1, 1))
|
directions, path = self.map.get_shortest_path((0, 0), (1, 1))
|
||||||
self.assertEqual([node.node_index for node in nodepath], [0, 1, 3])
|
self.assertEqual(directions, ['e', 'n'])
|
||||||
self.assertEqual(linkpath, ['e', 'n'])
|
self.assertEqual(
|
||||||
|
[str(node) for node in path],
|
||||||
|
[str(self.map.node_index_map[0]),
|
||||||
|
'e',
|
||||||
|
str(self.map.node_index_map[1]),
|
||||||
|
'n',
|
||||||
|
str(self.map.node_index_map[3])]
|
||||||
|
)
|
||||||
|
|
||||||
@parameterized.expand([
|
@parameterized.expand([
|
||||||
((0, 0), "#-\n| ", [["#", "-"], ["|", " "]]),
|
((0, 0), "| \n#-", [["|", " "], ["#", "-"]]),
|
||||||
((1, 0), "-#\n |", [["-", "#"], [" ", "|"]]),
|
((1, 0), " |\n-#", [[" ", "|"], ["-", "#"]]),
|
||||||
((0, 1), "| \n#-", [["|", " "], ["#", "-"]]),
|
((0, 1), "#-\n| ", [["#", "-"], ["|", " "]]),
|
||||||
((1, 1), " |\n-#", [[" ", "|"], ["-", "#"]]),
|
((1, 1), "-#\n |", [["-", "#"], [" ", "|"]]),
|
||||||
|
|
||||||
])
|
])
|
||||||
def test_get_map_display(self, coord, expectstr, expectlst):
|
def test_get_map_display(self, coord, expectstr, expectlst):
|
||||||
string = self.map.get_map_display(coord, dist=1, character=None)
|
"""
|
||||||
lst = self.map.get_map_display(coord, dist=1, return_str=False, character=None)
|
Test displaying a part of the map around a central point.
|
||||||
self.assertEqual(string, expectstr)
|
|
||||||
self.assertEqual(lst, expectlst)
|
"""
|
||||||
|
mapstr = self.map.get_map_display(coord, dist=1, character=None)
|
||||||
|
maplst = self.map.get_map_display(coord, dist=1, return_str=False, character=None)
|
||||||
|
self.assertEqual(expectstr, mapstr)
|
||||||
|
self.assertEqual(expectlst, maplst[::-1])
|
||||||
|
|
||||||
@parameterized.expand([
|
@parameterized.expand([
|
||||||
((0, 0), "@-\n| ", [["@", "-"], ["|", " "]]),
|
((0, 0), "| \n@-", [["|", " "], ["@", "-"]]),
|
||||||
((1, 0), "-@\n |", [["-", "@"], [" ", "|"]]),
|
((1, 0), " |\n-@", [[" ", "|"], ["-", "@"]]),
|
||||||
((0, 1), "| \n@-", [["|", " "], ["@", "-"]]),
|
((0, 1), "@-\n| ", [["@", "-"], ["|", " "]]),
|
||||||
((1, 1), " |\n-@", [[" ", "|"], ["-", "@"]]),
|
((1, 1), "-@\n |", [["-", "@"], [" ", "|"]]),
|
||||||
|
|
||||||
])
|
])
|
||||||
def test_get_map_display__character(self, coord, expectstr, expectlst):
|
def test_get_map_display__character(self, coord, expectstr, expectlst):
|
||||||
string = self.map.get_map_display(coord, dist=1, character='@')
|
"""
|
||||||
lst = self.map.get_map_display(coord, dist=1, return_str=False, character='@')
|
Test displaying a part of the map around a central point, showing the
|
||||||
self.assertEqual(string, expectstr)
|
character @-symbol in that spot.
|
||||||
self.assertEqual(lst, expectlst)
|
|
||||||
|
"""
|
||||||
|
mapstr = self.map.get_map_display(coord, dist=1, character='@')
|
||||||
|
maplst = self.map.get_map_display(coord, dist=1, return_str=False, character='@')
|
||||||
|
self.assertEqual(expectstr, mapstr)
|
||||||
|
self.assertEqual(expectlst, maplst[::-1]) # flip y-axis to match print direction
|
||||||
|
|
||||||
|
|
||||||
class TestMap2(TestCase):
|
class TestMap2(TestCase):
|
||||||
|
|
@ -125,4 +141,46 @@ class TestMap2(TestCase):
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
self.assertEqual(str(self.map).strip(), MAP2_DISPLAY)
|
# strip the leftover spaces on the right to better
|
||||||
|
# work with text editor stripping this automatically ...
|
||||||
|
stripped_map = "\n".join(line.rstrip() for line in str(self.map).split('\n'))
|
||||||
|
self.assertEqual(stripped_map, MAP2_DISPLAY)
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.assertEqual(node, mapnode)
|
||||||
|
self.assertEqual(node.x // 2, node.X)
|
||||||
|
self.assertEqual(node.y // 2, node.Y)
|
||||||
|
|
||||||
|
@parameterized.expand([
|
||||||
|
((1, 0), (4, 0), ('e', 'e', 'e')), # straight path
|
||||||
|
((1, 0), (5, 1), ('n', 'e', 'e', 'e')), # shortcut over long link
|
||||||
|
((2, 2), (2, 5), ('n', 'n')), # shortcut over long link (vertical)
|
||||||
|
((4, 4), (0, 5), ('w', 'n', 'w', 'w')), # shortcut over long link (vertical)
|
||||||
|
((4, 0), (0, 5), ('n', 'w', 'n', 'n', 'n', 'w', 'w')), # across entire grid
|
||||||
|
((4, 0), (0, 5), ('n', 'w', 'n', 'n', 'n', 'w', 'w')), # across entire grid
|
||||||
|
((5, 3), (0, 3), ('s', 'w', 'w', 'w', 'w', 'n')), # down and back
|
||||||
|
])
|
||||||
|
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([
|
||||||
|
((1, 0), '#-#-#-#\n| | \n#-#-#--\n | \n @-#-#'),
|
||||||
|
((2, 2), ' #---#\n | |\n# | #\n| | \n#-#-@-#--\n| '
|
||||||
|
'| \n#-#-#---#\n | |\n #-#-#-#'),
|
||||||
|
((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):
|
||||||
|
"""
|
||||||
|
Test showing smaller part of grid, showing @-character in the middle.
|
||||||
|
|
||||||
|
"""
|
||||||
|
mapstr = self.map.get_map_display(coord, dist=4, character='@')
|
||||||
|
self.assertEqual(expected, mapstr)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue