Start to build unit tests for grid
This commit is contained in:
parent
bab2f962f5
commit
c6b3749809
4 changed files with 151 additions and 46 deletions
|
|
@ -441,7 +441,7 @@ class MapLink:
|
||||||
next_target = xygrid[end_x][end_y]
|
next_target = xygrid[end_x][end_y]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# check if we have some special action up our sleeve
|
# check if we have some special action up our sleeve
|
||||||
next_target = self.at_empty_target(end_direction, xygrid)
|
next_target = self.at_empty_target(start_direction, end_direction, xygrid)
|
||||||
|
|
||||||
if not next_target:
|
if not next_target:
|
||||||
raise MapParserError(
|
raise MapParserError(
|
||||||
|
|
@ -679,7 +679,7 @@ class TeleporterMapLink(MapLink):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.paired_teleporter = None
|
self.paired_teleporter = None
|
||||||
|
|
||||||
def at_empty_target(self, start_direction, xygrid):
|
def at_empty_target(self, start_direction, end_direction, xygrid):
|
||||||
"""
|
"""
|
||||||
Called during traversal, when finding an unknown direction out of the link (same as
|
Called during traversal, when finding an unknown direction out of the link (same as
|
||||||
targeting a link at an empty spot on the grid). This will also search for
|
targeting a link at an empty spot on the grid). This will also search for
|
||||||
|
|
@ -807,7 +807,12 @@ class MapTransitionLink(TeleporterMapLink):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not self.paired_map_link:
|
if not self.paired_map_link:
|
||||||
grid = self.xymap.grid.grid
|
try:
|
||||||
|
grid = self.xymap.grid.grid
|
||||||
|
except AttributeError:
|
||||||
|
raise MapParserError(f"requires this map being set up within an XYZgrid. No grid "
|
||||||
|
"was found (maybe it was not passed during XYMap initialization?",
|
||||||
|
self)
|
||||||
try:
|
try:
|
||||||
target_map = grid[self.target_map]
|
target_map = grid[self.target_map]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ from time import time
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
from . import xymap
|
from . import xymap, xyzgrid, map_legend
|
||||||
|
|
||||||
|
|
||||||
MAP1 = """
|
MAP1 = """
|
||||||
|
|
||||||
|
|
@ -313,6 +314,31 @@ MAP11_DISPLAY = r"""
|
||||||
#-#
|
#-#
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
MAP12a = r"""
|
||||||
|
|
||||||
|
+ 0 1
|
||||||
|
|
||||||
|
1 #-T
|
||||||
|
|
|
||||||
|
0 #-#
|
||||||
|
|
||||||
|
+ 0 1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
MAP12b = r"""
|
||||||
|
|
||||||
|
+ 0 1
|
||||||
|
|
||||||
|
1 #-#
|
||||||
|
|
|
||||||
|
0 T-#
|
||||||
|
|
||||||
|
+ 0 1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class TestMap1(TestCase):
|
class TestMap1(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
@ -946,7 +972,7 @@ class TestMapStressTest(TestCase):
|
||||||
mapobj.parse()
|
mapobj.parse()
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
mapobj._calculate_path_matrix()
|
mapobj.calculate_path_matrix()
|
||||||
t1 = time()
|
t1 = time()
|
||||||
# print(f"pathfinder matrix for grid {Xmax}x{Ymax}: {t1 - t0}s")
|
# print(f"pathfinder matrix for grid {Xmax}x{Ymax}: {t1 - t0}s")
|
||||||
|
|
||||||
|
|
@ -979,7 +1005,7 @@ class TestMapStressTest(TestCase):
|
||||||
mapobj.parse()
|
mapobj.parse()
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
mapobj._calculate_path_matrix()
|
mapobj.calculate_path_matrix()
|
||||||
t1 = time()
|
t1 = time()
|
||||||
# print(f"pathfinder matrix for grid {Xmax}x{Ymax}: {t1 - t0}s")
|
# print(f"pathfinder matrix for grid {Xmax}x{Ymax}: {t1 - t0}s")
|
||||||
|
|
||||||
|
|
@ -1000,3 +1026,57 @@ class TestMapStressTest(TestCase):
|
||||||
f"slower than expected {max_time}s.")
|
f"slower than expected {max_time}s.")
|
||||||
|
|
||||||
|
|
||||||
|
# map transitions
|
||||||
|
class Map12aTransition(map_legend.MapTransitionLink):
|
||||||
|
symbol = "T"
|
||||||
|
target_map = "map12b"
|
||||||
|
|
||||||
|
|
||||||
|
class Map12bTransition(map_legend.MapTransitionLink):
|
||||||
|
symbol = "T"
|
||||||
|
target_map = "map12a"
|
||||||
|
|
||||||
|
|
||||||
|
class TestXYZGrid(TestCase):
|
||||||
|
"""
|
||||||
|
Test the XYZGrid class and transitions between maps.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
self.grid, err = xyzgrid.XYZGrid.create("testgrid")
|
||||||
|
|
||||||
|
self.map_data12a = {
|
||||||
|
"map": MAP12a,
|
||||||
|
"name": "map12a",
|
||||||
|
"legend": {"T": Map12aTransition}
|
||||||
|
}
|
||||||
|
self.map_data12b = {
|
||||||
|
"map": MAP12b,
|
||||||
|
"name": "map12b",
|
||||||
|
"legend": {"T": Map12bTransition}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
self.grid.add_maps(self.map_data12a, self.map_data12b)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.grid.delete()
|
||||||
|
|
||||||
|
@parameterized.expand([
|
||||||
|
((1, 0), (1, 1), ('e', 'nw', 'e')),
|
||||||
|
((1, 1), (0, 0), ('w', 'se', 'w')),
|
||||||
|
])
|
||||||
|
def test_shortest_path(self, startcoord, endcoord, expected_directions):
|
||||||
|
"""
|
||||||
|
test shortest-path calculations throughout the grid.
|
||||||
|
|
||||||
|
"""
|
||||||
|
directions, _ = self.grid.get('map12a').get_shortest_path(startcoord, endcoord)
|
||||||
|
self.assertEqual(expected_directions, tuple(directions))
|
||||||
|
|
||||||
|
def test_transition(self):
|
||||||
|
"""
|
||||||
|
Test transition.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,7 @@ class XYMap:
|
||||||
points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist)
|
points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist)
|
||||||
return list(set(points)), xmin, xmax, ymin, ymax
|
return list(set(points)), xmin, xmax, ymin, ymax
|
||||||
|
|
||||||
def _calculate_path_matrix(self):
|
def calculate_path_matrix(self):
|
||||||
"""
|
"""
|
||||||
Solve the pathfinding problem using Dijkstra's algorithm. This will try to
|
Solve the pathfinding problem using Dijkstra's algorithm. This will try to
|
||||||
load the solution from disk if possible.
|
load the solution from disk if possible.
|
||||||
|
|
@ -639,7 +639,7 @@ class XYMap:
|
||||||
f"{endnode}. They must both be MapNodes (not Links)")
|
f"{endnode}. They must both be MapNodes (not Links)")
|
||||||
|
|
||||||
if self.pathfinding_routes is None:
|
if self.pathfinding_routes is None:
|
||||||
self._calculate_path_matrix()
|
self.calculate_path_matrix()
|
||||||
|
|
||||||
pathfinding_routes = self.pathfinding_routes
|
pathfinding_routes = self.pathfinding_routes
|
||||||
node_index_map = self.node_index_map
|
node_index_map = self.node_index_map
|
||||||
|
|
|
||||||
|
|
@ -29,65 +29,97 @@ class XYZGrid(DefaultScript):
|
||||||
"""
|
"""
|
||||||
def at_script_creation(self):
|
def at_script_creation(self):
|
||||||
"""
|
"""
|
||||||
What we store persistently is the module-paths to each map.
|
What we store persistently is data used to create each map (the legends, names etc)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.db.map_data = {}
|
self.db.map_data = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def grid(self):
|
||||||
|
if self.ndb.grid is None:
|
||||||
|
self.reload()
|
||||||
|
return self.ndb.grid
|
||||||
|
|
||||||
|
def get(self, mapname, default=None):
|
||||||
|
return self.grid.get(mapname, default)
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
"""
|
"""
|
||||||
Reload the grid. This is done on a server reload and is also necessary if adding a new map
|
Reload and rebuild the grid. This is done on a server reload and is also necessary if adding
|
||||||
since this may introduce new between-map traversals.
|
a new map since this may introduce new between-map traversals.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# build the nodes of each map
|
logger.log_info("[grid] (Re)loading grid ...")
|
||||||
for name, xymap in self.grid:
|
grid = {}
|
||||||
|
nmaps = 0
|
||||||
|
# generate all Maps - this will also initialize their components
|
||||||
|
# and bake any pathfinding paths (or load from disk-cache)
|
||||||
|
for mapname, mapdata in self.db.map_data.items():
|
||||||
|
logger.log_info(f"[grid] Loading map '{mapname}'...")
|
||||||
|
xymap = XYMap(dict(mapdata), name=mapname, grid=self)
|
||||||
xymap.parse_first_pass()
|
xymap.parse_first_pass()
|
||||||
# link everything together
|
grid[mapname] = xymap
|
||||||
for name, xymap in self.grid:
|
nmaps += 1
|
||||||
xymap.parse_second_pass()
|
|
||||||
|
|
||||||
def add_map(self, mapdata, new=True):
|
# link maps together across grid
|
||||||
|
logger.log_info("[grid] Link {nmaps} maps (may be slow first time a map has changed) ...")
|
||||||
|
for name, xymap in grid.items():
|
||||||
|
xymap.parse_second_pass()
|
||||||
|
xymap.calculate_path_matrix()
|
||||||
|
|
||||||
|
# store
|
||||||
|
self.ndb.grid = grid
|
||||||
|
logger.log_info(f"[grid] Loaded and linked {nmaps} map(s).")
|
||||||
|
|
||||||
|
def at_init(self):
|
||||||
"""
|
"""
|
||||||
Add new map to the grid.
|
Called when the script loads into memory (on creation or after a reload). This will load all
|
||||||
|
map data into memory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.reload()
|
||||||
|
|
||||||
|
def add_maps(self, *mapdatas):
|
||||||
|
"""
|
||||||
|
Add map or maps to the grid.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mapdata (dict): A structure `{"map": <mapstr>, "legend": <legenddict>,
|
*mapdatas (dict): Each argument is a dict structure
|
||||||
"name": <name>, "prototypes": <dict-of-dicts>}`. The `prototypes are
|
`{"map": <mapstr>, "legend": <legenddict>, "name": <name>,
|
||||||
|
"prototypes": <dict-of-dicts>}`. The `prototypes are
|
||||||
coordinate-specific overrides for nodes/links on the map, keyed with their
|
coordinate-specific overrides for nodes/links on the map, keyed with their
|
||||||
(X,Y) coordinate (use .5 for link-positions between nodes).
|
(X,Y) coordinate.
|
||||||
new (bool, optional): If the data should be resaved.
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
RuntimeError: If mapdata is malformed.
|
RuntimeError: If mapdata is malformed.
|
||||||
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
This will assume that all added maps produce a complete set (that is, they are correctly
|
||||||
|
and completely linked together with each other and/or with existing maps). So
|
||||||
|
this will automatically trigger `.reload()` to rebuild the grid.
|
||||||
After this, you need to run `.sync_to_grid` to make the new map actually
|
After this, you need to run `.sync_to_grid` to make the new map actually
|
||||||
available in-game.
|
available in-game.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
name = mapdata.get('name')
|
for mapdata in mapdatas:
|
||||||
if not name:
|
name = mapdata.get('name')
|
||||||
raise RuntimeError("XYZGrid.add_map data must contain 'name'.")
|
if not name:
|
||||||
|
raise RuntimeError("XYZGrid.add_map data must contain 'name'.")
|
||||||
|
|
||||||
# this will raise MapErrors if there are issues with the map
|
|
||||||
self.grid[name] = XYMap(mapdata, name=name, grid=self)
|
|
||||||
if new:
|
|
||||||
self.db.map_data[name] = mapdata
|
self.db.map_data[name] = mapdata
|
||||||
|
|
||||||
def remove_map(self, zcoord, remove_objects=False):
|
def remove_map(self, mapname, remove_objects=False):
|
||||||
"""
|
"""
|
||||||
Remove a map from the grid.
|
Remove a map from the grid.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): The map to remove.
|
mapname (str): The map to remove.
|
||||||
remove_objects (bool, optional): If the synced database objects (rooms/exits) should
|
remove_objects (bool, optional): If the synced database objects (rooms/exits) should
|
||||||
be removed alongside this map.
|
be removed alongside this map.
|
||||||
"""
|
"""
|
||||||
if zcoord in self.grid:
|
if mapname in self.db.map_data:
|
||||||
self.db.map_data.pop(zcoord)
|
self.db.map_data.pop(zcoord)
|
||||||
self.grid.pop(zcoord)
|
self.reload()
|
||||||
|
|
||||||
if remove_objects:
|
if remove_objects:
|
||||||
pass
|
pass
|
||||||
|
|
@ -116,7 +148,7 @@ class XYZGrid(DefaultScript):
|
||||||
|
|
||||||
if z is None:
|
if z is None:
|
||||||
xymaps = self.grid
|
xymaps = self.grid
|
||||||
elif z in self.grid:
|
elif z in self.ndb.grid:
|
||||||
xymaps = [self.grid[z]]
|
xymaps = [self.grid[z]]
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f"The 'z' coordinate/name '{z}' is not found on the grid.")
|
raise RuntimeError(f"The 'z' coordinate/name '{z}' is not found on the grid.")
|
||||||
|
|
@ -132,15 +164,3 @@ class XYZGrid(DefaultScript):
|
||||||
for node in synced:
|
for node in synced:
|
||||||
node.sync_links_to_grid()
|
node.sync_links_to_grid()
|
||||||
|
|
||||||
def at_init(self):
|
|
||||||
"""
|
|
||||||
Called when the script loads into memory after a reload. This will load all map data into
|
|
||||||
memory.
|
|
||||||
|
|
||||||
"""
|
|
||||||
nmaps = 0
|
|
||||||
for mapname, mapdata in self.db.map_data:
|
|
||||||
self.add_map(mapdata, new=False)
|
|
||||||
nmaps += 1
|
|
||||||
self.reload()
|
|
||||||
logger.log_info(f"Loaded {nmaps} map(s) onto the grid.")
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue