Merge pull request #2859 from aMiss-aWry/contrib
Basic Worming Map Contrib
This commit is contained in:
commit
2db753dba5
4 changed files with 415 additions and 0 deletions
51
evennia/contrib/grid/ingame_map_display/README.md
Normal file
51
evennia/contrib/grid/ingame_map_display/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Basic Map
|
||||||
|
|
||||||
|
Contribution - 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.
|
||||||
|
|
||||||
|
## Installation:
|
||||||
|
|
||||||
|
Adding the `MapDisplayCmdSet` to the default character cmdset will add the `map` command.
|
||||||
|
|
||||||
|
Specifically, in `mygame/commands/default_cmdsets.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
...
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Settings:
|
||||||
|
|
||||||
|
In order to change your default map size, you can add to `mygame/server/settings.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
BASIC_MAP_SIZE = 5 # This changes the default map width/height.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features:
|
||||||
|
|
||||||
|
### ASCII map (and evennia supports UTF-8 characters and even emojis)
|
||||||
|
|
||||||
|
This produces an ASCII map for players of configurable size.
|
||||||
|
|
||||||
|
### New command
|
||||||
|
|
||||||
|
- `CmdMap` - view the map
|
||||||
6
evennia/contrib/grid/ingame_map_display/__init__.py
Normal file
6
evennia/contrib/grid/ingame_map_display/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""
|
||||||
|
Mapbuilder - helpme 2022
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .ingame_map_display import MapDisplayCmdSet # noqa
|
||||||
323
evennia/contrib/grid/ingame_map_display/ingame_map_display.py
Normal file
323
evennia/contrib/grid/ingame_map_display/ingame_map_display.py
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
"""
|
||||||
|
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)
|
||||||
35
evennia/contrib/grid/ingame_map_display/tests.py
Normal file
35
evennia/contrib/grid/ingame_map_display/tests.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""
|
||||||
|
Tests of ingame_map_display.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from evennia.commands.default.tests import BaseEvenniaCommandTest
|
||||||
|
from evennia.utils.create import create_object
|
||||||
|
from typeclasses import rooms, exits
|
||||||
|
from . import ingame_map_display
|
||||||
|
|
||||||
|
|
||||||
|
class TestIngameMap(BaseEvenniaCommandTest):
|
||||||
|
"""
|
||||||
|
Test the ingame map display by building two rooms and checking their connections are found
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
[ ]--[ ]
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.west_room = create_object(rooms.Room, key="Room 1")
|
||||||
|
self.east_room = create_object(rooms.Room, key="Room 2")
|
||||||
|
create_object(exits.Exit, key="east", aliases=["e"], location=self.west_room, destination=self.east_room)
|
||||||
|
create_object(exits.Exit, key="west", aliases=["w"], location=self.east_room, destination=self.west_room)
|
||||||
|
|
||||||
|
def test_west_room_map_room(self):
|
||||||
|
self.char1.location = self.west_room
|
||||||
|
map_here = ingame_map_display.Map(self.char1).show_map()
|
||||||
|
self.assertEqual(map_here.strip(), '[|n|[x|co|n]|n--[|n ]|n')
|
||||||
|
|
||||||
|
def test_east_room_map_room(self):
|
||||||
|
self.char1.location = self.east_room
|
||||||
|
map_here = ingame_map_display.Map(self.char1).show_map()
|
||||||
|
self.assertEqual(map_here.strip(), '[|n ]|n--[|n|[x|co|n]|n')
|
||||||
Loading…
Add table
Add a link
Reference in a new issue