Tested first unittests with map and pathfinding
This commit is contained in:
parent
b641d0ce71
commit
97865ed4d9
2 changed files with 101 additions and 54 deletions
|
|
@ -85,14 +85,14 @@ _REVERSE_DIRECTIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
_MAPSCAN = {
|
_MAPSCAN = {
|
||||||
"n": (0, 1),
|
"n": (0, -1),
|
||||||
"ne": (1, 1),
|
"ne": (1, -1),
|
||||||
"e": (1, 0),
|
"e": (1, 0),
|
||||||
"se": (1, -1),
|
"se": (1, 1),
|
||||||
"s": (0, -1),
|
"s": (0, 1),
|
||||||
"sw": (-1, -1),
|
"sw": (-1, 1),
|
||||||
"w": (-1, 0),
|
"w": (-1, 0),
|
||||||
"nw": (-1, 1)
|
"nw": (-1, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -118,6 +118,9 @@ class MapNode:
|
||||||
# to the empty string, use `symbol`.
|
# to the empty string, use `symbol`.
|
||||||
display_symbol = ''
|
display_symbol = ''
|
||||||
|
|
||||||
|
# set during generation, but is also used for identification of the node
|
||||||
|
node_index = None
|
||||||
|
|
||||||
def __init__(self, x, y, node_index):
|
def __init__(self, x, y, node_index):
|
||||||
"""
|
"""
|
||||||
Initialize the mapnode.
|
Initialize the mapnode.
|
||||||
|
|
@ -170,15 +173,16 @@ class MapNode:
|
||||||
# just because there is a link here, doesn't mean it's
|
# just because there is a link here, doesn't mean it's
|
||||||
# connected to this node. If so the `end_node` will be None.
|
# connected to this node. If so the `end_node` will be None.
|
||||||
|
|
||||||
end_node, weight = link.traverse(self, _REVERSE_DIRECTIONS(direction), string_map)
|
end_node, weight = link.traverse(_REVERSE_DIRECTIONS[direction], string_map)
|
||||||
if end_node:
|
if end_node:
|
||||||
# the link could be followed to an end node!
|
# the link could be followed to an end node!
|
||||||
self.links[direction] = end_node
|
|
||||||
self.weights[direction] = weight
|
|
||||||
node_index = end_node.node_index
|
node_index = end_node.node_index
|
||||||
|
self.links[direction] = end_node
|
||||||
|
self.weights[node_index] = weight
|
||||||
|
|
||||||
if weight < self.cheapest_to_node.get(node_index, _BIG):
|
cheapest = self.cheapest_to_node.get(node_index, ("", _BIG))[1]
|
||||||
self.cheapest_to_node[node_index] = direction
|
if weight < cheapest:
|
||||||
|
self.cheapest_to_node[node_index] = (direction, weight)
|
||||||
|
|
||||||
def linkweights(self, nnodes):
|
def linkweights(self, nnodes):
|
||||||
"""
|
"""
|
||||||
|
|
@ -211,7 +215,7 @@ class MapNode:
|
||||||
str: The direction (nw, se etc) to get to that node in the cheapest way.
|
str: The direction (nw, se etc) to get to that node in the cheapest way.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.cheapest_to_node[node.node_index]
|
return self.cheapest_to_node[node.node_index][0]
|
||||||
|
|
||||||
|
|
||||||
class MapLink:
|
class MapLink:
|
||||||
|
|
@ -286,7 +290,7 @@ class MapLink:
|
||||||
directions = _REVERSE_DIRECTIONS
|
directions = _REVERSE_DIRECTIONS
|
||||||
links = {}
|
links = {}
|
||||||
for direction in directions:
|
for direction in directions:
|
||||||
dx, dy = _MAPSCAN(direction)
|
dx, dy = _MAPSCAN[direction]
|
||||||
end_x, end_y = self.x + dx, self.y + dy
|
end_x, end_y = self.x + dx, self.y + dy
|
||||||
if end_x in string_map and end_y in string_map[end_x]:
|
if end_x in string_map and end_y in string_map[end_x]:
|
||||||
links[direction] = string_map[end_x][end_y]
|
links[direction] = string_map[end_x][end_y]
|
||||||
|
|
@ -344,6 +348,7 @@ class MapLink:
|
||||||
MapParserError: If a link lead to nowhere.
|
MapParserError: If a link lead to nowhere.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# from evennia import set_trace;set_trace()
|
||||||
end_direction = self.get_directions(start_direction, string_map).get(start_direction)
|
end_direction = self.get_directions(start_direction, string_map).get(start_direction)
|
||||||
if not end_direction:
|
if not end_direction:
|
||||||
raise MapParserError(f"Link at ({self.x}, {self.y}) was connected to "
|
raise MapParserError(f"Link at ({self.x}, {self.y}) was connected to "
|
||||||
|
|
@ -369,7 +374,7 @@ class MapLink:
|
||||||
else:
|
else:
|
||||||
# we hit another link. Progress recursively.
|
# we hit another link. Progress recursively.
|
||||||
return next_target.traverse(
|
return next_target.traverse(
|
||||||
_REVERSE_DIRECTIONS(end_direction),
|
_REVERSE_DIRECTIONS[end_direction],
|
||||||
string_map, _weight=_weight, _linklen=_linklen + 1)
|
string_map, _weight=_weight, _linklen=_linklen + 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -546,8 +551,8 @@ class Map:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
map_module_or_dict (str, module or dict): Path or module pointing to a map. If a dict,
|
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 'room_legend' and
|
this should be a dict with a key 'map' and optionally a 'legend'
|
||||||
'link_legend' dicts to specify the map structure.
|
dicts to specify the map structure.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# load data from dict or file
|
# load data from dict or file
|
||||||
|
|
@ -562,7 +567,7 @@ class Map:
|
||||||
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND)
|
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND)
|
||||||
|
|
||||||
self.mapstring = mapdata['map']
|
self.mapstring = mapdata['map']
|
||||||
self.node_legend = map_module_or_dict.get("legend", DEFAULT_LEGEND)
|
self.legend = map_module_or_dict.get("legend", DEFAULT_LEGEND)
|
||||||
|
|
||||||
self.string_map = None
|
self.string_map = None
|
||||||
self.node_map = None
|
self.node_map = None
|
||||||
|
|
@ -579,7 +584,7 @@ class Map:
|
||||||
self.parse()
|
self.parse()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "\n".join(self.display_map)
|
return "\n".join("".join(line) for line in self.display_map)
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -590,10 +595,12 @@ class Map:
|
||||||
Notes:
|
Notes:
|
||||||
"""
|
"""
|
||||||
mapcorner_symbol = self.mapcorner_symbol
|
mapcorner_symbol = self.mapcorner_symbol
|
||||||
# this allows for [x][y] mapping with arbitrary objects
|
# this allows for string-based [x][y] mapping with arbitrary objects
|
||||||
string_map = defaultdict(dict)
|
string_map = defaultdict(dict)
|
||||||
# needed by pathfinder
|
# needed by pathfinder
|
||||||
node_index_map = {}
|
node_index_map = {}
|
||||||
|
# mapping nodes to real x,y positions
|
||||||
|
node_map = defaultdict(dict)
|
||||||
|
|
||||||
mapstring = self.mapstring
|
mapstring = self.mapstring
|
||||||
if mapcorner_symbol not in mapstring:
|
if mapcorner_symbol not in mapstring:
|
||||||
|
|
@ -603,7 +610,7 @@ class Map:
|
||||||
# find the the (xstring, ystring) position where the corner symbol is
|
# find the the (xstring, ystring) position where the corner symbol is
|
||||||
maplines = mapstring.split("\n")
|
maplines = mapstring.split("\n")
|
||||||
mapcorner_x, mapcorner_y = 0, 0
|
mapcorner_x, mapcorner_y = 0, 0
|
||||||
for mapcorner_y, line in maplines:
|
for mapcorner_y, line in enumerate(maplines):
|
||||||
mapcorner_x = line.find(mapcorner_symbol)
|
mapcorner_x = line.find(mapcorner_symbol)
|
||||||
if mapcorner_x != -1:
|
if mapcorner_x != -1:
|
||||||
break
|
break
|
||||||
|
|
@ -619,7 +626,7 @@ class Map:
|
||||||
|
|
||||||
# first pass: read string-grid and parse even (x,y) coordinates into nodes
|
# first pass: read string-grid and parse even (x,y) coordinates into nodes
|
||||||
for iy, line in enumerate(maplines[origo_y:]):
|
for iy, line in enumerate(maplines[origo_y:]):
|
||||||
maxheight = max(maxheight, iy)
|
maxheight = max(maxheight, iy + 1)
|
||||||
even_iy = iy % 2 == 0
|
even_iy = iy % 2 == 0
|
||||||
for ix, char in enumerate(line[origo_x:]):
|
for ix, char in enumerate(line[origo_x:]):
|
||||||
|
|
||||||
|
|
@ -627,43 +634,37 @@ class Map:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
even_ix = ix % 2 == 0
|
even_ix = ix % 2 == 0
|
||||||
maxwidth = max(maxwidth, ix)
|
maxwidth = max(maxwidth, ix + 1)
|
||||||
|
|
||||||
|
mapnode_or_link_class = self.legend.get(char)
|
||||||
|
if not mapnode_or_link_class:
|
||||||
|
raise MapParserError(
|
||||||
|
f"Symbol '{char}' on grid position ({ix,iy}) is not found in LEGEND.")
|
||||||
|
|
||||||
if even_iy and even_ix:
|
if even_iy and even_ix:
|
||||||
# a node position will only appear on even positions in the string grid.
|
# a node position will only appear on even positions in the string grid.
|
||||||
|
if hasattr(mapnode_or_link_class, "node_index"):
|
||||||
mapnode_class = self.node_legend.get(char)
|
|
||||||
if not mapnode_class:
|
|
||||||
raise MapParserError(f"Node symbol '{char}' not in NODE_LEGEND.")
|
|
||||||
|
|
||||||
if hasattr(mapnode_class, "node_index"):
|
|
||||||
# this is an actual node that represents an in-game location
|
# this is an actual node that represents an in-game location
|
||||||
# - register it properly.
|
# - register it properly.
|
||||||
# the x,y stored on the node is the 'actual' xy position in the game
|
# the x,y stored on the node is the 'actual' xy position in the game
|
||||||
# world, not just the position in the string map (that is stored
|
# world, not just the position in the string map (that is stored
|
||||||
# in the string_map indices instead).
|
# in the string_map indices instead).
|
||||||
string_map[ix][iy] = self.node_map[ix][iy] = node_index_map[node_index] = \
|
realx, realy = ix // 2, iy // 2
|
||||||
mapnode_class(node_index=node_index, x=ix // 2, y=iy // 2)
|
string_map[ix][iy] = node_map[realx][realy] = node_index_map[node_index] = \
|
||||||
|
mapnode_or_link_class(node_index=node_index, x=realx, y=realy)
|
||||||
node_index += 1
|
node_index += 1
|
||||||
continue
|
continue
|
||||||
else:
|
|
||||||
# we have a link at a coordinate position. Store it but don't add
|
|
||||||
# it to the node_index_map since it doesn't have an in-game existence.
|
|
||||||
string_map[ix][iy] = mapnode_class() # actually a linknode class
|
|
||||||
else:
|
|
||||||
# an in-between coordinates link
|
|
||||||
linknode_class = self.link_legend(char)
|
|
||||||
if not linknode_class:
|
|
||||||
raise MapParserError(f"Link symbol '{char}' not in LINK_LEGEND.")
|
|
||||||
string_map[ix][iy] = linknode_class()
|
|
||||||
|
|
||||||
# second pass: Here we loop over all nodes and have them connect to each other
|
# an in-between coordinates, or on-node position link
|
||||||
# via the detected linkages.
|
string_map[ix][iy] = mapnode_or_link_class(x=ix, y=iy)
|
||||||
for node in node_index_map:
|
|
||||||
node.build_links(string_map)
|
# 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():
|
||||||
|
node.build_links(string_map)
|
||||||
|
|
||||||
# build display map
|
# build display map
|
||||||
display_map = [" " * maxwidth for _ in range(maxheight)]
|
display_map = [[" "] * maxwidth for _ in range(maxheight)]
|
||||||
for ix, ydct in string_map.items():
|
for ix, ydct in string_map.items():
|
||||||
for iy, node_or_link in ydct.items():
|
for iy, node_or_link in ydct.items():
|
||||||
display_map[iy][ix] = node_or_link.display_symbol
|
display_map[iy][ix] = node_or_link.display_symbol
|
||||||
|
|
@ -672,23 +673,28 @@ class Map:
|
||||||
self.width = maxwidth
|
self.width = maxwidth
|
||||||
self.height = maxheight
|
self.height = maxheight
|
||||||
self.string_map = string_map
|
self.string_map = string_map
|
||||||
self.node_index_map = self.node_index_map
|
self.node_index_map = node_index_map
|
||||||
self.display_map = display_map
|
self.display_map = display_map
|
||||||
|
self.node_map = node_map
|
||||||
|
|
||||||
def _get_node_from_coord(self, x, y):
|
def _get_node_from_coord(self, x, y):
|
||||||
"""
|
"""
|
||||||
Get a MapNode from a coordinate.
|
Get a MapNode from a coordinate.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
x (int): X-coordinate on grid.
|
x (int): X-coordinate on game grid.
|
||||||
y (int): Y-coordinate on grid.
|
y (int): Y-coordinate on game grid.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MapNode: The node found at the given coordinates.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not self.node_2d_map:
|
if not self.node_map:
|
||||||
self.parse()
|
self.parse()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.node_2d_map[x][y]
|
return self.node_map[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.width}, {self.height}).")
|
||||||
|
|
@ -729,8 +735,8 @@ class Map:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
istartnode = self._get_node_from_coord(*endcoord).node_index
|
istartnode = self._get_node_from_coord(*startcoord).node_index
|
||||||
endnode = self._get_node_from_coord(*startcoord)
|
endnode = self._get_node_from_coord(*endcoord)
|
||||||
|
|
||||||
if not self.pathfinding_routes:
|
if not self.pathfinding_routes:
|
||||||
self._calculate_path_matrix()
|
self._calculate_path_matrix()
|
||||||
|
|
@ -774,5 +780,5 @@ class Map:
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
for line in self.display_map[top:bottom]:
|
for line in self.display_map[top:bottom]:
|
||||||
output.append(line[left:right])
|
output.append("".join(line[left:right]))
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,46 @@ Tests for the Mapsystem
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.unittest import TestCase, mock
|
from unittest import TestCase, mock
|
||||||
from . import mapsystem
|
from . import mapsystem
|
||||||
|
|
||||||
|
|
||||||
|
MAP1 = """
|
||||||
|
|
||||||
|
+ 0 1 2
|
||||||
|
|
||||||
|
0 #-#
|
||||||
|
| |
|
||||||
|
1 #-#
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
MAP1_DISPLAY = """
|
||||||
|
#-#
|
||||||
|
| |
|
||||||
|
#-#
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
class TestMap1(TestCase):
|
||||||
|
"""
|
||||||
|
Test the Map class with a simple map and default symbol legend.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.map = mapsystem.Map({"map": MAP1})
|
||||||
|
|
||||||
|
def test_str_output(self):
|
||||||
|
"""Check the display_map"""
|
||||||
|
self.assertEqual(str(self.map).strip(), MAP1_DISPLAY)
|
||||||
|
|
||||||
|
def test_node_from_coord(self):
|
||||||
|
node = self.map._get_node_from_coord(1, 1)
|
||||||
|
self.assertEqual(node.x, 1)
|
||||||
|
self.assertEqual(node.y, 1)
|
||||||
|
|
||||||
|
def test_get_shortest_path(self):
|
||||||
|
nodepath, linkpath = self.map.get_shortest_path((0, 0), (1, 1))
|
||||||
|
self.assertEqual([node.node_index for node in nodepath], [0, 1, 3])
|
||||||
|
self.assertEqual(linkpath, ['e', 's'])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue