diff --git a/evennia/contrib/xyzgrid/README.md b/evennia/contrib/xyzgrid/README.md new file mode 100644 index 0000000000..0686772eaf --- /dev/null +++ b/evennia/contrib/xyzgrid/README.md @@ -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. + diff --git a/evennia/contrib/mapsystem/__init__.py b/evennia/contrib/xyzgrid/__init__.py similarity index 100% rename from evennia/contrib/mapsystem/__init__.py rename to evennia/contrib/xyzgrid/__init__.py diff --git a/evennia/contrib/xyzgrid/grid.py b/evennia/contrib/xyzgrid/grid.py new file mode 100644 index 0000000000..8c3d6d9983 --- /dev/null +++ b/evennia/contrib/xyzgrid/grid.py @@ -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 diff --git a/evennia/contrib/mapsystem/map_example.py b/evennia/contrib/xyzgrid/map_example.py similarity index 94% rename from evennia/contrib/mapsystem/map_example.py rename to evennia/contrib/xyzgrid/map_example.py index f1109e2f4c..92accb2766 100644 --- a/evennia/contrib/mapsystem/map_example.py +++ b/evennia/contrib/xyzgrid/map_example.py @@ -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." } diff --git a/evennia/contrib/mapsystem/map_legend.py b/evennia/contrib/xyzgrid/map_legend.py similarity index 99% rename from evennia/contrib/mapsystem/map_legend.py rename to evennia/contrib/xyzgrid/map_legend.py index 95a1a1d88f..123ea5cfea 100644 --- a/evennia/contrib/mapsystem/map_legend.py +++ b/evennia/contrib/xyzgrid/map_legend.py @@ -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 diff --git a/evennia/contrib/mapsystem/map_multi.py b/evennia/contrib/xyzgrid/map_multi.py similarity index 100% rename from evennia/contrib/mapsystem/map_multi.py rename to evennia/contrib/xyzgrid/map_multi.py diff --git a/evennia/contrib/mapsystem/map_single.py b/evennia/contrib/xyzgrid/map_single.py similarity index 99% rename from evennia/contrib/mapsystem/map_single.py rename to evennia/contrib/xyzgrid/map_single.py index cf4ac85297..0deb82109b 100644 --- a/evennia/contrib/mapsystem/map_single.py +++ b/evennia/contrib/xyzgrid/map_single.py @@ -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 diff --git a/evennia/contrib/mapsystem/maprunner.py b/evennia/contrib/xyzgrid/maprunner.py similarity index 100% rename from evennia/contrib/mapsystem/maprunner.py rename to evennia/contrib/xyzgrid/maprunner.py diff --git a/evennia/contrib/mapsystem/xyzroom.py b/evennia/contrib/xyzgrid/room.py similarity index 93% rename from evennia/contrib/mapsystem/xyzroom.py rename to evennia/contrib/xyzgrid/room.py index 973aa8923f..f42ec2e539 100644 --- a/evennia/contrib/mapsystem/xyzroom.py +++ b/evennia/contrib/xyzgrid/room.py @@ -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. diff --git a/evennia/contrib/mapsystem/tests.py b/evennia/contrib/xyzgrid/tests.py similarity index 99% rename from evennia/contrib/mapsystem/tests.py rename to evennia/contrib/xyzgrid/tests.py index 42baca1d8f..5e7df76700 100644 --- a/evennia/contrib/mapsystem/tests.py +++ b/evennia/contrib/xyzgrid/tests.py @@ -1,6 +1,6 @@ """ -Tests for the Mapsystem +Tests for the XYZgrid system. """ diff --git a/evennia/contrib/mapsystem/utils.py b/evennia/contrib/xyzgrid/utils.py similarity index 100% rename from evennia/contrib/mapsystem/utils.py rename to evennia/contrib/xyzgrid/utils.py