Tested first unittests with map and pathfinding

This commit is contained in:
Griatch 2021-06-05 18:59:34 +02:00
parent b641d0ce71
commit 97865ed4d9
2 changed files with 101 additions and 54 deletions

View file

@ -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 # an in-between coordinates, or on-node position link
# it to the node_index_map since it doesn't have an in-game existence. string_map[ix][iy] = mapnode_or_link_class(x=ix, y=iy)
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 # second pass: Here we loop over all nodes and have them connect to each other
# via the detected linkages. # via the detected linkages.
for node in node_index_map: for node in node_index_map.values():
node.build_links(string_map) 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)

View file

@ -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'])