mirror of
https://github.com/evennia/evennia.git
synced 2026-04-03 14:37:17 +02:00
Made north and +Y same direction for map
This commit is contained in:
parent
e8818f6bbb
commit
e5bc5f9a7d
3 changed files with 396 additions and 267 deletions
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue