Add stress-tests, start adding map-transitions

This commit is contained in:
Griatch 2021-06-12 16:50:52 +02:00
parent 686c17c4a1
commit 2932beb769
2 changed files with 184 additions and 14 deletions

View file

@ -668,7 +668,7 @@ class TeleporterMapLink(MapLink):
"""
The teleport link works by connecting to nowhere - and will then continue
on another teleport link with the same symbol elsewhere on the map. The teleport
must connect in only one direction, and only to another Link.
symbol must connect to only one other link (not to a node).
For this to work, there must be exactly one other teleport with the same `.symbol` on the map.
The two teleports will always operate as two-way connections, but by making the 'out-link' on
@ -683,14 +683,15 @@ class TeleporterMapLink(MapLink):
-#-t t># - one-way teleport from left to right.
#t - invalid, may only connect to another link
-#t - invalid, may only connect to another link
#-t-# - invalid, only one connected link is allowed.
-#-t-# - invalid, only one connected link is allowed.
"""
symbol = 't'
# usually invisible
display_symbol = ' '
direction_name = 'teleport'
def __init__(self, *args):
super().__init__(*args)
@ -756,19 +757,78 @@ class TeleporterMapLink(MapLink):
# the string 'teleport' will not be understood by the traverser, leading to
# this being interpreted as an empty target and the `at_empty_target`
# hook firing when trying to traverse this link.
if start_direction == 'teleport':
direction_name = self.direction_name
if start_direction == direction_name:
# called while traversing another teleport
# - we must make sure we can always access/leave the teleport.
self.directions = {"teleport": direction,
direction: "teleport"}
self.directions = {direction_name: direction,
direction: direction_name}
else:
# called while traversing a normal link
self.directions = {start_direction: "teleport",
"teleport": direction}
self.directions = {start_direction: direction_name,
direction_name: direction}
return self.directions.get(start_direction)
class MapTransitionLink(TeleporterMapLink):
"""
This link teleports the user to another map and lets them continue moving
from there. Like the TeleporterMapLink, the map-transition symbol must connect to only one other
link (not directly to a node).
The other map will be scanned for a matching `.symbol` that must also be a MapTransitionLink.
The link is always two-way, but the link connecting to the transition can be one-way to create
a one-way transition. Make new links with different symbols (like A, B, C, ...) to link
multiple maps together.
Note that unlike for teleports, pathfinding will *not* work across the map-transition.
Examples:
::
map1 map2
T
/ T-# - movement to the transition-link will continue on the other map.
-#
T
/
-# T># - one-way link from map1 to map2
-#t - invalid, may only connect to another link
-#-t-# - invalid, only one connected link is allowed.
"""
symbol = 'T'
display_symbol = ' '
direction_name = 'transition'
interrupt_path = True
map1_name = 'map'
map2_name = 'map'
def __init__(self, *args):
super().__init__(*args)
self.map1 = None
self.map2 = None
def at_empty_target(self, start_direction, end_direction, xygrid):
"""
This is called by .traverse when it finds this link pointing to nowhere.
Args:
start_direction (str): The direction (n, ne etc) from which
this traversal originates for this link.
end_direction (str): The direction found from `get_direction` earlier.
xygrid (dict): 2D dict with x,y coordinates as keys.
"""
# TODO - this needs some higher-level handler to work.
class SmartMapLink(MapLink):
"""
A 'smart' link withot visible direction, but which uses its topological surroundings
@ -1008,7 +1068,6 @@ class InterruptMapLink(InvisibleSmartMapLink):
symbol = "i"
interrupt_path = True
class BlockedMapLink(InvisibleSmartMapLink):
"""
A high-weight (but still passable) link that causes the shortest-path algorithm to consider this
@ -1047,6 +1106,7 @@ DEFAULT_LEGEND = {
"b": BlockedMapLink,
"i": InterruptMapLink,
't': TeleporterMapLink,
'T': MapTransitionLink,
}
# --------------------------------------------
@ -1097,7 +1157,7 @@ class Map:
# we normally only accept one single character for the legend key
legend_key_exceptions = ("\\")
def __init__(self, map_module_or_dict):
def __init__(self, map_module_or_dict, name="map"):
"""
Initialize the map parser by feeding it the map.
@ -1105,6 +1165,9 @@ class Map:
map_module_or_dict (str, module or dict): Path or module pointing to a map. If a dict,
this should be a dict with a key 'map' and optionally a 'legend'
dicts to specify the map structure.
name (str, optional): Unique identifier for this map. Needed if the game uses
more than one map. Used when referencing this map during map transitions,
baking of pathfinding matrices etc.
Notes:
The map deals with two sets of coorinate systems:
@ -1321,7 +1384,6 @@ class Map:
# we have a link at this xygrid position (this is ok everywhere)
xygrid[ix][iy] = mapnode_or_link_class(ix, iy)
# from evennia import set_trace;set_trace()
# second pass: Here we loop over all nodes and have them connect to each other
# via the detected linkages.
for node in node_index_map.values():
@ -1435,8 +1497,6 @@ class Map:
the full path including the start- and end-node.
"""
# from evennia import set_trace;set_trace()
startnode = self.get_node_from_coord(startcoord)
endnode = self.get_node_from_coord(endcoord)

View file

@ -4,11 +4,12 @@ Tests for the Mapsystem
"""
from time import time
from random import randint
from unittest import TestCase
from parameterized import parameterized
from . import mapsystem
MAP1 = """
+ 0 1 2
@ -873,3 +874,112 @@ class TestMap11(TestCase):
character='@',
max_size=max_size)
self.assertEqual(expected, mapstr)
class TestMapStressTest(TestCase):
"""
Performance test of map patfinder and visualizer.
#-#-#-#-#....
|x|x|x|x|
#-#-#-#-#
|x|x|x|x|
#-#-#-#-#
|x|x|x|x|
#-#-#-#-#
...
This should be a good stress-testing scenario because most each internal node has a maxiumum
number of connections and options to consider.
"""
def _get_grid(self, Xsize, Ysize):
edge = f"+ {' ' * Xsize * 2}"
l1 = f"\n {'#-' * Xsize}#"
l2 = f"\n {'|x' * Xsize}|"
return f"{edge}\n{(l1 + l2) * Ysize}{l1}\n\n{edge}"
@parameterized.expand([
((10, 10), 0.01),
((100, 100), 1),
])
def test_grid_creation(self, gridsize, max_time):
"""
Test of grid-creataion performance for Nx, Ny grid.
"""
Xmax, Ymax = gridsize
grid = self._get_grid(Xmax, Ymax)
# print(f"\n\n{grid}\n")
t0 = time()
mapsystem.Map({'map': grid})
t1 = time()
self.assertLess(t1 - t0, max_time, f"Map creation of ({Xmax}x{Ymax}) grid slower "
f"than expected {max_time}s.")
@parameterized.expand([
((10, 10), 10**-4),
((20, 20), 10**-4),
])
def test_grid_pathfind(self, gridsize, max_time):
"""
Test pathfinding performance for Nx, Ny grid.
"""
Xmax, Ymax = gridsize
grid = self._get_grid(Xmax, Ymax)
mapobj = mapsystem.Map({'map': grid})
t0 = time()
mapobj._calculate_path_matrix()
t1 = time()
# print(f"pathfinder matrix for grid {Xmax}x{Ymax}: {t1 - t0}s")
# get the maximum distance and 9 other random points in the grid
start_end_points = [((0, 0), (Xmax-1, Ymax-1))]
for _ in range(9):
start_end_points.append(((randint(0, Xmax), randint(0, Ymax)),
(randint(0, Xmax), randint(0, Ymax))))
t0 = time()
for startcoord, endcoord in start_end_points:
mapobj.get_shortest_path(startcoord, endcoord)
t1 = time()
self.assertLess((t1 - t0) / 10, max_time, f"Pathfinding for ({Xmax}x{Ymax}) grid slower "
f"than expected {max_time}s.")
@parameterized.expand([
((10, 10), 4, 0.01),
((20, 20), 4, 0.01),
])
def test_grid_visibility(self, gridsize, dist, max_time):
"""
Test grid visualization performance for Nx, Ny grid for
different visibility distances.
"""
Xmax, Ymax = gridsize
grid = self._get_grid(Xmax, Ymax)
mapobj = mapsystem.Map({'map': grid})
t0 = time()
mapobj._calculate_path_matrix()
t1 = time()
# print(f"pathfinder matrix for grid {Xmax}x{Ymax}: {t1 - t0}s")
# get random center points in grid and a range of targets to visualize the
# path to
start_end_points = [((0, 0), (Xmax-1, Ymax-1))] # include max distance
for _ in range(9):
start_end_points.append(((randint(0, Xmax), randint(0, Ymax)),
(randint(0, Xmax), randint(0, Ymax))))
t0 = time()
for coord, target in start_end_points:
mapobj.get_visual_range(coord, dist=dist, mode='nodes', character='@', target=target)
t1 = time()
self.assertLess((t1 - t0) / 10, max_time,
f"Visual Range calculation for ({Xmax}x{Ymax}) grid "
f"slower than expected {max_time}s.")