Made north and +Y same direction for map

This commit is contained in:
Griatch 2021-06-06 14:10:28 +02:00
parent e8818f6bbb
commit e5bc5f9a7d
3 changed files with 396 additions and 267 deletions

View file

@ -1,7 +1,7 @@
MAP = r"""
1 1 1 1 1 1 1 1 1 1 2
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1
+ 0 1 2 3 4 5 6 7 8 9 0
0 #
\
@ -23,27 +23,6 @@ MAP = r"""
^
9 #-# #
10
11
12
13
14
15
16
17
18
19
20
"""
@ -54,3 +33,33 @@ MAP_DATA = {
"map": MAP,
"legend": LEGEND,
}
MAP = r"""
1
+ 0 1 2 3 4 5 6 7 8 9 0
10 #-#-#
|
9 #
\
8 #-#-#-#
|\ |
7 #-#+#+#-----#
| |
6 #-#---#-#-#-#-#
| |x|x|
5 o-#-#-# #-#-#
\ |x|x|
4 o-o-#-# #-#-#
/
3 #
\
2 #-#-#-#
| |
1 #-#-#d# #
^
0 #-# #
+ 0 1 2 3 4 5 6 7 8 9 0
1
"""

View file

@ -4,33 +4,38 @@ Implement mapping, with path searching.
This builds a map graph based on an ASCII map-string with special, user-defined symbols.
```python
# in module passed to Map class
# in module passed to 'Map' class. It will either a dict
# MAP_DATA with keys 'map' and (optionally) 'legend', or
# the MAP/LEGEND variables directly.
MAP = r'''
1
+ 0 1 2 3 4 5 6 7 8 9 0
0 #
10 #
\
1 #-#-#-#
9 #-#-#-#
|\ |
2 #-#-#-#-----#
8 #-#-#-#-----#
| |
3 #-#---#-#-#-#-#
7 #-#---#-#-#-#-#
| |x|x|
4 o-#-#-# #-#-#
6 o-#-#-# #-#-#
\ |x|x|
5 o---#-# #-#-#
/
6 #
4 #
\
7 #-#-#-#
3 #-#-#-#
| |
8 #-#-#-# #
2 #-#-#-# #
^
9 #-# #
1 #-# #
|
0 #-#---o
10
+ 0 1 2 3 4 5 6 7 8 9 1
0
'''
@ -44,23 +49,27 @@ This builds a map graph based on an ASCII map-string with special, user-defined
```
The two `+` signs in the upper/lower left corners are required and marks the edge of the map area.
The origo of the grid is always two steps right and two up from the bottom test marker and the grid
extends to two lines below the top-left marker. Anything outside the grid is ignored, so numbering
the coordinate axes is optional but recommended for readability.
The XY positions represent XY positions in the game world. When existing, they are usually
represented by Rooms in-game. The links between nodes would normally represent Exits, but the length
of links on the map have no in-game equivalence except that traversing a multi-step link will place
you in a location with an XY coordinate different from what you'd expect by a single step (most
games don't relay the XY position to the player anyway).
In the map string, every XY coordinate must have exactly one spare space/line between them - this is
used for node linkings. This finer grid which has 2x resolution of the `XYgrid` is only used by the
mapper and is referred to as the `xygrid` (small xy) internally. Note that an XY position can also
be held by a link (for example a passthrough).
The nodes and links can be customized by add your own implementation of `MapNode` or `MapLink` to
the LEGEND dict, mapping them to a particular character symbol.
The single `+` sign in the upper left corner is required and marks the origo of the mapping area and
the 0,0 position will always start one space right and one line down from it. The coordinate axes
numbering is optional, but recommended for readability.
Every x-column should be spaced with one space and the y-rows must have a line between them.
The coordinate positions all corresponds to map 'nodes'. These are usually rooms (which require an
in-game coordinate system to work with the map) but can also be abstract 'link nodes' that links
rooms together and have no in-game equivalence.
All in-between-coordinates positions are reserved for links and no nodes will be detected in those
positions (since it would then not have a proper x,y coordinate).
the LEGEND dict, mapping them to a particular character symbol. A `MapNode` can only be added
on an even XY coordinate while `MapLink`s can be added anywhere on the xygrid.
See `./example_maps.py` for some empty grid areas to start from.
----
"""
@ -85,14 +94,14 @@ _REVERSE_DIRECTIONS = {
}
_MAPSCAN = {
"n": (0, -1),
"ne": (1, -1),
"n": (0, 1),
"ne": (1, 1),
"e": (1, 0),
"se": (1, 1),
"s": (0, 1),
"sw": (-1, 1),
"se": (1, -1),
"s": (0, -1),
"sw": (-1, -1),
"w": (-1, 0),
"nw": (-1, -1)
"nw": (1, -1)
}
@ -126,8 +135,8 @@ class MapNode:
Initialize the mapnode.
Args:
x (int): X coordinate. This is the actual room coordinate.
y (int): Y coordinate. This is the actual room coordinate.
x (int): Coordinate on xygrid.
y (int): Coordinate on xygrid.
node_index (int): This identifies this node with a running
index number required for pathfinding.
@ -135,6 +144,11 @@ class MapNode:
self.x = x
self.y = y
# XYgrid coordinate
self.X = x // 2
self.Y = y // 2
self.node_index = node_index
if not self.display_symbol:
@ -148,17 +162,17 @@ class MapNode:
# lowest direction to a given neighbor
self.cheapest_to_node = {}
def build_links(self, string_map):
def build_links(self, xygrid):
"""
Start tracking links in all cardinal directions to
tie this to another node.
Args:
string_map (dict): A 2d dict-of-dicts with x,y coordinates as keys and nodes as values.
xygrid (dict): A 2d dict-of-dicts with x,y coordinates as keys and nodes as values.
"""
# convert room-coordinates back to string-map coordinates
x, y = self.x * 2, self.y * 2
# we must use the xygrid coordinates
x, y = self.x, self.y
# scan in all directions for links
for direction, (dx, dy) in _MAPSCAN.items():
@ -168,12 +182,12 @@ class MapNode:
# hence we can step in integer steps
lx, ly = x + dx, y + dy
if lx in string_map and ly in string_map[lx]:
link = string_map[lx][ly]
if lx in xygrid and ly in xygrid[lx]:
link = xygrid[lx][ly]
# just because there is a link here, doesn't mean it's
# connected to this node. If so the `end_node` will be None.
end_node, weight = link.traverse(_REVERSE_DIRECTIONS[direction], string_map)
end_node, weight = link.traverse(_REVERSE_DIRECTIONS[direction], xygrid)
if end_node:
# the link could be followed to an end node!
node_index = end_node.node_index
@ -261,24 +275,23 @@ class MapLink:
Initialize the link.
Args:
x (int): The string-grid X coordinate of the link.
y (int): The string-grid Y coordinate of the link.
x (int): The xygrid x coordinate
y (int): The xygrid y coordinate.
"""
self.x = x
self.y = y
if not self.display_symbol:
self.display_symbol = self.symbol
def get_visually_connected(self, string_map, directions=None):
def get_visually_connected(self, xygrid, directions=None):
"""
A helper to get all directions to which there appears to be a
visual link/node. This does not trace the link and check weights etc.
Args:
link (MapLink): Currently active link.
string_map (dict): 2D dict with x,y coordinates as keys.
xygrid (dict): 2D dict with x,y coordinates as keys.
directions (list, optional): The directions (n, ne etc) to check
visual connection to.
@ -292,18 +305,18 @@ class MapLink:
for direction in directions:
dx, dy = _MAPSCAN[direction]
end_x, end_y = self.x + dx, self.y + dy
if end_x in string_map and end_y in string_map[end_x]:
links[direction] = string_map[end_x][end_y]
if end_x in xygrid and end_y in xygrid[end_x]:
links[direction] = xygrid[end_x][end_y]
return links
def get_directions(self, start_direction, string_map):
def get_directions(self, start_direction, xygrid):
"""
Hook to override for customizing how the directions are
determined.
Args:
start_direction (str): The starting direction (n, ne etc).
string_map (dict): 2D dict with x,y coordinates as keys.
xygrid (dict): 2D dict with x,y coordinates as keys.
Returns:
dict: The directions map {start_direction:end_direction} of
@ -312,13 +325,13 @@ class MapLink:
"""
return self.directions
def get_weights(self, start_direction, string_map, current_weight):
def get_weights(self, start_direction, xygrid, current_weight):
"""
Hook to override for customizing how the weights are determined.
Args:
start_direction (str): The starting direction (n, ne etc).
string_map (dict): 2D dict with x,y coordinates as keys.
xygrid (dict): 2D dict with x,y coordinates as keys.
current_weight (int): This can have an existing value if
we are progressing down a multi-step path.
@ -329,14 +342,14 @@ class MapLink:
"""
return self.weights
def traverse(self, start_direction, string_map, _weight=0, _linklen=1):
def traverse(self, start_direction, xygrid, _weight=0, _linklen=1):
"""
Recursively traverse a set of links.
Args:
start_direction (str): The direction (n, ne etc) from which
this traversal originates for this link.
string_map (dict): 2D dict with x,y coordinates as keys.
xygrid (dict): 2D dict with x,y coordinates as keys.
Kwargs:
_weight (int): Internal use.
_linklen (int): Internal use.
@ -349,7 +362,7 @@ class MapLink:
"""
# 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, xygrid).get(start_direction)
if not end_direction:
raise MapParserError(f"Link at ({self.x}, {self.y}) was connected to "
f"from {start_direction}, but does not link that way.")
@ -357,13 +370,13 @@ class MapLink:
dx, dy = _MAPSCAN[end_direction]
end_x, end_y = self.x + dx, self.y + dy
try:
next_target = string_map[end_x][end_y]
next_target = xygrid[end_x][end_y]
except KeyError:
raise MapParserError(f"Link at ({self.x}, {self.y}) points to "
f"empty space in direction {end_direction}!")
_weight += self.get_weights(
start_direction, string_map, _weight).get(
start_direction, xygrid, _weight).get(
start_direction, self.default_weight)
if hasattr(next_target, "node_index"):
@ -375,7 +388,7 @@ class MapLink:
# we hit another link. Progress recursively.
return next_target.traverse(
_REVERSE_DIRECTIONS[end_direction],
string_map, _weight=_weight, _linklen=_linklen + 1)
xygrid, _weight=_weight, _linklen=_linklen + 1)
# ----------------------------------
@ -462,10 +475,10 @@ class DynamicMapLink(MapLink):
"""
symbol = "o"
def get_directions(self, start_direction, string_map):
def get_directions(self, start_direction, xygrid):
# get all visually connected links
directions = {}
links = list(self.get_visually_connected(string_map).keys())
links = list(self.get_visually_connected(xygrid).keys())
loop_links = links.copy()
# first get all cross-through links
for direction in loop_links:
@ -492,7 +505,7 @@ DEFAULT_LEGEND = {
"|": NSMapLink,
"-": EWMapLink,
"/": NESWMapLink,
"//": SENWMapLink,
"\\": SENWMapLink,
"x": CrossMapLink,
"+": PlusMapLink,
"v": NSOneWayMapLink,
@ -501,49 +514,53 @@ DEFAULT_LEGEND = {
">": WEOneWayMapLink,
}
# --------------------------------------------
# Map parser implementation
class Map:
"""
This represents a map of interconnected nodes/rooms. Each room is connected
to each other as a directed graph with optional 'weights' between the the
connections.
r"""
This represents a map of interconnected nodes/rooms. Each room is connected to each other as a
directed graph with optional 'weights' between the the connections. It is created from a map
string with symbols describing the topological layout. It also provides pathfinding using the
Dijkstra algorithm.
This is a parser that parses a string with an ascii-created map into
a 2D-array understood by the Dijkstra algorithm.
The map-string is read from a string or from a module. The grid area of the string is marked by
two `+` characters - one in the top left of the area and the other in the bottom left.
The grid starts two spaces/lines in from the 'open box' created by these two markers and extend
any width to the right.
Any other markers or comments can be added outside of the grid - they will be ignored. Every
grid coordinate must always be separated by exactly one space/line since the space between
are used for links.
::
'''
1 1 1
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 ...
The result of this is labeling every node in the tree with a number 0...N.
4 # # #
| \ /
3 #-#-# # #
| \ /
2 #-#-# #
|x|x| |
1 #-#-#-#-#-#-#
/
0 #-#
The grid should be defined to be as readable as possible and every full coordinat must
be separated by a space/empty line. The single `+` in the upper left corner is used to
tell the parser where the axes cross. The (0,0) point will start one space/line away
from this point. The strict numbering is optional (the + is all that's needed), but it's
highly recommended for readability!
::
'''
1 1 1
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 ...
+ 0 1 2 3 4 5 6 7 8 9 1 1 1 ...
0 1 2
'''
0
1
2
.
.
10
11
.
.
'''
So origo (0,0) is in the bottom-left and north is +y movement, south is -y movement
while east/west is +/- x movement as expected. Adding numbers to axes is optional
but recommended for readability!
"""
mapcorner_symbol = '+'
max_pathfinding_length = 1000
empty_symbol = ' '
# we normally only accept one single character for the legend key
legend_key_exceptions = ("\\")
def __init__(self, map_module_or_dict):
"""
@ -554,26 +571,28 @@ class Map:
this should be a dict with a key 'map' and optionally a 'legend'
dicts to specify the map structure.
Notes:
The map deals with two sets of coorinate systems:
- grid-coordinates x,y are the character positions in the map string.
- world-coordinates X,Y are the in-world coordinates of nodes/rooms.
There are fewer of these since they ignore the 'link' spaces between
the nodes in the grid, so
X = x // 2
Y = y // 2
"""
# load data from dict or file
mapdata = {}
if isinstance(map_module_or_dict, dict):
mapdata = map_module_or_dict
else:
mod = mod_import(map_module_or_dict)
mapdata = variable_from_module(mod, "MAP_DATA")
if not mapdata:
mapdata['map'] = variable_from_module(mod, "MAP")
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND)
# store so we can reload
self.map_module_or_dict = map_module_or_dict
self.mapstring = mapdata['map']
self.legend = map_module_or_dict.get("legend", DEFAULT_LEGEND)
self.string_map = None
self.node_map = None
# map setup
self.xygrid = None
self.XYgrid = None
self.display_map = None
self.width = 0
self.height = 0
self.max_x = 0
self.max_y = 0
self.max_X = 0
self.max_Y = 0
# Dijkstra algorithm variables
self.node_index_map = None
@ -581,120 +600,30 @@ class Map:
self.dist_matrix = None
self.pathfinding_routes = None
self.parse()
# load data and parse it
self.reload()
def __str__(self):
return "\n".join("".join(line) for line in self.display_map)
def parse(self):
"""
Parse the numberical grid in the string. The result of this is a 2D array
of [[MapNode,...], [MapNode, ...]] with MapLinks inside them describing their
linkage to other nodes.
Notes:
"""
mapcorner_symbol = self.mapcorner_symbol
# this allows for string-based [x][y] mapping with arbitrary objects
string_map = defaultdict(dict)
# needed by pathfinder
node_index_map = {}
# mapping nodes to real x,y positions
node_map = defaultdict(dict)
mapstring = self.mapstring
if mapcorner_symbol not in mapstring:
raise MapParserError("mapstring must have a '+' in the upper left corner to mark "
"the origo of the coordinate system.")
# find the the (xstring, ystring) position where the corner symbol is
maplines = mapstring.split("\n")
mapcorner_x, mapcorner_y = 0, 0
for mapcorner_y, line in enumerate(maplines):
mapcorner_x = line.find(mapcorner_symbol)
if mapcorner_x != -1:
break
# in-string_position of (x,y)
origo_x, origo_y = mapcorner_x + 2, mapcorner_y + 2
# we have placed the origo, start parsing the grid
node_index = 0
maxwidth = 0
maxheight = 0
# first pass: read string-grid and parse even (x,y) coordinates into nodes
for iy, line in enumerate(maplines[origo_y:]):
even_iy = iy % 2 == 0
for ix, char in enumerate(line[origo_x:]):
if char == self.empty_symbol:
continue
even_ix = ix % 2 == 0
maxwidth = max(maxwidth, ix + 1)
maxheight = max(maxheight, iy + 1) # only increase if there's something on the line
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:
# a node position will only appear on even positions in the string grid.
if hasattr(mapnode_or_link_class, "node_index"):
# this is an actual node that represents an in-game location
# - register it properly.
# 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
# in the string_map indices instead).
realx, realy = ix // 2, 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
continue
# an in-between coordinates, or on-node position link
string_map[ix][iy] = mapnode_or_link_class(x=ix, y=iy)
# 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
display_map = [[" "] * maxwidth for _ in range(maxheight)]
for ix, ydct in string_map.items():
for iy, node_or_link in ydct.items():
display_map[iy][ix] = node_or_link.display_symbol
# store
self.width = maxwidth
self.height = maxheight
self.string_map = string_map
self.node_index_map = node_index_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.
Args:
x (int): X-coordinate on game grid.
y (int): Y-coordinate on game grid.
X (int): X-coordinate on XY (game) grid.
Y (int): Y-coordinate on XY (game) grid.
Returns:
MapNode: The node found at the given coordinates.
"""
if not self.node_map:
if not self.XYgrid:
self.parse()
try:
return self.node_map[x][y]
return self.XYgrid[X][Y]
except IndexError:
raise MapError("_get_node_from_coord got coordinate ({x},{y}) which is "
"outside the grid size of (0,0) - ({self.width}, {self.height}).")
@ -719,22 +648,189 @@ class Map:
pathfinding_matrix, directed=True,
return_predecessors=True, limit=1000)
def _parse(self):
"""
Parses the numerical grid from the string. The result of this is a 2D array
of [[MapNode,...], [MapNode, ...]] with MapLinks inside them describing their
linkage to other nodes. See the class docstring for details of how the grid
should be defined.
Notes:
In this parsing, the 'xygrid' is the full range of chraracters read from
the string. The `XYgrid` is used to denote the game-world coordinates
(which doesn't include the links)
"""
mapcorner_symbol = self.mapcorner_symbol
# this allows for string-based [x][y] mapping with arbitrary objects
xygrid = defaultdict(dict)
# mapping nodes to real X,Y positions
XYgrid = defaultdict(dict)
# needed by pathfinder
node_index_map = {}
mapstring = self.mapstring
if mapstring.count(mapcorner_symbol) < 2:
raise MapParserError(f"The mapstring must have at least two '{mapcorner_symbol}' "
"symbols marking the upper- and bottom-left corners of the "
"grid area.")
# find the the position (in the string as a whole) of the top-left corner-marker
maplines = mapstring.split("\n")
topleft_marker_x, topleft_marker_y = -1, -1
for topleft_marker_y, line in enumerate(maplines):
topleft_marker_x = line.find(mapcorner_symbol)
if topleft_marker_x != -1:
break
if topleft_marker_x == -1 or topleft_marker_y == -1:
raise MapParserError(f"No top-left corner-marker ({mapcorner_symbol}) found!")
# find the position (in the string as a whole) of the bottom-left corner-marker
# this is always in a stright line down from the first marker
botleft_marker_x, botleft_marker_y = topleft_marker_x, -1
for botleft_marker_y, line in enumerate(maplines[topleft_marker_y + 1:]):
if line.find(mapcorner_symbol) == topleft_marker_x:
break
if botleft_marker_y == -1:
raise MapParserError(f"No bottom-left corner-marker ({mapcorner_symbol}) found! "
"Make sure it lines up with the top-left corner-marker "
f"(found at column {topleft_marker_x} of the string).")
# in-string_position of the top- and bottom-left grid corners (2 steps in from marker)
# the bottom-left corner is also the origo (0,0) of the grid.
topleft_y = topleft_marker_y + 2
origo_x, origo_y = botleft_marker_x + 2, botleft_marker_y + 2
# highest actually filled grid points
max_x = 0
max_y = 0
max_X = 0
max_Y = 0
node_index = -1
# first pass: read string-grid (left-right, bottom-up) and parse all grid points
for iy, line in enumerate(reversed(maplines[topleft_y:origo_y])):
even_iy = iy % 2 == 0
for ix, char in enumerate(line[origo_x:]):
# from now on, coordinates are on the xygrid.
if char == self.empty_symbol:
continue
# only set this if there's actually something on the line
max_x, max_y = max(max_x, ix), max(max_y, iy)
mapnode_or_link_class = self.legend.get(char)
if not mapnode_or_link_class:
raise MapParserError(
f"Symbol '{char}' on xygrid position ({ix},{iy}) is not found in LEGEND."
)
if hasattr(mapnode_or_link_class, "node_index"):
# A mapnode. Mapnodes can only be placed on even grid positions, where
# there are integer X,Y coordinates defined.
if not (even_iy and ix % 2 == 0):
raise MapParserError(
f"Symbol '{char}' (xygrid ({ix},{iy}) marks a Node but is located "
"between valid (X,Y) positions!")
# save the node to several different maps for different uses
# in both coordinate systems
iX, iY = ix // 2, iy // 2
max_X, max_Y = max(max_X, iX), max(max_Y, iY)
node_index += 1
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[node_index] = \
mapnode_or_link_class(node_index=node_index, x=ix, y=iy)
else:
# we have a link at this xygrid position (this is ok everywhere)
xygrid[ix][iy] = mapnode_or_link_class(x=ix, y=iy)
# 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(xygrid)
# build display map
display_map = [[" "] * (max_x + 1) for _ in range(max_y + 1)]
for ix, ydct in xygrid.items():
for iy, node_or_link in ydct.items():
display_map[iy][ix] = node_or_link.display_symbol
# store
self.max_x, self.max_y = max_x, max_y
self.xygrid = xygrid
self.max_X, self.max_Y = max_X, max_Y
self.XYgrid = XYgrid
self.node_index_map = node_index_map
self.display_map = display_map
def reload(self, map_module_or_dict=None):
"""
(Re)Load a map.
Args:
map_module_or_dict (str, module or dict, optional): See description for the variable
in the class' `__init__` function. If given, replace the already loaded
map with a new one. If not given, the existing one given on class creation
will be reloaded.
parse (bool, optional): If set, auto-run `.parse()` on the newly loaded data.
Notes:
This will both (re)load the data and parse it into a new map structure, replacing any
existing one.
"""
if not map_module_or_dict:
map_module_or_dict = self.map_module_or_dict
mapdata = {}
if isinstance(map_module_or_dict, dict):
mapdata = map_module_or_dict
else:
mod = mod_import(map_module_or_dict)
mapdata = variable_from_module(mod, "MAP_DATA")
if not mapdata:
mapdata['map'] = variable_from_module(mod, "MAP")
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND)
# validate
for key in mapdata.get('legend', DEFAULT_LEGEND):
if not key or len(key) > 1:
if key not in self.legend_key_exceptions:
raise MapError(f"Map-legend key '{key}' is invalid: All keys must "
"be exactly one character long. Use the node/link's "
"`.display_symbol` property to change how it is "
"displayed.")
if 'map' not in mapdata or not mapdata['map']:
raise MapError("No map found. Add 'map' key to map-data (MAP_DATA) dict or "
"add variable MAP to a module passed into the parser.")
# store/update result
self.mapstring = mapdata['map']
self.legend = map_module_or_dict.get("legend", DEFAULT_LEGEND)
# process the new(?) data
self._parse()
def get_shortest_path(self, startcoord, endcoord):
"""
Get the shortest route between two points on the grid.
Args:
startcoord (tuple or MapNode): A starting (x,y) coordinate for where
we start from.
endcoord (tuple or MapNode): The end (x,y) coordinate we want to
find the shortest route to.
startcoord (tuple): A starting (X,Y) coordinate on the XYgrid (in-game coordinate) for
where we start from.
endcoord (tuple or MapNode): The end (X,Y) coordinate on the XYgrid (in-game coordinate)
we want to find the shortest route to.
Returns:
tuple: Two lists, first one containing the shortest sequence of map nodes to
traverse and the second a list of directions (n, se etc) describing the path.
"""
istartnode = self._get_node_from_coord(*startcoord).node_index
endnode = self._get_node_from_coord(*endcoord)
@ -760,37 +856,47 @@ class Map:
return nodepath, linkpath
def get_map_region(self, x, y, dist=2, return_str=True):
def get_map_display(self, coord, dist=2, character='@', return_str=True):
"""
Display the map centered on a point and everything around it within a certain distance.
Args:
x (int): In-world X coordinate.
y (int): In-world Y coordinate.
dist (int): Number of gridpoints distance to show.
coord (tuple): (X,Y) in-world coordinate location.
dist (int, optional): Number of gridpoints distance to show.
A value of 2 will show adjacent nodes, a value
of 1 will only show links from current node.
of 1 will only show links from current node. If this is None,
show entire map centered on iX,iY.
character (str, optional): Place this symbol at the `coord` position
of the displayed map. Ignored if falsy.
return_str (bool, optional): Return result as an
already formatted string.
Returns:
str or list: Depending on value of `return_str`. If a list,
this is 2D list of lines, [[str,str,str,...], [...]] where
this is 2D grid of lines, [[str,str,str,...], [...]] where
each element is a single character in the display grid. To
extract a coordinate from it, use listing[iy][ix]
extract a character at (ix,iy) coordinate from it, use
indexing `outlist[iy][ix]` in that order.
"""
width, height = self.width, self.height
# convert to string-map coordinates. Remember that y grid grows downwards
ix, iy = max(0, min(x * 2, width)), max(0, min(y * 2, height))
left, right = max(0, ix - dist), min(width, ix + dist + 1)
top, bottom = max(0, iy - dist), min(height, iy + dist + 1)
output = []
if return_str:
for line in self.display_map[top:bottom]:
output.append("".join(line[left:right]))
return "\n".join(output)
iX, iY = coord
# convert inputs to xygrid
width, height = self.max_x + 1, self.max_y + 1
ix, iy = max(0, min(iX * 2, width)), max(0, min(iY * 2, height))
if dist is None:
gridmap = self.display_map
ixc, iyc = ix, iy
else:
for line in self.display_map[top:bottom]:
output.append(line[left:right])
return output
left, right = max(0, ix - dist), min(width, ix + dist + 1)
bottom, top = max(0, iy - dist), min(height, iy + dist + 1)
ixc, iyc = ix - left, iy - bottom
gridmap = [line[left:right] for line in self.display_map[bottom:top]]
if character:
gridmap[iyc][ixc] = character # correct indexing; it's a list of lines
if return_str:
return "\n".join("".join(line) for line in gridmap)
else:
return gridmap

View file

@ -13,10 +13,11 @@ MAP1 = """
+ 0 1 2
0 #-#
| |
1 #-#
| |
0 #-#
+ 0 1 2
"""
MAP1_DISPLAY = """
@ -41,25 +42,38 @@ class TestMap1(TestCase):
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)
self.assertEqual(node.X, 1)
self.assertEqual(node.x, 2)
self.assertEqual(node.X, 1)
self.assertEqual(node.y, 2)
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'])
self.assertEqual(linkpath, ['e', 'n'])
@parameterized.expand([
(0, 0, "#-\n| ", [["#", "-"], ["|", " "]]),
(1, 0, "-#\n |", [["-", "#"], [" ", "|"]]),
(0, 1, "| \n#-", [["|", " "], ["#", "-"]]),
(1, 1, " |\n-#", [[" ", "|"], ["-", "#"]]),
((0, 0), "#-\n| ", [["#", "-"], ["|", " "]]),
((1, 0), "-#\n |", [["-", "#"], [" ", "|"]]),
((0, 1), "| \n#-", [["|", " "], ["#", "-"]]),
((1, 1), " |\n-#", [[" ", "|"], ["-", "#"]]),
])
def test_get_map_region(self, x, y, expectstr, expectlst):
string = self.map.get_map_region(x, y, dist=1)
lst = self.map.get_map_region(x, y, dist=1, return_str=False)
def test_get_map_display(self, coord, expectstr, expectlst):
string = self.map.get_map_display(coord, dist=1, character=None)
lst = self.map.get_map_display(coord, dist=1, return_str=False, character=None)
self.assertEqual(string, expectstr)
self.assertEqual(lst, expectlst)
@parameterized.expand([
((0, 0), "@-\n| ", [["@", "-"], ["|", " "]]),
((1, 0), "-@\n |", [["-", "@"], [" ", "|"]]),
((0, 1), "| \n@-", [["|", " "], ["@", "-"]]),
((1, 1), " |\n-@", [[" ", "|"], ["-", "@"]]),
])
def test_get_map_display__character(self, coord, expectstr, expectlst):
string = self.map.get_map_display(coord, dist=1, character='@')
lst = self.map.get_map_display(coord, dist=1, return_str=False, character='@')
self.assertEqual(string, expectstr)
self.assertEqual(lst, expectlst)