"""
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)