Finished launcher, testing example

This commit is contained in:
Griatch 2021-07-07 23:45:37 +02:00
parent a1438150c0
commit 1c06363bbe
10 changed files with 504 additions and 170 deletions

View file

@ -16,6 +16,8 @@ Use `evennia xyzgrid help` for usage help.
""" """
from os.path import join as pathjoin
from django.conf import settings
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid
_HELP_SHORT = """ _HELP_SHORT = """
@ -37,7 +39,7 @@ evennia xyzgrid init
First start of the grid. This will create the XYZGrid global script. No maps are loaded yet! First start of the grid. This will create the XYZGrid global script. No maps are loaded yet!
It's safe to run this command multiple times; the grid will only be initialized once. It's safe to run this command multiple times; the grid will only be initialized once.
evennia xyzgrid add path.to.xymap.module evennia xyzgrid add path.to.xymap.module [,path, path,...]
Add one or more XYmaps (each a string-map representing one Z position along with prototypes Add one or more XYmaps (each a string-map representing one Z position along with prototypes
etc). The module will be parsed for etc). The module will be parsed for
@ -45,11 +47,12 @@ evennia xyzgrid add path.to.xymap.module
- a XYMAP_DATA a dict - a XYMAP_DATA a dict
{"map": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict} {"map": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}
describing one single XYmap, or describing one single XYmap, or
- a XYMAP_LIST - a list of multiple dicts on the XYMAP_DATA form. This allows to load - a XYMAP_DATA_LIST - a list of multiple dicts on the XYMAP_DATA form. This allows to load
multiple maps from the same module. multiple maps from the same module.
Note that adding a map does *not* build it. If maps are linked to one another, you should add Note that adding a map does *not* build it. If maps are linked to one another, you should add
all linked maps before building, or you'll get errors when spawning the linking exits. all linked maps before running 'build', or you'll get errors when creating transitional exits
between maps.
evennia xyzgrid build evennia xyzgrid build
@ -106,38 +109,77 @@ def _option_list(**suboptions):
print(str(xymap)) print(str(xymap))
def _option_init(**suboptions): def _option_init(*suboptions):
""" """
Initialize a new grid. Will fail if a Grid already exists. Initialize a new grid. Will fail if a Grid already exists.
""" """
grid = get_xyzgrid() grid = get_xyzgrid()
print(f"The grid is initalized as the Script 'XYZGrid'({grid.dbref})") print(f"The grid is initalized as the Script '{grid.key}'({grid.dbref})")
def _option_add(**suboptions):
def _option_add(*suboptions):
""" """
Add a new map to the grid. Add one or more map to the grid. Supports `add path,path,path,...`
""" """
grid = get_xyzgrid()
xymap_data_list = []
for path in suboptions:
xymap_data_list.expand(grid.maps_from_module(path))
grid.add_maps(*xymap_data_list)
def _option_build(**suboptions):
def _option_build(*suboptions):
""" """
Build the grid or part of it. Build the grid or part of it.
""" """
grid = get_xyzgrid()
def _option_initpath(**suboptions): # override grid's logger to echo directly to console
def _log(self, msg):
print(msg)
grid.log = _log
if suboptions:
opts = ''.join(suboptions).strip('()')
# coordinate tuple
try:
x, y, z = (part.strip() for part in opts.split(","))
except ValueError:
print("Build coordinate must be given as (X, Y, Z) tuple, where '*' act "
"wild cards and Z is the mapname/z-coord of the map to load.")
return
else:
x, y, z = '*', '*', '*'
grid.spawn(xyz=(x, y, z))
def _option_initpath(*suboptions):
""" """
Initialize the pathfinding matrices for grid or part of it. (Re)Initialize the pathfinding matrices for grid or part of it.
""" """
grid = get_xyzgrid()
def _option_delete(**suboptions): xymaps = grid.all_rooms()
""" nmaps = len(xymaps)
Delete the grid or parts of it. for inum, xymap in enumerate(grid.all_rooms()):
print(f"Rebuilding pathfinding matrix for xymap Z={xymap.Z} ({inum+1}/{nmaps}) ...")
xymap.calculate_path_matrix(force=True)
cachepath = pathjoin(settings.GAMEDIR, "server", ".cache")
print(f"... done. Data cached to {cachepath}.")
def _option_delete(*suboptions):
"""
Delete the grid or parts of it. Allows mapname,mapname, ...
""" """
grid = get_xyzgrid()
if not suboptions: if not suboptions:
repl = input("WARNING: This will delete the ENTIRE Grid and wipe all rooms/exits!" repl = input("WARNING: This will delete the ENTIRE Grid and wipe all rooms/exits!"
"\nObjects/Chars inside deleted rooms will be moved to their home locations." "\nObjects/Chars inside deleted rooms will be moved to their home locations."
@ -146,16 +188,35 @@ def _option_delete(**suboptions):
print("Aborted.") print("Aborted.")
else: else:
print("Deleting grid ...") print("Deleting grid ...")
grid = get_xyzgrid()
grid.delete() grid.delete()
else: else:
pass zcoords = (part.strip() for part in suboptions)
err = False
for zcoord in zcoords:
if not grid.get_map(zcoord):
print(f"Mapname/zcoord {zcoord} is not a part of the grid.")
err = True
if err:
print("Valid mapnames/zcoords are\n:", "\n ".join(
xymap.Z for xymap in grid.all_rooms()))
return
repl = input("This will delete map(s) {', '.join(zcoords)} and wipe all corresponding "
"rooms/exits!"
"\nObjects/Chars inside deleted rooms will be moved to their home locations."
"\nThis can't be undone. Are you sure you want to continue? Y/[N]?")
if repl.lower() not in ('yes', 'y'):
print("Aborted.")
else:
print("Deleting selected xymaps ...")
grid.remove_map(*zcoords, remove_objects=True)
def xyzcommand(*args): def xyzcommand(*args):
""" """
Evennia launcher command. This is made available as `evennia xyzgrid` on the command line, Evennia launcher command. This is made available as `evennia xyzgrid` on the command line,
once `settings.EXTRA_LAUNCHER_COMMANDS` is updated. once added to `settings.EXTRA_LAUNCHER_COMMANDS`.
""" """
if not args: if not args:

View file

@ -1,68 +1,263 @@
MAP = r""" """
Example xymaps to use with the XYZgrid contrib. Build outside of the game using
the `evennia xyzgrid` launcher command.
First add the launcher extension in your mygame/server/conf/settings.py:
EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.xyzgrid.launchcmd.xyzcommand'
Then
evennia xyzgrid init
evennia xyzgrid add evennia.contrib.xyzgrid.map_example
evennia xyzgrid build
"""
from evennia.contrib.xyzgrid import map_legend
# default prototype parent. It's important that
# the typeclass inherits from the XYZRoom (or XYZExit)
# the map_legend.XYZROOM_PARENT and XYZEXIT_PARENTS can also
# be used as a shortcut.
PARENT = {
"key": "An empty room",
"prototype_key": "xyzmap_room_map1",
"typeclass": "evennia.contrib.xyzgrid.xyzroom.XYZRoom",
"desc": "An empty room."
}
# -------------------- map 1 - the large tree
# this exemplifies the various map symbols
# but is not heavily prototyped
MAP1 = r"""
1 1
+ 0 1 2 3 4 5 6 7 8 9 0 + 0 1 2 3 4 5 6 7 8 9 0
10 #-#-#-#-# 9 #-------#-#-------I
| | \ \ /
9 #---+---#-#-----I 8 #-#---# #-t
\ | /
8 #-#-#-#-# #
|\ | |\ |
7 #i#-#-#+#-----#-t 7 #i#-#b--#-t
| | | |
6 #i#-#---#-#-#-#-# 5 o-#---#
| |x|x| \ /
5 o-#-#-# #-#-# 4 o-o-#-#
\ / |x|x| / d
4 o-o-#-# #-#-# 3 #-----+-------#
/ / | d
3 #-# / # 2 | |
\ / d v u
2 o-o-#-# | 1 #---#>#-#
| | u /
1 #-#-#># # 0 T-#
^ |
0 T-----#-# #-t
+ 0 1 2 3 4 5 6 7 8 9 0 + 0 1 2 3 4 5 6 7 8 9 0
1 1
""" """
# use default legend
LEGEND = { class TransitionToCave(map_legend.MapTransitionMapNode):
"""
A transition from map2 to map1
"""
symbol = 'T'
target_map_xyz = (2, 3, 'small cave')
# extends the default legend
LEGEND_MAP1 = {
'T': TransitionToCave
} }
PARENT = {
"key": "An empty dungeon room",
"prototype_key": "dungeon_doom_prot",
"typeclass": "evennia.contrib.xyzgrid.xyzrooms.XYZRoom",
"desc": "Air is cold and stale in this barren room."
}
# link coordinates to rooms # link coordinates to rooms
ROOMS = { PROTOTYPES_MAP1 = {
(1, 0): { # node/room prototypes
(3, 0): {
"key": "Dungeon Entrance", "key": "Dungeon Entrance",
"prototype_parent": PARENT, "desc": "To the west, a narrow opening leads into darkness."
"desc": "A dark entrance."
}, },
(4, 0): { (4, 1): {
"key": "Antechamber", "key": "Under the foilage of a giant tree",
"prototype_parent": PARENT, "desc": "High above the branches of a giant tree blocs out the sunlight. A slide "
"desc": "A small antechamber", "leading down from the upper branches ends here."
},
(4, 4): {
"key": "The slide",
"desc": "A slide leads down to the ground from here. It looks like a one-way trip."
},
(6, 1): {
"key": "Thorny path",
"desc": "To the east is a pathway of thorns. If you get through, you don't think you'll be "
"able to get back here the same way."
},
(8, 1): {
"key": "By a large tree",
"desc": "You are standing at the root of a great tree."
},
(8, 3): {
"key": "At the top of the tree",
"desc": "You are at the top of the tree."
},
(3, 7): {
"key": "Dense foilage",
"desc": "The foilage to the east is extra dense. It will take forever to get through it."
},
(5, 7): {
"key": "On a huge branch",
"desc": "To the east is a glowing light, may be a teleporter."
},
(9, 8): {
"key": "On an enormous branch",
"desc": "To the east is a glowing light, may be a teleporter."
},
(10, 9): {
"key": "A gorgeous view",
"desc": "The view from here is breathtaking, showing the forest stretching far and wide."
},
# default rooms
('*', '*'): {
"key": "Among the branches of a giant tree",
"desc": "These branches are wide enough to easily walk on. There's green all around."
},
# directional prototypes
(3, 0, 'w'): {
"desc": "A dark passage into the underworld."
},
}
for prot in PROTOTYPES_MAP1.values():
prot['prototype_parent'] = PARENT
XYMAP_DATA_MAP1 = {
"zcoord": "the large tree",
"map": MAP1,
"legend": LEGEND_MAP1,
"prototypes": PROTOTYPES_MAP1
}
# ------------- map2 definitions - small cave
# this gives prototypes for every room
MAP2 = r"""
+ 0 1 2 3
3 #-#-#
|x|
2 #-#-#
| \
1 #---#
| /
0 T-#-#
+ 0 1 2 3
"""
# custom map node
class TransitionToLargeTree(map_legend.MapTransitionMapNode):
"""
A transition from map1 to map2
"""
symbol = 'T'
target_map_xyz = (3, 0, 'the large tree')
# this extends the default legend (that defines #,-+ etc)
LEGEND_MAP2 = {
"T": TransitionToLargeTree
}
# prototypes for specific locations
PROTOTYPES_MAP2 = {
# node/rooms prototype overrides
(1, 0): {
"key": "The entrance",
"desc": "This is the entrance to a small cave leading into the ground. "
"Light sifts in from the outside, while cavernous passages disappear "
"into darkness."
},
(2, 0): {
"key": "A gruesome sight.",
"desc": "Something was killed here recently. The smell is unbearable."
},
(1, 1): {
"key": "A dark pathway",
"desc": "The path splits three ways here. To the north a faint light can be seen."
},
(3, 2): {
"key": "Stagnant water",
"desc": "A pool of stagnant, black water dominates this small chamber. To the nortwest "
"a faint light can be seen."
},
(0, 2): {
"key": "A dark alcove",
"desc": "This alcove is empty."
},
(1, 2): {
"key": "South-west corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones."
},
(2, 2): {
"key": "South-east corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones."
},
(1, 3): {
"key": "North-west corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones."
},
(2, 3): {
"key": "North-east corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones. To the east is a dark passage."
},
(3, 3): {
"key": "Craggy crevice",
"desc": "This is the deepest part of the dungeon. The path shrinks away and there "
"is no way to continue deeper."
},
# default fallback for undefined nodes
('*', '*'): {
"key": "A dark room",
"desc": "A dark, but empty, room."
},
# directional prototypes
(1, 0, 'w'): {
"desc": "A narrow path to the fresh air of the outside world."
},
# directional fallbacks for unset directions
('*', '*', '*'): {
"desc": "A dark passage"
} }
} }
# this is required by the prototypes, but we add it all at once so we don't
# need to add it to every line above
for prot in PROTOTYPES_MAP2.values():
prot['prototype_parent'] = PARENT
MAP_DATA = {
"name": "Dungeon of Doom", XYMAP_DATA_MAP2 = {
"map": MAP, "map": MAP2,
"legend": LEGEND, "zcoord": "the small cave",
"rooms": ROOMS, "legend": LEGEND_MAP2,
"prototypes": PROTOTYPES_MAP2
} }
XYMAP_LIST = [ # This is read by the parser
MAP_DATA XYMAP_DATA_LIST = [
XYMAP_DATA_MAP1,
XYMAP_DATA_MAP2
] ]

View file

@ -79,8 +79,8 @@ class MapNode:
'sw': ('southwest', 'sw', 'south-west'), 'sw': ('southwest', 'sw', 'south-west'),
'w': ('west', 'w'), 'w': ('west', 'w'),
'nw': ('northwest', 'nw', 'north-west'), 'nw': ('northwest', 'nw', 'north-west'),
'd' : ('down', 'd', 'do'), 'd': ('down', 'd', 'do'),
'u' : ('up', 'u'), 'u': ('up', 'u'),
} }
def __init__(self, x, y, Z, node_index=0, xymap=None): def __init__(self, x, y, Z, node_index=0, xymap=None):
@ -261,6 +261,7 @@ class MapNode:
return return
xyz = self.get_spawn_xyz() xyz = self.get_spawn_xyz()
print("xyz:", xyz, self.node_index)
try: try:
nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz) nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
@ -944,9 +945,10 @@ class SmartMapLink(MapLink):
directions[direction] = REVERSE_DIRECTIONS[direction] directions[direction] = REVERSE_DIRECTIONS[direction]
else: else:
raise MapParserError( raise MapParserError(
f"must have exactly two connections - either " "must have exactly two connections - either directly to "
f"two nodes or unambiguous link directions. Found neighbor(s) in directions " "two nodes or connecting directly to one node and with exactly one other "
f"{list(neighbors.keys())}.", self) f"link direction. The neighbor(s) in directions {list(neighbors.keys())} do "
"not fulfill these criteria.", self)
self.directions = directions self.directions = directions
return self.directions.get(start_direction) return self.directions.get(start_direction)
@ -1027,7 +1029,7 @@ class InvisibleSmartMapLink(SmartMapLink):
class BasicMapNode(MapNode): class BasicMapNode(MapNode):
"""Basic map Node""" """Basic map Node"""
symbol = "#" symbol = "#"
prototype = "xyz_room_prototype" prototype = "xyz_room"
class MapTransitionMapNode(TransitionMapNode): class MapTransitionMapNode(TransitionMapNode):
@ -1043,35 +1045,35 @@ class InterruptMapNode(MapNode):
symbol = "I" symbol = "I"
display_symbol = "#" display_symbol = "#"
interrupt_path = True interrupt_path = True
prototype = "xyz_room_prototype" prototype = "xyz_room"
class NSMapLink(MapLink): class NSMapLink(MapLink):
"""Two-way, North-South link""" """Two-way, North-South link"""
symbol = "|" symbol = "|"
directions = {"n": "s", "s": "n"} directions = {"n": "s", "s": "n"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class EWMapLink(MapLink): class EWMapLink(MapLink):
"""Two-way, East-West link""" """Two-way, East-West link"""
symbol = "-" symbol = "-"
directions = {"e": "w", "w": "e"} directions = {"e": "w", "w": "e"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class NESWMapLink(MapLink): class NESWMapLink(MapLink):
"""Two-way, NorthWest-SouthWest link""" """Two-way, NorthWest-SouthWest link"""
symbol = "/" symbol = "/"
directions = {"ne": "sw", "sw": "ne"} directions = {"ne": "sw", "sw": "ne"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class SENWMapLink(MapLink): class SENWMapLink(MapLink):
"""Two-way, SouthEast-NorthWest link""" """Two-way, SouthEast-NorthWest link"""
symbol = "\\" symbol = "\\"
directions = {"se": "nw", "nw": "se"} directions = {"se": "nw", "nw": "se"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class PlusMapLink(MapLink): class PlusMapLink(MapLink):
@ -1079,7 +1081,7 @@ class PlusMapLink(MapLink):
symbol = "+" symbol = "+"
directions = {"s": "n", "n": "s", directions = {"s": "n", "n": "s",
"e": "w", "w": "e"} "e": "w", "w": "e"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class CrossMapLink(MapLink): class CrossMapLink(MapLink):
@ -1087,35 +1089,35 @@ class CrossMapLink(MapLink):
symbol = "x" symbol = "x"
directions = {"ne": "sw", "sw": "ne", directions = {"ne": "sw", "sw": "ne",
"se": "nw", "nw": "se"} "se": "nw", "nw": "se"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class NSOneWayMapLink(MapLink): class NSOneWayMapLink(MapLink):
"""One-way North-South link""" """One-way North-South link"""
symbol = "v" symbol = "v"
directions = {"n": "s"} directions = {"n": "s"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class SNOneWayMapLink(MapLink): class SNOneWayMapLink(MapLink):
"""One-way South-North link""" """One-way South-North link"""
symbol = "^" symbol = "^"
directions = {"s": "n"} directions = {"s": "n"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class EWOneWayMapLink(MapLink): class EWOneWayMapLink(MapLink):
"""One-way East-West link""" """One-way East-West link"""
symbol = "<" symbol = "<"
directions = {"e": "w"} directions = {"e": "w"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class WEOneWayMapLink(MapLink): class WEOneWayMapLink(MapLink):
"""One-way West-East link""" """One-way West-East link"""
symbol = ">" symbol = ">"
directions = {"w": "e"} directions = {"w": "e"}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class UpMapLink(SmartMapLink): class UpMapLink(SmartMapLink):
@ -1125,7 +1127,7 @@ class UpMapLink(SmartMapLink):
# all movement over this link is 'up', regardless of where on the xygrid we move. # all movement over this link is 'up', regardless of where on the xygrid we move.
direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol, direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol,
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol} 's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class DownMapLink(UpMapLink): class DownMapLink(UpMapLink):
@ -1134,14 +1136,14 @@ class DownMapLink(UpMapLink):
# all movement over this link is 'down', regardless of where on the xygrid we move. # all movement over this link is 'down', regardless of where on the xygrid we move.
direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol, direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol,
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol} 's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class InterruptMapLink(InvisibleSmartMapLink): class InterruptMapLink(InvisibleSmartMapLink):
"""A (still passable) link that causes the pathfinder to stop before crossing.""" """A (still passable) link that causes the pathfinder to stop before crossing."""
symbol = "i" symbol = "i"
interrupt_path = True interrupt_path = True
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class BlockedMapLink(InvisibleSmartMapLink): class BlockedMapLink(InvisibleSmartMapLink):
@ -1154,7 +1156,7 @@ class BlockedMapLink(InvisibleSmartMapLink):
symbol = 'b' symbol = 'b'
weights = {'n': BIGVAL, 'ne': BIGVAL, 'e': BIGVAL, 'se': BIGVAL, weights = {'n': BIGVAL, 'ne': BIGVAL, 'e': BIGVAL, 'se': BIGVAL,
's': BIGVAL, 'sw': BIGVAL, 'w': BIGVAL, 'nw': BIGVAL} 's': BIGVAL, 'sw': BIGVAL, 'w': BIGVAL, 'nw': BIGVAL}
prototype = "xyz_exit_prototype" prototype = "xyz_exit"
class RouterMapLink(SmartRerouterMapLink): class RouterMapLink(SmartRerouterMapLink):

View file

@ -1,25 +0,0 @@
"""
Maprunner
This is a stand-alone program for baking and preparing grid-maps.
## Baking
The Dijkstra algorithm is very powerful for pathfinding, but for very large grids it can be slow
to build the initial distance-matrix. As an example, for an extreme case of 10 000 nodes, all
connected along all 8 cardinal directions, there are so many possible combinations that it
takes about 25 seconds on medium hardware to build the matrix. 40 000 nodes takes about 9 minutes.
Once the matrix is built, pathfinding across the entire grid is a <0.1s operation however. So as
long as the grid doesn't change, it's a good idea to pre-build it. Pre-building like this is
often referred to as 'baking' the asset.
This program will build and run the Dijkstra on a given map and store the result as a
serialized binary file in the `mygame/server/.cache/ directory. If it exists, the Map
will load this file. If the map changed since it was saved, the file will be automatically
be rebuilt.
"""

View file

@ -1,35 +1,32 @@
""" """
Prototypes for building the XYZ-grid into actual game-rooms. Default prototypes for building the XYZ-grid into actual game-rooms.
Add this to mygame/conf/settings/settings.py: Add this to mygame/conf/settings/settings.py:
PROTOTYPE_MODULES += ['evennia.contrib.xyzgrid.prototypes'] PROTOTYPE_MODULES += ['evennia.contrib.xyzgrid.prototypes']
The prototypes can then be used in mapping prototypes as
{'prototype_parent': 'xyz_room', ...}
and/or
{'prototype_parent': 'xyz_exit', ...}
""" """
# Note - the XYZRoom/exit parents track the XYZ coordinates automatically # required by the prototype importer
# so we don't need to add custom tags to them here.
_ROOM_PARENT = {
'prototype_tags': ("xyzroom", ),
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZRoom'
}
_EXIT_PARENT = {
'prototype_tags': ("xyzexit", ),
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZExit'
}
PROTOTYPE_LIST = [ PROTOTYPE_LIST = [
{ {
'prototype_key': 'xyz_room_prototype', 'prototype_key': 'xyz_room',
'prototype_parent': _ROOM_PARENT, 'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZRoom',
'key': "A non-descript room", 'prototype_tags': ("xyzroom", ),
},{ 'key': "A room",
'prototype_key': 'xyz_transition_room_prototype', 'desc': "An empty room."
'prototype_parent': _ROOM_PARENT, }, {
'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZMapTransitionRoom', 'prototype_key': 'xyz_exit',
},{ 'prototype_tags': ("xyzexit", ),
'prototype_key': 'xyz_exit_prototype', 'typeclass': 'evennia.contrib.xyzgrid.xyzroom.XYZExit',
'prototype_parent': _EXIT_PARENT, 'desc': "An exit."
} }
] ]

View file

@ -349,11 +349,11 @@ class _MapTest(TestCase):
map_data = {'map': MAP1, 'zcoord': "map1"} map_data = {'map': MAP1, 'zcoord': "map1"}
map_display = MAP1_DISPLAY map_display = MAP1_DISPLAY
def setUp(self): def setUp(self):
"""Set up grid and map""" """Set up grid and map"""
self.grid, err = xyzgrid.XYZGrid.create("testgrid") self.grid, err = xyzgrid.XYZGrid.create("testgrid")
self.grid.add_maps(self.map_data) self.grid.add_maps(self.map_data)
self.map = self.grid.get(self.map_data['zcoord']) self.map = self.grid.get_map(self.map_data['zcoord'])
def tearDown(self): def tearDown(self):
self.grid.delete() self.grid.delete()
@ -1155,7 +1155,7 @@ class TestXYZGrid(TestCase):
def test_str_output(self): def test_str_output(self):
"""Check the display_map""" """Check the display_map"""
xymap = self.grid.get(self.zcoord) xymap = self.grid.get_map(self.zcoord)
stripped_map = "\n".join(line.rstrip() for line in str(xymap).split('\n')) stripped_map = "\n".join(line.rstrip() for line in str(xymap).split('\n'))
self.assertEqual(MAP1_DISPLAY, stripped_map) self.assertEqual(MAP1_DISPLAY, stripped_map)
@ -1215,7 +1215,7 @@ class TestXYZGridTransition(TestCase):
test shortest-path calculations throughout the grid. test shortest-path calculations throughout the grid.
""" """
directions, _ = self.grid.get('map12a').get_shortest_path(startcoord, endcoord) directions, _ = self.grid.get_map('map12a').get_shortest_path(startcoord, endcoord)
self.assertEqual(expected_directions, tuple(directions)) self.assertEqual(expected_directions, tuple(directions))
def test_spawn(self): def test_spawn(self):
@ -1236,3 +1236,41 @@ class TestXYZGridTransition(TestCase):
# make sure exits traverse the maps # make sure exits traverse the maps
self.assertEqual(east_exit.db_destination, room2) self.assertEqual(east_exit.db_destination, room2)
self.assertEqual(west_exit.db_destination, room1) self.assertEqual(west_exit.db_destination, room1)
class TestBuildExampleGrid(TestCase):
"""
Test building the map_example
"""
def setUp(self):
# build and populate grid
self.grid, err = xyzgrid.XYZGrid.create("testgrid")
def tearDown(self):
self.grid.delete()
def test_build(self):
"""
Build the map example.
"""
mapdatas = self.grid.maps_from_module("evennia.contrib.xyzgrid.map_example")
self.assertEqual(len(mapdatas), 2)
self.grid.add_maps(*mapdatas)
self.grid.spawn()
# testing
room1a = xyzroom.XYZRoom.objects.get_xyz(xyz=(3, 0, 'the large tree'))
room1b = xyzroom.XYZRoom.objects.get_xyz(xyz=(10, 9, 'the large tree'))
room2a = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 0, 'small cave'))
room2b = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 3, 'small cave'))
self.assertEqual(room1a.key, "Dungeon Entrance")
self.assertTrue(room1a.desc.startswith("To the west"))
self.assertEqual(room1b.key, "A gorgeous view")
self.assertTrue(room1b.desc.startswith("The view from here is breathtaking."))
self.assertEqual(room2a.key, "The entrance")
self.assertTrue(room2a.desc.startswith("This is the entrance to"))
self.assertEqual(room2b.key, "North-west corner of the atrium")
self.assertTrue(room2b.desc.startswith("Sunlight sifts down"))

View file

@ -36,7 +36,7 @@ class MapError(RuntimeError):
prefix = "" prefix = ""
if node_or_link: if node_or_link:
prefix = (f"{node_or_link.__class__.__name__} '{node_or_link.symbol}' " prefix = (f"{node_or_link.__class__.__name__} '{node_or_link.symbol}' "
f"at XY=({node_or_link.X:g},{node_or_link.Y:g}) ") f"at XYZ=({node_or_link.X:g},{node_or_link.Y:g},{node_or_link.Z}) ")
self.node_or_link = node_or_link self.node_or_link = node_or_link
self.message = f"{prefix}{error}" self.message = f"{prefix}{error}"
super().__init__(self.message) super().__init__(self.message)

View file

@ -12,9 +12,7 @@ as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw',
```python ```python
# in module passed to 'Map' class. It will either a dict # in module passed to 'Map' class
# MAP_DATA with keys 'map' and (optionally) 'legend', or
# the MAP/LEGEND variables directly.
MAP = r''' MAP = r'''
1 1
@ -47,10 +45,11 @@ as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw',
''' '''
LEGEND = {'#': xyzgrid.MapNode, '|': xyzgrid.NSMapLink,...} LEGEND = {'#': xyzgrid.MapNode, '|': xyzgrid.NSMapLink,...}
# optional, for more control # read by parser if XYMAP_DATA_LIST doesn't exist
MAP_DATA = { XYMAP_DATA = {
"map": MAP, "map": MAP,
"legend": LEGEND, "legend": LEGEND,
"zcoord": "City of Foo", "zcoord": "City of Foo",
@ -62,6 +61,11 @@ as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw',
} }
# will be parsed first, allows for multiple map-data dicts from one module
XYMAP_DATA_LIST = [
XYMAP_DATA
]
``` ```
The two `+` signs in the upper/lower left corners are required and marks the edge of the map area. The two `+` signs in the upper/lower left corners are required and marks the edge of the map area.
@ -102,7 +106,7 @@ except ImportError as err:
f"{err}\nThe XYZgrid contrib requires " f"{err}\nThe XYZgrid contrib requires "
"the SciPy package. Install with `pip install scipy'.") "the SciPy package. Install with `pip install scipy'.")
from django.conf import settings from django.conf import settings
from evennia.utils.utils import variable_from_module, mod_import from evennia.utils.utils import variable_from_module, mod_import, is_iter
from evennia.utils import logger from evennia.utils import logger
from evennia.prototypes import prototypes as protlib from evennia.prototypes import prototypes as protlib
@ -140,7 +144,6 @@ DEFAULT_LEGEND = {
} }
# -------------------------------------------- # --------------------------------------------
# Map parser implementation # Map parser implementation
@ -309,13 +312,18 @@ class XYMap:
else: else:
# read from contents of module # read from contents of module
mod = mod_import(map_module_or_dict) mod = mod_import(map_module_or_dict)
mapdata = variable_from_module(mod, "MAP_DATA") mapdata_list = variable_from_module(mod, "XYMAP_DATA_LIST")
if mapdata_list and self.Z:
# use the stored Z value to figure out which map data we want
mapping = {mapdata.get("zcoord") for mapdata in mapdata_list}
mapdata = mapping.get(self.Z, {})
if not mapdata: if not mapdata:
# try to read mapdata directly from global variables mapdata = variable_from_module(mod, "XYMAP_DATA")
mapdata['zcoord'] = variable_from_module(mod, "ZCOORD", default=self.name)
mapdata['map'] = variable_from_module(mod, "MAP") if not mapdata:
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND) raise MapError("No valid XYMAP_DATA or XYMAP_DATA_LIST could be found from "
mapdata['prototypes'] = variable_from_module(mod, "PROTOTYPES", default={}) f"{map_module_or_dict}.")
# validate # validate
if any(key for key in mapdata if key not in MAP_DATA_KEYS): if any(key for key in mapdata if key not in MAP_DATA_KEYS):
@ -330,10 +338,9 @@ class XYMap:
"`.display_symbol` property to change how it is " "`.display_symbol` property to change how it is "
"displayed.") "displayed.")
if 'map' not in mapdata or not mapdata['map']: if 'map' not in mapdata or not mapdata['map']:
raise MapError("No map found. Add 'map' key to map-data (MAP_DATA) dict or " raise MapError("No map found. Add 'map' key to map-data dict.")
"add variable MAP to a module passed into the parser.")
for key, prototype in mapdata.get('prototypes', {}).items(): for key, prototype in mapdata.get('prototypes', {}).items():
if not is_iter(key) and (2 <= len(key) <= 3): if not (is_iter(key) and (2 <= len(key) <= 3)):
raise MapError(f"Prototype override key {key} is malformed: It must be a " raise MapError(f"Prototype override key {key} is malformed: It must be a "
"coordinate (X, Y) for nodes or (X, Y, direction) for links; " "coordinate (X, Y) for nodes or (X, Y, direction) for links; "
"where direction is a supported direction string ('n', 'ne', etc).") "where direction is a supported direction string ('n', 'ne', etc).")
@ -491,10 +498,13 @@ class XYMap:
for node in node_index_map.values(): for node in node_index_map.values():
node_coord = (node.X, node.Y) node_coord = (node.X, node.Y)
# load prototype from override, or use default # load prototype from override, or use default
node.prototype = self.prototypes.get(node_coord, node.prototype) node.prototype = self.prototypes.get(
node_coord, self.prototypes.get(('*', '*'), node.prototype))
# do the same for links (x, y, direction) coords # do the same for links (x, y, direction) coords
for direction, maplink in node.first_links.items(): for direction, maplink in node.first_links.items():
maplink.prototype = self.prototypes.get(node_coord + (direction,), maplink.prototype) maplink.prototype = self.prototypes.get(
node_coord + (direction,),
self.prototypes.get(('*', '*', '*'), maplink.prototype))
# store # store
self.display_map = display_map self.display_map = display_map
@ -547,13 +557,16 @@ class XYMap:
points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist) points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist)
return list(set(points)), xmin, xmax, ymin, ymax return list(set(points)), xmin, xmax, ymin, ymax
def calculate_path_matrix(self): def calculate_path_matrix(self, force=False):
""" """
Solve the pathfinding problem using Dijkstra's algorithm. This will try to Solve the pathfinding problem using Dijkstra's algorithm. This will try to
load the solution from disk if possible. load the solution from disk if possible.
Args:
force (bool, optional): If the cache should always be rebuilt.
""" """
if self.pathfinder_baked_filename and isfile(self.pathfinder_baked_filename): if not force and self.pathfinder_baked_filename and isfile(self.pathfinder_baked_filename):
# check if the solution for this grid was already solved previously. # check if the solution for this grid was already solved previously.
mapstr, dist_matrix, pathfinding_routes = "", None, None mapstr, dist_matrix, pathfinding_routes = "", None, None
@ -569,7 +582,6 @@ class XYMap:
# we can re-use the stored data! # we can re-use the stored data!
self.dist_matrix = dist_matrix self.dist_matrix = dist_matrix
self.pathfinding_routes = pathfinding_routes self.pathfinding_routes = pathfinding_routes
return
# build a matrix representing the map graph, with 0s as impassable areas # build a matrix representing the map graph, with 0s as impassable areas
@ -615,7 +627,7 @@ class XYMap:
wildcard = '*' wildcard = '*'
spawned = [] spawned = []
for node in self.node_index_map.values(): for node in sorted(self.node_index_map.values(), key=lambda n: (n.Y, n.X)):
if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)): if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)):
node.spawn() node.spawn()
spawned.append(node) spawned.append(node)
@ -643,7 +655,7 @@ class XYMap:
wildcard = '*' wildcard = '*'
if not nodes: if not nodes:
nodes = self.node_index_map.values() nodes = sorted(self.node_index_map.values(), key=lambda n: (n.Z, n.Y, n.X))
for node in nodes: for node in nodes:
if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)): if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)):

View file

@ -18,8 +18,9 @@ The grid has three main functions:
""" """
from evennia.scripts.scripts import DefaultScript from evennia.scripts.scripts import DefaultScript
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.utils import variable_from_module, make_iter
from .xymap import XYMap from .xymap import XYMap
from .xyzroom import XYZRoom from .xyzroom import XYZRoom, XYZExit
class XYZGrid(DefaultScript): class XYZGrid(DefaultScript):
@ -40,7 +41,7 @@ class XYZGrid(DefaultScript):
self.reload() self.reload()
return self.ndb.grid return self.ndb.grid
def get(self, zcoord): def get_map(self, zcoord):
""" """
Get a specific xymap. Get a specific xymap.
@ -53,7 +54,7 @@ class XYZGrid(DefaultScript):
""" """
return self.grid.get(zcoord) return self.grid.get(zcoord)
def all(self): def all_maps(self):
""" """
Get all xymaps stored in the grid. Get all xymaps stored in the grid.
@ -63,20 +64,55 @@ class XYZGrid(DefaultScript):
""" """
return self.grid return self.grid
def log(self, msg):
logger.log_info(f"|grid| {msg}")
def get_room(xyz, **kwargs):
"""
Get room object from XYZ coordinate.
Args:
xyz (tuple): X,Y,Z coordinate of room to fetch.
Returns:
XYZRoom: The found room.
Raises:
XYZRoom.DoesNotExist: If room is not found.
Notes:
This assumes the room was previously built.
"""
return XYZRoom.objects.get_xyz(xyz=xyz, **kwargs)
def get_exit(xyz, name='north', **kwargs):
"""
Get exit object at coordinate.
Args:
xyz (tuple): X,Y,Z coordinate of the room the
exit leads out of.
name (str): The full name of the exit, e.g. 'north' or 'northwest'.
"""
kwargs['db_key'] = name
return XYZExit.objects.get_xyz_exit(xyz=xyz, **kwargs)
def reload(self): def reload(self):
""" """
Reload and rebuild the grid. This is done on a server reload and is also necessary if adding Reload and rebuild the grid. This is done on a server reload and is also necessary if adding
a new map since this may introduce new between-map traversals. a new map since this may introduce new between-map traversals.
""" """
logger.log_info("[grid] (Re)loading grid ...") self.log("(Re)loading grid ...")
self.ndb.grid = {} self.ndb.grid = {}
nmaps = 0 nmaps = 0
# generate all Maps - this will also initialize their components # generate all Maps - this will also initialize their components
# and bake any pathfinding paths (or load from disk-cache) # and bake any pathfinding paths (or load from disk-cache)
for zcoord, mapdata in self.db.map_data.items(): for zcoord, mapdata in self.db.map_data.items():
logger.log_info(f"[grid] Loading map '{zcoord}'...") self.log(f"Loading map '{zcoord}'...")
xymap = XYMap(dict(mapdata), Z=zcoord, xyzgrid=self) xymap = XYMap(dict(mapdata), Z=zcoord, xyzgrid=self)
xymap.parse() xymap.parse()
xymap.calculate_path_matrix() xymap.calculate_path_matrix()
@ -84,7 +120,7 @@ class XYZGrid(DefaultScript):
nmaps += 1 nmaps += 1
# store # store
logger.log_info(f"[grid] Loaded and linked {nmaps} map(s).") self.log(f"Loaded and linked {nmaps} map(s).")
def at_init(self): def at_init(self):
""" """
@ -94,6 +130,27 @@ class XYZGrid(DefaultScript):
""" """
self.reload() self.reload()
def maps_from_module(self, module):
"""
Load map data from module. The loader will look for a dict XYMAP_DATA or a list of
XYMAP_DATA_LIST (a list of XYMAP_DATA dicts). Each XYMAP_DATA dict should contain
`{"xymap": mapstring, "zcoord": mapname/zcoord, "legend": dict, "prototypes": dict}`.
Args:
module (module or str): A module or python-path to a module containing
map data as either `XYMAP_DATA` or `XYMAP_DATA_LIST` variables.
Returns:
list: List of zero, one or more xy-map data dicts loaded from the module.
"""
map_data_list = variable_from_module(module, "XYMAP_DATA_LIST")
if not map_data_list:
map_data_list = variable_from_module(module, "XYMAP_DATA")
if map_data_list:
map_data_list = make_iter(map_data_list)
return map_data_list
def add_maps(self, *mapdatas): def add_maps(self, *mapdatas):
""" """
Add map or maps to the grid. Add map or maps to the grid.
@ -176,12 +233,12 @@ class XYZGrid(DefaultScript):
# first build all nodes/rooms # first build all nodes/rooms
for zcoord, xymap in xymaps.items(): for zcoord, xymap in xymaps.items():
logger.log_info(f"[grid] spawning/updating nodes for {zcoord} ...") self.log(f"spawning/updating nodes for {zcoord} ...")
xymap.spawn_nodes(xy=(x, y)) xymap.spawn_nodes(xy=(x, y))
# next build all links between nodes (including between maps) # next build all links between nodes (including between maps)
for zcoord, xymap in xymaps.items(): for zcoord, xymap in xymaps.items():
logger.log_info(f"[grid] spawning/updating links for {zcoord} ...") self.log(f"spawning/updating links for {zcoord} ...")
xymap.spawn_links(xy=(x, y), directions=directions) xymap.spawn_links(xy=(x, y), directions=directions)

View file

@ -10,7 +10,6 @@ used as stand-alone XYZ-coordinate-aware rooms.
from django.db.models import Q from django.db.models import Q
from evennia.objects.objects import DefaultRoom, DefaultExit from evennia.objects.objects import DefaultRoom, DefaultExit
from evennia.objects.manager import ObjectManager from evennia.objects.manager import ObjectManager
from evennia.utils.utils import inherits_from
# name of all tag categories. Note that the Z-coordinate is # name of all tag categories. Note that the Z-coordinate is
# the `map_name` of the XYZgrid # the `map_name` of the XYZgrid
@ -50,8 +49,6 @@ class XYZManager(ObjectManager):
x, y, z = xyz x, y, z = xyz
wildcard = '*' wildcard = '*'
return ( return (
self self
.filter_family(**kwargs) .filter_family(**kwargs)