Note

You are reading an old version of the Evennia documentation. The latest version is here

.

# 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"),
    }

    class Map(object):
        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

        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

        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

        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]

        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

        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)

        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

        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)

        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)

        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

        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

    class CmdMap(MuxCommand):
        """
        Check the local map around you.

        Usage: map (optional size)
        """

        key = "map"

        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
    class MapDisplayCmdSet(CmdSet):
        """
        The map command.
        """

        def at_cmdset_creation(self):
            self.add(CmdMap)

Note

You are reading an old version of the Evennia documentation. The latest version is here

.