Source code for evennia.contrib.grid.ingame_map_display.ingame_map_display

"""
Basic Map - helpme 2022

This adds an ascii `map` to a given room which can be viewed with the `map` command.
You can easily alter it to add special characters, room colors etc. The map shown is
dynamically generated on use, and supports all compass directions and up/down. Other
directions are ignored.

If you don't expect the map to be updated frequently, you could choose to save the
calculated map as a .ndb value on the room and render that instead of running mapping
calculations anew each time.

An example map:
```
       |
     -[-]-
       |
       |
-[-]--[-]--[-]--[-]
  |    |    |    |
       |    |    |
     -[-]--[-]  [-]
       | \/ |    |
     \ | /\ |
     -[-]--[-]
```

Installation:

Adding the `MapDisplayCmdSet` to the default character cmdset will add the `map` command.

Specifically, in `mygame/commands/default_cmdsets.py`:

```
...
from evennia.contrib.grid.ingame_map_display import MapDisplayCmdSet  # <---

class CharacterCmdset(default_cmds.Character_CmdSet):
    ...
    def at_cmdset_creation(self):
        ...
        self.add(MapDisplayCmdSet)  # <---

```

Then `reload` to make the new commands available.

Additional Settings:

In order to change your default map size, you can add to `mygame/server/settings.py`:

BASIC_MAP_SIZE = 5

This changes the default map width/height. 2-5 for most clients is sensible.

If you don't want the player to be able to specify the size of the map, ignore any
arguments passed into the Map command.
"""
import time

from django.conf import settings

from evennia import CmdSet
from evennia.commands.default.muxcommand import MuxCommand

_BASIC_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, "BASIC_MAP_SIZE") else 2
_MAX_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, "MAX_MAP_SIZE") else 10

# _COMPASS_DIRECTIONS specifies which way to move the pointer on the x/y axes and what characters to use to depict the exits on the map.
_COMPASS_DIRECTIONS = {
    "north": (0, -3, " | "),
    "south": (0, 3, " | "),
    "east": (3, 0, "-"),
    "west": (-3, 0, "-"),
    "northeast": (3, -3, "/"),
    "northwest": (-3, -3, "\\"),
    "southeast": (3, 3, "\\"),
    "southwest": (-3, 3, "/"),
    "up": (0, 0, "^"),
    "down": (0, 0, "v"),
}


[docs]class Map(object):
[docs] def __init__(self, caller, size=_BASIC_MAP_SIZE, location=None): """ Initializes the map. Args: caller (object): Any object, though generally a puppeted character. size (int): The seed size of the map, which will be multiplied to get the final grid size. location (object): The location at the map's center (will default to caller.location if none provided). """ self.start_time = time.time() self.caller = caller self.max_width = int(size * 2 + 1) * 5 # This must be an odd number self.max_length = int(size * 2 + 1) * 3 # This must be an odd number self.has_mapped = {} self.curX = None self.curY = None self.size = size self.location = location or caller.location
[docs] def create_grid(self): """ Create the empty grid for the map based on the configured size Returns: list: The created grid, a list of lists. """ board = [] for row in range(self.max_length): board.append([]) for column in range(int(self.max_width / 5)): board[row].extend([" ", " ", " "]) return board
[docs] def exit_name_as_ordinal(self, ex): """ Get the exit name as a compass direction if possible Args: ex (Exit): The current exit being mapped. Returns: string: The exit name as a compass direction or an empty string. """ exit_name = ex.name if exit_name not in _COMPASS_DIRECTIONS: compass_aliases = [ direction in ex.aliases.all() for direction in _COMPASS_DIRECTIONS.keys() ] if compass_aliases[0]: exit_name = compass_aliases[0] if exit_name not in _COMPASS_DIRECTIONS: return "" return exit_name
[docs] def update_pos(self, room, exit_name): """ Update the position pointer. Args: room (Room): The current location. exit_name (str): The name of the exit to to use in this room. This must be a valid compass direction, or an error will be raised. Raises: KeyError: If providing a non-compass exit name. """ # Update the pointer self.curX, self.curY = self.has_mapped[room][0], self.has_mapped[room][1] # Move the pointer depending on which direction the exit lies # exit_name has already been validated as an ordinal direction at this point self.curY += _COMPASS_DIRECTIONS[exit_name][0] self.curX += _COMPASS_DIRECTIONS[exit_name][1]
[docs] def has_drawn(self, room): """ Checks if the given room has already been drawn or not Args: room (Room): Room to check. Returns: bool: Whether or not the room has been drawn. """ return True if room in self.has_mapped.keys() else False
[docs] def draw_room_on_map(self, room, max_distance): """ Draw the room and its exits on the map recursively Args: room (Room): The room to draw out. max_distance (int): How extensive the map is. """ self.draw(room) self.draw_exits(room) if max_distance == 0: return # Check if the caller has access to the room in question. If not, don't draw it. # Additionally, if the name of the exit is not ordinal but an alias of it is, use that. for ex in [x for x in room.exits if x.access(self.caller, "traverse")]: ex_name = self.exit_name_as_ordinal(ex) if not ex_name or ex_name in ["up", "down"]: continue if self.has_drawn(ex.destination): continue self.update_pos(room, ex_name.lower()) self.draw_room_on_map(ex.destination, max_distance - 1)
[docs] def draw_exits(self, room): """ Draw a given room's exit paths Args: room (Room): The room to draw exits of. """ x, y = self.curX, self.curY for ex in room.exits: ex_name = self.exit_name_as_ordinal(ex) if not ex_name: continue ex_character = _COMPASS_DIRECTIONS[ex_name][2] delta_x = int(_COMPASS_DIRECTIONS[ex_name][1] / 3) delta_y = int(_COMPASS_DIRECTIONS[ex_name][0] / 3) # Make modifications if the exit has BOTH up and down exits if ex_name == "up": if "v" in self.grid[x][y]: self.render_room(room, x, y, p1="^", p2="v") else: self.render_room(room, x, y, here="^") elif ex_name == "down": if "^" in self.grid[x][y]: self.render_room(room, x, y, p1="^", p2="v") else: self.render_room(room, x, y, here="v") else: self.grid[x + delta_x][y + delta_y] = ex_character
[docs] def draw(self, room): """ Draw the map starting from a given room and add it to the cache of mapped rooms Args: room (Room): The room to render. """ # draw initial caller location on map first! if room == self.location: self.start_loc_on_grid(room) self.has_mapped[room] = [self.curX, self.curY] else: # map all other rooms self.has_mapped[room] = [self.curX, self.curY] self.render_room(room, self.curX, self.curY)
[docs] def render_room(self, room, x, y, p1="[", p2="]", here=None): """ Draw a given room with ascii characters Args: room (Room): The room to render. x (int): The x-value of the room on the grid (horizontally, east/west). y (int): The y-value of the room on the grid (vertically, north/south). p1 (str): The first character of the 3-character room depiction. p2 (str): The last character of the 3-character room depiction. here (str): Defaults to none, a special character depicting the room. """ # Note: This is where you would set colors, symbols etc. # Render the room you = list("[ ]") you[0] = f"{p1}|n" you[1] = f"{here if here else you[1]}" if room == self.caller.location: you[1] = "|[x|co|n" # Highlight the location you are currently in you[2] = f"{p2}|n" self.grid[x][y] = "".join(you)
[docs] def start_loc_on_grid(self, room): """ Set the starting location on the grid based on the maximum width and length Args: room (Room): The room to begin with. """ x = int((self.max_width * 0.6 - 1) / 2) y = int((self.max_length - 1) / 2) self.render_room(room, x, y) self.curX, self.curY = x, y
[docs] def show_map(self, debug=False): """ Create and show the map, piecing it all together in the end Args: debug (bool): Whether or not to return the time taken to build the map. """ map_string = "" self.grid = self.create_grid() self.draw_room_on_map(self.location, self.size) for row in self.grid: map_row = "".join(row) if map_row.strip() != "": map_string += f"{map_row}\n" elapsed = time.time() - self.start_time if debug: map_string += f"\nTook {elapsed}ms to render the map.\n" return "%s" % map_string
[docs]class CmdMap(MuxCommand): """ Check the local map around you. Usage: map (optional size) """ key = "map"
[docs] def func(self): size = _BASIC_MAP_SIZE max_size = _MAX_MAP_SIZE if self.args.isnumeric(): size = min(max_size, int(self.args)) # You can run show_map(debug=True) to see how long it takes. map_here = Map(self.caller, size=size).show_map() self.caller.msg((map_here, {"type": "map"}))
# CmdSet for easily install all commands
[docs]class MapDisplayCmdSet(CmdSet): """ The map command. """
[docs] def at_cmdset_creation(self): self.add(CmdMap)