From ae2f8562009df2ef81e9acca90a5d68239675cf1 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 24 Jun 2021 00:01:50 +0200 Subject: [PATCH] Simplified map-transition logic using a transition-node rather than a -link --- evennia/contrib/xyzgrid/map_legend.py | 150 +++++++++++--------------- evennia/contrib/xyzgrid/tests.py | 14 +-- evennia/contrib/xyzgrid/xymap.py | 50 +++------ evennia/contrib/xyzgrid/xyzgrid.py | 14 +-- 4 files changed, 91 insertions(+), 137 deletions(-) diff --git a/evennia/contrib/xyzgrid/map_legend.py b/evennia/contrib/xyzgrid/map_legend.py index 64ddf9c0a9..a7e0be2e76 100644 --- a/evennia/contrib/xyzgrid/map_legend.py +++ b/evennia/contrib/xyzgrid/map_legend.py @@ -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" diff --git a/evennia/contrib/xyzgrid/tests.py b/evennia/contrib/xyzgrid/tests.py index b4a23daa60..064fb682bb 100644 --- a/evennia/contrib/xyzgrid/tests.py +++ b/evennia/contrib/xyzgrid/tests.py @@ -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): """ diff --git a/evennia/contrib/xyzgrid/xymap.py b/evennia/contrib/xyzgrid/xymap.py index dac00e29a5..e4bf13274d 100644 --- a/evennia/contrib/xyzgrid/xymap.py +++ b/evennia/contrib/xyzgrid/xymap.py @@ -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"" - 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): """ diff --git a/evennia/contrib/xyzgrid/xyzgrid.py b/evennia/contrib/xyzgrid/xyzgrid.py index 526a72b4e1..e7bb83673d 100644 --- a/evennia/contrib/xyzgrid/xyzgrid.py +++ b/evennia/contrib/xyzgrid/xyzgrid.py @@ -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):