Evennia for roleplaying sessions¶
This tutorial will explain how to set up a realtime or play-by-post tabletop style game using a fresh Evennia server.
The scenario is thus: You and a bunch of friends want to play a tabletop role playing game online. One of you will be the game master and you are all okay with playing using written text. You want both the ability to role play in real-time (when people happen to be online at the same time) as well as the ability for people to post when they can and catch up on what happened since they were last online.
This is the functionality we will be needing and using:
The ability to make one of you the GM (game master), with special abilities.
A Character sheet that players can create, view and fill in. It can also be locked so only the GM can modify it.
A dice roller mechanism, for whatever type of dice the RPG rules require.
Rooms, to give a sense of location and to compartmentalize play going on- This means both Character movements from location to location and GM explicitly moving them around.
Channels, for easily sending text to all subscribing accounts, regardless of location.
Account-to-Account messaging capability, including sending to multiple recipients simultaneously, regardless of location.
We will find most of these things are already part of vanilla Evennia, but that we can expand on the defaults for our particular use-case. Below we will flesh out these components from start to finish.
Starting out¶
We will assume you start from scratch. You need Evennia installed, as per the Setup Quickstart
instructions. Initialize a new game directory with evennia init <gamedirname>
. In this tutorial we assume your game dir is simply named mygame
. You can use the default database and keep all other settings to default for now. Familiarize yourself with the
mygame
folder before continuing. You might want to browse the Beginner Tutorial tutorial, just to see roughly where things are modified.
The Game Master role¶
In brief:
Simplest way: Being an admin, just give one account
Admins
permission using the standardperm
command.Better but more work: Make a custom command to set/unset the above, while tweaking the Character to show your renewed GM status to the other accounts.
The permission hierarchy¶
Evennia has the following permission hierarchy out of the box: Players, Helpers, Builders, Admins and finally Developers. We could change these but then we’d need to update our Default commands to use the changes. We want to keep this simple, so instead we map our different roles on top of this permission ladder.
Players
is the permission set on normal players. This is the default for anyone creating a new account on the server.Helpers
are likePlayers
except they also have the ability to create/edit new help entries. This could be granted to players who are willing to help with writing lore or custom logs for everyone.Builders
is not used in our case since the GM should be the only world-builder.Admins
is the permission level the GM should have. Admins can do everything builders can (create/describe rooms etc) but also kick accounts, rename them and things like that.Developers
-level permission are the server administrators, the ones with the ability to restart/shutdown the server as well as changing the permission levels.
The superuser is not part of the hierarchy and actually completely bypasses it. We’ll assume server admin(s) will “just” be Developers.
How to grant permissions¶
Only Developers
can (by default) change permission level. Only they have access to the @perm
command:
> perm Yvonne
Permissions on Yvonne: accounts
> perm Yvonne = Admins
> perm Yvonne
Permissions on Yvonne: accounts, admins
> perm/del Yvonne = Admins
> perm Yvonne
Permissions on Yvonne: accounts
There is no need to remove the basic Players
permission when adding the higher permission: the
highest will be used. Permission level names are not case sensitive. You can also use both plural
and singular, so “Admins” gives the same powers as “Admin”.
Optional: Making a GM-granting command¶
Use of perm
works out of the box, but it’s really the bare minimum. Would it not be nice if other
accounts could tell at a glance who the GM is? Also, we shouldn’t really need to remember that the
permission level is called “Admins”. It would be easier if we could just do @gm <account>
and
@notgm <account>
and at the same time change something make the new GM status apparent.
So let’s make this possible. This is what we’ll do:
We’ll customize the default Character class. If an object of this class has a particular flag, its name will have the string
(GM)
added to the end.We’ll add a new command, for the server admin to assign the GM-flag properly.
Character modification¶
Let’s first start by customizing the Character. We recommend you browse the beginning of the
Account page to make sure you know how Evennia differentiates between the OOC “Account
objects” (not to be confused with the Accounts
permission, which is just a string specifying your
access) and the IC “Character objects”.
Open mygame/typeclasses/characters.py
and modify the default Character
class:
# in mygame/typeclasses/characters.py
# [...]
class Character(DefaultCharacter):
# [...]
def get_display_name(self, looker, **kwargs):
"""
This method customizes how character names are displayed. We assume
only permissions of types "Developers" and "Admins" require
special attention.
"""
name = self.key
selfaccount = self.account # will be None if we are not puppeted
lookaccount = looker.account # - " -
if selfaccount and selfaccount.db.is_gm:
# A GM. Show name as name(GM)
name = f"{name}(GM)"
if lookaccount and \
(lookaccount.permissions.get("Developers") or lookaccount.db.is_gm):
# Developers/GMs see name(#dbref) or name(GM)(#dbref)
name = f"{name}(#{self.id})"
return name
Above, we change how the Character’s name is displayed: If the account controlling this Character is
a GM, we attach the string (GM)
to the Character’s name so everyone can tell who’s the boss. If we
ourselves are Developers or GM’s we will see database ids attached to Characters names, which can
help if doing database searches against Characters of exactly the same name. We base the “gm-
ingness” on having an flag (an Attribute) named is_gm
. We’ll make sure new GM’s
actually get this flag below.
Extra exercise: This will only show the
(GM)
text on Characters puppeted by a GM account, that is, it will show only to those in the same location. If we wanted it to also pop up in, say,who
listings and channels, we’d need to make a similar change to theAccount
typeclass inmygame/typeclasses/accounts.py
. We leave this as an exercise to the reader.
New @gm/@ungm command¶
We will describe in some detail how to create and add an Evennia command here with the hope that we don’t need to be as detailed when adding commands in the future. We will build on Evennia’s default “mux-like” commands here.
Open mygame/commands/command.py
and add a new Command class at the bottom:
# in mygame/commands/command.py
from evennia import default_cmds
# [...]
import evennia
class CmdMakeGM(default_cmds.MuxCommand):
"""
Change an account's GM status
Usage:
@gm <account>
@ungm <account>
"""
# note using the key without @ means both @gm !gm etc will work
key = "gm"
aliases = "ungm"
locks = "cmd:perm(Developers)"
help_category = "RP"
def func(self):
"Implement the command"
caller = self.caller
if not self.args:
caller.msg("Usage: @gm account or @ungm account")
return
accountlist = evennia.search_account(self.args) # returns a list
if not accountlist:
caller.msg(f"Could not find account '{self.args}'")
return
elif len(accountlist) > 1:
caller.msg(f"Multiple matches for '{self.args}': {accountlist}")
return
else:
account = accountlist[0]
if self.cmdstring == "gm":
# turn someone into a GM
if account.permissions.get("Admins"):
caller.msg(f"Account {account} is already a GM.")
else:
account.permissions.add("Admins")
caller.msg(f"Account {account} is now a GM.")
account.msg(f"You are now a GM (changed by {caller}).")
account.character.db.is_gm = True
else:
# @ungm was entered - revoke GM status from someone
if not account.permissions.get("Admins"):
caller.msg(f"Account {account} is not a GM.")
else:
account.permissions.remove("Admins")
caller.msg(f"Account {account} is no longer a GM.")
account.msg(f"You are no longer a GM (changed by {caller}).")
del account.character.db.is_gm
All the command does is to locate the account target and assign it the Admins
permission if we
used gm
or revoke it if using the ungm
alias. We also set/unset the is_gm
Attribute that is
expected by our new Character.get_display_name
method from earlier.
We could have made this into two separate commands or opted for a syntax like
gm/revoke <accountname>
. Instead we examine how this command was called (stored inself.cmdstring
) in order to act accordingly. Either way works, practicality and coding style decides which to go with.
To actually make this command available (only to Developers, due to the lock on it), we add it to the default Account command set. Open the file mygame/commands/default_cmdsets.py
and find the AccountCmdSet
class:
# mygame/commands/default_cmdsets.py
# [...]
from commands.command import CmdMakeGM
class AccountCmdSet(default_cmds.AccountCmdSet):
# [...]
def at_cmdset_creation(self):
# [...]
self.add(CmdMakeGM())
Finally, issue the reload
command to update the server to your changes. Developer-level players
(or the superuser) should now have the gm/ungm
command available.
Character sheet¶
In brief:
Use Evennia’s EvTable/EvForm to build a Character sheet
Tie individual sheets to a given Character.
Add new commands to modify the Character sheet, both by Accounts and GMs.
Make the Character sheet lockable by a GM, so the Player can no longer modify it.
Building a Character sheet¶
There are many ways to build a Character sheet in text, from manually pasting strings together to more automated ways. Exactly what is the best/easiest way depends on the sheet one tries to create. We will here show two examples using the EvTable and EvForm utilities.Later we will create Commands to edit and display the output from those utilities.
Note that these docs don’t show the color. see the text tag documentation for how to add color to the tables and forms.
Making a sheet with EvTable¶
EvTable is a text-table generator. It helps with displaying text in ordered rows and columns. This is an example of using it in code:
# this can be tried out in a Python shell like iPython
from evennia.utils import evtable
# we hardcode these for now, we'll get them as input later
STR, CON, DEX, INT, WIS, CHA = 12, 13, 8, 10, 9, 13
table = evtable.EvTable("Attr", "Value",
table = [
["STR", "CON", "DEX", "INT", "WIS", "CHA"],
[STR, CON, DEX, INT, WIS, CHA]
], align='r', border="incols")
Above, we create a two-column table by supplying the two columns directly. We also tell the table to be right-aligned and to use the “incols” border type (borders drawns only in between columns). The EvTable
class takes a lot of arguments for customizing its look, you can see some of the possible keyword arguments here. Once you have the table
you could also retroactively add new columns and rows to it with table.add_row()
and table.add_column()
: if necessary the table will expand with empty rows/columns to always remain rectangular.
The result from printing the above table will be
table_string = str(table)
print(table_string)
Attr | Value
~~~~~~+~~~~~~~
STR | 12
CON | 13
DEX | 8
INT | 10
WIS | 9
CHA | 13
This is a minimalistic but effective Character sheet. By combining the table_string
with other
strings one could build up a reasonably full graphical representation of a Character. For more
advanced layouts we’ll look into EvForm next.
Making a sheet with EvForm¶
EvForm allows the creation of a two-dimensional “graphic” made by text characters. On this surface, one marks and tags rectangular regions (“cells”) to be filled with content. This content can be either normal strings or EvTable
instances (see the previous section, one such instance would be the table
variable in that example).
In the case of a Character sheet, these cells would be comparable to a line or box where you could enter the name of your character or their strength score. EvMenu also easily allows to update the content of those fields in code (it use EvTables so you rebuild the table first before re-sending it to EvForm).
The drawback of EvForm is that its shape is static; if you try to put more text in a region than it was sized for, the text will be cropped. Similarly, if you try to put an EvTable instance in a field too small for it, the EvTable will do its best to try to resize to fit, but will eventually resort to cropping its data or even give an error if too small to fit any data.
An EvForm is defined in a Python module. Create a new file mygame/world/charsheetform.py
and
modify it thus:
#coding=utf-8
# in mygame/world/charsheetform.py
FORMCHAR = "x"
TABLECHAR = "c"
FORM = """
.--------------------------------------.
| |
| Name: xxxxxxxxxxxxxx1xxxxxxxxxxxxxxx |
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| |
>------------------------------------<
| |
| ccccccccccc Advantages: |
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
| ccccccccccc xxxxxxxxxx3xxxxxxxxxxx |
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
| ccccc2ccccc Disadvantages: |
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
| ccccccccccc xxxxxxxxxx4xxxxxxxxxxx |
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
| |
+--------------------------------------+
"""
The #coding
statement (which must be put on the very first line to work) tells Python to use the
utf-8 encoding for the file. Using the FORMCHAR
and TABLECHAR
we define what single-character we
want to use to “mark” the regions of the character sheet holding cells and tables respectively.
Within each block (which must be separated from one another by at least one non-marking character) we embed identifiers 1-4 to identify each block. The identifier could be any single character except for the FORMCHAR
and TABLECHAR
You can still use
FORMCHAR
andTABLECHAR
elsewhere in your sheet, but not in a way that it would identify a cell/table. The smallest identifiable cell/table area is 3 characters wide including the identifier (for examplex2x
).
Now we will map content to this form.
# again, this can be tested in a Python shell
# hard-code this info here, later we'll ask the
# account for this info. We will re-use the 'table'
# variable from the EvTable example.
NAME = "John, the wise old admin with a chip on his shoulder"
ADVANTAGES = "Language-wiz, Intimidation, Firebreathing"
DISADVANTAGES = "Bad body odor, Poor eyesight, Troubled history"
from evennia.utils import evform
# load the form from the module
form = evform.EvForm("world/charsheetform.py")
# map the data to the form
form.map(cells={"1":NAME, "3": ADVANTAGES, "4": DISADVANTAGES},
tables={"2":table})
We create some RP-sounding input and re-use the table
variable from the previous EvTable
example.
Note, that if you didn’t want to create the form in a separate module you could also load it directly into the
EvForm
call like this:EvForm(form={"FORMCHAR":"x", "TABLECHAR":"c", "FORM": formstring})
whereFORM
specifies the form as a string in the same way as listed in the module above. Note however that the very first line of theFORM
string is ignored, so start with a\n
.
We then map those to the cells of the form:
print(form)
.--------------------------------------.
| |
| Name: John, the wise old admin with |
| a chip on his shoulder |
| |
>------------------------------------<
| |
| Attr|Value Advantages: |
| ~~~~~+~~~~~ Language-wiz, |
| STR| 12 Intimidation, |
| CON| 13 Firebreathing |
| DEX| 8 Disadvantages: |
| INT| 10 Bad body odor, Poor |
| WIS| 9 eyesight, Troubled |
| CHA| 13 history |
| |
+--------------------------------------+
As seen, the texts and tables have been slotted into the text areas and line breaks have been added where needed. We chose to just enter the Advantages/Disadvantages as plain strings here, meaning long names ended up split between rows. If we wanted more control over the display we could have inserted \n
line breaks after each line or used a borderless EvTable
to display those as well.
Tie a Character sheet to a Character¶
We will assume we go with the EvForm
example above. We now need to attach this to a Character so it can be modified. For this we will modify our Character
class a little more:
# mygame/typeclasses/character.py
from evennia.utils import evform, evtable
[...]
class Character(DefaultCharacter):
[...]
def at_object_creation(self):
"called only once, when object is first created"
# we will use this to stop account from changing sheet
self.db.sheet_locked = False
# we store these so we can build these on demand
self.db.chardata = {"str": 0,
"con": 0,
"dex": 0,
"int": 0,
"wis": 0,
"cha": 0,
"advantages": "",
"disadvantages": ""}
self.db.charsheet = evform.EvForm("world/charsheetform.py")
self.update_charsheet()
def update_charsheet(self):
"""
Call this to update the sheet after any of the ingoing data
has changed.
"""
data = self.db.chardata
table = evtable.EvTable("Attr", "Value",
table = [
["STR", "CON", "DEX", "INT", "WIS", "CHA"],
[data["str"], data["con"], data["dex"],
data["int"], data["wis"], data["cha"]]],
align='r', border="incols")
self.db.charsheet.map(tables={"2": table},
cells={"1":self.key,
"3":data["advantages"],
"4":data["disadvantages"]})
Use reload
to make this change available to all newly created Characters. Already existing
Characters will not have the charsheet defined, since at_object_creation
is only called once.
The easiest to force an existing Character to re-fire its at_object_creation
is to use the
typeclass
command in-game:
typeclass/force <Character Name>
Command for Account to change Character sheet¶
We will add a command to edit the sections of our Character sheet. Open
mygame/commands/command.py
.
# at the end of mygame/commands/command.py
ALLOWED_ATTRS = ("str", "con", "dex", "int", "wis", "cha")
ALLOWED_FIELDNAMES = ALLOWED_ATTRS + \
("name", "advantages", "disadvantages")
def _validate_fieldname(caller, fieldname):
"Helper function to validate field names."
if fieldname not in ALLOWED_FIELDNAMES:
list_of_fieldnames = ", ".join(ALLOWED_FIELDNAMES)
err = f"Allowed field names: {list_of_fieldnames}"
caller.msg(err)
return False
if fieldname in ALLOWED_ATTRS and not value.isdigit():
caller.msg(f"{fieldname} must receive a number.")
return False
return True
class CmdSheet(MuxCommand):
"""
Edit a field on the character sheet
Usage:
@sheet field value
Examples:
@sheet name Ulrik the Warrior
@sheet dex 12
@sheet advantages Super strength, Night vision
If given without arguments, will view the current character sheet.
Allowed field names are:
name,
str, con, dex, int, wis, cha,
advantages, disadvantages
"""
key = "sheet"
aliases = "editsheet"
locks = "cmd: perm(Players)"
help_category = "RP"
def func(self):
caller = self.caller
if not self.args or len(self.args) < 2:
# not enough arguments. Display the sheet
if sheet:
caller.msg(caller.db.charsheet)
else:
caller.msg("You have no character sheet.")
return
# if caller.db.sheet_locked:
caller.msg("Your character sheet is locked.")
return
# split input by whitespace, once
fieldname, value = self.args.split(None, 1)
fieldname = fieldname.lower() # ignore case
if not _validate_fieldnames(caller, fieldname):
return
if fieldname == "name":
self.key = value
else:
caller.chardata[fieldname] = value
caller.update_charsheet()
caller.msg(f"{fieldname} was set to {value}.")
Most of this command is error-checking to make sure the right type of data was input. Note how the sheet_locked
Attribute is checked and will return if not set.
This command you import into mygame/commands/default_cmdsets.py
and add to the CharacterCmdSet
, in the same way the @gm
command was added to the AccountCmdSet
earlier.
Commands for GM to change Character sheet¶
Game masters use basically the same input as Players do to edit a character sheet, except they can do it on other players than themselves. They are also not stopped by any sheet_locked
flags.
# continuing in mygame/commands/command.py
class CmdGMsheet(MuxCommand):
"""
GM-modification of char sheets
Usage:
@gmsheet character [= fieldname value]
Switches:
lock - lock the character sheet so the account
can no longer edit it (GM's still can)
unlock - unlock character sheet for Account
editing.
Examples:
@gmsheet Tom
@gmsheet Anna = str 12
@gmsheet/lock Tom
"""
key = "gmsheet"
locks = "cmd: perm(Admins)"
help_category = "RP"
def func(self):
caller = self.caller
if not self.args:
caller.msg("Usage: @gmsheet character [= fieldname value]")
if self.rhs:
# rhs (right-hand-side) is set only if a '='
# was given.
if len(self.rhs) < 2:
caller.msg("You must specify both a fieldname and value.")
return
fieldname, value = self.rhs.split(None, 1)
fieldname = fieldname.lower()
if not _validate_fieldname(caller, fieldname):
return
charname = self.lhs
else:
# no '=', so we must be aiming to look at a charsheet
fieldname, value = None, None
charname = self.args.strip()
character = caller.search(charname, global_search=True)
if not character:
return
if "lock" in self.switches:
if character.db.sheet_locked:
caller.msg("The character sheet is already locked.")
else:
character.db.sheet_locked = True
caller.msg(f"{character.key} can no longer edit their character sheet.")
elif "unlock" in self.switches:
if not character.db.sheet_locked:
caller.msg("The character sheet is already unlocked.")
else:
character.db.sheet_locked = False
caller.msg(f"{character.key} can now edit their character sheet.")
if fieldname:
if fieldname == "name":
character.key = value
else:
character.db.chardata[fieldname] = value
character.update_charsheet()
caller.msg(f"You set {character.key}'s {fieldname} to {value}.")
else:
# just display
caller.msg(character.db.charsheet)
The gmsheet
command takes an additional argument to specify which Character’s character sheet to edit. It also takes /lock
and /unlock
switches to block the Player from tweaking their sheet.
Before this can be used, it should be added to the default CharacterCmdSet
in the same way as the normal sheet
. Due to the lock set on it, this command will only be available to Admins
(i.e. GMs) or higher permission levels.
Dice roller¶
Evennia’s contrib folder already comes with a full dice roller. To add it to the game, simply import contrib.dice.CmdDice
into mygame/commands/default_cmdsets.py
and add CmdDice
to the CharacterCmdset
as done with other commands in this tutorial. After a @reload
you will be able
to roll dice using normal RPG-style format:
roll 2d6 + 3
7
Use help dice
to see what syntax is supported or look at evennia/contrib/dice.py
to see how it’s implemented.
Rooms¶
Evennia comes with rooms out of the box, so no extra work needed. A GM will automatically have all needed building commands available. A fuller go-through is found in the Building tutorial. Here are some useful highlights:
dig roomname;alias = exit_there;alias, exit_back;alias
- this is the basic command for digging a new room. You can specify any exit-names and just enter the name of that exit to go there.tunnel direction = roomname
- this is a specialized command that only accepts directions in the cardinal directions (n,ne,e,se,s,sw,w,nw) as well as in/out and up/down. It also automatically builds “matching” exits back in the opposite direction.create/drop objectname
- this creates and drops a new simple object in the current location.desc obj
- change the look-description of the object.tel object = location
- teleport an object to a named location.search objectname
- locate an object in the database.
TODO: Describe how to add a logging room, that logs says and poses to a log file that people can access after the fact.
Channels¶
Evennia comes with Channels in-built and they are described fully in the documentation. For brevity, here are the relevant commands for normal use:
channel/create = new_channel;alias;alias = short description
- Creates a new channel.channel/sub channel
- subscribe to a channel.channel/unsub channel
- unsubscribel from a channel.channels
lists all available channels, including your subscriptions and any aliases you have set up for them.
You can read channel history: if you for example are chatting on the public
channel you can do
public/history
to see the 20 last posts to that channel or public/history 32
to view twenty
posts backwards, starting with the 32nd from the end.
PMs¶
To send PMs to one another, players can use the page
(or tell
) command:
page recipient = message
page recipient, recipient, ... = message
Players can use page
alone to see the latest messages. This also works if they were not online
when the message was sent.