First working function of map-spawning

This commit is contained in:
Griatch 2021-07-04 18:03:07 +02:00
parent 5ba16cf63f
commit cc820beb98
5 changed files with 327 additions and 208 deletions

View file

@ -73,7 +73,7 @@ class MapNode:
direction_spawn_defaults = {
'n': ('north', 'n'),
'ne': ('northeast', 'ne', 'north-east'),
'e': ('east',),
'e': ('east', 'e'),
'se': ('southeast', 'se', 'south-east'),
's': ('south', 's'),
'sw': ('southwest', 'sw', 'south-west'),
@ -231,7 +231,7 @@ class MapNode:
"""
return self.symbol if self.display_symbol is None else self.display_symbol
def get_spawn_coords(self):
def get_spawn_xyz(self):
"""
This should return the XYZ-coordinates for spawning this node. This normally
the XYZ of the current map, but for traversal-nodes, it can also be the location
@ -260,15 +260,15 @@ class MapNode:
# a 'virtual' node.
return
coord = self.get_spawn_coords()
xyz = self.get_spawn_xyz()
try:
nodeobj = NodeTypeclass.objects.get_xyz(coord=coord)
nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
except NodeTypeclass.DoesNotExist:
# create a new entity with proper coordinates etc
nodeobj, err = NodeTypeclass.create(
self.prototype.get('key', 'An Empty room'),
coord=coord
xyz=xyz
)
if err:
raise RuntimeError(err)
@ -277,12 +277,12 @@ class MapNode:
spawner.batch_update_objects_with_prototype(
self.prototype, objects=[nodeobj], exact=False)
def spawn_links(self, only_directions=None):
def spawn_links(self, directions=None):
"""
Build actual in-game exits based on the links out of this room.
Args:
only_directions (list, optional): If given, this should be a list of supported
directions (list, optional): If given, this should be a list of supported
directions (n, ne, etc). Only links in these directions will be spawned
for this node.
@ -290,7 +290,12 @@ class MapNode:
the entire XYZgrid. This creates/syncs all exits to their locations and destinations.
"""
coord = (self.X, self.Y, self.Z)
if not self.prototype:
# no exits to spawn out of a 'virtual' node.
return
xyz = (self.X, self.Y, self.Z)
direction_limits = directions
global ExitTypeclass
if not ExitTypeclass:
@ -308,7 +313,7 @@ class MapNode:
# we need to search for exits in all directions since some
# may have been removed since last sync
linkobjs = {exi.db_key.lower(): exi
for exi in ExitTypeclass.objects.filter_xyz(coord=coord)}
for exi in ExitTypeclass.objects.filter_xyz(xyz=xyz)}
# figure out if the topology changed between grid and map (will always
# build all exits first run)
@ -321,20 +326,25 @@ class MapNode:
else:
# missing in linkobjs - create a new exit
key, aliases, direction, link = maplinks[differing_key]
exitnode = self.links[direction]
linkobjs[direction] = ExitTypeclass.create(
# either get name from the prototype or use our custom set
if direction_limits and direction not in direction_limits:
continue
exitnode = self.links[direction]
exi, err = ExitTypeclass.create(
key,
coord=coord,
destination_coord=exitnode.get_spawn_coords(),
xyz=xyz,
xyz_destination=exitnode.get_spawn_xyz(),
aliases=aliases,
)
if err:
raise RuntimeError(err)
linkobjs[key.lower()] = exi
# apply prototypes to catch any changes
for direction, linkobj in linkobjs:
for key, linkobj in linkobjs.items():
spawner.batch_update_objects_with_prototype(
maplinks[direction].prototype, objects=[linkobj], exact=False)
maplinks[key.lower()][3].prototype, objects=[linkobj], exact=False)
def unspawn(self):
"""
@ -345,8 +355,10 @@ class MapNode:
if not NodeTypeclass:
from .room import XYZRoom as NodeTypeclass
xyz = (self.X, self.Y, self.Z)
try:
nodeobj = NodeTypeclass.objects.get_xyz(coord=coord)
nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
except NodeTypeclass.DoesNotExist:
# no object exists
pass
@ -364,7 +376,7 @@ class TransitionMapNode(MapNode):
to this node.
Properties:
- `target_map_coord` (tuple) - the (X, Y, Z) coordinate of a node on the other map to teleport
- `target_map_xyz` (tuple) - the (X, Y, Z) coordinate of a node on the other map to teleport
to when moving to this node. This should not be another TransitionMapNode (see below for
how to make a two-way link).
@ -380,16 +392,21 @@ class TransitionMapNode(MapNode):
"""
symbol = 'T'
display_symbol = ' '
# X,Y,Z coordinates of target node (not a transitionalmapnode)
taget_map_coord = (None, None, None)
# X,Y,Z coordinates of target node
taget_map_xyz = (None, None, None)
def get_spawn_coords(self):
def get_spawn_xyz(self):
"""
Make sure to return the coord of the *target* - this will be used when building
the exit to this node (since the prototype is None, this node itself will not be built).
"""
return self.target_map_coord
if any(True for coord in self.target_map_xyz if coord in (None, 'unset')):
raise MapParserError(f"(Z={self.xymap.Z}) has not defined its "
"`.target_map_xyz` property. It must point "
"to another valid xymap (Z coordinate).", self)
return self.target_map_xyz
def build_links(self):
"""Check so we don't have too many links"""
@ -449,7 +466,7 @@ class MapLink:
of the link is only used to determine its destination). This can be overridden on a
per-direction basis.
- `spawn_aliases` (list): A list of [key, alias, alias, ...] for the node to use when spawning
exits from this link. If not given, a sane set of defaults (n=north etc) will be used. This
exits from this link. If not given, a sane set of defaults ((north, n) etc) will be used. This
is required if you use any custom directions outside of the cardinal directions + up/down.
"""
@ -1017,8 +1034,8 @@ class MapTransitionMapNode(TransitionMapNode):
"""Transition-target to other map"""
symbol = "T"
display_symbol = " "
target_map_coords = (0, 0, 'unset') # must be changed
prototype = None # important!
prototype = None # important to leave None!
target_map_xyz = (None, None, None) # must be set manually
class InterruptMapNode(MapNode):

View file

@ -9,7 +9,7 @@ from random import randint
from unittest import TestCase
from parameterized import parameterized
from django.test import override_settings
from . import xymap, xyzgrid, map_legend
from . import xymap, xyzgrid, map_legend, xyzroom
MAP1 = """
@ -341,14 +341,36 @@ MAP12b = r"""
"""
class TestMap1(TestCase):
class _MapTest(TestCase):
"""
Parent for map tests
"""
map_data = {
'map': MAP1,
'zcoord': "map1",
}
map_display = MAP1_DISPLAY
def setUp(self):
"""Set up grid and map"""
self.grid, err = xyzgrid.XYZGrid.create("testgrid")
self.grid.add_maps(self.map_data)
self.map = self.grid.get(self.map_data['zcoord'])
def tearDown(self):
self.grid.delete()
class TestMap1(_MapTest):
"""
Test the Map class with a simple 4-node map
"""
def setUp(self):
self.map = xymap.XYMap({"map": MAP1}, Z="testmap")
self.map.parse()
# def setUp(self):
# self.map = xymap.XYMap({"map": MAP1}, Z="testmap")
# self.map.parse()
def test_str_output(self):
"""Check the display_map"""
@ -1057,17 +1079,23 @@ class TestXYZGrid(TestCase):
def test_spawn(self):
"""Spawn objects for the grid"""
self.grid.spawn()
# import sys
# sys.stderr.write("\nrooms: " + repr(xyzroom.XYZRoom.objects.all()))
# sys.stderr.write("\n\nexits: " + repr(xyzroom.XYZExit.objects.all()) + "\n")
self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 4)
self.assertEqual(xyzroom.XYZExit.objects.all().count(), 8)
# map transitions
class Map12aTransition(map_legend.MapTransitionMapNode):
symbol = "T"
target_map_coords = (1, 0, "map12b")
target_map_xyz = (1, 0, "map12b")
class Map12bTransition(map_legend.MapTransitionMapNode):
symbol = "T"
target_map_coords = (0, 1, "map12a")
target_map_xyz = (0, 1, "map12a")
class TestXYZGridTransition(TestCase):
@ -1112,4 +1140,17 @@ class TestXYZGridTransition(TestCase):
Spawn the two maps into actual objects.
"""
# from evennia import set_trace;set_trace()
self.grid.spawn()
self.assertEqual(xyzroom.XYZRoom.objects.all().count(), 6)
self.assertEqual(xyzroom.XYZExit.objects.all().count(), 10)
room1 = xyzroom.XYZRoom.objects.get_xyz(xyz=(0, 1, 'map12a'))
room2 = xyzroom.XYZRoom.objects.get_xyz(xyz=(1, 0, 'map12b'))
east_exit = [exi for exi in room1.exits if exi.db_key == 'east'][0]
west_exit = [exi for exi in room2.exits if exi.db_key == 'west'][0]
# make sure exits traverse the maps
self.assertEqual(east_exit.db_destination, room2)
self.assertEqual(west_exit.db_destination, room1)

View file

@ -272,7 +272,8 @@ class XYMap:
return "\n".join("".join(line) for line in self.display_map[::-1])
def __repr__(self):
return f"<Map {self.max_X + 1}x{self.max_Y + 1}, {len(self.node_index_map)} nodes>"
return (f"<XYMap(Z={self.Z}), {self.max_X + 1}x{self.max_Y + 1}, "
f"{len(self.node_index_map)} nodes>")
def reload(self, map_module_or_dict=None):
"""
@ -498,20 +499,20 @@ class XYMap:
# store
self.display_map = display_map
def _get_topology_around_coord(self, coord, dist=2):
def _get_topology_around_coord(self, xy, dist=2):
"""
Get all links and nodes up to a certain distance from an XY coordinate.
Args:
coord (tuple), the X,Y coordinate of the center point.
xy (tuple), the X,Y coordinate of the center point.
dist (int): How many nodes away from center point to find paths for.
Returns:
tuple: A tuple of 5 elements `(coords, xmin, xmax, ymin, ymax)`, where the
tuple: A tuple of 5 elements `(xy_coords, xmin, xmax, ymin, ymax)`, where the
first element is a list of xy-coordinates (on xygrid) for all linked nodes within
range. This is meant to be used with the xygrid for extracting a subset
for display purposes. The others are the minimum size of the rectangle
surrounding the area containing `coords`.
surrounding the area containing `xy_coords`.
Notes:
This performs a depth-first pass down the the given dist.
@ -542,7 +543,7 @@ class XYMap:
return points, xmin, xmax, ymin, ymax
center_node = self.get_node_from_coord(coord)
center_node = self.get_node_from_coord(xy)
points, xmin, xmax, ymin, ymax = _scan_neighbors(center_node, [], dist=dist)
return list(set(points)), xmin, xmax, ymin, ymax
@ -591,7 +592,7 @@ class XYMap:
pickle.dump((self.mapstring, self.dist_matrix, self.pathfinding_routes),
fil, protocol=4)
def spawn_nodes(self, coord=(None, None)):
def spawn_nodes(self, xy=('*', '*')):
"""
Convert the nodes of this XYMap into actual in-world rooms by spawning their
related prototypes in the correct coordinate positions. This must be done *first*
@ -599,58 +600,61 @@ class XYMap:
to exist. It's also possible to only spawn a subset of the map
Args:
coord (tuple, optional): An (X,Y) coordinate of node(s). `None` acts as a wildcard.
xy (tuple, optional): An (X,Y) coordinate of node(s). `'*'` acts as a wildcard.
Examples:
- `coord=(1, 3) - spawn (1,3) coordinate only.
- `coord=(None, 1) - spawn all nodes in the first row of the map only.
- `coord=(None, None)` - spawn all nodes
- `xy=(1, 3) - spawn (1,3) coordinate only.
- `xy=('*', 1) - spawn all nodes in the first row of the map only.
- `xy=('*', '*')` - spawn all nodes
Returns:
list: A list of nodes that were spawned.
"""
x, y = coord
x, y = xy
wildcard = '*'
spawned = []
for node in self.node_index_map.values():
if (x is None or x == node.X) and (y is None or y == node.Y):
if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)):
node.spawn()
spawned.append(node)
return spawned
def spawn_links(self, coord=(None, None), nodes=None, only_directions=None):
def spawn_links(self, xy=('*', '*'), nodes=None, directions=None):
"""
Convert links of this XYMap into actual in-game exits by spawning their related
prototypes. It's possible to only spawn a specic exit by specifying the node and
Args:
coord (tuple, optional): An (X,Y) coordinate of node(s). `None` acts as a wildcard.
xy (tuple, optional): An (X,Y) coordinate of node(s). `'*'` acts as a wildcard.
nodes (list, optional): If given, only consider links out of these nodes. This also
affects `coords`, so that if there are no nodes of given coords in `nodes`, no
affects `xy`, so that if there are no nodes of given coords in `nodes`, no
links will be spawned at all.
directions (list, optional): A list of cardinal directions ('n', 'ne' etc). If given,
sync only the exit in the given directions (`coords` limits which links out of which
nodes should be considered). `None` acts as a wildcard.
sync only the exit in the given directions (`xy` limits which links out of which
nodes should be considered). If unset, there are no limits to directions.
Examples:
- `coord=(1, 3 )`, `direction='ne'` - sync only the north-eastern exit
- `xy=(1, 3 )`, `direction='ne'` - sync only the north-eastern exit
out of the (1, 3) node.
"""
x, y = coord
x, y = xy
wildcard = '*'
if not nodes:
nodes = self.node_index_map.values()
for node in nodes:
if (x is None or x == node.X) and (y is None or y == node.Y):
node.spawn_links(only_directions=only_directions)
if (x in (wildcard, node.X)) and (y in (wildcard, node.Y)):
node.spawn_links(directions=directions)
def get_node_from_coord(self, coords):
def get_node_from_coord(self, xy):
"""
Get a MapNode from a coordinate.
Args:
coords (tuple): X,Y coordinates on XYgrid.
xy (tuple): X,Y coordinate on XYgrid.
Returns:
MapNode: The node found at the given coordinates. Returns
@ -664,12 +668,12 @@ class XYMap:
if not self.XYgrid:
self.parse()
iX, iY = coords
iX, iY = xy
if not ((0 <= iX <= self.max_X) and (0 <= iY <= self.max_Y)):
raise MapError(f"get_node_from_coord got coordinate {coords} which is "
raise MapError(f"get_node_from_coord got coordinate {xy} which is "
f"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).")
try:
return self.XYgrid[coords[0]][coords[1]]
return self.XYgrid[iX][iY]
except KeyError:
return None
@ -686,14 +690,14 @@ class XYMap:
"""
return self.symbol_map.get(symbol, [])
def get_shortest_path(self, startcoord, endcoord):
def get_shortest_path(self, start_xy, end_xy):
"""
Get the shortest route between two points on the grid.
Args:
startcoord (tuple): A starting (X,Y) coordinate on the XYgrid (in-game coordinate) for
start_xy (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)
end_xy (tuple or MapNode): The end (X,Y) coordinate on the XYgrid (in-game coordinate)
we want to find the shortest route to.
Returns:
@ -702,8 +706,8 @@ class XYMap:
the full path including the start- and end-node.
"""
startnode = self.get_node_from_coord(startcoord)
endnode = self.get_node_from_coord(endcoord)
startnode = self.get_node_from_coord(start_xy)
endnode = self.get_node_from_coord(end_xy)
if not (startnode and endnode):
# no node at given coordinate. No path is possible.
@ -751,7 +755,7 @@ class XYMap:
return directions, path
def get_visual_range(self, coord, dist=2, mode='nodes',
def get_visual_range(self, xy, dist=2, mode='nodes',
character='@',
target=None, target_path_style="|y{display_symbol}|n",
max_size=None,
@ -761,7 +765,7 @@ class XYMap:
of nodes or grid points in every direction.
Args:
coord (tuple): (X,Y) in-world coordinate location. If this is not the location
xy (tuple): (X,Y) in-world coordinate location. If this is not the location
of a node on the grid, the `character` or the empty-space symbol (by default
an empty space) will be shown.
dist (int, optional): Number of gridpoints distance to show. Which
@ -771,7 +775,7 @@ class XYMap:
number of xy grid points in all directions and doesn't care about if visible
nodes are reachable or not. If 'nodes', distance measure how many linked nodes
away from the center coordinate to display.
character (str, optional): Place this symbol at the `coord` position
character (str, optional): Place this symbol at the `xy` position
of the displayed map. The center node' symbol is shown if this is falsy.
target (tuple, optional): A target XY coordinate to go to. The path to this
(or the beginning of said path, if outside of visual range) will be
@ -823,7 +827,7 @@ class XYMap:
# @-#
"""
iX, iY = coord
iX, iY = xy
# 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))
@ -835,14 +839,14 @@ class XYMap:
gridmap = self.display_map
ixc, iyc = ix, iy
elif dist is None or dist <= 0 or not self.get_node_from_coord(coord):
elif dist is None or dist <= 0 or not self.get_node_from_coord(xy):
# There is no node at these coordinates. Show
# nothing but ourselves or emptiness
return character if character else self.empty_symbol
elif mode == 'nodes':
# dist measures only full, reachable nodes.
points, xmin, xmax, ymin, ymax = self._get_topology_around_coord(coord, dist=dist)
points, xmin, xmax, ymin, ymax = self._get_topology_around_coord(xy, dist=dist)
ixc, iyc = ix - xmin, iy - ymin
# note - override width/height here since our grid is
@ -878,7 +882,7 @@ class XYMap:
else:
_target_path_style = _default_callable
_, path = self.get_shortest_path(coord, target)
_, path = self.get_shortest_path(xy, target)
maxstep = dist if mode == 'nodes' else dist / 2
nsteps = 0

View file

@ -106,12 +106,15 @@ class XYZGrid(DefaultScript):
remove_objects (bool, optional): If the synced database objects (rooms/exits) should
be removed alongside this map.
"""
# from evennia import set_trace;set_trace()
for zcoord in zcoords:
if zcoord in self.db.map_data:
self.db.map_data.pop(zcoord)
if remove_objects:
# this should also remove all exits automatically
XYZRoom.objects.filter_xyz(coord=(None, None, zcoord)).delete()
# we can't batch-delete because we want to run the .delete
# method that also wipes exits and moves content to save locations
for xyzroom in XYZRoom.objects.filter_xyz(xyz=('*', '*', zcoord)):
xyzroom.delete()
self.reload()
def delete(self):
@ -121,41 +124,42 @@ class XYZGrid(DefaultScript):
"""
self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True)
def spawn(self, coord=(None, None, None), only_directions=None):
def spawn(self, xyz=('*', '*', '*'), directions=None):
"""
Create/recreate/update the in-game grid based on the stored Maps or for a specific Map
or coordinate.
Args:
coord (tuple, optional): An (X,Y,Z) coordinate, where Z is the name of the map. `None`
xyz (tuple, optional): An (X,Y,Z) coordinate, where Z is the name of the map. `'*'`
acts as a wildcard.
only_directions (list, optional): A list of cardinal directions ('n', 'ne' etc).
If given, spawn exits only the given direction. `None` acts as a wildcard.
directions (list, optional): A list of cardinal directions ('n', 'ne' etc).
Spawn exits only the given direction. If unset, all needed directions are spawned.
Examples:
- `coord=(1, 3, 'foo')` - sync a specific element of map 'foo' only.
- `coord=(None, None, 'foo') - sync all elements of map 'foo'
- `coord=(1, 3, None) - sync all (1,3) coordinates on all maps (rarely useful)
- `coord=(None, None, None)` - sync all maps.
- `coord=(1, 3, 'foo')`, `direction='ne'` - sync only the north-eastern exit
- `xyz=('*', '*', '*')` (default) - spawn/update all maps.
- `xyz=(1, 3, 'foo')` - sync a specific element of map 'foo' only.
- `xyz=('*', '*', 'foo') - sync all elements of map 'foo'
- `xyz=(1, 3, '*') - sync all (1,3) coordinates on all maps (rarely useful)
- `xyz=(1, 3, 'foo')`, `direction='ne'` - sync only the north-eastern exit
out of the specific node on map 'foo'.
"""
x, y, z = coord
x, y, z = xyz
wildcard = '*'
if z is None:
if z == wildcard:
xymaps = self.grid
elif z in self.ndb.grid:
xymaps = [self.grid[z]]
elif self.ndb.grid and z in self.ndb.grid:
xymaps = {z: self.grid[z]}
else:
raise RuntimeError(f"The 'z' coordinate/name '{z}' is not found on the grid.")
# first build all nodes/rooms
for zcoord, xymap in xymaps.items():
logger.log_info(f"[grid] spawning/updating nodes for {zcoord} ...")
xymap.spawn_nodes(coord=(x, y))
xymap.spawn_nodes(xy=(x, y))
# next build all links between nodes (including between maps)
for zcoord, xymap in xymaps.items():
logger.log_info(f"[grid] spawning/updating links for {zcoord} ...")
xymap.spawn_links(coord=(x, y), only_directions=only_directions)
xymap.spawn_links(xy=(x, y), directions=directions)

View file

@ -30,16 +30,15 @@ class XYZManager(ObjectManager):
efficiently querying the room in the database based on XY coordinates.
"""
def filter_xyz(self, coord=(None, None, 'map'), **kwargs):
def filter_xyz(self, xyz=('*', '*', '*'), **kwargs):
"""
Filter queryset based on map as well as x- or y-coordinate, or both. The map-name is
required but not the coordinates - if only one coordinate is given, multiple rooms may be
returned from the same coordinate row/column. If both coordinates are omitted (set to
`None`), then all rooms of a given map is returned.
Filter queryset based on XYZ position on the grid. The Z-position is the name of the XYMap
Set a coordinate to `'*'` to act as a wildcard (setting all coords to `*` will thus find
*all* XYZ rooms). This will also find children of XYZRooms on the given coordinates.
Kwargs:
coord (tuple, optional): A tuple (X, Y, Z) where each element is either
an `int`, `str` or `None`. `None` acts as a wild card. Note that
xyz (tuple, optional): A coordinate tuple (X, Y, Z) where each element is either
an `int` or `str`. The character `'*'` acts as a wild card. Note that
the `Z`-coordinate is the name of the map (case-sensitive) in the XYZgrid contrib.
**kwargs: All other kwargs are passed on to the query.
@ -48,25 +47,34 @@ class XYZManager(ObjectManager):
with further filtering.
"""
x, y, z = coord
x, y, z = xyz
wildcard = '*'
return self.filter_family(
(Q() if x is None else Q(db_tags__db_key=str(x),
db_tags__db_category=MAP_X_TAG_CATEGORY)),
(Q() if y is None else Q(db_tags__db_key=str(y),
db_tags__db_category=MAP_Y_TAG_CATEGORY)),
(Q() if z is None else Q(db_tags__db_key=str(z),
db_tags__db_category=MAP_Z_TAG_CATEGORY)),
**kwargs
return (
self
.filter_family(**kwargs)
.filter(
Q() if x == wildcard
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY))
.filter(
Q() if y == wildcard
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY))
.filter(
Q() if z == wildcard
else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY))
)
def get_xyz(self, coord=(0, 0, 'map'), **kwargs):
def get_xyz(self, xyz=(0, 0, 'map'), **kwargs):
"""
Always return a single matched entity directly.
Always return a single matched entity directly. This accepts no `*`-wildcards.
This will also find children of XYZRooms on the given coordinates.
Kwargs:
coord (tuple): A tuple of `int` or `str` (not `None`). The `Z`-coordinate
acts as the name (case-sensitive) of the map in the XYZgrid contrib.
xyz (tuple): A coordinate tuple of `int` or `str` (not `'*'`, no wildcards are
allowed in get). The `Z`-coordinate acts as the name (case-sensitive) of the map in
the XYZgrid contrib.
**kwargs: All other kwargs are passed on to the query.
Returns:
@ -78,13 +86,27 @@ class XYZManager(ObjectManager):
possible with a unique combination of x,y,z).
"""
x, y, z = coord
return self.get_family(
Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY),
Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY),
Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY),
**kwargs
)
x, y, z = xyz
# mimic get_family
paths = [self.model.path] + [
"%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model)
]
kwargs["db_typeclass_path__in"] = paths
try:
return (
self
.filter(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
.filter(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
.filter(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
.get(**kwargs)
)
except self.model.DoesNotExist:
inp = (f"xyz=({x},{y},{z}), " +
",".join(f"{key}={val}" for key, val in kwargs.items()))
raise self.model.DoesNotExist(f"{self.model.__name__} "
f"matching query {inp} does not exist.")
class XYZExitManager(XYZManager):
@ -94,17 +116,19 @@ class XYZExitManager(XYZManager):
"""
def filter_xyz_exit(self, coord=(None, None, 'map'),
destination_coord=(None, None, 'map'), **kwargs):
def filter_xyz_exit(self, xyz=('*', '*', '*'),
xyz_destination=('*', '*', '*'), **kwargs):
"""
Used by exits (objects with a source and -destination property).
Find all exits out of a source or to a particular destination.
Find all exits out of a source or to a particular destination. This will also find
children of XYZExit on the given coords..
Kwargs:
coord (tuple, optional): A tuple (X, Y, Z) for the source location. Each
element is either an `int`, `str` or `None`. `None` acts as a wild card. Note that
xyz (tuple, optional): A coordinate (X, Y, Z) for the source location. Each
element is either an `int` or `str`. The character `'*'` is used as a wildcard -
so setting all coordinates to the wildcard will return *all* XYZExits.
the `Z`-coordinate is the name of the map (case-sensitive) in the XYZgrid contrib.
destination_coord (tuple, optional): Same as the `coord` but for the destination of the
xyz_destination (tuple, optional): Same as `xyz` but for the destination of the
exit.
**kwargs: All other kwargs are passed on to the query.
@ -113,42 +137,52 @@ class XYZExitManager(XYZManager):
with further filtering.
Notes:
Depending on what coordinates are set to `None`, this can be used to
Depending on what coordinates are set to `*`, this can be used to
e.g. find all exits in a room, or leading to a room or even to rooms
in a particular X/Y row/column.
In the XYZgrid, `z != zdest` means a _transit_ between different maps.
In the XYZgrid, `z_source != z_destination` means a _transit_ between different maps.
"""
x, y, z = coord
xdest, ydest, zdest = destination_coord
x, y, z = xyz
xdest, ydest, zdest = xyz_destination
wildcard = '*'
return self.filter_family(
(Q() if x is None else Q(db_tags__db_key=str(x),
db_tags__db_category=MAP_X_TAG_CATEGORY)),
(Q() if y is None else Q(db_tags__db_key=str(y),
db_tags__db_category=MAP_Y_TAG_CATEGORY)),
(Q() if z is None else Q(db_tags__db_key=str(z),
db_tags__db_category=MAP_Z_TAG_CATEGORY)),
(Q() if xdest is None else Q(db_tags__db_key=str(xdest),
db_tags__db_category=MAP_XDEST_TAG_CATEGORY)),
(Q() if ydest is None else Q(db_tags__db_key=str(ydest),
db_tags__db_category=MAP_YDEST_TAG_CATEGORY)),
(Q() if zdest is None else Q(db_tags__db_key=str(zdest),
db_tags__db_category=MAP_ZDEST_TAG_CATEGORY)),
return (
self
.filter_family(**kwargs)
.filter(
Q() if x == wildcard
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY))
.filter(
Q() if y == wildcard
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY))
.filter(
Q() if z == wildcard
else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY))
.filter(
Q() if xdest == wildcard
else Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY))
.filter(
Q() if ydest == wildcard
else Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY))
.filter(
Q() if zdest == wildcard
else Q(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY))
)
def get_xyz_exit(self, coord=(0, 0, 'map'), destination_coord=(0, 0, 'map'), **kwargs):
def get_xyz_exit(self, xyz=(0, 0, 'map'), xyz_destination=(0, 0, 'map'), **kwargs):
"""
Used by exits (objects with a source and -destination property). Get a single
exit. All source/destination coordinates (as well as the map's name) are required.
This will also find children of XYZExits on the given coords.
Kwargs:
coord (tuple, optional): A tuple (X, Y, Z) for the source location. Each
element is either an `int` or `str` (not `None`).
xyz (tuple, optional): A coordinate (X, Y, Z) for the source location. Each
element is either an `int` or `str` (not `*`, no wildcards are allowed for get).
the `Z`-coordinate is the name of the map (case-sensitive) in the XYZgrid contrib.
destination_coord (tuple, optional): Same as the `coord` but for the destination of the
exit.
xyz_destination_coord (tuple, optional): Same as the `xyz` but for the destination of
the exit.
**kwargs: All other kwargs are passed on to the query.
Returns:
@ -157,29 +191,41 @@ class XYZExitManager(XYZManager):
Raises:
DoesNotExist: If no matching query was found.
MultipleObjectsReturned: If more than one match was found (which should not
possible with a unique combination of x,y,x).
be possible with a unique combination of x,y,x).
Notes:
All coordinates are required.
"""
x, y, z = coord
xdest, ydest, zdest = destination_coord
x, y, z = xyz
xdest, ydest, zdest = xyz_destination
# mimic get_family
paths = [self.model.path] + [
"%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model)
]
kwargs["db_typeclass_path__in"] = paths
return self.get_family(
Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY),
Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY),
Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY),
Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY),
Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY),
Q(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY),
**kwargs
)
try:
return (
self
.filter(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
.filter(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
.filter(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
.filter(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY)
.filter(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY)
.filter(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY)
.get(**kwargs)
)
except self.model.DoesNotExist:
inp = (f"xyz=({x},{y},{z}),xyz_destination=({xdest},{ydest},{zdest})," +
",".join(f"{key}={val}" for key, val in kwargs.items()))
raise self.model.DoesNotExist(f"{self.model.__name__} "
f"matching query {inp} does not exist.")
class XYZRoom(DefaultRoom):
"""
A game location aware of its XY-coordinate and map.
A game location aware of its XYZ-position.
"""
@ -190,28 +236,32 @@ class XYZRoom(DefaultRoom):
return repr(self)
def __repr__(self):
x, y, z = self.xyzcoords
x, y, z = self.xyz
return f"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>"
@property
def xyzcoords(self):
if not hasattr(self, "_xyzcoords"):
def xyz(self):
if not hasattr(self, "_xyz"):
x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False)
y = self.tags.get(category=MAP_Y_TAG_CATEGORY, return_list=False)
z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False)
self._xyzcoords = (x, y, z)
return self._xyzcoords
if x is None or y is None or z is None:
# don't cache unfinished coordinate
return (x, y, z)
# cache result
self._xyz = (x, y, z)
return self._xyz
@classmethod
def create(cls, key, account=None, coord=(0, 0, 'map'), **kwargs):
def create(cls, key, account=None, xyz=(0, 0, 'map'), **kwargs):
"""
Creation method aware of coordinates.
Creation method aware of XYZ coordinates.
Args:
key (str): New name of object to create.
account (Account, optional): Any Account to tie to this entity (usually not used for
rooms).
coords (tuple, optional): A 3D coordinate (X, Y, Z) for this room's location on a
xyz (tuple, optional): A 3D coordinate (X, Y, Z) for this room's location on a
map grid. Each element can theoretically be either `int` or `str`, but for the
XYZgrid, the X, Y are always integers while the `Z` coordinate is used for the
map's name.
@ -227,15 +277,15 @@ class XYZRoom(DefaultRoom):
"""
try:
x, y, z = coord
x, y, z = xyz
except ValueError:
return None, [f"XYRroom.create got `coord={coord}` - needs a valid (X,Y,Z) "
return None, [f"XYRroom.create got `xyz={xyz}` - needs a valid (X,Y,Z) "
"coordinate of ints/strings."]
existing_query = cls.objects.filter_xyz(coord=(x, y, z))
existing_query = cls.objects.filter_xyz(xyz=(x, y, z))
if existing_query.exists():
existing_room = existing_query.first()
return None, [f"XYRoom XYZ={coord} already exists "
return None, [f"XYRoom XYZ=({x},{y},{z}) already exists "
f"(existing room is named '{existing_room.db_key}')!"]
tags = (
@ -249,7 +299,7 @@ class XYZRoom(DefaultRoom):
class XYZExit(DefaultExit):
"""
An exit that is aware of the XY coordinate system.
An exit that is aware of the XYZ coordinate system.
"""
@ -259,30 +309,38 @@ class XYZExit(DefaultExit):
return repr(self)
def __repr__(self):
x, y, z = self.xyzcoords
xd, yd, zd = self.xyzdestcoords
x, y, z = self.xyz
xd, yd, zd = self.xyz_destination
return f"<XYZExit '{self.db_key}', XYZ=({x},{y},{z})->({xd},{yd},{zd})>"
@property
def xyzcoords(self):
if not hasattr(self, "_xyzcoords"):
def xyz(self):
if not hasattr(self, "_xyz"):
x = self.tags.get(category=MAP_X_TAG_CATEGORY, return_list=False)
y = self.tags.get(category=MAP_Y_TAG_CATEGORY, return_list=False)
z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False)
self._xyzcoords = (x, y, z)
return self._xyzcoords
if x is None or y is None or z is None:
# don't cache yet unfinished coordinate
return (x, y, z)
# cache result
self._xyz = (x, y, z)
return self._xyz
@property
def xyzdestcoords(self):
if not hasattr(self, "_xyzdestcoords"):
def xyz_destination(self):
if not hasattr(self, "_xyz_destination"):
xd = self.tags.get(category=MAP_XDEST_TAG_CATEGORY, return_list=False)
yd = self.tags.get(category=MAP_YDEST_TAG_CATEGORY, return_list=False)
zd = self.tags.get(category=MAP_ZDEST_TAG_CATEGORY, return_list=False)
self._xyzdestcoords = (xd, yd, zd)
return self._xyzdestcoords
if xd is None or yd is None or zd is None:
# don't cache unfinished coordinate
return (xd, yd, zd)
# cache result
self._xyz_destination = (xd, yd, zd)
return self._xyz_destination
@classmethod
def create(cls, key, account=None, coord=(0, 0, 'map'), destination_coord=(0, 0, 'map'),
def create(cls, key, account=None, xyz=(0, 0, 'map'), xyz_destination=(0, 0, 'map'),
location=None, destination=None, **kwargs):
"""
Creation method aware of coordinates.
@ -290,19 +348,17 @@ class XYZExit(DefaultExit):
Args:
key (str): New name of object to create.
account (Account, optional): Any Account to tie to this entity (unused for exits).
coords (tuple or None, optional): A 3D coordinate (X, Y, Z) for this room's location
xyz (tuple or None, optional): A 3D coordinate (X, Y, Z) for this room's location
on a map grid. Each element can theoretically be either `int` or `str`, but for the
XYZgrid contrib, the X, Y are always integers while the `Z` coordinate is used for
the map's name. Set to `None` if instead using a direct room reference with
`location`. destination_coord (tuple or None, optional): Works as `coords`, but for
destination of
the exit. Set to `None` if using the `destination` kwarg to point to room directly.
destination_coord (tuple, optional): The XYZ coordinate of the place the exit
`location`.
xyz_destination (tuple, optional): The XYZ coordinate of the place the exit
leads to. Will be ignored if `destination` is given directly.
location (Object, optional): Only used if `coord` is not given. This can be used
location (Object, optional): If given, overrides `xyz` coordinate. This can be used
to place this exit in any room, including non-XYRoom type rooms.
destination (Object, optional): If given, overrides `destination_coord`. This can
be any room (including non-XYRooms) and is not checked for XY coordinates.
destination (Object, optional): If given, overrides `xyz_destination`. This can
be any room (including non-XYRooms) and is not checked for XYZ coordinates.
**kwargs: Will be passed into the normal `DefaultRoom.create` method.
Returns:
@ -311,35 +367,32 @@ class XYZExit(DefaultExit):
"""
tags = []
try:
x, y, z = coord
except ValueError:
if not location:
return None, ["XYExit.create need either a `coord` or a `location`."]
if location:
source = location
else:
print("rooms:", XYZRoom.objects.all().count(), XYZRoom.objects.all())
print("exits:", XYZExit.objects.all().count(), XYZExit.objects.all())
source = XYZRoom.objects.get_xyz(coord=(x, y, z))
tags.extend(((str(x), MAP_X_TAG_CATEGORY),
(str(y), MAP_Y_TAG_CATEGORY),
(str(z), MAP_Z_TAG_CATEGORY)))
try:
x, y, z = xyz
except ValueError:
return None, ["XYExit.create need either `xyz=(X,Y,Z)` coordinate or a `location`."]
else:
source = XYZRoom.objects.get_xyz(xyz=(x, y, z))
tags.extend(((str(x), MAP_X_TAG_CATEGORY),
(str(y), MAP_Y_TAG_CATEGORY),
(str(z), MAP_Z_TAG_CATEGORY)))
if destination:
dest = destination
else:
try:
xdest, ydest, zdest = destination_coord
xdest, ydest, zdest = xyz_destination
except ValueError:
if not destination:
return None, ["XYExit.create need either a `destination_coord` or "
"a `destination`."]
dest = destination
return None, ["XYExit.create need either `xyz_destination=(X,Y,Z)` coordinate "
"or a `destination`."]
else:
dest = XYZRoom.objects.get_xyz(coord=(xdest, ydest, zdest))
dest = XYZRoom.objects.get_xyz(xyz=(xdest, ydest, zdest))
tags.extend(((str(xdest), MAP_XDEST_TAG_CATEGORY),
(str(ydest), MAP_YDEST_TAG_CATEGORY),
(str(zdest), MAP_ZDEST_TAG_CATEGORY)))
return DefaultExit.create(
key, source, dest, account=account,
location=location, tags=tags, typeclass=cls, **kwargs)
key, source, dest,
account=account, tags=tags, typeclass=cls, **kwargs)