XYZgrid

Contribution by Griatch 2021

Places Evennia’s game world on an xy (z being different maps) coordinate grid. Grid is created and maintained externally by drawing and parsing 2D ASCII maps, including teleports, map transitions and special markers to aid pathfinding. Supports very fast shortest-route pathfinding on each map. Also includes a fast view function for seeing only a limited number of steps away from your current location (useful for displaying the grid as an in-game, updating map).

Grid-management is done outside of the game using a new evennia-launcher option.

Examples

#-#-#-#   #
|  /      d
#-#       |   #
   \      u   |\
o---#-----#---+-#-#
|         ^   |/
|         |   #
v         |    \
#-#-#-#-#-# #---#
    |x|x|     /
    #-#-#    #-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                     #---#
                                    /
                                   @-
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Dungeon Entrance
To the east, a narrow opening leads into darkness.
Exits: northeast and east

Installation

  1. XYZGrid requires the scipy library. Easiest is to get the ‘extra’ dependencies of Evennia with

    pip install evennia[extra]
    

    If you use the git install, you can also

    (cd to evennia/ folder)
    pip install --upgrade -e .[extra]
    

    This will install all optional requirements of Evennia.

  2. Import and add the evennia.contrib.grid.xyzgrid.commands.XYZGridCmdSet to the CharacterCmdset cmdset in mygame/commands.default_cmds.py. Reload the server. This makes the map, goto/path and the modified teleport and open commands available in-game.

  1. Edit mygame/server/conf/settings.py and add

    EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'
    PROTOTYPE_MODULES += ['evennia.contrib.grid.xyzgrid.prototypes']
    

    This will add the new ability to enter evennia xyzgrid <option> on the command line. It will also make the xyz_room and xyz_exit prototypes available for use as prototype-parents when spawning the grid.

  2. Run evennia xyzgrid help for available options.

  3. (Optional): By default, the xyzgrid will only spawn module-based prototypes. This is an optimization and usually makes sense since the grid is entirely defined outside the game anyway. If you want to also make use of in-game (db-) created prototypes, add XYZGRID_USE_DB_PROTOTYPES = True to settings.

Overview

The grid contrib consists of multiple components.

  1. The XYMap - This class parses modules with special Map strings and Map legends into one Python object. It has helpers for pathfinding and visual-range handling.

  2. The XYZGrid - This is a singleton Script that stores all XYMaps in the game. It is the central point for managing the ‘grid’ of the game.

  3. XYZRoom and XYZExitare custom typeclasses that use Tags to know which X,Y,Z coordinate they are located at. The XYZGrid is abstract until it is used to spawn these database entities into something you can actually interract with in the game. The XYZRoom typeclass is using its return_appearance hook to display the in-game map.

  4. Custom Commands have been added for interacting with XYZ-aware locations.

  5. A new custom Launcher command, evennia xyzgrid <options> is used to manage the grid from the terminal (no game login is needed).

We’ll start exploring these components with an example.

First example usage

After installation, do the following from your command line (where the evennia command is available):

$ evennia xyzgrid init

use evennia xyzgrid help to see all options) This will create a new XYZGrid Script if one didn’t already exist. The evennia xyzgrid is a custom launch option added only for this contrib.

The xyzgrid-contrib comes with a full grid example. Let’s add it:

$ evennia xyzgrid add evennia.contrib.grid.xyzgrid.example

You can now list the maps on your grid:

$ evennia xyzgrid list

You’ll find there are two new maps added. You can find a lot of extra info about each map with the show subcommand:

$ evennia xyzgrid show "the large tree"
$ evennia xyzgrid show "the small cave"

If you want to peek at how the grid’s code, open evennia/contrib/grid/xyzgrid/example.py. (We’ll explain the details in later sections).

So far the grid is ‘abstract’ and has no actual in-game presence. Let’s spawn actual rooms/exits from it. This will take a little while.

$ evennia xyzgrid spawn

This will take prototypes stored with each map’s map legend and use that to build XYZ-aware rooms there. It will also parse all links to make suitable exits between locations. You should rerun this command if you ever modify the layout/prototypes of your grid. Running it multiple times is safe.

$ evennia reload

(or evennia start if server was not running). This is important to do after every spawning operation, since the evennia xyzgrid operates outside of the regular evennia process. Reloading makes sure all caches are refreshed.

Now you can log into the server. Some new commands should be available to you.

teleport (3,0,the large tree)

The teleport command now accepts an optional (X, Y, Z) coordinate. Teleporting to a room-name or #dbref still works the same. This will teleport you onto the grid. You should see a map-display. Try walking around.

map

This new builder-only command shows the current map in its full form (also showing ‘invisible’ markers usually not visible to users.

teleport (3, 0)

Once you are in a grid-room, you can teleport to another grid room on the same map without specifying the Z coordinate/map name.

You can use open to make an exit back to the ‘non-grid’, but remember that you mustn’t use a cardinal direction to do so - if you do, the evennia xyzgrid spawn will likely remove it next time you run it.

open To limbo;limbo = #2
limbo

You are back in Limbo (which doesn’t know anything about XYZ coordinates). You can however make a permanent link back into the gridmap:

open To grid;grid = (3,0,the large tree)
grid

This is how you link non-grid and grid locations together. You could for example embed a house ‘inside’ the grid this way.

the (3,0,the large tree) is a ‘Dungeon entrance’. If you walk east you’ll transition into “the small cave” map. This is a small underground dungeon with limited visibility. Go back outside again (back on “the large tree” map).

path view

This finds the shortest path to the “A gorgeous view” room, high up in the large tree. If you have color in your client, you should see the start of the path visualized in yellow.

goto view

This will start auto-walking you to the view. On the way you’ll both move up into the tree as well as traverse an in-map teleporter. Use goto on its own to abort the auto-walk.

When you are done exploring, open the terminal (outside the game) again and remove everything:

$ evennia xyzgrid delete

You will be asked to confirm the deletion of the grid and unloading of the XYZGrid script. Reload the server afterwards. If you were on a map that was deleted you will have been moved back to your home location.

Defining an XYMap

For a module to be suitable to pass to evennia xyzgrid add <module>, the module must contain one of the following variables:

  • XYMAP_DATA - a dict containing data that fully defines the XYMap

  • XYMAP_DATA_LIST - a list of XYMAP_DATA dicts. If this exists, it will take precedence. This allows for storing multiple maps in one module.

The XYMAP_DATA dict has the following form:

XYMAP_DATA = {
    "zcoord": <str>
    "map": <str>,
    "legend": <dict, optional>,
    "prototypes": <dict, optional>
    "options": <dict, optional>
}

  • "zcoord" (str): The Z-coordinate/map name of the map.

  • "map" (str): A Map string describing the topology of the map.

  • "legend" (dict, optional): Maps each symbol on the map to Python code. This dict can be left out or only partially filled - any symbol not specified will instead use the default legend from the contrib.

  • "prototypes" (dict, optional): This is a dict that maps map-coordinates to custom prototype overrides. This is used when spawning the map into actual rooms/exits.

  • "options" (dict, optional): These are passed into the return_appearance hook of the room and allows for customizing how a map should be displayed, how pathfinding should work etc.

Here’s a minimal example of the whole setup:

# In, say, a module gamedir/world/mymap.py

MAPSTR = r"""

+ 0 1 2

2 #-#-#
     /
1 #-#
  |  \
0 #---#

+ 0 1 2


"""
# use only defaults
LEGEND = {}

# tweak only one room. The 'xyz_room/exit' parents are made available
# by adding the xyzgrid prototypes to settings during installation.
# the '*' are wildcards and allows for giving defaults on this map.
PROTOTYPES = {
    (0, 0): {
        "prototype_parent": "xyz_room",
        "key": "A nice glade",
        "desc": "Sun shines through the branches above.",
    },
    (0, 0, 'e'): {
        "prototype_parent": "xyz_exit",
        "desc": "A quiet path through the foilage",
    },
    ('*', '*'): {
        "prototype_parent": "xyz_room",
        "key": "In a bright forest",
        "desc": "There is green all around.",
    },
    ('*', '*', '*'): {
        "prototype_parent": "xyz_exit",
        "desc": "The path leads further into the forest.",
    },
}

# collect all info for this one map
XYMAP_DATA = {
    "zcoord": "mymap",  # important!
    "map": MAPSTR,
    "legend": LEGEND,
    "prototypes": PROTOTYPES,
    "options": {}
}

# this can be skipped if there is only one map in module
XYMAP_DATA_LIST = [
    XYMAP_DATA
]

The above map would be added to the grid with

$ evennia xyzgrid add world.mymap

In the following sections we’ll discuss each component in turn.

The Zcoord

Each XYMap on the grid has a Z-coordinate which usually can be treated just as the name of the map. The Z-coordinate can be either a string or an integer, and must be unique across the entire grid. It is added as the key ‘zcoord’ to XYMAP_DATA.

Most users will want to just treat each map as a location, and name the “Z-coordinate” things like Dungeon of Doom, The ice queen's palace or City of Blackhaven. But you could also name it -1, 0, 1, 2, 3 if you wanted.

Note that the Zcoord is searched non-case senstively in the

Pathfinding happens only within each XYMap (up/down is normally ‘faked’ by moving sideways to a new area of the XY plane).

A true 3D map

Even for the most hardcore of sci-fi space game, consider sticking to 2D movement. It’s hard enough for players to visualize a 3D volume with graphics. In text it’s even harder.

That said, if you want to set up a true X, Y, Z 3D coordinate system (where you can move up/down from every point), you can do that too.

This contrib provides an example command commands.CmdFlyAndDive that provides the player with the ability to use fly and dive to move straight up/down between Z coordinates. Just add it (or its cmdset commands.XYZGridFlyDiveCmdSet) to your Character cmdset and reload to try it out.

For the fly/dive to work you need to build your grid as a ‘stack’ of XY-grid maps and name them by their Z-coordinate as an integer. The fly/dive actions will only work if there is actually a matching room directly above/below.

Note that since pathfinding only works within each XYmap, the player will not be able to include fly/dive in their autowalking - this is always a manual action.

As an example, let’s assume coordinate (1, 1, -3) is the bottom of a deep well leading up to the surface (at level 0)

LEVEL_MINUS_3 = r"""
+ 0 1

1   #
    |
0 #-#

+ 0 1
"""

LEVEL_MINUS_2 = r"""
+ 0 1

1   #

0

+ 0 1
"""

LEVEL_MINUS_1 = r"""
+ 0 1

1   #

0

+ 0 1
"""

LEVEL_0 = r"""
+ 0 1

1 #-#
  |x|
0 #-#

+ 0 1
"""

XYMAP_DATA_LIST = [
    {"zcoord": -3, "map": LEVEL_MINUS_3},
    {"zcoord": -2, "map": LEVEL_MINUS_2},
    {"zcoord": -1, "map": LEVEL_MINUS_1},
    {"zcoord": 0, "map": LEVEL_0},
]

In this example, if we arrive to the bottom of the well at (1, 1, -3) we fly straight up three levels until we arrive at (1, 1, 0), at the corner of some sort of open field.

We can dive down from (1, 1, 0). In the default implementation you must dive 3 times to get to the bottom. If you wanted you could tweak the command so you automatically fall to the bottom and take damage etc.

We can’t fly/dive up/down from any other XY positions because there are no open rooms at the adjacent Z coordinates.

Map String

The creation of a new map starts with a Map string. This allows you to ‘draw’ your map, describing and how rooms are positioned in an X,Y coordinate system. It is added to XYMAP_DATA with the key ‘map’.

MAPSTR = r"""

+ 0 1 2

2 #-#-#
     /
1 #-#
  |  \
0 #---#

+ 0 1 2

"""

On the coordinate axes, only the two + are significant - the numbers are optional, so this is equivalent:

MAPSTR = r"""

+

  #-#-#
     /
  #-#
  | \
  #---#

+

"""

Even though it’s optional, it’s highly recommended that you add numbers to your axes - if only for your own sanity.

The coordinate area starts two spaces to the right and two spaces below/above the mandatory + signs (which marks the corners of the map area). Origo (0,0) is in the bottom left (so X-coordinate increases to the right and Y-coordinate increases towards the top). There is no limit to how high/wide the map can be, but splitting a large world into multiple maps can make it easier to organize.

Position is important on the grid. Full coordinates are placed on every second space along all axes. Between these ‘full’ coordinates are .5 coordinates. Note that there are no .5 coordinates spawned in-game; they are only used in the map string to have space to describe how rooms/nodes link to one another.

+ 0 1 2 3 4 5

4           E
   B
3

2         D

1    C

0 A

+ 0 1 2 3 4 5
  • A is at origo, (0, 0) (a ‘full’ coordinate)

  • B is at (0.5, 3.5)

  • C is at (1.5, 1)

  • D is at (4, 2) (a ‘full’ coordinate).

  • E is the top-right corner of the map, at (5, 4) (a ‘full’ coordinate)

The map string consists of two main classes of entities - nodes and links.

  • A node usually represents a room in-game (but not always). Nodes must always be placed on a ‘full’ coordinate.

  • A link describes a connection between two nodes. In-game, links are usuallyj represented by exits. A link can be placed anywhere in the coordinate space (both on full and 0.5 coordinates). Multiple links are often chained together, but the chain must always end in nodes on both sides.

Even though a link-chain may consist of several steps, like #-----#, in-game it will still only represent one ‘step’ (e.g. you go ‘east’ only once to move from leftmost to the rightmost node/room).

Map legend

There can be many different types of nodes and links. Whereas the map string describes where they are located, the Map Legend connects each symbol on the map to Python code.


LEGEND = {
    '#': xymap_legend.MapNode,
    '-': xymap_legende.EWMapLink
}

# added to XYMAP_DATA dict as 'legend': LEGEND below

The legend is optional, and any symbol not explicitly given in your legend will fall back to its value in the default legend outlined below.

  • MapNode is the base class for all nodes.

  • MapLink is the base class for all links.

As the Map String is parsed, each found symbol is looked up in the legend and initialized into the corresponding MapNode/Link instance.

Default Legend

Below is the default map legend. The symbol is what should be put in the Map string. It must always be a single character. The display-symbol is what is actually visualized when displaying the map to players in-game. This could have colors etc. All classes are found in evennia.contrib.grid.xyzgrid.xymap_legend and their names are included to make it easy to know what to override.

symbol

display-symbol

type

class

description

#

#

node

BasicMapNode

A basic node/room.

T

node

MapTransitionNode

Transition-target for links between maps (see below)

I (letter I)

#

node

InterruptMapNode

Point of interest, auto-step will always stop here (see below).

|

|

link

NSMapLink

North-South two-way

-

-

link

EWMapLink

East-West two-way

/

/

link

NESWMapLink

NorthEast-SouthWest two-way

\

\

link

SENWMapLink

NorthWest two-way

u

u

link

UpMapLink

Up, one or two-way (see below)

d

d

link

DownMapLink

Down, one or two-way (see below)

x

x

link

CrossMapLink

SW-NE and SE-NW two-way

+

+

link

PlusMapLink

Crossing N-S and E-W two-way

v

v

link

NSOneWayMapLink

North-South one-way

^

^

link

SNOneWayMapLink

South-North one-way

<

<

link

EWOneWayMapLink

East-West one-way

>

>

link

WEOneWayMapLink

West-East one-way

o

o

link

RouterMapLink

Routerlink, used for making link ‘knees’ and non-orthogonal crosses (see below)

b

(varies)

link

BlockedMapLink

Block pathfinder from using this link. Will appear as logically placed normal link (see below).

i

(varies)

link

InterruptMapLink

Interrupt-link; auto-step will never cross this link (must move manually, see below)

t

link

TeleporterMapLink

Inter-map teleporter; will teleport to same-symbol teleporter on the same map. (see below)

Map Nodes

The basic map node (#) usually represents a ‘room’ in the game world. Links can connect to the node from any of the 8 cardinal directions, but since nodes must only exist on full coordinates, they can never appear directly next to each other.

\|/
-#-
/|\

##     invalid!

All links or link-chains must end in nodes on both sides.

#-#-----#

#-#-----   invalid!

Interrupt-nodes

An interrupt-node (I, InterruptMapNode) is a node that acts like any other node except it is considered a ‘point of interest’ and the auto-walk of the goto command will always stop auto-stepping at this location.

#-#-I-#-#

So if auto-walking from left to right, the auto-walk will correctly map a path to the end room, but will always stop at the I node. If the user starts from the I room, they will move away from it without interruption (so you can manually run the goto again to resume the auto-step).

The use of this room is to anticipate blocks not covered by the map. For example there could be a guard standing in this room that will arrest you unless you show them the right paperwork - trying to auto-walk past them would be bad!

By default, this node looks just like a normal # to the player.

Map-Transition Nodes

The map transition (MapTransitionNode) teleports between XYMaps (a Z-coordinate transition, if you will), like walking from the “Dungeon” map to the “Castle” map. Unlike other nodes, the MapTransitionNode is never spawned into an actual room (it has no prototype). It just holds an XYZ coordinate pointing to somewhere on the other map. The link leading to the node will use those coordinates to make an exit pointing there. Only one single link may lead to this type of node.

Unlike for TeleporterMapLink, there need not be a matching MapTransitionNode on the other map - the transition can choose to send the player to any valid coordinate on the other map.

Each MapTransitionNode has a property target_map_xyz that holds the XYZ coordinate the player should end up in when going towards this node. This must be customized in a child class for every transition.

If there are more than one transition, separate transition classes should be added, with different map-legend symbols:

# in your map definition module (let's say this is mapB)

from evennia.contrib.grid.xyzgrid import xymap_legend

MAPSTR = r"""

+ 0 1 2

2   #-C
    |
1 #-#-#
     \
0 A-#-#

+ 0 1 2


"""

class TransitionToMapA(xymap_legend.MapTransitionNode):
    """Transition to MapA"""
    target_map_xyz = (1, 4, "mapA")

class TransitionToMapC(xymap_legend.MapTransitionNode):
    """Transition to MapB"""
    target_map_xyz = (12, 14, "mapC")

LEGEND = {
    'A': TransitionToMapA
    'C': TransitionToMapC

}

XYMAP_DATA = {
    # ...
    "map": MAPSTR,
    "legend": LEGEND
    # ...
}

Moving west from (1,0) will bring you to (1,4) of MapA, and moving east from (1,2) will bring you to (12,14) on MapC (assuming those maps exist).

A map transition is always one-way, and can lead to the coordinates of any existing node on the other map:

map1        map2

#-T         #-#---#-#-#-#

A player moving east towards T could for example end up at the 4th # from the left on map2 if so desired (even though it doesn’t make sense visually). There is no way to get back to map1 from there.

To create the effect of a two-way transition, one can set up a mirrored transition-node on the other map:

citymap    dungeonmap

#-T        T-#

The transition-node of each map above has target_map_xyz pointing to the coordinate of the # node of the other map (not to the other T, that is not spawned and would lead to the exit finding no destination!). The result is that one can go east into the dungeon and then immediately go back west to the city across the map boundary.

Prototypes

Prototypes are dicts that describe how to spawn a new instance of an object. Each of the nodes and links above have a default prototype that allows the evennia xyzgrid spawn command to convert them to a XYZRoom or an XYZExit respectively.

The default prototypes are found in evennia.contrib.grid.xyzgrid.prototypes (added during installation of this contrib), with prototype_keys "xyz_room" and "xyz_exit" - use these as prototype_parent to add your own custom prototypes.

The "prototypes" key of the XYMap-data dict allows you to customize which prototype is used for each coordinate in your XYMap. The coordinate is given as (X, Y) for nodes/rooms and (X, Y, direction) for links/exits, where the direction is one of “n”, “ne”, “e”, “se”, “s”, “sw”, “w”, “nw”, “u” or “d”. For exits, it’s recommended to not set a key since this is generated automatically by the grid spawner to be as expected (“north” with alias “n”, for example).

A special coordinate is *. This acts as a wild card for that coordinate and allows you to add ‘default’ prototypes to be used for rooms.


MAPSTR = r"""

+ 0 1

1 #-#
   \
0 #-#

+ 0 1


"""


PROTOTYPES = {
    (0,0): {
	"prototype_parent": "xyz_room",
	"key": "End of a the tunnel",
	"desc": "This is is the end of the dark tunnel. It smells of sewage."
    },
    (0,0, 'e') : {
	"prototype_parent": "xyz_exit",
	"desc": "The tunnel continues into darkness to the east"
    },
    (1,1): {
	"prototype_parent": "xyz_room",
	"key": "Other end of the tunnel",
	"desc": The other end of the dark tunnel. It smells better here."
    }
    # defaults
    ('*', '*'): {
    	"prototype_parent": "xyz_room",
	"key": "A dark tunnel",
	"desc": "It is dark here."
    },
    ('*', '*', '*'): {
	"prototype_parent": "xyz_exit",
	"desc": "The tunnel stretches into darkness."
    }
}

XYMAP_DATA = {
    # ...
    "map": MAPSTR,
    "prototypes": PROTOTYPES
    # ...
}

When spawning the above map, the room at the bottom-left and top-right of the map will get custom descriptions and names, while the others will have default values. One exit (the east exit out of the room in the bottom-left will have a custom description.

If you are used to using prototypes, you may notice that we didn’t add a prototype_key for the above prototypes. This is normally required for every prototype. This is for convenience - if you don’t add a prototype_key, the grid will automatically generate one for you - a hash based on the current XYZ (+ direction) of the node/link to spawn.

If you find yourself changing your prototypes after already spawning the grid/map, you can rerun evennia xyzgrid spawn again; The changes will be picked up and applied to the existing objects.

Extending the base prototypes

The default prototypes are found in evennia.contrib.grid.xyzgrid.prototypes and should be included as prototype_parents for prototypes on the map. Would it not be nice to be able to change these and have the change apply to all of the grid? You can, by adding the following to your mygame/server/conf/settings.py:

XYZROOM_PROTOTYPE_OVERRIDE = {"typeclass": "myxyzroom.MyXYZRoom"}
XYZEXIT_PROTOTYPE_OVERRIDE = {...}

If you override the typeclass in your prototypes, the typeclass used MUST inherit from XYZRoom and/or XYZExit. The BASE_ROOM_TYPECLASS and BASE_EXIT_TYPECLASS settings will not help - these are still useful for non-xyzgrid rooms/exits though.

Only add what you want to change - these dicts will extend the default parent prototypes rather than replace them. As long as you define your map’s prototypes to use a prototype_parent of "xyz_room" and/or "xyz_exit", your changes will now be applied. You may need to respawn your grid and reload the server after a change like this.

Options

The last element of the XYMAP_DATA dict is the "options", for example

XYMAP_DATA = {
    # ...
    "options": {
	"map_visual_range": 2
    }
}

The options dict is passed as **kwargs to XYZRoom.return_appearance when visualizing the map in-game. It allows for making different maps display differently from one another (note that while these options are convenient one could of course also override return_appearance entirely by inheriting from XYZRoom and then pointing to it in your prototypes).

The default visualization is this:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                     #---#
                                    /
                                   @-
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Dungeon Entrance
To the east, a narrow opening leads into darkness.
Exits: northeast and east

  • map_display (bool): This turns off the display entirely for this map.

  • map_character_symbol (str): The symbol used to show ‘you’ on the map. It can have colors but should only take up one character space. By default this is a green @.

  • map_visual_range (int): This how far away from your current location you can see.

  • map_mode (str): This is either “node” or “scan” and affects how the visual range is calculated. In “node” mode, the range shows how many nodes away from that you can see. In “scan” mode you can instead see that many on-screen characters away from your character. To visualize, assume this is the full map (where ‘@’ is the character location):

    #----------------#
    |                |
    |                |
    # @------------#-#
    |                |
    #----------------#
    

    This is what the player will see in ‘nodes’ mode with map_visual_range=2:

    @------------#-#
    

    … and in ‘scan’ mode:

    |
    |
    # @--
    |
    #----
    

    The ‘nodes’ mode has the advantage of showing only connected links and is great for navigation but depending on the map it can include nodes quite visually far away from you. The ‘scan’ mode can accidentally reveal unconnected parts of the map (see example above), but limiting the range can be used as a way to hide information.

    This is what the player will see in ‘nodes’ mode with map_visual_range=1:

    @------------#
    

    … and in ‘scan’ mode:

    @-
    

    One could for example use ‘nodes’ for outdoor/town maps and ‘scan’ for exploring dungeons.

  • map_align (str): One of ‘r’, ‘c’ or ‘l’. This shifts the map relative to the room text. By default it’s centered.

  • map_target_path_style: How to visualize the path to a target. This is a string that takes the {display_symbol} formatting tag. This will be replaced with the display_symbol of each map element in the path. By default this is "|y{display_symbol}|n", that is, the path is colored yellow.

  • map_fill_all (bool): If the map area should fill the entire client width (default) or change to always only be as wide as the room description. Note that in the latter case, the map can end up ‘dancing around’ in the client window if descriptions vary a lot in width.

  • map_separator_char (str): The char to use for the separator-lines between the map and the room description. Defaults to "|x~|n" - wavy, dark-grey lines.

Changing the options of an already spawned map does not require re-spawning the map, but you do need to reload the server!

About the Pathfinder

The new goto command exemplifies the use of the Pathfinder. This is an algorithm that calculates the shortest route between nodes (rooms) on an XY-map of arbitrary size and complexity. It allows players to quickly move to a location if they know that location’s name. Here are some details about

  • The pathfinder parses the nodes and links to build a matrix of distances of moving from each node to all other nodes on one XYMap. The path is solved using the Dijkstra algorithm.

  • The pathfinder’s matrices can take a long time to build for very large maps. Therefore they are are cached as pickled binary files in mygame/server/.cache/ and only rebuilt if the map changes. They are safe to delete (you can also use evennia xyzgrid initpath to force-create/rebuild the cache files).

  • Once cached, the pathfinder is fast (Finding a 500-step shortest-path over 20 000 nodes/rooms takes below 0.1s).

  • It’s important to remember that the pathfinder only works within one XYMap. It will not find paths across map transitions. If this is a concern, one can consider making all regions of the game as one XYMap. This probably works fine, but makes it harder to add/remove new maps to/from the grid.

  • The pathfinder will actually sum up the ‘weight’ of each link to determine which is the ‘cheapest’ (shortest) route. By default every link except blocking links have a cost of 1 (so cost is equal to the number of steps to move between nodes). Individual links can however change this to a higher/lower weight (must be >=1). A higher weight means the pathfinder will be less likely to use that route compared to others (this can also be vidually confusing for the user, so use with care).

  • The pathfinder will average the weight of long link-chains. Since all links default to having the same weight (=1), this means that #-# has the same movement cost as #----# even though it is visually ‘shorter’. This behavior can be changed per-link by using links with average_long_link_weights = False.

XYZGrid

The XYZGrid is a Global Script that holds all XYMap objects on the grid. There should be only one XYZGrid created at any time.

To access the grid in-code, there are several ways:

  • You can search for the grid like any other Script. It’s named “XYZGrid”.

    grid = evennia.search_script(“XYZGrid”)[0]

    (search_script always returns a list)

  • You can get it with evennia.contrib.grid.xyzgrid.xyzgrid.get_xyzgrid

    from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid grid = get_xyzgrid()

    This will always return a grid, creating an empty grid if one didn’t previously exist. So this is also the recommended way of creating a fresh grid in-code.

  • You can get it from an existing XYZRoom/Exit by accessing their .xyzgrid property

    grid = self.caller.location.xyzgrid # if currently in grid room

Most tools on the grid class have to do with loading/adding and deleting maps, something you are expected to use the evennia xyzgrid commands for. But there are also several methods that are generally useful:

  • .get_room(xyz) - Get a room at a specific coordinate (X, Y, Z). This will only work if the map has been actually spawned first. For example .get_room((0,4,"the dark castle)). Use '*' as a wild card, so .get_room(('*','*',"the dark castle)) will get you all rooms spawned on the dark castle map.

  • .get_exit(xyz, name) - get a particular exit, e.g. .get_exit((0,4,"the dark castle", "north"). You can also use '*' as wildcards.

One can also access particular parsed XYMap objects on the XYZGrid directly:

  • .grid - this is the actual (cached) store of all XYMaps, as {zcoord: XYMap, ...}

  • .get_map(zcoord) - get a specific XYMap.

  • .all_maps() - get a list of all XYMaps.

Unless you want to heavily change how the map works (or learn what it does), you will probably never need to modify the XYZMap object itself. You may want to know how to call find the pathfinder though:

  • xymap.get_shortest_path(start_xy, end_xy)

  • xymap.get_visual_range(xy, dist=2, **kwargs)

See the XYMap documentation for details.

XYZRoom and XYZExit

These are new custom Typeclasses located in evennia.contrib.xyzgrid.xyzroom. They extend the base DefaultRoom and DefaultExit to be aware of their X, Y and Z coordinates.

Warning

You should usually **not** create XYZRooms/Exits manually. They are intended
to be created/deleted based on the layout of the grid. So to add a new room, add
a new node to your map. To delete it, you remove it. Then rerun
**evennia xyzgrid spawn**. Having manually created XYZRooms/exits in the mix
can lead to them getting deleted or the system getting confused.

If you **still** want to create XYZRoom/Exits manually (don't say we didn't
warn you!), you should do it with their `XYZRoom.create()` and
`XYZExit.create()` methods. This makes sure the XYZ they use are unique.

Useful (extra) properties on XYZRoom, XYZExit:

  • xyz The (X, Y, Z) coordinate of the entity, for example (23, 1, "greenforest")

  • xyzmap The XYMap this belongs to.

  • get_display_name(looker) - this has been modified to show the coordinates of the entity as well as the #dbref if you have Builder or higher privileges.

  • return_appearance(looker, **kwargs) - this has been extensively modified for XYZRoom, to display the map. The options given in XYMAP_DATA will appear as **kwargs to this method and if you override this you can customize the map display in depth.

  • xyz_destination (only for XYZExits) - this gives the xyz-coordinate of the exit’s destination.

The coordinates are stored as Tags where both rooms and exits tag categories room_x_coordinate, room_y_coordinate and room_z_coordinate while exits use the same in addition to tags for their destination, with tag categories exit_dest_x_coordinate, exit_dest_y_coordinate and exit_dest_z_coordinate.

The make it easier to query the database by coordinates, each typeclass offers custom manager methods. The filter methods allow for '*' as a wildcard.


# find a list of all rooms in map foo
rooms = XYZRoom.objects.filter_xyz(('*', '*', 'foo'))

# find list of all rooms with name "Tunnel" on map foo
rooms = XYZRoom.objects.filter_xyz(('*', '*', 'foo'), db_key="Tunnel")

# find all rooms in the first column of map footer
rooms = XYZRoom.objects.filter_xyz((0, '*', 'foo'))

# find exactly one room at given coordinate (no wildcards allowed)
room = XYZRoom.objects.get_xyz((13, 2, foo))

# find all exits in a given room
exits = XYZExit.objects.filter_xyz((10, 4, foo))

# find all exits pointing to a specific destination (from all maps)
exits = XYZExit.objects.filter_xyz_exit(xyz_destination=(13,5,'bar'))

# find exits from a room to anywhere on another map
exits = XYZExit.objects.filter_xyz_exit(xyz=(1, 5, 'foo'), xyz_destination=('*', '*', 'bar'))

# find exactly one exit to specific destination (no wildcards allowed)
exit = XYZExit.objects.get_xyz_exit(xyz=(0, 12, 'foo'), xyz_destination=(5, 2, 'foo'))

You can customize the XYZRoom/Exit by having the grid spawn your own subclasses of them. To do this you need to override the prototype used to spawn rooms on the grid. Easiest is to modify the base prototype-parents in settings (see the XYZRoom and XYZExit section above).

Working with the grid

The work flow of working with the grid is usually as follows:

  1. Prepare a module with a Map String, Map Legend, Prototypes and Options packaged into a dict XYMAP_DATA. Include multiple maps per module by adding several XYMAP_DATA to a variable XYMAP_DATA_LIST instead.

  2. If your map contains TransitionMapNodes, the target map must either also be added or already exist in the grid. If not, you should skip that node for now (otherwise you’ll face errors when spawning because the exit-destination does not exist).

  3. Run evennia xyzgrid add <module> to register the maps with the grid. If no grid existed, it will be created by this. Fix any errors reported by the parser.

  4. Inspect the parsed map(s) with evennia xyzgrid show <zcoord> and make sure they look okay.

  5. Run evennia xyzgrid spawn to spawn/update maps into actual XYZRooms and XYZExits.

  6. If you want you can now tweak your grid manually by usual building commands. Anything you do not specify in your grid prototypes you can modify locally in your game - as long as the whole room/exit is not deleted, those will be untouched by evennia xyzgrid spawn. You can also dig/open exits to other rooms ‘embedded’ in your grid. These exits must not be named one of the grid directions (north, northeast, etc, nor up/down) or the grid will delete it next evennia xyzgrid spawn runs (since it’s not on the map).

  7. If you want to add new grid-rooms/exits you should always do so by modifying the Map String and then rerunning evennia xyzgrid spawn to apply the changes.

Details

The default Evennia’s rooms are non-euclidian - they can connect to each other with any types of exits without necessarily having a clear position relative to each other. This gives maximum flexibility, but many games want to use cardinal movements (north, east etc) and also features like finding the shortest-path between two points.

This contrib forces each room to exist on a 3-dimensional XYZ grid and also implements very efficient pathfinding along with tools for displaying your current visual-range and a lot of related features.

The rooms of the grid are entirely controlled from outside the game, using python modules with strings and dicts defining the map(s) of the game. It’s possible to combine grid- with non-grid rooms, and you can decorate grid rooms as much as you like in-game, but you cannot spawn new grid rooms without editing the map files outside of the game.

Installation

  1. If you haven’t before, install the extra contrib requirements. You can do so by doing pip install evennia[extra], or if you used git to install, do pip install --upgrade -e .[extra] from the evennia/ repo folder.

  2. Import and add the evennia.contrib.grid.xyzgrid.commands.XYZGridCmdSet to the CharacterCmdset cmdset in mygame/commands.default_cmds.py. Reload the server. This makes the map, goto/path and modified teleport and open commands available in-game.

  3. Edit mygame/server/conf/settings.py and set

     EXTRA_LAUNCHER_COMMANDS['xyzgrid'] = 'evennia.contrib.grid.xyzgrid.launchcmd.xyzcommand'
    
  4. Run the new evennia xyzgrid help for instructions on how to spawn the grid.

Example usage

After installation, do the following (from your command line, where the evennia command is available) to install an example grid:

evennia xyzgrid init
evennia xyzgrid add evennia.contrib.grid.xyzgrid.example
evennia xyzgrid list
evennia xyzgrid show "the large tree"
evennia xyzgrid show "the small cave"
evennia xyzgrid spawn
evennia reload

(remember to reload the server after spawn operations).

Now you can log into the server and do teleport (3,0,the large tree) to teleport into the map.

You can use open togrid = (3, 0, the large tree) to open a permanent (one-way) exit from your current location into the grid. To make a way back to a non-grid location just stand in a grid room and open a new exit out of it: open tolimbo = #2.

Try goto view to go to the top of the tree and goto dungeon to go down to the dungeon entrance at the bottom of the tree.


This document page is generated from evennia/contrib/grid/xyzgrid/README.md. Changes to this file will be overwritten, so edit that file rather than this one.