Simplified map-transition logic using a transition-node rather than a -link

This commit is contained in:
Griatch 2021-06-24 00:01:50 +02:00
parent aaa67218d6
commit ae2f856200
4 changed files with 91 additions and 137 deletions

View file

@ -49,6 +49,11 @@ class MapNode:
(such as a guard or locked gate etc).
- `prototype` (dict) - The default `prototype` dict to use for reproducing this map component
on the game grid. This is used if not overridden specifically for this coordinate.
- `deferred` (bool): A deferred node is used to indicate a link (currently) pointing to nowhere
because the end node is not yet available - usually because that node is on another map
and won't be available until the full grid has loaded. A deferred node doesn't need a symbol
but is returned from links. Links pointing to deferred nodes will be re-parsed once the entire
grid has been built, in order to correctly link maps together.
"""
# symbol used to identify this link on the map
@ -68,6 +73,7 @@ class MapNode:
# the prototype to use for mapping this to the grid.
prototype = None
def __init__(self, x, y, node_index=0, xymap=None):
"""
Initialize the mapnode.
@ -118,7 +124,7 @@ class MapNode:
def __repr__(self):
return str(self)
def scan_all_directions(self, xygrid):
def build_links(self, xygrid):
"""
This is called by the map parser when this node is encountered. It tells the node
to scan in all directions and follow any found links to other nodes. Since there
@ -286,6 +292,42 @@ class MapNode:
maplinks[direction].prototype, objects=[linkobj], exact=False)
class TransitionMapNode(MapNode):
"""
Entering this node teleports the user to another Map (this is completely handled by the
prototyped Room class). This teleportation is not understood by the pathfinder, so why it will
be possible to pathfind to this node, it really represents a map transition. Only a single link
must ever be connected to this node.
Properties:
- `linked_map_name` (str) - the map you will move to when entering this node.
- `linked_coords` (tuple) - the XY coordinates *on the linked* map this node
will teleport to. This must be another node that is not a TransitionMapNode.
Note that for the trip to be two-way, a similar set up must be created from the
other map.
Examples:
::
map1 map2
#-T #- - one-way transition from map1 -> map2.
#-T T-# - two-way. Both ExternalMapNodes links to the coords of the
`#` (NOT the `T`) on the other map!
"""
symbol = 'T'
display_symbol = ' '
linked_map_name = ""
linked_map_coords = None
def build_links(self, xygrid):
"""Check so we don't have too many links"""
super().build_links(xygrid)
if len(self.links) > 1:
raise MapParserError("may have at most one link connecting to it.", self)
class MapLink:
"""
This represents one or more links between an 'incoming direction'
@ -338,6 +380,10 @@ class MapLink:
on the game grid. This is only relevant for the *first* link out of a Node (the continuation
of the link is only used to determine its destination). This can be overridden on a
per-direction basis.
- `requires_grid` (bool): If set, it indicates this component requires the full grid (multiple
maps to be available before it can be processed. This is usually only needed for
inter-map traversal links where the other map must already be ready. Note that this is
*only* relevant for the *first* link out of a node.
"""
# symbol for identifying this link on the map
@ -375,8 +421,7 @@ class MapLink:
interrupt_path = False
# prototype for the first link out of a node.
prototype = None
# only traverse this after all of the grid is complete
delay_traversal = False
def __init__(self, x, y, xymap=None):
"""
@ -447,10 +492,11 @@ class MapLink:
raise MapParserError(
f"points to empty space in the direction {end_direction}!", self)
if next_target.xymap.name != self.xymap.name:
# this target is on another map. Immediately exit the traversal
# and set a high weight.
return (next_target, BIGVAL, [start_direction])
if ((hasattr(next_target, "deferred") and next_target.deferred)
or (next_target.xymap.name != self.xymap.name)):
# this target is either deferred until grid exists, or sits on another map. Immediately
# exit the traversal and set a high weight.
return (next_target, BIGVAL, [self])
_weight += self.get_weight(start_direction, xygrid, _weight)
if _steps is None:
@ -577,6 +623,7 @@ class MapLink:
"""
return self.symbol if self.display_symbol is None else self.display_symbol
class SmartRerouterMapLink(MapLink):
r"""
A 'smart' link without visible direction, but which uses its topological surroundings
@ -753,86 +800,6 @@ class TeleporterMapLink(MapLink):
return self.directions.get(start_direction)
class MapTransitionLink(TeleporterMapLink):
"""
This link teleports the user to another map and lets them continue moving
from there. Like the TeleporterMapLink, the map-transition symbol must connect to only one other
link (not directly to a node).
The other map will be scanned for a matching `.symbol` that must also be a MapTransitionLink.
The link is always two-way, but the link connecting to the transition can be one-way to create
a one-way transition. Make new links with different symbols (like A, B, C, ...) to link
multiple maps together.
Note that unlike for teleports, pathfinding will *not* work across the map-transition.
Examples:
::
map1 map2
T
/ T-# - movement to the transition-link will continue on the other map.
-#
T
/
-# T># - one-way link from map1 to map2
-#t - invalid, may only connect to another link
-#-t-# - invalid, only one connected link is allowed.
"""
symbol = 'T'
display_symbol = ' '
direction_name = 'transition'
interrupt_path = True
target_map = 'map2'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.paired_map_link = None
def at_empty_target(self, start_direction, end_direction, xygrid):
"""
This is called by .traverse when it finds this link pointing to nowhere.
Args:
start_direction (str): The direction (n, ne etc) from which
this traversal originates for this link.
end_direction (str): The direction found from `get_direction` earlier.
xygrid (dict): 2D dict with x,y coordinates as keys.
"""
if not self.paired_map_link:
try:
grid = self.xymap.grid.grid
except AttributeError:
raise MapParserError(f"requires this map being set up within an XYZgrid. No grid "
"was found (maybe it was not passed during XYMap initialization?",
self)
try:
target_map = grid[self.target_map]
except KeyError:
raise MapParserError(f"cannot find target_map '{self.target_map}' "
f"on the grid.", self)
# find the matching link on the other side
link = target_map.get_components_with_symbol(self.symbol)
if not link:
raise MapParserError(f"must have a matching '{self.symbol}' on "
f"its target_map `{self.target_map}`.", self)
if len(link) > 1:
raise MapParserError(f"must have a singl mathing '{self.symbol}' on "
f"its target_map (found {len(link)}): {link}")
# this is a link on another map
self.paired_map_link = link[0]
return self.paired_map_link
class SmartMapLink(MapLink):
"""
A 'smart' link withot visible direction, but which uses its topological surroundings
@ -985,6 +952,15 @@ class BasicMapNode(MapNode):
symbol = "#"
class MapTransitionMapNode(TransitionMapNode):
"""Teleports entering players to other map"""
symbol = "T"
display_symbol = " "
interrupt_path = True
linked_map_name = ""
linked_map_coords = None
class InterruptMapNode(MapNode):
"""A point of interest, where pathfinder will stop"""
symbol = "I"

View file

@ -1027,14 +1027,16 @@ class TestMapStressTest(TestCase):
# map transitions
class Map12aTransition(map_legend.MapTransitionLink):
class Map12aTransition(map_legend.MapTransitionMapNode):
symbol = "T"
target_map = "map12b"
linked_map_name = "map12b"
linked_map_coords = (1, 0)
class Map12bTransition(map_legend.MapTransitionLink):
class Map12bTransition(map_legend.MapTransitionMapNode):
symbol = "T"
target_map = "map12a"
linked_map_name= "map12a"
linked_map_coords = (0, 1)
class TestXYZGrid(TestCase):
@ -1063,8 +1065,8 @@ class TestXYZGrid(TestCase):
self.grid.delete()
@parameterized.expand([
((1, 0), (1, 1), ('e', 'nw', 'e')),
((1, 1), (0, 0), ('w', 'se', 'w')),
((1, 0), (1, 1), ('w', 'n', 'e')),
((1, 1), (1, 0), ('w', 's', 'e')),
])
def test_shortest_path(self, startcoord, endcoord, expected_directions):
"""

View file

@ -114,6 +114,7 @@ _CACHE_DIR = settings.CACHE_DIR
# these are all symbols used for x,y coordinate spots
DEFAULT_LEGEND = {
"#": map_legend.BasicMapNode,
"T": map_legend.MapTransitionMapNode,
"I": map_legend.InterruptMapNode,
"|": map_legend.NSMapLink,
"-": map_legend.EWMapLink,
@ -131,7 +132,6 @@ DEFAULT_LEGEND = {
"b": map_legend.BlockedMapLink,
"i": map_legend.InterruptMapLink,
't': map_legend.TeleporterMapLink,
'T': map_legend.MapTransitionLink,
}
# --------------------------------------------
@ -196,8 +196,7 @@ class XYMap:
more than one map. Used when referencing this map during map transitions,
baking of pathfinding matrices etc. This will be overridden by any 'name' given
in the MAP_DATA itself.
grid (xyzgrid.XYZGrid, optional): Reference to the top-level grid object, which
stores all maps. This is necessary for transitioning from map to another.
grid (.xyzgrid.XYZgrid): A top-level grid this map is a part of.
Notes:
The map deals with two sets of coorinate systems:
@ -219,6 +218,7 @@ class XYMap:
self.grid = grid
self.prototypes = None
# transitional mapping
self.symbol_map = None
@ -257,7 +257,7 @@ class XYMap:
def __repr__(self):
return f"<Map {self.max_X + 1}x{self.max_Y + 1}, {len(self.node_index_map)} nodes>"
def parse_first_pass(self):
def parse(self):
"""
Parses the numerical grid from the string. The first pass means parsing out
all nodes. The linking-together of nodes is not happening until the second pass
@ -368,33 +368,11 @@ class XYMap:
# store the symbol mapping for transition lookups
symbol_map[char].append(xygrid[ix][iy])
# store results
self.max_x, self.max_y = max_x, max_y
self.xygrid = xygrid
# second pass - link all nodes of the map except the inter-map traversals.
self.max_X, self.max_Y = max_X, max_Y
self.XYgrid = XYgrid
self.node_index_map = node_index_map
self.symbol_map = symbol_map
def parse_second_pass(self):
"""
Parsing, second pass. Here we loop over all nodes and have them connect to each other via
the detected linkages. For multi-map grids (that links to one another), this must run after
all maps have run through the first pass of their parsing.
This will create the linkages, build the display map for visualization and validate
all prototypes for the nodes and their connected links.
"""
node_index_map = self.node_index_map
max_x, max_y = self.max_x, self.max_y
xygrid = self.xygrid
# build all links
# build all links except the transitional links
for node in node_index_map.values():
node.scan_all_directions(xygrid)
node.build_links(xygrid)
# build display map
display_map = [[" "] * (max_x + 1) for _ in range(max_y + 1)]
@ -412,12 +390,16 @@ class XYMap:
maplink.prototype = self.prototypes.get(node_coord + (direction,), maplink.prototype)
# store results
self.display_map = display_map
self.max_x, self.max_y = max_x, max_y
self.xygrid = xygrid
def parse(self):
"""Shortcut for running the full parsing of a single map. Useful for testing."""
self.parse_first_pass()
self.parse_second_pass()
self.max_X, self.max_Y = max_X, max_Y
self.XYgrid = XYgrid
self.node_index_map = node_index_map
self.symbol_map = symbol_map
self.display_map = display_map
def _get_topology_around_coord(self, coord, dist=2):
"""

View file

@ -50,25 +50,19 @@ class XYZGrid(DefaultScript):
"""
logger.log_info("[grid] (Re)loading grid ...")
grid = {}
self.ndb.grid = {}
nmaps = 0
# generate all Maps - this will also initialize their components
# and bake any pathfinding paths (or load from disk-cache)
for mapname, mapdata in self.db.map_data.items():
logger.log_info(f"[grid] Loading map '{mapname}'...")
xymap = XYMap(dict(mapdata), name=mapname, grid=self)
xymap.parse_first_pass()
grid[mapname] = xymap
xymap.parse()
xymap.calculate_path_matrix()
self.ndb.grid[mapname] = xymap
nmaps += 1
# link maps together across grid
logger.log_info("[grid] Link {nmaps} maps (may be slow first time a map has changed) ...")
for name, xymap in grid.items():
xymap.parse_second_pass()
xymap.calculate_path_matrix()
# store
self.ndb.grid = grid
logger.log_info(f"[grid] Loaded and linked {nmaps} map(s).")
def at_init(self):