Made depth-first map display mechanism

This commit is contained in:
Griatch 2021-06-10 17:07:49 +02:00
parent 49aa390cd6
commit c8b5db1b25
2 changed files with 177 additions and 125 deletions

View file

@ -170,7 +170,7 @@ class MapNode:
# This catches eventual longer link chains that would otherwise be lost
# {startdirection: [direction, ...], ...}
# where the directional path-lists also include the start-direction
self.xy_steps_in_direction = {}
self.xy_steps_to_node = {}
def __str__(self):
return f"<MapNode '{self.symbol}' {self.node_index} XY=({round(self.X)},{round(self.Y)})"
@ -207,12 +207,13 @@ class MapNode:
if end_node:
# the link could be followed to an end node!
node_index = end_node.node_index
self.links[direction] = end_node
self.weights[node_index] = weight
self.links[direction] = end_node
# this is useful for map building later - there could be multiple
# links tied together until getting to the node
self.xy_steps_to_node[direction] = steps
# this is useful for map building later
self.xy_steps_in_direction[direction] = steps
# used for building the shortest path
cheapest = self.cheapest_to_node.get(node_index, ("", _BIG))[1]
if weight < cheapest:
self.cheapest_to_node[node_index] = (direction, weight)
@ -253,18 +254,16 @@ class MapNode:
class MapLink:
"""
This represents a link between up to 8 nodes. A link is always
located on a (.5, .5) location on the map like (1.5, 2.5).
This represents a link between up to 8 nodes. A Link can be placed
on any location in the grid, but when on an integer XY position they
don't represent an actual in-game place but just a link between such
places (nodes).
Each link has a 'weight' from 1...inf, whis indicates how 'slow'
Each link has a 'weight' >=1, this indicates how 'slow'
it is to traverse that link. This is used by the Dijkstra algorithm
to find the 'fastest' route to a point. By default this weight is 1
for every link, but a locked door, terrain etc could increase this
and have the algorithm prefer to use another route.
It is usually bidirectional, but could also be one-directional.
It is also possible for a link to have some sort of blockage, like
a door.
and have the shortest-path algorithm prefer to use another route.
"""
# link setup
@ -312,7 +311,7 @@ class MapLink:
def get_visually_connected(self, xygrid, directions=None):
"""
A helper to get all directions to which there appears to be a
visual link/node. This does not trace the link and check weights etc.
visual link/node. This does not trace the length of the link and check weights etc.
Args:
link (MapLink): Currently active link.
@ -620,7 +619,7 @@ class Map:
"""
mapcorner_symbol = '+'
max_pathfinding_length = 1000
max_pathfinding_length = 500
empty_symbol = ' '
# we normally only accept one single character for the legend key
legend_key_exceptions = ("\\")
@ -675,7 +674,57 @@ class Map:
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>"
return f"<Map {self.max_X + 1}x{self.max_Y + 1}, {len(self.node_index_map)} nodes>"
def _get_topology_around_coord(self, coord, dist=2):
"""
Get all links and nodes up to a certain distance from an XY coordinate.
Args:
coord (tuple), the X,Y coordinate of the center point.
dist (int): How many nodes away from center point to find paths for.
Returns:
tuple: A tuple of 5 elements `(coords, xmin, xmax, ymin, ymax)`, where the
first element is a list of xy-coordinates (on xygrid) for all linked nodes within
range. This is meant to be used with the xygrid for extracting a subset
for display purposes. The others are the minimum size of the rectangle
surrounding the area containing `coords`.
Notes:
This performs a depth-first pass down the the given dist.
"""
def _scan_neighbors(start_node, points, dist=2,
xmin=_BIG, ymin=_BIG, xmax=0, ymax=0, depth=0):
x0, y0 = start_node.x, start_node.y
points.append((x0, y0))
xmin, xmax = min(xmin, x0), max(xmax, x0)
ymin, ymax = min(ymin, y0), max(ymax, y0)
if depth < dist:
# keep stepping
for direction, end_node in start_node.links.items():
x, y = x0, y0
for stepdirection in start_node.xy_steps_to_node[direction]:
dx, dy = _MAPSCAN[stepdirection]
x, y = x + dx, y + dy
points.append((x, y))
xmin, xmax = min(xmin, x), max(xmax, x)
ymin, ymax = min(ymin, y), max(ymax, y)
points, xmin, xmax, ymin, ymax = _scan_neighbors(
end_node, points, dist=dist,
xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax,
depth=depth + 1)
return points, xmin, xmax, ymin, ymax
center_node = self.get_node_from_coord(coord)
points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist)
return list(set(points)), xmin, xmax, ymin, ymax
def _calculate_path_matrix(self):
"""
@ -695,7 +744,7 @@ class Map:
# solve using Dijkstra's algorithm
self.dist_matrix, self.pathfinding_routes = dijkstra(
pathfinding_matrix, directed=True,
return_predecessors=True, limit=1000)
return_predecessors=True, limit=self.max_pathfinding_length)
def _parse(self):
"""
@ -958,14 +1007,17 @@ class Map:
Display the map centered on a point and everything around it within a certain distance.
Args:
coord (tuple): (X,Y) in-world coordinate location.
coord (tuple): (X,Y) in-world coordinate location. If this is not the location
of a node on the grid, the `character` or the empty-space symbol (by default
an empty space) will be shown.
dist (int, optional): Number of gridpoints distance to show. Which
grid to use depends on the setting of `only_nodes`.
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.
number of xy grid points in all directions and doesn't care about if visible
nodes are reachable or not. If 'nodes', distance measure how many linked nodes
away from the center coordinate to display.
character (str, optional): Place this symbol at the `coord` position
of the displayed map. Ignored if falsy.
of the displayed map. The center node' symbol is shown if this is falsy.
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.
@ -1013,63 +1065,18 @@ class Map:
ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height))
display_map = self.display_map
if dist <= 0:
# show nothing but ourselves
return character if character else ' '
if dist <= 0 or not self.get_node_from_coord(coord):
# There is no node at these coordinates. Show
# nothing but ourselves or emptiness
return character if character else self.empty_symbol
if mode == 'nodes':
# dist measures only full, reachable nodes.
# this requires a series of shortest-path
# Steps from on the pre-calulcated grid.
# from evennia import set_trace;set_trace()
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)]
node_index_map = self.node_index_map
# 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((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
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.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)
points, xmin, xmax, ymin, ymax = self._get_topology_around_coord(coord, dist=dist)
ixc, iyc = ix - xmin, iy - ymin
# note - override width/height here since our grid is
@ -1079,6 +1086,63 @@ class Map:
for (ix0, iy0) in points:
gridmap[iy0 - ymin][ix0 - xmin] = display_map[iy0][ix0]
# 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)]
# node_index_map = self.node_index_map
#
# # 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((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_to_noden[path_element]:
# dx, dy = _MAPSCAN[dstep]
# ix0, iy0 = ix0 + dx, iy0 + dy
# 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.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)
#
# 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:
# scan-mode (default) - dist measures individual grid points
if dist is None:

View file

@ -62,19 +62,7 @@ MAP2_DISPLAY = """
#-#-#-#
""".strip()
MAP4 = r"""
+ 0 1
1 #-#
|\|
0 #-#
+ 0 1
"""
MAP4 = r"""
MAP3 = r"""
+ 0 1 2 3 4 5
@ -94,7 +82,7 @@ MAP4 = r"""
"""
MAP4_DISPLAY = r"""
MAP3_DISPLAY = r"""
#-#---# #
| / \ /
# / #
@ -108,7 +96,7 @@ MAP4_DISPLAY = r"""
# #---#-#
""".strip()
MAP5 = r"""
MAP4 = r"""
+ 0 1 2 3 4
@ -126,7 +114,7 @@ MAP5 = r"""
"""
MAP5_DISPLAY = r"""
MAP4_DISPLAY = r"""
#-# #---#
x /
#-#-#
@ -138,7 +126,7 @@ MAP5_DISPLAY = r"""
#---#
""".strip()
MAP6 = r"""
MAP5 = r"""
+ 0 1 2
@ -152,7 +140,7 @@ MAP6 = r"""
"""
MAP6_DISPLAY = r"""
MAP5_DISPLAY = r"""
#-#
| |
#>#
@ -160,7 +148,7 @@ MAP6_DISPLAY = r"""
#>#
""".strip()
MAP7 = r"""
MAP6 = r"""
+ 0 1 2 3 4
@ -178,7 +166,7 @@ MAP7 = r"""
"""
MAP7_DISPLAY = r"""
MAP6_DISPLAY = r"""
#-#-#-#
^ |
| #>#
@ -191,7 +179,7 @@ MAP7_DISPLAY = r"""
""".strip()
MAP8 = r"""
MAP7 = r"""
+ 0 1 2
2 #-#
@ -204,7 +192,7 @@ MAP8 = r"""
"""
MAP8_DISPLAY = r"""
MAP7_DISPLAY = r"""
#-#
|
#-o-#
@ -213,7 +201,7 @@ MAP8_DISPLAY = r"""
""".strip()
MAP9 = r"""
MAP8 = r"""
+ 0 1 2 3 4 5
4 #-#-o o o-o
@ -230,7 +218,7 @@ MAP9 = r"""
"""
MAP9_DISPLAY = r"""
MAP8_DISPLAY = r"""
#-#-o o o-o
| \|/| | |
#-o-o-# o-#
@ -315,7 +303,7 @@ class TestMap1(TestCase):
((0, 1), 1, '@-#\n| \n# '),
((1, 0), 1, ' #\n |\n#-@'),
((1, 1), 1, '#-@\n |\n #'),
((0, 0), 2, ''),
((0, 0), 2, '#-#\n| |\n@-#'),
])
def test_get_map_display__nodes__character(self, coord, dist, expected):
@ -387,7 +375,7 @@ class TestMap2(TestCase):
"""
node = self.map.get_node_from_coord((4, 1))
self.assertEqual(
node.xy_steps_in_direction,
node.xy_steps_to_node,
{'e': ['e'],
's': ['s'],
'w': ['w', 'w', 'w']}
@ -400,7 +388,7 @@ class TestMap2(TestCase):
"""
node = self.map.get_node_from_coord((2, 2))
self.assertEqual(
node.xy_steps_in_direction,
node.xy_steps_to_node,
{'n': ['n', 'n', 'n'],
'e': ['e'],
's': ['s'],
@ -434,18 +422,18 @@ class TestMap2(TestCase):
self.assertEqual(expected, mapstr)
class TestMap4(TestCase):
class TestMap3(TestCase):
"""
Test Map4 - Map with diaginal links
Test Map3 - Map with diagonal links
"""
def setUp(self):
self.map = mapsystem.Map({"map": MAP4})
self.map = mapsystem.Map({"map": MAP3})
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(MAP4_DISPLAY, stripped_map)
self.assertEqual(MAP3_DISPLAY, stripped_map)
@parameterized.expand([
((0, 0), (1, 0), ()), # no node at (1, 0)!
@ -472,7 +460,7 @@ class TestMap4(TestCase):
((2, 2), 2, None,
' # \n / \n # / \n |/ \n # #\n \\ / '
'\n # @-# \n |/ \\ \n # #\n / \\ \n# # '),
((5, 2), 2, None, '')
((5, 2), 2, None, ' # \n | \n # \n / \\ \n# @\n \\ / \n # \n | \n # ')
])
def test_get_map_display__nodes__character(self, coord, dist, max_size, expected):
"""
@ -481,21 +469,21 @@ class TestMap4(TestCase):
"""
mapstr = self.map.get_map_display(coord, dist=dist, mode='nodes', character='@',
max_size=max_size)
# print(repr(mapstr))
print(f"\n\n{expected}\n\n{mapstr}\n\n{repr(mapstr)}")
self.assertEqual(expected, mapstr)
class TestMap5(TestCase):
class TestMap4(TestCase):
"""
Test Map5 - Map with + and x crossing links
Test Map4 - Map with + and x crossing links
"""
def setUp(self):
self.map = mapsystem.Map({"map": MAP5})
self.map = mapsystem.Map({"map": MAP4})
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(MAP5_DISPLAY, stripped_map)
self.assertEqual(MAP4_DISPLAY, stripped_map)
@parameterized.expand([
((1, 0), (1, 2), ('n',)), # cross + vertically
@ -514,18 +502,18 @@ class TestMap5(TestCase):
self.assertEqual(expected_directions, tuple(directions))
class TestMap6(TestCase):
class TestMap5(TestCase):
"""
Test Map6 - Small map with one-way links
Test Map5 - Small map with one-way links
"""
def setUp(self):
self.map = mapsystem.Map({"map": MAP6})
self.map = mapsystem.Map({"map": MAP5})
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)
self.assertEqual(MAP5_DISPLAY, stripped_map)
@parameterized.expand([
((0, 0), (1, 0), ('e',)), # cross one-way
@ -542,18 +530,18 @@ class TestMap6(TestCase):
self.assertEqual(expected_directions, tuple(directions))
class TestMap7(TestCase):
class TestMap6(TestCase):
"""
Test Map6 - Bigger map with one-way links in different directions
"""
def setUp(self):
self.map = mapsystem.Map({"map": MAP7})
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(MAP7_DISPLAY, stripped_map)
self.assertEqual(MAP6_DISPLAY, stripped_map)
@parameterized.expand([
((0, 0), (2, 0), ('e', 'e')), # cross one-way
@ -574,18 +562,18 @@ class TestMap7(TestCase):
self.assertEqual(expected_directions, tuple(directions))
class TestMap8(TestCase):
class TestMap7(TestCase):
"""
Test Map6 - Small test of dynamic link node
Test Map7 - Small test of dynamic link node
"""
def setUp(self):
self.map = mapsystem.Map({"map": MAP8})
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(MAP8_DISPLAY, stripped_map)
self.assertEqual(MAP7_DISPLAY, stripped_map)
@parameterized.expand([
((1, 0), (1, 2), ('n', )),
@ -599,21 +587,21 @@ class TestMap8(TestCase):
"""
directions, _ = self.map.get_shortest_path(startcoord, endcoord)
self.assertequal(expected_directions, tuple(directions))
self.assertEqual(expected_directions, tuple(directions))
class TestMap9(TestCase):
class TestMap8(TestCase):
"""
Test Map6 - Small test of dynamic link node
Test Map8 - Small test of dynamic link node
"""
def setUp(self):
self.map = mapsystem.Map({"map": MAP9})
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(MAP9_DISPLAY, stripped_map)
self.assertEqual(MAP8_DISPLAY, stripped_map)
@parameterized.expand([
((2, 0), (2, 2), ('n',)),