Add caching of pathfinder data
This commit is contained in:
parent
2932beb769
commit
27eca05452
3 changed files with 71 additions and 22 deletions
|
|
@ -81,7 +81,10 @@ See `./example_maps.py` for some empty grid areas to start from.
|
||||||
|
|
||||||
----
|
----
|
||||||
"""
|
"""
|
||||||
|
import pickle
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from os import mkdir
|
||||||
|
from os.path import isdir, isfile, join as pathjoin
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from scipy.sparse.csgraph import dijkstra
|
from scipy.sparse.csgraph import dijkstra
|
||||||
|
|
@ -91,7 +94,11 @@ except ImportError as err:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
f"{err}\nThe MapSystem contrib requires "
|
f"{err}\nThe MapSystem contrib requires "
|
||||||
"the SciPy package. Install with `pip install scipy'.")
|
"the SciPy package. Install with `pip install scipy'.")
|
||||||
|
from django.conf import settings
|
||||||
from evennia.utils.utils import variable_from_module, mod_import
|
from evennia.utils.utils import variable_from_module, mod_import
|
||||||
|
from evennia.utils import logger
|
||||||
|
|
||||||
|
_CACHE_DIR = settings.CACHE_DIR
|
||||||
|
|
||||||
_BIG = 999999999999
|
_BIG = 999999999999
|
||||||
|
|
||||||
|
|
@ -1180,6 +1187,10 @@ class Map:
|
||||||
Y = y // 2
|
Y = y // 2
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
mapstring = ""
|
||||||
|
|
||||||
# store so we can reload
|
# store so we can reload
|
||||||
self.map_module_or_dict = map_module_or_dict
|
self.map_module_or_dict = map_module_or_dict
|
||||||
|
|
||||||
|
|
@ -1197,6 +1208,12 @@ class Map:
|
||||||
self.dist_matrix = None
|
self.dist_matrix = None
|
||||||
self.pathfinding_routes = None
|
self.pathfinding_routes = None
|
||||||
|
|
||||||
|
self.pathfinder_baked_filename = None
|
||||||
|
if name:
|
||||||
|
if not isdir(_CACHE_DIR):
|
||||||
|
mkdir(_CACHE_DIR)
|
||||||
|
self.pathfinder_baked_filename = pathjoin(_CACHE_DIR, f"{name}.P")
|
||||||
|
|
||||||
# load data and parse it
|
# load data and parse it
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
|
|
@ -1262,13 +1279,32 @@ class Map:
|
||||||
|
|
||||||
def _calculate_path_matrix(self):
|
def _calculate_path_matrix(self):
|
||||||
"""
|
"""
|
||||||
Solve the pathfinding problem using Dijkstra's algorithm.
|
Solve the pathfinding problem using Dijkstra's algorithm. This will try to
|
||||||
|
load the solution from disk if possible.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
nnodes = len(self.node_index_map)
|
if self.pathfinder_baked_filename and isfile(self.pathfinder_baked_filename):
|
||||||
|
# check if the solution for this grid was already solved previously.
|
||||||
|
|
||||||
|
mapstr, dist_matrix, pathfinding_routes = "", None, None
|
||||||
|
with open(self.pathfinder_baked_filename, 'rb') as fil:
|
||||||
|
try:
|
||||||
|
mapstr, dist_matrix, pathfinding_routes = pickle.load(fil)
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
if (mapstr == self.mapstring
|
||||||
|
and dist_matrix is not None
|
||||||
|
and pathfinding_routes is not None):
|
||||||
|
# this is important - it means the map hasn't changed so
|
||||||
|
# we can re-use the stored data!
|
||||||
|
self.dist_matrix = dist_matrix
|
||||||
|
self.pathfinding_routes = pathfinding_routes
|
||||||
|
return
|
||||||
|
|
||||||
pathfinding_graph = zeros((nnodes, nnodes))
|
|
||||||
# build a matrix representing the map graph, with 0s as impassable areas
|
# build a matrix representing the map graph, with 0s as impassable areas
|
||||||
|
|
||||||
|
nnodes = len(self.node_index_map)
|
||||||
|
pathfinding_graph = zeros((nnodes, nnodes))
|
||||||
for inode, node in self.node_index_map.items():
|
for inode, node in self.node_index_map.items():
|
||||||
pathfinding_graph[inode, :] = node.linkweights(nnodes)
|
pathfinding_graph[inode, :] = node.linkweights(nnodes)
|
||||||
|
|
||||||
|
|
@ -1280,6 +1316,12 @@ class Map:
|
||||||
pathfinding_matrix, directed=True,
|
pathfinding_matrix, directed=True,
|
||||||
return_predecessors=True, limit=self.max_pathfinding_length)
|
return_predecessors=True, limit=self.max_pathfinding_length)
|
||||||
|
|
||||||
|
if self.pathfinder_baked_filename:
|
||||||
|
# try to cache the results
|
||||||
|
with open(self.pathfinder_baked_filename, 'wb') as fil:
|
||||||
|
pickle.dump((self.mapstring, self.dist_matrix, self.pathfinding_routes),
|
||||||
|
fil, protocol=4)
|
||||||
|
|
||||||
def _parse(self):
|
def _parse(self):
|
||||||
"""
|
"""
|
||||||
Parses the numerical grid from the string. The result of this is a 2D array
|
Parses the numerical grid from the string. The result of this is a 2D array
|
||||||
|
|
@ -1665,7 +1707,8 @@ class Map:
|
||||||
# stylize path to target
|
# stylize path to target
|
||||||
|
|
||||||
def _default_callable(node):
|
def _default_callable(node):
|
||||||
return target_path_style.format(display_symbol=node)
|
return target_path_style.format(
|
||||||
|
display_symbol=node.get_display_symbol(self.xygrid))
|
||||||
|
|
||||||
if callable(target_path_style):
|
if callable(target_path_style):
|
||||||
_target_path_style = target_path_style
|
_target_path_style = target_path_style
|
||||||
|
|
|
||||||
|
|
@ -320,7 +320,7 @@ class TestMap1(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP1})
|
self.map = mapsystem.Map({"map": MAP1}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -404,7 +404,7 @@ class TestMap2(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP2})
|
self.map = mapsystem.Map({"map": MAP2}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -514,7 +514,7 @@ class TestMap3(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP3})
|
self.map = mapsystem.Map({"map": MAP3}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -563,7 +563,7 @@ class TestMap4(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP4})
|
self.map = mapsystem.Map({"map": MAP4}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -593,7 +593,7 @@ class TestMap5(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP5})
|
self.map = mapsystem.Map({"map": MAP5}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -621,7 +621,7 @@ class TestMap6(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP6})
|
self.map = mapsystem.Map({"map": MAP6}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -653,7 +653,7 @@ class TestMap7(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP7})
|
self.map = mapsystem.Map({"map": MAP7}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -681,7 +681,7 @@ class TestMap8(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP8})
|
self.map = mapsystem.Map({"map": MAP8}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -747,7 +747,7 @@ class TestMap9(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP9})
|
self.map = mapsystem.Map({"map": MAP9}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -776,7 +776,7 @@ class TestMap10(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP10})
|
self.map = mapsystem.Map({"map": MAP10}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -824,7 +824,7 @@ class TestMap11(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = mapsystem.Map({"map": MAP11})
|
self.map = mapsystem.Map({"map": MAP11}, name="testmap")
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -914,14 +914,14 @@ class TestMapStressTest(TestCase):
|
||||||
grid = self._get_grid(Xmax, Ymax)
|
grid = self._get_grid(Xmax, Ymax)
|
||||||
# print(f"\n\n{grid}\n")
|
# print(f"\n\n{grid}\n")
|
||||||
t0 = time()
|
t0 = time()
|
||||||
mapsystem.Map({'map': grid})
|
mapsystem.Map({'map': grid}, name="testmap")
|
||||||
t1 = time()
|
t1 = time()
|
||||||
self.assertLess(t1 - t0, max_time, f"Map creation of ({Xmax}x{Ymax}) grid slower "
|
self.assertLess(t1 - t0, max_time, f"Map creation of ({Xmax}x{Ymax}) grid slower "
|
||||||
f"than expected {max_time}s.")
|
f"than expected {max_time}s.")
|
||||||
|
|
||||||
@parameterized.expand([
|
@parameterized.expand([
|
||||||
((10, 10), 10**-4),
|
((10, 10), 10**-3),
|
||||||
((20, 20), 10**-4),
|
((20, 20), 10**-3),
|
||||||
])
|
])
|
||||||
def test_grid_pathfind(self, gridsize, max_time):
|
def test_grid_pathfind(self, gridsize, max_time):
|
||||||
"""
|
"""
|
||||||
|
|
@ -930,7 +930,7 @@ class TestMapStressTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Xmax, Ymax = gridsize
|
Xmax, Ymax = gridsize
|
||||||
grid = self._get_grid(Xmax, Ymax)
|
grid = self._get_grid(Xmax, Ymax)
|
||||||
mapobj = mapsystem.Map({'map': grid})
|
mapobj = mapsystem.Map({'map': grid}, name="testmap")
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
mapobj._calculate_path_matrix()
|
mapobj._calculate_path_matrix()
|
||||||
|
|
@ -962,7 +962,7 @@ class TestMapStressTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Xmax, Ymax = gridsize
|
Xmax, Ymax = gridsize
|
||||||
grid = self._get_grid(Xmax, Ymax)
|
grid = self._get_grid(Xmax, Ymax)
|
||||||
mapobj = mapsystem.Map({'map': grid})
|
mapobj = mapsystem.Map({'map': grid}, name="testmap")
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
mapobj._calculate_path_matrix()
|
mapobj._calculate_path_matrix()
|
||||||
|
|
@ -978,8 +978,11 @@ class TestMapStressTest(TestCase):
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
for coord, target in start_end_points:
|
for coord, target in start_end_points:
|
||||||
mapobj.get_visual_range(coord, dist=dist, mode='nodes', character='@', target=target)
|
mapobj.get_visual_range(coord, dist=dist, mode='nodes',
|
||||||
|
character='@', target=target)
|
||||||
t1 = time()
|
t1 = time()
|
||||||
self.assertLess((t1 - t0) / 10, max_time,
|
self.assertLess((t1 - t0) / 10, max_time,
|
||||||
f"Visual Range calculation for ({Xmax}x{Ymax}) grid "
|
f"Visual Range calculation for ({Xmax}x{Ymax}) grid "
|
||||||
f"slower than expected {max_time}s.")
|
f"slower than expected {max_time}s.")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,6 @@ else:
|
||||||
GAME_DIR = gpath
|
GAME_DIR = gpath
|
||||||
break
|
break
|
||||||
os.chdir(os.pardir)
|
os.chdir(os.pardir)
|
||||||
|
|
||||||
# Place to put log files, how often to rotate the log and how big each log file
|
# Place to put log files, how often to rotate the log and how big each log file
|
||||||
# may become before rotating.
|
# may become before rotating.
|
||||||
LOG_DIR = os.path.join(GAME_DIR, "server", "logs")
|
LOG_DIR = os.path.join(GAME_DIR, "server", "logs")
|
||||||
|
|
@ -152,6 +151,10 @@ LOCKWARNING_LOG_FILE = os.path.join(LOG_DIR, "lockwarnings.log")
|
||||||
CHANNEL_LOG_NUM_TAIL_LINES = 20
|
CHANNEL_LOG_NUM_TAIL_LINES = 20
|
||||||
# Max size (in bytes) of channel log files before they rotate
|
# Max size (in bytes) of channel log files before they rotate
|
||||||
CHANNEL_LOG_ROTATE_SIZE = 1000000
|
CHANNEL_LOG_ROTATE_SIZE = 1000000
|
||||||
|
# Unused by default, but used by e.g. the MapSystem contrib. A place for storing
|
||||||
|
# semi-permanent data and avoid it being rebuilt over and over. It is created
|
||||||
|
# on-demand only.
|
||||||
|
CACHE_DIR = os.path.join(GAME_DIR, "server", ".cache")
|
||||||
# Local time zone for this installation. All choices can be found here:
|
# Local time zone for this installation. All choices can be found here:
|
||||||
# http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
|
# http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
|
||||||
TIME_ZONE = "UTC"
|
TIME_ZONE = "UTC"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue