Reorganize docs into flat folder layout

This commit is contained in:
Griatch 2020-06-17 18:06:41 +02:00
parent 106558cec0
commit 892d8efb93
135 changed files with 34 additions and 1180 deletions

View file

@ -0,0 +1,232 @@
# Add a wiki on your website
**Before doing this tutorial you will probably want to read the intro in
[Basic Web tutorial](Web-Tutorial).** Reading the three first parts of the
[Django tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/) might help as well.
This tutorial will provide a step-by-step process to installing a wiki on your website.
Fortunately, you don't have to create the features manually, since it has been done by others, and
we can integrate their work quite easily with Django. I have decided to focus on
the [Django-wiki](http://django-wiki.readthedocs.io/).
> Note: this article has been updated for Evennia 0.9. If you're not yet using this version, be
careful, as the django wiki doesn't support Python 2 anymore. (Remove this note when enough time
has passed.)
The [Django-wiki](http://django-wiki.readthedocs.io/) offers a lot of features associated with
wikis, is
actively maintained (at this time, anyway), and isn't too difficult to install in Evennia. You can
see a [demonstration of Django-wiki here](https://demo.django.wiki).
## Basic installation
You should begin by shutting down the Evennia server if it is running. We will run migrations and
alter the virtual environment just a bit. Open a terminal and activate your Python environment, the
one you use to run the `evennia` command.
* On Linux:
```
source evenv/bin/activate
```
* Or Windows:
```
evenv\bin\activate
```
### Installing with pip
Install the wiki using pip:
pip install wiki
> Note: this will install the last version of Django wiki. Version >0.4 doesn't support Python 2, so
install wiki 0.3 if you haven't updated to Python 3 yet.
It might take some time, the Django-wiki having some dependencies.
### Adding the wiki in the settings
You will need to add a few settings to have the wiki app on your website. Open your
`server/conf/settings.py` file and add the following at the bottom (but before importing
`secret_settings`). Here's what you'll find in my own setting file (add the whole Django-wiki
section):
```python
r"""
Evennia settings file.
...
"""
# Use the defaults from Evennia unless explicitly overridden
from evennia.settings_default import *
######################################################################
# Evennia base server config
######################################################################
# This is the name of your game. Make it catchy!
SERVERNAME = "demowiki"
######################################################################
# Django-wiki settings
######################################################################
INSTALLED_APPS += (
'django.contrib.humanize.apps.HumanizeConfig',
'django_nyt.apps.DjangoNytConfig',
'mptt',
'sorl.thumbnail',
'wiki.apps.WikiConfig',
'wiki.plugins.attachments.apps.AttachmentsConfig',
'wiki.plugins.notifications.apps.NotificationsConfig',
'wiki.plugins.images.apps.ImagesConfig',
'wiki.plugins.macros.apps.MacrosConfig',
)
# Disable wiki handling of login/signup
WIKI_ACCOUNT_HANDLING = False
WIKI_ACCOUNT_SIGNUP_ALLOWED = False
######################################################################
# Settings given in secret_settings.py override those in this file.
######################################################################
try:
from server.conf.secret_settings import *
except ImportError:
print("secret_settings.py file not found or failed to import.")
```
### Adding the new URLs
Next we need to add two URLs in our `web/urls.py` file. Open it and compare the following output:
you will need to add two URLs in `custom_patterns` and add one import line:
```python
from django.conf.urls import url, include
from django.urls import path # NEW!
# default evenni a patterns
from evennia.web.urls import urlpatterns
# eventual custom patterns
custom_patterns = [
# url(r'/desired/url/', view, name='example'),
url('notifications/', include('django_nyt.urls')), # NEW!
url('wiki/', include('wiki.urls')), # NEW!
]
# this is required by Django.
urlpatterns = custom_patterns + urlpatterns
```
You will probably need to copy line 2, 10, and 11. Be sure to place them correctly, as shown in
the example above.
### Running migrations
It's time to run the new migrations. The wiki app adds a few tables in our database. We'll need to
run:
evennia migrate
And that's it, you can start the server. If you go to http://localhost:4001/wiki , you should see
the wiki. Use your account's username and password to connect to it. That's how simple it is.
## Customizing privileges
A wiki can be a great collaborative tool, but who can see it? Who can modify it? Django-wiki comes
with a privilege system centered around four values per wiki page. The owner of an article can
always read and write in it (which is somewhat logical). The group of the article defines who can
read and who can write, if the user seeing the page belongs to this group. The topic of groups in
wiki pages will not be discussed here. A last setting determines which other user (that is, these
who aren't in the groups, and aren't the article's owner) can read and write. Each article has
these four settings (group read, group write, other read, other write). Depending on your purpose,
it might not be a good default choice, particularly if you have to remind every builder to keep the
pages private. Fortunately, Django-wiki gives us additional settings to customize who can read, and
who can write, a specific article.
These settings must be placed, as usual, in your `server/conf/settings.py` file. They take a
function as argument, said function (or callback) will be called with the article and the user.
Remember, a Django user, for us, is an account. So we could check lockstrings on them if needed.
Here is a default setting to restrict the wiki: only builders can write in it, but anyone (including
non-logged in users) can read it. The superuser has some additional privileges.
```python
# In server/conf/settings.py
# ...
def is_superuser(article, user):
"""Return True if user is a superuser, False otherwise."""
return not user.is_anonymous() and user.is_superuser
def is_builder(article, user):
"""Return True if user is a builder, False otherwise."""
return not user.is_anonymous() and user.locks.check_lockstring(user, "perm(Builders)")
def is_anyone(article, user):
"""Return True even if the user is anonymous."""
return True
# Who can create new groups and users from the wiki?
WIKI_CAN_ADMIN = is_superuser
# Who can change owner and group membership?
WIKI_CAN_ASSIGN = is_superuser
# Who can change group membership?
WIKI_CAN_ASSIGN_OWNER = is_superuser
# Who can change read/write access to groups or others?
WIKI_CAN_CHANGE_PERMISSIONS = is_superuser
# Who can soft-delete an article?
WIKI_CAN_DELETE = is_builder
# Who can lock an article and permanently delete it?
WIKI_CAN_MODERATE = is_superuser
# Who can edit articles?
WIKI_CAN_WRITE = is_builder
# Who can read articles?
WIKI_CAN_READ = is_anyone
```
Here, we have created three functions: one to return `True` if the user is the superuser, one to
return `True` if the user is a builder, one to return `True` no matter what (this includes if the
user is anonymous, E.G. if it's not logged-in). We then change settings to allow either the
superuser or
each builder to moderate, read, write, delete, and more. You can, of course, add more functions,
adapting them to your need. This is just a demonstration.
Providing the `WIKI_CAN*...` settings will bypass the original permission system. The superuser
could change permissions of an article, but still, only builders would be able to write it. If you
need something more custom, you will have to expand on the functions you use.
### Managing wiki pages from Evennia
Unfortunately, Django wiki doesn't provide a clear and clean entry point to read and write articles
from Evennia and it doesn't seem to be a very high priority. If you really need to keep Django wiki
and to create and manage wiki pages from your code, you can do so, but this article won't elaborate,
as this is somewhat more technical.
However, it is a good opportunity to present a small project that has been created more recently:
[evennia-wiki](https://github.com/vincent-lg/evennia-wiki) has been created to provide a simple
wiki, more tailored to Evennia and easier to connect. It doesn't, as yet, provide as many options
as does Django wiki, but it's perfectly usable:
- Pages have an inherent and much-easier to understand hierarchy based on URLs.
- Article permissions are connected to Evennia groups and are much easier to accommodate specific
requirements.
- Articles can easily be created, read or updated from the Evennia code itself.
- Markdown is fully-supported with a default integration to Bootstrap to look good on an Evennia
website. Tables and table of contents are supported as well as wiki links.
- The process to override wiki templates makes full use of the `template_overrides` directory.
However evennia-wiki doesn't yet support:
- Images in markdown and the uploading schema. If images are important to you, please consider
contributing to this new project.
- Modifying permissions on a per page/setting basis.
- Moving pages to new locations.
- Viewing page history.
Considering the list of features in Django wiki, obviously other things could be added to the list.
However, these features may be the most important and useful. Additional ones might not be that
necessary. If you're interested in supporting this little project, you are more than welcome to
[contribute to it](https://github.com/vincent-lg/evennia-wiki). Thanks!

View file

@ -0,0 +1,236 @@
# Building a mech tutorial
> This page was adapted from the article "Building a Giant Mech in Evennia" by Griatch, published in
Imaginary Realities Volume 6, issue 1, 2014. The original article is no longer available online,
this is a version adopted to be compatible with the latest Evennia.
## Creating the Mech
Let us create a functioning giant mech using the Python MUD-creation system Evennia. Everyone likes
a giant mech, right? Start in-game as a character with build privileges (or the superuser).
@create/drop Giant Mech ; mech
Boom. We created a Giant Mech Object and dropped it in the room. We also gave it an alias *mech*.
Lets describe it.
@desc mech = This is a huge mech. It has missiles and stuff.
Next we define who can “puppet” the mech object.
@lock mech = puppet:all()
This makes it so that everyone can control the mech. More mechs to the people! (Note that whereas
Evennias default commands may look vaguely MUX-like, you can change the syntax to look like
whatever interface style you prefer.)
Before we continue, lets make a brief detour. Evennia is very flexible about its objects and even
more flexible about using and adding commands to those objects. Here are some ground rules well
worth remembering for the remainder of this article:
- The [Account](Accounts) represents the real person logging in and has no game-world existence.
- Any [Object](Objects) can be puppeted by an Account (with proper permissions).
- [Characters](Objects#characters), [Rooms](Objects#rooms), and [Exits](Objects#exits) are just
children of normal Objects.
- Any Object can be inside another (except if it creates a loop).
- Any Object can store custom sets of commands on it. Those commands can:
- be made available to the puppeteer (Account),
- be made available to anyone in the same location as the Object, and
- be made available to anyone “inside” the Object
- Also Accounts can store commands on themselves. Account commands are always available unless
commands on a puppeted Object explicitly override them.
In Evennia, using the `@ic` command will allow you to puppet a given Object (assuming you have
puppet-access to do so). As mentioned above, the bog-standard Character class is in fact like any
Object: it is auto-puppeted when logging in and just has a command set on it containing the normal
in-game commands, like look, inventory, get and so on.
@ic mech
You just jumped out of your Character and *are* now the mech! If people look at you in-game, they
will look at a mech. The problem at this point is that the mech Object has no commands of its own.
The usual things like look, inventory and get sat on the Character object, remember? So at the
moment the mech is not quite as cool as it could be.
@ic <Your old Character>
You just jumped back to puppeting your normal, mundane Character again. All is well.
> (But, you ask, where did that `@ic` command come from, if the mech had no commands on it? The
answer is that it came from the Account's command set. This is important. Without the Account being
the one with the `@ic` command, we would not have been able to get back out of our mech again.)
### Arming the Mech
Let us make the mech a little more interesting. In our favorite text editor, we will create some new
mech-suitable commands. In Evennia, commands are defined as Python classes.
```python
# in a new file mygame/commands/mechcommands.py
from evennia import Command
class CmdShoot(Command):
"""
Firing the mechs gun
Usage:
shoot [target]
This will fire your mechs main gun. If no
target is given, you will shoot in the air.
"""
key = "shoot"
aliases = ["fire", "fire!"]
def func(self):
"This actually does the shooting"
caller = self.caller
location = caller.location
if not self.args:
# no argument given to command - shoot in the air
message = "BOOM! The mech fires its gun in the air!"
location.msg_contents(message)
return
# we have an argument, search for target
target = caller.search(self.args.strip())
if target:
message = "BOOM! The mech fires its gun at %s" % target.key
location.msg_contents(message)
class CmdLaunch(Command):
# make your own 'launch'-command here as an exercise!
# (it's very similar to the 'shoot' command above).
```
This is saved as a normal Python module (lets call it `mechcommands.py`), in a place Evennia looks
for such modules (`mygame/commands/`). This command will trigger when the player gives the command
“shoot”, “fire,” or even “fire!” with an exclamation mark. The mech can shoot in the air or at a
target if you give one. In a real game the gun would probably be given a chance to hit and give
damage to the target, but this is enough for now.
We also make a second command for launching missiles (`CmdLaunch`). To save
space we wont describe it here; it looks the same except it returns a text
about the missiles being fired and has different `key` and `aliases`. We leave
that up to you to create as an exercise. You could have it print "WOOSH! The
mech launches missiles against <target>!", for example.
Now we shove our commands into a command set. A [Command Set](Command-Sets) (CmdSet) is a container
holding any number of commands. The command set is what we will store on the mech.
```python
# in the same file mygame/commands/mechcommands.py
from evennia import CmdSet
from evennia import default_cmds
class MechCmdSet(CmdSet):
"""
This allows mechs to do do mech stuff.
"""
key = "mechcmdset"
def at_cmdset_creation(self):
"Called once, when cmdset is first created"
self.add(CmdShoot())
self.add(CmdLaunch())
```
This simply groups all the commands we want. We add our new shoot/launch commands. Lets head back
into the game. For testing we will manually attach our new CmdSet to the mech.
@py self.search("mech").cmdset.add("commands.mechcommands.MechCmdSet")
This is a little Python snippet (run from the command line as an admin) that searches for the mech
in our current location and attaches our new MechCmdSet to it. What we add is actually the Python
path to our cmdset class. Evennia will import and initialize it behind the scenes.
@ic mech
We are back as the mech! Lets do some shooting!
fire!
BOOM! The mech fires its gun in the air!
There we go, one functioning mech. Try your own `launch` command and see that it works too. We can
not only walk around as the mech — since the CharacterCmdSet is included in our MechCmdSet, the mech
can also do everything a Character could do, like look around, pick up stuff, and have an inventory.
We could now shoot the gun at a target or try the missile launch command. Once you have your own
mech, what else do you need?
> Note: You'll find that the mech's commands are available to you by just standing in the same
location (not just by puppeting it). We'll solve this with a *lock* in the next section.
## Making a Mech production line
What weve done so far is just to make a normal Object, describe it and put some commands on it.
This is great for testing. The way we added it, the MechCmdSet will even go away if we reload the
server. Now we want to make the mech an actual object “type” so we can create mechs without those
extra steps. For this we need to create a new Typeclass.
A [Typeclass](Typeclasses) is a near-normal Python class that stores its existence to the database
behind the scenes. A Typeclass is created in a normal Python source file:
```python
# in the new file mygame/typeclasses/mech.py
from typeclasses.objects import Object
from commands.mechcommands import MechCmdSet
from evennia import default_cmds
class Mech(Object):
"""
This typeclass describes an armed Mech.
"""
def at_object_creation(self):
"This is called only when object is first created"
self.cmdset.add_default(default_cmds.CharacterCmdSet)
self.cmdset.add(MechCmdSet, permanent=True)
self.locks.add("puppet:all();call:false()")
self.db.desc = "This is a huge mech. It has missiles and stuff."
```
For convenience we include the full contents of the default `CharacterCmdSet` in there. This will
make a Characters normal commands available to the mech. We also add the mech-commands from before,
making sure they are stored persistently in the database. The locks specify that anyone can puppet
the meck and no-one can "call" the mech's Commands from 'outside' it - you have to puppet it to be
able to shoot.
Thats it. When Objects of this type are created, they will always start out with the mechs command
set and the correct lock. We set a default description, but you would probably change this with
`@desc` to individualize your mechs as you build them.
Back in the game, just exit the old mech (`@ic` back to your old character) then do
@create/drop The Bigger Mech ; bigmech : mech.Mech
We create a new, bigger mech with an alias bigmech. Note how we give the python-path to our
Typeclass at the end — this tells Evennia to create the new object based on that class (we don't
have to give the full path in our game dir `typeclasses.mech.Mech` because Evennia knows to look in
the `typeclasses` folder already). A shining new mech will appear in the room! Just use
@ic bigmech
to take it on a test drive.
## Future Mechs
To expand on this you could add more commands to the mech and remove others. Maybe the mech
shouldnt work just like a Character after all. Maybe it makes loud noises every time it passes from
room to room. Maybe it cannot pick up things without crushing them. Maybe it needs fuel, ammo and
repairs. Maybe youll lock it down so it can only be puppeted by emo teenagers.
Having you puppet the mech-object directly is also just one way to implement a giant mech in
Evennia.
For example, you could instead picture a mech as a “vehicle” that you “enter” as your normal
Character (since any Object can move inside another). In that case the “insides” of the mech Object
could be the “cockpit”. The cockpit would have the `MechCommandSet` stored on itself and all the
shooting goodness would be made available to you only when you enter it.
And of course you could put more guns on it. And make it fly.

View file

@ -0,0 +1,378 @@
# Coding FAQ
*This FAQ page is for users to share their solutions to coding problems. Keep it brief and link to
the docs if you can rather than too lengthy explanations. Don't forget to check if an answer already
exists before answering - maybe you can clarify that answer rather than to make a new Q&A section.*
## Table of Contents
- [Removing default commands](Coding-FAQ#removing-default-commands)
- [Preventing character from moving based on a condition](Coding-FAQ#preventing-character-from-
moving-based-on-a-condition)
- [Reference initiating object in an EvMenu command](Coding-FAQ#reference-initiating-object-in-an-
evmenu-command)
- [Adding color to default Evennia Channels](Coding-FAQ#adding-color-to-default-evennia-channels)
- [Selectively turn off commands in a room](Coding-FAQ#selectively-turn-off-commands-in-a-room)
- [Select Command based on a condition](Coding-FAQ#select-command-based-on-a-condition)
- [Automatically updating code when reloading](Coding-FAQ#automatically-updating-code-when-
reloading)
- [Changing all exit messages](Coding-FAQ#changing-all-exit-messages)
- [Add parsing with the "to" delimiter](Coding-FAQ#add-parsing-with-the-to-delimiter)
- [Store last used session IP address](Coding-FAQ#store-last-used-session-ip-address)
- [Use wide characters with EvTable](Coding-FAQ#non-latin-characters-in-evtable)
## Removing default commands
**Q:** How does one *remove* (not replace) e.g. the default `get` [Command](Commands) from the
Character [Command Set](Command-Sets)?
**A:** Go to `mygame/commands/default_cmdsets.py`. Find the `CharacterCmdSet` class. It has one
method named `at_cmdset_creation`. At the end of that method, add the following line:
`self.remove(default_cmds.CmdGet())`. See the [Adding Commands Tutorial](Adding-Command-Tutorial)
for more info.
## Preventing character from moving based on a condition
**Q:** How does one keep a character from using any exit, if they meet a certain condition? (I.E. in
combat, immobilized, etc.)
**A:** The `at_before_move` hook is called by Evennia just before performing any move. If it returns
`False`, the move is aborted. Let's say we want to check for an [Attribute](Attributes) `cantmove`.
Add the following code to the `Character` class:
```python
def at_before_move(self, destination):
"Called just before trying to move"
if self.db.cantmove: # replace with condition you want to test
self.msg("Something is preventing you from moving!")
return False
return True
```
## Reference initiating object in an EvMenu command.
**Q:** An object has a Command on it starts up an EvMenu instance. How do I capture a reference to
that object for use in the menu?
**A:** When an [EvMenu](EvMenu) is started, the menu object is stored as `caller.ndb._menutree`.
This is a good place to store menu-specific things since it will clean itself up when the menu
closes. When initiating the menu, any additional keywords you give will be available for you as
properties on this menu object:
```python
class MyObjectCommand(Command):
# A Command stored on an object (the object is always accessible from
# the Command as self.obj)
def func(self):
# add the object as the stored_obj menu property
EvMenu(caller, ..., stored_obj=self.obj)
```
Inside the menu you can now access the object through `caller.ndb._menutree.stored_obj`.
## Adding color to default Evennia Channels
**Q:** How do I add colors to the names of Evennia channels?
**A:** The Channel typeclass' `channel_prefix` method decides what is shown at the beginning of a
channel send. Edit `mygame/typeclasses/channels.py` (and then `@reload`):
```python
# define our custom color names
CHANNEL_COLORS = {'public': '|015Public|n',
'newbie': '|550N|n|551e|n|552w|n|553b|n|554i|n|555e|n',
'staff': '|010S|n|020t|n|030a|n|040f|n|050f|n'}
# Add to the Channel class
# ...
def channel_prefix(self, msg, emit=False):
prefix_string = ""
if self.key in COLORS:
prefix_string = "[%s] " % CHANNEL_COLORS.get(self.key.lower())
else:
prefix_string = "[%s] " % self.key.capitalize()
return prefix_string
```
Additional hint: To make colors easier to change from one place you could instead put the
`CHANNEL_COLORS` dict in your settings file and import it as `from django.conf.settings import
CHANNEL_COLORS`.
## Selectively turn off commands in a room
**Q:** I want certain commands to turn off in a given room. They should still work normally for
staff.
**A:** This is done using a custom cmdset on a room [locked with the 'call' lock type](Locks). Only
if this lock is passed will the commands on the room be made available to an object inside it. Here
is an example of a room where certain commands are disabled for non-staff:
```python
# in mygame/typeclasses/rooms.py
from evennia import default_commands, CmdSet
class CmdBlocking(default_commands.MuxCommand):
# block commands give, get, inventory and drop
key = "give"
aliases = ["get", "inventory", "drop"]
def func(self):
self.caller.msg("You cannot do that in this room.")
class BlockingCmdSet(CmdSet):
key = "blocking_cmdset"
# default commands have prio 0
priority = 1
def at_cmdset_creation(self):
self.add(CmdBlocking())
class BlockingRoom(Room):
def at_object_creation(self):
self.cmdset.add(BlockingCmdSet, permanent=True)
# only share commands with players in the room that
# are NOT Builders or higher
self.locks.add("call:not perm(Builders)")
```
After `@reload`, make some `BlockingRooms` (or switch a room to it with `@typeclass`). Entering one
will now replace the given commands for anyone that does not have the `Builders` or higher
permission. Note that the 'call' lock is special in that even the superuser will be affected by it
(otherwise superusers would always see other player's cmdsets and a game would be unplayable for
superusers).
## Select Command based on a condition
**Q:** I want a command to be available only based on a condition. For example I want the "werewolf"
command to only be available on a full moon, from midnight to three in-game time.
**A:** This is easiest accomplished by putting the "werewolf" command on the Character as normal,
but to [lock](Locks) it with the "cmd" type lock. Only if the "cmd" lock type is passed will the
command be available.
```python
# in mygame/commands/command.py
from evennia import Command
class CmdWerewolf(Command):
key = "werewolf"
# lock full moon, between 00:00 (midnight) and 03:00.
locks = "cmd:is_full_moon(0, 3)"
def func(self):
# ...
```
Add this to the [default cmdset as usual](Adding-Command-Tutorial). The `is_full_moon` [lock
function](Locks#lock-functions) does not yet exist. We must create that:
```python
# in mygame/server/conf/lockfuncs.py
def is_full_moon(accessing_obj, accessed_obj,
starthour, endhour, *args, **kwargs):
# calculate if the moon is full here and
# if current game time is between starthour and endhour
# return True or False
```
After a `@reload`, the `werewolf` command will be available only at the right time, that is when the
`is_full_moon` lock function returns True.
## Automatically updating code when reloading
**Q:** I have a development server running Evennia. Can I have the server update its code-base when
I reload?
**A:** Having a development server that pulls updated code whenever you reload it can be really
useful if you have limited shell access to your server, or want to have it done automatically. If
you have your project in a configured Git environment, it's a matter of automatically calling `git
pull` when you reload. And that's pretty straightforward:
In `/server/conf/at_server_startstop.py`:
```python
import subprocess
# ... other hooks ...
def at_server_reload_stop():
"""
This is called only time the server stops before a reload.
"""
print("Pulling from the game repository...")
process = subprocess.call(["git", "pull"], shell=False)
```
That's all. We call `subprocess` to execute a shell command (that code works on Windows and Linux,
assuming the current directory is your game directory, which is probably the case when you run
Evennia). `call` waits for the process to complete, because otherwise, Evennia would reload on
partially-modified code, which would be problematic.
Now, when you enter `@reload` on your development server, the game repository is updated from the
configured remote repository (Github, for instance). Your development cycle could resemble
something like:
1. Coding on the local machine.
2. Testing modifications.
3. Committing once, twice or more (being sure the code is still working, unittests are pretty useful
here).
4. When the time comes, login to the development server and run `@reload`.
The reloading might take one or two additional seconds, since Evennia will pull from your remote Git
repository. But it will reload on it and you will have your modifications ready, without needing
connecting to your server using SSH or something similar.
## Changing all exit messages
**Q:** How can I change the default exit messages to something like "XXX leaves east" or "XXX
arrives from the west"?
**A:** the default exit messages are stored in two hooks, namely `announce_move_from` and
`announce_move_to`, on the `Character` typeclass (if what you want to change is the message other
characters will see when a character exits).
These two hooks provide some useful features to easily update the message to be displayed. They
take both the default message and mapping as argument. You can easily call the parent hook with
these information:
* The message represents the string of characters sent to characters in the room when a character
leaves.
* The mapping is a dictionary containing additional mappings (you will probably not need it for
simple customization).
It is advisable to look in the [code of both
hooks](https://github.com/evennia/evennia/tree/master/evennia/objects/objects.py), and read the
hooks' documentation. The explanations on how to quickly update the message are shown below:
```python
# In typeclasses/characters.py
"""
Characters
"""
from evennia import DefaultCharacter
class Character(DefaultCharacter):
"""
The default character class.
...
"""
def announce_move_from(self, destination, msg=None, mapping=None):
"""
Called if the move is to be announced. This is
called while we are still standing in the old
location.
Args:
destination (Object): The place we are going to.
msg (str, optional): a replacement message.
mapping (dict, optional): additional mapping objects.
You can override this method and call its parent with a
message to simply change the default message. In the string,
you can use the following as mappings (between braces):
object: the object which is moving.
exit: the exit from which the object is moving (if found).
origin: the location of the object before the move.
destination: the location of the object after moving.
"""
super().announce_move_from(destination, msg="{object} leaves {exit}.")
def announce_move_to(self, source_location, msg=None, mapping=None):
"""
Called after the move if the move was not quiet. At this point
we are standing in the new location.
Args:
source_location (Object): The place we came from
msg (str, optional): the replacement message if location.
mapping (dict, optional): additional mapping objects.
You can override this method and call its parent with a
message to simply change the default message. In the string,
you can use the following as mappings (between braces):
object: the object which is moving.
exit: the exit from which the object is moving (if found).
origin: the location of the object before the move.
destination: the location of the object after moving.
"""
super().announce_move_to(source_location, msg="{object} arrives from the {exit}.")
```
We override both hooks, but call the parent hook to display a different message. If you read the
provided docstrings, you will better understand why and how we use mappings (information between
braces). You can provide additional mappings as well, if you want to set a verb to move, for
instance, or other, extra information.
## Add parsing with the "to" delimiter
**Q:** How do I change commands to undestand say `give obj to target` as well as the default `give
obj = target`?
**A:** You can make change the default `MuxCommand` parent with your own class making a small change
in its `parse` method:
```python
# in mygame/commands/command.py
from evennia import default_cmds
class MuxCommand(default_cmds.MuxCommand):
def parse(self):
"""Implement an additional parsing of 'to'"""
super().parse()
if " to " in self.args:
self.lhs, self.rhs = self.args.split(" to ", 1)
```
Next you change the parent of the default commands in settings:
```python
COMMAND_DEFAULT_CLASS = "commands.command.MuxCommand"
```
Do a `@reload` and all default commands will now use your new tweaked parent class. A copy of the
MuxCommand class is also found commented-out in the `mygame/commands/command.py` file.
## Store last used session IP address
**Q:** If a user has already logged out of an Evennia account, their IP is no longer visible to
staff that wants to ban-by-ip (instead of the user) with `@ban/ip`?
**A:** One approach is to write the IP from the last session onto the "account" account object.
`typeclasses/accounts.py`
```python
def at_post_login(self, session=None, **kwargs):
super().at_post_login(session=session, **kwargs)
self.db.lastsite = self.sessions.all()[-1].address
```
Adding timestamp for login time and appending to a list to keep the last N login IP addresses and
timestamps is possible, also. Additionally, if you don't want the list to grow beyond a
`do_not_exceed` length, conditionally pop a value after you've added it, if the length has grown too
long.
**NOTE:** You'll need to add `import time` to generate the login timestamp.
```python
def at_post_login(self, session=None, **kwargs):
super().at_post_login(session=session, **kwargs)
do_not_exceed = 24 # Keep the last two dozen entries
session = self.sessions.all()[-1] # Most recent session
if not self.db.lastsite:
self.db.lastsite = []
self.db.lastsite.insert(0, (session.address, int(time.time())))
if len(self.db.lastsite) > do_not_exceed:
self.db.lastsite.pop()
```
This only stores the data. You may want to interface the `@ban` command or make a menu-driven viewer
for staff to browse the list and display how long ago the login occurred.
## Non-latin characters in EvTable
**Q:** When using e.g. Chinese characters in EvTable, some lines appear to be too wide, for example
```
+------+------+
| | |
| 测试 | 测试 |
| | |
+~~~~~~+~~~~~~+
```
**A:** The reason for this is because certain non-latin characters are *visually* much wider than
their len() suggests. There is little Evennia can (reliably) do about this. If you are using such
characters, you need to make sure to use a suitable mono-spaced font where are width are equal. You
can set this in your web client and need to recommend it for telnet-client users. See [this
discussion](https://github.com/evennia/evennia/issues/1522) where some suitable fonts are suggested.

View file

@ -0,0 +1,98 @@
# Command Cooldown
Some types of games want to limit how often a command can be run. If a
character casts the spell *Firestorm*, you might not want them to spam that
command over and over. Or in an advanced combat system, a massive swing may
offer a chance of lots of damage at the cost of not being able to re-do it for
a while. Such effects are called *cooldowns*.
This page exemplifies a very resource-efficient way to do cooldowns. A more
'active' way is to use asynchronous delays as in the [command duration
tutorial](Command-Duration#Blocking-Commands), the two might be useful to
combine if you want to echo some message to the user after the cooldown ends.
## Non-persistent cooldown
This little recipe will limit how often a particular command can be run. Since
Commands are class instances, and those are cached in memory, a command
instance will remember things you store on it. So just store the current time
of execution! Next time the command is run, it just needs to check if it has
that time stored, and compare it with the current time to see if a desired
delay has passed.
```python
import time
from evennia import default_cmds
class CmdSpellFirestorm(default_cmds.MuxCommand):
"""
Spell - Firestorm
Usage:
cast firestorm <target>
This will unleash a storm of flame. You can only release one
firestorm every five minutes (assuming you have the mana).
"""
key = "cast firestorm"
locks = "cmd:isFireMage()"
def func(self):
"Implement the spell"
# check cooldown (5 minute cooldown)
now = time.time()
if hasattr(self, "lastcast") and \
now - self.lastcast < 5 * 60:
message = "You cannot cast this spell again yet."
self.caller.msg(message)
return
#[the spell effect is implemented]
# if the spell was successfully cast, store the casting time
self.lastcast = now
```
We just check the `lastcast` flag, and update it if everything works out.
Simple and very effective since everything is just stored in memory. The
drawback of this simple scheme is that it's non-persistent. If you do
`@reload`, the cache is cleaned and all such ongoing cooldowns will be
forgotten. It is also limited only to this one command, other commands cannot
(easily) check for this value.
## Persistent cooldown
This is essentially the same mechanism as the simple one above, except we use
the database to store the information which means the cooldown will survive a
server reload/reboot. Since commands themselves have no representation in the
database, you need to use the caster for the storage.
```python
# inside the func() of CmdSpellFirestorm as above
# check cooldown (5 minute cooldown)
now = time.time()
lastcast = self.caller.db.firestorm_lastcast
if lastcast and now - lastcast < 5 * 60:
message = "You need to wait before casting this spell again."
self.caller.msg(message)
return
#[the spell effect is implemented]
# if the spell was successfully cast, store the casting time
self.caller.db.firestorm_lastcast = now
```
Since we are storing as an [Attribute](Attributes), we need to identify the
variable as `firestorm_lastcast` so we are sure we get the right one (we'll
likely have other skills with cooldowns after all). But this method of
using cooldowns also has the advantage of working *between* commands - you can
for example let all fire-related spells check the same cooldown to make sure
the casting of *Firestorm* blocks all fire-related spells for a while. Or, in
the case of taking that big swing with the sword, this could now block all
other types of attacks for a while before the warrior can recover.

View file

@ -0,0 +1,403 @@
# Command Duration
Before reading this tutorial, if you haven't done so already, you might want to
read [the documentation on commands](Commands) to get a basic understanding of
how commands work in Evennia.
In some types of games a command should not start and finish immediately.
Loading a crossbow might take a bit of time to do - time you don't have when
the enemy comes rushing at you. Crafting that armour will not be immediate
either. For some types of games the very act of moving or changing pose all
comes with a certain time associated with it.
## The simple way to pause commands with yield
Evennia allows a shortcut in syntax to create simple pauses in commands. This
syntax uses the `yield` keyword. The `yield` keyword is used in Python to
create generators, although you don't need to know what generators are to use
this syntax. A short example will probably make it clear:
```python
class CmdTest(Command):
"""
A test command just to test waiting.
Usage:
test
"""
key = "test"
locks = "cmd:all()"
def func(self):
self.msg("Before ten seconds...")
yield 10
self.msg("Afterwards.")
```
> Important: The `yield` functionality will *only* work in the `func` method of
> Commands. It only works because Evennia has especially
> catered for it in Commands. If you want the same functionality elsewhere you
> must use the [interactive decorator](Async-Process#The-@interactive-decorator).
The important line is the `yield 10`. It tells Evennia to "pause" the command
and to wait for 10 seconds to execute the rest. If you add this command and
run it, you'll see the first message, then, after a pause of ten seconds, the
next message. You can use `yield` several times in your command.
This syntax will not "freeze" all commands. While the command is "pausing",
you can execute other commands (or even call the same command again). And
other players aren't frozen either.
> Note: this will not save anything in the database. If you reload the game
> while a command is "paused", it will not resume after the server has
> reloaded.
## The more advanced way with utils.delay
The `yield` syntax is easy to read, easy to understand, easy to use. But it's not that flexible if
you want more advanced options. Learning to use alternatives might be much worth it in the end.
Below is a simple command example for adding a duration for a command to finish.
```python
from evennia import default_cmds, utils
class CmdEcho(default_cmds.MuxCommand):
"""
wait for an echo
Usage:
echo <string>
Calls and waits for an echo
"""
key = "echo"
locks = "cmd:all()"
def func(self):
"""
This is called at the initial shout.
"""
self.caller.msg("You shout '%s' and wait for an echo ..." % self.args)
# this waits non-blocking for 10 seconds, then calls self.echo
utils.delay(10, self.echo) # call echo after 10 seconds
def echo(self):
"Called after 10 seconds."
shout = self.args
string = "You hear an echo: %s ... %s ... %s"
string = string % (shout.upper(), shout.capitalize(), shout.lower())
self.caller.msg(string)
```
Import this new echo command into the default command set and reload the server. You will find that
it will take 10 seconds before you see your shout coming back. You will also find that this is a
*non-blocking* effect; you can issue other commands in the interim and the game will go on as usual.
The echo will come back to you in its own time.
### About utils.delay()
`utils.delay(timedelay, callback, persistent=False, *args, **kwargs)` is a useful function. It will
wait `timedelay` seconds, then call the `callback` function, optionally passing to it the arguments
provided to utils.delay by way of *args and/or **kwargs`.
> Note: The callback argument should be provided with a python path to the desired function, for
instance `my_object.my_function` instead of `my_object.my_function()`. Otherwise my_function would
get called and run immediately upon attempting to pass it to the delay function.
If you want to provide arguments for utils.delay to use, when calling your callback function, you
have to do it separatly, for instance using the utils.delay *args and/or **kwargs, as mentioned
above.
> If you are not familiar with the syntax `*args` and `**kwargs`, [see the Python documentation
here](https://docs.python.org/2/tutorial/controlflow.html#arbitrary-argument-lists).
Looking at it you might think that `utils.delay(10, callback)` in the code above is just an
alternative to some more familiar thing like `time.sleep(10)`. This is *not* the case. If you do
`time.sleep(10)` you will in fact freeze the *entire server* for ten seconds! The `utils.delay()`is
a thin wrapper around a Twisted
[Deferred](http://twistedmatrix.com/documents/11.0.0/core/howto/defer.html) that will delay
execution until 10 seconds have passed, but will do so asynchronously, without bothering anyone else
(not even you - you can continue to do stuff normally while it waits to continue).
The point to remember here is that the `delay()` call will not "pause" at that point when it is
called (the way `yield` does in the previous section). The lines after the `delay()` call will
actually execute *right away*. What you must do is to tell it which function to call *after the time
has passed* (its "callback"). This may sound strange at first, but it is normal practice in
asynchronous systems. You can also link such calls together as seen below:
```python
from evennia import default_cmds, utils
class CmdEcho(default_cmds.MuxCommand):
"""
waits for an echo
Usage:
echo <string>
Calls and waits for an echo
"""
key = "echo"
locks = "cmd:all()"
def func(self):
"This sets off a chain of delayed calls"
self.caller.msg("You shout '%s', waiting for an echo ..." % self.args)
# wait 2 seconds before calling self.echo1
utils.delay(2, self.echo1)
# callback chain, started above
def echo1(self):
"First echo"
self.caller.msg("... %s" % self.args.upper())
# wait 2 seconds for the next one
utils.delay(2, self.echo2)
def echo2(self):
"Second echo"
self.caller.msg("... %s" % self.args.capitalize())
# wait another 2 seconds
utils.delay(2, callback=self.echo3)
def echo3(self):
"Last echo"
self.caller.msg("... %s ..." % self.args.lower())
```
The above version will have the echoes arrive one after another, each separated by a two second
delay.
> echo Hello!
... HELLO!
... Hello!
... hello! ...
## Blocking commands
As mentioned, a great thing about the delay introduced by `yield` or `utils.delay()` is that it does
not block. It just goes on in the background and you are free to play normally in the interim. In
some cases this is not what you want however. Some commands should simply "block" other commands
while they are running. If you are in the process of crafting a helmet you shouldn't be able to also
start crafting a shield at the same time, or if you just did a huge power-swing with your weapon you
should not be able to do it again immediately.
The simplest way of implementing blocking is to use the technique covered in the [Command
Cooldown](Command-Cooldown) tutorial. In that tutorial we implemented cooldowns by having the
Command store the current time. Next time the Command was called, we compared the current time to
the stored time to determine if enough time had passed for a renewed use. This is a *very*
efficient, reliable and passive solution. The drawback is that there is nothing to tell the Player
when enough time has passed unless they keep trying.
Here is an example where we will use `utils.delay` to tell the player when the cooldown has passed:
```python
from evennia import utils, default_cmds
class CmdBigSwing(default_cmds.MuxCommand):
"""
swing your weapon in a big way
Usage:
swing <target>
Makes a mighty swing. Doing so will make you vulnerable
to counter-attacks before you can recover.
"""
key = "bigswing"
locks = "cmd:all()"
def func(self):
"Makes the swing"
if self.caller.ndb.off_balance:
# we are still off-balance.
self.caller.msg("You are off balance and need time to recover!")
return
# [attack/hit code goes here ...]
self.caller.msg("You swing big! You are off balance now.")
# set the off-balance flag
self.caller.ndb.off_balance = True
# wait 8 seconds before we can recover. During this time
# we won't be able to swing again due to the check at the top.
utils.delay(8, self.recover)
def recover(self):
"This will be called after 8 secs"
del self.caller.ndb.off_balance
self.caller.msg("You regain your balance.")
```
Note how, after the cooldown, the user will get a message telling them they are now ready for
another swing.
By storing the `off_balance` flag on the character (rather than on, say, the Command instance
itself) it can be accessed by other Commands too. Other attacks may also not work when you are off
balance. You could also have an enemy Command check your `off_balance` status to gain bonuses, to
take another example.
## Abortable commands
One can imagine that you will want to abort a long-running command before it has a time to finish.
If you are in the middle of crafting your armor you will probably want to stop doing that when a
monster enters your smithy.
You can implement this in the same way as you do the "blocking" command above, just in reverse.
Below is an example of a crafting command that can be aborted by starting a fight:
```python
from evennia import utils, default_cmds
class CmdCraftArmour(default_cmds.MuxCommand):
"""
Craft armour
Usage:
craft <name of armour>
This will craft a suit of armour, assuming you
have all the components and tools. Doing some
other action (such as attacking someone) will
abort the crafting process.
"""
key = "craft"
locks = "cmd:all()"
def func(self):
"starts crafting"
if self.caller.ndb.is_crafting:
self.caller.msg("You are already crafting!")
return
if self._is_fighting():
self.caller.msg("You can't start to craft "
"in the middle of a fight!")
return
# [Crafting code, checking of components, skills etc]
# Start crafting
self.caller.ndb.is_crafting = True
self.caller.msg("You start crafting ...")
utils.delay(60, self.step1)
def _is_fighting(self):
"checks if we are in a fight."
if self.caller.ndb.is_fighting:
del self.caller.ndb.is_crafting
return True
def step1(self):
"first step of armour construction"
if self._is_fighting():
return
self.msg("You create the first part of the armour.")
utils.delay(60, callback=self.step2)
def step2(self):
"second step of armour construction"
if self._is_fighting():
return
self.msg("You create the second part of the armour.")
utils.delay(60, step3)
def step3(self):
"last step of armour construction"
if self._is_fighting():
return
# [code for creating the armour object etc]
del self.caller.ndb.is_crafting
self.msg("You finalize your armour.")
# example of a command that aborts crafting
class CmdAttack(default_cmds.MuxCommand):
"""
attack someone
Usage:
attack <target>
Try to cause harm to someone. This will abort
eventual crafting you may be currently doing.
"""
key = "attack"
aliases = ["hit", "stab"]
locks = "cmd:all()"
def func(self):
"Implements the command"
self.caller.ndb.is_fighting = True
# [...]
```
The above code creates a delayed crafting command that will gradually create the armour. If the
`attack` command is issued during this process it will set a flag that causes the crafting to be
quietly canceled next time it tries to update.
## Persistent delays
In the latter examples above we used `.ndb` storage. This is fast and easy but it will reset all
cooldowns/blocks/crafting etc if you reload the server. If you don't want that you can replace
`.ndb` with `.db`. But even this won't help because the `yield` keyword is not persisent and nor is
the use of `delay` shown above. To resolve this you can use `delay` with the `persistent=True`
keyword. But wait! Making something persistent will add some extra complications, because now you
must make sure Evennia can properly store things to the database.
Here is the original echo-command reworked to function with persistence:
```python
from evennia import default_cmds, utils
# this is now in the outermost scope and takes two args!
def echo(caller, args):
"Called after 10 seconds."
shout = args
string = "You hear an echo: %s ... %s ... %s"
string = string % (shout.upper(), shout.capitalize(), shout.lower())
caller.msg(string)
class CmdEcho(default_cmds.MuxCommand):
"""
wait for an echo
Usage:
echo <string>
Calls and waits for an echo
"""
key = "echo"
locks = "cmd:all()"
def func(self):
"""
This is called at the initial shout.
"""
self.caller.msg("You shout '%s' and wait for an echo ..." % self.args)
# this waits non-blocking for 10 seconds, then calls echo(self.caller, self.args)
utils.delay(10, echo, self.caller, self.args, persistent=True) # changes!
```
Above you notice two changes:
- The callback (`echo`) was moved out of the class and became its own stand-alone function in the
outermost scope of the module. It also now takes `caller` and `args` as arguments (it doesn't have
access to them directly since this is now a stand-alone function).
- `utils.delay` specifies the `echo` function (not `self.echo` - it's no longer a method!) and sends
`self.caller` and `self.args` as arguments for it to use. We also set `persistent=True`.
The reason for this change is because Evennia needs to `pickle` the callback into storage and it
cannot do this correctly when the method sits on the command class. Now this behave the same as the
first version except if you reload (or even shut down) the server mid-delay it will still fire the
callback when the server comes back up (it will resume the countdown and ignore the downtime).

View file

@ -0,0 +1,129 @@
# Command Prompt
A *prompt* is quite common in MUDs. The prompt display useful details about your character that you
are likely to want to keep tabs on at all times, such as health, magical power etc. It might also
show things like in-game time, weather and so on. Many modern MUD clients (including Evennia's own
webclient) allows for identifying the prompt and have it appear in a correct location (usually just
above the input line). Usually it will remain like that until it is explicitly updated.
## Sending a prompt
A prompt is sent using the `prompt` keyword to the `msg()` method on objects. The prompt will be
sent without any line breaks.
```python
self.msg(prompt="HP: 5, MP: 2, SP: 8")
```
You can combine the sending of normal text with the sending (updating of the prompt):
```python
self.msg("This is a text", prompt="This is a prompt")
```
You can update the prompt on demand, this is normally done using [OOB](OOB)-tracking of the relevant
Attributes (like the character's health). You could also make sure that attacking commands update
the prompt when they cause a change in health, for example.
Here is a simple example of the prompt sent/updated from a command class:
```python
from evennia import Command
class CmdDiagnose(Command):
"""
see how hurt your are
Usage:
diagnose [target]
This will give an estimate of the target's health. Also
the target's prompt will be updated.
"""
key = "diagnose"
def func(self):
if not self.args:
target = self.caller
else:
target = self.search(self.args)
if not target:
return
# try to get health, mana and stamina
hp = target.db.hp
mp = target.db.mp
sp = target.db.sp
if None in (hp, mp, sp):
# Attributes not defined
self.caller.msg("Not a valid target!")
return
text = "You diagnose %s as having " \
"%i health, %i mana and %i stamina." \
% (hp, mp, sp)
prompt = "%i HP, %i MP, %i SP" % (hp, mp, sp)
self.caller.msg(text, prompt=prompt)
```
## A prompt sent with every command
The prompt sent as described above uses a standard telnet instruction (the Evennia web client gets a
special flag). Most MUD telnet clients will understand and allow users to catch this and keep the
prompt in place until it updates. So *in principle* you'd not need to update the prompt every
command.
However, with a varying user base it can be unclear which clients are used and which skill level the
users have. So sending a prompt with every command is a safe catch-all. You don't need to manually
go in and edit every command you have though. Instead you edit the base command class for your
custom commands (like `MuxCommand` in your `mygame/commands/command.py` folder) and overload the
`at_post_cmd()` hook. This hook is always called *after* the main `func()` method of the Command.
```python
from evennia import default_cmds
class MuxCommand(default_cmds.MuxCommand):
# ...
def at_post_cmd(self):
"called after self.func()."
caller = self.caller
prompt = "%i HP, %i MP, %i SP" % (caller.db.hp,
caller.db.mp,
caller.db.sp)
caller.msg(prompt=prompt)
```
### Modifying default commands
If you want to add something small like this to Evennia's default commands without modifying them
directly the easiest way is to just wrap those with a multiple inheritance to your own base class:
```python
# in (for example) mygame/commands/mycommands.py
from evennia import default_cmds
# our custom MuxCommand with at_post_cmd hook
from commands.command import MuxCommand
# overloading the look command
class CmdLook(default_cmds.CmdLook, MuxCommand):
pass
```
The result of this is that the hooks from your custom `MuxCommand` will be mixed into the default
`CmdLook` through multiple inheritance. Next you just add this to your default command set:
```python
# in mygame/commands/default_cmdsets.py
from evennia import default_cmds
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
def at_cmdset_creation(self):
# ...
self.add(mycommands.CmdLook())
```
This will automatically replace the default `look` command in your game with your own version.

View file

@ -0,0 +1,349 @@
# Coordinates
# Adding room coordinates in your game
This tutorial is moderately difficult in content. You might want to be familiar and at ease with
some Python concepts (like properties) and possibly Django concepts (like queries), although this
tutorial will try to walk you through the process and give enough explanations each time. If you
don't feel very confident with math, don't hesitate to pause, go to the example section, which shows
a tiny map, and try to walk around the code or read the explanation.
Evennia doesn't have a coordinate system by default. Rooms and other objects are linked by location
and content:
- An object can be in a location, that is, another object. Like an exit in a room.
- An object can access its content. A room can see what objects uses it as location (that would
include exits, rooms, characters and so on).
This system allows for a lot of flexibility and, fortunately, can be extended by other systems.
Here, I offer you a way to add coordinates to every room in a way most compliant with Evennia
design. This will also show you how to use coordinates, find rooms around a given point for
instance.
## Coordinates as tags
The first concept might be the most surprising at first glance: we will create coordinates as
[tags](Tags).
> Why not attributes, wouldn't that be easier?
It would. We could just do something like `room.db.x = 3`. The advantage of using tags is that it
will be easy and effective to search. Although this might not seem like a huge advantage right now,
with a database of thousands of rooms, it might make a difference, particularly if you have a lot of
things based on coordinates.
Rather than giving you a step-by-step process, I'll show you the code. Notice that we use
properties to easily access and update coordinates. This is a Pythonic approach. Here's our first
`Room` class, that you can modify in `typeclasses/rooms.py`:
```python
# in typeclasses/rooms.py
from evennia import DefaultRoom
class Room(DefaultRoom):
"""
Rooms are like any Object, except their location is None
(which is default). They also use basetype_setup() to
add locks so they cannot be puppeted or picked up.
(to change that, use at_object_creation instead)
See examples/object.py for a list of
properties and methods available on all Objects.
"""
@property
def x(self):
"""Return the X coordinate or None."""
x = self.tags.get(category="coordx")
return int(x) if isinstance(x, str) else None
@x.setter
def x(self, x):
"""Change the X coordinate."""
old = self.tags.get(category="coordx")
if old is not None:
self.tags.remove(old, category="coordx")
if x is not None:
self.tags.add(str(x), category="coordx")
@property
def y(self):
"""Return the Y coordinate or None."""
y = self.tags.get(category="coordy")
return int(y) if isinstance(y, str) else None
@y.setter
def y(self, y):
"""Change the Y coordinate."""
old = self.tags.get(category="coordy")
if old is not None:
self.tags.remove(old, category="coordy")
if y is not None:
self.tags.add(str(y), category="coordy")
@property
def z(self):
"""Return the Z coordinate or None."""
z = self.tags.get(category="coordz")
return int(z) if isinstance(z, str) else None
@z.setter
def z(self, z):
"""Change the Z coordinate."""
old = self.tags.get(category="coordz")
if old is not None:
self.tags.remove(old, category="coordz")
if z is not None:
self.tags.add(str(z), category="coordz")
```
If you aren't familiar with the concept of properties in Python, I encourage you to read a good
tutorial on the subject. [This article on Python properties](https://www.programiz.com/python-
programming/property)
is well-explained and should help you understand the idea.
Let's look at our properties for `x`. First of all is the read property.
```python
@property
def x(self):
"""Return the X coordinate or None."""
x = self.tags.get(category="coordx")
return int(x) if isinstance(x, str) else None
```
What it does is pretty simple:
1. It gets the tag of category `"coordx"`. It's the tag category where we store our X coordinate.
The `tags.get` method will return `None` if the tag can't be found.
2. We convert the value to an integer, if it's a `str`. Remember that tags can only contain `str`,
so we'll need to convert it.
> I thought tags couldn't contain values?
Well, technically, they can't: they're either here or not. But using tag categories, as we have
done, we get a tag, knowing only its category. That's the basic approach to coordinates in this
tutorial.
Now, let's look at the method that will be called when we wish to set `x` in our room:
```python
@x.setter
def x(self, x):
"""Change the X coordinate."""
old = self.tags.get(category="coordx")
if old is not None:
self.tags.remove(old, category="coordx")
if x is not None:
self.tags.add(str(x), category="coordx")
```
1. First, we remove the old X coordinate, if it exists. Otherwise, we'd end up with two tags in our
room with "coordx" as their category, which wouldn't do at all.
2. Then we add the new tag, giving it the proper category.
> Now what?
If you add this code and reload your game, once you're logged in with a character in a room as its
location, you can play around:
```
@py here.x
@py here.x = 0
@py here.y = 3
@py here.z = -2
@py here.z = None
```
The code might not be that easy to read, but you have to admit it's fairly easy to use.
## Some additional searches
Having coordinates is useful for several reasons:
1. It can help in shaping a truly logical world, in its geography, at least.
2. It can allow to look for specific rooms at given coordinates.
3. It can be good in order to quickly find the rooms around a location.
4. It can even be great in path-finding (finding the shortest path between two rooms).
So far, our coordinate system can help with 1., but not much else. Here are some methods that we
could add to the `Room` typeclass. These methods will just be search methods. Notice that they are
class methods, since we want to get rooms.
### Finding one room
First, a simple one: how to find a room at a given coordinate? Say, what is the room at X=0, Y=0,
Z=0?
```python
class Room(DefaultRoom):
# ...
@classmethod
def get_room_at(cls, x, y, z):
"""
Return the room at the given location or None if not found.
Args:
x (int): the X coord.
y (int): the Y coord.
z (int): the Z coord.
Return:
The room at this location (Room) or None if not found.
"""
rooms = cls.objects.filter(
db_tags__db_key=str(x), db_tags__db_category="coordx").filter(
db_tags__db_key=str(y), db_tags__db_category="coordy").filter(
db_tags__db_key=str(z), db_tags__db_category="coordz")
if rooms:
return rooms[0]
return None
```
This solution includes a bit of [Django
queries](https://docs.djangoproject.com/en/1.11/topics/db/queries/).
Basically, what we do is reach for the object manager and search for objects with the matching tags.
Again, don't spend too much time worrying about the mechanism, the method is quite easy to use:
```
Room.get_room_at(5, 2, -3)
```
Notice that this is a class method: you will call it from `Room` (the class), not an instance.
Though you still can:
@py here.get_room_at(3, 8, 0)
### Finding several rooms
Here's another useful method that allows us to look for rooms around a given coordinate. This is
more advanced search and doing some calculation, beware! Look at the following section if you're
lost.
```python
from math import sqrt
class Room(DefaultRoom):
# ...
@classmethod
def get_rooms_around(cls, x, y, z, distance):
"""
Return the list of rooms around the given coordinates.
This method returns a list of tuples (distance, room) that
can easily be browsed. This list is sorted by distance (the
closest room to the specified position is always at the top
of the list).
Args:
x (int): the X coord.
y (int): the Y coord.
z (int): the Z coord.
distance (int): the maximum distance to the specified position.
Returns:
A list of tuples containing the distance to the specified
position and the room at this distance. Several rooms
can be at equal distance from the position.
"""
# Performs a quick search to only get rooms in a square
x_r = list(reversed([str(x - i) for i in range(0, distance + 1)]))
x_r += [str(x + i) for i in range(1, distance + 1)]
y_r = list(reversed([str(y - i) for i in range(0, distance + 1)]))
y_r += [str(y + i) for i in range(1, distance + 1)]
z_r = list(reversed([str(z - i) for i in range(0, distance + 1)]))
z_r += [str(z + i) for i in range(1, distance + 1)]
wide = cls.objects.filter(
db_tags__db_key__in=x_r, db_tags__db_category="coordx").filter(
db_tags__db_key__in=y_r, db_tags__db_category="coordy").filter(
db_tags__db_key__in=z_r, db_tags__db_category="coordz")
# We now need to filter down this list to find out whether
# these rooms are really close enough, and at what distance
# In short: we change the square to a circle.
rooms = []
for room in wide:
x2 = int(room.tags.get(category="coordx"))
y2 = int(room.tags.get(category="coordy"))
z2 = int(room.tags.get(category="coordz"))
distance_to_room = sqrt(
(x2 - x) ** 2 + (y2 - y) ** 2 + (z2 - z) ** 2)
if distance_to_room <= distance:
rooms.append((distance_to_room, room))
# Finally sort the rooms by distance
rooms.sort(key=lambda tup: tup[0])
return rooms
```
This gets more serious.
1. We have specified coordinates as parameters. We determine a broad range using the distance.
That is, for each coordinate, we create a list of possible matches. See the example below.
2. We then search for the rooms within this broader range. It gives us a square
around our location. Some rooms are definitely outside the range. Again, see the example below
to follow the logic.
3. We filter down the list and sort it by distance from the specified coordinates.
Notice that we only search starting at step 2. Thus, the Django search doesn't look and cache all
objects, just a wider range than what would be really necessary. This method returns a circle of
coordinates around a specified point. Django looks for a square. What wouldn't fit in the circle
is removed at step 3, which is the only part that includes systematic calculation. This method is
optimized to be quick and efficient.
### An example
An example might help. Consider this very simple map (a textual description follows):
```
4 A B C D
3 E F G H
2 I J K L
1 M N O P
1 2 3 4
```
The X coordinates are given below. The Y coordinates are given on the left. This is a simple
square with 16 rooms: 4 on each line, 4 lines of them. All the rooms are identified by letters in
this example: the first line at the top has rooms A to D, the second E to H, the third I to L and
the fourth M to P. The bottom-left room, X=1 and Y=1, is M. The upper-right room X=4 and Y=4 is D.
So let's say we want to find all the neighbors, distance 1, from the room J. J is at X=2, Y=2.
So we use:
Room.get_rooms_around(x=2, y=2, z=0, distance=1)
# we'll assume a z coordinate of 0 for simplicity
1. First, this method gets all the rooms in a square around J. So it gets E F G, I J K, M N O. If
you want, draw the square around these coordinates to see what's happening.
2. Next, we browse over this list and check the real distance between J (X=2, Y=2) and the room.
The four corners of the square are not in this circle. For instance, the distance between J and M
is not 1. If you draw a circle of center J and radius 1, you'll notice that the four corners of our
square (E, G, M and O) are not in this circle. So we remove them.
3. We sort by distance from J.
So in the end we might obtain something like this:
```
[
(0, J), # yes, J is part of this circle after all, with a distance of 0
(1, F),
(1, I),
(1, K),
(1, N),
]
```
You can try with more examples if you want to see this in action.
### To conclude
You can definitely use this system to map other objects, not just rooms. You can easily remove the
`Z coordinate too, if you simply need X and Y.

View file

@ -0,0 +1,484 @@
# Customize channels
# Channel commands in Evennia
By default, Evennia's default channel commands are inspired by MUX. They all
begin with "c" followed by the action to perform (like "ccreate" or "cdesc").
If this default seems strange to you compared to other Evennia commands that
rely on switches, you might want to check this tutorial out.
This tutorial will also give you insight into the workings of the channel system.
So it may be useful even if you don't plan to make the exact changes shown here.
## What we will try to do
Our mission: change the default channel commands to have a different syntax.
This tutorial will do the following changes:
- Remove all the default commands to handle channels.
- Add a `+` and `-` command to join and leave a channel. So, assuming there is
a `public` channel on your game (most often the case), you could type `+public`
to join it and `-public` to leave it.
- Group the commands to manipulate channels under the channel name, after a
switch. For instance, instead of writing `cdesc public = My public channel`,
you would write `public/desc My public channel`.
> I listed removing the default Evennia commands as a first step in the
> process. Actually, we'll move it at the very bottom of the list, since we
> still want to use them, we might get it wrong and rely on Evennia commands
> for a while longer.
## A command to join, another to leave
We'll do the most simple task at first: create two commands, one to join a
channel, one to leave.
> Why not have them as switches? `public/join` and `public/leave` for instance?
For security reasons, I will hide channels to which the caller is not
connected. It means that if the caller is not connected to the "public"
channel, he won't be able to use the "public" command. This is somewhat
standard: if we create an administrator-only channel, we don't want players to
try (or even know) the channel command. Again, you could design it a different
way should you want to.
First create a file named `comms.py` in your `commands` package. It's
a rather logical place, since we'll write different commands to handle
communication.
Okay, let's add the first command to join a channel:
```python
# in commands/comms.py
from evennia.utils.search import search_channel
from commands.command import Command
class CmdConnect(Command):
"""
Connect to a channel.
"""
key = "+"
help_category = "Comms"
locks = "cmd:not pperm(channel_banned)"
auto_help = False
def func(self):
"""Implement the command"""
caller = self.caller
args = self.args
if not args:
self.msg("Which channel do you want to connect to?")
return
channelname = self.args
channel = search_channel(channelname)
if not channel:
return
# Check permissions
if not channel.access(caller, 'listen'):
self.msg("%s: You are not allowed to listen to this channel." % channel.key)
return
# If not connected to the channel, try to connect
if not channel.has_connection(caller):
if not channel.connect(caller):
self.msg("%s: You are not allowed to join this channel." % channel.key)
return
else:
self.msg("You now are connected to the %s channel. " % channel.key.lower())
else:
self.msg("You already are connected to the %s channel. " % channel.key.lower())
```
Okay, let's review this code, but if you're used to Evennia commands, it shouldn't be too strange:
1. We import `search_channel`. This is a little helper function that we will use to search for
channels by name and aliases, found in `evennia.utils.search`. It's just more convenient.
2. Our class `CmdConnect` contains the body of our command to join a channel.
3. Notice the key of this command is simply `"+"`. When you enter `+something` in the game, it will
try to find a command key `+something`. Failing that, it will look at other potential matches.
Evennia is smart enough to understand that when we type `+something`, `+` is the command key and
`something` is the command argument. This will, of course, fail if you have a command beginning by
`+` conflicting with the `CmdConnect` key.
4. We have altered some class attributes, like `auto_help`. If you want to know what they do and
why they have changed here, you can check the [documentation on commands](Commands).
5. In the command body, we begin by extracting the channel name. Remember that this name should be
in the command arguments (that is, in `self.args`). Following the same example, if a player enters
`+something`, `self.args` should contain `"something"`. We use `search_channel` to see if this
channel exists.
6. We then check the access level of the channel, to see if the caller can listen to it (not
necessarily use it to speak, mind you, just listen to others speak, as these are two different locks
on Evennia).
7. Finally, we connect the caller if he's not already connected to the channel. We use the
channel's `connect` method to do this. Pretty straightforward eh?
Now we'll add a command to leave a channel. It's almost the same, turned upside down:
```python
class CmdDisconnect(Command):
"""
Disconnect from a channel.
"""
key = "-"
help_category = "Comms"
locks = "cmd:not pperm(channel_banned)"
auto_help = False
def func(self):
"""Implement the command"""
caller = self.caller
args = self.args
if not args:
self.msg("Which channel do you want to disconnect from?")
return
channelname = self.args
channel = search_channel(channelname)
if not channel:
return
# If connected to the channel, try to disconnect
if channel.has_connection(caller):
if not channel.disconnect(caller):
self.msg("%s: You are not allowed to disconnect from this channel." % channel.key)
return
else:
self.msg("You stop listening to the %s channel. " % channel.key.lower())
else:
self.msg("You are not connected to the %s channel. " % channel.key.lower())
```
So far, you shouldn't have trouble following what this command does: it's
pretty much the same as the `CmdConnect` class in logic, though it accomplishes
the opposite. If you are connected to the channel `public` you could
disconnect from it using `-public`. Remember, you can use channel aliases too
(`+pub` and `-pub` will also work, assuming you have the alias `pub` on the
`public` channel).
It's time to test this code, and to do so, you will need to add these two
commands. Here is a good time to say it: by default, Evennia connects accounts
to channels. Some other games (usually with a higher multisession mode) will
want to connect characters instead of accounts, so that several characters in
the same account can be connected to various channels. You can definitely add
these commands either in the `AccountCmdSet` or `CharacterCmdSet`, the caller
will be different and the command will add or remove accounts of characters.
If you decide to install these commands on the `CharacterCmdSet`, you might
have to disconnect your superuser account (account #1) from the channel before
joining it with your characters, as Evennia tends to subscribe all accounts
automatically if you don't tell it otherwise.
So here's an example of how to add these commands into your `AccountCmdSet`.
Edit the file `commands/default_cmdsets.py` to change a few things:
```python
# In commands/default_cmdsets.py
from evennia import default_cmds
from commands.comms import CmdConnect, CmdDisconnect
# ... Skip to the AccountCmdSet class ...
class AccountCmdSet(default_cmds.AccountCmdSet):
"""
This is the cmdset available to the Account at all times. It is
combined with the `CharacterCmdSet` when the Account puppets a
Character. It holds game-account-specific commands, channel
commands, etc.
"""
key = "DefaultAccount"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
# Channel commands
self.add(CmdConnect())
self.add(CmdDisconnect())
```
Save, reload your game, and you should be able to use `+public` and `-public`
now!
## A generic channel command with switches
It's time to dive a little deeper into channel processing. What happens in
Evennia when a player enters `public Hello everybody!`?
Like exits, channels are a particular command that Evennia automatically
creates and attaches to individual channels. So when you enter `public
message` in your game, Evennia calls the `public` command.
> But I didn't add any public command...
Evennia will just create these commands automatically based on the existing
channels. The base command is the command we'll need to edit.
> Why edit it? It works just fine to talk.
Unfortunately, if we want to add switches to our channel names, we'll have to
edit this command. It's not too hard, however, we'll just start writing a
standard command with minor twitches.
### Some additional imports
You'll need to add a line of import in your `commands/comms.py` file. We'll
see why this import is important when diving in the command itself:
```python
from evennia.comms.models import ChannelDB
```
### The class layout
```python
# In commands/comms.py
class ChannelCommand(Command):
"""
{channelkey} channel
{channeldesc}
Usage:
{lower_channelkey} <message>
{lower_channelkey}/history [start]
{lower_channelkey}/me <message>
{lower_channelkey}/who
Switch:
history: View 20 previous messages, either from the end or
from <start> number of messages from the end.
me: Perform an emote on this channel.
who: View who is connected to this channel.
Example:
{lower_channelkey} Hello World!
{lower_channelkey}/history
{lower_channelkey}/history 30
{lower_channelkey}/me grins.
{lower_channelkey}/who
"""
# note that channeldesc and lower_channelkey will be filled
# automatically by ChannelHandler
# this flag is what identifies this cmd as a channel cmd
# and branches off to the system send-to-channel command
# (which is customizable by admin)
is_channel = True
key = "general"
help_category = "Channel Names"
obj = None
arg_regex = ""
```
There are some differences here compared to most common commands.
- There is something disconcerting in the class docstring. Some information is
between curly braces. This is a format-style which is only used for channel
commands. `{channelkey}` will be replaced by the actual channel key (like
public). `{channeldesc}` will be replaced by the channel description (like
"public channel"). And `{lower_channelkey}`.
- We have set `is_channel` to `True` in the command class variables. You
shouldn't worry too much about that: it just tells Evennia this is a special
command just for channels.
- `key` is a bit misleading because it will be replaced eventually. So we
could set it to virtually anything.
- The `obj` class variable is another one we won't detail right now.
- `arg_regex` is important: the default `arg_regex` in the channel command will
forbid to use switches (a slash just after the channel name is not allowed).
That's why we enforce it here, we allow any syntax.
> What will become of this command?
Well, when we'll be through with it, and once we'll add it as the default
command to handle channels, Evennia will create one per existing channel. For
instance, the public channel will receive one command of this class, with `key`
set to `public` and `aliases` set to the channel aliases (like `['pub']`).
> Can I see it work?
Not just yet, there's still a lot of code needed.
Okay we have the command structure but it's rather empty.
### The parse method
The `parse` method is called before `func` in every command. Its job is to
parse arguments and in our case, we will analyze switches here.
```python
# ...
def parse(self):
"""
Simple parser
"""
# channel-handler sends channame:msg here.
channelname, msg = self.args.split(":", 1)
self.switch = None
if msg.startswith("/"):
try:
switch, msg = msg[1:].split(" ", 1)
except ValueError:
switch = msg[1:]
msg = ""
self.switch = switch.lower().strip()
self.args = (channelname.strip(), msg.strip())
```
Reading the comments we see that the channel handler will send the command in a
strange way: a string with the channel name, a colon and the actual message
entered by the player. So if the player enters "public hello", the command
`args` will contain `"public:hello"`. You can look at the way the channel name
and message are parsed, this can be used in a lot of different commands.
Next we check if there's any switch, that is, if the message starts with a
slash. This would be the case if a player entered `public/me jumps up and
down`, for instance. If there is a switch, we save it in `self.switch`. We
alter `self.args` at the end to contain a tuple with two values: the channel
name, and the message (if a switch was used, notice that the switch will be
stored in `self.switch`, not in the second element of `self.args`).
### The command func
Finally, let's see the `func` method in the command class. It will have to
handle switches and also the raw message to send if no switch was used.
```python
# ...
def func(self):
"""
Create a new message and send it to channel, using
the already formatted input.
"""
channelkey, msg = self.args
caller = self.caller
channel = ChannelDB.objects.get_channel(channelkey)
# Check that the channel exists
if not channel:
self.msg(_("Channel '%s' not found.") % channelkey)
return
# Check that the caller is connected
if not channel.has_connection(caller):
string = "You are not connected to channel '%s'."
self.msg(string % channelkey)
return
# Check that the caller has send access
if not channel.access(caller, 'send'):
string = "You are not permitted to send to channel '%s'."
self.msg(string % channelkey)
return
# Handle the various switches
if self.switch == "me":
if not msg:
self.msg("What do you want to do on this channel?")
else:
msg = "{} {}".format(caller.key, msg)
channel.msg(msg, online=True)
elif self.switch:
self.msg("{}: Invalid switch {}.".format(channel.key, self.switch))
elif not msg:
self.msg("Say what?")
else:
if caller in channel.mutelist:
self.msg("You currently have %s muted." % channel)
return
channel.msg(msg, senders=self.caller, online=True)
```
- First of all, we try to get the channel object from the channel name we have
in the `self.args` tuple. We use `ChannelDB.objects.get_channel` this time
because we know the channel name isn't an alias (that was part of the deal,
`channelname` in the `parse` method contains a command key).
- We check that the channel does exist.
- We then check that the caller is connected to the channel. Remember, if the
caller isn't connected, we shouldn't allow him to use this command (that
includes the switches on channels).
- We then check that the caller has access to the channel's `send` lock. This
time, we make sure the caller can send messages to the channel, no matter what
operation he's trying to perform.
- Finally we handle switches. We try only one switch: `me`. This switch would
be used if a player entered `public/me jumps up and down` (to do a channel
emote).
- We handle the case where the switch is unknown and where there's no switch
(the player simply wants to talk on this channel).
The good news: The code is not too complicated by itself. The bad news is that
this is just an abridged version of the code. If you want to handle all the
switches mentioned in the command help, you will have more code to write. This
is left as an exercise.
### End of class
It's almost done, but we need to add a method in this command class that isn't
often used. I won't detail it's usage too much, just know that Evennia will use
it and will get angry if you don't add it. So at the end of your class, just
add:
```python
# ...
def get_extra_info(self, caller, **kwargs):
"""
Let users know that this command is for communicating on a channel.
Args:
caller (TypedObject): A Character or Account who has entered an ambiguous command.
Returns:
A string with identifying information to disambiguate the object, conventionally with a
preceding space.
"""
return " (channel)"
```
### Adding this channel command
Contrary to most Evennia commands, we won't add our `ChannelCommand` to a
`CmdSet`. Instead we need to tell Evennia that it should use the command we
just created instead of its default channel-command.
In your `server/conf/settings.py` file, add a new setting:
```python
# Channel options
CHANNEL_COMMAND_CLASS = "commands.comms.ChannelCommand"
```
Then you can reload your game. Try to type `public hello` and `public/me jumps
up and down`. Don't forget to enter `help public` to see if your command has
truly been added.
## Conclusion and full code
That was some adventure! And there's still things to do! But hopefully, this
tutorial will have helped you in designing your own channel system. Here are a
few things to do:
- Add more switches to handle various actions, like changing the description of
a channel for instance, or listing the connected participants.
- Remove the default Evennia commands to handle channels.
- Alter the behavior of the channel system so it better aligns with what you
want to do.
As a special bonus, you can find a full, working example of a communication
system similar to the one I've shown you: this is a working example, it
integrates all switches and does ever some extra checking, but it's also very
close from the code I've provided here. Notice, however, that this resource is
external to Evennia and not maintained by anyone but the original author of
this article.
[Read the full example on Github](https://github.com/vincent-
lg/avenew/blob/master/commands/comms.py)

View file

@ -0,0 +1,122 @@
# Default Exit Errors
Evennia allows for exits to have any name. The command "kitchen" is a valid exit name as well as
"jump out the window" or "north". An exit actually consists of two parts: an [Exit Object](Objects)
and an [Exit Command](Commands) stored on said exit object. The command has the same key and aliases
as the object, which is why you can see the exit in the room and just write its name to traverse it.
If you try to enter the name of a non-existing exit, it is thus the same as trying a non-exising
command; Evennia doesn't care about the difference:
> jump out the window
Command 'jump out the window' is not available. Type "help" for help.
Many games don't need this type of freedom however. They define only the cardinal directions as
valid exit names (Evennia's `@tunnel` command also offers this functionality). In this case, the
error starts to look less logical:
> west
Command 'west' is not available. Maybe you meant "@set" or "@reset"?
Since we for our particular game *know* that west is an exit direction, it would be better if the
error message just told us that we couldn't go there.
## Adding default error commands
To solve this you need to be aware of how to [write and add new commands](Adding-Command-Tutorial).
What you need to do is to create new commands for all directions you want to support in your game.
In this example all we'll do is echo an error message, but you could certainly consider more
advanced uses. You add these commands to the default command set. Here is an example of such a set
of commands:
```python
# for example in a file mygame/commands/movecommands.py
from evennia import default_cmds
class CmdExitError(default_cmds.MuxCommand):
"Parent class for all exit-errors."
locks = "cmd:all()"
arg_regex = r"\s|$"
auto_help = False
def func(self):
"returns the error"
self.caller.msg("You cannot move %s." % self.key)
class CmdExitErrorNorth(CmdExitError):
key = "north"
aliases = ["n"]
class CmdExitErrorEast(CmdExitError):
key = "east"
aliases = ["e"]
class CmdExitErrorSouth(CmdExitError):
key = "south"
aliases = ["s"]
class CmdExitErrorWest(CmdExitError):
key = "west"
aliases = ["w"]
```
Make sure to add the directional commands (not their parent) to the `CharacterCmdSet` class in
`mygame/commands/default_cmdsets.py`:
```python
# in mygame/commands/default_cmdsets.py
from commands import movecommands
# [...]
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# [...]
def at_cmdset_creation(self):
# [...]
self.add(movecommands.CmdExitErrorNorth())
self.add(movecommands.CmdExitErrorEast())
self.add(movecommands.CmdExitErrorSouth())
self.add(movecommands.CmdExitErrorWest())
```
After a `@reload` these commands (assuming you don't get any errors - check your log) will be
loaded. What happens henceforth is that if you are in a room with an Exitobject (let's say it's
"north"), the proper Exit-command will overload your error command (also named "north"). But if you
enter an direction without having a matching exit for it, you will fallback to your default error
commands:
> east
You cannot move east.
Further expansions by the exit system (including manipulating the way the Exit command itself is
created) can be done by modifying the [Exit typeclass](Typeclasses) directly.
## Additional Comments
So why didn't we create a single error command above? Something like this:
```python
class CmdExitError(default_cmds.MuxCommand):
"Handles all exit-errors."
key = "error_cmd"
aliases = ["north", "n",
"east", "e",
"south", "s",
"west", "w"]
#[...]
```
The anwer is that this would *not* work and understanding why is important in order to not be
confused when working with commands and command sets.
The reason it doesn't work is because Evennia's [command system](Commands) compares commands *both*
by `key` and by `aliases`. If *either* of those match, the two commands are considered *identical*
as far as cmdset merging system is concerned.
So the above example would work fine as long as there were no Exits at all in the room. But what
happens when we enter a room with an exit "north"? The Exit's cmdset is merged onto the default one,
and since there is an alias match, the system determines our `CmdExitError` to be identical. It is
thus overloaded by the Exit command (which also correctly defaults to a higher priority). The result
is that you can go through the north exit normally but none of the error messages for the other
directions are available since the single error command was completely overloaded by the single
matching "north" exit-command.

View file

@ -0,0 +1,199 @@
# Evennia for Diku Users
Evennia represents a learning curve for those who used to code on
[Diku](https://en.wikipedia.org/wiki/DikuMUD) type MUDs. While coding in Python is easy if you
already know C, the main effort is to get rid of old C programming habits. Trying to code Python the
way you code C will not only look ugly, it will lead to less optimal and harder to maintain code.
Reading Evennia example code is a good way to get a feel for how different problems are approached
in Python.
Overall, Python offers an extensive library of resources, safe memory management and excellent
handling of errors. While Python code does not run as fast as raw C code does, the difference is not
all that important for a text-based game. The main advantage of Python is an extremely fast
development cycle with and easy ways to create game systems that would take many times more code and
be much harder to make stable and maintainable in C.
### Core Differences
- As mentioned, the main difference between Evennia and a Diku-derived codebase is that Evennia is
written purely in Python. Since Python is an interpreted language there is no compile stage. It is
modified and extended by the server loading Python modules at run-time. It also runs on all computer
platforms Python runs on (which is basically everywhere).
- Vanilla Diku type engines save their data in custom *flat file* type storage solutions. By
contrast, Evennia stores all game data in one of several supported SQL databases. Whereas flat files
have the advantage of being easier to implement, they (normally) lack many expected safety features
and ways to effectively extract subsets of the stored data. For example, if the server loses power
while writing to a flatfile it may become corrupt and the data lost. A proper database solution is
not susceptible to this - at no point is the data in a state where it cannot be recovered. Databases
are also highly optimized for querying large data sets efficiently.
### Some Familiar Things
Diku expresses the character object referenced normally by:
`struct char ch*` then all character-related fields can be accessed by `ch->`. In Evennia, one must
pay attention to what object you are using, and when you are accessing another through back-
handling, that you are accessing the right object. In Diku C, accessing character object is normally
done by:
```c
/* creating pointer of both character and room struct */
void(struct char ch*, struct room room*){
int dam;
if (ROOM_FLAGGED(room, ROOM_LAVA)){
dam = 100
ch->damage_taken = dam
};
};
```
As an example for creating Commands in Evennia via the `from evennia import Command` the character
object that calls the command is denoted by a class property as `self.caller`. In this example
`self.caller` is essentially the 'object' that has called the Command, but most of the time it is an
Account object. For a more familiar Diku feel, create a variable that becomes the account object as:
```python
#mygame/commands/command.py
from evennia import Command
class CmdMyCmd(Command):
"""
This is a Command Evennia Object
"""
[...]
def func(self):
ch = self.caller
# then you can access the account object directly by using the familiar ch.
ch.msg("...")
account_name = ch.name
race = ch.db.race
```
As mentioned above, care must be taken what specific object you are working with. If focused on a
room object and you need to access the account object:
```python
#mygame/typeclasses/room.py
from evennia import DefaultRoom
class MyRoom(DefaultRoom):
[...]
def is_account_object(self, object):
# a test to see if object is an account
[...]
def myMethod(self):
#self.caller would not make any sense, since self refers to the
# object of 'DefaultRoom', you must find the character obj first:
for ch in self.contents:
if self.is_account_object(ch):
# now you can access the account object with ch:
account_name = ch.name
race = ch.db.race
```
## Emulating Evennia to Look and Feel Like A Diku/ROM
To emulate a Diku Mud on Evennia some work has to be done before hand. If there is anything that all
coders and builders remember from Diku/Rom days is the presence of VNUMs. Essentially all data was
saved in flat files and indexed by VNUMs for easy access. Evennia has the ability to emulate VNUMS
to the extent of categorising rooms/mobs/objs/trigger/zones[...] into vnum ranges.
Evennia has objects that are called Scripts. As defined, they are the 'out of game' instances that
exist within the mud, but never directly interacted with. Scripts can be used for timers, mob AI,
and even a stand alone databases.
Because of their wonderful structure all mob, room, zone, triggers, etc.. data can be saved in
independently created global scripts.
Here is a sample mob file from a Diku Derived flat file.
```text
#0
mob0~
mob0~
mob0
~
Mob0
~
10 0 0 0 0 0 0 0 0 E
1 20 9 0d0+10 1d2+0
10 100
8 8 0
E
#1
Puff dragon fractal~
Puff~
Puff the Fractal Dragon is here, contemplating a higher reality.
~
Is that some type of differential curve involving some strange, and unknown
calculus that she seems to be made out of?
~
516106 0 0 0 2128 0 0 0 1000 E
34 9 -10 6d6+340 5d5+5
340 115600
8 8 2
BareHandAttack: 12
E
T 95
```
Each line represents something that the MUD reads in and does something with it. This isn't easy to
read, but let's see if we can emulate this as a dictionary to be stored on a database script created
in Evennia.
First, let's create a global script that does absolutely nothing and isn't attached to anything. You
can either create this directly in-game with the @py command or create it in another file to do some
checks and balances if for whatever reason the script needs to be created again. Progmatically it
can be done like so:
```python
from evennia import create_script
mob_db = create_script("typeclasses.scripts.DefaultScript", key="mobdb",
persistent=True, obj=None)
mob_db.db.vnums = {}
```
Just by creating a simple script object and assigning it a 'vnums' attribute as a type dictionary.
Next we have to create the mob layout..
```python
# vnum : mob_data
mob_vnum_1 = {
'key' : 'puff',
'sdesc' : 'puff the fractal dragon',
'ldesc' : 'Puff the Fractal Dragon is here, ' \
'contemplating a higher reality.',
'ddesc' : ' Is that some type of differential curve ' \
'involving some strange, and unknown calculus ' \
'that she seems to be made out of?',
[...]
}
# Then saving it to the data, assuming you have the script obj stored in a variable.
mob_db.db.vnums[1] = mob_vnum_1
```
This is a very 'caveman' example, but it gets the idea across. You can use the keys in the
`mob_db.vnums` to act as the mob vnum while the rest contains the data..
Much simpler to read and edit. If you plan on taking this route, you must keep in mind that by
default evennia 'looks' at different properties when using the `look` command for instance. If you
create an instance of this mob and make its `self.key = 1`, by default evennia will say
`Here is : 1`
You must restructure all default commands so that the mud looks at different properties defined on
your mob.

View file

@ -0,0 +1,222 @@
# Evennia for MUSH Users
*This page is adopted from an article originally posted for the MUSH community [here on
musoapbox.net](http://musoapbox.net/topic/1150/evennia-for-mushers).*
[MUSH](https://en.wikipedia.org/wiki/MUSH)es are text multiplayer games traditionally used for
heavily roleplay-focused game styles. They are often (but not always) utilizing game masters and
human oversight over code automation. MUSHes are traditionally built on the TinyMUSH-family of game
servers, like PennMUSH, TinyMUSH, TinyMUX and RhostMUSH. Also their siblings
[MUCK](https://en.wikipedia.org/wiki/TinyMUCK) and [MOO](https://en.wikipedia.org/wiki/MOO) are
often mentioned together with MUSH since they all inherit from the same
[TinyMUD](https://en.wikipedia.org/wiki/MUD_trees#TinyMUD_family_tree) base. A major feature is the
ability to modify and program the game world from inside the game by using a custom scripting
language. We will refer to this online scripting as *softcode* here.
Evennia works quite differently from a MUSH both in its overall design and under the hood. The same
things are achievable, just in a different way. Here are some fundamental differences to keep in
mind if you are coming from the MUSH world.
## Developers vs Players
In MUSH, users tend to code and expand all aspects of the game from inside it using softcode. A MUSH
can thus be said to be managed solely by *Players* with different levels of access. Evennia on the
other hand, differentiates between the role of the *Player* and the *Developer*.
- An Evennia *Developer* works in Python from *outside* the game, in what MUSH would consider
“hardcode”. Developers implement larger-scale code changes and can fundamentally change how the game
works. They then load their changes into the running Evennia server. Such changes will usually not
drop any connected players.
- An Evennia *Player* operates from *inside* the game. Some staff-level players are likely to double
as developers. Depending on access level, players can modify and expand the game's world by digging
new rooms, creating new objects, alias commands, customize their experience and so on. Trusted staff
may get access to Python via the `@py` command, but this would be a security risk for normal Players
to use. So the *Player* usually operates by making use of the tools prepared for them by the
*Developer* - tools that can be as rigid or flexible as the developer desires.
## Collaborating on a game - Python vs Softcode
For a *Player*, collaborating on a game need not be too different between MUSH and Evennia. The
building and description of the game world can still happen mostly in-game using build commands,
using text tags and [inline functions](TextTags#inline-functions) to prettify and customize the
experience. Evennia offers external ways to build a world but those are optional. There is also
nothing *in principle* stopping a Developer from offering a softcode-like language to Players if
that is deemed necessary.
For *Developers* of the game, the difference is larger: Code is mainly written outside the game in
Python modules rather than in-game on the command line. Python is a very popular and well-supported
language with tons of documentation and help to be found. The Python standard library is also a
great help for not having to reinvent the wheel. But that said, while Python is considered one of
the easier languages to learn and use it is undoubtedly very different from MUSH softcode.
While softcode allows collaboration in-game, Evennia's external coding instead opens up the
possibility for collaboration using professional version control tools and bug tracking using
websites like github (or bitbucket for a free private repo). Source code can be written in proper
text editors and IDEs with refactoring, syntax highlighting and all other conveniences. In short,
collaborative development of an Evennia game is done in the same way most professional collaborative
development is done in the world, meaning all the best tools can be used.
## `@parent` vs `@typeclass` and `@spawn`
Inheritance works differently in Python than in softcode. Evennia has no concept of a "master
object" that other objects inherit from. There is in fact no reason at all to introduce "virtual
objects" in the game world - code and data are kept separate from one another.
In Python (which is an [object oriented](https://en.wikipedia.org/wiki/Object-oriented_programming)
language) one instead creates *classes* - these are like blueprints from which you spawn any number
of *object instances*. Evennia also adds the extra feature that every instance is persistent in the
database (this means no SQL is ever needed). To take one example, a unique character in Evennia is
an instances of the class `Character`.
One parallel to MUSH's `@parent` command may be Evennia's `@typeclass` command, which changes which
class an already existing object is an instance of. This way you can literally turn a `Character`
into a `Flowerpot` on the spot.
if you are new to object oriented design it's important to note that all object instances of a class
does *not* have to be identical. If they did, all Characters would be named the same. Evennia allows
to customize individual objects in many different ways. One way is through *Attributes*, which are
database-bound properties that can be linked to any object. For example, you could have an `Orc`
class that defines all the stuff an Orc should be able to do (probably in turn inheriting from some
`Monster` class shared by all monsters). Setting different Attributes on different instances
(different strength, equipment, looks etc) would make each Orc unique despite all sharing the same
class.
The `@spawn` command allows one to conveniently choose between different "sets" of Attributes to
put on each new Orc (like the "warrior" set or "shaman" set) . Such sets can even inherit one
another which is again somewhat remniscent at least of the *effect* of `@parent` and the object-
based inheritance of MUSH.
There are other differences for sure, but that should give some feel for things. Enough with the
theory. Let's get down to more practical matters next. To install, see the [Getting Started
instructions](Getting-Started).
## A first step making things more familiar
We will here give two examples of customizing Evennia to be more familiar to a MUSH *Player*.
### Activating a multi-descer
By default Evennias `desc` command updates your description and thats it. There is a more feature-
rich optional “multi-descer” in `evennia/contrib/multidesc.py` though. This alternative allows for
managing and combining a multitude of keyed descriptions.
To activate the multi-descer, `cd` to your game folder and into the `commands` sub-folder. There
youll find the file `default_cmdsets.py`. In Python lingo all `*.py` files are called *modules*.
Open the module in a text editor. We wont go into Evennia in-game *Commands* and *Command sets*
further here, but suffice to say Evennia allows you to change which commands (or versions of
commands) are available to the player from moment to moment depending on circumstance.
Add two new lines to the module as seen below:
```python
# the file mygame/commands/default_cmdsets.py
# [...]
from evennia.contrib import multidescer # <- added now
class CharacterCmdSet(default_cmds.CharacterCmdSet):
"""
The CharacterCmdSet contains general in-game commands like look,
get etc available on in-game Character objects. It is merged with
the AccountCmdSet when an Account puppets a Character.
"""
key = "DefaultCharacter"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones.
#
self.add(multidescer.CmdMultiDesc()) # <- added now
# [...]
```
Note that Python cares about indentation, so make sure to indent with the same number of spaces as
shown above!
So what happens above? We [import the
module](http://www.linuxtopia.org/online_books/programming_books/python_programming/python_ch28s03.html)
`evennia/contrib/multidescer.py` at the top. Once imported we can access stuff inside that module
using full stop (`.`). The multidescer is defined as a class `CmdMultiDesc` (we could find this out
by opening said module in a text editor). At the bottom we create a new instance of this class and
add it to the `CharacterCmdSet` class. For the sake of this tutorial we only need to know that
`CharacterCmdSet` contains all commands that should be be available to the `Character` by default.
This whole thing will be triggered when the command set is first created, which happens on server
start. So we need to reload Evennia with `@reload` - no one will be disconnected by doing this. If
all went well you should now be able to use `desc` (or `+desc`) and find that you have more
possibilities:
```text
> help +desc # get help on the command
> +desc eyes = His eyes are blue.
> +desc basic = A big guy.
> +desc/set basic + + eyes # we add an extra space between
> look me
A big guy. His eyes are blue.
```
If there are errors, a *traceback* will show in the server log - several lines of text showing
where the error occurred. Find where the error is by locating the line number related to the
`default_cmdsets.py` file (it's the only one you've changed so far). Most likely you mis-spelled
something or missed the indentation. Fix it and either `@reload` again or run `evennia start` as
needed.
### Customizing the multidescer syntax
As seen above the multidescer uses syntax like this (where `|/` are Evennia's tags for line breaks)
:
```text
> +desc/set basic + |/|/ + cape + footwear + |/|/ + attitude
```
This use of `+ ` was prescribed by the *Developer* that coded this `+desc` command. What if the
*Player* doesnt like this syntax though? Do players need to pester the dev to change it? Not
necessarily. While Evennia does not allow the player to build their own multi-descer on the command
line, it does allow for *re-mapping* the command syntax to one they prefer. This is done using the
`nick` command.
Heres a nick that changes how to input the command above:
```text
> nick setdesc $1 $2 $3 $4 = +desc/set $1 + |/|/ + $2 + $3 + |/|/ + $4
```
The string on the left will be matched against your input and if matching, it will be replaced with
the string on the right. The `$`-type tags will store space-separated arguments and put them into
the replacement. The nick allows [shell-like wildcards](http://www.linfo.org/wildcard.html), so you
can use `*`, `?`, `[...]`, `[!...]` etc to match parts of the input.
The same description as before can now be set as
```text
> setdesc basic cape footwear attitude
```
With the `nick` functionality players can mitigate a lot of syntax dislikes even without the
developer changing the underlying Python code.
## Next steps
If you are a *Developer* and are interested in making a more MUSH-like Evennia game, a good start is
to look into the Evennia [Tutorial for a first MUSH-like game](Tutorial-for-basic-MUSH-like-game).
That steps through building a simple little game from scratch and helps to acquaint you with the
various corners of Evennia. There is also the [Tutorial for running roleplaying sessions](Evennia-
for-roleplaying-sessions) that can be of interest.
An important aspect of making things more familiar for *Players* is adding new and tweaking existing
commands. How this is done is covered by the [Tutorial on adding new commands](Adding-Command-
Tutorial). You may also find it useful to shop through the `evennia/contrib/` folder. The [Tutorial
world](Tutorial-World-Introduction) is a small single-player quest you can try (its not very MUSH-
like but it does show many Evennia concepts in action). Beyond that there are [many more
tutorials](Tutorials) to try out. If you feel you want a more visual overview you can also look at
[Evennia in pictures](https://evennia.blogspot.se/2016/05/evennia-in-pictures.html).
… And of course, if you need further help you can always drop into the [Evennia
chatroom](http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb)
or post a question in our [forum/mailing list](https://groups.google.com/forum/#%21forum/evennia)!

View file

@ -0,0 +1,732 @@
# 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 [Getting
Started](Getting-Started) 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 [First Steps Coding](First-Steps-
Coding) 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 standard `@perm`
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](Building-Permissions#assigning-permissions) 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.
1. `Players` is the permission set on normal players. This is the default for anyone creating a new
account on the server.
2. `Helpers` are like `Players` 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.
3. `Builders` is not used in our case since the GM should be the only world-builder.
4. `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.
5. `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](Building-Permissions#the-super-user) 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:
1. 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.
2. 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](Accounts) 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:
```python
# 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 = "%s(GM)" % name
if lookaccount and \
(lookaccount.permissions.get("Developers") or lookaccount.db.is_gm):
# Developers/GMs see name(#dbref) or name(GM)(#dbref)
return "%s(#%s)" % (name, self.id)
else:
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](Attributes)) 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 the `Account` typeclass in
`mygame/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](Commands) 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:
```python
# 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("Could not find account '%s'" % self.args)
return
elif len(accountlist) > 1:
caller.msg("Multiple matches for '%s': %s" % (self.args, accountlist))
return
else:
account = accountlist[0]
if self.cmdstring == "gm":
# turn someone into a GM
if account.permissions.get("Admins"):
caller.msg("Account %s is already a GM." % account)
else:
account.permissions.add("Admins")
caller.msg("Account %s is now a GM." % account)
account.msg("You are now a GM (changed by %s)." % caller)
account.character.db.is_gm = True
else:
# @ungm was entered - revoke GM status from someone
if not account.permissions.get("Admins"):
caller.msg("Account %s is not a GM." % account)
else:
account.permissions.remove("Admins")
caller.msg("Account %s is no longer a GM." % account)
account.msg("You are no longer a GM (changed by %s)." % 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 in `self.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:
```python
# 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 due to the limitations of the wiki, no color is used in any of the examples. See [the
text tag documentation](TextTags) for how to add color to the tables and forms.
#### Making a sheet with EvTable
[EvTable](github:evennia.utils.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:
````python
# 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](github:evennia.utils.evtable#evtable__init__). 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
```python
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](github:evennia.utils.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:
````python
#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` and `TABLECHAR` 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 example `x2x`).
Now we will map content to this form.
````python
# 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})` where `FORM` specifies the form as a string in the same way as listed in the module
above. Note however that the very first line of the `FORM` string is ignored, so start with a `\n`.
We then map those to the cells of the form:
````python
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:
```python
# 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`.
```python
# 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:
err = "Allowed field names: %s" % (", ".join(ALLOWED_FIELDNAMES))
caller.msg(err)
return False
if fieldname in ALLOWED_ATTRS and not value.isdigit():
caller.msg("%s must receive a number." % fieldname)
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("%s was set to %s." % (fieldname, 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.
```python
# 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("%s can no longer edit their character sheet." % character.key)
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("%s can now edit their character sheet." % character.key)
if fieldname:
if fieldname == "name":
character.key = value
else:
character.db.chardata[fieldname] = value
character.update_charsheet()
caller.msg("You set %s's %s to %s." % (character.key, fieldname, 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](Building-Quickstart). 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](Communications#Channels) in-built and they are described fully in the
documentation. For brevity, here are the relevant commands for normal use:
* `@ccreate new_channel;alias;alias = short description` - Creates a new channel.
* `addcom channel` - join an existing channel. Use `addcom alias = channel` to add a new alias you
can use to talk to the channel, as many as desired.
* `delcom alias or channel` - remove an alias from a channel or, if the real channel name is given,
unsubscribe completely.
* `@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.

View file

@ -0,0 +1,218 @@
# Game Planning
So you have Evennia up and running. You have a great game idea in mind. Now it's time to start
cracking! But where to start? Here are some ideas for a workflow. Note that the suggestions on this
page are just that - suggestions. Also, they are primarily aimed at a lone hobby designer or a small
team developing a game in their free time. There is an article in the Imaginary Realities e-zine
which was written by the Evennia lead dev. It focuses more on you finding out your motivations for
making a game - you can [read the article here](http://journal.imaginary-
realities.com/volume-07/issue-03/where-do-i-begin/index.html).
Below are some minimal steps for getting the first version of a new game world going with players.
It's worth to at least make the attempt to do these steps in order even if you are itching to jump
ahead in the development cycle. On the other hand, you should also make sure to keep your work fun
for you, or motivation will falter. Making a full game is a lot of work as it is, you'll need all
your motivation to make it a reality.
Remember that *99.99999% of all great game ideas never lead to a game*. Especially not to an online
game that people can actually play and enjoy. So our first all overshadowing goal is to beat those
odds and get *something* out the door! Even if it's a scaled-down version of your dream game,
lacking many "must-have" features! It's better to get it out there and expand on it later than to
code in isolation forever until you burn out, lose interest or your hard drive crashes.
Like is common with online games, getting a game out the door does not mean you are going to be
"finished" with the game - most MUDs add features gradually over the course of years - it's often
part of the fun!
## Planning (step 1)
This is what you do before having coded a single line or built a single room. Many prospective game
developers are very good at *parts* of this process, namely in defining what their world is "about":
The theme, the world concept, cool monsters and so on. It is by all means very important to define
what is the unique appeal of your game. But it's unfortunately not enough to make your game a
reality. To do that you must also have an idea of how to actually map those great ideas onto
Evennia.
A good start is to begin by planning out the basic primitives of the game and what they need to be
able to do. Below are a far-from-complete list of examples (and for your first version you should
definitely try for a much shorter list):
### Systems
These are the behind-the-scenes features that exist in your game, often without being represented by
a specific in-game object.
- Should your game rules be enforced by coded systems or are you planning for human game masters to
run and arbitrate rules?
- What are the actual mechanical game rules? How do you decide if an action succeeds or fails? What
"rolls" does the game need to be able to do? Do you base your game off an existing system or make up
your own?
- Does the flow of time matter in your game - does night and day change? What about seasons? Maybe
your magic system is affected by the phase of the moon?
- Do you want changing, global weather? This might need to operate in tandem over a large number of
rooms.
- Do you want a game-wide economy or just a simple barter system? Or no formal economy at all?
- Should characters be able to send mail to each other in-game?
- Should players be able to post on Bulletin boards?
- What is the staff hierarchy in your game? What powers do you want your staff to have?
- What should a Builder be able to build and what commands do they need in order to do that?
- etc.
### Rooms
Consider the most basic room in your game.
- Is a simple description enough or should the description be able to change (such as with time, by
light conditions, weather or season)?
- Should the room have different statuses? Can it have smells, sounds? Can it be affected by
dramatic weather, fire or magical effects? If so, how would this affect things in the room? Or are
these things something admins/game masters should handle manually?
- Can objects be hidden in the room? Can a person hide in the room? How does the room display this?
- etc.
### Objects
Consider the most basic (non-player-controlled) object in your game.
- How numerous are your objects? Do you want large loot-lists or are objects just role playing props
created on demand?
- Does the game use money? If so, is each coin a separate object or do you just store a bank account
value?
- What about multiple identical objects? Do they form stacks and how are those stacks handled in
that case?
- Does an object have weight or volume (so you cannot carry an infinite amount of them)?
- Can objects be broken? If so, does it have a health value? Is burning it causing the same damage
as smashing it? Can it be repaired?
- Is a weapon a specific type of object or are you supposed to be able to fight with a chair too?
Can you fight with a flower or piece of paper as well?
- NPCs/mobs are also objects. Should they just stand around or should they have some sort of AI?
- Are NPCs/mobs differet entities? How is an Orc different from a Kobold, in code - are they the
same object with different names or completely different types of objects, with custom code?
- Should there be NPCs giving quests? If so, how would you track quest status and what happens when
multiple players try to do the same quest? Do you use instances or some other mechanism?
- etc.
### Characters
These are the objects controlled directly by Players.
- Can players have more than one Character active at a time or are they allowed to multi-play?
- How does a Player create their Character? A Character-creation screen? Answering questions?
Filling in a form?
- Do you want to use classes (like "Thief", "Warrior" etc) or some other system, like Skill-based?
- How do you implement different "classes" or "races"? Are they separate types of objects or do you
simply load different stats on a basic object depending on what the Player wants?
- If a Character can hide in a room, what skill will decide if they are detected?
- What skill allows a Character to wield a weapon and hit? Do they need a special skill to wield a
chair rather than a sword?
- Does a Character need a Strength attribute to tell how much they can carry or which objects they
can smash?
- What does the skill tree look like? Can a Character gain experience to improve? By killing
enemies? Solving quests? By roleplaying?
- etc.
A MUD's a lot more involved than you would think and these things hang together in a complex web. It
can easily become overwhelming and it's tempting to want *all* functionality right out of the door.
Try to identify the basic things that "make" your game and focus *only* on them for your first
release. Make a list. Keep future expansions in mind but limit yourself.
## Coding (step 2)
This is the actual work of creating the "game" part of your game. Many "game-designer" types tend to
gloss over this bit and jump directly to **World Building**. Vice versa, many "game-coder" types
tend to jump directly to this part without doing the **Planning** first. Neither way is good and
*will* lead to you having to redo all your hard work at least once, probably more.
Evennia's [Developer Central](Developer-Central) tries to help you with this bit of development. We
also have a slew of [Tutorials](Tutorials) with worked examples. Evennia tries hard to make this
part easier for you, but there is no way around the fact that if you want anything but a very basic
Talker-type game you *will* have to bite the bullet and code your game (or find a coder willing to
do it for you).
Even if you won't code anything yourself, as a designer you need to at least understand the basic
paradigms of Evennia, such as [Objects](Objects), [Commands](Commands) and [Scripts](Scripts) and
how they hang together. We recommend you go through the [Tutorial World](Tutorial-World-
Introduction) in detail (as well as glancing at its code) to get at least a feel for what is
involved behind the scenes. You could also look through the tutorial for [building a game from
scratch](Tutorial-for-basic-MUSH-like-game).
During Coding you look back at the things you wanted during the **Planning** phase and try to
implement them. Don't be shy to update your plans if you find things easier/harder than you thought.
The earlier you revise problems, the easier they will be to fix.
A good idea is to host your code online (publicly or privately) using version control. Not only will
this make it easy for multiple coders to collaborate (and have a bug-tracker etc), it also means
your work is backed up at all times. The [Version Control](Version-Control) tutorial has
instructions for setting up a sane developer environment with proper version control.
### "Tech Demo" Building
This is an integral part of your Coding. It might seem obvious to experienced coders, but it cannot
be emphasized enough that you should *test things on a small scale* before putting your untested
code into a large game-world. The earlier you test, the easier and cheaper it will be to fix bugs
and even rework things that didn't work out the way you thought they would. You might even have to
go back to the **Planning** phase if your ideas can't handle their meet with reality.
This means building singular in-game examples. Make one room and one object of each important type
and test so they work correctly in isolation. Then add more if they are supposed to interact with
each other in some way. Build a small series of rooms to test how mobs move around ... and so on. In
short, a test-bed for your growing code. It should be done gradually until you have a fully
functioning (if not guaranteed bug-free) miniature tech demo that shows *all* the features you want
in the first release of your game. There does not need to be any game play or even a theme to your
tests, this is only for you and your co-coders to see. The more testing you do on this small scale,
the less headaches you will have in the next phase.
## World Building (step 3)
Up until this point we've only had a few tech-demo objects in the database. This step is the act of
populating the database with a larger, thematic world. Too many would-be developers jump to this
stage too soon (skipping the **Coding** or even **Planning** stages). What if the rooms you build
now doesn't include all the nice weather messages the code grows to support? Or the way you store
data changes under the hood? Your building work would at best require some rework and at worst you
would have to redo the whole thing. And whereas Evennia's typeclass system does allow you to edit
the properties of existing objects, some hooks are only called at object creation ... Suffice to
say you are in for a *lot* of unnecessary work if you build stuff en masse without having the
underlying code systems in some reasonable shape first.
So before starting to build, the "game" bit (**Coding** + **Testing**) should be more or less
**complete**, *at least to the level of your initial release*.
Before starting to build, you should also plan ahead again. Make sure it is clear to yourself and
your eventual builders just which parts of the world you want for your initial release. Establish
for everyone which style, quality and level of detail you expect. Your goal should *not* be to
complete your entire world in one go. You want just enough to make the game's "feel" come across.
You want a minimal but functioning world where the intended game play can be tested and roughly
balanced. You can always add new areas later.
During building you get free and extensive testing of whatever custom build commands and systems you
have made at this point. Since Building often involves different people than those Coding, you also
get a chance to hear if some things are hard to understand or non-intuitive. Make sure to respond
to this feedback.
## Alpha Release
As mentioned, don't hold onto your world more than necessary. *Get it out there* with a huge *Alpha*
flag and let people try it! Call upon your alpha-players to try everything - they *will* find ways
to break your game in ways that you never could have imagined. In Alpha you might be best off to
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
feedback and bug reports (there *will* be bugs, there is no way around it). Follow the quick
instructions for [Online Setup](Online-Setup) to make your game visible online. If you hadn't
already, make sure to put up your game on the [Evennia game index](http://games.evennia.com/) so
people know it's in the works (actually, even pre-alpha games are allowed in the index so don't be
shy)!
## Beta Release/Perpetual Beta
Once things stabilize in Alpha you can move to *Beta* and let more people in. Many MUDs are in
[perpetual beta](http://en.wikipedia.org/wiki/Perpetual_beta), meaning they are never considered
"finished", but just repeat the cycle of Planning, Coding, Testing and Building over and over as new
features get implemented or Players come with suggestions. As the game designer it is now up to you
to gradually perfect your vision.
## Congratulate yourself!
You are worthy of a celebration since at this point you have joined the small, exclusive crowd who
have made their dream game a reality!

View file

@ -0,0 +1,302 @@
# Gametime Tutorial
A lot of games use a separate time system we refer to as *game time*. This runs in parallel to what
we usually think of as *real time*. The game time might run at a different speed, use different
names for its time units or might even use a completely custom calendar. You don't need to rely on a
game time system at all. But if you do, Evennia offers basic tools to handle these various
situations. This tutorial will walk you through these features.
### A game time with a standard calendar
Many games let their in-game time run faster or slower than real time, but still use our normal
real-world calendar. This is common both for games set in present day as well as for games in
historical or futuristic settings. Using a standard calendar has some advantages:
- Handling repetitive actions is much easier, since converting from the real time experience to the
in-game perceived one is easy.
- The intricacies of the real world calendar, with leap years and months of different length etc are
automatically handled by the system.
Evennia's game time features assume a standard calendar (see the relevant section below for a custom
calendar).
#### Setting up game time for a standard calendar
All is done through the settings. Here are the settings you should use if you want a game time with
a standard calendar:
```python
# in a file settings.py in mygame/server/conf
# The time factor dictates if the game world runs faster (timefactor>1)
# or slower (timefactor<1) than the real world.
TIME_FACTOR = 2.0
# The starting point of your game time (the epoch), in seconds.
# In Python a value of 0 means Jan 1 1970 (use negatives for earlier
# start date). This will affect the returns from the utils.gametime
# module.
TIME_GAME_EPOCH = None
```
By default, the game time runs twice as fast as the real time. You can set the time factor to be 1
(the game time would run exactly at the same speed than the real time) or lower (the game time will
be slower than the real time). Most games choose to have the game time spinning faster (you will
find some games that have a time factor of 60, meaning the game time runs sixty times as fast as the
real time, a minute in real time would be an hour in game time).
The epoch is a slightly more complex setting. It should contain a number of seconds that would
indicate the time your game started. As indicated, an epoch of 0 would mean January 1st, 1970. If
you want to set your time in the future, you just need to find the starting point in seconds. There
are several ways to do this in Python, this method will show you how to do it in local time:
```python
# We're looking for the number of seconds representing
# January 1st, 2020
from datetime import datetime
import time
start = datetime(2020, 1, 1)
time.mktime(start.timetuple())
```
This should return a huge number - the number of seconds since Jan 1 1970. Copy that directly into
your settings (editing `server/conf/settings.py`):
```python
# in a file settings.py in mygame/server/conf
TIME_GAME_EPOCH = 1577865600
```
Reload the game with `@reload`, and then use the `@time` command. You should see something like
this:
```
+----------------------------+-------------------------------------+
| Server time | |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Current uptime | 20 seconds |
| Total runtime | 1 day, 1 hour, 55 minutes |
| First start | 2017-02-12 15:47:50.565000 |
| Current time | 2017-02-13 17:43:10.760000 |
+----------------------------+-------------------------------------+
| In-Game time | Real time x 2 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Epoch (from settings) | 2020-01-01 00:00:00 |
| Total time passed: | 1 day, 17 hours, 34 minutes |
| Current time | 2020-01-02 17:34:55.430000 |
+----------------------------+-------------------------------------+
```
The line that is most relevant here is the game time epoch. You see it shown at 2020-01-01. From
this point forward, the game time keeps increasing. If you keep typing `@time`, you'll see the game
time updated correctly... and going (by default) twice as fast as the real time.
#### Time-related events
The `gametime` utility also has a way to schedule game-related events, taking into account your game
time, and assuming a standard calendar (see below for the same feature with a custom calendar). For
instance, it can be used to have a specific message every (in-game) day at 6:00 AM showing how the
sun rises.
The function `schedule()` should be used here. It will create a [script](Scripts) with some
additional features to make sure the script is always executed when the game time matches the given
parameters.
The `schedule` function takes the following arguments:
- The *callback*, a function to be called when time is up.
- The keyword `repeat` (`False` by default) to indicate whether this function should be called
repeatedly.
- Additional keyword arguments `sec`, `min`, `hour`, `day`, `month` and `year` to describe the time
to schedule. If the parameter isn't given, it assumes the current time value of this specific unit.
Here is a short example for making the sun rise every day:
```python
# in a file ingame_time.py in mygame/world/
from evennia.utils import gametime
from typeclasses.rooms import Room
def at_sunrise():
"""When the sun rises, display a message in every room."""
# Browse all rooms
for room in Room.objects.all():
room.msg_contents("The sun rises from the eastern horizon.")
def start_sunrise_event():
"""Schedule an sunrise event to happen every day at 6 AM."""
script = gametime.schedule(at_sunrise, repeat=True, hour=6, min=0, sec=0)
script.key = "at sunrise"
```
If you want to test this function, you can easily do something like:
```
@py from world import ingame_time; ingame_time.start_sunrise_event()
```
The script will be created silently. The `at_sunrise` function will now be called every in-game day
at 6 AM. You can use the `@scripts` command to see it. You could stop it using `@scripts/stop`. If
we hadn't set `repeat` the sun would only have risen once and then never again.
We used the `@py` command here: nothing prevents you from adding the system into your game code.
Remember to be careful not to add each event at startup, however, otherwise there will be a lot of
overlapping events scheduled when the sun rises.
The `schedule` function when using `repeat` set to `True` works with the higher, non-specified unit.
In our example, we have specified hour, minute and second. The higher unit we haven't specified is
day: `schedule` assumes we mean "run the callback every day at the specified time". Therefore, you
can have an event that runs every hour at HH:30, or every month on the 3rd day.
> A word of caution for repeated scripts on a monthly or yearly basis: due to the variations in the
real-life calendar you need to be careful when scheduling events for the end of the month or year.
For example, if you set a script to run every month on the 31st it will run in January but find no
such day in February, April etc. Similarly, leap years may change the number of days in the year.
### A game time with a custom calendar
Using a custom calendar to handle game time is sometimes needed if you want to place your game in a
fictional universe. For instance you may want to create the Shire calendar which Tolkien described
having 12 months, each which 30 days. That would give only 360 days per year (presumably hobbits
weren't really fond of the hassle of following the astronomical calendar). Another example would be
creating a planet in a different solar system with, say, days 29 hours long and months of only 18
days.
Evennia handles custom calendars through an optional *contrib* module, called `custom_gametime`.
Contrary to the normal `gametime` module described above it is not active by default.
#### Setting up the custom calendar
In our first example of the Shire calendar, used by hobbits in books by Tolkien, we don't really
need the notion of weeks... but we need the notion of months having 30 days, not 28.
The custom calendar is defined by adding the `TIME_UNITS` setting to your settings file. It's a
dictionary containing as keys the name of the units, and as value the number of seconds (the
smallest unit for us) in this unit. Its keys must be picked among the following: "sec", "min",
"hour", "day", "week", "month" and "year" but you don't have to include them all. Here is the
configuration for the Shire calendar:
```python
# in a file settings.py in mygame/server/conf
TIME_UNITS = {"sec": 1,
"min": 60,
"hour": 60 * 60,
"day": 60 * 60 * 24,
"month": 60 * 60 * 24 * 30,
"year": 60 * 60 * 24 * 30 * 12 }
```
We give each unit we want as keys. Values represent the number of seconds in that unit. Hour is
set to 60 * 60 (that is, 3600 seconds per hour). Notice that we don't specify the week unit in this
configuration: instead, we skip from days to months directly.
In order for this setting to work properly, remember all units have to be multiples of the previous
units. If you create "day", it needs to be multiple of hours, for instance.
So for our example, our settings may look like this:
```python
# in a file settings.py in mygame/server/conf
# Time factor
TIME_FACTOR = 4
# Game time epoch
TIME_GAME_EPOCH = 0
# Units
TIME_UNITS = {
"sec": 1,
"min": 60,
"hour": 60 * 60,
"day": 60 * 60 * 24,
"month": 60 * 60 * 24 * 30,
"year": 60 * 60 * 24 * 30 * 12,
}
```
Notice we have set a time epoch of 0. Using a custom calendar, we will come up with a nice display
of time on our own. In our case the game time starts at year 0, month 0, day 0, and at midnight.
Note that while we use "month", "week" etc in the settings, your game may not use those terms in-
game, instead referring to them as "cycles", "moons", "sand falls" etc. This is just a matter of you
displaying them differently. See next section.
#### A command to display the current game time
As pointed out earlier, the `@time` command is meant to be used with a standard calendar, not a
custom one. We can easily create a new command though. We'll call it `time`, as is often the case
on other MU*. Here's an example of how we could write it (for the example, you can create a file
`showtime.py` in your `commands` directory and paste this code in it):
```python
# in a file mygame/commands/gametime.py
from evennia.contrib import custom_gametime
from commands.command import Command
class CmdTime(Command):
"""
Display the time.
Syntax:
time
"""
key = "time"
locks = "cmd:all()"
def func(self):
"""Execute the time command."""
# Get the absolute game time
year, month, day, hour, min, sec = custom_gametime.custom_gametime(absolute=True)
string = "We are in year {year}, day {day}, month {month}."
string += "\nIt's {hour:02}:{min:02}:{sec:02}."
self.msg(string.format(year=year, month=month, day=day,
hour=hour, min=min, sec=sec))
```
Don't forget to add it in your CharacterCmdSet to see this command:
```python
# in mygame/commands/default_cmdset.py
from commands.gametime import CmdTime # <-- Add
# ...
class CharacterCmdSet(default_cmds.CharacterCmdSet):
"""
The `CharacterCmdSet` contains general in-game commands like `look`,
`get`, etc available on in-game Character objects. It is merged with
the `AccountCmdSet` when an Account puppets a Character.
"""
key = "DefaultCharacter"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
# ...
self.add(CmdTime()) # <- Add
```
Reload your game with the `@reload` command. You should now see the `time` command. If you enter
it, you might see something like:
We are in year 0, day 0, month 0.
It's 00:52:17.
You could display it a bit more prettily with names for months and perhaps even days, if you want.
And if "months" are called "moons" in your game, this is where you'd add that.
#### Time-related events in custom gametime
The `custom_gametime` module also has a way to schedule game-related events, taking into account
your game time (and your custom calendar). It can be used to have a specific message every day at
6:00 AM, to show the sun rises, for instance. The `custom_gametime.schedule` function works in the
same way as described for the default one above.

View file

@ -0,0 +1,467 @@
# Help System Tutorial
**Before doing this tutorial you will probably want to read the intro in [Basic Web tutorial](Web-
Tutorial).** Reading the three first parts of the [Django
tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/) might help as well.
This tutorial will show you how to access the help system through your website. Both help commands
and regular help entries will be visible, depending on the logged-in user or an anonymous character.
This tutorial will show you how to:
- Create a new page to add to your website.
- Take advantage of a basic view and basic templates.
- Access the help system on your website.
- Identify whether the viewer of this page is logged-in and, if so, to what account.
## Creating our app
The first step is to create our new Django *app*. An app in Django can contain pages and
mechanisms: your website may contain different apps. Actually, the website provided out-of-the-box
by Evennia has already three apps: a "webclient" app, to handle the entire webclient, a "website"
app to contain your basic pages, and a third app provided by Django to create a simple admin
interface. So we'll create another app in parallel, giving it a clear name to represent our help
system.
From your game directory, use the following command:
evennia startapp help_system
> Note: calling the app "help" would have been more explicit, but this name is already used by
Django.
This will create a directory named `help_system` at the root of your game directory. It's a good
idea to keep things organized and move this directory in the "web" directory of your game. Your
game directory should look like:
mygame/
...
web/
help_system/
...
The "web/help_system" directory contains files created by Django. We'll use some of them, but if
you want to learn more about them all, you should read [the Django
tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/).
There is a last thing to be done: your folder has been added, but Django doesn't know about it, it
doesn't know it's a new app. We need to tell it, and we do so by editing a simple setting. Open
your "server/conf/settings.py" file and add, or edit, these lines:
```python
# Web configuration
INSTALLED_APPS += (
"web.help_system",
)
```
You can start Evennia if you want, and go to your website, probably at
[http://localhost:4001](http://localhost:4001) . You won't see anything different though: we added
the app but it's fairly empty.
## Our new page
At this point, our new *app* contains mostly empty files that you can explore. In order to create
a page for our help system, we need to add:
- A *view*, dealing with the logic of our page.
- A *template* to display our new page.
- A new *URL* pointing to our page.
> We could get away by creating just a view and a new URL, but that's not a recommended way to work
with your website. Building on templates is so much more convenient.
### Create a view
A *view* in Django is a simple Python function placed in the "views.py" file in your app. It will
handle the behavior that is triggered when a user asks for this information by entering a *URL* (the
connection between *views* and *URLs* will be discussed later).
So let's create our view. You can open the "web/help_system/view.py" file and paste the following
lines:
```python
from django.shortcuts import render
def index(request):
"""The 'index' view."""
return render(request, "help_system/index.html")
```
Our view handles all code logic. This time, there's not much: when this function is called, it will
render the template we will now create. But that's where we will do most of our work afterward.
### Create a template
The `render` function called into our *view* asks the *template* `help_system/index.html`. The
*templates* of our apps are stored in the app directory, "templates" sub-directory. Django may have
created the "templates" folder already. If not, create it yourself. In it, create another folder
"help_system", and inside of this folder, create a file named "index.html". Wow, that's some
hierarchy. Your directory structure (starting from `web`) should look like this:
web/
help_system/
...
templates/
help_system/
index.html
Open the "index.html" file and paste in the following lines:
```
{% extends "base.html" %}
{% block titleblock %}Help index{% endblock %}
{% block content %}
<h2>Help index</h2>
{% endblock %}
```
Here's a little explanation line by line of what this template does:
1. It loads the "base.html" *template*. This describes the basic structure of all your pages, with
a menu at the top and a footer, and perhaps other information like images and things to be present
on each page. You can create templates that do not inherit from "base.html", but you should have a
good reason for doing so.
2. The "base.html" *template* defines all the structure of the page. What is left is to override
some sections of our pages. These sections are called *blocks*. On line 2, we override the block
named "blocktitle", which contains the title of our page.
3. Same thing here, we override the *block* named "content", which contains the main content of our
web page. This block is bigger, so we define it on several lines.
4. This is perfectly normal HTML code to display a level-2 heading.
5. And finally we close the *block* named "content".
### Create a new URL
Last step to add our page: we need to add a *URL* leading to it... otherwise users won't be able to
access it. The URLs of our apps are stored in the app's directory "urls.py" file.
Open the "web/help_system/urls.py" file (you might have to create it) and write in it:
```python
# URL patterns for the help_system app
from django.conf.urls import url
from web.help_system.views import index
urlpatterns = [
url(r'^$', index, name="index")
]
```
We also need to add our app as a namespace holder for URLS. Edit the file "web/urls.py". In it you
will find the `custom_patterns` variable. Replace it with:
```python
custom_patterns = [
url(r'^help/', include('web.help_system.urls',
namespace='help_system', app_name='help_system')),
]
```
When a user will ask for a specific *URL* on your site, Django will:
1. Read the list of custom patterns defined in "web/urls.py". There's one pattern here, which
describes to Django that all URLs beginning by 'help/' should be sent to the 'help_system' app. The
'help/' part is removed.
2. Then Django will check the "web.help_system/urls.py" file. It contains only one URL, which is
empty (`^$`).
In other words, if the URL is '/help/', then Django will execute our defined view.
### Let's see it work
You can now reload or start Evennia. Open a tab in your browser and go to
[http://localhost:4001/help/](http://localhost:4001/help/) . If everything goes well, you should
see your new page... which isn't empty since Evennia uses our "base.html" *template*. In the
content of our page, there's only a heading that reads "help index". Notice that the title of our
page is "mygame - Help index" ("mygame" is replaced by the name of your game).
From now on, it will be easier to move forward and add features.
### A brief reminder
We'll be trying the following things:
- Have the help of commands and help entries accessed online.
- Have various commands and help entries depending on whether the user is logged in or not.
In terms of pages, we'll have:
- One to display the list of help topics.
- One to display the content of a help topic.
The first one would link to the second.
> Should we create two URLs?
The answer is... maybe. It depends on what you want to do. We have our help index accessible
through the "/help/" URL. We could have the detail of a help entry accessible through "/help/desc"
(to see the detail of the "desc" command). The problem is that our commands or help topics may
contain special characters that aren't to be present in URLs. There are different ways around this
problem. I have decided to use a *GET variable* here, which would create URLs like this:
/help?name=desc
If you use this system, you don't have to add a new URL: GET and POST variables are accessible
through our requests and we'll see how soon enough.
## Handling logged-in users
One of our requirements is to have a help system tailored to our accounts. If an account with admin
access logs in, the page should display a lot of commands that aren't accessible to common users.
And perhaps even some additional help topics.
Fortunately, it's fairly easy to get the logged in account in our view (remember that we'll do most
of our coding there). The *request* object, passed to our function, contains a `user` attribute.
This attribute will always be there: we cannot test whether it's `None` or not, for instance. But
when the request comes from a user that isn't logged in, the `user` attribute will contain an
anonymous Django user. We then can use the `is_anonymous` method to see whether the user is logged-
in or not. Last gift by Evennia, if the user is logged in, `request.user` contains a reference to
an account object, which will help us a lot in coupling the game and online system.
So we might end up with something like:
```python
def index(request):
"""The 'index' view."""
user = request.user
if not user.is_anonymous() and user.character:
character = user.character
```
> Note: this code works when your MULTISESSION_MODE is set to 0 or 1. When it's above, you would
have something like:
```python
def index(request):
"""The 'index' view."""
user = request.user
if not user.is_anonymous() and user.db._playable_characters:
character = user.db._playable_characters[0]
```
In this second case, it will select the first character of the account.
But what if the user's not logged in? Again, we have different solutions. One of the most simple
is to create a character that will behave as our default character for the help system. You can
create it through your game: connect to it and enter:
@charcreate anonymous
The system should answer:
Created new character anonymous. Use @ic anonymous to enter the game as this character.
So in our view, we could have something like this:
```python
from typeclasses.characters import Character
def index(request):
"""The 'index' view."""
user = request.user
if not user.is_anonymous() and user.character:
character = user.character
else:
character = Character.objects.get(db_key="anonymous")
```
This time, we have a valid character no matter what: remember to adapt this code if you're running
in multisession mode above 1.
## The full system
What we're going to do is to browse through all commands and help entries, and list all the commands
that can be seen by this character (either our 'anonymous' character, or our logged-in character).
The code is longer, but it presents the entire concept in our view. Edit the
"web/help_system/views.py" file and paste into it:
```python
from django.http import Http404
from django.shortcuts import render
from evennia.help.models import HelpEntry
from typeclasses.characters import Character
def index(request):
"""The 'index' view."""
user = request.user
if not user.is_anonymous() and user.character:
character = user.character
else:
character = Character.objects.get(db_key="anonymous")
# Get the categories and topics accessible to this character
categories, topics = _get_topics(character)
# If we have the 'name' in our GET variable
topic = request.GET.get("name")
if topic:
if topic not in topics:
raise Http404("This help topic doesn't exist.")
topic = topics[topic]
context = {
"character": character,
"topic": topic,
}
return render(request, "help_system/detail.html", context)
else:
context = {
"character": character,
"categories": categories,
}
return render(request, "help_system/index.html", context)
def _get_topics(character):
"""Return the categories and topics for this character."""
cmdset = character.cmdset.all()[0]
commands = cmdset.commands
entries = [entry for entry in HelpEntry.objects.all()]
categories = {}
topics = {}
# Browse commands
for command in commands:
if not command.auto_help or not command.access(character):
continue
# Create the template for a command
template = {
"name": command.key,
"category": command.help_category,
"content": command.get_help(character, cmdset),
}
category = command.help_category
if category not in categories:
categories[category] = []
categories[category].append(template)
topics[command.key] = template
# Browse through the help entries
for entry in entries:
if not entry.access(character, 'view', default=True):
continue
# Create the template for an entry
template = {
"name": entry.key,
"category": entry.help_category,
"content": entry.entrytext,
}
category = entry.help_category
if category not in categories:
categories[category] = []
categories[category].append(template)
topics[entry.key] = template
# Sort categories
for entries in categories.values():
entries.sort(key=lambda c: c["name"])
categories = list(sorted(categories.items()))
return categories, topics
```
That's a bit more complicated here, but all in all, it can be divided in small chunks:
- The `index` function is our view:
- It begins by getting the character as we saw in the previous section.
- It gets the help topics (commands and help entries) accessible to this character. It's another
function that handles that part.
- If there's a *GET variable* "name" in our URL (like "/help?name=drop"), it will retrieve it. If
it's not a valid topic's name, it returns a *404*. Otherwise, it renders the template called
"detail.html", to display the detail of our topic.
- If there's no *GET variable* "name", render "index.html", to display the list of topics.
- The `_get_topics` is a private function. Its sole mission is to retrieve the commands a character
can execute, and the help entries this same character can see. This code is more Evennia-specific
than Django-specific, it will not be detailed in this tutorial. Just notice that all help topics
are stored in a dictionary. This is to simplify our job when displaying them in our templates.
Notice that, in both cases when we asked to render a *template*, we passed to `render` a third
argument which is the dictionary of variables used in our templates. We can pass variables this
way, and we will use them in our templates.
### The index template
Let's look at our full "index" *template*. You can open the
"web/help_system/templates/help_sstem/index.html" file and paste the following into it:
```
{% extends "base.html" %}
{% block titleblock %}Help index{% endblock %}
{% block content %}
<h2>Help index</h2>
{% if categories %}
{% for category, topics in categories %}
<h2>{{ category|capfirst }}</h2>
<table>
<tr>
{% for topic in topics %}
{% if forloop.counter|divisibleby:"5" %}
</tr>
<tr>
{% endif %}
<td><a href="{% url 'help_system:index' %}?name={{ topic.name|urlencode }}">
{{ topic.name }}</td>
{% endfor %}
</tr>
</table>
{% endfor %}
{% endif %}
{% endblock %}
```
This template is definitely more detailed. What it does is:
1. Browse through all categories.
2. For all categories, display a level-2 heading with the name of the category.
3. All topics in a category (remember, they can be either commands or help entries) are displayed in
a table. The trickier part may be that, when the loop is above 5, it will create a new line. The
table will have 5 columns at the most per row.
4. For every cell in the table, we create a link redirecting to the detail page (see below). The
URL would look something like "help?name=say". We use `urlencode` to ensure special characters are
properly escaped.
### The detail template
It's now time to show the detail of a topic (command or help entry). You can create the file
"web/help_system/templates/help_system/detail.html". You can paste into it the following code:
```
{% extends "base.html" %}
{% block titleblock %}Help for {{ topic.name }}{% endblock %}
{% block content %}
<h2>{{ topic.name|capfirst }} help topic</h2>
<p>Category: {{ topic.category|capfirst }}</p>
{{ topic.content|linebreaks }}
{% endblock %}
```
This template is much easier to read. Some *filters* might be unknown to you, but they are just
used to format here.
### Put it all together
Remember to reload or start Evennia, and then go to
[http://localhost:4001/help](http://localhost:4001/help/). You should see the list of commands and
topics accessible by all characters. Try to login (click the "login" link in the menu of your
website) and go to the same page again. You should now see a more detailed list of commands and
help entries. Click on one to see its detail.
## To improve this feature
As always, a tutorial is here to help you feel comfortable adding new features and code by yourself.
Here are some ideas of things to improve this little feature:
- Links at the bottom of the detail template to go back to the index might be useful.
- A link in the main menu to link to this page would be great... for the time being you have to
enter the URL, users won't guess it's there.
- Colors aren't handled at this point, which isn't exactly surprising. You could add it though.
- Linking help entries between one another won't be simple, but it would be great. For instance, if
you see a help entry about how to use several commands, it would be great if these commands were
themselves links to display their details.

View file

@ -0,0 +1,265 @@
# Implementing a game rule system
The simplest way to create an online roleplaying game (at least from a code perspective) is to
simply grab a paperback RPG rule book, get a staff of game masters together and start to run scenes
with whomever logs in. Game masters can roll their dice in front of their computers and tell the
players the results. This is only one step away from a traditional tabletop game and puts heavy
demands on the staff - it is unlikely staff will be able to keep up around the clock even if they
are very dedicated.
Many games, even the most roleplay-dedicated, thus tend to allow for players to mediate themselves
to some extent. A common way to do this is to introduce *coded systems* - that is, to let the
computer do some of the heavy lifting. A basic thing is to add an online dice-roller so everyone can
make rolls and make sure noone is cheating. Somewhere at this level you find the most bare-bones
roleplaying MUSHes.
The advantage of a coded system is that as long as the rules are fair the computer is too - it makes
no judgement calls and holds no personal grudges (and cannot be accused of holding any). Also, the
computer doesn't need to sleep and can always be online regardless of when a player logs on. The
drawback is that a coded system is not flexible and won't adapt to the unprogrammed actions human
players may come up with in role play. For this reason many roleplay-heavy MUDs do a hybrid
variation - they use coded systems for things like combat and skill progression but leave role play
to be mostly freeform, overseen by staff game masters.
Finally, on the other end of the scale are less- or no-roleplay games, where game mechanics (and
thus player fairness) is the most important aspect. In such games the only events with in-game value
are those resulting from code. Such games are very common and include everything from hack-and-slash
MUDs to various tactical simulations.
So your first decision needs to be just what type of system you are aiming for. This page will try
to give some ideas for how to organize the "coded" part of your system, however big that may be.
## Overall system infrastructure
We strongly recommend that you code your rule system as stand-alone as possible. That is, don't
spread your skill check code, race bonus calculation, die modifiers or what have you all over your
game.
- Put everything you would need to look up in a rule book into a module in `mygame/world`. Hide away
as much as you can. Think of it as a black box (or maybe the code representation of an all-knowing
game master). The rest of your game will ask this black box questions and get answers back. Exactly
how it arrives at those results should not need to be known outside the box. Doing it this way
makes it easier to change and update things in one place later.
- Store only the minimum stuff you need with each game object. That is, if your Characters need
values for Health, a list of skills etc, store those things on the Character - don't store how to
roll or change them.
- Next is to determine just how you want to store things on your Objects and Characters. You can
choose to either store things as individual [Attributes](Attributes), like `character.db.STR=34` and
`character.db.Hunting_skill=20`. But you could also use some custom storage method, like a
dictionary `character.db.skills = {"Hunting":34, "Fishing":20, ...}`. A much more fancy solution is
to look at the Ainneve [Trait
handler](https://github.com/evennia/ainneve/blob/master/world/traits.py). Finally you could even go
with a [custom django model](New-Models). Which is the better depends on your game and the
complexity of your system.
- Make a clear [API](http://en.wikipedia.org/wiki/Application_programming_interface) into your
rules. That is, make methods/functions that you feed with, say, your Character and which skill you
want to check. That is, you want something similar to this:
```python
from world import rules
result = rules.roll_skill(character, "hunting")
result = rules.roll_challenge(character1, character2, "swords")
```
You might need to make these functions more or less complex depending on your game. For example the
properties of the room might matter to the outcome of a roll (if the room is dark, burning etc).
Establishing just what you need to send into your game mechanic module is a great way to also get a
feel for what you need to add to your engine.
## Coded systems
Inspired by tabletop role playing games, most game systems mimic some sort of die mechanic. To this
end Evennia offers a full [dice
roller](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) in its `contrib`
folder. For custom implementations, Python offers many ways to randomize a result using its in-built
`random` module. No matter how it's implemented, we will in this text refer to the action of
determining an outcome as a "roll".
In a freeform system, the result of the roll is just compared with values and people (or the game
master) just agree on what it means. In a coded system the result now needs to be processed somehow.
There are many things that may happen as a result of rule enforcement:
- Health may be added or deducted. This can effect the character in various ways.
- Experience may need to be added, and if a level-based system is used, the player might need to be
informed they have increased a level.
- Room-wide effects need to be reported to the room, possibly affecting everyone in the room.
There are also a slew of other things that fall under "Coded systems", including things like
weather, NPC artificial intelligence and game economy. Basically everything about the world that a
Game master would control in a tabletop role playing game can be mimicked to some level by coded
systems.
## Example of Rule module
Here is a simple example of a rule module. This is what we assume about our simple example game:
- Characters have only four numerical values:
- Their `level`, which starts at 1.
- A skill `combat`, which determines how good they are at hitting things. Starts between 5 and
10.
- Their Strength, `STR`, which determine how much damage they do. Starts between 1 and 10.
- Their Health points, `HP`, which starts at 100.
- When a Character reaches `HP = 0`, they are presumed "defeated". Their HP is reset and they get a
failure message (as a stand-in for death code).
- Abilities are stored as simple Attributes on the Character.
- "Rolls" are done by rolling a 100-sided die. If the result is below the `combat` value, it's a
success and damage is rolled. Damage is rolled as a six-sided die + the value of `STR` (for this
example we ignore weapons and assume `STR` is all that matters).
- Every successful `attack` roll gives 1-3 experience points (`XP`). Every time the number of `XP`
reaches `(level + 1) ** 2`, the Character levels up. When leveling up, the Character's `combat`
value goes up by 2 points and `STR` by one (this is a stand-in for a real progression system).
### Character
The Character typeclass is simple. It goes in `mygame/typeclasses/characters.py`. There is already
an empty `Character` class there that Evennia will look to and use.
```python
from random import randint
from evennia import DefaultCharacter
class Character(DefaultCharacter):
"""
Custom rule-restricted character. We randomize
the initial skill and ability values bettween 1-10.
"""
def at_object_creation(self):
"Called only when first created"
self.db.level = 1
self.db.HP = 100
self.db.XP = 0
self.db.STR = randint(1, 10)
self.db.combat = randint(5, 10)
```
`@reload` the server to load up the new code. Doing `examine self` will however *not* show the new
Attributes on yourself. This is because the `at_object_creation` hook is only called on *new*
Characters. Your Character was already created and will thus not have them. To force a reload, use
the following command:
```
@typeclass/force/reset self
```
The `examine self` command will now show the new Attributes.
### Rule module
This is a module `mygame/world/rules.py`.
```python
from random import randint
def roll_hit():
"Roll 1d100"
return randint(1, 100)
def roll_dmg():
"Roll 1d6"
return randint(1, 6)
def check_defeat(character):
"Checks if a character is 'defeated'."
if character.db.HP <= 0:
character.msg("You fall down, defeated!")
character.db.HP = 100 # reset
def add_XP(character, amount):
"Add XP to character, tracking level increases."
character.db.XP += amount
if character.db.XP >= (character.db.level + 1) ** 2:
character.db.level += 1
character.db.STR += 1
character.db.combat += 2
character.msg("You are now level %i!" % character.db.level)
def skill_combat(*args):
"""
This determines outcome of combat. The one who
rolls under their combat skill AND higher than
their opponent's roll hits.
"""
char1, char2 = args
roll1, roll2 = roll_hit(), roll_hit()
failtext = "You are hit by %s for %i damage!"
wintext = "You hit %s for %i damage!"
xp_gain = randint(1, 3)
if char1.db.combat >= roll1 > roll2:
# char 1 hits
dmg = roll_dmg() + char1.db.STR
char1.msg(wintext % (char2, dmg))
add_XP(char1, xp_gain)
char2.msg(failtext % (char1, dmg))
char2.db.HP -= dmg
check_defeat(char2)
elif char2.db.combat >= roll2 > roll1:
# char 2 hits
dmg = roll_dmg() + char2.db.STR
char1.msg(failtext % (char2, dmg))
char1.db.HP -= dmg
check_defeat(char1)
char2.msg(wintext % (char1, dmg))
add_XP(char2, xp_gain)
else:
# a draw
drawtext = "Neither of you can find an opening."
char1.msg(drawtext)
char2.msg(drawtext)
SKILLS = {"combat": skill_combat}
def roll_challenge(character1, character2, skillname):
"""
Determine the outcome of a skill challenge between
two characters based on the skillname given.
"""
if skillname in SKILLS:
SKILLS[skillname](character1, character2)
else:
raise RunTimeError("Skillname %s not found." % skillname)
```
These few functions implement the entirety of our simple rule system. We have a function to check
the "defeat" condition and reset the `HP` back to 100 again. We define a generic "skill" function.
Multiple skills could all be added with the same signature; our `SKILLS` dictionary makes it easy to
look up the skills regardless of what their actual functions are called. Finally, the access
function `roll_challenge` just picks the skill and gets the result.
In this example, the skill function actually does a lot - it not only rolls results, it also informs
everyone of their results via `character.msg()` calls.
Here is an example of usage in a game command:
```python
from evennia import Command
from world import rules
class CmdAttack(Command):
"""
attack an opponent
Usage:
attack <target>
This will attack a target in the same room, dealing
damage with your bare hands.
"""
def func(self):
"Implementing combat"
caller = self.caller
if not self.args:
caller.msg("You need to pick a target to attack.")
return
target = caller.search(self.args)
if target:
rules.roll_challenge(caller, target, "combat")
```
Note how simple the command becomes and how generic you can make it. It becomes simple to offer any
number of Combat commands by just extending this functionality - you can easily roll challenges and
pick different skills to check. And if you ever decided to, say, change how to determine hit chance,
you don't have to change every command, but need only change the single `roll_hit` function inside
your `rules` module.

View file

@ -0,0 +1,70 @@
# Learn Python for Evennia The Hard Way
# WORK IN PROGRESS - DO NOT USE
Evennia provides a great foundation to build your very own MU* whether you have programming
experience or none at all. Whilst Evennia has a number of in-game building commands and tutorials
available to get you started, when approaching game systems of any complexity it is advisable to
have the basics of Python under your belt before jumping into the code. There are many Python
tutorials freely available online however this page focuses on Learn Python the Hard Way (LPTHW) by
Zed Shaw. This tutorial takes you through the basics of Python and progresses you to creating your
very own online text based game. Whilst completing the course feel free to install Evennia and try
out some of our beginner tutorials. On completion you can return to this page, which will act as an
overview to the concepts separating your online text based game and the inner-workings of Evennia.
-The latter portion of the tutorial focuses working your engine into a webpage and is not strictly
required for development in Evennia.
## Exercise 23
You may have returned here when you were invited to read some code. If you havent already, you
should now have the knowledge necessary to install Evennia. Head over to the Getting Started page
for install instructions. You can also try some of our tutorials to get you started on working with
Evennia.
## Bridging the gap.
If you have successfully completed the Learn Python the Hard Way tutorial you should now have a
simple browser based Interactive Fiction engine which looks similar to this.
This engine is built using a single interactive object type, the Room class. The Room class holds a
description of itself that is presented to the user and a list of hardcoded commands which if
selected correctly will present you with the next rooms description and commands. Whilst your game
only has one interactive object, MU* have many more: Swords and shields, potions and scrolls or even
laser guns and robots. Even the player has an in-game representation in the form of your character.
Each of these examples are represented by their own object with their own description that can be
presented to the user.
A basic object in Evennia has a number of default functions but perhaps most important is the idea
of location. In your text engine you receive a description of a room but you are not really in the
room because you have no in-game representation. However, in Evennia when you enter a Dungeon you
ARE in the dungeon. That is to say your character.location = Dungeon whilst the Dungeon.contents now
has a spunky young adventurer added to it. In turn, your character.contents may have amongst it a
number of swords and potions to help you on your adventure and their location would be you.
In reality each of these “objects” are just an entry in your Evennia projects database which keeps
track of all these attributes, such as location and contents. Making changes to those attributes and
the rules in which they are changed is the most fundamental perspective of how your game works. We
define those rules in the objects Typeclass. The Typeclass is a Python class with a special
connection to the games database which changes values for us through various class methods. Lets
look at your characters Typeclass rules for changing location.
1. `self.at_before_move(destination)` (if this returns False, move is aborted)
2. `self.announce_move_from(destination)`
3. (move happens here)
4. `self.announce_move_to(source_location)`
5. `self.at_after_move(source_location)`
First we check if its okay to leave our current location, then we tell everyone there that were
leaving. We move locations and tell everyone at our new location that weve arrived before checking
were okay to be there. By default stages 1 and 5 are empty ready for us to add some rules. Well
leave an explanation as to how to make those changes for the tutorial section, but imagine if you
were an astronaut. A smart astronaut might stop at step 1 to remember to put his helmet on whilst a
slower astronaut might realise hes forgotten in step 5 before shortly after ceasing to be an
astronaut.
With all these objects and all this moving around it raises another problem. In your text engine the
commands available to the player were hard-coded to the room. That means if we have commands we
always want available to the player well need to have those commands hard-coded on every single
room. What about an armoury? When all the swords are gone the command to take a sword would still
remain causing confusion. Evennia solves this problem by giving each object the ability to hold
commands. Rooms can have commands attached to them specific to that location, like climbing a tree;
Players can have commands which are always available to them, like look, get and say; and
objects can have commands attached to them which unlock when taking possession of it, like attack
commands when obtaining a weapon.

View file

@ -0,0 +1,169 @@
# Manually Configuring Color
This is a small tutorial for customizing your character objects, using the example of letting users
turn on and off ANSI color parsing as an example. `@options NOCOLOR=True` will now do what this
tutorial shows, but the tutorial subject can be applied to other toggles you may want, as well.
In the Building guide's [Colors](TextTags#coloured-text) page you can learn how to add color to your
game by using special markup. Colors enhance the gaming experience, but not all users want color.
Examples would be users working from clients that don't support color, or people with various seeing
disabilities that rely on screen readers to play your game. Also, whereas Evennia normally
automatically detects if a client supports color, it may get it wrong. Being able to turn it on
manually if you know it **should** work could be a nice feature.
So here's how to allow those users to remove color. It basically means you implementing a simple
configuration system for your characters. This is the basic sequence:
1. Define your own default character typeclass, inheriting from Evennia's default.
1. Set an attribute on the character to control markup on/off.
1. Set your custom character class to be the default for new accounts.
1. Overload the `msg()` method on the typeclass and change how it uses markup.
1. Create a custom command to allow users to change their setting.
## Setting up a custom Typeclass
Create a new module in `mygame/typeclasses` named, for example, `mycharacter.py`. Alternatively you
can simply add a new class to 'mygamegame/typeclasses/characters.py'.
In your new module(or characters.py), create a new [Typeclass](Typeclasses) inheriting from
`evennia.DefaultCharacter`. We will also import `evennia.utils.ansi`, which we will use later.
```python
from evennia import Character
from evennia.utils import ansi
class ColorableCharacter(Character):
at_object_creation(self):
# set a color config value
self.db.config_color = True
```
Above we set a simple config value as an [Attribute](Attributes).
Let's make sure that new characters are created of this type. Edit your
`mygame/server/conf/settings.py` file and add/change `BASE_CHARACTER_TYPECLASS` to point to your new
character class. Observe that this will only affect *new* characters, not those already created. You
have to convert already created characters to the new typeclass by using the `@typeclass` command
(try on a secondary character first though, to test that everything works - you don't want to render
your root user unusable!).
@typeclass/reset/force Bob = mycharacter.ColorableCharacter
`@typeclass` changes Bob's typeclass and runs all its creation hooks all over again. The `/reset`
switch clears all attributes and properties back to the default for the new typeclass - this is
useful in this case to avoid ending up with an object having a "mixture" of properties from the old
typeclass and the new one. `/force` might be needed if you edit the typeclass and want to update the
object despite the actual typeclass name not having changed.
## Overload the `msg()` method
Next we need to overload the `msg()` method. What we want is to check the configuration value before
calling the main function. The original `msg` method call is seen in `evennia/objects/objects.py`
and is called like this:
```python
msg(self, text=None, from_obj=None, session=None, options=None, **kwargs):
```
As long as we define a method on our custom object with the same name and keep the same number of
arguments/keywords we will overload the original. Here's how it could look:
```python
class ColorableCharacter(Character):
# [...]
msg(self, text=None, from_obj=None, session=None, options=None,
**kwargs):
"our custom msg()"
if self.db.config_color is not None: # this would mean it was not set
if not self.db.config_color:
# remove the ANSI from the text
text = ansi.strip_ansi(text)
super().msg(text=text, from_obj=from_obj,
session=session, **kwargs)
```
Above we create a custom version of the `msg()` method. If the configuration Attribute is set, it
strips the ANSI from the text it is about to send, and then calls the parent `msg()` as usual. You
need to `@reload` before your changes become visible.
There we go! Just flip the attribute `config_color` to False and your users will not see any color.
As superuser (assuming you use the Typeclass `ColorableCharacter`) you can test this with the `@py`
command:
@py self.db.config_color = False
## Custom color config command
For completeness, let's add a custom command so users can turn off their color display themselves if
they want.
In `mygame/commands`, create a new file, call it for example `configcmds.py` (it's likely that
you'll want to add other commands for configuration down the line). You can also copy/rename the
command template.
```python
from evennia import Command
class CmdConfigColor(Command):
"""
Configures your color
Usage:
@togglecolor on|off
This turns ANSI-colors on/off.
Default is on.
"""
key = "@togglecolor"
aliases = ["@setcolor"]
def func(self):
"implements the command"
# first we must remove whitespace from the argument
self.args = self.args.strip()
if not self.args or not self.args in ("on", "off"):
self.caller.msg("Usage: @setcolor on|off")
return
if self.args == "on":
self.caller.db.config_color = True
# send a message with a tiny bit of formatting, just for fun
self.caller.msg("Color was turned |won|W.")
else:
self.caller.db.config_color = False
self.caller.msg("Color was turned off.")
```
Lastly, we make this command available to the user by adding it to the default `CharacterCmdSet` in
`mygame/commands/default_cmdsets.py` and reloading the server. Make sure you also import the
command:
```python
from mygame.commands import configcmds
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# [...]
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones.
#
# here is the only line that we edit
self.add(configcmds.CmdConfigColor())
```
## More colors
Apart from ANSI colors, Evennia also supports **Xterm256** colors (See [Colors](TextTags#colored-
text)). The `msg()` method supports the `xterm256` keyword for manually activating/deactiving
xterm256. It should be easy to expand the above example to allow players to customize xterm256
regardless of if Evennia thinks their client supports it or not.
To get a better understanding of how `msg()` works with keywords, you can try this as superuser:
@py self.msg("|123Dark blue with xterm256, bright blue with ANSI", xterm256=True)
@py self.msg("|gThis should be uncolored", nomarkup=True)

View file

@ -0,0 +1,96 @@
# Mass and weight for objects
An easy addition to add dynamic variety to your world objects is to give them some mass. Why mass
and not weight? Weight varies in setting; for example things on the Moon weigh 1/6 as much. On
Earth's surface and in most environments, no relative weight factor is needed.
In most settings, mass can be used as weight to spring a pressure plate trap or a floor giving way,
determine a character's burden weight for travel speed... The total mass of an object can
contribute to the force of a weapon swing, or a speeding meteor to give it a potential striking
force.
#### Objects
Now that we have reasons for keeping track of object mass, let's look at the default object class
inside your mygame/typeclasses/objects.py and see how easy it is to total up mass from an object and
its contents.
```python
# inside your mygame/typeclasses/objects.py
class Object(DefaultObject):
# [...]
def get_mass(self):
mass = self.attributes.get('mass', 1) # Default objects have 1 unit mass.
return mass + sum(obj.get_mass() for obj in self.contents)
```
Adding the `get_mass` definition to the objects you want to sum up the masses for is done with
Python's "sum" function which operates on all the contents, in this case by summing them to
return a total mass value.
If you only wanted specific object types to have mass or have the new object type in a different
module, see [[Adding-Object-Typeclass-Tutorial]] with its Heavy class object. You could set the
default for Heavy types to something much larger than 1 gram or whatever unit you want to use. Any
non-default mass would be stored on the `mass` [[Attributes]] of the objects.
#### Characters and rooms
You can add a `get_mass` definition to characters and rooms, also.
If you were in a one metric-ton elevator with four other friends also wearing armor and carrying
gold bricks, you might wonder if this elevator's going to move, and how fast.
Assuming the unit is grams and the elevator itself weights 1,000 kilograms, it would already be
`@set elevator/mass=1000000`, we're `@set me/mass=85000` and our armor is `@set armor/mass=50000`.
We're each carrying 20 gold bars each `@set gold bar/mass=12400` then step into the elevator and see
the following message in the elevator's appearance: `Elevator weight and contents should not exceed
3 metric tons.` Are we safe? Maybe not if you consider dynamic loading. But at rest:
```python
# Elevator object knows when it checks itself:
if self.get_mass() < 3000000:
pass # Elevator functions as normal.
else:
pass # Danger! Alarm sounds, cable snaps, elevator stops...
```
#### Inventory
Example of listing mass of items in your inventory:
```python
class CmdInventory(MuxCommand):
"""
view inventory
Usage:
inventory
inv
Switches:
/weight to display all available channels.
Shows your inventory: carrying, wielding, wearing, obscuring.
"""
key = "inventory"
aliases = ["inv", "i"]
locks = "cmd:all()"
def func(self):
"check inventory"
items = self.caller.contents
if not items:
string = "You are not carrying anything."
else:
table = prettytable.PrettyTable(["name", "desc"])
table.header = False
table.border = False
for item in items:
second = item.get_mass() \
if 'weight' in self.switches else item.db.desc
table.add_row(["%s" % item.get_display_name(self.caller.sessions),
second and second or ""])
string = "|wYou are carrying:\n%s" % table
self.caller.msg(string)
```

View file

@ -0,0 +1,334 @@
# NPC shop Tutorial
This tutorial will describe how to make an NPC-run shop. We will make use of the [EvMenu](EvMenu)
system to present shoppers with a menu where they can buy things from the store's stock.
Our shop extends over two rooms - a "front" room open to the shop's customers and a locked "store
room" holding the wares the shop should be able to sell. We aim for the following features:
- The front room should have an Attribute `storeroom` that points to the store room.
- Inside the front room, the customer should have a command `buy` or `browse`. This will open a
menu listing all items available to buy from the store room.
- A customer should be able to look at individual items before buying.
- We use "gold" as an example currency. To determine cost, the system will look for an Attribute
`gold_value` on the items in the store room. If not found, a fixed base value of 1 will be assumed.
The wealth of the customer should be set as an Attribute `gold` on the Character. If not set, they
have no gold and can't buy anything.
- When the customer makes a purchase, the system will check the `gold_value` of the goods and
compare it to the `gold` Attribute of the customer. If enough gold is available, this will be
deducted and the goods transferred from the store room to the inventory of the customer.
- We will lock the store room so that only people with the right key can get in there.
### The shop menu
We want to show a menu to the customer where they can list, examine and buy items in the store. This
menu should change depending on what is currently for sale. Evennia's *EvMenu* utility will manage
the menu for us. It's a good idea to [read up on EvMenu](EvMenu) if you are not familiar with it.
#### Designing the menu
The shopping menu's design is straightforward. First we want the main screen. You get this when you
enter a shop and use the `browse` or `buy` command:
```
*** Welcome to ye Old Sword shop! ***
Things for sale (choose 1-3 to inspect, quit to exit):
_________________________________________________________
1. A rusty sword (5 gold)
2. A sword with a leather handle (10 gold)
3. Excalibur (100 gold)
```
There are only three items to buy in this example but the menu should expand to however many items
are needed. When you make a selection you will get a new screen showing the options for that
particular item:
```
You inspect A rusty sword:
This is an old weapon maybe once used by soldiers in some
long forgotten army. It is rusty and in bad condition.
__________________________________________________________
1. Buy A rusty sword (5 gold)
2. Look for something else.
```
Finally, when you buy something, a brief message should pop up:
```
You pay 5 gold and purchase A rusty sword!
```
or
```
You cannot afford 5 gold for A rusty sword!
```
After this you should be back to the top level of the shopping menu again and can continue browsing.
#### Coding the menu
EvMenu defines the *nodes* (each menu screen with options) as normal Python functions. Each node
must be able to change on the fly depending on what items are currently for sale. EvMenu will
automatically make the `quit` command available to us so we won't add that manually. For compactness
we will put everything needed for our shop in one module, `mygame/typeclasses/npcshop.py`.
```python
# mygame/typeclasses/npcshop.py
from evennia.utils import evmenu
def menunode_shopfront(caller):
"This is the top-menu screen."
shopname = caller.location.key
wares = caller.location.db.storeroom.contents
# Wares includes all items inside the storeroom, including the
# door! Let's remove that from our for sale list.
wares = [ware for ware in wares if ware.key.lower() != "door"]
text = "*** Welcome to %s! ***\n" % shopname
if wares:
text += " Things for sale (choose 1-%i to inspect);" \
" quit to exit:" % len(wares)
else:
text += " There is nothing for sale; quit to exit."
options = []
for ware in wares:
# add an option for every ware in store
options.append({"desc": "%s (%s gold)" %
(ware.key, ware.db.gold_value or 1),
"goto": "menunode_inspect_and_buy"})
return text, options
```
In this code we assume the caller to be *inside* the shop when accessing the menu. This means we can
access the shop room via `caller.location` and get its `key` to display as the shop's name. We also
assume the shop has an Attribute `storeroom` we can use to get to our stock. We loop over our goods
to build up the menu's options.
Note that *all options point to the same menu node* called `menunode_inspect_and_buy`! We can't know
which goods will be available to sale so we rely on this node to modify itself depending on the
circumstances. Let's create it now.
```python
# further down in mygame/typeclasses/npcshop.py
def menunode_inspect_and_buy(caller, raw_string):
"Sets up the buy menu screen."
wares = caller.location.db.storeroom.contents
# Don't forget, we will need to remove that pesky door again!
wares = [ware for ware in wares if ware.key.lower() != "door"]
iware = int(raw_string) - 1
ware = wares[iware]
value = ware.db.gold_value or 1
wealth = caller.db.gold or 0
text = "You inspect %s:\n\n%s" % (ware.key, ware.db.desc)
def buy_ware_result(caller):
"This will be executed first when choosing to buy."
if wealth >= value:
rtext = "You pay %i gold and purchase %s!" % \
(value, ware.key)
caller.db.gold -= value
ware.move_to(caller, quiet=True)
else:
rtext = "You cannot afford %i gold for %s!" % \
(value, ware.key)
caller.msg(rtext)
options = ({"desc": "Buy %s for %s gold" % \
(ware.key, ware.db.gold_value or 1),
"goto": "menunode_shopfront",
"exec": buy_ware_result},
{"desc": "Look for something else",
"goto": "menunode_shopfront"})
return text, options
```
In this menu node we make use of the `raw_string` argument to the node. This is the text the menu
user entered on the *previous* node to get here. Since we only allow numbered options in our menu,
`raw_input` must be an number for the player to get to this point. So we convert it to an integer
index (menu lists start from 1, whereas Python indices always starts at 0, so we need to subtract
1). We then use the index to get the corresponding item from storage.
We just show the customer the `desc` of the item. In a more elaborate setup you might want to show
things like weapon damage and special stats here as well.
When the user choose the "buy" option, EvMenu will execute the `exec` instruction *before* we go
back to the top node (the `goto` instruction). For this we make a little inline function
`buy_ware_result`. EvMenu will call the function given to `exec` like any menu node but it does not
need to return anything. In `buy_ware_result` we determine if the customer can afford the cost and
give proper return messages. This is also where we actually move the bought item into the inventory
of the customer.
#### The command to start the menu
We could *in principle* launch the shopping menu the moment a customer steps into our shop room, but
this would probably be considered pretty annoying. It's better to create a [Command](Commands) for
customers to explicitly wanting to shop around.
```python
# mygame/typeclasses/npcshop.py
from evennia import Command
class CmdBuy(Command):
"""
Start to do some shopping
Usage:
buy
shop
browse
This will allow you to browse the wares of the
current shop and buy items you want.
"""
key = "buy"
aliases = ("shop", "browse")
def func(self):
"Starts the shop EvMenu instance"
evmenu.EvMenu(self.caller,
"typeclasses.npcshop",
startnode="menunode_shopfront")
```
This will launch the menu. The `EvMenu` instance is initialized with the path to this very module -
since the only global functions available in this module are our menu nodes, this will work fine
(you could also have put those in a separate module). We now just need to put this command in a
[CmdSet](Command-Sets) so we can add it correctly to the game:
```python
from evennia import CmdSet
class ShopCmdSet(CmdSet):
def at_cmdset_creation(self):
self.add(CmdBuy())
```
### Building the shop
There are really only two things that separate our shop from any other Room:
- The shop has the `storeroom` Attribute set on it, pointing to a second (completely normal) room.
- It has the `ShopCmdSet` stored on itself. This makes the `buy` command available to users entering
the shop.
For testing we could easily add these features manually to a room using `@py` or other admin
commands. Just to show how it can be done we'll instead make a custom [Typeclass](Typeclasses) for
the shop room and make a small command that builders can use to build both the shop and the
storeroom at once.
```python
# bottom of mygame/typeclasses/npcshop.py
from evennia import DefaultRoom, DefaultExit, DefaultObject
from evennia.utils.create import create_object
# class for our front shop room
class NPCShop(DefaultRoom):
def at_object_creation(self):
# we could also use add(ShopCmdSet, permanent=True)
self.cmdset.add_default(ShopCmdSet)
self.db.storeroom = None
# command to build a complete shop (the Command base class
# should already have been imported earlier in this file)
class CmdBuildShop(Command):
"""
Build a new shop
Usage:
@buildshop shopname
This will create a new NPCshop room
as well as a linked store room (named
simply <storename>-storage) for the
wares on sale. The store room will be
accessed through a locked door in
the shop.
"""
key = "@buildshop"
locks = "cmd:perm(Builders)"
help_category = "Builders"
def func(self):
"Create the shop rooms"
if not self.args:
self.msg("Usage: @buildshop <storename>")
return
# create the shop and storeroom
shopname = self.args.strip()
shop = create_object(NPCShop,
key=shopname,
location=None)
storeroom = create_object(DefaultRoom,
key="%s-storage" % shopname,
location=None)
shop.db.storeroom = storeroom
# create a door between the two
shop_exit = create_object(DefaultExit,
key="back door",
aliases=["storage", "store room"],
location=shop,
destination=storeroom)
storeroom_exit = create_object(DefaultExit,
key="door",
location=storeroom,
destination=shop)
# make a key for accessing the store room
storeroom_key_name = "%s-storekey" % shopname
storeroom_key = create_object(DefaultObject,
key=storeroom_key_name,
location=shop)
# only allow chars with this key to enter the store room
shop_exit.locks.add("traverse:holds(%s)" % storeroom_key_name)
# inform the builder about progress
self.caller.msg("The shop %s was created!" % shop)
```
Our typeclass is simple and so is our `buildshop` command. The command (which is for Builders only)
just takes the name of the shop and builds the front room and a store room to go with it (always
named `"<shopname>-storage"`. It connects the rooms with a two-way exit. You need to add
`CmdBuildShop` [to the default cmdset](Adding-Command-Tutorial#step-2-adding-the-command-to-a-
default-cmdset) before you can use it. Once having created the shop you can now `@teleport` to it or
`@open` a new exit to it. You could also easily expand the above command to automatically create
exits to and from the new shop from your current location.
To avoid customers walking in and stealing everything, we create a [Lock](Locks) on the storage
door. It's a simple lock that requires the one entering to carry an object named
`<shopname>-storekey`. We even create such a key object and drop it in the shop for the new shop
keeper to pick up.
> If players are given the right to name their own objects, this simple lock is not very secure and
you need to come up with a more robust lock-key solution.
> We don't add any descriptions to all these objects so looking "at" them will not be too thrilling.
You could add better default descriptions as part of the `@buildshop` command or leave descriptions
this up to the Builder.
### The shop is open for business!
We now have a functioning shop and an easy way for Builders to create it. All you need now is to
`@open` a new exit from the rest of the game into the shop and put some sell-able items in the store
room. Our shop does have some shortcomings:
- For Characters to be able to buy stuff they need to also have the `gold` Attribute set on
themselves.
- We manually remove the "door" exit from our items for sale. But what if there are other unsellable
items in the store room? What if the shop owner walks in there for example - anyone in the store
could then buy them for 1 gold.
- What if someone else were to buy the item we're looking at just before we decide to buy it? It
would then be gone and the counter be wrong - the shop would pass us the next item in the list.
Fixing these issues are left as an exercise.
If you want to keep the shop fully NPC-run you could add a [Script](Scripts) to restock the shop's
store room regularly. This shop example could also easily be owned by a human Player (run for them
by a hired NPC) - the shop owner would get the key to the store room and be responsible for keeping
it well stocked.

View file

@ -0,0 +1,100 @@
# Add a simple new web page
Evennia leverages [Django](https://docs.djangoproject.com) which is a web development framework.
Huge professional websites are made in Django and there is extensive documentation (and books) on it
. You are encouraged to at least look at the Django basic tutorials. Here we will just give a brief
introduction for how things hang together, to get you started.
We assume you have installed and set up Evennia to run. A webserver and website comes out of the
box. You can get to that by entering `http://localhost:4001` in your web browser - you should see a
welcome page with some game statistics and a link to the web client. Let us add a new page that you
can get to by going to `http://localhost:4001/story`.
### Create the view
A django "view" is a normal Python function that django calls to render the HTML page you will see
in the web browser. Here we will just have it spit back the raw html, but Django can do all sorts of
cool stuff with the page in the view, like adding dynamic content or change it on the fly. Open
`mygame/web` folder and add a new module there named `story.py` (you could also put it in its own
folder if you wanted to be neat. Don't forget to add an empty `__init__.py` file if you do, to tell
Python you can import from the new folder). Here's how it looks:
```python
# in mygame/web/story.py
from django.shortcuts import render
def storypage(request):
return render(request, "story.html")
```
This view takes advantage of a shortcut provided to use by Django, _render_. This shortcut gives the
template some information from the request, for instance, the game name, and then renders it.
### The HTML page
We need to find a place where Evennia (and Django) looks for html files (called *templates* in
Django parlance). You can specify such places in your settings (see the `TEMPLATES` variable in
`default_settings.py` for more info), but here we'll use an existing one. Go to
`mygame/template/overrides/website/` and create a page `story.html` there.
This is not a HTML tutorial, so we'll go simple:
```html
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col">
<h1>A story about a tree</h1>
<p>
This is a story about a tree, a classic tale ...
</p>
</div>
</div>
{% endblock %}
```
Since we've used the _render_ shortcut, Django will allow us to extend our base styles easily.
If you'd rather not take advantage of Evennia's base styles, you can do something like this instead:
```html
<html>
<body>
<h1>A story about a tree</h1>
<p>
This is a story about a tree, a classic tale ...
</body>
</html>
```
### The URL
When you enter the address `http://localhost:4001/story` in your web browser, Django will parse that
field to figure out which page you want to go to. You tell it which patterns are relevant in the
file
[mygame/web/urls.py](https://github.com/evennia/evennia/blob/master/evennia/game_template/web/urls.py).
Open it now.
Django looks for the variable `urlpatterns` in this file. You want to add your new pattern to the
`custom_patterns` list we have prepared - that is then merged with the default `urlpatterns`. Here's
how it could look:
```python
from web import story
# ...
custom_patterns = [
url(r'story', story.storypage, name='Story'),
]
```
That is, we import our story view module from where we created it earlier and then create an `url`
instance. The first argument to `url` is the pattern of the url we want to find (`"story"`) (this is
a regular expression if you are familiar with those) and then our view function we want to direct
to.
That should be it. Reload Evennia and you should be able to browse to your new story page!

View file

@ -0,0 +1,171 @@
# Adding Command Tutorial
This is a quick first-time tutorial expanding on the [Commands](Commands) documentation.
Let's assume you have just downloaded Evennia, installed it and created your game folder (let's call
it just `mygame` here). Now you want to try to add a new command. This is the fastest way to do it.
## Step 1: Creating a custom command
1. Open `mygame/commands/command.py` in a text editor. This is just one place commands could be
placed but you get it setup from the onset as an easy place to start. It also already contains some
example code.
1. Create a new class in `command.py` inheriting from `default_cmds.MuxCommand`. Let's call it
`CmdEcho` in this example.
1. Set the class variable `key` to a good command name, like `echo`.
1. Give your class a useful _docstring_. A docstring is the string at the very top of a class or
function/method. The docstring at the top of the command class is read by Evennia to become the help
entry for the Command (see
[Command Auto-help](Help-System#command-auto-help-system)).
1. Define a class method `func(self)` that echoes your input back to you.
Below is an example how this all could look for the echo command:
```python
# file mygame/commands/command.py
#[...]
from evennia import default_cmds
class CmdEcho(default_cmds.MuxCommand):
"""
Simple command example
Usage:
echo [text]
This command simply echoes text back to the caller.
"""
key = "echo"
def func(self):
"This actually does things"
if not self.args:
self.caller.msg("You didn't enter anything!")
else:
self.caller.msg("You gave the string: '%s'" % self.args)
```
## Step 2: Adding the Command to a default Cmdset
The command is not available to use until it is part of a [Command Set](Command-Sets). In this
example we will go the easiest route and add it to the default Character commandset that already
exists.
1. Edit `mygame/commands/default_cmdsets.py`
1. Import your new command with `from commands.command import CmdEcho`.
1. Add a line `self.add(CmdEcho())` to `CharacterCmdSet`, in the `at_cmdset_creation` method (the
template tells you where).
This is approximately how it should look at this point:
```python
# file mygame/commands/default_cmdsets.py
#[...]
from commands.command import CmdEcho
#[...]
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
# this first adds all default commands
super(DefaultSet, self).at_cmdset_creation()
# all commands added after this point will extend or
# overwrite the default commands.
self.add(CmdEcho())
```
Next, run the `@reload` command. You should now be able to use your new `echo` command from inside
the game. Use `help echo` to see the documentation for the command.
If you have trouble, make sure to check the log for error messages (probably due to syntax errors in
your command definition).
> Note: Typing `echotest` will also work. It will be handled as the command `echo` directly followed
by
its argument `test` (which will end up in `self.args). To change this behavior, you can add the
`arg_regex` property alongside `key`, `help_category` etc. [See the arg_regex
documentation](Commands#on-arg_regex) for more info.
If you want to overload existing default commands (such as `look` or `get`), just add your new
command with the same key as the old one - it will then replace it. Just remember that you must use
`@reload` to see any changes.
See [Commands](Commands) for many more details and possibilities when defining Commands and using
Cmdsets in various ways.
## Adding the command to specific object types
Adding your Command to the `CharacterCmdSet` is just one easy exapmple. The cmdset system is very
generic. You can create your own cmdsets (let's say in a module `mycmdsets.py`) and add them to
objects as you please (how to control their merging is described in detail in the [Command Set
documentation](Command-Sets)).
```python
# file mygame/commands/mycmdsets.py
#[...]
from commands.command import CmdEcho
from evennia import CmdSet
#[...]
class MyCmdSet(CmdSet):
key = "MyCmdSet"
def at_cmdset_creation(self):
self.add(CmdEcho())
```
Now you just need to add this to an object. To test things (as superuser) you can do
@py self.cmdset.add("mycmdsets.MyCmdSet")
This will add this cmdset (along with its echo command) to yourself so you can test it. Note that
you cannot add a single Command to an object on its own, it must be part of a CommandSet in order to
do so.
The Command you added is not there permanently at this point. If you do a `@reload` the merger will
be gone. You *could* add the `permanent=True` keyword to the `cmdset.add` call. This will however
only make the new merged cmdset permanent on that *single* object. Often you want *all* objects of
this particular class to have this cmdset.
To make sure all new created objects get your new merged set, put the `cmdset.add` call in your
custom [Typeclasses](Typeclasses)' `at_object_creation` method:
```python
# e.g. in mygame/typeclasses/objects.py
from evennia import DefaultObject
class MyObject(DefaultObject):
def at_object_creation(self):
"called when the object is first created"
self.cmdset.add("mycmdset.MyCmdSet", permanent=True)
```
All new objects of this typeclass will now start with this cmdset and it will survive a `@reload`.
*Note:* An important caveat with this is that `at_object_creation` is only called *once*, when the
object is first created. This means that if you already have existing objects in your databases
using that typeclass, they will not have been initiated the same way. There are many ways to update
them; since it's a one-time update you can usually just simply loop through them. As superuser, try
the following:
@py from typeclasses.objects import MyObject; [o.cmdset.add("mycmdset.MyCmdSet") for o in
MyObject.objects.all()]
This goes through all objects in your database having the right typeclass, adding the new cmdset to
each. The good news is that you only have to do this if you want to post-add *cmdsets*. If you just
want to add a new *command*, you can simply add that command to the cmdset's `at_cmdset_creation`
and `@reload` to make the Command immediately available.
## Change where Evennia looks for command sets
Evennia uses settings variables to know where to look for its default command sets. These are
normally not changed unless you want to re-organize your game folder in some way. For example, the
default character cmdset defaults to being defined as
CMDSET_CHARACTER="commands.default_cmdset.CharacterCmdSet"
See `evennia/settings_default.py` for the other settings.

View file

@ -0,0 +1,109 @@
# Adding Object Typeclass Tutorial
Evennia comes with a few very basic classes of in-game entities:
DefaultObject
|
DefaultCharacter
DefaultRoom
DefaultExit
DefaultChannel
When you create a new Evennia game (with for example `evennia --init mygame`) Evennia will
automatically create empty child classes `Object`, `Character`, `Room` and `Exit` respectively. They
are found `mygame/typeclasses/objects.py`, `mygame/typeclasses/rooms.py` etc.
> Technically these are all [Typeclassed](Typeclasses), which can be ignored for now. In
> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably
> [Channels](Communications), [Accounts](Accounts) and [Scripts](Scripts). We don't cover those in
> this tutorial.
For your own game you will most likely want to expand on these very simple beginnings. It's normal
to want your Characters to have various attributes, for example. Maybe Rooms should hold extra
information or even *all* Objects in your game should have properties not included in basic Evennia.
## Change Default Rooms, Exits, Character Typeclass
This is the simplest case.
The default build commands of a new Evennia game is set up to use the `Room`, `Exit` and `Character`
classes found in the same-named modules under `mygame/typeclasses/`. By default these are empty and
just implements the default parents from the Evennia library (`DefaultRoom`etc). Just add the
changes you want to these classes and run `@reload` to add your new functionality.
## Create a new type of object
Say you want to create a new "Heavy" object-type that characters should not have the ability to pick
up.
1. Edit `mygame/typeclasses/objects.py` (you could also create a new module there, named something
like `heavy.py`, that's up to how you want to organize things).
1. Create a new class inheriting at any distance from `DefaultObject`. It could look something like
this:
```python
# end of file mygame/typeclasses/objects.py
from evennia import DefaultObject
class Heavy(DefaultObject):
"Heavy object"
def at_object_creation(self):
"Called whenever a new object is created"
# lock the object down by default
self.locks.add("get:false()")
# the default "get" command looks for this Attribute in order
# to return a customized error message (we just happen to know
# this, you'd have to look at the code of the 'get' command to
# find out).
self.db.get_err_msg = "This is too heavy to pick up."
```
1. Once you are done, log into the game with a build-capable account and do `@create/drop
rock:objects.Heavy` to drop a new heavy "rock" object in your location. Next try to pick it up
(`@quell` yourself first if you are a superuser). If you get errors, look at your log files where
you will find the traceback. The most common error is that you have some sort of syntax error in
your class.
Note that the [Locks](Locks) and [Attribute](Attributes) which are set in the typeclass could just
as well have been set using commands in-game, so this is a *very* simple example.
## Storing data on initialization
The `at_object_creation` is only called once, when the object is first created. This makes it ideal
for database-bound things like [Attributes](Attributes). But sometimes you want to create temporary
properties (things that are not to be stored in the database but still always exist every time the
object is created). Such properties can be initialized in the `at_init` method on the object.
`at_init` is called every time the object is loaded into memory.
> Note: It's usually pointless and wasteful to assign database data in `at_init`, since this will
> hit the database with the same value over and over. Put those in `at_object_creation` instead.
You are wise to use `ndb` (non-database Attributes) to store these non-persistent properties, since
ndb-properties are protected against being cached out in various ways and also allows you to list
them using various in-game tools:
```python
def at_init(self):
self.ndb.counter = 0
self.ndb.mylist = []
```
> Note: As mentioned in the [Typeclasses](Typeclasses) documentation, `at_init` replaces the use of
> the standard `__init__` method of typeclasses due to how the latter may be called in situations
> other than you'd expect. So use `at_init` where you would normally use `__init__`.
## Updating existing objects
If you already have some `Heavy` objects created and you add a new `Attribute` in
`at_object_creation`, you will find that those existing objects will not have this Attribute. This
is not so strange, since `at_object_creation` is only called once, it will not be called again just
because you update it. You need to update existing objects manually.
If the number of objects is limited, you can use `@typeclass/force/reload objectname` to force a
re-load of the `at_object_creation` method (only) on the object. This case is common enough that
there is an alias `@update objectname` you can use to get the same effect. If there are multiple
objects you can use `@py` to loop over the objects you need:
```
@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()]
```

View file

@ -0,0 +1,274 @@
# Building Quickstart
The [default command](Default-Command-Help) definitions coming with Evennia
follows a style [similar](Using-MUX-as-a-Standard) to that of MUX, so the
commands should be familiar if you used any such code bases before.
> Throughout the larger documentation you may come across commands prefixed
> with `@`. This is just an optional marker used in some places to make a
> command stand out. Evennia defaults to ignoring the use of `@` in front of
> your command (so entering `dig` is the same as entering `@dig`).
The default commands have the following style (where `[...]` marks optional parts):
command[/switch/switch...] [arguments ...]
A _switch_ is a special, optional flag to the command to make it behave differently. It is always
put directly after the command name, and begins with a forward slash (`/`). The _arguments_ are one
or more inputs to the commands. It's common to use an equal sign (`=`) when assigning something to
an object.
Below are some examples of commands you can try when logged in to the game. Use `help <command>` for
learning more about each command and their detailed options.
## Stepping Down From Godhood
If you just installed Evennia, your very first player account is called user #1, also known as the
_superuser_ or _god user_. This user is very powerful, so powerful that it will override many game
restrictions such as locks. This can be useful, but it also hides some functionality that you might
want to test.
To temporarily step down from your superuser position you can use the `quell` command in-game:
quell
This will make you start using the permission of your current character's level instead of your
superuser level. If you didn't change any settings your game Character should have an _Developer_
level permission - high as can be without bypassing locks like the superuser does. This will work
fine for the examples on this page. Use `unquell` to get back to superuser status again afterwards.
## Creating an Object
Basic objects can be anything -- swords, flowers and non-player characters. They are created using
the `create` command:
create box
This created a new 'box' (of the default object type) in your inventory. Use the command `inventory`
(or `i`) to see it. Now, 'box' is a rather short name, let's rename it and tack on a few aliases.
name box = very large box;box;very;crate
We now renamed the box to _very large box_ (and this is what we will see when looking at it), but we
will also recognize it by any of the other names we give - like _crate_ or simply _box_ as before.
We could have given these aliases directly after the name in the `create` command, this is true for
all creation commands - you can always tag on a list of `;`-separated aliases to the name of your
new object. If you had wanted to not change the name itself, but to only add aliases, you could have
used the `alias` command.
We are currently carrying the box. Let's drop it (there is also a short cut to create and drop in
one go by using the `/drop` switch, for example `create/drop box`).
drop box
Hey presto - there it is on the ground, in all its normality.
examine box
This will show some technical details about the box object. For now we will ignore what this
information means.
Try to `look` at the box to see the (default) description.
look box
You see nothing special.
The description you get is not very exciting. Let's add some flavor.
describe box = This is a large and very heavy box.
If you try the `get` command we will pick up the box. So far so good, but if we really want this to
be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent
this we need to lock it down. This is done by assigning a _Lock_ to it. Make sure the box was
dropped in the room, then try this:
lock box = get:false()
Locks represent a rather [big topic](Locks), but for now that will do what we want. This will lock
the box so noone can lift it. The exception is superusers, they override all locks and will pick it
up anyway. Make sure you are quelling your superuser powers and try to get the box now:
> get box
You can't get that.
Think thís default error message looks dull? The `get` command looks for an [Attribute](Attributes)
named `get_err_msg` for returning a nicer error message (we just happen to know this, you would need
to peek into the
[code](https://github.com/evennia/evennia/blob/master/evennia/commands/default/general.py#L235) for
the `get` command to find out.). You set attributes using the `set` command:
set box/get_err_msg = It's way too heavy for you to lift.
Try to get it now and you should see a nicer error message echoed back to you. To see what this
message string is in the future, you can use 'examine.'
examine box/get_err_msg
Examine will return the value of attributes, including color codes. `examine here/desc` would return
the raw description of your current room (including color codes), so that you can copy-and-paste to
set its description to something else.
You create new Commands (or modify existing ones) in Python outside the game. See the [Adding
Commands tutorial](Adding-Command-Tutorial) for help with creating your first own Command.
## Get a Personality
[Scripts](Scripts) are powerful out-of-character objects useful for many "under the hood" things.
One of their optional abilities is to do things on a timer. To try out a first script, let's put one
on ourselves. There is an example script in `evennia/contrib/tutorial_examples/bodyfunctions.py`
that is called `BodyFunctions`. To add this to us we will use the `script` command:
script self = tutorial_examples.bodyfunctions.BodyFunctions
(note that you don't have to give the full path as long as you are pointing to a place inside the
`contrib` directory, it's one of the places Evennia looks for Scripts). Wait a while and you will
notice yourself starting making random observations.
script self
This will show details about scripts on yourself (also `examine` works). You will see how long it is
until it "fires" next. Don't be alarmed if nothing happens when the countdown reaches zero - this
particular script has a randomizer to determine if it will say something or not. So you will not see
output every time it fires.
When you are tired of your character's "insights", kill the script with
script/stop self = tutorial_examples.bodyfunctions.BodyFunctions
You create your own scripts in Python, outside the game; the path you give to `script` is literally
the Python path to your script file. The [Scripts](Scripts) page explains more details.
## Pushing Your Buttons
If we get back to the box we made, there is only so much fun you can do with it at this point. It's
just a dumb generic object. If you renamed it to `stone` and changed its description noone would be
the wiser. However, with the combined use of custom [Typeclasses](Typeclasses), [Scripts](Scripts)
and object-based [Commands](Commands), you could expand it and other items to be as unique, complex
and interactive as you want.
Let's take an example. So far we have only created objects that use the default object typeclass
named simply `Object`. Let's create an object that is a little more interesting. Under
`evennia/contrib/tutorial_examples` there is a module `red_button.py`. It contains the enigmatic
`RedButton` typeclass.
Let's make us one of _those_!
create/drop button:tutorial_examples.red_button.RedButton
We import the RedButton python class the same way you would import it in Python except Evennia makes
sure to look in`evennia/contrib/` so you don't have to write the full path every time. There you go
- one red button.
The RedButton is an example object intended to show off a few of Evennia's features. You will find
that the [Typeclass](Typeclasses) and [Commands](Commands) controlling it are inside
`evennia/contrib/tutorial_examples/`.
If you wait for a while (make sure you dropped it!) the button will blink invitingly. Why don't you
try to push it ...? Surely a big red button is meant to be pushed. You know you want to.
## Making Yourself a House
The main command for shaping the game world is `dig`. For example, if you are standing in Limbo you
can dig a route to your new house location like this:
dig house = large red door;door;in,to the outside;out
This will create a new room named 'house'. Spaces at the start/end of names and aliases are ignored
so you could put more air if you wanted. This call will directly create an exit from your current
location named 'large red door' and a corresponding exit named 'to the outside' in the house room
leading back to Limbo. We also define a few aliases to those exits, so people don't have to write
the full thing all the time.
If you wanted to use normal compass directions (north, west, southwest etc), you could do that with
`dig` too. But Evennia also has a limited version of `dig` that helps for compass directions (and
also up/down and in/out). It's called `tunnel`:
tunnel sw = cliff
This will create a new room "cliff" with an exit "southwest" leading there and a path "northeast"
leading back from the cliff to your current location.
You can create new exits from where you are using the `open` command:
open north;n = house
This opens an exit `north` (with an alias `n`) to the previously created room `house`.
If you have many rooms named `house` you will get a list of matches and have to select which one you
want to link to. You can also give its database (#dbref) number, which is unique to every object.
This can be found with the `examine` command or by looking at the latest constructions with
`objects`.
Follow the north exit to your 'house' or `teleport` to it:
north
or:
teleport house
To manually open an exit back to Limbo (if you didn't do so with the `dig` command):
open door = limbo
(or give limbo's dbref which is #2)
## Reshuffling the World
You can find things using the `find` command. Assuming you are back at `Limbo`, let's teleport the
_large box to our house_.
> teleport box = house
very large box is leaving Limbo, heading for house.
Teleported very large box -> house.
We can still find the box by using find:
> find box
One Match(#1-#8):
very large box(#8) - src.objects.objects.Object
Knowing the `#dbref` of the box (#8 in this example), you can grab the box and get it back here
without actually yourself going to `house` first:
teleport #8 = here
(You can usually use `here` to refer to your current location. To refer to yourself you can use
`self` or `me`). The box should now be back in Limbo with you.
We are getting tired of the box. Let's destroy it.
destroy box
You can destroy many objects in one go by giving a comma-separated list of objects (or their
#dbrefs, if they are not in the same location) to the command.
## Adding a Help Entry
An important part of building is keeping the help files updated. You can add, delete and append to
existing help entries using the `sethelp` command.
sethelp/add MyTopic = This help topic is about ...
## Adding a World
After this brief introduction to building you may be ready to see a more fleshed-out example.
Evennia comes with a tutorial world for you to explore.
First you need to switch back to _superuser_ by using the `unquell` command. Next, place yourself in
`Limbo` and run the following command:
batchcommand tutorial_world.build
This will take a while (be patient and don't re-run the command). You will see all the commands used
to build the world scroll by as the world is built for you.
You will end up with a new exit from Limbo named _tutorial_. Apart from being a little solo-
adventure in its own right, the tutorial world is a good source for learning Evennia building (and
coding).
Read [the batch
file](https://github.com/evennia/evennia/blob/master/evennia/contrib/tutorial_world/build.ev) to see
exactly how it's built, step by step. See also more info about the tutorial world [here](Tutorial-
World-Introduction).

View file

@ -0,0 +1,104 @@
# Coding Introduction
Evennia allows for a lot of freedom when designing your game - but to code efficiently you still
need to adopt some best practices as well as find a good place to start to learn.
Here are some pointers to get you going.
### Python
Evennia is developed using Python. Even if you are more of a designer than a coder, it is wise to
learn how to read and understand basic Python code. If you are new to Python, or need a refresher,
take a look at our two-part [Python introduction](Python-basic-introduction).
### Explore Evennia interactively
When new to Evennia it can be hard to find things or figure out what is available. Evennia offers a
special interactive python shell that allows you to experiment and try out things. It's recommended
to use [ipython](http://ipython.org/) for this since the vanilla python prompt is very limited. Here
are some simple commands to get started:
# [open a new console/terminal]
# [activate your evennia virtualenv in this console/terminal]
pip install ipython # [only needed the first time]
cd mygame
evennia shell
This will open an Evennia-aware python shell (using ipython). From within this shell, try
import evennia
evennia.<TAB>
That is, enter `evennia.` and press the `<TAB>` key. This will show you all the resources made
available at the top level of Evennia's "flat API". See the [flat API](Evennia-API) page for more
info on how to explore it efficiently.
You can complement your exploration by peeking at the sections of the much more detailed [Developer
Central](Developer-Central). The [Tutorials](Tutorials) section also contains a growing collection
of system- or implementation-specific help.
### Use a python syntax checker
Evennia works by importing your own modules and running them as part of the server. Whereas Evennia
should just gracefully tell you what errors it finds, it can nevertheless be a good idea for you to
check your code for simple syntax errors *before* you load it into the running server. There are
many python syntax checkers out there. A fast and easy one is
[pyflakes](https://pypi.python.org/pypi/pyflakes), a more verbose one is
[pylint](http://www.pylint.org/). You can also check so that your code looks up to snuff using
[pep8](https://pypi.python.org/pypi/pep8). Even with a syntax checker you will not be able to catch
every possible problem - some bugs or problems will only appear when you actually run the code. But
using such a checker can be a good start to weed out the simple problems.
### Plan before you code
Before you start coding away at your dream game, take a look at our [Game Planning](Game-Planning)
page. It might hopefully help you avoid some common pitfalls and time sinks.
### Code in your game folder, not in the evennia/ repository
As part of the Evennia setup you will create a game folder to host your game code. This is your
home. You should *never* need to modify anything in the `evennia` library (anything you download
from us, really). You import useful functionality from here and if you see code you like, copy&paste
it out into your game folder and edit it there.
If you find that Evennia doesn't support some functionality you need, make a [Feature
Request](feature-request) about it. Same goes for [bugs][bug]. If you add features or fix bugs
yourself, please consider [Contributing](Contributing) your changes upstream!
### Learn to read tracebacks
Python is very good at reporting when and where things go wrong. A *traceback* shows everything you
need to know about crashing code. The text can be pretty long, but you usually are only interested
in the last bit, where it says what the error is and at which module and line number it happened -
armed with this info you can resolve most problems.
Evennia will usually not show the full traceback in-game though. Instead the server outputs errors
to the terminal/console from which you started Evennia in the first place. If you want more to show
in-game you can add `IN_GAME_ERRORS = True` to your settings file. This will echo most (but not all)
tracebacks both in-game as well as to the terminal/console. This is a potential security problem
though, so don't keep this active when your game goes into production.
> A common confusing error is finding that objects in-game are suddenly of the type `DefaultObject`
rather than your custom typeclass. This happens when you introduce a critical Syntax error to the
module holding your custom class. Since such a module is not valid Python, Evennia can't load it at
all. Instead of crashing, Evennia will then print the full traceback to the terminal/console and
temporarily fall back to the safe `DefaultObject` until you fix the problem and reload.
### Docs are here to help you
Some people find reading documentation extremely dull and shun it out of principle. That's your
call, but reading docs really *does* help you, promise! Evennia's documentation is pretty thorough
and knowing what is possible can often give you a lot of new cool game ideas. That said, if you
can't find the answer in the docs, don't be shy to ask questions! The [discussion
group](https://sites.google.com/site/evenniaserver/discussions) and the [irc
chat](http://webchat.freenode.net/?channels=evennia) are also there for you.
### The most important point
And finally, of course, have fun!
[feature-request]:
(https://github.com/evennia/evennia/issues/new?title=Feature+Request%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Description+of+the+suggested+feature+and+how+it+is+supposed+to+work+for+the+admin%2fend+user%3a%0D%0A%0D%0A%0D%0A%23%23%23%23+A+list+of+arguments+for+why+you+think+this+new+feature+should+be+included+in+Evennia%3a%0D%0A%0D%0A1.%0D%0A2.%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+requirements+or+ideas+on+implementation%3a%0D%0A%0D%0A
[bug]:
https://github.com/evennia/evennia/issues/new?title=Bug%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Steps+to+reproduce+the+issue%3a%0D%0A%0D%0A1.+%0D%0A2.+%0D%0A3.+%0D%0A%0D%0A%23%23%23%23+What+I+expect+to+see+and+what+I+actually+see+%28tracebacks%2c+error+messages+etc%29%3a%0D%0A%0D%0A%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+Evennia+revision%2frepo%2fbranch%2c+operating+system+and+ideas+for+how+to+solve%3a%0D%0A%0D%0A

View file

@ -0,0 +1,120 @@
# Execute Python Code
The `@py` command supplied with the default command set of Evennia allows you to execute Python
commands directly from inside the game. An alias to `@py` is simply "`!`". *Access to the `@py`
command should be severely restricted*. This is no joke - being able to execute arbitrary Python
code on the server is not something you should entrust to just anybody.
@py 1+2
<<< 3
## Available variables
A few local variables are made available when running `@py`. These offer entry into the running
system.
- **self** / **me** - the calling object (i.e. you)
- **here** - the current caller's location
- **obj** - a dummy [Object](Objects) instance
- **evennia** - Evennia's [flat API](Evennia-API) - through this you can access all of Evennia.
For accessing other objects in the same room you need to use `self.search(name)`. For objects in
other locations, use one of the `evennia.search_*` methods. See [below](Execute-Python-Code#finding-
objects).
## Returning output
This is an example where we import and test one of Evennia's utilities found in
`src/utils/utils.py`, but also accessible through `ev.utils`:
@py from ev import utils; utils.time_format(33333)
<<< Done.
Note that we didn't get any return value, all we where told is that the code finished executing
without error. This is often the case in more complex pieces of code which has no single obvious
return value. To see the output from the `time_format()` function we need to tell the system to
echo it to us explicitly with `self.msg()`.
@py from ev import utils; self.msg(str(utils.time_format(33333)))
09:15
<<< Done.
> Warning: When using the `msg` function wrap our argument in `str()` to convert it into a string
above. This is not strictly necessary for most types of data (Evennia will usually convert to a
string behind the scenes for you). But for *lists* and *tuples* you will be confused by the output
if you don't wrap them in `str()`: only the first item of the iterable will be returned. This is
because doing `msg(text)` is actually just a convenience shortcut; the full argument that `msg`
accepts is something called an *outputfunc* on the form `(cmdname, (args), {kwargs})` (see [the
message path](Messagepath) for more info). Sending a list/tuple confuses Evennia to think you are
sending such a structure. Converting it to a string however makes it clear it should just be
displayed as-is.
If you were to use Python's standard `print`, you will see the result in your current `stdout` (your
terminal by default, otherwise your log file).
## Finding objects
A common use for `@py` is to explore objects in the database, for debugging and performing specific
operations that are not covered by a particular command.
Locating an object is best done using `self.search()`:
@py self.search("red_ball")
<<< Ball
@py self.search("red_ball").db.color = "red"
<<< Done.
@py self.search("red_ball").db.color
<<< red
`self.search()` is by far the most used case, but you can also search other database tables for
other Evennia entities like scripts or configuration entities. To do this you can use the generic
search entries found in `ev.search_*`.
@py evennia.search_script("sys_game_time")
<<< [<src.utils.gametime.GameTime object at 0x852be2c>]
(Note that since this becomes a simple statement, we don't have to wrap it in `self.msg()` to get
the output). You can also use the database model managers directly (accessible through the `objects`
properties of database models or as `evennia.managers.*`). This is a bit more flexible since it
gives you access to the full range of database search methods defined in each manager.
@py evennia.managers.scripts.script_search("sys_game_time")
<<< [<src.utils.gametime.GameTime object at 0x852be2c>]
The managers are useful for all sorts of database studies.
@py ev.managers.configvalues.all()
<<< [<ConfigValue: default_home]>, <ConfigValue:site_name>, ...]
## Testing code outside the game
`@py` has the advantage of operating inside a running server (sharing the same process), where you
can test things in real time. Much of this *can* be done from the outside too though.
In a terminal, cd to the top of your game directory (this bit is important since we need access to
your config file) and run
evennia shell
Your default Python interpreter will start up, configured to be able to work with and import all
modules of your Evennia installation. From here you can explore the database and test-run individual
modules as desired.
It's recommended that you get a more fully featured Python interpreter like
[iPython](http://ipython.scipy.org/moin/). If you use a virtual environment, you can just get it
with `pip install ipython`. IPython allows you to better work over several lines, and also has a lot
of other editing features, such as tab-completion and `__doc__`-string reading.
$ evennia shell
IPython 0.10 -- An enhanced Interactive Python
...
In [1]: import evennia
In [2]: evennia.managers.objects.all()
Out[3]: [<ObjectDB: Harry>, <ObjectDB: Limbo>, ...]
See the page about the [Evennia-API](Evennia-API) for more things to explore.

View file

@ -0,0 +1,292 @@
# First Steps Coding
This section gives a brief step-by-step introduction on how to set up Evennia for the first time so
you can modify and overload the defaults easily. You should only need to do these steps once. It
also walks through you making your first few tweaks.
Before continuing, make sure you have Evennia installed and running by following the [Getting
Started](Getting-Started) instructions. You should have initialized a new game folder with the
`evennia --init foldername` command. We will in the following assume this folder is called
"mygame".
It might be a good idea to eye through the brief [Coding Introduction](Coding-Introduction) too
(especially the recommendations in the section about the evennia "flat" API and about using `evennia
shell` will help you here and in the future).
To follow this tutorial you also need to know the basics of operating your computer's
terminal/command line. You also need to have a text editor to edit and create source text files.
There are plenty of online tutorials on how to use the terminal and plenty of good free text
editors. We will assume these things are already familiar to you henceforth.
## Your First Changes
Below are some first things to try with your new custom modules. You can test these to get a feel
for the system. See also [Tutorials](Tutorials) for more step-by-step help and special cases.
### Tweak Default Character
We will add some simple rpg attributes to our default Character. In the next section we will follow
up with a new command to view those attributes.
1. Edit `mygame/typeclasses/characters.py` and modify the `Character` class. The
`at_object_creation` method also exists on the `DefaultCharacter` parent and will overload it. The
`get_abilities` method is unique to our version of `Character`.
```python
class Character(DefaultCharacter):
# [...]
def at_object_creation(self):
"""
Called only at initial creation. This is a rather silly
example since ability scores should vary from Character to
Character and is usually set during some character
generation step instead.
"""
#set persistent attributes
self.db.strength = 5
self.db.agility = 4
self.db.magic = 2
def get_abilities(self):
"""
Simple access method to return ability
scores as a tuple (str,agi,mag)
"""
return self.db.strength, self.db.agility, self.db.magic
```
1. [Reload](Start-Stop-Reload) the server (you will still be connected to the game after doing
this). Note that if you examine *yourself* you will *not* see any new Attributes appear yet. Read
the next section to understand why.
#### Updating Yourself
It's important to note that the new [Attributes](Attributes) we added above will only be stored on
*newly* created characters. The reason for this is simple: The `at_object_creation` method, where we
added those Attributes, is per definition only called when the object is *first created*, then never
again. This is usually a good thing since those Attributes may change over time - calling that hook
would reset them back to start values. But it also means that your existing character doesn't have
them yet. You can see this by calling the `get_abilities` hook on yourself at this point:
```
# (you have to be superuser to use @py)
@py self.get_abilities()
<<< (None, None, None)
```
This is easily remedied.
```
@update self
```
This will (only) re-run `at_object_creation` on yourself. You should henceforth be able to get the
abilities successfully:
```
@py self.get_abilities()
<<< (5, 4, 2)
```
This is something to keep in mind if you start building your world before your code is stable -
startup-hooks will not (and should not) automatically run on *existing* objects - you have to update
your existing objects manually. Luckily this is a one-time thing and pretty simple to do. If the
typeclass you want to update is in `typeclasses.myclass.MyClass`, you can do the following (e.g.
from `evennia shell`):
```python
from typeclasses.myclass import MyClass
# loop over all MyClass instances in the database
# and call .swap_typeclass on them
for obj in MyClass.objects.all():
obj.swap_typeclass(MyClass, run_start_hooks="at_object_creation")
```
Using `swap_typeclass` to the same typeclass we already have will re-run the creation hooks (this is
what the `@update` command does under the hood). From in-game you can do the same with `@py`:
```
@py typeclasses.myclass import MyClass;[obj.swap_typeclass(MyClass) for obj in
MyClass.objects.all()]
```
See the [Object Typeclass tutorial](Adding-Object-Typeclass-Tutorial) for more help and the
[Typeclasses](Typeclasses) and [Attributes](Attributes) page for detailed documentation about
Typeclasses and Attributes.
#### Troubleshooting: Updating Yourself
One may experience errors for a number of reasons. Common beginner errors are spelling mistakes,
wrong indentations or code omissions leading to a `SyntaxError`. Let's say you leave out a colon
from the end of a class function like so: ```def at_object_creation(self)```. The client will reload
without issue. *However*, if you look at the terminal/console (i.e. not in-game), you will see
Evennia complaining (this is called a *traceback*):
```
Traceback (most recent call last):
File "C:\mygame\typeclasses\characters.py", line 33
def at_object_creation(self)
^
SyntaxError: invalid syntax
```
Evennia will still be restarting and following the tutorial, doing `@py self.get_abilities()` will
return the right response `(None, None, None)`. But when attempting to `@typeclass/force self` you
will get this response:
```python
AttributeError: 'DefaultObject' object has no attribute 'get_abilities'
```
The full error will show in the terminal/console but this is confusing since you did add
`get_abilities` before. Note however what the error says - you (`self`) should be a `Character` but
the error talks about `DefaultObject`. What has happened is that due to your unhandled `SyntaxError`
earlier, Evennia could not load the `character.py` module at all (it's not valid Python). Rather
than crashing, Evennia handles this by temporarily falling back to a safe default - `DefaultObject`
- in order to keep your MUD running. Fix the original `SyntaxError` and reload the server. Evennia
will then be able to use your modified `Character` class again and things should work.
> Note: Learning how to interpret an error traceback is a critical skill for anyone learning Python.
Full tracebacks will appear in the terminal/Console you started Evennia from. The traceback text can
sometimes be quite long, but you are usually just looking for the last few lines: The description of
the error and the filename + line number for where the error occurred. In the example above, we see
it's a `SyntaxError` happening at `line 33` of `mygame\typeclasses\characters.py`. In this case it
even points out *where* on the line it encountered the error (the missing colon). Learn to read
tracebacks and you'll be able to resolve the vast majority of common errors easily.
### Add a New Default Command
The `@py` command used above is only available to privileged users. We want any player to be able to
see their stats. Let's add a new [command](Commands) to list the abilities we added in the previous
section.
1. Open `mygame/commands/command.py`. You could in principle put your command anywhere but this
module has all the imports already set up along with some useful documentation. Make a new class at
the bottom of this file:
```python
class CmdAbilities(Command):
"""
List abilities
Usage:
abilities
Displays a list of your current ability values.
"""
key = "abilities"
aliases = ["abi"]
lock = "cmd:all()"
help_category = "General"
def func(self):
"implements the actual functionality"
str, agi, mag = self.caller.get_abilities()
string = "STR: %s, AGI: %s, MAG: %s" % (str, agi, mag)
self.caller.msg(string)
```
1. Next you edit `mygame/commands/default_cmdsets.py` and add a new import to it near the top:
```python
from commands.command import CmdAbilities
```
1. In the `CharacterCmdSet` class, add the following near the bottom (it says where):
```python
self.add(CmdAbilities())
```
1. [Reload](Start-Stop-Reload) the server (noone will be disconnected by doing this).
You (and anyone else) should now be able to use `abilities` (or its alias `abi`) as part of your
normal commands in-game:
```
abilities
STR: 5, AGI: 4, MAG: 2
```
See the [Adding a Command tutorial](Adding-Command-Tutorial) for more examples and the
[Commands](Commands) section for detailed documentation about the Command system.
### Make a New Type of Object
Let's test to make a new type of object. This example is an "wise stone" object that returns some
random comment when you look at it, like this:
> look stone
A very wise stone
This is a very wise old stone.
It grumbles and says: 'The world is like a rock of chocolate.'
1. Create a new module in `mygame/typeclasses/`. Name it `wiseobject.py` for this example.
1. In the module import the base `Object` (`typeclasses.objects.Object`). This is empty by default,
meaning it is just a proxy for the default `evennia.DefaultObject`.
1. Make a new class in your module inheriting from `Object`. Overload hooks on it to add new
functionality. Here is an example of how the file could look:
```python
from random import choice
from typeclasses.objects import Object
class WiseObject(Object):
"""
An object speaking when someone looks at it. We
assume it looks like a stone in this example.
"""
def at_object_creation(self):
"Called when object is first created"
self.db.wise_texts = \
["Stones have feelings too.",
"To live like a stone is to not have lived at all.",
"The world is like a rock of chocolate."]
def return_appearance(self, looker):
"""
Called by the look command. We want to return
a wisdom when we get looked at.
"""
# first get the base string from the
# parent's return_appearance.
string = super().return_appearance(looker)
wisewords = "\n\nIt grumbles and says: '%s'"
wisewords = wisewords % choice(self.db.wise_texts)
return string + wisewords
```
1. Check your code for bugs. Tracebacks will appear on your command line or log. If you have a grave
Syntax Error in your code, the source file itself will fail to load which can cause issues with the
entire cmdset. If so, fix your bug and [reload the server from the command line](Start-Stop-Reload)
(noone will be disconnected by doing this).
1. Use `@create/drop stone:wiseobject.WiseObject` to create a talkative stone. If the `@create`
command spits out a warning or cannot find the typeclass (it will tell you which paths it searched),
re-check your code for bugs and that you gave the correct path. The `@create` command starts looking
for Typeclasses in `mygame/typeclasses/`.
1. Use `look stone` to test. You will see the default description ("You see nothing special")
followed by a random message of stony wisdom. Use `@desc stone = This is a wise old stone.` to make
it look nicer. See the [Builder Docs](Builder-Docs) for more information.
Note that `at_object_creation` is only called once, when the stone is first created. If you make
changes to this method later, already existing stones will not see those changes. As with the
`Character` example above you can use `@typeclass/force` to tell the stone to re-run its
initialization.
The `at_object_creation` is a special case though. Changing most other aspects of the typeclass does
*not* require manual updating like this - you just need to `@reload` to have all changes applied
automatically to all existing objects.
## Where to Go From Here?
There are more [Tutorials](Tutorials), including one for building a [whole little MUSH-like
game](Tutorial-for-basic-MUSH-like-game) - that is instructive also if you have no interest in
MUSHes per se. A good idea is to also get onto the [IRC
chat](http://webchat.freenode.net/?channels=evennia) and the [mailing
list](https://groups.google.com/forum/#!forum/evennia) to get in touch with the community and other
developers.

View file

@ -0,0 +1,748 @@
# Parsing command arguments, theory and best practices
This tutorial will elaborate on the many ways one can parse command arguments. The first step after
[adding a command](Adding-Command-Tutorial) usually is to parse its arguments. There are lots of
ways to do it, but some are indeed better than others and this tutorial will try to present them.
If you're a Python beginner, this tutorial might help you a lot. If you're already familiar with
Python syntax, this tutorial might still contain useful information. There are still a lot of
things I find in the standard library that come as a surprise, though they were there all along.
This might be true for others.
In this tutorial we will:
- Parse arguments with numbers.
- Parse arguments with delimiters.
- Take a look at optional arguments.
- Parse argument containing object names.
## What are command arguments?
I'm going to talk about command arguments and parsing a lot in this tutorial. So let's be sure we
talk about the same thing before going any further:
> A command is an Evennia object that handles specific user input.
For instance, the default `look` is a command. After having created your Evennia game, and
connected to it, you should be able to type `look` to see what's around. In this context, `look` is
a command.
> Command arguments are additional text passed after the command.
Following the same example, you can type `look self` to look at yourself. In this context, `self`
is the text specified after `look`. `" self"` is the argument to the `look` command.
Part of our task as a game developer is to connect user inputs (mostly commands) with actions in the
game. And most of the time, entering commands is not enough, we have to rely on arguments for
specifying actions with more accuracy.
Take the `say` command. If you couldn't specify what to say as a command argument (`say hello!`),
you would have trouble communicating with others in the game. One would need to create a different
command for every kind of word or sentence, which is, of course, not practical.
Last thing: what is parsing?
> In our case, parsing is the process by which we convert command arguments into something we can
work with.
We don't usually use the command argument as is (which is just text, of type `str` in Python). We
need to extract useful information. We might want to ask the user for a number, or the name of
another character present in the same room. We're going to see how to do all that now.
## Working with strings
In object terms, when you write a command in Evennia (when you write the Python class), the
arguments are stored in the `args` attribute. Which is to say, inside your `func` method, you can
access the command arguments in `self.args`.
### self.args
To begin with, look at this example:
```python
class CmdTest(Command):
"""
Test command.
Syntax:
test [argument]
Enter any argument after test.
"""
key = "test"
def func(self):
self.msg(f"You have entered: {self.args}.")
```
If you add this command and test it, you will receive exactly what you have entered without any
parsing:
```
> test Whatever
You have entered: Whatever.
> test
You have entered: .
```
> The lines starting with `>` indicate what you enter into your client. The other lines are what
you receive from the game server.
Notice two things here:
1. The left space between our command key ("test", here) and our command argument is not removed.
That's why there are two spaces in our output at line 2. Try entering something like "testok".
2. Even if you don't enter command arguments, the command will still be called with an empty string
in `self.args`.
Perhaps a slight modification to our code would be appropriate to see what's happening. We will
force Python to display the command arguments as a debug string using a little shortcut.
```python
class CmdTest(Command):
"""
Test command.
Syntax:
test [argument]
Enter any argument after test.
"""
key = "test"
def func(self):
self.msg(f"You have entered: {self.args!r}.")
```
The only line we have changed is the last one, and we have added `!r` between our braces to tell
Python to print the debug version of the argument (the repr-ed version). Let's see the result:
```
> test Whatever
You have entered: ' Whatever'.
> test
You have entered: ''.
> test And something with '?
You have entered: " And something with '?".
```
This displays the string in a way you could see in the Python interpreter. It might be easier to
read... to debug, anyway.
I insist so much on that point because it's crucial: the command argument is just a string (of type
`str`) and we will use this to parse it. What you will see is mostly not Evennia-specific, it's
Python-specific and could be used in any other project where you have the same need.
### Stripping
As you've seen, our command arguments are stored with the space. And the space between the command
and the arguments is often of no importance.
> Why is it ever there?
Evennia will try its best to find a matching command. If the user enters your command key with
arguments (but omits the space), Evennia will still be able to find and call the command. You might
have seen what happened if the user entered `testok`. In this case, `testok` could very well be a
command (Evennia checks for that) but seeing none, and because there's a `test` command, Evennia
calls it with the arguments `"ok"`.
But most of the time, we don't really care about this left space, so you will often see code to
remove it. There are different ways to do it in Python, but a command use case is the `strip`
method on `str` and its cousins, `lstrip` and `rstrip`.
- `strip`: removes one or more characters (either spaces or other characters) from both ends of the
string.
- `lstrip`: same thing but only removes from the left end (left strip) of the string.
- `rstrip`: same thing but only removes from the right end (right strip) of the string.
Some Python examples might help:
```python
>>> ' this is '.strip() # remove spaces by default
'this is'
>>> " What if I'm right? ".lstrip() # strip spaces from the left
"What if I'm right? "
>>> 'Looks good to me...'.strip('.') # removes '.'
'Looks good to me'
>>> '"Now, what is it?"'.strip('"?') # removes '"' and '?' from both ends
'Now, what is it'
```
Usually, since we don't need the space separator, but still want our command to work if there's no
separator, we call `lstrip` on the command arguments:
```python
class CmdTest(Command):
"""
Test command.
Syntax:
test [argument]
Enter any argument after test.
"""
key = "test"
def parse(self):
"""Parse arguments, just strip them."""
self.args = self.args.lstrip()
def func(self):
self.msg(f"You have entered: {self.args!r}.")
```
> We are now beginning to override the command's `parse` method, which is typically useful just for
argument parsing. This method is executed before `func` and so `self.args` in `func()` will contain
our `self.args.lstrip()`.
Let's try it:
```
> test Whatever
You have entered: 'Whatever'.
> test
You have entered: ''.
> test And something with '?
You have entered: "And something with '?".
> test And something with lots of spaces
You have entered: 'And something with lots of spaces'.
```
Spaces at the end of the string are kept, but all spaces at the beginning are removed:
> `strip`, `lstrip` and `rstrip` without arguments will strip spaces, line breaks and other common
separators. You can specify one or more characters as a parameter. If you specify more than one
character, all of them will be stripped from your original string.
### Convert arguments to numbers
As pointed out, `self.args` is a string (of type `str`). What if we want the user to enter a
number?
Let's take a very simple example: creating a command, `roll`, that allows to roll a six-sided die.
The player has to guess the number, specifying the number as argument. To win, the player has to
match the number with the die. Let's see an example:
```
> roll 3
You roll a die. It lands on the number 4.
You played 3, you have lost.
> dice 1
You roll a die. It lands on the number 2.
You played 1, you have lost.
> dice 1
You roll a die. It lands on the number 1.
You played 1, you have won!
```
If that's your first command, it's a good opportunity to try to write it. A command with a simple
and finite role always is a good starting choice. Here's how we could (first) write it... but it
won't work as is, I warn you:
```python
from random import randint
from evennia import Command
class CmdRoll(Command):
"""
Play random, enter a number and try your luck.
Usage:
roll <number>
Enter a valid number as argument. A random die will be rolled and you
will win if you have specified the correct number.
Example:
roll 3
"""
key = "roll"
def parse(self):
"""Convert the argument to a number."""
self.args = self.args.lstrip()
def func(self):
# Roll a random die
figure = randint(1, 6) # return a pseudo-random number between 1 and 6, including both
self.msg(f"You roll a die. It lands on the number {figure}.")
if self.args == figure: # THAT WILL BREAK!
self.msg(f"You played {self.args}, you have won!")
else:
self.msg(f"You played {self.args}, you have lost.")
```
If you try this code, Python will complain that you try to compare a number with a string: `figure`
is a number and `self.args` is a string and can't be compared as-is in Python. Python doesn't do
"implicit converting" as some languages do. By the way, this might be annoying sometimes, and other
times you will be glad it tries to encourage you to be explicit rather than implicit about what to
do. This is an ongoing debate between programmers. Let's move on!
So we need to convert the command argument from a `str` into an `int`. There are a few ways to do
it. But the proper way is to try to convert and deal with the `ValueError` Python exception.
Converting a `str` into an `int` in Python is extremely simple: just use the `int` function, give it
the string and it returns an integer, if it could. If it can't, it will raise `ValueError`. So
we'll need to catch that. However, we also have to indicate to Evennia that, should the number be
invalid, no further parsing should be done. Here's a new attempt at our command with this
converting:
```python
from random import randint
from evennia import Command, InterruptCommand
class CmdRoll(Command):
"""
Play random, enter a number and try your luck.
Usage:
roll <number>
Enter a valid number as argument. A random die will be rolled and you
will win if you have specified the correct number.
Example:
roll 3
"""
key = "roll"
def parse(self):
"""Convert the argument to number if possible."""
args = self.args.lstrip()
# Convert to int if possible
# If not, raise InterruptCommand. Evennia will catch this
# exception and not call the 'func' method.
try:
self.entered = int(args)
except ValueError:
self.msg(f"{args} is not a valid number.")
raise InterruptCommand
def func(self):
# Roll a random die
figure = randint(1, 6) # return a pseudo-random number between 1 and 6, including both
self.msg(f"You roll a die. It lands on the number {figure}.")
if self.entered == figure:
self.msg(f"You played {self.entered}, you have won!")
else:
self.msg(f"You played {self.entered}, you have lost.")
```
Before enjoying the result, let's examine the `parse` method a little more: what it does is try to
convert the entered argument from a `str` to an `int`. This might fail (if a user enters `roll
something`). In such a case, Python raises a `ValueError` exception. We catch it in our
`try/except` block, send a message to the user and raise the `InterruptCommand` exception in
response to tell Evennia to not run `func()`, since we have no valid number to give it.
In the `func` method, instead of using `self.args`, we use `self.entered` which we have defined in
our `parse` method. You can expect that, if `func()` is run, then `self.entered` contains a valid
number.
If you try this command, it will work as expected this time: the number is converted as it should
and compared to the die roll. You might spend some minutes playing this game. Time out!
Something else we could want to address: in our small example, we only want the user to enter a
positive number between 1 and 6. And the user can enter `roll 0` or `roll -8` or `roll 208` for
that matter, the game still works. It might be worth addressing. Again, you could write a
condition to do that, but since we're catching an exception, we might end up with something cleaner
by grouping:
```python
from random import randint
from evennia import Command, InterruptCommand
class CmdRoll(Command):
"""
Play random, enter a number and try your luck.
Usage:
roll <number>
Enter a valid number as argument. A random die will be rolled and you
will win if you have specified the correct number.
Example:
roll 3
"""
key = "roll"
def parse(self):
"""Convert the argument to number if possible."""
args = self.args.lstrip()
# Convert to int if possible
try:
self.entered = int(args)
if not 1 <= self.entered <= 6:
# self.entered is not between 1 and 6 (including both)
raise ValueError
except ValueError:
self.msg(f"{args} is not a valid number.")
raise InterruptCommand
def func(self):
# Roll a random die
figure = randint(1, 6) # return a pseudo-random number between 1 and 6, including both
self.msg(f"You roll a die. It lands on the number {figure}.")
if self.entered == figure:
self.msg(f"You played {self.entered}, you have won!")
else:
self.msg(f"You played {self.entered}, you have lost.")
```
Using grouped exceptions like that makes our code easier to read, but if you feel more comfortable
checking, afterward, that the number the user entered is in the right range, you can do so in a
latter condition.
> Notice that we have updated our `parse` method only in this last attempt, not our `func()` method
which remains the same. This is one goal of separating argument parsing from command processing,
these two actions are best kept isolated.
### Working with several arguments
Often a command expects several arguments. So far, in our example with the "roll" command, we only
expect one argument: a number and just a number. What if we want the user to specify several
numbers? First the number of dice to roll, then the guess?
> You won't win often if you roll 5 dice but that's for the example.
So we would like to interpret a command like this:
> roll 3 12
(To be understood: roll 3 dice, my guess is the total number will be 12.)
What we need is to cut our command argument, which is a `str`, break it at the space (we use the
space as a delimiter). Python provides the `str.split` method which we'll use. Again, here are
some examples from the Python interpreter:
>>> args = "3 12"
>>> args.split(" ")
['3', '12']
>>> args = "a command with several arguments"
>>> args.split(" ")
['a', 'command', 'with', 'several', 'arguments']
>>>
As you can see, `str.split` will "convert" our strings into a list of strings. The specified
argument (`" "` in our case) is used as delimiter. So Python browses our original string. When it
sees a delimiter, it takes whatever is before this delimiter and append it to a list.
The point here is that `str.split` will be used to split our argument. But, as you can see from the
above output, we can never be sure of the length of the list at this point:
>>> args = "something"
>>> args.split(" ")
['something']
>>> args = ""
>>> args.split(" ")
['']
>>>
Again we could use a condition to check the number of split arguments, but Python offers a better
approach, making use of its exception mechanism. We'll give a second argument to `str.split`, the
maximum number of splits to do. Let's see an example, this feature might be confusing at first
glance:
>>> args = "that is something great"
>>> args.split(" ", 1) # one split, that is a list with two elements (before, after)
['that', 'is something great']
>>>
Read this example as many times as needed to understand it. The second argument we give to
`str.split` is not the length of the list that should be returned, but the number of times we have
to split. Therefore, we specify 1 here, but we get a list of two elements (before the separator,
after the separator).
> What will happen if Python can't split the number of times we ask?
It won't:
>>> args = "whatever"
>>> args.split(" ", 1) # there isn't even a space here...
['whatever']
>>>
This is one moment I would have hoped for an exception and didn't get one. But there's another way
which will raise an exception if there is an error: variable unpacking.
We won't talk about this feature in details here. It would be complicated. But the code is really
straightforward to use. Let's take our example of the roll command but let's add a first argument:
the number of dice to roll.
```python
from random import randint
from evennia import Command, InterruptCommand
class CmdRoll(Command):
"""
Play random, enter a number and try your luck.
Specify two numbers separated by a space. The first number is the
number of dice to roll (1, 2, 3) and the second is the expected sum
of the roll.
Usage:
roll <dice> <number>
For instance, to roll two 6-figure dice, enter 2 as first argument.
If you think the sum of these two dice roll will be 10, you could enter:
roll 2 10
"""
key = "roll"
def parse(self):
"""Split the arguments and convert them."""
args = self.args.lstrip()
# Split: we expect two arguments separated by a space
try:
number, guess = args.split(" ", 1)
except ValueError:
self.msg("Invalid usage. Enter two numbers separated by a space.")
raise InterruptCommand
# Convert the entered number (first argument)
try:
self.number = int(number)
if self.number <= 0:
raise ValueError
except ValueError:
self.msg(f"{number} is not a valid number of dice.")
raise InterruptCommand
# Convert the entered guess (second argument)
try:
self.guess = int(guess)
if not 1 <= self.guess <= self.number * 6:
raise ValueError
except ValueError:
self.msg(f"{self.guess} is not a valid guess.")
raise InterruptCommand
def func(self):
# Roll a random die X times (X being self.number)
figure = 0
for _ in range(self.number):
figure += randint(1, 6)
self.msg(f"You roll {self.number} dice and obtain the sum {figure}.")
if self.guess == figure:
self.msg(f"You played {self.guess}, you have won!")
else:
self.msg(f"You played {self.guess}, you have lost.")
```
The beginning of the `parse()` method is what interests us most:
```python
try:
number, guess = args.split(" ", 1)
except ValueError:
self.msg("Invalid usage. Enter two numbers separated by a space.")
raise InterruptCommand
```
We split the argument using `str.split` but we capture the result in two variables. Python is smart
enough to know that we want what's left of the space in the first variable, what's right of the
space in the second variable. If there is not even a space in the string, Python will raise a
`ValueError` exception.
This code is much easier to read than browsing through the returned strings of `str.split`. We can
convert both variables the way we did previously. Actually there are not so many changes in this
version and the previous one, most of it is due to name changes for clarity.
> Splitting a string with a maximum of splits is a common occurrence while parsing command
arguments. You can also see the `str.rspli8t` method that does the same thing but from the right of
the string. Therefore, it will attempt to find delimiters at the end of the string and work toward
the beginning of it.
We have used a space as a delimiter. This is absolutely not necessary. You might remember that
most default Evennia commands can take an `=` sign as a delimiter. Now you know how to parse them
as well:
>>> cmd_key = "tel"
>>> cmd_args = "book = chest"
>>> left, right = cmd_args.split("=") # mighht raise ValueError!
>>> left
'book '
>>> right
' chest'
>>>
### Optional arguments
Sometimes, you'll come across commands that have optional arguments. These arguments are not
necessary but they can be set if more information is needed. I will not provide the entire command
code here but just enough code to show the mechanism in Python:
Again, we'll use `str.split`, knowing that we might not have any delimiter at all. For instance,
the player could enter the "tel" command like this:
> tel book
> tell book = chest
The equal sign is optional along with whatever is specified after it. A possible solution in our
`parse` method would be:
```python
def parse(self):
args = self.args.lstrip()
# = is optional
try:
obj, destination = args.split("=", 1)
except ValueError:
obj = args
destination = None
```
This code would place everything the user entered in `obj` if she didn't specify any equal sign.
Otherwise, what's before the equal sign will go in `obj`, what's after the equal sign will go in
`destination`. This makes for quick testing after that, more robust code with less conditions that
might too easily break your code if you're not careful.
> Again, here we specified a maximum numbers of splits. If the users enters:
> tel book = chest = chair
Then `destination` will contain: `" chest = chair"`. This is often desired, but it's up to you to
set parsing however you like.
## Evennia searches
After this quick tour of some `str` methods, we'll take a look at some Evennia-specific features
that you won't find in standard Python.
One very common task is to convert a `str` into an Evennia object. Take the previous example:
having `"book"` in a variable is great, but we would prefer to know what the user is talking
about... what is this `"book"`?
To get an object from a string, we perform an Evennia search. Evennia provides a `search` method on
all typeclassed objects (you will most likely use the one on characters or accounts). This method
supports a very wide array of arguments and has [its own tutorial](Tutorial-Searching-For-Objects).
Some examples of useful cases follow:
### Local searches
When an account or a character enters a command, the account or character is found in the `caller`
attribute. Therefore, `self.caller` will contain an account or a character (or a session if that's
a session command, though that's not as frequent). The `search` method will be available on this
caller.
Let's take the same example of our little "tel" command. The user can specify an object as
argument:
```python
def parse(self):
name = self.args.lstrip()
```
We then need to "convert" this string into an Evennia object. The Evennia object will be searched
in the caller's location and its contents by default (that is to say, if the command has been
entered by a character, it will search the object in the character's room and the character's
inventory).
```python
def parse(self):
name = self.args.lstrip()
self.obj = self.caller.search(name)
```
We specify only one argument to the `search` method here: the string to search. If Evennia finds a
match, it will return it and we keep it in the `obj` attribute. If it can't find anything, it will
return `None` so we need to check for that:
```python
def parse(self):
name = self.args.lstrip()
self.obj = self.caller.search(name)
if self.obj is None:
# A proper error message has already been sent to the caller
raise InterruptCommand
```
That's it. After this condition, you know that whatever is in `self.obj` is a valid Evennia object
(another character, an object, an exit...).
### Quiet searches
By default, Evennia will handle the case when more than one match is found in the search. The user
will be asked to narrow down and re-enter the command. You can, however, ask to be returned the
list of matches and handle this list yourself:
```python
def parse(self):
name = self.args.lstrip()
objs = self.caller.search(name, quiet=True)
if not objs:
# This is an empty list, so no match
self.msg(f"No {name!r} was found.")
raise InterruptCommand
self.obj = objs[0] # Take the first match even if there are several
```
All we have changed to obtain a list is a keyword argument in the `search` method: `quiet`. If set
to `True`, then errors are ignored and a list is always returned, so we need to handle it as such.
Notice in this example, `self.obj` will contain a valid object too, but if several matches are
found, `self.obj` will contain the first one, even if more matches are available.
### Global searches
By default, Evennia will perform a local search, that is, a search limited by the location in which
the caller is. If you want to perform a global search (search in the entire database), just set the
`global_search` keyword argument to `True`:
```python
def parse(self):
name = self.args.lstrip()
self.obj = self.caller.search(name, global_search=True)
```
## Conclusion
Parsing command arguments is vital for most game designers. If you design "intelligent" commands,
users should be able to guess how to use them without reading the help, or with a very quick peek at
said help. Good commands are intuitive to users. Better commands do what they're told to do. For
game designers working on MUDs, commands are the main entry point for users into your game. This is
no trivial. If commands execute correctly (if their argument is parsed, if they don't behave in
unexpected ways and report back the right errors), you will have happier players that might stay
longer on your game. I hope this tutorial gave you some pointers on ways to improve your command
parsing. There are, of course, other ways you will discover, or ways you are already using in your
code.

View file

@ -0,0 +1,267 @@
# Python basic introduction
This is the first part of our beginner's guide to the basics of using Python with Evennia. It's
aimed at you with limited or no programming/Python experience. But also if you are an experienced
programmer new to Evennia or Python you might still pick up a thing or two. It is by necessity brief
and low on detail. There are countless Python guides and tutorials, books and videos out there for
learning more in-depth - use them!
**Contents:**
- [Evennia Hello world](Python-basic-introduction#evennia-hello-world)
- [Importing modules](Python-basic-introduction#importing-modules)
- [Parsing Python errors](Python-basic-introduction#parsing-python-errors)
- [Our first function](Python-basic-introduction#our-first-function)
- [Looking at the log](Python-basic-introduction#looking-at-the-log)
- (continued in [part 2](Python-basic-tutorial-part-two))
This quickstart assumes you have [gotten Evennia started](Getting-Started). You should make sure
that you are able to see the output from the server in the console from which you started it. Log
into the game either with a mud client on `localhost:4000` or by pointing a web browser to
`localhost:4001/webclient`. Log in as your superuser (the user you created during install).
Below, lines starting with a single `>` means command input.
### Evennia Hello world
The `py` (or `!` which is an alias) command allows you as a superuser to run raw Python from in-
game. From the game's input line, enter the following:
> py print("Hello World!")
You will see
> print("Hello world!")
Hello World
To understand what is going on: some extra info: The `print(...)` *function* is the basic, in-built
way to output text in Python. The quotes `"..."` means you are inputing a *string* (i.e. text). You
could also have used single-quotes `'...'`, Python accepts both.
The first return line (with `>>>`) is just `py` echoing what you input (we won't include that in the
examples henceforth).
> Note: You may sometimes see people/docs refer to `@py` or other commands starting with `@`.
Evennia ignores `@` by default, so `@py` is the exact same thing as `py`.
The `print` command is a standard Python structure. We can use that here in the `py` command, and
it's great for debugging and quick testing. But if you need to send a text to an actual player,
`print` won't do, because it doesn't know _who_ to send to. Try this:
> py me.msg("Hello world!")
Hello world!
This looks the same as the `print` result, but we are now actually messaging a specific *object*,
`me`. The `me` is something uniquely available in the `py` command (we could also use `self`, it's
an alias). It represents "us", the ones calling the `py` command. The `me` is an example of an
*Object instance*. Objects are fundamental in Python and Evennia. The `me` object not only
represents the character we play in the game, it also contains a lot of useful resources for doing
things with that Object. One such resource is `msg`. `msg` works like `print` except it sends the
text to the object it is attached to. So if we, for example, had an object `you`, doing
`you.msg(...)` would send a message to the object `you`.
You access an Object's resources by using the full-stop character `.`. So `self.msg` accesses the
`msg` resource and then we call it like we did print, with our "Hello World!" greeting in
parentheses.
> Important: something like `print(...)` we refer to as a *function*, while `msg(...)` which sits on
an object is called a *method*.
For now, `print` and `me.msg` behaves the same, just remember that you're going to mostly be using
the latter in the future. Try printing other things. Also try to include `|r` at the start of your
string to make the output red in-game. Use `color` to learn more color tags.
### Importing modules
Keep your game running, then open a text editor of your choice. If your game folder is called
`mygame`, create a new text file `test.py` in the subfolder `mygame/world`. This is how the file
structure should look:
```
mygame/
world/
test.py
```
For now, only add one line to `test.py`:
```python
print("Hello World!")
```
Don't forget to save the file. A file with the ending `.py` is referred to as a Python *module*. To
use this in-game we have to *import* it. Try this:
```python
> @py import world.test
Hello World
```
If you make some error (we'll cover how to handle errors below) you may need to run the `@reload`
command for your changes to take effect.
So importing `world.test` actually means importing `world/test.py`. Think of the period `.` as
replacing `/` (or `\` for Windows) in your path. The `.py` ending of `test.py` is also never
included in this "Python-path", but _only_ files with that ending can be imported this way. Where is
`mygame` in that Python-path? The answer is that Evennia has already told Python that your `mygame`
folder is a good place to look for imports. So we don't include `mygame` in the path - Evennia
handles this for us.
When you import the module, the top "level" of it will execute. In this case, it will immediately
print "Hello World".
> If you look in the folder you'll also often find new files ending with `.pyc`. These are compiled
Python binaries that Python auto-creates when running code. Just ignore them, you should never edit
those anyway.
Now try to run this a second time:
```python
> py import world.test
```
You will *not* see any output this second time or any subsequent times! This is not a bug. Rather
it is because Python is being clever - it stores all imported modules and to be efficient it will
avoid importing them more than once. So your `print` will only run the first time, when the module
is first imported. To see it again you need to `@reload` first, so Python forgets about the module
and has to import it again.
We'll get back to importing code in the second part of this tutorial. For now, let's press on.
### Parsing Python errors
Next, erase the single `print` statement you had in `test.py` and replace it with this instead:
```python
me.msg("Hello World!")
```
As you recall we used this from `py` earlier - it echoed "Hello World!" in-game.
Save your file and `reload` your server - this makes sure Evennia sees the new version of your code.
Try to import it from `py` in the same way as earlier:
```python
> py import world.test
```
No go - this time you get an error!
```python
File "./world/test.py", line 1, in <module>
me.msg("Hello world!")
NameError: name 'me' is not defined
```
This is called a *traceback*. Python's errors are very friendly and will most of the time tell you
exactly what and where things are wrong. It's important that you learn to parse tracebacks so you
can fix your code. Let's look at this one. A traceback is to be read from the _bottom up_. The last
line is the error Python balked at, while the two lines above it details exactly where that error
was encountered.
1. An error of type `NameError` is the problem ...
2. ... more specifically it is due to the variable `me` not being defined.
3. This happened on the line `me.msg("Hello world!")` ...
4. ... which is on line `1` of the file `./world/test.py`.
In our case the traceback is short. There may be many more lines above it, tracking just how
different modules called each other until it got to the faulty line. That can sometimes be useful
information, but reading from the bottom is always a good start.
The `NameError` we see here is due to a module being its own isolated thing. It knows nothing about
the environment into which it is imported. It knew what `print` is because that is a special
[reserved Python keyword](https://docs.python.org/2.5/ref/keywords.html). But `me` is *not* such a
reserved word. As far as the module is concerned `me` is just there out of nowhere. Hence the
`NameError`.
### Our first function
Let's see if we can resolve that `NameError` from the previous section. We know that `me` is defined
at the time we use the `@py` command because if we do `py me.msg("Hello World!")` directly in-game
it works fine. What if we could *send* that `me` to the `test.py` module so it knows what it is? One
way to do this is with a *function*.
Change your `mygame/world/test.py` file to look like this:
```python
def hello_world(who):
who.msg("Hello World!")
```
Now that we are moving onto multi-line Python code, there are some important things to remember:
- Capitalization matters in Python. It must be `def` and not `DEF`, `who` is not the same as `Who`
etc.
- Indentation matters in Python. The second line must be indented or it's not valid code. You should
also use a consistent indentation length. We *strongly* recommend that you set up your editor to
always indent *4 spaces* (**not** a single tab-character) when you press the TAB key - it will make
your life a lot easier.
- `def` is short for "define" and defines a *function* (or a *method*, if sitting on an object).
This is a [reserved Python keyword](https://docs.python.org/2.5/ref/keywords.html); try not to use
these words anywhere else.
- A function name can not have spaces but otherwise we could have called it almost anything. We call
it `hello_world`. Evennia follows [Python's standard naming
style](https://github.com/evennia/evennia/blob/master/CODING_STYLE.md#a-quick-list-of-code-style-
points) with lowercase letters and underscores. Use this style for now.
- `who` is what we call the *argument* to our function. Arguments are variables we pass to the
function. We could have named it anything and we could also have multiple arguments separated by
commas. What `who` is depends on what we pass to this function when we *call* it later (hint: we'll
pass `me` to it).
- The colon (`:`) at the end of the first line indicates that the header of the function is
complete.
- The indentation marks the beginning of the actual operating code of the function (the function's
*body*). If we wanted more lines to belong to this function those lines would all have to have to
start at this indentation level.
- In the function body we take the `who` argument and treat it as we would have treated `me` earlier
- we expect it to have a `.msg` method we can use to send "Hello World" to.
First, `reload` your game to make it aware of the updated Python module. Now we have defined our
first function, let's use it.
> reload
> py import world.test
Nothing happened! That is because the function in our module won't do anything just by importing it.
It will only act when we *call* it. We will need to enter the module we just imported and do so.
> py import world.test ; world.test.hello_world(me)
Hello world!
There is our "Hello World"! The `;` is the way to put multiple Python-statements on one line.
> Some MUD clients use `;` for their own purposes to separate client-inputs. If so you'll get a
`NameError` stating that `world` is not defined. Check so you understand why this is! Change the use
of `;` in your client or use the Evennia web client if this is a problem.
In the second statement we access the module path we imported (`world.test`) and reach for the
`hello_world` function within. We *call* the function with `me`, which becomes the `who` variable we
use inside the `hello_function`.
> As an exercise, try to pass something else into `hello_world`. Try for example to pass _who_ as
the number `5` or the simple string `"foo"`. You'll get errors that they don't have the attribute
`msg`. As we've seen, `me` *does* make `msg` available which is why it works (you'll learn more
about Objects like `me` in the next part of this tutorial). If you are familiar with other
programming languages you may be tempted to start *validating* `who` to make sure it works as
expected. This is usually not recommended in Python which suggests it's better to
[handle](https://docs.python.org/2/tutorial/errors.html) the error if it happens rather than to make
a lot of code to prevent it from happening. See also [duck
typing](https://en.wikipedia.org/wiki/Duck_typing).
# Looking at the log
As you start to explore Evennia, it's important that you know where to look when things go wrong.
While using the friendly `py` command you'll see errors directly in-game. But if something goes
wrong in your code while the game runs, you must know where to find the _log_.
Open a terminal (or go back to the terminal you started Evennia in), make sure your `virtualenv` is
active and that you are standing in your game directory (the one created with `evennia --init`
during installation). Enter
```
evennia --log
```
(or `evennia -l`)
This will show the log. New entries will show up in real time. Whenever you want to leave the log,
enter `Ctrl-C` or `Cmd-C` depending on your system. As a game dev it is important to look at the
log output when working in Evennia - many errors will only appear with full details here. You may
sometimes have to scroll up in the history if you miss it.
This tutorial is continued in [Part 2](Python-basic-tutorial-part-two), where we'll start learning
about objects and to explore the Evennia library.

View file

@ -0,0 +1,506 @@
# Python basic tutorial part two
[In the first part](Python-basic-introduction) of this Python-for-Evennia basic tutorial we learned
how to run some simple Python code from inside the game. We also made our first new *module*
containing a *function* that we called. Now we're going to start exploring the very important
subject of *objects*.
**Contents:**
- [On the subject of objects](Python-basic-tutorial-part-two#on-the-subject-of-objects)
- [Exploring the Evennia library](Python-basic-tutorial-part-two#exploring-the-evennia-library)
- [Tweaking our Character class](Python-basic-tutorial-part-two#tweaking-our-character-class)
- [The Evennia shell](Python-basic-tutorial-part-two#the-evennia-shell)
- [Where to go from here](Python-basic-tutorial-part-two#where-to-go-from-here)
### On the subject of objects
In the first part of the tutorial we did things like
> py me.msg("Hello World!")
To learn about functions and imports we also passed that `me` on to a function `hello_world` in
another module.
Let's learn some more about this `me` thing we are passing around all over the place. In the
following we assume that we named our superuser Character "Christine".
> py me
Christine
> py me.key
Christine
These returns look the same at first glance, but not if we examine them more closely:
> py type(me)
<class 'typeclasses.characters.Character'>
> py type(me.key)
<type str>
> Note: In some MU clients, such as Mudlet and MUSHclient simply returning `type(me)`, you may not
see the proper return from the above commands. This is likely due to the HTML-like tags `<...>`,
being swallowed by the client.
The `type` function is, like `print`, another in-built function in Python. It
tells us that we (`me`) are of the *class* `typeclasses.characters.Character`.
Meanwhile `me.key` is a *property* on us, a string. It holds the name of this
object.
> When you do `py me`, the `me` is defined in such a way that it will use its `.key` property to
represent itself. That is why the result is the same as when doing `py me.key`. Also, remember that
as noted in the first part of the tutorial, the `me` is *not* a reserved Python word; it was just
defined by the Evennia developers as a convenient short-hand when creating the `py` command. So
don't expect `me` to be available elsewhere.
A *class* is like a "factory" or blueprint. From a class you then create individual *instances*. So
if class is`Dog`, an instance of `Dog` might be `fido`. Our in-game persona is of a class
`Character`. The superuser `christine` is an *instance* of the `Character` class (an instance is
also often referred to as an *object*). This is an important concept in *object oriented
programming*. You are wise to [familiarize yourself with it](https://en.wikipedia.org/wiki/Class-
based_programming) a little.
> In other terms:
> * class: A description of a thing, all the methods (code) and data (information)
> * object: A thing, defined as an *instance* of a class.
>
> So in "Fido is a Dog", "Fido" is an object--a unique thing--and "Dog" is a class. Coders would
also say, "Fido is an instance of Dog". There can be other dogs too, such as Butch and Fifi. They,
too, would be instances of Dog.
>
> As another example: "Christine is a Character", or "Christine is an instance of
typeclasses.characters.Character". To start, all characters will be instances of
typeclass.characters.Character.
>
> You'll be writing your own class soon! The important thing to know here is how classes and objects
relate.
The string `'typeclasses.characters.Character'` we got from the `type()` function is not arbitrary.
You'll recognize this from when we _imported_ `world.test` in part one. This is a _path_ exactly
describing where to find the python code describing this class. Python treats source code files on
your hard drive (known as *modules*) as well as folders (known as *packages*) as objects that you
access with the `.` operator. It starts looking at a place that Evennia has set up for you - namely
the root of your own game directory.
Open and look at your game folder (named `mygame` if you exactly followed the Getting Started
instructions) in a file editor or in a new terminal/console. Locate the file
`mygame/typeclasses/characters.py`
```
mygame/
typeclasses
characters.py
```
This represents the first part of the python path - `typeclasses.characters` (the `.py` file ending
is never included in the python path). The last bit, `.Character` is the actual class name inside
the `characters.py` module. Open that file in a text editor and you will see something like this:
```python
"""
(Doc string for module)
"""
from evennia import DefaultCharacter
class Character(DefaultCharacter):
"""
(Doc string for class)
"""
pass
```
There is `Character`, the last part of the path. Note how empty this file is. At first glance one
would think a Character had no functionality at all. But from what we have used already we know it
has at least the `key` property and the method `msg`! Where is the code? The answer is that this
'emptiness' is an illusion caused by something called *inheritance*. Read on.
Firstly, in the same way as the little `hello.py` we did in the first part of the tutorial, this is
an example of full, multi-line Python code. Those triple-quoted strings are used for strings that
have line breaks in them. When they appear on their own like this, at the top of a python module,
class or similar they are called *doc strings*. Doc strings are read by Python and is used for
producing online help about the function/method/class/module. By contrast, a line starting with `#`
is a *comment*. It is ignored completely by Python and is only useful to help guide a human to
understand the code.
The line
```python
class Character(DefaultCharacter):
```
means that the class `Character` is a *child* of the class `DefaultCharacter`. This is called
*inheritance* and is another fundamental concept. The answer to the question "where is the code?" is
that the code is *inherited* from its parent, `DefaultCharacter`. And that in turn may inherit code
from *its* parent(s) and so on. Since our child, `Character` is empty, its functionality is *exactly
identical* to that of its parent. The moment we add new things to Character, these will take
precedence. And if we add something that already existed in the parent, our child-version will
*override* the version in the parent. This is very practical: It means that we can let the parent do
the heavy lifting and only tweak the things we want to change. It also means that we could easily
have many different Character classes, all inheriting from `DefaultCharacter` but changing different
things. And those can in turn also have children ...
Let's go on an expedition up the inheritance tree.
### Exploring the Evennia library
Let's figure out how to tweak `Character`. Right now we don't know much about `DefaultCharacter`
though. Without knowing that we won't know what to override. At the top of the file you find
```python
from evennia import DefaultCharacter
```
This is an `import` statement again, but on a different form to what we've seen before. `from ...
import ...` is very commonly used and allows you to precisely dip into a module to extract just the
component you need to use. In this case we head into the `evennia` package to get
`DefaultCharacter`.
Where is `evennia`? To find it you need to go to the `evennia` folder (repository) you originally
cloned from us. If you open it, this is how it looks:
```
evennia/
__init__.py
bin/
CHANGELOG.txt etc.
...
evennia/
...
```
There are lots of things in there. There are some docs but most of those have to do with the
distribution of Evennia and does not concern us right now. The `evennia` subfolder is what we are
looking for. *This* is what you are accessing when you do `from evennia import ...`. It's set up by
Evennia as a good place to find modules when the server starts. The exact layout of the Evennia
library [is covered by our directory overview](Directory-Overview#evennia-library-layout). You can
also explore it [online on github](https://github.com/evennia/evennia/tree/master/evennia).
The structure of the library directly reflects how you import from it.
- To, for example, import [the text justify
function](https://github.com/evennia/evennia/blob/master/evennia/utils/utils.py#L201) from
`evennia/utils/utils.py` you would do `from evennia.utils.utils import justify`. In your code you
could then just call `justify(...)` to access its functionality.
- You could also do `from evennia.utils import utils`. In code you would then have to write
`utils.justify(...)`. This is practical if want a lot of stuff from that `utils.py` module and don't
want to import each component separately.
- You could also do `import evennia`. You would then have to enter the full
`evennia.utils.utils.justify(...)` every time you use it. Using `from` to only import the things you
need is usually easier and more readable.
- See [this overview](http://effbot.org/zone/import-confusion.htm) about the different ways to
import in Python.
Now, remember that our `characters.py` module did `from evennia import DefaultCharacter`. But if we
look at the contents of the `evennia` folder, there is no `DefaultCharacter` anywhere! This is
because Evennia gives a large number of optional "shortcuts", known as [the "flat" API](Evennia-
API). The intention is to make it easier to remember where to find stuff. The flat API is defined in
that weirdly named `__init__.py` file. This file just basically imports useful things from all over
Evennia so you can more easily find them in one place.
We could [just look at the documenation](github:evennia#typeclasses) to find out where we can look
at our `DefaultCharacter` parent. But for practice, let's figure it out. Here is where
`DefaultCharacter` [is imported
from](https://github.com/evennia/evennia/blob/master/evennia/__init__.py#L188) inside `__init__.py`:
```python
from .objects.objects import DefaultCharacter
```
The period at the start means that it imports beginning from the same location this module sits(i.e.
the `evennia` folder). The full python-path accessible from the outside is thus
`evennia.objects.objects.DefaultCharacter`. So to import this into our game it'd be perfectly valid
to do
```python
from evennia.objects.objects import DefaultCharacter
```
Using
```python
from evennia import DefaultCharacter
```
is the same thing, just a little easier to remember.
> To access the shortcuts of the flat API you *must* use `from evennia import
> ...`. Using something like `import evennia.DefaultCharacter` will not work.
> See [more about the Flat API here](Evennia-API).
### Tweaking our Character class
In the previous section we traced the parent of our `Character` class to be
`DefaultCharacter` in
[evennia/objects/objects.py](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py).
Open that file and locate the `DefaultCharacter` class. It's quite a bit down
in this module so you might want to search using your editor's (or browser's)
search function. Once you find it, you'll find that the class starts like this:
```python
class DefaultCharacter(DefaultObject):
"""
This implements an Object puppeted by a Session - that is, a character
avatar controlled by an account.
"""
def basetype_setup(self):
"""
Setup character-specific security.
You should normally not need to overload this, but if you do,
make sure to reproduce at least the two last commands in this
method (unless you want to fundamentally change how a
Character object works).
"""
super().basetype_setup()
self.locks.add(";".join(["get:false()", # noone can pick up the character
"call:false()"])) # no commands can be called on character from
outside
# add the default cmdset
self.cmdset.add_default(settings.CMDSET_CHARACTER, permanent=True)
def at_after_move(self, source_location, **kwargs):
"""
We make sure to look around after a move.
"""
if self.location.access(self, "view"):
self.msg(self.at_look(self.location))
def at_pre_puppet(self, account, session=None, **kwargs):
"""
Return the character from storage in None location in `at_post_unpuppet`.
"""
# ...
```
... And so on (you can see the full [class online
here](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1915)). Here we
have functional code! These methods may not be directly visible in `Character` back in our game dir,
but they are still available since `Character` is a child of `DefaultCharacter` above. Here is a
brief summary of the methods we find in `DefaultCharacter` (follow in the code to see if you can see
roughly where things happen)::
- `basetype_setup` is called by Evennia only once, when a Character is first created. In the
`DefaultCharacter` class it sets some particular [Locks](Locks) so that people can't pick up and
puppet Characters just like that. It also adds the [Character Cmdset](Command-Sets) so that
Characters always can accept command-input (this should usually not be modified - the normal hook to
override is `at_object_creation`, which is called after `basetype_setup` (it's in the parent)).
- `at_after_move` makes it so that every time the Character moves, the `look` command is
automatically fired (this would not make sense for just any regular Object).
- `at_pre_puppet` is called when an Account begins to puppet this Character. When not puppeted, the
Character is hidden away to a `None` location. This brings it back to the location it was in before.
Without this, "headless" Characters would remain in the game world just standing around.
- `at_post_puppet` is called when puppeting is complete. It echoes a message to the room that his
Character has now connected.
- `at_post_unpuppet` is called once stopping puppeting of the Character. This hides away the
Character to a `None` location again.
- There are also some utility properties which makes it easier to get some time stamps from the
Character.
Reading the class we notice another thing:
```python
class DefaultCharacter(DefaultObject):
# ...
```
This means that `DefaultCharacter` is in *itself* a child of something called `DefaultObject`! Let's
see what this parent class provides. It's in the same module as `DefaultCharacter`, you just need to
[scroll up near the
top](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L182):
```python
class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
# ...
```
This is a really big class where the bulk of code defining an in-game object resides. It consists of
a large number of methods, all of which thus also becomes available on the `DefaultCharacter` class
below *and* by extension in your `Character` class over in your game dir. In this class you can for
example find the `msg` method we have been using before.
> You should probably not expect to understand all details yet, but as an exercise, find and read
the doc string of `msg`.
> As seen, `DefaultObject` actually has multiple parents. In one of those the basic `key` property
is defined, but we won't travel further up the inheritance tree in this tutorial. If you are
interested to see them, you can find `TypeclassBase` in
[evennia/typeclasses/models.py](https://github.com/evennia/evennia/blob/master/evennia/typeclasses/models.py#L93)
and `ObjectDB` in
[evennia/objects/models.py](https://github.com/evennia/evennia/blob/master/evennia/objects/models.py#L121).
We will also not go into the details of [Multiple
Inheritance](https://docs.python.org/2/tutorial/classes.html#multiple-inheritance) or
[Metaclasses](http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html) here. The general rule
is that if you realize that you need these features, you already know enough to use them.
Remember the `at_pre_puppet` method we looked at in `DefaultCharacter`? If you look at the
`at_pre_puppet` hook as defined in `DefaultObject` you'll find it to be completely empty (just a
`pass`). So if you puppet a regular object it won't be hiding/retrieving the object when you
unpuppet it. The `DefaultCharacter` class *overrides* its parent's functionality with a version of
its own. And since it's `DefaultCharacter` that our `Character` class inherits back in our game dir,
it's *that* version of `at_pre_puppet` we'll get. Anything not explicitly overridden will be passed
down as-is.
While it's useful to read the code, we should never actually modify anything inside the `evennia`
folder. Only time you would want that is if you are planning to release a bug fix or new feature for
Evennia itself. Instead you *override* the default functionality inside your game dir.
So to conclude our little foray into classes, objects and inheritance, locate the simple little
`at_before_say` method in the `DefaultObject` class:
```python
def at_before_say(self, message, **kwargs):
"""
(doc string here)
"""
return message
```
If you read the doc string you'll find that this can be used to modify the output of `say` before it
goes out. You can think of it like this: Evennia knows the name of this method, and when someone
speaks, Evennia will make sure to redirect the outgoing message through this method. It makes it
ripe for us to replace with a version of our own.
> In the Evennia documentation you may sometimes see the term *hook* used for a method explicitly
meant to be overridden like this.
As you can see, the first argument to `at_before_say` is `self`. In Python, the first argument of a
method is *always a back-reference to the object instance on which the method is defined*. By
convention this argument is always called `self` but it could in principle be named anything. The
`self` is very useful. If you wanted to, say, send a message to the same object from inside
`at_before_say`, you would do `self.msg(...)`.
What can trip up newcomers is that you *don't* include `self` when you *call* the method. Try:
> @py me.at_before_say("Hello World!")
Hello World!
Note that we don't send `self` but only the `message` argument. Python will automatically add `self`
for us. In this case, `self` will become equal to the Character instance `me`.
By default the `at_before_say` method doesn't do anything. It just takes the `message` input and
`return`s it just the way it was (the `return` is another reserved Python word).
> We won't go into `**kwargs` here, but it (and its sibling `*args`) is also important to
understand, extra reading is [here for
`**kwargs`](https://stackoverflow.com/questions/1769403/understanding-kwargs-in-python).
Now, open your game folder and edit `mygame/typeclasses/characters.py`. Locate your `Character`
class and modify it as such:
```python
class Character(DefaultCharacter):
"""
(docstring here)
"""
def at_before_say(self, message, **kwargs):
"Called before say, allows for tweaking message"
return f"{message} ..."
```
So we add our own version of `at_before_say`, duplicating the `def` line from the parent but putting
new code in it. All we do in this tutorial is to add an ellipsis (`...`) to the message as it passes
through the method.
Note that `f` in front of the string, it means we turned the string into a 'formatted string'. We
can now easily inject stuff directly into the string by wrapping them in curly brackets `{ }`. In
this example, we put the incoming `message` into the string, followed by an ellipsis. This is only
one way to format a string. Python has very powerful [string
formatting](https://docs.python.org/2/library/string.html#format-specification-mini-language) and
you are wise to learn it well, considering your game will be mainly text-based.
> You could also copy & paste the relevant method from `DefaultObject` here to get the full doc
string. For more complex methods, or if you only want to change some small part of the default
behavior, copy & pasting will eliminate the need to constantly look up the original method and keep
you sane.
In-game, now try
> @reload
> say Hello
You say, "Hello ..."
An ellipsis `...` is added to what you said! This is a silly example but you have just made your
first code change to core functionality - without touching any of Evennia's original code! We just
plugged in our own version of the `at_before_say` method and it replaced the default one. Evennia
happily redirected the message through our version and we got a different output.
> For sane overriding of parent methods you should also be aware of Python's
[super](https://docs.python.org/3/library/functions.html#super), which allows you to call the
methods defined on a parent in your child class.
### The Evennia shell
Now on to some generally useful tools as you continue learning Python and Evennia. We have so far
explored using `py` and have inserted Python code directly in-game. We have also modified Evennia's
behavior by overriding default functionality with our own. There is a third way to conveniently
explore Evennia and Python - the Evennia shell.
Outside of your game, `cd` to your mygame folder and make sure any needed virtualenv is running.
Next:
> pip install ipython # only needed once
The [`IPython`](https://en.wikipedia.org/wiki/IPython) program is just a nicer interface to the
Python interpreter - you only need to install it once, after which Evennia will use it
automatically.
> evennia shell
If you did this call from your game dir you will now be in a Python prompt managed by the IPython
program.
IPython ...
...
In [1]:
IPython has some very nice ways to explore what Evennia has to offer.
> import evennia
> evennia.<TAB>
That is, write `evennia.` and press the Tab key. You will be presented with a list of all available
resources in the Evennia Flat API. We looked at the `__init__.py` file in the `evennia` folder
earlier, so some of what you see should be familiar. From the IPython prompt, do:
> from evennia import DefaultCharacter
> DefaultCharacter.at_before_say?
Don't forget that you can use `<TAB>` to auto-complete code as you write. Appending a single `?` to
the end will show you the doc-string for `at_before_say` we looked at earlier. Use `??` to get the
whole source code.
Let's look at our over-ridden version instead. Since we started the `evennia shell` from our game
dir we can easily get to our code too:
> from typeclasses.characters import Character
> Character.at_before_say??
This will show us the changed code we just did. Having a window with IPython running is very
convenient for quickly exploring code without having to go digging through the file structure!
### Where to go from here
This should give you a running start using Python with Evennia. If you are completely new to
programming or Python you might want to look at a more formal Python tutorial. You can find links
and resources [on our link page](Links).
We have touched upon many of the concepts here but to use Evennia and to be able to follow along in
the code, you will need basic understanding of Python
[modules](http://docs.python.org/2/tutorial/modules.html),
[variables](http://www.tutorialspoint.com/python/python_variable_types.htm), [conditional
statements](http://docs.python.org/tutorial/controlflow.html#if-statements),
[loops](http://docs.python.org/tutorial/controlflow.html#for-statements),
[functions](http://docs.python.org/tutorial/controlflow.html#defining-functions), [lists,
dictionaries, list comprehensions](http://docs.python.org/tutorial/datastructures.html) and [string
formatting](http://docs.python.org/tutorial/introduction.html#strings). You should also have a basic
understanding of [object-oriented
programming](http://www.tutorialspoint.com/python/python_classes_objects.htm) and what Python
[Classes](http://docs.python.org/tutorial/classes.html) are.
Once you have familiarized yourself, or if you prefer to pick Python up as you go, continue to one
of the beginning-level [Evennia tutorials](Tutorials) to gradually build up your understanding.
Good luck!

View file

@ -0,0 +1,520 @@
# Turn based Combat System
This tutorial gives an example of a full, if simplified, combat system for Evennia. It was inspired
by the discussions held on the [mailing
list](https://groups.google.com/forum/#!msg/evennia/wnJNM2sXSfs/-dbLRrgWnYMJ).
## Overview of combat system concepts
Most MUDs will use some sort of combat system. There are several main variations:
- _Freeform_ - the simplest form of combat to implement, common to MUSH-style roleplaying games.
This means the system only supplies dice rollers or maybe commands to compare skills and spit out
the result. Dice rolls are done to resolve combat according to the rules of the game and to direct
the scene. A game master may be required to resolve rule disputes.
- _Twitch_ - This is the traditional MUD hack&slash style combat. In a twitch system there is often
no difference between your normal "move-around-and-explore mode" and the "combat mode". You enter an
attack command and the system will calculate if the attack hits and how much damage was caused.
Normally attack commands have some sort of timeout or notion of recovery/balance to reduce the
advantage of spamming or client scripting. Whereas the simplest systems just means entering `kill
<target>` over and over, more sophisticated twitch systems include anything from defensive stances
to tactical positioning.
- _Turn-based_ - a turn based system means that the system pauses to make sure all combatants can
choose their actions before continuing. In some systems, such entered actions happen immediately
(like twitch-based) whereas in others the resolution happens simultaneously at the end of the turn.
The disadvantage of a turn-based system is that the game must switch to a "combat mode" and one also
needs to take special care of how to handle new combatants and the passage of time. The advantage is
that success is not dependent on typing speed or of setting up quick client macros. This potentially
allows for emoting as part of combat which is an advantage for roleplay-heavy games.
To implement a freeform combat system all you need is a dice roller and a roleplaying rulebook. See
[contrib/dice.py](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) for an
example dice roller. To implement at twitch-based system you basically need a few combat
[commands](Commands), possibly ones with a [cooldown](Command-Cooldown). You also need a [game rule
module](Implementing-a-game-rule-system) that makes use of it. We will focus on the turn-based
variety here.
## Tutorial overview
This tutorial will implement the slightly more complex turn-based combat system. Our example has the
following properties:
- Combat is initiated with `attack <target>`, this initiates the combat mode.
- Characters may join an ongoing battle using `attack <target>` against a character already in
combat.
- Each turn every combating character will get to enter two commands, their internal order matters
and they are compared one-to-one in the order given by each combatant. Use of `say` and `pose` is
free.
- The commands are (in our example) simple; they can either `hit <target>`, `feint <target>` or
`parry <target>`. They can also `defend`, a generic passive defense. Finally they may choose to
`disengage/flee`.
- When attacking we use a classic [rock-paper-scissors](https://en.wikipedia.org/wiki/Rock-paper-
scissors) mechanic to determine success: `hit` defeats `feint`, which defeats `parry` which defeats
`hit`. `defend` is a general passive action that has a percentage chance to win against `hit`
(only).
- `disengage/flee` must be entered two times in a row and will only succeed if there is no `hit`
against them in that time. If so they will leave combat mode.
- Once every player has entered two commands, all commands are resolved in order and the result is
reported. A new turn then begins.
- If players are too slow the turn will time out and any unset commands will be set to `defend`.
For creating the combat system we will need the following components:
- A combat handler. This is the main mechanic of the system. This is a [Script](Scripts) object
created for each combat. It is not assigned to a specific object but is shared by the combating
characters and handles all the combat information. Since Scripts are database entities it also means
that the combat will not be affected by a server reload.
- A combat [command set](Command-Sets) with the relevant commands needed for combat, such as the
various attack/defend options and the `flee/disengage` command to leave the combat mode.
- A rule resolution system. The basics of making such a module is described in the [rule system
tutorial](Implementing-a-game-rule-system). We will only sketch such a module here for our end-turn
combat resolution.
- An `attack` [command](Commands) for initiating the combat mode. This is added to the default
command set. It will create the combat handler and add the character(s) to it. It will also assign
the combat command set to the characters.
## The combat handler
The _combat handler_ is implemented as a stand-alone [Script](Scripts). This Script is created when
the first Character decides to attack another and is deleted when no one is fighting any more. Each
handler represents one instance of combat and one combat only. Each instance of combat can hold any
number of characters but each character can only be part of one combat at a time (a player would
need to disengage from the first combat before they could join another).
The reason we don't store this Script "on" any specific character is because any character may leave
the combat at any time. Instead the script holds references to all characters involved in the
combat. Vice-versa, all characters holds a back-reference to the current combat handler. While we
don't use this very much here this might allow the combat commands on the characters to access and
update the combat handler state directly.
_Note: Another way to implement a combat handler would be to use a normal Python object and handle
time-keeping with the [TickerHandler](TickerHandler). This would require either adding custom hook
methods on the character or to implement a custom child of the TickerHandler class to track turns.
Whereas the TickerHandler is easy to use, a Script offers more power in this case._
Here is a basic combat handler. Assuming our game folder is named `mygame`, we store it in
`mygame/typeclasses/combat_handler.py`:
```python
# mygame/typeclasses/combat_handler.py
import random
from evennia import DefaultScript
from world.rules import resolve_combat
class CombatHandler(DefaultScript):
"""
This implements the combat handler.
"""
# standard Script hooks
def at_script_creation(self):
"Called when script is first created"
self.key = "combat_handler_%i" % random.randint(1, 1000)
self.desc = "handles combat"
self.interval = 60 * 2 # two minute timeout
self.start_delay = True
self.persistent = True
# store all combatants
self.db.characters = {}
# store all actions for each turn
self.db.turn_actions = {}
# number of actions entered per combatant
self.db.action_count = {}
def _init_character(self, character):
"""
This initializes handler back-reference
and combat cmdset on a character
"""
character.ndb.combat_handler = self
character.cmdset.add("commands.combat.CombatCmdSet")
def _cleanup_character(self, character):
"""
Remove character from handler and clean
it of the back-reference and cmdset
"""
dbref = character.id
del self.db.characters[dbref]
del self.db.turn_actions[dbref]
del self.db.action_count[dbref]
del character.ndb.combat_handler
character.cmdset.delete("commands.combat.CombatCmdSet")
def at_start(self):
"""
This is called on first start but also when the script is restarted
after a server reboot. We need to re-assign this combat handler to
all characters as well as re-assign the cmdset.
"""
for character in self.db.characters.values():
self._init_character(character)
def at_stop(self):
"Called just before the script is stopped/destroyed."
for character in list(self.db.characters.values()):
# note: the list() call above disconnects list from database
self._cleanup_character(character)
def at_repeat(self):
"""
This is called every self.interval seconds (turn timeout) or
when force_repeat is called (because everyone has entered their
commands). We know this by checking the existence of the
`normal_turn_end` NAttribute, set just before calling
force_repeat.
"""
if self.ndb.normal_turn_end:
# we get here because the turn ended normally
# (force_repeat was called) - no msg output
del self.ndb.normal_turn_end
else:
# turn timeout
self.msg_all("Turn timer timed out. Continuing.")
self.end_turn()
# Combat-handler methods
def add_character(self, character):
"Add combatant to handler"
dbref = character.id
self.db.characters[dbref] = character
self.db.action_count[dbref] = 0
self.db.turn_actions[dbref] = [("defend", character, None),
("defend", character, None)]
# set up back-reference
self._init_character(character)
def remove_character(self, character):
"Remove combatant from handler"
if character.id in self.db.characters:
self._cleanup_character(character)
if not self.db.characters:
# if no more characters in battle, kill this handler
self.stop()
def msg_all(self, message):
"Send message to all combatants"
for character in self.db.characters.values():
character.msg(message)
def add_action(self, action, character, target):
"""
Called by combat commands to register an action with the handler.
action - string identifying the action, like "hit" or "parry"
character - the character performing the action
target - the target character or None
actions are stored in a dictionary keyed to each character, each
of which holds a list of max 2 actions. An action is stored as
a tuple (character, action, target).
"""
dbref = character.id
count = self.db.action_count[dbref]
if 0 <= count <= 1: # only allow 2 actions
self.db.turn_actions[dbref][count] = (action, character, target)
else:
# report if we already used too many actions
return False
self.db.action_count[dbref] += 1
return True
def check_end_turn(self):
"""
Called by the command to eventually trigger
the resolution of the turn. We check if everyone
has added all their actions; if so we call force the
script to repeat immediately (which will call
`self.at_repeat()` while resetting all timers).
"""
if all(count > 1 for count in self.db.action_count.values()):
self.ndb.normal_turn_end = True
self.force_repeat()
def end_turn(self):
"""
This resolves all actions by calling the rules module.
It then resets everything and starts the next turn. It
is called by at_repeat().
"""
resolve_combat(self, self.db.turn_actions)
if len(self.db.characters) < 2:
# less than 2 characters in battle, kill this handler
self.msg_all("Combat has ended")
self.stop()
else:
# reset counters before next turn
for character in self.db.characters.values():
self.db.characters[character.id] = character
self.db.action_count[character.id] = 0
self.db.turn_actions[character.id] = [("defend", character, None),
("defend", character, None)]
self.msg_all("Next turn begins ...")
```
This implements all the useful properties of our combat handler. This Script will survive a reboot
and will automatically re-assert itself when it comes back online. Even the current state of the
combat should be unaffected since it is saved in Attributes at every turn. An important part to note
is the use of the Script's standard `at_repeat` hook and the `force_repeat` method to end each turn.
This allows for everything to go through the same mechanisms with minimal repetition of code.
What is not present in this handler is a way for players to view the actions they set or to change
their actions once they have been added (but before the last one has added theirs). We leave this as
an exercise.
## Combat commands
Our combat commands - the commands that are to be available to us during the combat - are (in our
example) very simple. In a full implementation the commands available might be determined by the
weapon(s) held by the player or by which skills they know.
We create them in `mygame/commands/combat.py`.
```python
# mygame/commands/combat.py
from evennia import Command
class CmdHit(Command):
"""
hit an enemy
Usage:
hit <target>
Strikes the given enemy with your current weapon.
"""
key = "hit"
aliases = ["strike", "slash"]
help_category = "combat"
def func(self):
"Implements the command"
if not self.args:
self.caller.msg("Usage: hit <target>")
return
target = self.caller.search(self.args)
if not target:
return
ok = self.caller.ndb.combat_handler.add_action("hit",
self.caller,
target)
if ok:
self.caller.msg("You add 'hit' to the combat queue")
else:
self.caller.msg("You can only queue two actions per turn!")
# tell the handler to check if turn is over
self.caller.ndb.combat_handler.check_end_turn()
```
The other commands `CmdParry`, `CmdFeint`, `CmdDefend` and `CmdDisengage` look basically the same.
We should also add a custom `help` command to list all the available combat commands and what they
do.
We just need to put them all in a cmdset. We do this at the end of the same module:
```python
# mygame/commands/combat.py
from evennia import CmdSet
from evennia import default_cmds
class CombatCmdSet(CmdSet):
key = "combat_cmdset"
mergetype = "Replace"
priority = 10
no_exits = True
def at_cmdset_creation(self):
self.add(CmdHit())
self.add(CmdParry())
self.add(CmdFeint())
self.add(CmdDefend())
self.add(CmdDisengage())
self.add(CmdHelp())
self.add(default_cmds.CmdPose())
self.add(default_cmds.CmdSay())
```
## Rules module
A general way to implement a rule module is found in the [rule system tutorial](Implementing-a-game-
rule-system). Proper resolution would likely require us to change our Characters to store things
like strength, weapon skills and so on. So for this example we will settle for a very simplistic
rock-paper-scissors kind of setup with some randomness thrown in. We will not deal with damage here
but just announce the results of each turn. In a real system the Character objects would hold stats
to affect their skills, their chosen weapon affect the choices, they would be able to lose health
etc.
Within each turn, there are "sub-turns", each consisting of one action per character. The actions
within each sub-turn happens simultaneously and only once they have all been resolved we move on to
the next sub-turn (or end the full turn).
*Note: In our simple example the sub-turns don't affect each other (except for `disengage/flee`),
nor do any effects carry over between turns. The real power of a turn-based system would be to add
real tactical possibilities here though; For example if your hit got parried you could be out of
balance and your next action would be at a disadvantage. A successful feint would open up for a
subsequent attack and so on ...*
Our rock-paper-scissor setup works like this:
- `hit` beats `feint` and `flee/disengage`. It has a random chance to fail against `defend`.
- `parry` beats `hit`.
- `feint` beats `parry` and is then counted as a `hit`.
- `defend` does nothing but has a chance to beat `hit`.
- `flee/disengage` must succeed two times in a row (i.e. not beaten by a `hit` once during the
turn). If so the character leaves combat.
```python
# mygame/world/rules.py
import random
# messages
def resolve_combat(combat_handler, actiondict):
"""
This is called by the combat handler
actiondict is a dictionary with a list of two actions
for each character:
{char.id:[(action1, char, target), (action2, char, target)], ...}
"""
flee = {} # track number of flee commands per character
for isub in range(2):
# loop over sub-turns
messages = []
for subturn in (sub[isub] for sub in actiondict.values()):
# for each character, resolve the sub-turn
action, char, target = subturn
if target:
taction, tchar, ttarget = actiondict[target.id][isub]
if action == "hit":
if taction == "parry" and ttarget == char:
msg = "%s tries to hit %s, but %s parries the attack!"
messages.append(msg % (char, tchar, tchar))
elif taction == "defend" and random.random() < 0.5:
msg = "%s defends against the attack by %s."
messages.append(msg % (tchar, char))
elif taction == "flee":
msg = "%s stops %s from disengaging, with a hit!"
flee[tchar] = -2
messages.append(msg % (char, tchar))
else:
msg = "%s hits %s, bypassing their %s!"
messages.append(msg % (char, tchar, taction))
elif action == "parry":
if taction == "hit":
msg = "%s parries the attack by %s."
messages.append(msg % (char, tchar))
elif taction == "feint":
msg = "%s tries to parry, but %s feints and hits!"
messages.append(msg % (char, tchar))
else:
msg = "%s parries to no avail."
messages.append(msg % char)
elif action == "feint":
if taction == "parry":
msg = "%s feints past %s's parry, landing a hit!"
messages.append(msg % (char, tchar))
elif taction == "hit":
msg = "%s feints but is defeated by %s hit!"
messages.append(msg % (char, tchar))
else:
msg = "%s feints to no avail."
messages.append(msg % char)
elif action == "defend":
msg = "%s defends."
messages.append(msg % char)
elif action == "flee":
if char in flee:
flee[char] += 1
else:
flee[char] = 1
msg = "%s tries to disengage (two subsequent turns needed)"
messages.append(msg % char)
# echo results of each subturn
combat_handler.msg_all("\n".join(messages))
# at the end of both sub-turns, test if anyone fled
msg = "%s withdraws from combat."
for (char, fleevalue) in flee.items():
if fleevalue == 2:
combat_handler.msg_all(msg % char)
combat_handler.remove_character(char)
```
To make it simple (and to save space), this example rule module actually resolves each interchange
twice - first when it gets to each character and then again when handling the target. Also, since we
use the combat handler's `msg_all` method here, the system will get pretty spammy. To clean it up,
one could imagine tracking all the possible interactions to make sure each pair is only handled and
reported once.
## Combat initiator command
This is the last component we need, a command to initiate combat. This will tie everything together.
We store this with the other combat commands.
```python
# mygame/commands/combat.py
from evennia import create_script
class CmdAttack(Command):
"""
initiates combat
Usage:
attack <target>
This will initiate combat with <target>. If <target is
already in combat, you will join the combat.
"""
key = "attack"
help_category = "General"
def func(self):
"Handle command"
if not self.args:
self.caller.msg("Usage: attack <target>")
return
target = self.caller.search(self.args)
if not target:
return
# set up combat
if target.ndb.combat_handler:
# target is already in combat - join it
target.ndb.combat_handler.add_character(self.caller)
target.ndb.combat_handler.msg_all("%s joins combat!" % self.caller)
else:
# create a new combat handler
chandler = create_script("combat_handler.CombatHandler")
chandler.add_character(self.caller)
chandler.add_character(target)
self.caller.msg("You attack %s! You are in combat." % target)
target.msg("%s attacks you! You are in combat." % self.caller)
```
The `attack` command will not go into the combat cmdset but rather into the default cmdset. See e.g.
the [Adding Command Tutorial](Adding-Command-Tutorial) if you are unsure about how to do this.
## Expanding the example
At this point you should have a simple but flexible turn-based combat system. We have taken several
shortcuts and simplifications in this example. The output to the players is likely too verbose
during combat and too limited when it comes to informing about things surrounding it. Methods for
changing your commands or list them, view who is in combat etc is likely needed - this will require
play testing for each game and style. There is also currently no information displayed for other
people happening to be in the same room as the combat - some less detailed information should
probably be echoed to the room to
show others what's going on.

View file

@ -0,0 +1,431 @@
# Tutorial Searching For Objects
You will often want to operate on a specific object in the database. For example when a player
attacks a named target you'll need to find that target so it can be attacked. Or when a rain storm
draws in you need to find all outdoor-rooms so you can show it raining in them. This tutorial
explains Evennia's tools for searching.
## Things to search for
The first thing to consider is the base type of the thing you are searching for. Evennia organizes
its database into a few main tables: [Objects](Objects), [Accounts](Accounts), [Scripts](Scripts),
[Channels](Communications#channels), [Messages](Communication#Msg) and [Help Entries](Help-System).
Most of the time you'll likely spend your time searching for Objects and the occasional Accounts.
So to find an entity, what can be searched for?
- The `key` is the name of the entity. While you can get this from `obj.key` the *database field*
is actually named `obj.db_key` - this is useful to know only when you do [direct database
queries](Tutorial-Searching-For-Objects#queries-in-django). The one exception is `Accounts`, where
the database field for `.key` is instead named `username` (this is a Django requirement). When you
don't specify search-type, you'll usually search based on key. *Aliases* are extra names given to
Objects using something like `@alias` or `obj.aliases.add('name')`. The main search functions (see
below) will automatically search for aliases whenever you search by-key.
- [Tags](Tags) are the main way to group and identify objects in Evennia. Tags can most often be
used (sometimes together with keys) to uniquely identify an object. For example, even though you
have two locations with the same name, you can separate them by their tagging (this is how Evennia
implements 'zones' seen in other systems). Tags can also have categories, to further organize your
data for quick lookups.
- An object's [Attributes](Attributes) can also used to find an object. This can be very useful but
since Attributes can store almost any data they are far less optimized to search for than Tags or
keys.
- The object's [Typeclass](Typeclasses) indicate the sub-type of entity. A Character, Flower or
Sword are all types of Objects. A Bot is a kind of Account. The database field is called
`typeclass_path` and holds the full Python-path to the class. You can usually specify the
`typeclass` as an argument to Evennia's search functions as well as use the class directly to limit
queries.
- The `location` is only relevant for [Objects](Objects) but is a very common way to weed down the
number of candidates before starting to search. The reason is that most in-game commands tend to
operate on things nearby (in the same room) so the choices can be limited from the start.
- The database id or the '#dbref' is unique (and never re-used) within each database table. So while
there is one and only one Object with dbref `#42` there could also be an Account or Script with the
dbref `#42` at the same time. In almost all search methods you can replace the "key" search
criterion with `"#dbref"` to search for that id. This can occasionally be practical and may be what
you are used to from other code bases. But it is considered *bad practice* in Evennia to rely on
hard-coded #dbrefs to do your searches. It makes your code tied to the exact layout of the database.
It's also not very maintainable to have to remember abstract numbers. Passing the actual objects
around and searching by Tags and/or keys will usually get you what you need.
## Getting objects inside another
All in-game [Objects](Objects) have a `.contents` property that returns all objects 'inside' them
(that is, all objects which has its `.location` property set to that object. This is a simple way to
get everything in a room and is also faster since this lookup is cached and won't hit the database.
- `roomobj.contents` returns a list of all objects inside `roomobj`.
- `obj.contents` same as for a room, except this usually represents the object's inventory
- `obj.location.contents` gets everything in `obj`'s location (including `obj` itself).
- `roomobj.exits` returns all exits starting from `roomobj` (Exits are here defined as Objects with
their `destination` field set).
- `obj.location.contents_get(exclude=obj)` - this helper method returns all objects in `obj`'s
location except `obj`.
## Searching using `Object.search`
Say you have a [command](Commands), and you want it to do something to a target. You might be
wondering how you retrieve that target in code, and that's where Evennia's search utilities come in.
In the most common case, you'll often use the `search` method of the `Object` or `Account`
typeclasses. In a command, the `.caller` property will refer back to the object using the command
(usually a `Character`, which is a type of `Object`) while `.args` will contain Command's arguments:
```python
# e.g. in file mygame/commands/command.py
from evennia import default_cmds
class CmdPoke(default_cmds.MuxCommand):
"""
Pokes someone.
Usage: poke <target>
"""
key = "poke"
def func(self):
"""Executes poke command"""
target = self.caller.search(self.args)
if not target:
# we didn't find anyone, but search has already let the
# caller know. We'll just return, since we're done
return
# we found a target! we'll do stuff to them.
target.msg("You have been poked by %s." % self.caller)
self.caller.msg("You have poked %s." % target)
```
By default, the search method of a Character will attempt to find a unique object match for the
string sent to it (`self.args`, in this case, which is the arguments passed to the command by the
player) in the surroundings of the Character - the room or their inventory. If there is no match
found, the return value (which is assigned to `target`) will be `None`, and an appropriate failure
message will be sent to the Character. If there's not a unique match, `None` will again be returned,
and a different error message will be sent asking them to disambiguate the multi-match. By default,
the user can then pick out a specific match using with a number and dash preceding the name of the
object: `character.search("2-pink unicorn")` will try to find the second pink unicorn in the room.
The search method has many [arguments](github:evennia.objects.objects#defaultcharactersearch) that
allow you to refine the search, such as by designating the location to search in or only matching
specific typeclasses.
## Searching using `utils.search`
Sometimes you will want to find something that isn't tied to the search methods of a character or
account. In these cases, Evennia provides a [utility module with a number of search
functions](github:evennia.utils.search). For example, suppose you want a command that will find and
display all the rooms that are tagged as a 'hangout', for people to gather by. Here's a simple
Command to do this:
```python
# e.g. in file mygame/commands/command.py
from evennia import default_cmds
from evennia.utils.search import search_tag
class CmdListHangouts(default_cmds.MuxCommand):
"""Lists hangouts"""
key = "hangouts"
def func(self):
"""Executes 'hangouts' command"""
hangouts = search_tag(key="hangout",
category="location tags")
self.caller.msg("Hangouts available: {}".format(
", ".join(str(ob) for ob in hangouts)))
```
This uses the `search_tag` function to find all objects previously tagged with [Tags](Tags)
"hangout" and with category "location tags".
Other important search methods in `utils.search` are
- `search_object`
- `search_account`
- `search_scripts`
- `search_channel`
- `search_message`
- `search_help`
- `search_tag` - find Objects with a given Tag.
- `search_account_tag` - find Accounts with a given Tag.
- `search_script_tag` - find Scripts with a given Tag.
- `search_channel_tag` - find Channels with a given Tag.
- `search_object_attribute` - find Objects with a given Attribute.
- `search_account_attribute` - find Accounts with a given Attribute.
- `search_attribute_object` - this returns the actual Attribute, not the object it sits on.
> Note: All search functions return a Django `queryset` which is technically a list-like
representation of the database-query it's about to do. Only when you convert it to a real list, loop
over it or try to slice or access any of its contents will the datbase-lookup happen. This means you
could yourself customize the query further if you know what you are doing (see the next section).
## Queries in Django
*This is an advanced topic.*
Evennia's search methods should be sufficient for the vast majority of situations. But eventually
you might find yourself trying to figure out how to get searches for unusual circumstances: Maybe
you want to find all characters who are *not* in rooms tagged as hangouts *and* have the lycanthrope
tag *and* whose names start with a vowel, but *not* with 'Ab', and *only if* they have 3 or more
objects in their inventory ... You could in principle use one of the earlier search methods to find
all candidates and then loop over them with a lot of if statements in raw Python. But you can do
this much more efficiently by querying the database directly.
Enter [django's querysets](https://docs.djangoproject.com/en/1.11/ref/models/querysets/). A QuerySet
is the representation of a database query and can be modified as desired. Only once one tries to
retrieve the data of that query is it *evaluated* and does an actual database request. This is
useful because it means you can modify a query as much as you want (even pass it around) and only
hit the database once you are happy with it.
Evennia's search functions are themselves an even higher level wrapper around Django's queries, and
many search methods return querysets. That means that you could get the result from a search
function and modify the resulting query to your own ends to further tweak what you search for.
Evaluated querysets can either contain objects such as Character objects, or lists of values derived
from the objects. Queries usually use the 'manager' object of a class, which by convention is the
`.objects` attribute of a class. For example, a query of Accounts that contain the letter 'a' could
be:
```python
from typeclasses.accounts import Account
queryset = Account.objects.filter(username__contains='a')
```
The `filter` method of a manager takes arguments that allow you to define the query, and you can
continue to refine the query by calling additional methods until you evaluate the queryset, causing
the query to be executed and return a result. For example, if you have the result above, you could,
without causing the queryset to be evaluated yet, get rid of matches that contain the letter 'e by
doing this:
```python
queryset = result.exclude(username__contains='e')
```
> You could also have chained `.exclude` directly to the end of the previous line.
Once you try to access the result, the queryset will be evaluated automatically under the hood:
```python
accounts = list(queryset) # this fills list with matches
for account in queryset:
# do something with account
accounts = queryset[:4] # get first four matches
account = queryset[0] # get first match
# etc
```
### Limiting by typeclass
Although `Character`s, `Exit`s, `Room`s, and other children of `DefaultObject` all shares the same
underlying database table, Evennia provides a shortcut to do more specific queries only for those
typeclasses. For example, to find only `Character`s whose names start with 'A', you might do:
```python
Character.objects.filter(db_key__startswith="A")
```
If Character has a subclass `Npc` and you wanted to find only Npc's you'd instead do
```python
Npc.objects.filter(db_key__startswith="A")
```
If you wanted to search both Characters and all its subclasses (like Npc) you use the `*_family`
method which is added by Evennia:
```python
Character.objects.filter_family(db_key__startswith="A")
```
The higher up in the inheritance hierarchy you go the more objects will be included in these
searches. There is one special case, if you really want to include *everything* from a given
database table. You do that by searching on the database model itself. These are named `ObjectDB`,
`AccountDB`, `ScriptDB` etc.
```python
from evennia import AccountDB
# all Accounts in the database, regardless of typeclass
all = AccountDB.objects.all()
```
Here are the most commonly used methods to use with the `objects` managers:
- `filter` - query for a listing of objects based on search criteria. Gives empty queryset if none
were found.
- `get` - query for a single match - raises exception if none were found, or more than one was
found.
- `all` - get all instances of the particular type.
- `filter_family` - like `filter`, but search all sub classes as well.
- `get_family` - like `get`, but search all sub classes as well.
- `all_family` - like `all`, but return entities of all subclasses as well.
## Multiple conditions
If you pass more than one keyword argument to a query method, the query becomes an `AND`
relationship. For example, if we want to find characters whose names start with "A" *and* are also
werewolves (have the `lycanthrope` tag), we might do:
```python
queryset = Character.objects.filter(db_key__startswith="A", db_tags__db_key="lycanthrope")
```
To exclude lycanthropes currently in rooms tagged as hangouts, we might tack on an `.exclude` as
before:
```python
queryset = quersyet.exclude(db_location__db_tags__db_key="hangout")
```
Note the syntax of the keywords in building the queryset. For example, `db_location` is the name of
the database field sitting on (in this case) the `Character` (Object). Double underscore `__` works
like dot-notation in normal Python (it's used since dots are not allowed in keyword names). So the
instruction `db_location__db_tags__db_key="hangout"` should be read as such:
1. "On the `Character` object ... (this comes from us building this queryset using the
`Character.objects` manager)
2. ... get the value of the `db_location` field ... (this references a Room object, normally)
3. ... on that location, get the value of the `db_tags` field ... (this is a many-to-many field that
can be treated like an object for this purpose. It references all tags on the location)
4. ... through the `db_tag` manager, find all Tags having a field `db_key` set to the value
"hangout"."
This may seem a little complex at first, but this syntax will work the same for all queries. Just
remember that all *database-fields* in Evennia are prefaced with `db_`. So even though Evennia is
nice enough to alias the `db_key` field so you can normally just do `char.key` to get a character's
name, the database field is actually called `db_key` and the real name must be used for the purpose
of building a query.
> Don't confuse database fields with [Attributes](Attributes) you set via `obj.db.attr = 'foo'` or
`obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not
separate fields *on* that object like `db_key` or `db_location` are. You can get attached Attributes
manually through the `db_attributes` many-to-many field in the same way as `db_tags` above.
### Complex queries
What if you want to have a query with with `OR` conditions or negated requirements (`NOT`)? Enter
Django's Complex Query object,
[Q](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). `Q()`
objects take a normal django keyword query as its arguments. The special thing is that these Q
objects can then be chained together with set operations: `|` for OR, `&` for AND, and preceded with
`~` for NOT to build a combined, complex query.
In our original Lycanthrope example we wanted our werewolves to have names that could start with any
vowel except for the specific beginning "ab".
```python
from django.db.models import Q
from typeclasses.characters import Character
query = Q()
for letter in ("aeiouy"):
query |= Q(db_key__istartswith=letter)
query &= ~Q(db_key__istartswith="ab")
query = Character.objects.filter(query)
list_of_lycanthropes = list(query)
```
In the above example, we construct our query our of several Q objects that each represent one part
of the query. We iterate over the list of vowels, and add an `OR` condition to the query using `|=`
(this is the same idea as using `+=` which may be more familiar). Each `OR` condition checks that
the name starts with one of the valid vowels. Afterwards, we add (using `&=`) an `AND` condition
that is negated with the `~` symbol. In other words we require that any match should *not* start
with the string "ab". Note that we don't actually hit the database until we convert the query to a
list at the end (we didn't need to do that either, but could just have kept the query until we
needed to do something with the matches).
### Annotations and `F` objects
What if we wanted to filter on some condition that isn't represented easily by a field on the
object? Maybe we want to find rooms only containing five or more objects?
We *could* retrieve all interesting candidates and run them through a for-loop to get and count
their `.content` properties. We'd then just return a list of only those objects with enough
contents. It would look something like this (note: don't actually do this!):
```python
# probably not a good idea to do it this way
from typeclasses.rooms import Room
queryset = Room.objects.all() # get all Rooms
rooms = [room for room in queryset if len(room.contents) >= 5]
```
Once the number of rooms in your game increases, this could become quite expensive. Additionally, in
some particular contexts, like when using the web features of Evennia, you must have the result as a
queryset in order to use it in operations, such as in Django's admin interface when creating list
filters.
Enter [F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions) and
*annotations*. So-called F expressions allow you to do a query that looks at a value of each object
in the database, while annotations allow you to calculate and attach a value to a query. So, let's
do the same example as before directly in the database:
```python
from typeclasses.rooms import Room
from django.db.models import Count
room_count = Room.objects.annotate(num_objects=Count('locations_set'))
queryset = room_count.filter(num_objects__gte=5)
rooms = (Room.objects.annotate(num_objects=Count('locations_set'))
.filter(num_objects__gte=5))
rooms = list(rooms)
```
Here we first create an annotation `num_objects` of type `Count`, which is a Django class. Note that
use of `location_set` in that `Count`. The `*_set` is a back-reference automatically created by
Django. In this case it allows you to find all objects that *has the current object as location*.
Once we have those, they are counted.
Next we filter on this annotation, using the name `num_objects` as something we can filter for. We
use `num_objects__gte=5` which means that `num_objects` should be greater than 5. This is a little
harder to get one's head around but much more efficient than lopping over all objects in Python.
What if we wanted to compare two parameters against one another in a query? For example, what if
instead of having 5 or more objects, we only wanted objects that had a bigger inventory than they
had tags? Here an F-object comes in handy:
```python
from django.db.models import Count, F
from typeclasses.rooms import Room
result = (Room.objects.annotate(num_objects=Count('locations_set'),
num_tags=Count('db_tags'))
.filter(num_objects__gt=F('num_tags')))
```
F-objects allows for wrapping an annotated structure on the right-hand-side of the expression. It
will be evaluated on-the-fly as needed.
### Grouping By and Values
Suppose you used tags to mark someone belonging an organization. Now you want to make a list and
need to get the membership count of every organization all at once. That's where annotations and the
`.values_list` queryset method come in. Values/Values Lists are an alternate way of returning a
queryset - instead of objects, you get a list of dicts or tuples that hold selected properties from
the the matches. It also allows you a way to 'group up' queries for returning information. For
example, to get a display about each tag per Character and the names of the tag:
```python
result = (Character.objects.filter(db_tags__db_category="organization")
.values_list('db_tags__db_key')
.annotate(cnt=Count('id'))
.order_by('-cnt'))
```
The result queryset will be a list of tuples ordered in descending order by the number of matches,
in a format like the following:
```
[('Griatch Fanclub', 3872), ("Chainsol's Ainneve Testers", 2076), ("Blaufeuer's Whitespace Fixers",
1903),
("Volund's Bikeshed Design Crew", 1764), ("Tehom's Misanthropes", 1)]

View file

@ -0,0 +1,654 @@
# Tutorial for basic MUSH like game
This tutorial lets you code a small but complete and functioning MUSH-like game in Evennia. A
[MUSH](http://en.wikipedia.org/wiki/MUSH) is, for our purposes, a class of roleplay-centric games
focused on free form storytelling. Even if you are not interested in MUSH:es, this is still a good
first game-type to try since it's not so code heavy. You will be able to use the same principles for
building other types of games.
The tutorial starts from scratch. If you did the [First Steps Coding](First-Steps-Coding) tutorial
already you should have some ideas about how to do some of the steps already.
The following are the (very simplistic and cut-down) features we will implement (this was taken from
a feature request from a MUSH user new to Evennia). A Character in this system should:
- Have a “Power” score from 1 to 10 that measures how strong they are (stand-in for the stat
system).
- Have a command (e.g. `+setpower 4`) that sets their power (stand-in for character generation
code).
- Have a command (e.g. `+attack`) that lets them roll their power and produce a "Combat Score"
between `1` and `10*Power`, displaying the result and editing their object to record this number
(stand-in for `+actions` in the command code).
- Have a command that displays everyone in the room and what their most recent "Combat Score" roll
was (stand-in for the combat code).
- Have a command (e.g. `+createNPC Jenkins`) that creates an NPC with full abilities.
- Have a command to control NPCs, such as `+npc/cmd (name)=(command)` (stand-in for the NPC
controlling code).
In this tutorial we will assume you are starting from an empty database without any previous
modifications.
## Server Settings
To emulate a MUSH, the default `MULTISESSION_MODE=0` is enough (one unique session per
account/character). This is the default so you don't need to change anything. You will still be able
to puppet/unpuppet objects you have permission to, but there is no character selection out of the
box in this mode.
We will assume our game folder is called `mygame` henceforth. You should be fine with the default
SQLite3 database.
## Creating the Character
First thing is to choose how our Character class works. We don't need to define a special NPC object
-- an NPC is after all just a Character without an Account currently controlling them.
Make your changes in the `mygame/typeclasses/characters.py` file:
```python
# mygame/typeclasses/characters.py
from evennia import DefaultCharacter
class Character(DefaultCharacter):
"""
[...]
"""
def at_object_creation(self):
"This is called when object is first created, only."
self.db.power = 1
self.db.combat_score = 1
```
We defined two new [Attributes](Attributes) `power` and `combat_score` and set them to default
values. Make sure to `@reload` the server if you had it already running (you need to reload every
time you update your python code, don't worry, no accounts will be disconnected by the reload).
Note that only *new* characters will see your new Attributes (since the `at_object_creation` hook is
called when the object is first created, existing Characters won't have it). To update yourself,
run
@typeclass/force self
This resets your own typeclass (the `/force` switch is a safety measure to not do this
accidentally), this means that `at_object_creation` is re-run.
examine self
Under the "Persistent attributes" heading you should now find the new Attributes `power` and `score`
set on yourself by `at_object_creation`. If you don't, first make sure you `@reload`ed into the new
code, next look at your server log (in the terminal/console) to see if there were any syntax errors
in your code that may have stopped your new code from loading correctly.
## Character Generation
We assume in this example that Accounts first connect into a "character generation area". Evennia
also supports full OOC menu-driven character generation, but for this example, a simple start room
is enough. When in this room (or rooms) we allow character generation commands. In fact, character
generation commands will *only* be available in such rooms.
Note that this again is made so as to be easy to expand to a full-fledged game. With our simple
example, we could simply set an `is_in_chargen` flag on the account and have the `+setpower` command
check it. Using this method however will make it easy to add more functionality later.
What we need are the following:
- One character generation [Command](Commands) to set the "Power" on the `Character`.
- A chargen [CmdSet](Command-Sets) to hold this command. Lets call it `ChargenCmdset`.
- A custom `ChargenRoom` type that makes this set of commands available to players in such rooms.
- One such room to test things in.
### The +setpower command
For this tutorial we will add all our new commands to `mygame/commands/command.py` but you could
split your commands into multiple module if you prefered.
For this tutorial character generation will only consist of one [Command](Commands) to set the
Character s "power" stat. It will be called on the following MUSH-like form:
+setpower 4
Open `command.py` file. It contains documented empty templates for the base command and the
"MuxCommand" type used by default in Evennia. We will use the plain `Command` type here, the
`MuxCommand` class offers some extra features like stripping whitespace that may be useful - if so,
just import from that instead.
Add the following to the end of the `command.py` file:
```python
# end of command.py
from evennia import Command # just for clarity; already imported above
class CmdSetPower(Command):
"""
set the power of a character
Usage:
+setpower <1-10>
This sets the power of the current character. This can only be
used during character generation.
"""
key = "+setpower"
help_category = "mush"
def func(self):
"This performs the actual command"
errmsg = "You must supply a number between 1 and 10."
if not self.args:
self.caller.msg(errmsg)
return
try:
power = int(self.args)
except ValueError:
self.caller.msg(errmsg)
return
if not (1 <= power <= 10):
self.caller.msg(errmsg)
return
# at this point the argument is tested as valid. Let's set it.
self.caller.db.power = power
self.caller.msg("Your Power was set to %i." % power)
```
This is a pretty straightforward command. We do some error checking, then set the power on ourself.
We use a `help_category` of "mush" for all our commands, just so they are easy to find and separate
in the help list.
Save the file. We will now add it to a new [CmdSet](Command-Sets) so it can be accessed (in a full
chargen system you would of course have more than one command here).
Open `mygame/commands/default_cmdsets.py` and import your `command.py` module at the top. We also
import the default `CmdSet` class for the next step:
```python
from evennia import CmdSet
from commands import command
```
Next scroll down and define a new command set (based on the base `CmdSet` class we just imported at
the end of this file, to hold only our chargen-specific command(s):
```python
# end of default_cmdsets.py
class ChargenCmdset(CmdSet):
"""
This cmdset it used in character generation areas.
"""
key = "Chargen"
def at_cmdset_creation(self):
"This is called at initialization"
self.add(command.CmdSetPower())
```
In the future you can add any number of commands to this cmdset, to expand your character generation
system as you desire. Now we need to actually put that cmdset on something so it's made available to
users. We could put it directly on the Character, but that would make it available all the time.
It's cleaner to put it on a room, so it's only available when players are in that room.
### Chargen areas
We will create a simple Room typeclass to act as a template for all our Chargen areas. Edit
`mygame/typeclasses/rooms.py` next:
```python
from commands.default_cmdsets import ChargenCmdset
# ...
# down at the end of rooms.py
class ChargenRoom(Room):
"""
This room class is used by character-generation rooms. It makes
the ChargenCmdset available.
"""
def at_object_creation(self):
"this is called only at first creation"
self.cmdset.add(ChargenCmdset, permanent=True)
```
Note how new rooms created with this typeclass will always start with `ChargenCmdset` on themselves.
Don't forget the `permanent=True` keyword or you will lose the cmdset after a server reload. For
more information about [Command Sets](Command-Sets) and [Commands](Commands), see the respective
links.
### Testing chargen
First, make sure you have `@reload`ed the server (or use `evennia reload` from the terminal) to have
your new python code added to the game. Check your terminal and fix any errors you see - the error
traceback lists exactly where the error is found - look line numbers in files you have changed.
We can't test things unless we have some chargen areas to test. Log into the game (you should at
this point be using the new, custom Character class). Let's dig a chargen area to test.
@dig chargen:rooms.ChargenRoom = chargen,finish
If you read the help for `@dig` you will find that this will create a new room named `chargen`. The
part after the `:` is the python-path to the Typeclass you want to use. Since Evennia will
automatically try the `typeclasses` folder of our game directory, we just specify
`rooms.ChargenRoom`, meaning it will look inside the module `rooms.py` for a class named
`ChargenRoom` (which is what we created above). The names given after `=` are the names of exits to
and from the room from your current location. You could also append aliases to each one name, such
as `chargen;character generation`.
So in summary, this will create a new room of type ChargenRoom and open an exit `chargen` to it and
an exit back here named `finish`. If you see errors at this stage, you must fix them in your code.
`@reload`
between fixes. Don't continue until the creation seems to have worked okay.
chargen
This should bring you to the chargen room. Being in there you should now have the `+setpower`
command available, so test it out. When you leave (via the `finish` exit), the command will go away
and trying `+setpower` should now give you a command-not-found error. Use `ex me` (as a privileged
user) to check so the `Power` [Attribute](Attributes) has been set correctly.
If things are not working, make sure your typeclasses and commands are free of bugs and that you
have entered the paths to the various command sets and commands correctly. Check the logs or command
line for tracebacks and errors.
## Combat System
We will add our combat command to the default command set, meaning it will be available to everyone
at all times. The combat system consists of a `+attack` command to get how successful our attack is.
We also change the default `look` command to display the current combat score.
### Attacking with the +attack command
Attacking in this simple system means rolling a random "combat score" influenced by the `power` stat
set during Character generation:
> +attack
You +attack with a combat score of 12!
Go back to `mygame/commands/command.py` and add the command to the end like this:
```python
import random
# ...
class CmdAttack(Command):
"""
issues an attack
Usage:
+attack
This will calculate a new combat score based on your Power.
Your combat score is visible to everyone in the same location.
"""
key = "+attack"
help_category = "mush"
def func(self):
"Calculate the random score between 1-10*Power"
caller = self.caller
power = caller.db.power
if not power:
# this can happen if caller is not of
# our custom Character typeclass
power = 1
combat_score = random.randint(1, 10 * power)
caller.db.combat_score = combat_score
# announce
message = "%s +attack%s with a combat score of %s!"
caller.msg(message % ("You", "", combat_score))
caller.location.msg_contents(message %
(caller.key, "s", combat_score),
exclude=caller)
```
What we do here is simply to generate a "combat score" using Python's inbuilt `random.randint()`
function. We then store that and echo the result to everyone involved.
To make the `+attack` command available to you in game, go back to
`mygame/commands/default_cmdsets.py` and scroll down to the `CharacterCmdSet` class. At the correct
place add this line:
```python
self.add(command.CmdAttack())
```
`@reload` Evennia and the `+attack` command should be available to you. Run it and use e.g. `@ex` to
make sure the `combat_score` attribute is saved correctly.
### Have "look" show combat scores
Players should be able to view all current combat scores in the room. We could do this by simply
adding a second command named something like `+combatscores`, but we will instead let the default
`look` command do the heavy lifting for us and display our scores as part of its normal output, like
this:
> look Tom
Tom (combat score: 3)
This is a great warrior.
We don't actually have to modify the `look` command itself however. To understand why, take a look
at how the default `look` is actually defined. It sits in `evennia/commands/default/general.py` (or
browse it online
[here](https://github.com/evennia/evennia/blob/master/evennia/commands/default/general.py#L44)).
You will find that the actual return text is done by the `look` command calling a *hook method*
named `return_appearance` on the object looked at. All the `look` does is to echo whatever this hook
returns. So what we need to do is to edit our custom Character typeclass and overload its
`return_appearance` to return what we want (this is where the advantage of having a custom typeclass
comes into play for real).
Go back to your custom Character typeclass in `mygame/typeclasses/characters.py`. The default
implementation of `return appearance` is found in `evennia.DefaultCharacter` (or online
[here](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1438)). If you
want to make bigger changes you could copy & paste the whole default thing into our overloading
method. In our case the change is small though:
```python
class Character(DefaultCharacter):
"""
[...]
"""
def at_object_creation(self):
"This is called when object is first created, only."
self.db.power = 1
self.db.combat_score = 1
def return_appearance(self, looker):
"""
The return from this method is what
looker sees when looking at this object.
"""
text = super().return_appearance(looker)
cscore = " (combat score: %s)" % self.db.combat_score
if "\n" in text:
# text is multi-line, add score after first line
first_line, rest = text.split("\n", 1)
text = first_line + cscore + "\n" + rest
else:
# text is only one line; add score to end
text += cscore
return text
```
What we do is to simply let the default `return_appearance` do its thing (`super` will call the
parent's version of the same method). We then split out the first line of this text, append our
`combat_score` and put it back together again.
`@reload` the server and you should be able to look at other Characters and see their current combat
scores.
> Note: A potentially more useful way to do this would be to overload the entire `return_appearance`
of the `Room`s of your mush and change how they list their contents; in that way one could see all
combat scores of all present Characters at the same time as looking at the room. We leave this as an
exercise.
## NPC system
Here we will re-use the Character class by introducing a command that can create NPC objects. We
should also be able to set its Power and order it around.
There are a few ways to define the NPC class. We could in theory create a custom typeclass for it
and put a custom NPC-specific cmdset on all NPCs. This cmdset could hold all manipulation commands.
Since we expect NPC manipulation to be a common occurrence among the user base however, we will
instead put all relevant NPC commands in the default command set and limit eventual access with
[Permissions and Locks](Locks#Permissions).
### Creating an NPC with +createNPC
We need a command for creating the NPC, this is a very straightforward command:
> +createnpc Anna
You created the NPC 'Anna'.
At the end of `command.py`, create our new command:
```python
from evennia import create_object
class CmdCreateNPC(Command):
"""
create a new npc
Usage:
+createNPC <name>
Creates a new, named NPC. The NPC will start with a Power of 1.
"""
key = "+createnpc"
aliases = ["+createNPC"]
locks = "call:not perm(nonpcs)"
help_category = "mush"
def func(self):
"creates the object and names it"
caller = self.caller
if not self.args:
caller.msg("Usage: +createNPC <name>")
return
if not caller.location:
# may not create npc when OOC
caller.msg("You must have a location to create an npc.")
return
# make name always start with capital letter
name = self.args.strip().capitalize()
# create npc in caller's location
npc = create_object("characters.Character",
key=name,
location=caller.location,
locks="edit:id(%i) and perm(Builders);call:false()" % caller.id)
# announce
message = "%s created the NPC '%s'."
caller.msg(message % ("You", name))
caller.location.msg_contents(message % (caller.key, name),
exclude=caller)
```
Here we define a `+createnpc` (`+createNPC` works too) that is callable by everyone *not* having the
`nonpcs` "[permission](Locks#Permissions)" (in Evennia, a "permission" can just as well be used to
block access, it depends on the lock we define). We create the NPC object in the caller's current
location, using our custom `Character` typeclass to do so.
We set an extra lock condition on the NPC, which we will use to check who may edit the NPC later --
we allow the creator to do so, and anyone with the Builders permission (or higher). See
[Locks](Locks) for more information about the lock system.
Note that we just give the object default permissions (by not specifying the `permissions` keyword
to the `create_object()` call). In some games one might want to give the NPC the same permissions
as the Character creating them, this might be a security risk though.
Add this command to your default cmdset the same way you did the `+attack` command earlier.
`@reload` and it will be available to test.
### Editing the NPC with +editNPC
Since we re-used our custom character typeclass, our new NPC already has a *Power* value - it
defaults to 1. How do we change this?
There are a few ways we can do this. The easiest is to remember that the `power` attribute is just a
simple [Attribute](Attributes) stored on the NPC object. So as a Builder or Admin we could set this
right away with the default `@set` command:
@set mynpc/power = 6
The `@set` command is too generally powerful though, and thus only available to staff. We will add a
custom command that only changes the things we want players to be allowed to change. We could in
principle re-work our old `+setpower` command, but let's try something more useful. Let's make a
`+editNPC` command.
> +editNPC Anna/power = 10
Set Anna's property 'power' to 10.
This is a slightly more complex command. It goes at the end of your `command.py` file as before.
```python
class CmdEditNPC(Command):
"""
edit an existing NPC
Usage:
+editnpc <name>[/<attribute> [= value]]
Examples:
+editnpc mynpc/power = 5
+editnpc mynpc/power - displays power value
+editnpc mynpc - shows all editable
attributes and values
This command edits an existing NPC. You must have
permission to edit the NPC to use this.
"""
key = "+editnpc"
aliases = ["+editNPC"]
locks = "cmd:not perm(nonpcs)"
help_category = "mush"
def parse(self):
"We need to do some parsing here"
args = self.args
propname, propval = None, None
if "=" in args:
args, propval = [part.strip() for part in args.rsplit("=", 1)]
if "/" in args:
args, propname = [part.strip() for part in args.rsplit("/", 1)]
# store, so we can access it below in func()
self.name = args
self.propname = propname
# a propval without a propname is meaningless
self.propval = propval if propname else None
def func(self):
"do the editing"
allowed_propnames = ("power", "attribute1", "attribute2")
caller = self.caller
if not self.args or not self.name:
caller.msg("Usage: +editnpc name[/propname][=propval]")
return
npc = caller.search(self.name)
if not npc:
return
if not npc.access(caller, "edit"):
caller.msg("You cannot change this NPC.")
return
if not self.propname:
# this means we just list the values
output = "Properties of %s:" % npc.key
for propname in allowed_propnames:
propvalue = npc.attributes.get(propname, default="N/A")
output += "\n %s = %s" % (propname, propvalue)
caller.msg(output)
elif self.propname not in allowed_propnames:
caller.msg("You may only change %s." %
", ".join(allowed_propnames))
elif self.propval:
# assigning a new propvalue
# in this example, the properties are all integers...
intpropval = int(self.propval)
npc.attributes.add(self.propname, intpropval)
caller.msg("Set %s's property '%s' to %s" %
(npc.key, self.propname, self.propval))
else:
# propname set, but not propval - show current value
caller.msg("%s has property %s = %s" %
(npc.key, self.propname,
npc.attributes.get(self.propname, default="N/A")))
```
This command example shows off the use of more advanced parsing but otherwise it's mostly error
checking. It searches for the given npc in the same room, and checks so the caller actually has
permission to "edit" it before continuing. An account without the proper permission won't even be
able to view the properties on the given NPC. It's up to each game if this is the way it should be.
Add this to the default command set like before and you should be able to try it out.
_Note: If you wanted a player to use this command to change an on-object property like the NPC's
name (the `key` property), you'd need to modify the command since "key" is not an Attribute (it is
not retrievable via `npc.attributes.get` but directly via `npc.key`). We leave this as an optional
exercise._
### Making the NPC do stuff - the +npc command
Finally, we will make a command to order our NPC around. For now, we will limit this command to only
be usable by those having the "edit" permission on the NPC. This can be changed if it's possible for
anyone to use the NPC.
The NPC, since it inherited our Character typeclass has access to most commands a player does. What
it doesn't have access to are Session and Player-based cmdsets (which means, among other things that
they cannot chat on channels, but they could do that if you just added those commands). This makes
the `+npc` command simple:
+npc Anna = say Hello!
Anna says, 'Hello!'
Again, add to the end of your `command.py` module:
```python
class CmdNPC(Command):
"""
controls an NPC
Usage:
+npc <name> = <command>
This causes the npc to perform a command as itself. It will do so
with its own permissions and accesses.
"""
key = "+npc"
locks = "call:not perm(nonpcs)"
help_category = "mush"
def parse(self):
"Simple split of the = sign"
name, cmdname = None, None
if "=" in self.args:
name, cmdname = [part.strip()
for part in self.args.rsplit("=", 1)]
self.name, self.cmdname = name, cmdname
def func(self):
"Run the command"
caller = self.caller
if not self.cmdname:
caller.msg("Usage: +npc <name> = <command>")
return
npc = caller.search(self.name)
if not npc:
return
if not npc.access(caller, "edit"):
caller.msg("You may not order this NPC to do anything.")
return
# send the command order
npc.execute_cmd(self.cmdname)
caller.msg("You told %s to do '%s'." % (npc.key, self.cmdname))
```
Note that if you give an erroneous command, you will not see any error message, since that error
will be returned to the npc object, not to you. If you want players to see this, you can give the
caller's session ID to the `execute_cmd` call, like this:
```python
npc.execute_cmd(self.cmdname, sessid=self.caller.sessid)
```
Another thing to remember is however that this is a very simplistic way to control NPCs. Evennia
supports full puppeting very easily. An Account (assuming the "puppet" permission was set correctly)
could simply do `@ic mynpc` and be able to play the game "as" that NPC. This is in fact just what
happens when an Account takes control of their normal Character as well.
## Concluding remarks
This ends the tutorial. It looks like a lot of text but the amount of code you have to write is
actually relatively short. At this point you should have a basic skeleton of a game and a feel for
what is involved in coding your game.
From here on you could build a few more ChargenRooms and link that to a bigger grid. The `+setpower`
command can either be built upon or accompanied by many more to get a more elaborate character
generation.
The simple "Power" game mechanic should be easily expandable to something more full-fledged and
useful, same is true for the combat score principle. The `+attack` could be made to target a
specific player (or npc) and automatically compare their relevant attributes to determine a result.
To continue from here, you can take a look at the [Tutorial World](Tutorial-World-Introduction). For
more specific ideas, see the [other tutorials and hints](Tutorials) as well
as the [Developer Central](Developer-Central).

View file

@ -0,0 +1,127 @@
# Web Tutorial
Evennia uses the [Django](https://www.djangoproject.com/) web framework as the basis of both its
database configuration and the website it provides. While a full understanding of Django requires
reading the Django documentation, we have provided this tutorial to get you running with the basics
and how they pertain to Evennia. This text details getting everything set up. The [Web-based
Character view Tutorial](Web-Character-View-Tutorial) gives a more explicit example of making a
custom web page connected to your game, and you may want to read that after finishing this guide.
## A Basic Overview
Django is a web framework. It gives you a set of development tools for building a website quickly
and easily.
Django projects are split up into *apps* and these apps all contribute to one project. For instance,
you might have an app for conducting polls, or an app for showing news posts or, like us, one for
creating a web client.
Each of these applications has a `urls.py` file, which specifies what
[URL](http://en.wikipedia.org/wiki/Uniform_resource_locator)s are used by the app, a `views.py` file
for the code that the URLs activate, a `templates` directory for displaying the results of that code
in [HTML](http://en.wikipedia.org/wiki/Html) for the user, and a `static` folder that holds assets
like [CSS](http://en.wikipedia.org/wiki/CSS), [Javascript](http://en.wikipedia.org/wiki/Javascript),
and Image files (You may note your mygame/web folder does not have a `static` or `template` folder.
This is intended and explained further below). Django applications may also have a `models.py` file
for storing information in the database. We will not change any models here, take a look at the [New
Models](New-Models) page (as well as the [Django
docs](https://docs.djangoproject.com/en/1.7/topics/db/models/) on models) if you are interested.
There is also a root `urls.py` that determines the URL structure for the entire project. A starter
`urls.py` is included in the default game template, and automatically imports all of Evennia's
default URLs for you. This is located in `web/urls.py`.
## Changing the logo on the front page
Evennia's default logo is a fun little googly-eyed snake wrapped around a gear globe. As cute as it
is, it probably doesn't represent your game. So one of the first things you may wish to do is
replace it with a logo of your own.
Django web apps all have _static assets_: CSS files, Javascript files, and Image files. In order to
make sure the final project has all the static files it needs, the system collects the files from
every app's `static` folder and places it in the `STATIC_ROOT` defined in `settings.py`. By default,
the Evennia `STATIC_ROOT` is in `web/static`.
Because Django pulls files from all of those separate places and puts them in one folder, it's
possible for one file to overwrite another. We will use this to plug in our own files without having
to change anything in the Evennia itself.
By default, Evennia is configured to pull files you put in the `web/static_overrides` *after* all
other static files. That means that files in `static_overrides` folder will overwrite any previously
loaded files *having the same path under its static folder*. This last part is important to repeat:
To overload the static resource from a standard `static` folder you need to replicate the path of
folders and file names from that `static` folder in exactly the same way inside `static_overrides`.
Let's see how this works for our logo. The default web application is in the Evennia library itself,
in `evennia/web/`. We can see that there is a `static` folder here. If we browse down, we'll
eventually find the full path to the Evennia logo file:
`evennia/web/static/evennia_general/images/evennia_logo.png`.
Inside our `static_overrides` we must replicate the part of the path inside the `static` folder, in
other words, we must replicate `evennia_general/images/evennia_logo.png`.
So, to change the logo, we need to create the folder path `evennia_general/images/` in
`static_overrides`. We then rename our own logo file to `evennia_logo.png` and copy it there. The
final path for this file would thus be:
`web/static_overrides/evennia_general/images/evennia_logo.png` in your local game folder.
To get this file pulled in, just change to your own game directory and reload the server:
```
evennia reload
```
This will reload the configuration and bring in the new static file(s). If you didn't want to reload
the server you could instead use
```
evennia collectstatic
```
to only update the static files without any other changes.
> **Note**: Evennia will collect static files automatically during startup. So if `evennia
collectstatic` reports finding 0 files to collect, make sure you didn't start the engine at some
point - if so the collector has already done its work! To make sure, connect to the website and
check so the logo has actually changed to your own version.
> **Note**: Sometimes the static asset collector can get confused. If no matter what you do, your
overridden files aren't getting copied over the defaults, try removing the target file (or
everything) in the `web/static` directory, and re-running `collectstatic` to gather everything from
scratch.
## Changing the Front Page's Text
The default front page for Evennia contains information about the Evennia project. You'll probably
want to replace this information with information about your own project. Changing the page template
is done in a similar way to changing static resources.
Like static files, Django looks through a series of template folders to find the file it wants. The
difference is that Django does not copy all of the template files into one place, it just searches
through the template folders until it finds a template that matches what it's looking for. This
means that when you edit a template, the changes are instant. You don't have to reload the server or
run any extra commands to see these changes - reloading the web page in your browser is enough.
To replace the index page's text, we'll need to find the template for it. We'll go into more detail
about how to determine which template is used for rendering a page in the [Web-based Character view
Tutorial](Web-Character-View-Tutorial). For now, you should know that the template we want to change
is stored in `evennia/web/website/templates/website/index.html`.
To replace this template file, you will put your changed template inside the
`web/template_overrides/website` directory in your game folder. In the same way as with static
resources you must replicate the path inside the default `template` directory exactly. So we must
copy our replacement template named `index.html` there (or create the `website` directory in
web/template_overrides` if it does not exist, first). The final path to the file should thus be:
`web/template_overrides/website/index.html` within your game directory.
Note that it is usually easier to just copy the original template over and edit it in place. The
original file already has all the markup and tags, ready for editing.
## Further reading
For further hints on working with the web presence, you could now continue to the [Web-based
Character view Tutorial](Web-Character-View-Tutorial) where you learn to make a web page that
displays in-game character stats. You can also look at [Django's own
tutorial](https://docs.djangoproject.com/en/1.7/intro/tutorial01/) to get more insight in how Django
works and what possibilities exist.

View file

@ -0,0 +1,127 @@
# Tutorial Aggressive NPCs
This tutorial shows the implementation of an NPC object that responds to characters entering their
location. In this example the NPC has the option to respond aggressively or not, but any actions
could be triggered this way.
One could imagine using a [Script](Scripts) that is constantly checking for newcomers. This would be
highly inefficient (most of the time its check would fail). Instead we handle this on-demand by
using a couple of existing object hooks to inform the NPC that a Character has entered.
It is assumed that you already know how to create custom room and character typeclasses, please see
the [Basic Game tutorial](Tutorial-for-basic-MUSH-like-game) if you haven't already done this.
What we will need is the following:
- An NPC typeclass that can react when someone enters.
- A custom [Room](Objects#rooms) typeclass that can tell the NPC that someone entered.
- We will also tweak our default `Character` typeclass a little.
To begin with, we need to create an NPC typeclass. Create a new file inside of your typeclasses
folder and name it `npcs.py` and then add the following code:
```python
from typeclasses.characters import Character
class NPC(Character):
"""
A NPC typeclass which extends the character class.
"""
def at_char_entered(self, character):
"""
A simple is_aggressive check.
Can be expanded upon later.
"""
if self.db.is_aggressive:
self.execute_cmd(f"say Graaah, die {character}!")
else:
self.execute_cmd(f"say Greetings, {character}!")
```
We will define our custom `Character` typeclass below. As for the new `at_char_entered` method we've
just defined, we'll ensure that it will be called by the room where the NPC is located, when a
player enters that room. You'll notice that right now, the NPC merely speaks. You can expand this
part as you like and trigger all sorts of effects here (like combat code, fleeing, bartering or
quest-giving) as your game design dictates.
Now your `typeclasses.rooms` module needs to have the following added:
```python
# Add this import to the top of your file.
from evennia import utils
# Add this hook in any empty area within your Room class.
def at_object_receive(self, obj, source_location):
if utils.inherits_from(obj, 'typeclasses.npcs.NPC'): # An NPC has entered
return
elif utils.inherits_from(obj, 'typeclasses.characters.Character'):
# A PC has entered.
# Cause the player's character to look around.
obj.execute_cmd('look')
for item in self.contents:
if utils.inherits_from(item, 'typeclasses.npcs.NPC'):
# An NPC is in the room
item.at_char_entered(obj)
```
`inherits_from` must be given the full path of the class. If the object inherited a class from your
`world.races` module, then you would check inheritance with `world.races.Human`, for example. There
is no need to import these prior, as we are passing in the full path. As a matter of a fact,
`inherits_from` does not properly work if you import the class and only pass in the name of the
class.
> Note:
[at_object_receive](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1529)
is a default hook of the `DefaultObject` typeclass (and its children). Here we are overriding this
hook in our customized room typeclass to suit our needs.
This room checks the typeclass of objects entering it (using `utils.inherits_from` and responds to
`Characters`, ignoring other NPCs or objects. When triggered the room will look through its
contents and inform any `NPCs inside by calling their `at_char_entered` method.
You'll also see that we have added a 'look' into this code. This is because, by default, the
`at_object_receive` is carried out *before* the character's `at_after_move` which, we will now
overload. This means that a character entering would see the NPC perform its actions before the
'look' command. Deactivate the look command in the default `Character` class within the
`typeclasses.characters` module:
```python
# Add this hook in any blank area within your Character class.
def at_after_move(self, source_location):
"""
Default is to look around after a move
Note: This has been moved to Room.at_object_receive
"""
#self.execute_cmd('look')
pass
```
Now let's create an NPC and make it aggressive. Type the following commands into your MUD client:
```
reload
create/drop Orc:npcs.NPC
```
> Note: You could also give the path as `typeclasses.npcs.NPC`, but Evennia will look into the
`typeclasses` folder automatically, so this is a little shorter.
When you enter the aggressive NPC's location, it will default to using its peaceful action (say your
name is Anna):
```
Orc says, "Greetings, Anna!"
```
Now we turn on the aggressive mode (we do it manually but it could also be triggered by some sort of
AI code).
```
set orc/is_aggressive = True
```
Now it will perform its aggressive action whenever a character enters.
```
Orc says, "Graaah, die, Anna!"
```

View file

@ -0,0 +1,110 @@
# Tutorial NPCs listening
This tutorial shows the implementation of an NPC object that responds to characters speaking in
their location. In this example the NPC parrots what is said, but any actions could be triggered
this way.
It is assumed that you already know how to create custom room and character typeclasses, please see
the [Basic Game tutorial](Tutorial-for-basic-MUSH-like-game) if you haven't already done this.
What we will need is simply a new NPC typeclass that can react when someone speaks.
```python
# mygame/typeclasses/npc.py
from characters import Character
class Npc(Character):
"""
A NPC typeclass which extends the character class.
"""
def at_heard_say(self, message, from_obj):
"""
A simple listener and response. This makes it easy to change for
subclasses of NPCs reacting differently to says.
"""
# message will be on the form `<Person> says, "say_text"`
# we want to get only say_text without the quotes and any spaces
message = message.split('says, ')[1].strip(' "')
# we'll make use of this in .msg() below
return "%s said: '%s'" % (from_obj, message)
```
When someone in the room speaks to this NPC, its `msg` method will be called. We will modify the
NPCs `.msg` method to catch says so the NPC can respond.
```python
# mygame/typeclasses/npc.py
from characters import Character
class Npc(Character):
# [at_heard_say() goes here]
def msg(self, text=None, from_obj=None, **kwargs):
"Custom msg() method reacting to say."
if from_obj != self:
# make sure to not repeat what we ourselves said or we'll create a loop
try:
# if text comes from a say, `text` is `('say_text', {'type': 'say'})`
say_text, is_say = text[0], text[1]['type'] == 'say'
except Exception:
is_say = False
if is_say:
# First get the response (if any)
response = self.at_heard_say(say_text, from_obj)
# If there is a response
if response != None:
# speak ourselves, using the return
self.execute_cmd("say %s" % response)
# this is needed if anyone ever puppets this NPC - without it you would never
# get any feedback from the server (not even the results of look)
super().msg(text=text, from_obj=from_obj, **kwargs)
```
So if the NPC gets a say and that say is not coming from the NPC itself, it will echo it using the
`at_heard_say` hook. Some things of note in the above example:
- The `text` input can be on many different forms depending on where this `msg` is called from.
Instead of trying to analyze `text` in detail with a range of `if` statements we just assume the
form we want and catch the error if it does not match. This simplifies the code considerably. It's
called 'leap before you look' and is a Python paradigm that may feel unfamiliar if you are used to
other languages. Here we 'swallow' the error silently, which is fine when the code checked is
simple. If not we may want to import `evennia.logger.log_trace` and add `log_trace()` in the
`except` clause.
- We use `execute_cmd` to fire the `say` command back. We could also have called
`self.location.msg_contents` directly but using the Command makes sure all hooks are called (so
those seeing the NPC's `say` can in turn react if they want).
- Note the comments about `super` at the end. This will trigger the 'default' `msg` (in the parent
class) as well. It's not really necessary as long as no one puppets the NPC (by `@ic <npcname>`) but
it's wise to keep in there since the puppeting player will be totally blind if `msg()` is never
returning anything to them!
Now that's done, let's create an NPC and see what it has to say for itself.
```
@reload
@create/drop Guild Master:npc.Npc
```
(you could also give the path as `typeclasses.npc.Npc`, but Evennia will look into the `typeclasses`
folder automatically so this is a little shorter).
> say hi
You say, "hi"
Guild Master says, "Anna said: 'hi'"
## Assorted notes
There are many ways to implement this kind of functionality. An alternative example to overriding
`msg` would be to modify the `at_say` hook on the *Character* instead. It could detect that it's
sending to an NPC and call the `at_heard_say` hook directly.
While the tutorial solution has the advantage of being contained only within the NPC class,
combining this with using the Character class gives more direct control over how the NPC will react.
Which way to go depends on the design requirements of your particular game.

View file

@ -0,0 +1,96 @@
# Tutorial Tweeting Game Stats
This tutorial will create a simple script that will send a tweet to your already configured twitter
account. Please see: [How to connect Evennia to Twitter](How-to-connect-Evennia-to-Twitter) if you
haven't already done so.
The script could be expanded to cover a variety of statistics you might wish to tweet about
regularly, from player deaths to how much currency is in the economy etc.
```python
# evennia/typeclasses/tweet_stats.py
import twitter
from random import randint
from django.conf import settings
from evennia import ObjectDB
from evennia import spawner
from evennia import logger
from evennia import DefaultScript
class TweetStats(DefaultScript):
"""
This implements the tweeting of stats to a registered twitter account
"""
# standard Script hooks
def at_script_creation(self):
"Called when script is first created"
self.key = "tweet_stats"
self.desc = "Tweets interesting stats about the game"
self.interval = 86400 # 1 day timeout
self.start_delay = False
def at_repeat(self):
"""
This is called every self.interval seconds to tweet interesting stats about the game.
"""
api = twitter.Api(consumer_key='consumer_key',
consumer_secret='consumer_secret',
access_token_key='access_token_key',
access_token_secret='access_token_secret')
number_tweet_outputs = 2
tweet_output = randint(1, number_tweet_outputs)
if tweet_output == 1:
##Game Chars, Rooms, Objects taken from @stats command
nobjs = ObjectDB.objects.count()
base_char_typeclass = settings.BASE_CHARACTER_TYPECLASS
nchars = ObjectDB.objects.filter(db_typeclass_path=base_char_typeclass).count()
nrooms =
ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=base_char_typeclass).count()
nexits = ObjectDB.objects.filter(db_location__isnull=False,
db_destination__isnull=False).count()
nother = nobjs - nchars - nrooms - nexits
tweet = "Chars: %s, Rooms: %s, Objects: %s" %(nchars, nrooms, nother)
else:
if tweet_output == 2: ##Number of prototypes and 3 random keys - taken from @spawn
command
prototypes = spawner.spawn(return_prototypes=True)
keys = prototypes.keys()
nprots = len(prototypes)
tweet = "Prototype Count: %s Random Keys: " % nprots
tweet += " %s" % keys[randint(0,len(keys)-1)]
for x in range(0,2): ##tweet 3
tweet += ", %s" % keys[randint(0,len(keys)-1)]
# post the tweet
try:
response = api.PostUpdate(tweet)
except:
logger.log_trace("Tweet Error: When attempting to tweet %s" % tweet)
```
In the `at_script_creation` method, we configure the script to fire immediately (useful for testing)
and setup the delay (1 day) as well as script information seen when you use `@scripts`
In the `at_repeat` method (which is called immediately and then at interval seconds later) we setup
the Twitter API (just like in the initial configuration of twitter). numberTweetOutputs is used to
show how many different types of outputs we have (in this case 2). We then build the tweet based on
randomly choosing between these outputs.
1. Shows the number of Player Characters, Rooms and Other/Objects
2. Shows the number of prototypes currently in the game and then selects 3 random keys to show
[Scripts Information](Scripts) will show you how to add it as a Global script, however, for testing
it may be useful to start/stop it quickly from within the game. Assuming that you create the file
as `mygame/typeclasses/tweet_stats.py` it can be started by using the following command
@script Here = tweet_stats.TweetStats

View file

@ -0,0 +1,422 @@
# Tutorial Vehicles
This tutorial explains how you can create vehicles that can move around in your world. The tutorial
will explain how to create a train, but this can be equally applied to create other kind of vehicles
(cars, planes, boats, spaceships, submarines, ...).
## How it works
Objects in Evennia have an interesting property: you can put any object inside another object. This
is most obvious in rooms: a room in Evennia is just like any other game object (except rooms tend to
not themselves be inside anything else).
Our train will be similar: it will be an object that other objects can get inside. We then simply
move the Train, which brings along everyone inside it.
## Creating our train object
The first step we need to do is create our train object, including a new typeclass. To do this,
create a new file, for instance in `mygame/typeclasses/train.py` with the following content:
```python
# file mygame/typeclasses/train.py
from evennia import DefaultObject
class TrainObject(DefaultObject):
def at_object_creation(self):
# We'll add in code here later.
pass
```
Now we can create our train in our game:
```
@create/drop train:train.TrainObject
```
Now this is just an object that doesn't do much yet... but we can already force our way inside it
and back (assuming we created it in limbo).
```
@tel train
@tel limbo
```
## Entering and leaving the train
Using the `@tel`command like shown above is obviously not what we want. `@tel` is an admin command
and normal players will thus never be able to enter the train! It is also not really a good idea to
use [Exits](Objects#exits) to get in and out of the train - Exits are (at least by default) objects
too. They point to a specific destination. If we put an Exit in this room leading inside the train
it would stay here when the train moved away (still leading into the train like a magic portal!). In
the same way, if we put an Exit object inside the train, it would always point back to this room,
regardless of where the Train has moved. Now, one *could* define custom Exit types that move with
the train or change their destination in the right way - but this seems to be a pretty cumbersome
solution.
What we will do instead is to create some new [commands](Commands): one for entering the train and
another for leaving it again. These will be stored *on the train object* and will thus be made
available to whomever is either inside it or in the same room as the train.
Let's create a new command module as `mygame/commands/train.py`:
```python
# mygame/commands/train.py
from evennia import Command, CmdSet
class CmdEnterTrain(Command):
"""
entering the train
Usage:
enter train
This will be available to players in the same location
as the train and allows them to embark.
"""
key = "enter train"
locks = "cmd:all()"
def func(self):
train = self.obj
self.caller.msg("You board the train.")
self.caller.move_to(train)
class CmdLeaveTrain(Command):
"""
leaving the train
Usage:
leave train
This will be available to everyone inside the
train. It allows them to exit to the train's
current location.
"""
key = "leave train"
locks = "cmd:all()"
def func(self):
train = self.obj
parent = train.location
self.caller.move_to(parent)
class CmdSetTrain(CmdSet):
def at_cmdset_creation(self):
self.add(CmdEnterTrain())
self.add(CmdLeaveTrain())
```
Note that while this seems like a lot of text, the majority of lines here are taken up by
documentation.
These commands are work in a pretty straightforward way: `CmdEnterTrain` moves the location of the
player to inside the train and `CmdLeaveTrain` does the opposite: it moves the player back to the
current location of the train (back outside to its current location). We stacked them in a
[cmdset](Command-Sets) `CmdSetTrain` so they can be used.
To make the commands work we need to add this cmdset to our train typeclass:
```python
# file mygame/typeclasses/train.py
from evennia import DefaultObject
from commands.train import CmdSetTrain
class TrainObject(DefaultObject):
def at_object_creation(self):
self.cmdset.add_default(CmdSetTrain)
```
If we now `@reload` our game and reset our train, those commands should work and we can now enter
and leave the train:
```
@reload
@typeclass/force/reset train = train.TrainObject
enter train
leave train
```
Note the switches used with the `@typeclass` command: The `/force` switch is necessary to assign our
object the same typeclass we already have. The `/reset` re-triggers the typeclass'
`at_object_creation()` hook (which is otherwise only called the very first an instance is created).
As seen above, when this hook is called on our train, our new cmdset will be loaded.
## Locking down the commands
If you have played around a bit, you've probably figured out that you can use `leave train` when
outside the train and `enter train` when inside. This doesn't make any sense ... so let's go ahead
and fix that. We need to tell Evennia that you can not enter the train when you're already inside
or leave the train when you're outside. One solution to this is [locks](Locks): we will lock down
the commands so that they can only be called if the player is at the correct location.
Right now commands defaults to the lock `cmd:all()`. The `cmd` lock type in combination with the
`all()` lock function means that everyone can run those commands as long as they are in the same
room as the train *or* inside the train. We're going to change this to check the location of the
player and *only* allow access if they are inside the train.
First of all we need to create a new lock function. Evennia comes with many lock functions built-in
already, but none that we can use for locking a command in this particular case. Create a new entry
in `mygame/server/conf/lockfuncs.py`:
```python
# file mygame/server/conf/lockfuncs.py
def cmdinside(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage: cmdinside()
Used to lock commands and only allows access if the command
is defined on an object which accessing_obj is inside of.
"""
return accessed_obj.obj == accessing_obj.location
```
If you didn't know, Evennia is by default set up to use all functions in this module as lock
functions (there is a setting variable that points to it).
Our new lock function, `cmdinside`, is to be used by Commands. The `accessed_obj` is the Command
object (in our case this will be `CmdEnterTrain` and `CmdLeaveTrain`) — Every command has an `obj`
property: this is the the object on which the command "sits". Since we added those commands to our
train object, the `.obj` property will be set to the train object. Conversely, `accessing_obj` is
the object that called the command: in our case it's the Character trying to enter or leave the
train.
What this function does is to check that the player's location is the same as the train object. If
it is, it means the player is inside the train. Otherwise it means the player is somewhere else and
the check will fail.
The next step is to actually use this new lock function to create a lock of type `cmd`:
```python
# file commands/train.py
...
class CmdEnterTrain(Command):
key = "enter train"
locks = "cmd:not cmdinside()"
# ...
class CmdLeaveTrain(Command):
key = "leave train"
locks = "cmd:cmdinside()"
# ...
```
Notice how we use the `not` here so that we can use the same `cmdinside` to check if we are inside
and outside, without having to create two separate lock functions. After a `@reload` our commands
should be locked down appropriately and you should only be able to use them at the right places.
> Note: If you're logged in as the super user (user `#1`) then this lock will not work: the super
user ignores lock functions. In order to use this functionality you need to `@quell` first.
## Making our train move
Now that we can enter and leave the train correctly, it's time to make it move. There are different
things we need to consider for this:
* Who can control your vehicle? The first player to enter it, only players that have a certain
"drive" skill, automatically?
* Where should it go? Can the player steer the vehicle to go somewhere else or will it always follow
the same route?
For our example train we're going to go with automatic movement through a predefined route (its
track). The train will stop for a bit at the start and end of the route to allow players to enter
and leave it.
Go ahead and create some rooms for our train. Make a list of the room ids along the route (using the
`@ex` command).
```
@dig/tel South station
@ex # note the id of the station
@tunnel/tel n = Following a railroad
@ex # note the id of the track
@tunnel/tel n = Following a railroad
...
@tunnel/tel n = North Station
```
Put the train onto the tracks:
```
@tel south station
@tel train = here
```
Next we will tell the train how to move and which route to take.
```python
# file typeclasses/train.py
from evennia import DefaultObject, search_object
from commands.train import CmdSetTrain
class TrainObject(DefaultObject):
def at_object_creation(self):
self.cmdset.add_default(CmdSetTrain)
self.db.driving = False
# The direction our train is driving (1 for forward, -1 for backwards)
self.db.direction = 1
# The rooms our train will pass through (change to fit your game)
self.db.rooms = ["#2", "#47", "#50", "#53", "#56", "#59"]
def start_driving(self):
self.db.driving = True
def stop_driving(self):
self.db.driving = False
def goto_next_room(self):
currentroom = self.location.dbref
idx = self.db.rooms.index(currentroom) + self.db.direction
if idx < 0 or idx >= len(self.db.rooms):
# We reached the end of our path
self.stop_driving()
# Reverse the direction of the train
self.db.direction *= -1
else:
roomref = self.db.rooms[idx]
room = search_object(roomref)[0]
self.move_to(room)
self.msg_contents("The train is moving forward to %s." % (room.name, ))
```
We added a lot of code here. Since we changed the `at_object_creation` to add in variables we will
have to reset our train object like earlier (using the `@typeclass/force/reset` command).
We are keeping track of a few different things now: whether the train is moving or standing still,
which direction the train is heading to and what rooms the train will pass through.
We also added some methods: one to start moving the train, another to stop and a third that actually
moves the train to the next room in the list. Or makes it stop driving if it reaches the last stop.
Let's try it out, using `@py` to call the new train functionality:
```
@reload
@typeclass/force/reset train = train.TrainObject
enter train
@py here.goto_next_room()
```
You should see the train moving forward one step along the rail road.
## Adding in scripts
If we wanted full control of the train we could now just add a command to step it along the track
when desired. We want the train to move on its own though, without us having to force it by manually
calling the `goto_next_room` method.
To do this we will create two [scripts](Scripts): one script that runs when the train has stopped at
a station and is responsible for starting the train again after a while. The other script will take
care of the driving.
Let's make a new file in `mygame/typeclasses/trainscript.py`
```python
# file mygame/typeclasses/trainscript.py
from evennia import DefaultScript
class TrainStoppedScript(DefaultScript):
def at_script_creation(self):
self.key = "trainstopped"
self.interval = 30
self.persistent = True
self.repeats = 1
self.start_delay = True
def at_repeat(self):
self.obj.start_driving()
def at_stop(self):
self.obj.scripts.add(TrainDrivingScript)
class TrainDrivingScript(DefaultScript):
def at_script_creation(self):
self.key = "traindriving"
self.interval = 1
self.persistent = True
def is_valid(self):
return self.obj.db.driving
def at_repeat(self):
if not self.obj.db.driving:
self.stop()
else:
self.obj.goto_next_room()
def at_stop(self):
self.obj.scripts.add(TrainStoppedScript)
```
Those scripts work as a state system: when the train is stopped, it waits for 30 seconds and then
starts again. When the train is driving, it moves to the next room every second. The train is always
in one of those two states - both scripts take care of adding the other one once they are done.
As a last step we need to link the stopped-state script to our train, reload the game and reset our
train again., and we're ready to ride it around!
```python
# file typeclasses/train.py
from typeclasses.trainscript import TrainStoppedScript
class TrainObject(DefaultObject):
def at_object_creation(self):
# ...
self.scripts.add(TrainStoppedScript)
```
```
@reload
@typeclass/force/reset train = train.TrainObject
enter train
# output:
< The train is moving forward to Following a railroad.
< The train is moving forward to Following a railroad.
< The train is moving forward to Following a railroad.
...
< The train is moving forward to Following a railroad.
< The train is moving forward to North station.
leave train
```
Our train will stop 30 seconds at each end station and then turn around to go back to the other end.
## Expanding
This train is very basic and still has some flaws. Some more things to do:
* Make it look like a train.
* Make it impossible to exit and enter the train mid-ride. This could be made by having the
enter/exit commands check so the train is not moving before allowing the caller to proceed.
* Have train conductor commands that can override the automatic start/stop.
* Allow for in-between stops between the start- and end station
* Have a rail road track instead of hard-coding the rooms in the train object. This could for
example be a custom [Exit](Objects#exits) only traversable by trains. The train will follow the
track. Some track segments can split to lead to two different rooms and a player can switch the
direction to which room it goes.
* Create another kind of vehicle!

View file

@ -0,0 +1,198 @@
# Understanding Color Tags
This tutorial aims at dispelling confusions regarding the use of color tags within Evennia.
Correct understanding of this topic requires having read the [TextTags](TextTags) page and learned
Evennia's color tags. Here we'll explain by examples the reasons behind the unexpected (or
apparently incoherent) behaviors of some color tags, as mentioned _en passant_ in the
[TextTags](TextTags) page.
All you'll need for this tutorial is access to a running instance of Evennia via a color-enabled
client. The examples provided are just commands that you can type in your client.
Evennia, ANSI and Xterm256
==========================
All modern MUD clients support colors; nevertheless, the standards to which all clients abide dates
back to old day of terminals, and when it comes to colors we are dealing with ANSI and Xterm256
standards.
Evennia handles transparently, behind the scenes, all the code required to enforce these
standards—so, if a user connects with a client which doesn't support colors, or supports only ANSI
(16 colors), Evennia will take all due steps to ensure that the output will be adjusted to look
right at the client side.
As for you, the developer, all you need to care about is knowing how to correctly use the color tags
within your MUD. Most likely, you'll be adding colors to help pages, descriptions, automatically
generated text, etc.
You are free to mix together ANSI and Xterm256 color tags, but you should be aware of a few
pitfalls. ANSI and Xterm256 coexist without conflicts in Evennia, but in many ways they don't «see»
each other: ANSI-specific color tags will have no effect on Xterm-defined colors, as we shall see
here.
ANSI
====
ANSI has a set of 16 colors, to be more precise: ANSI has 8 basic colors which come in _dark_ and
_bright_ flavours—with _dark_ being _normal_. The colors are: red, green, yellow, blue, magenta,
cyan, white and black. White in its dark version is usually referred to as gray, and black in its
bright version as darkgray. Here, for sake of simplicity they'll be referred to as dark and bright:
bright/dark black, bright/dark white.
The default colors of MUD clients is normal (dark) white on normal black (ie: gray on black).
It's important to grasp that in the ANSI standard bright colors apply only to text (foreground), not
to background. Evennia allows to bypass this limitation via Xterm256, but doing so will impact the
behavior of ANSI tags, as we shall see.
Also, it's important to remember that the 16 ANSI colors are a convention, and the final user can
always customize their appearance—he might decide to have green show as red, and dark green as blue,
etc.
Xterm256
========
The 16 colors of ANSI should be more than enough to handle simple coloring of text. But when an
author wants to be sure that a given color will show as he intended it, she might choose to rely on
Xterm256 colors.
Xterm256 doesn't rely on a palette of named colors, it instead represent colors by their values. So,
a red color could be `|[500` (bright and pure red), or `|[300` (darker red), and so on.
ANSI Color Tags in Evennia
==========================
> NOTE: for ease of reading, the examples contain extra white spaces after the
> color tags (eg: `|g green |b blue` ). This is done only so that it's easier
> to see the tags separated from their context; it wouldn't be good practice
> in real-life coding.
Let's proceed by examples. In your MUD client type:
say Normal |* Negative
Evennia should output the word "Normal" normally (ie: gray on black) and "Negative" in reversed
colors (ie: black on gray).
This is pretty straight forward, the `|*` ANSI *invert* tag switches between foreground and
background—from now on, **FG** and **BG** shorthands will be used to refer to foreground and
background.
But take mental note of this: `|*` has switched *dark white* and *dark black*.
Now try this:
say |w Bright white FG |* Negative
You'll notice that the word "Negative" is not black on white, it's darkgray on gray. Why is this?
Shouldn't it be black text on a white BG? Two things are happening here.
As mentioned, ANSI has 8 base colors, the dark ones. The bright ones are achieved by means of
*highlighting* the base/dark/normal colors, and they only apply to FG.
What happened here is that when we set the bright white FG with `|w`, Evennia translated this into
the ANSI sequence of Highlight On + White FG. In terms of Evennia's color tags, it's as if we typed:
say |h|!W Bright white FG |* Negative
Furthermore, the Highlight-On property (which only works for BG!) is preserved after the FG/BG
switch, this being the reason why we see black as darkgray: highlighting makes it *bright black*
(ie: darkgray).
As for the BG being also grey, that is normal—ie: you are seeing *normal white* (ie: dark white =
gray). Remember that since there are no bright BG colors, the ANSI `|*` tag will transpose any FG
color in its normal/dark version. So here the FG's bright white became dark white in the BG! In
reality, it was always normal/dark white, except that in the FG is seen as bright because of the
highlight tag behind the scenes.
Let's try the same thing with some color:
say |m |[G Bright Magenta on Dark Green |* Negative
Again, the BG stays dark because of ANSI rules, and the FG stays bright because of the implicit `|h`
in `|m`.
Now, let's see what happens if we set a bright BG and then invert—yes, Evennia kindly allows us to
do it, even if it's not within ANSI expectations.
say |[b Dark White on Bright Blue |* Negative
Before color inversion, the BG does show in bright blue, and after inversion (as expected) it's
*dark white* (gray). The bright blue of the BG survived the inversion and gave us a bright blue FG.
This behavior is tricky though, and not as simple as it might look.
If the inversion were to be pure ANSI, the bright blue would have been accounted just as normal
blue, and should have converted to normal blue in the FG (after all, there was no highlighting on).
The fact is that in reality this color is not bright blue at all, it just an Xterm version of it!
To demonstrate this, type:
say |[b Dark White on Bright Blue |* Negative |H un-bright
The `|H` Highlight-Off tag should have turned *dark blue* the last word; but it didn't because it
couldn't: in order to enforce the non-ANSI bright BG Evennia turned to Xterm, and Xterm entities are
not affected by ANSI tags!
So, we are getting at the heart of all confusions and possible odd-behaviors pertaining color tags
in Evennia: apart from Evennia's translations from- and to- ANSI/Xterm, the two systems are
independent and transparent to each other.
The bright blue of the previous example was just an Xterm representation of the ANSI standard blue.
Try to change the default settings of your client, so that blue shows as some other color, you'll
then realize the difference when Evennia is sending a true ANSI color (which will show up according
to your settings) and when instead it's sending an Xterm representation of that color (which will
show up always as defined by Evennia).
You'll have to keep in mind that the presence of an Xterm BG or FG color might affect the way your
tags work on the text. For example:
say |[b Bright Blue BG |* Negative |!Y Dark Yellow |h not bright
Here the `|h` tag no longer affects the FG color. Even though it was changed via the `|!` tag, the
ANSI system is out-of-tune because of the intrusion of an Xterm color (bright blue BG, then moved to
FG with `|*`).
All unexpected ANSI behaviours are the result of mixing Xterm colors (either on purpose or either
via bright BG colors). The `|n` tag will restore things in place and ANSI tags will respond properly
again. So, at the end is just an issue of being mindful when using Xterm colors or bright BGs, and
avoid wild mixing them with ANSI tags without normalizing (`|n`) things again.
Try this:
say |[b Bright Blue BG |* Negative |!R Red FG
And then:
say |[B Dark Blue BG |* Negative |!R Red BG??
In this second example the `|!` changes the BG color instead of the FG! In fact, the odd behavior is
the one from the former example, non the latter. When you invert FG and BG with `|*` you actually
inverting their references. This is why the last example (which has a normal/dark BG!) allows `|!`
to change the BG color. In the first example, it's again the presence of an Xterm color (bright blue
BG) which changes the default behavior.
Try this:
`say Normal |* Negative |!R Red BG`
This is the normal behavior, and as you can see it allows `|!` to change BG color after the
inversion of FG and BG.
As long as you have an understanding of how ANSI works, it should be easy to handle color tags
avoiding the pitfalls of Xterm-ANSI promisquity.
One last example:
`say Normal |* Negative |* still Negative`
Shows that `|*` only works once in a row and will not (and should not!) revert back if used again.
Nor it will have any effect until the `|n` tag is called to "reset" ANSI back to normal. This is how
it is meant to work.
ANSI operates according to a simple states-based mechanism, and it's important to understand the
positive effect of resetting with the `|n` tag, and not try to
push it over the limit, so to speak.

View file

@ -0,0 +1,54 @@
# Weather Tutorial
This tutorial will have us create a simple weather system for our MUD. The way we want to use this
is to have all outdoor rooms echo weather-related messages to the room at regular and semi-random
intervals. Things like "Clouds gather above", "It starts to rain" and so on.
One could imagine every outdoor room in the game having a script running on themselves that fires
regularly. For this particular example it is however more efficient to do it another way, namely by
using a "ticker-subscription" model. The principle is simple: Instead of having each Object
individually track the time, they instead subscribe to be called by a global ticker who handles time
keeping. Not only does this centralize and organize much of the code in one place, it also has less
computing overhead.
Evennia offers the [TickerHandler](TickerHandler) specifically for using the subscription model. We
will use it for our weather system.
We will assume you know how to make your own Typeclasses. If not see one of the beginning tutorials.
We will create a new WeatherRoom typeclass that is aware of the day-night cycle.
```python
import random
from evennia import DefaultRoom, TICKER_HANDLER
ECHOES = ["The sky is clear.",
"Clouds gather overhead.",
"It's starting to drizzle.",
"A breeze of wind is felt.",
"The wind is picking up"] # etc
class WeatherRoom(DefaultRoom):
"This room is ticked at regular intervals"
def at_object_creation(self):
"called only when the object is first created"
TICKER_HANDLER.add(60 * 60, self.at_weather_update)
def at_weather_update(self, *args, **kwargs):
"ticked at regular intervals"
echo = random.choice(ECHOES)
self.msg_contents(echo)
```
In the `at_object_creation` method, we simply added ourselves to the TickerHandler and tell it to
call `at_weather_update` every hour (`60*60` seconds). During testing you might want to play with a
shorter time duration.
For this to work we also create a custom hook `at_weather_update(*args, **kwargs)`, which is the
call sign required by TickerHandler hooks.
Henceforth the room will inform everyone inside it when the weather changes. This particular example
is of course very simplistic - the weather echoes are just randomly chosen and don't care what
weather came before it. Expanding it to be more realistic is a useful exercise.

View file

@ -0,0 +1,639 @@
# Web Character Generation
## Introduction
This tutorial will create a simple web-based interface for generating a new in-game Character.
Accounts will need to have first logged into the website (with their `AccountDB` account). Once
finishing character generation the Character will be created immediately and the Accounts can then
log into the game and play immediately (the Character will not require staff approval or anything
like that). This guide does not go over how to create an AccountDB on the website with the right
permissions to transfer to their web-created characters.
It is probably most useful to set `MULTISESSION_MODE = 2` or `3` (which gives you a character-
selection screen when you log into the game later). Other modes can be used with some adaptation to
auto-puppet the new Character.
You should have some familiarity with how Django sets up its Model Template View framework. You need
to understand what is happening in the basic [Web Character View tutorial](Web-Character-View-
Tutorial). If you dont understand the listed tutorial or have a grasp of Django basics, please look
at the [Django tutorial](https://docs.djangoproject.com/en/1.8/intro/) to get a taste of what Django
does, before throwing Evennia into the mix (Evennia shares its API and attributes with the website
interface). This guide will outline the format of the models, views, urls, and html templates
needed.
## Pictures
Here are some screenshots of the simple app we will be making.
Index page, with no character application yet done:
***
![Index page, with no character application yet done.](https://lh3.googleusercontent.com/-57KuSWHXQ_M/VWcULN152tI/AAAAAAAAEZg/kINTmVlHf6M/w425-h189-no/webchargen_index2.gif)
***
Having clicked the "create" link you get to create your character (here we will only have name and
background, you can add whatever is needed to fit your game):
***
![Character creation.](https://lh3.googleusercontent.com/-ORiOEM2R_yQ/VWcUKgy84rI/AAAAAAAAEZY/B3CBh3FHii4/w607-h60-no/webchargen_creation.gif)
***
Back to the index page. Having entered our character application (we called our character "TestApp")
you see it listed:
***
![Having entered an application.](https://lh6.googleusercontent.com/-HlxvkvAimj4/VWcUKjFxEiI/AAAAAAAAEZo/gLppebr05JI/w321-h194-no/webchargen_index1.gif)
***
We can also view an already written character application by clicking on it - this brings us to the
*detail* page:
***
![Detail view of character application.](https://lh6.googleusercontent.com/-2m1UhSE7s_k/VWcUKfLRfII/AAAAAAAAEZc/UFmBOqVya4k/w267-h175-no/webchargen_detail.gif)
***
## Installing an App
Assuming your game is named "mygame", navigate to your `mygame/` directory, and type:
evennia startapp chargen
This will initialize a new Django app we choose to call "chargen". It is directory containing some
basic starting things Django needs. You will need to move this directory: for the time being, it is
in your `mygame` directory. Better to move it in your `mygame/web` directory, so you have
`mygame/web/chargen` in the end.
Next, navigate to `mygame/server/conf/settings.py` and add or edit the following line to make
Evennia (and Django) aware of our new app:
INSTALLED_APPS += ('web.chargen',)
After this, we will get into defining our *models* (the description of the database storage),
*views* (the server-side website content generators), *urls* (how the web browser finds the pages)
and *templates* (how the web page should be structured).
### Installing - Checkpoint:
* you should have a folder named `chargen` or whatever you chose in your mygame/web/ directory
* you should have your application name added to your INSTALLED_APPS in settings.py
## Create Models
Models are created in `mygame/web/chargen/models.py`.
A [Django database model](New-Models) is a Python class that describes the database storage of the
data you want to manage. Any data you choose to store is stored in the same database as the game and
you have access to all the game's objects here.
We need to define what a character application actually is. This will differ from game to game so
for this tutorial we will define a simple character sheet with the following database fields:
* `app_id` (AutoField): Primary key for this character application sheet.
* `char_name` (CharField): The new character's name.
* `date_applied` (DateTimeField): Date that this application was received.
* `background` (TextField): Character story background.
* `account_id` (IntegerField): Which account ID does this application belong to? This is an
AccountID from the AccountDB object.
* `submitted` (BooleanField): `True`/`False` depending on if the application has been submitted yet.
> Note: In a full-fledged game, youd likely want them to be able to select races, skills,
attributes and so on.
Our `models.py` file should look something like this:
```python
# in mygame/web/chargen/models.py
from django.db import models
class CharApp(models.Model):
app_id = models.AutoField(primary_key=True)
char_name = models.CharField(max_length=80, verbose_name='Character Name')
date_applied = models.DateTimeField(verbose_name='Date Applied')
background = models.TextField(verbose_name='Background')
account_id = models.IntegerField(default=1, verbose_name='Account ID')
submitted = models.BooleanField(default=False)
```
You should consider how you are going to link your application to your account. For this tutorial,
we are using the account_id attribute on our character application model in order to keep track of
which characters are owned by which accounts. Since the account id is a primary key in Evennia, it
is a good candidate, as you will never have two of the same IDs in Evennia. You can feel free to use
anything else, but for the purposes of this guide, we are going to use account ID to join the
character applications with the proper account.
### Model - Checkpoint:
* you should have filled out `mygame/web/chargen/models.py` with the model class shown above
(eventually adding fields matching what you need for your game).
## Create Views
*Views* are server-side constructs that make dynamic data available to a web page. We are going to
add them to `mygame/web/chargen.views.py`. Each view in our example represents the backbone of a
specific web page. We will use three views and three pages here:
* The index (managing `index.html`). This is what you see when you navigate to
`http://yoursite.com/chargen`.
* The detail display sheet (manages `detail.html`). A page that passively displays the stats of a
given Character.
* Character creation sheet (manages `create.html`). This is the main form with fields to fill in.
### *Index* view
Lets get started with the index first.
Well want characters to be able to see their created characters so lets
```python
# file mygame/web/chargen.views.py
from .models import CharApp
def index(request):
current_user = request.user # current user logged in
p_id = current_user.id # the account id
# submitted Characters by this account
sub_apps = CharApp.objects.filter(account_id=p_id, submitted=True)
context = {'sub_apps': sub_apps}
# make the variables in 'context' available to the web page template
return render(request, 'chargen/index.html', context)
```
### *Detail* view
Our detail page will have pertinent character application information our users can see. Since this
is a basic demonstration, our detail page will only show two fields:
* Character name
* Character background
We will use the account ID again just to double-check that whoever tries to check our character page
is actually the account who owns the application.
```python
# file mygame/web/chargen.views.py
def detail(request, app_id):
app = CharApp.objects.get(app_id=app_id)
name = app.char_name
background = app.background
submitted = app.submitted
p_id = request.user.id
context = {'name': name, 'background': background,
'p_id': p_id, 'submitted': submitted}
return render(request, 'chargen/detail.html', context)
```
## *Creating* view
Predictably, our *create* function will be the most complicated of the views, as it needs to accept
information from the user, validate the information, and send the information to the server. Once
the form content is validated will actually create a playable Character.
The form itself we will define first. In our simple example we are just looking for the Character's
name and background. This form we create in `mygame/web/chargen/forms.py`:
```python
# file mygame/web/chargen/forms.py
from django import forms
class AppForm(forms.Form):
name = forms.CharField(label='Character Name', max_length=80)
background = forms.CharField(label='Background')
```
Now we make use of this form in our view.
```python
# file mygame/web/chargen/views.py
from web.chargen.models import CharApp
from web.chargen.forms import AppForm
from django.http import HttpResponseRedirect
from datetime import datetime
from evennia.objects.models import ObjectDB
from django.conf import settings
from evennia.utils import create
def creating(request):
user = request.user
if request.method == 'POST':
form = AppForm(request.POST)
if form.is_valid():
name = form.cleaned_data['name']
background = form.cleaned_data['background']
applied_date = datetime.now()
submitted = True
if 'save' in request.POST:
submitted = False
app = CharApp(char_name=name, background=background,
date_applied=applied_date, account_id=user.id,
submitted=submitted)
app.save()
if submitted:
# Create the actual character object
typeclass = settings.BASE_CHARACTER_TYPECLASS
home = ObjectDB.objects.get_id(settings.GUEST_HOME)
# turn the permissionhandler to a string
perms = str(user.permissions)
# create the character
char = create.create_object(typeclass=typeclass, key=name,
home=home, permissions=perms)
user.db._playable_characters.append(char)
# add the right locks for the character so the account can
# puppet it
char.locks.add("puppet:id(%i) or pid(%i) or perm(Developers) "
"or pperm(Developers)" % (char.id, user.id))
char.db.background = background # set the character background
return HttpResponseRedirect('/chargen')
else:
form = AppForm()
return render(request, 'chargen/create.html', {'form': form})
```
> Note also that we basically create the character using the Evennia API, and we grab the proper
permissions from the `AccountDB` object and copy them to the character object. We take the user
permissions attribute and turn that list of strings into a string object in order for the
create_object function to properly process the permissions.
Most importantly, the following attributes must be set on the created character object:
* Evennia [permissions](Locks#permissions) (copied from the `AccountDB`).
* The right `puppet` [locks](Locks) so the Account can actually play as this Character later.
* The relevant Character [typeclass](Typeclasses)
* Character name (key)
* The Character's home room location (`#2` by default)
Other attributes are strictly speaking optional, such as the `background` attribute on our
character. It may be a good idea to decompose this function and create a separate _create_character
function in order to set up your character object the account owns. But with the Evennia API,
setting custom attributes is as easy as doing it in the meat of your Evennia game directory.
After all of this, our `views.py` file should look like something like this:
```python
# file mygame/web/chargen/views.py
from django.shortcuts import render
from web.chargen.models import CharApp
from web.chargen.forms import AppForm
from django.http import HttpResponseRedirect
from datetime import datetime
from evennia.objects.models import ObjectDB
from django.conf import settings
from evennia.utils import create
def index(request):
current_user = request.user # current user logged in
p_id = current_user.id # the account id
# submitted apps under this account
sub_apps = CharApp.objects.filter(account_id=p_id, submitted=True)
context = {'sub_apps': sub_apps}
return render(request, 'chargen/index.html', context)
def detail(request, app_id):
app = CharApp.objects.get(app_id=app_id)
name = app.char_name
background = app.background
submitted = app.submitted
p_id = request.user.id
context = {'name': name, 'background': background,
'p_id': p_id, 'submitted': submitted}
return render(request, 'chargen/detail.html', context)
def creating(request):
user = request.user
if request.method == 'POST':
form = AppForm(request.POST)
if form.is_valid():
name = form.cleaned_data['name']
background = form.cleaned_data['background']
applied_date = datetime.now()
submitted = True
if 'save' in request.POST:
submitted = False
app = CharApp(char_name=name, background=background,
date_applied=applied_date, account_id=user.id,
submitted=submitted)
app.save()
if submitted:
# Create the actual character object
typeclass = settings.BASE_CHARACTER_TYPECLASS
home = ObjectDB.objects.get_id(settings.GUEST_HOME)
# turn the permissionhandler to a string
perms = str(user.permissions)
# create the character
char = create.create_object(typeclass=typeclass, key=name,
home=home, permissions=perms)
user.db._playable_characters.append(char)
# add the right locks for the character so the account can
# puppet it
char.locks.add("puppet:id(%i) or pid(%i) or perm(Developers) "
"or pperm(Developers)" % (char.id, user.id))
char.db.background = background # set the character background
return HttpResponseRedirect('/chargen')
else:
form = AppForm()
return render(request, 'chargen/create.html', {'form': form})
```
### Create Views - Checkpoint:
* youve defined a `views.py` that has an index, detail, and creating functions.
* youve defined a forms.py with the `AppForm` class needed by the `creating` function of
`views.py`.
* your `mygame/web/chargen` directory should now have a `views.py` and `forms.py` file
## Create URLs
URL patterns helps redirect requests from the web browser to the right views. These patterns are
created in `mygame/web/chargen/urls.py`.
```python
# file mygame/web/chargen/urls.py
from django.conf.urls import url
from web.chargen import views
urlpatterns = [
# ex: /chargen/
url(r'^$', views.index, name='index'),
# ex: /chargen/5/
url(r'^(?P<app_id>[0-9]+)/$', views.detail, name='detail'),
# ex: /chargen/create
url(r'^create/$', views.creating, name='creating'),
]
```
You could change the format as you desire. To make it more secure, you could remove app_id from the
"detail" url, and instead just fetch the accounts applications using a unifying field like
account_id to find all the character application objects to display.
We must also update the main `mygame/web/urls.py` file (that is, one level up from our chargen app),
so the main website knows where our app's views are located. Find the `patterns` variable, and
change it to include:
```python
# in file mygame/web/urls.py
from django.conf.urls import url, include
# default evennia patterns
from evennia.web.urls import urlpatterns
# eventual custom patterns
custom_patterns = [
# url(r'/desired/url/', view, name='example'),
]
# this is required by Django.
urlpatterns += [
url(r'^chargen/', include('web.chargen.urls')),
]
urlpatterns = custom_patterns + urlpatterns
```
### URLs - Checkpoint:
* Youve created a urls.py file in the `mygame/web/chargen` directory
* You have edited the main `mygame/web/urls.py` file to include urls to the `chargen` directory.
## HTML Templates
So we have our url patterns, views, and models defined. Now we must define our HTML templates that
the actual user will see and interact with. For this tutorial we us the basic *prosimii* template
that comes with Evennia.
Take note that we use `user.is_authenticated` to make sure that the user cannot create a character
without logging in.
These files will all go into the `/mygame/web/chargen/templates/chargen/` directory.
### index.html
This HTML template should hold a list of all the applications the account currently has active. For
this demonstration, we will only list the applications that the account has submitted. You could
easily adjust this to include saved applications, or other types of applications if you have
different kinds.
Please refer back to `views.py` to see where we define the variables these templates make use of.
```html
<!-- file mygame/web/chargen/templates/chargen/index.html-->
{% extends "base.html" %}
{% block content %}
{% if user.is_authenticated %}
<h1>Character Generation</h1>
{% if sub_apps %}
<ul>
{% for sub_app in sub_apps %}
<li><a href="/chargen/{{ sub_app.app_id }}/">{{ sub_app.char_name }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>You haven't submitted any character applications.</p>
{% endif %}
{% else %}
<p>Please <a href="{% url 'login'%}">login</a>first.<a/></p>
{% endif %}
{% endblock %}
```
### detail.html
This page should show a detailed character sheet of their application. This will only show their
name and character background. You will likely want to extend this to show many more fields for your
game. In a full-fledged character generation, you may want to extend the boolean attribute of
submitted to allow accounts to save character applications and submit them later.
```html
<!-- file mygame/web/chargen/templates/chargen/detail.html-->
{% extends "base.html" %}
{% block content %}
<h1>Character Information</h1>
{% if user.is_authenticated %}
{% if user.id == p_id %}
<h2>{{name}}</h2>
<h2>Background</h2>
<p>{{background}}</p>
<p>Submitted: {{submitted}}</p>
{% else %}
<p>You didn't submit this character.</p>
{% endif %}
{% else %}
<p>You aren't logged in.</p>
{% endif %}
{% endblock %}
```
### create.html
Our create HTML template will use the Django form we defined back in views.py/forms.py to drive the
majority of the application process. There will be a form input for every field we defined in
forms.py, which is handy. We have used POST as our method because we are sending information to the
server that will update the database. As an alternative, GET would be much less secure. You can read
up on documentation elsewhere on the web for GET vs. POST.
```html
<!-- file mygame/web/chargen/templates/chargen/create.html-->
{% extends "base.html" %}
{% block content %}
<h1>Character Creation</h1>
{% if user.is_authenticated %}
<form action="/chargen/create/" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" name="submit" value="Submit"/>
</form>
{% else %}
<p>You aren't logged in.</p>
{% endif %}
{% endblock %}
```
### Templates - Checkpoint:
* Create a `index.html`, `detail.html` and `create.html` template in your
`mygame/web/chargen/templates/chargen` directory
## Activating your new character generation
After finishing this tutorial you should have edited or created the following files:
```bash
mygame/web/urls.py
mygame/web/chargen/models.py
mygame/web/chargen/views.py
mygame/web/chargen/urls.py
mygame/web/chargen/templates/chargen/index.html
mygame/web/chargen/templates/chargen/create.html
mygame/web/chargen/templates/chargen/detail.html
```
Once you have all these files stand in your `mygame/`folder and run:
```bash
evennia makemigrations
evennia migrate
```
This will create and update the models. If you see any errors at this stage, read the traceback
carefully, it should be relatively easy to figure out where the error is.
Login to the website (you need to have previously registered an Player account with the game to do
this). Next you navigate to `http://yourwebsite.com/chargen` (if you are running locally this will
be something like `http://localhost:4001/chargen` and you will see your new app in action.
This should hopefully give you a good starting point in figuring out how youd like to approach your
own web generation. The main difficulties are in setting the appropriate settings on your newly
created character object. Thankfully, the Evennia API makes this easy.
## Adding a no CAPCHA reCAPCHA on your character generation
As sad as it is, if your server is open to the web, bots might come to visit and take advantage of
your open form to create hundreds, thousands, millions of characters if you give them the
opportunity. This section shows you how to use the [No CAPCHA
reCAPCHA](https://www.google.com/recaptcha/intro/invisible.html) designed by Google. Not only is it
easy to use, it is user-friendly... for humans. A simple checkbox to check, except if Google has
some suspicion, in which case you will have a more difficult test with an image and the usual text
inside. It's worth pointing out that, as long as Google doesn't suspect you of being a robot, this
is quite useful, not only for common users, but to screen-reader users, to which reading inside of
an image is pretty difficult, if not impossible. And to top it all, it will be so easy to add in
your website.
### Step 1: Obtain a SiteKey and secret from Google
The first thing is to ask Google for a way to safely authenticate your website to their service. To
do it, we need to create a site key and a secret. Go to
[https://www.google.com/recaptcha/admin](https://www.google.com/recaptcha/admin) to create such a
site key. It's quite easy when you have a Google account.
When you have created your site key, save it safely. Also copy your secret key as well. You should
find both information on the web page. Both would contain a lot of letters and figures.
### Step 2: installing and configuring the dedicated Django app
Since Evennia runs on Django, the easiest way to add our CAPCHA and perform the proper check is to
install the dedicated Django app. Quite easy:
pip install django-nocaptcha-recaptcha
And add it to the installed apps in your settings. In your `mygame/server/conf/settings.py`, you
might have something like this:
```python
# ...
INSTALLED_APPS += (
'web.chargen',
'nocaptcha_recaptcha',
)
```
Don't close the setting file just yet. We have to add in the site key and secret key. You can add
them below:
```python
# NoReCAPCHA site key
NORECAPTCHA_SITE_KEY = "PASTE YOUR SITE KEY HERE"
# NoReCAPCHA secret key
NORECAPTCHA_SECRET_KEY = "PUT YOUR SECRET KEY HERE"
```
### Step 3: Adding the CAPCHA to our form
Finally we have to add the CAPCHA to our form. It will be pretty easy too. First, open your
`web/chargen/forms.py` file. We're going to add a new field, but hopefully, all the hard work has
been done for us. Update at your convenience, You might end up with something like this:
```python
from django import forms
from nocaptcha_recaptcha.fields import NoReCaptchaField
class AppForm(forms.Form):
name = forms.CharField(label='Character Name', max_length=80)
background = forms.CharField(label='Background')
captcha = NoReCaptchaField()
```
As you see, we added a line of import (line 2) and a field in our form.
And lastly, we need to update our HTML file to add in the Google library. You can open
`web/chargen/templates/chargen/create.html`. There's only one line to add:
```html
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
```
And you should put it at the bottom of the page. Just before the closing body would be good, but
for the time being, the base page doesn't provide a footer block, so we'll put it in the content
block. Note that it's not the best place, but it will work. In the end, your
`web/chargen/templates/chargen/create.html` file should look like this:
```html
{% extends "base.html" %}
{% block content %}
<h1>Character Creation</h1>
{% if user.is_authenticated %}
<form action="/chargen/create/" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" name="submit" value="Submit"/>
</form>
{% else %}
<p>You aren't logged in.</p>
{% endif %}
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
{% endblock %}
```
Reload and open [http://localhost:4001/chargen/create](http://localhost:4001/chargen/create/) and
you should see your beautiful CAPCHA just before the "submit" button. Try not to check the checkbox
to see what happens. And do the same while checking the checkbox!

View file

@ -0,0 +1,229 @@
# Web Character View Tutorial
**Before doing this tutorial you will probably want to read the intro in [Basic Web tutorial](Web-
Tutorial).**
In this tutorial we will create a web page that displays the stats of a game character. For this,
and all other pages we want to make specific to our game, we'll need to create our own Django "app"
We'll call our app `character`, since it will be dealing with character information. From your game
dir, run
evennia startapp character
This will create a directory named `character` in the root of your game dir. It contains all basic
files that a Django app needs. To keep `mygame` well ordered, move it to your `mygame/web/`
directory instead:
mv character web/
Note that we will not edit all files in this new directory, many of the generated files are outside
the scope of this tutorial.
In order for Django to find our new web app, we'll need to add it to the `INSTALLED_APPS` setting.
Evennia's default installed apps are already set, so in `server/conf/settings.py`, we'll just extend
them:
```python
INSTALLED_APPS += ('web.character',)
```
> Note: That end comma is important. It makes sure that Python interprets the addition as a tuple
instead of a string.
The first thing we need to do is to create a *view* and an *URL pattern* to point to it. A view is a
function that generates the web page that a visitor wants to see, while the URL pattern lets Django
know what URL should trigger the view. The pattern may also provide some information of its own as
we shall see.
Here is our `character/urls.py` file (**Note**: you may have to create this file if a blank one
wasn't generated for you):
```python
# URL patterns for the character app
from django.conf.urls import url
from web.character.views import sheet
urlpatterns = [
url(r'^sheet/(?P<object_id>\d+)/$', sheet, name="sheet")
]
```
This file contains all of the URL patterns for the application. The `url` function in the
`urlpatterns` list are given three arguments. The first argument is a pattern-string used to
identify which URLs are valid. Patterns are specified as *regular expressions*. Regular expressions
are used to match strings and are written in a special, very compact, syntax. A detailed description
of regular expressions is beyond this tutorial but you can learn more about them
[here](https://docs.python.org/2/howto/regex.html). For now, just accept that this regular
expression requires that the visitor's URL looks something like this:
````
sheet/123/
````
That is, `sheet/` followed by a number, rather than some other possible URL pattern. We will
interpret this number as object ID. Thanks to how the regular expression is formulated, the pattern
recognizer stores the number in a variable called `object_id`. This will be passed to the view (see
below). We add the imported view function (`sheet`) in the second argument. We also add the `name`
keyword to identify the URL pattern itself. You should always name your URL patterns, this makes
them easy to refer to in html templates using the `{% url %}` tag (but we won't get more into that
in this tutorial).
> Security Note: Normally, users do not have the ability to see object IDs within the game (it's
restricted to superusers only). Exposing the game's object IDs to the public like this enables
griefers to perform what is known as an [account enumeration
attack](http://www.sans.edu/research/security-laboratory/article/attacks-browsing) in the efforts of
hijacking your superuser account. Consider this: in every Evennia installation, there are two
objects that we can *always* expect to exist and have the same object IDs-- Limbo (#2) and the
superuser you create in the beginning (#1). Thus, the griefer can get 50% of the information they
need to hijack the admin account (the admin's username) just by navigating to `sheet/1`!
Next we create `views.py`, the view file that `urls.py` refers to.
```python
# Views for our character app
from django.http import Http404
from django.shortcuts import render
from django.conf import settings
from evennia.utils.search import object_search
from evennia.utils.utils import inherits_from
def sheet(request, object_id):
object_id = '#' + object_id
try:
character = object_search(object_id)[0]
except IndexError:
raise Http404("I couldn't find a character with that ID.")
if not inherits_from(character, settings.BASE_CHARACTER_TYPECLASS):
raise Http404("I couldn't find a character with that ID. "
"Found something else instead.")
return render(request, 'character/sheet.html', {'character': character})
```
As explained earlier, the URL pattern parser in `urls.py` parses the URL and passes `object_id` to
our view function `sheet`. We do a database search for the object using this number. We also make
sure such an object exists and that it is actually a Character. The view function is also handed a
`request` object. This gives us information about the request, such as if a logged-in user viewed it
- we won't use that information here but it is good to keep in mind.
On the last line, we call the `render` function. Apart from the `request` object, the `render`
function takes a path to an html template and a dictionary with extra data you want to pass into
said template. As extra data we pass the Character object we just found. In the template it will be
available as the variable "character".
The html template is created as `templates/character/sheet.html` under your `character` app folder.
You may have to manually create both `template` and its subfolder `character`. Here's the template
to create:
````html
{% extends "base.html" %}
{% block content %}
<h1>{{ character.name }}</h1>
<p>{{ character.db.desc }}</p>
<h2>Stats</h2>
<table>
<thead>
<tr>
<th>Stat</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Strength</td>
<td>{{ character.db.str }}</td>
</tr>
<tr>
<td>Intelligence</td>
<td>{{ character.db.int }}</td>
</tr>
<tr>
<td>Speed</td>
<td>{{ character.db.spd }}</td>
</tr>
</tbody>
</table>
<h2>Skills</h2>
<ul>
{% for skill in character.db.skills %}
<li>{{ skill }}</li>
{% empty %}
<li>This character has no skills yet.</li>
{% endfor %}
</ul>
{% if character.db.approved %}
<p class="success">This character has been approved!</p>
{% else %}
<p class="warning">This character has not yet been approved!</p>
{% endif %}
{% endblock %}
````
In Django templates, `{% ... %}` denotes special in-template "functions" that Django understands.
The `{{ ... }}` blocks work as "slots". They are replaced with whatever value the code inside the
block returns.
The first line, `{% extends "base.html" %}`, tells Django that this template extends the base
template that Evennia is using. The base template is provided by the theme. Evennia comes with the
open-source third-party theme `prosimii`. You can find it and its `base.html` in
`evennia/web/templates/prosimii`. Like other templates, these can be overwritten.
The next line is `{% block content %}`. The `base.html` file has `block`s, which are placeholders
that templates can extend. The main block, and the one we use, is named `content`.
We can access the `character` variable anywhere in the template because we passed it in the `render`
call at the end of `view.py`. That means we also have access to the Character's `db` attributes,
much like you would in normal Python code. You don't have the ability to call functions with
arguments in the template-- in fact, if you need to do any complicated logic, you should do it in
`view.py` and pass the results as more variables to the template. But you still have a great deal of
flexibility in how you display the data.
We can do a little bit of logic here as well. We use the `{% for %} ... {% endfor %}` and `{% if %}
... {% else %} ... {% endif %}` structures to change how the template renders depending on how many
skills the user has, or if the user is approved (assuming your game has an approval system).
The last file we need to edit is the master URLs file. This is needed in order to smoothly integrate
the URLs from your new `character` app with the URLs from Evennia's existing pages. Find the file
`web/urls.py` and update its `patterns` list as follows:
```python
# web/urls.py
custom_patterns = [
url(r'^character/', include('web.character.urls'))
]
```
Now reload the server with `evennia reload` and visit the page in your browser. If you haven't
changed your defaults, you should be able to find the sheet for character `#1` at
`http://localhost:4001/character/sheet/1/`
Try updating the stats in-game and refresh the page in your browser. The results should show
immediately.
As an optional final step, you can also change your character typeclass to have a method called
'get_absolute_url'.
```python
# typeclasses/characters.py
# inside Character
def get_absolute_url(self):
from django.urls import reverse
return reverse('character:sheet', kwargs={'object_id':self.id})
```
Doing so will give you a 'view on site' button in the top right of the Django Admin Objects
changepage that links to your new character sheet, and allow you to get the link to a character's
page by using {{ object.get_absolute_url }} in any template where you have a given object.
*Now that you've made a basic page and app with Django, you may want to read the full Django
tutorial to get a better idea of what it can do. [You can find Django's tutorial
here](https://docs.djangoproject.com/en/1.8/intro/tutorial01/).*