Rename MapSystem to XYZgrid, add README, planning

This commit is contained in:
Griatch 2021-06-20 00:31:14 +02:00
parent 2f6779920b
commit c819c6b8f3
11 changed files with 229 additions and 17 deletions

View file

@ -0,0 +1,191 @@
# XYZgrid
Full grid coordinate- pathfinding and visualization system
Evennia Contrib by Griatch 2021
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.
## A note on 3D = 2D + 1D
Since actual 3D movement usually is impractical to visualize in text, most
action in this contrib takes place on 2-dimenstional XY-planes we refer to as
_Maps_. Changing Z-coordinate means moving to another Map. Maps does not need
be the same size as one another and there is no (enforced) concept of moving
'up' or 'down' between maps - instead you basically 'teleport' between them,
which means you can have your characters end up anywhere you want in the next
map, regardless of which XY coordinate they were leaving from.
If you really want an actual 3D coordinate system, you could make all maps the
same size and name them `0`, `1`, `2` etc. But 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`.
Whereas the included rooms and exits can be used for 'true' 3D movement, the more
advanced tools like pathfinding will only operate within each XY `Map`.
## Components of the XYgrid
1. The Map String - describes the topology of a single
map/location/Z-coordinate.
2. The Map Legend - describes how to parse each symbol in the map string to a
topological relation, such as 'a room' or 'a two-way link east-west'.
3. The Map - combines the Map String and Legend into a parsed object with
pathfinding and visual-range handling.
4. The MultiMap - tracks multiple maps
5. Rooms, Exits and Prototypes - custom Typeclasses that understands XYZ coordinates.
The prototype describes how to build a database-entity from a given
Map Legend component.
6. The Grid - the combination of prototype-built rooms/exits with Maps for
pathfinding and visualization. This is kept in-sync with changes to Map
Strings.
### The Map string
A `Map` represents one Z-coordinate/location. The Map starts out as an text
string visually laying out one 2D map (so one Z-position). It is created
manually by the developer/builder outside of the game. The string-map
has one character per node(room) and descibe how rooms link together. Each
symbol is linked to a particular abstract Python class which helps parse
the map-string. While the contrib comes with a large number of nodes and links,
one can also make one's own.
```
MAP = r"""
+ 0 1 2 3
3 #-#---o
v |
2 #-# |
|x| ^
1 #-#-#
/ \
0 # #d#
+ 0 1 2 3
"""
```
Above, only the two `+`-characters in the upper-left and bottom-left are
required to mark the start of grid area - the numbered axes are optional but
recommended for readability! Note that the coordinate system has (0, 0) in the
bottom left - this means that +Y movement is 'upwards' in the string as
expected.
### Map Legend
The map legend is a mapping of one-character symbols to Python classes
representing _Nodes_ (usually equivalent to in-game rooms) or _Links_ (which
usually start as an Exit, but the length of the link only describes the
target-destination and has no in-game representation otherwise). These 'map
components' are Python classes that can be inherited and modified as needed.
The default legend support nearly 20 different symbols, including:
- Nodes (rooms) are always on XY coordinates (not between) and
- 8 two-way cardinal directions (n, ne etc)
- up/down - this is a 'fake' up-down using XY coordinates for ease of
visualization (the exit is just called 'up' or 'down', unless you display the
actual coordinate to the user they'll never know the difference).
- One-way links in 4 cardinal directions (not because it's hard to add more,
but because there are no obvious ASCII characters for the diaginal movements ...)
- Multi-step links are by default considered as one step in-game.
- 'Invisible' symbols that are used to block or act as deterrent for the
pathfinder to use certain routes (like a hidden entrance you should be
auto-pathing through).
- 'Points of interest' are nodes or links where the auto-stepper will always
stop, even if it can see what's behind. This is great for places where you
expect to have a door or a guard (which are not represented on the map).
- Teleporter-links, for jumping from one edge of the map to the other without
having to draw an actual link across. Good for maps that 'wrap around'.
- Transitional links between maps/locations/Z-positions. Note that pathfinding
will _not_ work across map transitions.
### Map
All `Map strings` are combined with their `Map Legends` to be parsed into a `Map`
object. The `Map` object has the relations between all nodes stored in a very
efficient matrix for quick lookup. This allows for:
- Shortest-path finding. The shortest-path from one coordinate to another
is calculated using an optimized implementation of the
[Dijkstra algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm).
While large solutions can be slow (up to 20 seconds for a 10 000 node map
with 8 cardinal links between every room), the solution is cached to disk -
meaning that as long as the map-string does not change, subsequent
path-finding operations are all very fast (<0.1s, usually much faster).
Once retrieving the route, the player can easily be auto-stepped along it.
- Visual-range - useful for displaying the map to the user as they move. The
system can either show everything within a certain grid-distance, or a
certain number of connected nodes away from the character's position.
- Visualize-paths. The system can highlight the path to a target on the grid.
Useful for showing where the pathfinder is moving you.
- The Map parser can be customized so as to give different weight to longer
links - by default a 3-step long link has the same 'weight' for the
pathfinder as two nodes next to each other. This could be combined with
some measure of it taking longer to traverse such exits.
### MultiMap
Multiple `Maps` (or 'Z coordinates') are combined together in the `MultiMap`
handler. The MultiMap knows all maps in the game because it must be possible to
transition from one to the other by giving the name/Z-coordinate to jump to.
It's worth pointing out that neither `Maps`, nor `MultiMap` has any direct
link to the game at this point - these are all just abstract _representations_
of the game world stored in memory.
### Rooms and Exits
The component (_Nodes_ and _Links_ both) of each `Map` can have a `prototype`
dict associated with it. This is the default used when converting this node/link
into something actually visible in-game. It is also possible to override
the default on a per-XY coordinate level.
- For _Nodes_, the `evennia.contrib.xyzgrid.room.XYZRoom` typeclass (or a child
thereof) should be used. This sets up storage of the XYZ-coordinates correctly
(using `Tags`).
- For _Links_, one uses the `evennia.contrib.xyzgrid.room.XYZExit` typeclass
(or a child thereof). This also sets up the proper coordinates, both for the
location they are in and for the exit's destination.
### The Grid
The combination of `MultiMaps` and `prototypes` are used to create the `Grid`, a
series of coordinate-bound rooms/exits actually present in the game world. Each
node of this grid has a unique `(X, Y, Z)` position (no duplicates).
Once the prototypes have been used to create the grid, it must be kept in-sync
with any changes in map-strings `Map` structures - otherwise pathfinding and
visual-range displays will not work (or at least be confusingly inaccurate). So
changes should _only_ be done on the Map-string outside of the game, _not_ by
digging new XYZRooms manually!
The contrib provides a sync mechanism. This compares the stored `Map` with
the current topology and rebuilds/removes any nodes/rooms/links/exits that
has changed. Since this process can be slow, you need to run this manually when
you know you've made a change.
Remember that syncing is only necesssary for topological changes! That is,
changes visible in the map string. Fixing the `desc` of a room or adding a new
enemy does not require any re-sync. Also, things not visible on the
map (like a secret entrance) should not be available to the pathfinder anyway.
You can dig non-XYZRoom objects and link them to `XYZRooms` with no issues -
they will work like normal in-game. But such rooms (and exits leading to/from
them) are _not_ considered part of the grid for the purposes of pathfinding etc.
Exactly how to organize this depends on your game.

View file

@ -0,0 +1,20 @@
"""
The grid
This represents the full XYZ grid, which consists of
- 2D `Map`-objects parsed from Map strings and Map-legend components. Each represents one
Z-coordinate or location.
- `Prototypes` for how to build each XYZ component into 'real' rooms and exits.
- Actual in-game rooms and exits, mapped to the game based on Map data.
The grid has three main functions:
- Building new rooms/exits from scratch based on one or more Maps.
- Updating the rooms/exits tied to an existing Map when the Map string
of that map changes.
"""
class XYZGrid:
pass

View file

@ -37,7 +37,7 @@ LEGEND = {
PARENT = {
"key": "An empty dungeon room",
"prototype_key": "dungeon_doom_prot",
"typeclass": "evennia.contrib.mapsystem.rooms.XYRoom",
"typeclass": "evennia.contrib.xyzgrid.xyzrooms.XYZRoom",
"desc": "Air is cold and stale in this barren room."
}

View file

@ -11,7 +11,7 @@ try:
from scipy import zeros
except ImportError as err:
raise ImportError(
f"{err}\nThe MapSystem contrib requires "
f"{err}\nThe XYZgrid contrib requires "
"the SciPy package. Install with `pip install scipy'.")
from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL

View file

@ -47,7 +47,7 @@ as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw',
'''
LEGEND = {'#': mapsystem.MapNode, '|': mapsystem.NSMapLink,...}
LEGEND = {'#': xyzgrid.MapNode, '|': xyzgrid.NSMapLink,...}
# optional, for more control
MAP_DATA = {
@ -92,7 +92,7 @@ try:
from scipy import zeros
except ImportError as err:
raise ImportError(
f"{err}\nThe MapSystem contrib requires "
f"{err}\nThe XYZgrid contrib requires "
"the SciPy package. Install with `pip install scipy'.")
from django.conf import settings
from evennia.utils.utils import variable_from_module, mod_import

View file

@ -1,8 +1,8 @@
"""
XYZ-aware rooms and exits.
These are intended to be used with the MapSystem - which interprets the `Z` 'coordinate' as
different (named) 2D XY maps. But if not wanting to use the MapSystem gridding, these can also be
These are intended to be used with the XYZgrid - which interprets the `Z` 'coordinate' as
different (named) 2D XY maps. But if not wanting to use the XYZgrid gridding, these can also be
used as stand-alone XYZ-coordinate-aware rooms.
"""
@ -12,7 +12,7 @@ from evennia.objects.objects import DefaultRoom, DefaultExit
from evennia.objects.manager import ObjectManager
# name of all tag categories. Note that the Z-coordinate is
# the `map_name` of the MapSystem
# the `map_name` of the XYZgrid
MAP_X_TAG_CATEGORY = "room_x_coordinate"
MAP_Y_TAG_CATEGORY = "room_y_coordinate"
MAP_Z_TAG_CATEGORY = "room_z_coordinate"
@ -39,7 +39,7 @@ class XYZManager(ObjectManager):
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
the `Z`-coordinate is the name of the map (case-sensitive) in the MapSystem contrib.
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.
Returns:
@ -65,7 +65,7 @@ class XYZManager(ObjectManager):
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 MapSystem contrib.
acts as the name (case-sensitive) of the map in the XYZgrid contrib.
**kwargs: All other kwargs are passed on to the query.
Returns:
@ -102,7 +102,7 @@ class XYZExitManager(XYZManager):
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
the `Z`-coordinate is the name of the map (case-sensitive) in the MapSystem contrib.
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.
**kwargs: All other kwargs are passed on to the query.
@ -116,7 +116,7 @@ class XYZExitManager(XYZManager):
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 MapSystem, `z != zdest` means a _transit_ between different maps.
In the XYZgrid, `z != zdest` means a _transit_ between different maps.
"""
x, y, z = coord
@ -145,7 +145,7 @@ class XYZExitManager(XYZManager):
Kwargs:
coord (tuple, optional): A tuple (X, Y, Z) for the source location. Each
element is either an `int` or `str` (not `None`).
the `Z`-coordinate is the name of the map (case-sensitive) in the MapSystem contrib.
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.
**kwargs: All other kwargs are passed on to the query.
@ -196,7 +196,7 @@ class XYZRoom(DefaultRoom):
rooms).
coords (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
MapSystem, the X, Y are always integers while the `Z` coordinate is used for the
XYZgrid, the X, Y are always integers while the `Z` coordinate is used for the
map's name.
**kwargs: Will be passed into the normal `DefaultRoom.create` method.
@ -250,9 +250,10 @@ class XYZExit(DefaultExit):
rooms).
coords (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
MapSystem, 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
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.
location (Object, optional): Only used if `coord` is not given. This can be used
to place this exit in any room, including non-XYRoom type rooms.

View file

@ -1,6 +1,6 @@
"""
Tests for the Mapsystem
Tests for the XYZgrid system.
"""