Refactor 1.0 docs with new toctree structure and inheritance

This commit is contained in:
Griatch 2022-02-06 19:27:15 +01:00
parent 62477eac50
commit 628afe9367
142 changed files with 3967 additions and 3024 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](Beginner-Tutorial/Part5/Web-Tutorial.md).** 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](https://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](https://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,262 @@
# Arxcode installing help
[Arx - After the Reckoning](https://play.arxmush.org/) is a big and very popular
[Evennia](https://www.evennia.com)-based game. Arx is heavily roleplaying-centric, relying on game
masters to drive the story. Technically it's maybe best described as "a MUSH, but with more coded
systems". In August of 2018, the game's developer, Tehom, generously released the [source code of
Arx on github](https://github.com/Arx-Game/arxcode). This is a treasure-trove for developers wanting
to pick ideas or even get a starting game to build on.
> These instructions are based on the Arx-code released as of *Aug 12, 2018*. They will probably
> not work 100% out of the box anymore. Report any differences and changes needed.
It's not too hard to run Arx from the sources (of course you'll start with an empty database) but
since part of Arx has grown organically, it doesn't follow standard Evennia paradigms everywhere.
This page covers one take on installing and setting things up while making your new Arx-based game
better match with the vanilla Evennia install.
## Installing Evennia
Firstly, set aside a folder/directory on your drive for everything to follow.
You need to start by installing [Evennia](https://www.evennia.com) by following most of the
[Git-installation instructions](../Setup/Installation-Git.md) for your OS. The difference is that you
need to `git clone https://github.com/TehomCD/evennia.git` instead of Evennia's repo because Arx
uses TehomCD's older Evennia 0.8 [fork](https://github.com/TehomCD/evennia), notably still using
Python2. This detail is important if referring to newer Evennia documentation.
If you are new to Evennia it's *highly* recommended that you run through the normal install
instructions in full - including initializing and starting a new empty game and connecting to it.
That way you can be sure Evennia works correctly as a baseline.
After installing you should have a `virtualenv` running and you should have the following file
structure in your set-aside folder:
```
muddev/
vienv/
evennia/
mygame/
```
Here `mygame` is the empty game you created during the Evennia install, with `evennia --init`. Go to
that and run `evennia stop` to make sure your empty game is not running. We'll instead let Evenna
run Arx, so in principle you could erase `mygame` - but it could also be good to have a clean game
to compare to.
## Installing Arxcode
`cd` to the root of your directory and clone the released source code from github:
git clone https://github.com/Arx-Game/arxcode.git myarx
A new folder `myarx` should appear next to the ones you already had. You could rename this to
something else if you want.
`cd` into `myarx`. If you wonder about the structure of the game dir, you can
[read more about it here](Beginner-Tutorial/Part1/Gamedir-Overview.md).
### Clean up settings
Arx has split evennia's normal settings into `base_settings.py` and `production_settings.py`. It
also has its own solution for managing 'secret' parts of the settings file. We'll keep most of Arx
way but we'll remove the secret-handling and replace it with the normal Evennia method.
`cd` into `myarx/server/conf/` and open the file `settings.py` in a text editor. The top part (within
`"""..."""`) is just help text. Wipe everything underneath that and make it look like this instead
(don't forget to save):
```
from base_settings import *
TELNET_PORTS = [4000]
SERVERNAME = "MyArx"
GAME_SLOGAN = "The cool game"
try:
from server.conf.secret_settings import *
except ImportError:
print("secret_settings.py file not found or failed to import.")
```
> Note: Indents and capitalization matter in Python. Make indents 4 spaces (not tabs) for your own
> sanity. If you want a starter on Python in Evennia, [you can look here](Python-basic-
introduction).
This will import Arx' base settings and override them with the Evennia-default telnet port and give
the game a name. The slogan changes the sub-text shown under the name of your game in the website
header. You can tweak these to your own liking later.
Next, create a new, empty file `secret_settings.py` in the same location as the `settings.py` file.
This can just contain the following:
```python
SECRET_KEY = "sefsefiwwj3 jnwidufhjw4545_oifej whewiu hwejfpoiwjrpw09&4er43233fwefwfw"
```
Replace the long random string with random ASCII characters of your own. The secret key should not
be shared.
Next, open `myarx/server/conf/base_settings.py` in your text editor. We want to remove/comment out
all mentions of the `decouple` package, which Evennia doesn't use (we use `private_settings.py` to
hide away settings that should not be shared).
Comment out `from decouple import config` by adding a `#` to the start of the line: `# from decouple
import config`. Then search for `config(` in the file and comment out all lines where this is used.
Many of these are specific to the server environment where the original Arx runs, so is not that
relevant to us.
### Install Arx dependencies
Arx has some further dependencies beyond vanilla Evennia. Start by `cd`:ing to the root of your
`myarx` folder.
> If you run *Linux* or *Mac*: Edit `myarx/requirements.txt` and comment out the line
> `pypiwin32==219` - it's only needed on Windows and will give an error on other platforms.
Make sure your `virtualenv` is active, then run
pip install -r requirements.txt
The needed Python packages will be installed for you.
### Adding logs/ folder
The Arx repo does not contain the `myarx/server/logs/` folder Evennia expects for storing server
logs. This is simple to add:
# linux/mac
mkdir server/logs
# windows
mkdir server\logs
### Setting up the database and starting
From the `myarx` folder, run
evennia migrate
This creates the database and will step through all database migrations needed.
evennia start
If all goes well Evennia will now start up, running Arx! You can connect to it on `localhost` (or
`127.0.0.1` if your platform doesn't alias `localhost`), port `4000` using a Telnet client.
Alternatively, you can use your web browser to browse to `http://localhost:4001` to see the game's
website and get to the web client.
When you log in you'll get the standard Evennia greeting (since the database is empty), but you can
try `help` to see that it's indeed Arx that is running.
### Additional Setup Steps
The first time you start Evennia after creating the database with the `evennia migrate` step above,
it should create a few starting objects for you - your superuser account, which it will prompt you
to enter, a starting room (Limbo), and a character object for you. If for some reason this does not
occur, you may have to follow the steps below. For the first time Superuser login you may have to
run steps 7-8 and 10 to create and connect to your in-came Character.
1. Login to the game website with your Superuser account.
2. Press the `Admin` button to get into the (Django-) Admin Interface.
3. Navigate to the `Accounts` section.
4. Add a new Account named for the new staffer. Use a place holder password and dummy e-mail
address.
5. Flag account as `Staff` and apply the `Admin` permission group (This assumes you have already set
up an Admin Group in Django).
6. Add Tags named `player` and `developer`.
7. Log into the game using the web client (or a third-party telnet client) using your superuser
account. Move to where you want the new staffer character to appear.
8. In the game client, run `@create/drop <staffername>:typeclasses.characters.Character`, where
`<staffername>` is usually the same name you used for the Staffer account you created in the
Admin earlier (if you are creating a Character for your superuser, use your superuser account
name).
This creates a new in-game Character and places it in your current location.
9. Have the new Admin player log into the game.
10. Have the new Admin puppet the character with `@ic StafferName`.
11. Have the new Admin change their password - `@password <old password> = <new password>`.
Now that you have a Character and an Account object, there's a few additional things you may need to
do in order for some commands to function properly. You can either execute these as in-game commands
while `ic` (controlling your character object).
1. `py from web.character.models import RosterEntry;RosterEntry.objects.create(player=self.player,
character=self)`
2. `py from world.dominion.models import PlayerOrNpc, AssetOwner;dompc =
PlayerOrNpc.objects.create(player = self.player);AssetOwner.objects.create(player=dompc)`
Those steps will give you a 'RosterEntry', 'PlayerOrNpc', and 'AssetOwner' objects. RosterEntry
explicitly connects a character and account object together, even while offline, and contains
additional information about a character's current presence in game (such as which 'roster' they're
in, if you choose to use an active roster of characters). PlayerOrNpc are more character extensions,
as well as support for npcs with no in-game presence and just represented by a name which can be
offscreen members of a character's family. It also allows for membership in Organizations.
AssetOwner holds information about a character or organization's money and resources.
## Alternate Windows install guide
_Contributed by Pax_
If for some reason you cannot use the Windows Subsystem for Linux (which would use instructions
identical to the ones above), it's possible to get Evennia/Arx running under Anaconda for Windows. The
process is a little bit trickier.
Make sure you have:
* Git for Windows https://git-scm.com/download/win
* Anaconda for Windows https://www.anaconda.com/distribution/
* VC++ Compiler for Python 2.7 https://aka.ms/vcpython27
conda update conda
conda create -n arx python=2.7
source activate arx
Set up a convenient repository place for things.
cd ~
mkdir Source
cd Source
mkdir Arx
cd Arx
Replace the SSH git clone links below with your own github forks.
If you don't plan to change Evennia at all, you can use the
evennia/evennia.git repo instead of a forked one.
git clone git@github.com:<youruser>/evennia.git
git clone git@github.com:<youruser>/arxcode.git
Evennia is a package itself, so we want to install it and all of its
prerequisites, after switching to the appropriately-tagged branch for
Arxcode.
cd evennia
git checkout tags/v0.7 -b arx-master
pip install -e .
Arx has some dependencies of its own, so now we'll go install them
As it is not a package, we'll use the normal requirements file.
cd ../arxcode
pip install -r requirements.txt
The git repo doesn't include the empty log directory and Evennia is unhappy if you
don't have it, so while still in the arxcode directory...
mkdir server/logs
Now hit https://github.com/evennia/evennia/wiki/Arxcode-installing-help and
change the setup stuff as in the 'Clean up settings' section.
Then we will create our default database...
../evennia/bin/windows/evennia.bat migrate
...and do the first run. You need winpty because Windows does not have a TTY/PTY
by default, and so the Python console input commands (used for prompts on first
run) will fail and you will end up in an unhappy place. Future runs, you should
not need winpty.
winpty ../evennia/bin/windows/evennia.bat start
Once this is done, you should have your Evennia server running Arxcode up
on localhost at port 4000, and the webserver at http://localhost:4001/

View file

@ -0,0 +1,116 @@
# Beginner Tutorial
```{eval-rst}
.. sidebar:: Tutorial Parts
**Introduction**
Getting set up.
Part 1: `What we have <Part1/Beginner-Tutorial-Part1-Intro.html>`_
A tour of Evennia and how to use the tools, including an introduction to Python.
Part 2: `What we want <Part2/Beginner-Tutorial-Part2-Intro.html>`_
Planning our tutorial game and what to think about when planning your own in the future.
Part 3: `How we get there <Part3/Beginner-Tutorial-Part3-Intro.html>`_
Getting down to the meat of extending Evennia to make our game
Part 4: `Using what we created <Part4/Beginner-Tutorial-Part4-Intro.html>`_
Building a tech-demo and world content to go with our code
Part 5: `Showing the world <Part5/Beginner-Tutorial-Part5-Intro.html>`_
Taking our new game online and let players try it out
```
Welcome to Evennia! This multi-part Beginner Tutorial will help you get off the ground. It consists
of five parts, each with several lessons. You can pick what seems interesting, but if you
follow through to the end you will have created a little online game of your own to play
and share with others!
Use the menu on the right to get the index of each tutorial-part. Use the [next](Part1/Beginner-Tutorial-Part1-Intro.md)
and [previous](../Howtos-Overview.md) links to step from lesson to lesson.
## Things you need
- A Command line
- A MUD client (or web browser)
- A text-editor/IDE
- Evennia installed and a game-dir initialized
### A Command line
You need to know how to find your Terminal/Console in your OS. The Evennia server can be controlled
from in-game, but you _will_ need to use the command-line to get anywhere. Here are some starters:
- [Django-girls' Intro to the Command line for different OS:es](https://tutorial.djangogirls.org/en/intro_to_command_line/)
Note that we usually only show forward-slashes `/` for file system paths. Windows users should mentally convert this to
back-slashes `\` instead.
### A MUD client
You might already have a MUD-client you prefer. Check out the [grid of supported clients](../../Setup/Client-Support-Grid.md) for aid.
If telnet's not your thing, you can also just use Evennia's web client in your browser.
> In this documentation we often use the terms 'MUD', 'MU' or 'MU*' interchangeably
to represent all the historically different forms of text-based multiplayer game-styles,
like MUD, MUX, MUSH, MUCK, MOO and others. Evennia can be used to create all those game-styles
and more.
### An Editor
You need a text-editor to edit Python source files. Most everything that can edit and output raw
text works (so not Word).
- [Here's a blog post summing up some of the alternatives](https://www.elegantthemes.com/blog/resources/best-code-editors) - these
things don't change much from year to year. Popular choices for Python are PyCharm, VSCode, Atom, Sublime Text and Notepad++.
Evennia is to a very large degree coded in VIM, but that's not suitable for beginners.
> Hint: When setting up your editor, make sure that pressing TAB inserts _4 spaces_ rather than a Tab-character. Since
> Python is whitespace-aware, this will make your life a lot easier.
### Set up a game dir for the tutorial
Next you should make sure you have [installed Evennia](../../Setup/Installation.md). If you followed the instructions
you will already have created a game-dir. You could use that for this tutorial or you may want to do the
tutorial in its own, isolated game dir; it's up to you.
- If you want a new gamedir for the tutorial game and already have Evennia running with another gamedir,
first enter that gamedir and run
evennia stop
> If you want to run two parallel servers, that'd be fine too, but one would have to use
> different ports from the defaults, or there'd be a clash. We will go into changing settings later.
- Now go to where you want to create your tutorial-game. We will always refer to it as `mygame` so
it may be convenient if you do too:
evennia --init mygame
cd mygame
evennia migrate
evennia start --log
Add your superuser name and password at the prompt (email is optional). Make sure you can
go to `localhost:4000` in your MUD client or to [http://localhost:4001](http://localhost:4001)
in your web browser (Mac users: Try `127.0.0.1` instead of `localhost` if you have trouble).
The above `--log` flag will have Evennia output all its logs to the terminal. This will block
the terminal from other input. To leave the log-view, press `Ctrl-C` (`Cmd-C` on Mac). To see
the log again just run
evennia --log
You should now be good to go on to [the first part of the tutorial](Part1/Beginner-Tutorial-Part1-Intro.md).
Good luck!
<details>
<summary>
Click here to expand a list of all Beginner-Tutorial sections (all parts).
</summary>
```{toctree}
Part1/Beginner-Tutorial-Part1-Intro
Part2/Beginner-Tutorial-Part2-Intro
Part3/Beginner-Tutorial-Part3-Intro
Part4/Beginner-Tutorial-Part4-Intro
Part5/Beginner-Tutorial-Part5-Intro
```
</details>

View file

@ -0,0 +1,392 @@
# Adding custom commands
In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll
also learn some more basics about how to manipulate strings and get information out of Evennia.
A Command is something that handles the input from a user and causes a result to happen.
An example is `look`, which examines your current location and tells how it looks like and
what is in it.
```{sidebar} Commands are not typeclassed
If you just came from the previous lesson, you might want to know that Commands and
CommandSets are not `typeclassed`. That is, instances of them are not saved to the
database. They are "just" normal Python classes.
```
In Evennia, a Command is a Python _class_. If you are unsure about what a class is, review the
previous lessons! A Command inherits from `evennia.Command` or from one of the alternative command-
classes, such as `MuxCommand` which is what most default commands use.
All Commands are in turn grouped in another class called a _Command Set_. Think of a Command Set
as a bag holding many different commands. One CmdSet could for example hold all commands for
combat, another for building etc. By default, Evennia groups all character-commands into one
big cmdset.
Command-Sets are then associated with objects, for example with your Character. Doing so makes the
commands in that cmdset available to the object. So, to summarize:
- Commands are classes
- A group of Commands is stored in a CmdSet
- CmdSets are stored on objects - this defines which commands are available to that object.
## Creating a custom command
Open `mygame/commands/command.py`:
```python
"""
(module docstring)
"""
from evennia import Command as BaseCommand
# from evennia import default_cmds
class Command(BaseCommand):
"""
(class docstring)
"""
pass
# (lots of commented-out stuff)
# ...
```
Ignoring the docstrings (which you can read if you want), this is the only really active code in the module.
We can see that we import `Command` from `evennia` and use the `from ... import ... as ...` form to rename it
to `BaseCommand`. This is so we can let our child class also be named `Command` for reference. The class
itself doesn't do anything, it just has `pass`. So in the same way as `Object` in the previous lesson, this
class is identical to its parent.
> The commented out `default_cmds` gives us access to Evennia's default commands for easy overriding. We'll try
> that a little later.
We could modify this module directly, but to train imports we'll work in a separate module. Open a new file
`mygame/commands/mycommands.py` and add the following code:
```python
from commands.command import Command
class CmdEcho(Command):
key = "echo"
```
This is the simplest form of command you can imagine. It just gives itself a name, "echo". This is
what you will use to call this command later.
Next we need to put this in a CmdSet. It will be a one-command CmdSet for now! Change your file as such:
```python
from commands.command import Command
from evennia import CmdSet
class CmdEcho(Command):
key = "echo"
class MyCmdSet(CmdSet):
def at_cmdset_creation(self):
self.add(CmdEcho)
```
Our `EchoCmdSet` class must have an `at_cmdset_creation` method, named exactly
like this - this is what Evennia will be looking for when setting up the cmdset later, so
if you didn't set it up, it will use the parent's version, which is empty. Inside we add the
command class to the cmdset by `self.add()`. If you wanted to add more commands to this CmdSet you
could just add more lines of `self.add` after this.
Finally, let's add this command to ourselves so we can try it out. In-game you can experiment with `py` again:
> py self.cmdset.add("commands.mycommands.MyCmdSet")
Now try
> echo
Command echo has no defined `func()` - showing on-command variables:
...
...
You should be getting a long list of outputs. The reason for this is that your `echo` function is not really
"doing" anything yet and the default function is then to show all useful resources available to you when you
use your Command. Let's look at some of those listed:
Command echo has no defined `func()` - showing on-command variables:
obj (<class 'typeclasses.characters.Character'>): YourName
lockhandler (<class 'evennia.locks.lockhandler.LockHandler'>): cmd:all()
caller (<class 'typeclasses.characters.Character'>): YourName
cmdname (<class 'str'>): echo
raw_cmdname (<class 'str'>): echo
cmdstring (<class 'str'>): echo
args (<class 'str'>):
cmdset (<class 'evennia.commands.cmdset.CmdSet'>): @mail, about, access, accounts, addcom, alias, allcom, ban, batchcode, batchcommands, boot, cboot, ccreate,
cdesc, cdestroy, cemit, channels, charcreate, chardelete, checklockstring, clientwidth, clock, cmdbare, cmdsets, color, copy, cpattr, create, cwho, delcom,
desc, destroy, dig, dolphin, drop, echo, emit, examine, find, force, get, give, grapevine2chan, help, home, ic, inventory, irc2chan, ircstatus, link, lock,
look, menutest, mudinfo, mvattr, name, nick, objects, ooc, open, option, page, password, perm, pose, public, py, quell, quit, reload, reset, rss2chan, say,
script, scripts, server, service, sessions, set, setdesc, sethelp, sethome, shutdown, spawn, style, tag, tel, test2010, test2028, testrename, testtable,
tickers, time, tunnel, typeclass, unban, unlink, up, up, userpassword, wall, whisper, who, wipe
session (<class 'evennia.server.serversession.ServerSession'>): Griatch(#1)@1:2:7:.:0:.:0:.:1
account (<class 'typeclasses.accounts.Account'>): Griatch(account 1)
raw_string (<class 'str'>): echo
--------------------------------------------------
echo - Command variables from evennia:
--------------------------------------------------
name of cmd (self.key): echo
cmd aliases (self.aliases): []
cmd locks (self.locks): cmd:all();
help category (self.help_category): General
object calling (self.caller): Griatch
object storing cmdset (self.obj): Griatch
command string given (self.cmdstring): echo
current cmdset (self.cmdset): ChannelCmdSet
These are all properties you can access with `.` on the Command instance, such as `.key`, `.args` and so on.
Evennia makes these available to you and they will be different every time a command is run. The most
important ones we will make use of now are:
- `caller` - this is 'you', the person calling the command.
- `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find
that this would be `" foo bar"`.
- `obj` - this is object on which this Command (and CmdSet) "sits". So you, in this case.
The reason our command doesn't do anything yet is because it's missing a `func` method. This is what Evennia
looks for to figure out what a Command actually does. Modify your `CmdEcho` class:
```python
# ...
class CmdEcho(Command):
"""
A simple echo command
Usage:
echo <something>
"""
key = "echo"
def func(self):
self.caller.msg(f"Echo: '{self.args}'")
# ...
```
First we added a docstring. This is always a good thing to do in general, but for a Command class, it will also
automatically become the in-game help entry! Next we add the `func` method. It has one active line where it
makes use of some of those variables we found the Command offers to us. If you did the
[basic Python tutorial](./Python-basic-introduction.md), you will recognize `.msg` - this will send a message
to the object it is attached to us - in this case `self.caller`, that is, us. We grab `self.args` and includes
that in the message.
Since we haven't changed `MyCmdSet`, that will work as before. Reload and re-add this command to ourselves to
try out the new version:
> reload
> py self.cmdset.add("commands.mycommands.MyCmdSet")
> echo
Echo: ''
Try to pass an argument:
> echo Woo Tang!
Echo: ' Woo Tang!'
Note that there is an extra space before `Woo!`. That is because self.args contains the _everything_ after
the command name, including spaces. Evennia will happily understand if you skip that space too:
> echoWoo Tang!
Echo: 'Woo Tang!'
There are ways to force Evennia to _require_ an initial space, but right now we want to just ignore it since
it looks a bit weird for our echo example. Tweak the code:
```python
# ...
class CmdEcho(Command):
"""
A simple echo command
Usage:
echo <something>
"""
key = "echo"
def func(self):
self.caller.msg(f"Echo: '{self.args.strip()}'")
# ...
```
The only difference is that we called `.strip()` on `self.args`. This is a helper method available on all
strings - it strips out all whitespace before and after the string. Now the Command-argument will no longer
have any space in front of it.
> reload
> py self.cmdset.add("commands.mycommands.MyCmdSet")
> echo Woo Tang!
Echo: 'Woo Tang!'
Don't forget to look at the help for the echo command:
> help echo
You will get the docstring you put in your Command-class.
### Making our cmdset persistent
It's getting a little annoying to have to re-add our cmdset every time we reload, right? It's simple
enough to make `echo` a _persistent_ change though:
> py self.cmdset.add("commands.mycommands.MyCmdSet", persistent=True)
Now you can `reload` as much as you want and your code changes will be available directly without
needing to re-add the MyCmdSet again. To remove the cmdset again, do
> py self.cmdset.remove("commands.mycommands.MyCmdSet")
But for now, keep it around, we'll expand it with some more examples.
### Figuring out who to hit
Let's try something a little more exciting than just echo. Let's make a `hit` command, for punching
someone in the face! This is how we want it to work:
> hit <target>
You hit <target> with full force!
Not only that, we want the <target> to see
You got hit by <hitter> with full force!
Here, `<hitter>` would be the one using the `hit` command and `<target>` is the one doing the punching.
Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`.
```python
# ...
class CmdHit(Command):
"""
Hit a target.
Usage:
hit <target>
"""
key = "hit"
def func(self):
args = self.args.strip()
if not args:
self.caller.msg("Who do you want to hit?")
return
target = self.caller.search(args)
if not target:
return
self.caller.msg(f"You hit {target.key} with full force!")
target.msg(f"You got hit by {self.caller.key} with full force!")
# ...
```
A lot of things to dissect here:
- **Line 4**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
- **Lines 5**-11: The docstring and help-entry for the command. You could expand on this as much as you wanted.
- **Line 12**: We want to write `hit` to use this command.
- **Line 15**: We strip the whitespace from the argument like before. Since we don't want to have to do
`self.args.strip()` over and over, we store the stripped version
in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still
have the whitespace and is not the same as `args` in this example.
```{sidebar} if-statements
The full form of the if statement is
if condition:
...
elif othercondition:
...
else:
...
There can be any number of `elifs` to mark when different branches of the code should run. If
the `else` condition is given, it will run if none of the other conditions was truthy. In Python
the `if..elif..else` structure also serves the same function as `case` in some other languages.
```
- **Line 16** has our first _conditional_, an `if` statement. This is written on the form `if <condition>:` and only
if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in
Python it's usually easier to learn what is "falsy":
- `False` - this is a reserved boolean word in Python. The opposite is `True`.
- `None` - another reserved word. This represents nothing, a null-result or value.
- `0` or `0.0`
- The empty string `""` or `''` or `""""""` or `''''''`
- Empty _iterables_ we haven't seen yet, like empty lists `[]`, empty tuples `()` and empty dicts `{}`.
- Everything else is "truthy".
Line 16's condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the
whole conditional becomes truthy. Let's continue in the code:
- **Lines 17-18**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string.
- **Line 18**: `return` is a reserved Python word that exits `func` immediately.
- **Line 19**: We use `self.caller.search` to look for the target in the current location.
- **Lines 20-21**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target.
In that case, `target` will be `None` and we should just directly `return`.
- **Lines 22-23**: At this point we have a suitable target and can send our punching strings to each.
Finally we must also add this to a CmdSet. Let's add it to `MyCmdSet` which we made persistent earlier.
```python
# ...
class MyCmdSet(CmdSet):
def at_cmdset_creation(self):
self.add(CmdEcho)
self.add(CmdHit)
```
```{sidebar} Errors in your code
With longer code snippets to try, it gets more and more likely you'll
make an error and get a `traceback` when you reload. This will either appear
directly in-game or in your log (view it with `evennia -l` in a terminal).
Don't panic; tracebacks are your friends - they are to be read bottom-up and usually describe
exactly where your problem is. Refer to `The Python intro <Python-basic-introduction.html>`_ for
more hints. If you get stuck, reach out to the Evennia community for help.
```
Next we reload to let Evennia know of these code changes and try it out:
> reload
hit
Who do you want to hit?
hit me
You hit YourName with full force!
You got hit by YourName with full force!
Lacking a target, we hit ourselves. If you have one of the dragons still around from the previous lesson
you could try to hit it (if you dare):
hit smaug
You hit Smaug with full force!
You won't see the second string. Only Smaug sees that (and is not amused).
## Summary
In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves.
We also upset a dragon.
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
get into how we replace and extend Evennia's default Commands.

View file

@ -0,0 +1,66 @@
# Part 1: What we have
```{eval-rst}
.. sidebar:: Beginner Tutorial Parts
`Introduction <../Beginner-Tutorial-Intro.html>`_
Getting set up.
**Part 1: What we have**
A tour of Evennia and how to use the tools, including an introduction to Python.
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
Planning our tutorial game and what to think about when planning your own in the future.
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
Getting down to the meat of extending Evennia to make our game
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
Building a tech-demo and world content to go with our code
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
Taking our new game online and let players try it out
```
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
and how to find things we are looking for. We will also dive into some of things you'll
need to know to fully utilize the system, including giving you a brief rundown of Python concepts. If you are
an experienced Python programmer, some sections may feel a bit basic, but you will at least not have seen
these concepts in the context of Evennia before.
## Lessons
```{toctree}
:maxdepth: 1
:numbered:
Building-Quickstart
Tutorial-World
Python-basic-introduction
Gamedir-Overview
Python-classes-and-objects
Evennia-Library-Overview
Learning-Typeclasses
Adding-Commands
More-on-Commands
Creating-Things
Searching-Things
Django-queries
```
## Table of Contents
```{toctree}
:maxdepth: 2
Building-Quickstart
Tutorial-World
Python-basic-introduction
Gamedir-Overview
Python-classes-and-objects
Evennia-Library-Overview
Learning-Typeclasses
Adding-Commands
More-on-Commands
Creating-Things
Searching-Things
Django-queries
```

View file

@ -0,0 +1,311 @@
# Using commands and building stuff
In this lesson we will test out what we can do in-game out-of-the-box. Evennia ships with
[around 90 default commands](../../../Components/Default-Commands.md), and while you can override those as you please,
they can be quite useful.
Connect and log into your new game and you will end up in the "Limbo" location. This
is the only room in the game at this point. Let's explore the commands a little.
The default commands has syntax [similar to MUX](../../../Concepts/Using-MUX-as-a-Standard.md):
command[/switch/switch...] [arguments ...]
An example would be
create/drop box
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.
> Are you used to commands starting with @, like @create? That will work too. Evennia simply ignores
> the preceeding @.
## Getting help
help
Will give you a list of all commands available to you. Use
help <commandname>
to see the in-game help for that command.
## Looking around
The most common comman is
look
This will show you the description of the current location. `l` is an alias.
When targeting objects in commands you have two special labels you can use, `here` for the current
room or `me`/`self` to point back to yourself. So
look me
will give you your own description. `look here` is, in this case, the same as plain `look`.
## 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 superuser status again when you are done.
## 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
```{warning} MUD clients and semi-colon
Some traditional MUD clients use the semi-colon `;` to separate client inputs. If so,
the above line will give an error. You need to change your client to use another command-separator
or to put it in 'verbatim' mode. If you still have trouble, use the Evennia web client instead.
```
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](../../../Components/Locks.md), 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](../../../Components/Attributes.md)
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. We will get to that
later, in the [Commands tutorial](./Adding-Commands.md).
## Get a Personality
[Scripts](../../../Components/Scripts.md) 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
This string will tell Evennia to dig up the Python code at the place we indicate. It already knows
to look in the `contrib/` folder, so we don't have to give the full path.
> Note also how we use `.` instead of `/` (or `\` on Windows). This is a so-called "Python path". In a Python-path,
> you separate the parts of the path with `.` and skip the `.py` file-ending. Importantly, it also allows you to point to
Python code _inside_ files, like the `BodyFunctions` class inside `bodyfunctions.py` (we'll get to classes later).
These "Python-paths" are used extensively throughout Evennia.
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](../../../Components/Scripts.md) page explains more details.
## Pushing Your Buttons
If we get back to the box we made, there is only so much fun you can have 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](../../../Components/Typeclasses.md), [Scripts](../../../Components/Scripts.md)
and object-based [Commands](../../../Components/Commands.md), 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` class.
Let's make us one of _those_!
create/drop button:tutorial_examples.red_button.RedButton
The same way we did with the Script Earler, we specify a "Python-path" to the Python code we want Evennia
to use for creating the object. 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](../../../Components/Typeclasses.md) and [Commands](../../../Components/Commands.md) controlling it are
inside [evennia/contrib/tutorials/red_button](evennia.contrib.tutorials.red_button)
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.
```{warning} Don't press the invitingly blinking red button.
```
## 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.
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
(You can also us the #dbref of limbo, which you can find by using `examine here` when in limbo).
## 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
As mentioned, `here` is an alias for 'your current location'. The box should now be back in Limbo with you.
We are getting tired of the box. Let's destroy it.
destroy box
It will ask you for confirmation. Once you give it, the box will be gone.
You can destroy many objects in one go by giving a comma-separated list of objects (or a range
of #dbrefs, if they are not in the same location) to the command.
## Adding a Help Entry
The Command-help is something you modify in Python code. We'll get to that when we get to how to
add Commands. But you can also add regular help entries, for example to explain something about
the history of your game world:
sethelp/add History = At the dawn of time ...
You will now find your new `History` entry in the `help` list and read your help-text with `help History`.
## Adding a World
After this brief introduction to building and using in-game commands you may be ready to see a more fleshed-out
example. Evennia comes with a tutorial world for you to explore. We will try that out in the next lesson.

View file

@ -0,0 +1,48 @@
# Creating things
We have already created some things - dragons for example. There are many different things to create
in Evennia though. In the last lesson we learned about typeclasses, the way to make objects persistent in the database.
Given the path to a Typeclass, there are three ways to create an instance of it:
- Firstly, you can call the class directly, and then `.save()` it:
obj = SomeTypeClass(db_key=...)
obj.save()
This has the drawback of being two operations; you must also import the class and have to pass
the actual database field names, such as `db_key` instead of `key` as keyword arguments.
- Secondly you can use the Evennia creation helpers:
obj = evennia.create_object(SomeTypeClass, key=...)
This is the recommended way if you are trying to create things in Python. The first argument can either be
the class _or_ the python-path to the typeclass, like `"path.to.SomeTypeClass"`. It can also be `None` in which
case the Evennia default will be used. While all the creation methods
are available on `evennia`, they are actually implemented in [evennia/utils/create.py](../../../api/evennia.utils.create.md).
- Finally, you can create objects using an in-game command, such as
create/drop obj:path.to.SomeTypeClass
As a developer you are usually best off using the two other methods, but a command is usually the only way
to let regular players or builders without Python-access help build the game world.
## Creating Objects
This is one of the most common creation-types. These are entities that inherits from `DefaultObject` at any distance.
They have an existence in the game world and includes rooms, characters, exits, weapons, flower pots and castles.
> py
> import evennia
> rose = evennia.create_object(key="rose")
Since we didn't specify the `typeclass` as the first argument, the default given by `settings.BASE_OBJECT_TYPECLASS`
(`typeclasses.objects.Object`) will be used.
## Creating Accounts
An _Account_ is an out-of-character (OOC) entity, with no existence in the game world.
You can find the parent class for Accounts in `typeclasses/accounts.py`.
_TODO_

View file

@ -0,0 +1,397 @@
# Advanced searching - Django Database queries
```{important} More advanced lesson!
Learning about Django's queryset language is very useful once you start doing more advanced things
in Evennia. But it's not strictly needed out the box and can be a little overwhelming for a first
reading. So if you are new to Python and Evennia, feel free to just skim this lesson and refer
back to it later when you've gained more experience.
```
The search functions and methods we used in the previous lesson are enough for most cases.
But sometimes you need to be more specific:
- You want to find all `Characters` ...
- ... who are in Rooms tagged as `moonlit` ...
- ... _and_ who has the Attribute `lycantrophy` with a level higher than 2 ...
- ... because they'll should immediately transform to werewolves!
In principle you could achieve this with the existing search functions combined with a lot of loops
and if statements. But for something non-standard like this, querying the database directly will be
much more efficient.
Evennia uses [Django](https://www.djangoproject.com/) to handle its connection to the database.
A [django queryset](https://docs.djangoproject.com/en/3.0/ref/models/querysets/) represents
a database query. One can add querysets together to build ever-more complicated queries. Only when
you are trying to use the results of the queryset will it actually call the database.
The normal way to build a queryset is to define what class of entity you want to search by getting its
`.objects` resource, and then call various methods on that. We've seen this one before:
all_weapons = Weapon.objects.all()
This is now a queryset representing all instances of `Weapon`. If `Weapon` had a subclass `Cannon` and we
only wanted the cannons, we would do
all_cannons = Cannon.objects.all()
Note that `Weapon` and `Cannon` are different typeclasses. You won't find any `Cannon` instances in
the `all_weapon` result above, confusing as that may sound. To get instances of a Typeclass _and_ the
instances of all its children classes you need to use `_family`:
```{sidebar} _family
The all_family, filter_family etc is an Evennia-specific
thing. It's not part of regular Django.
```
really_all_weapons = Weapon.objects.all_family()
This result now contains both `Weapon` and `Cannon` instances.
To limit your search by other criteria than the Typeclass you need to use `.filter`
(or `.filter_family`) instead:
roses = Flower.objects.filter(db_key="rose")
This is a queryset representing all objects having a `db_key` equal to `"rose"`.
Since this is a queryset you can keep adding to it; this will act as an `AND` condition.
local_roses = roses.filter(db_location=myroom)
We could also have written this in one statement:
local_roses = Flower.objects.filter(db_key="rose", db_location=myroom)
We can also `.exclude` something from results
local_non_red_roses = local_roses.exclude(db_key="red_rose")
Only until we actually try to examine the result will the database be called. Here it's called when we
try to loop over the queryset:
for rose in local_non_red_roses:
print(rose)
From now on, the queryset is _evaluated_ and we can't keep adding more queries to it - we'd need to
create a new queryset if we wanted to find some other result. Other ways to evaluate the queryset is to
print it, convert it to a list with `list()` and otherwise try to access its results.
Note how we use `db_key` and `db_location`. This is the actual names of these database fields. By convention
Evennia uses `db_` in front of every database field. When you use the normal Evennia search helpers and objects
you can skip the `db_` but here we are calling the database directly and need to use the 'real' names.
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.
> All of Evennia search functions use querysets under the hood. The `evennia.search_*` functions actually
> return querysets, which means you could in principle keep adding queries to their results as well.
## Queryset field lookups
Above we found roses with exactly the `db_key` `"rose"`. This is an _exact_ match that is _case sensitive_,
so it would not find `"Rose"`.
# this is case-sensitive and the same as =
roses = Flower.objects.filter(db_key__exact="rose"
# the i means it's case-insensitive
roses = Flower.objects.filter(db_key__iexact="rose")
The Django field query language uses `__` in the same way as Python uses `.` to access resources. This
is because `.` is not allowed in a function keyword.
roses = Flower.objects.filter(db_key__icontains="rose")
This will find all flowers whose name contains the string `"rose"`, like `"roses"`, `"wild rose"` etc. The
`i` in the beginning makes the search case-insensitive. Other useful variations to use
are `__istartswith` and `__iendswith`. You can also use `__gt`, `__ge` for "greater-than"/"greater-or-equal-than"
comparisons (same for `__lt` and `__le`). There is also `__in`:
swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword"))
One also uses `__` to access foreign objects like Tags. Let's for example assume this is how we identify mages:
char.tags.add("mage", category="profession")
Now, in this case we have an Evennia helper to do this search:
mages = evennia.search_tags("mage", category="profession")
But this will find all Objects with this tag+category. Maybe you are only looking for Vampire mages:
sparkly_mages = Vampire.objects.filter(db_tags__db_key="mage", db_tags__db_category="profession")
This looks at the `db_tags` field on the `Vampire` and filters on the values of each tag's
`db_key` and `db_category` together.
For more field lookups, see the
[django docs](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#field-lookups) on the subject.
## Get that werewolf ...
Let's see if we can make a query for the werewolves in the moonlight we mentioned at the beginning
of this section.
Firstly, we make ourselves and our current location match the criteria, so we can test:
> py here.tags.add("moonlit")
> py me.db.lycantrophy = 3
This is an example of a more complex query. We'll consider it an example of what is
possible.
```{sidebar} Line breaks
Note the way of writing this code. It would have been very hard to read if we just wrote it in
one long line. But since we wrapped it in `(...)` we can spread it out over multiple lines
without worrying about line breaks!
```
```python
from typeclasses.characters import Character
will_transform = (
Character.objects
.filter(
db_location__db_tags__db_key__iexact="moonlit",
db_attributes__db_key="lycantrophy",
db_attributes__db_value__gt=2)
)
```
- **Line 3** - We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
- **Line 4** - We start to filter ...
- **Line 5**
- ... by accessing the `db_location` field (usually this is a Room)
- ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field
that we can treat like an object for this purpose; it references all Tags on the location)
- ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive).
- **Line 6** - ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
- **Line 7** - ... at the same time as the `Attribute`'s `db_value` is greater-than 2.
Running this query makes our newly lycantrrophic Character appear in `will_transform`. Success!
> Don't confuse database fields with [Attributes](../../../Components/Attributes.md) 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.
## Complex queries
All examples so far used `AND` relations. The arguments to `.filter` are added together with `AND`
("we want tag room to be "monlit" _and_ lycantrhopy be > 2").
For queries using `OR` and `NOT` we need Django's
[Q object](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). It is
imported from Django directly:
from django.db.models import Q
The `Q` is an object that is created with the same arguments as `.filter`, for example
Q(db_key="foo")
You can then use this `Q` instance as argument in a `filter`:
q1 = Q(db_key="foo")
Character.objects.filter(q1)
The useful thing about `Q` is that these objects can be chained together with special symbols (bit operators):
`|` for `OR` and `&` for `AND`. A tilde `~` in front negates the expression inside the `Q` and thus
works like `NOT`.
q1 = Q(db_key="Dalton")
q2 = Q(db_location=prison)
Character.objects.filter(q1 | ~q2)
Would get all Characters that are either named "Dalton" _or_ which is _not_ in prison. The result is a mix
of Daltons and non-prisoners.
Let us expand our original werewolf query. Not only do we want to find all Characters in a moonlit room
with a certain level of `lycanthrophy`. Now we also want the full moon to immediately transform people who were
recently bitten, even if their `lycantrophy` level is not yet high enough (more dramatic this way!). Let's say there is
a Tag "recently_bitten" that controls this.
This is how we'd change our query:
```python
from django.db.models import Q
will_transform = (
Character.objects
.filter(
Q(db_location__db_tags__db_key__iexact="moonlit")
& (
Q(db_attributes__db_key="lycantrophy",
db_attributes__db_value__gt=2)
| Q(db_tags__db_key__iexact="recently_bitten")
))
.distinct()
)
```
That's quite compact. It may be easier to see what's going on if written this way:
```python
from django.db.models import Q
q_moonlit = Q(db_location__db_tags__db_key__iexact="moonlit")
q_lycantropic = Q(db_attributes__db_key="lycantrophy", db_attributes__db_value__gt=2)
q_recently_bitten = Q(db_tags__db_key__iexact="recently_bitten")
will_transform = (
Character.objects
.filter(q_moonlit & (q_lycantropic | q_recently_bitten))
.distinct()
)
```
```{sidebar} SQL
These Python structures are internally converted to SQL, the native language of the database.
If you are familiar with SQL, these are many-to-many tables joined with `LEFT OUTER JOIN`,
which may lead to multiple merged rows combining the same object with different relations.
```
This reads as "Find all Characters in a moonlit room that either has the Attribute `lycantrophy` higher
than two _or_ which has the Tag `recently_bitten`". With an OR-query like this it's possible to find the
same Character via different paths, so we add `.distinct()` at the end. This makes sure that there is only
one instance of each Character in the result.
## Annotations
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* do it like this (don't actually do it this way!):
```python
from typeclasses.rooms import Room
all_rooms = Rooms.objects.all()
rooms_with_five_objects = []
for room in all_rooms:
if len(room.contents) >= 5:
rooms_with_five_objects.append(room)
```
Above we get all rooms and then use `list.append()` to keep adding the right rooms
to an ever-growing list. This is _not_ a good idea, once your database grows this will
be unnecessarily computing-intensive. The database is much more suitable for this.
_Annotations_ allow you to set a 'variable' inside the query that you can
then access from other parts of the query. Let's do the same example as before directly in the database:
```python
from typeclasses.rooms import Room
from django.db.models import Count
rooms = (
Room.objects
.annotate(
num_objects=Count('locations_set'))
.filter(num_objects__gte=5)
)
```
`Count` is a Django class for counting the number of things in the database.
Here we first create an annotation `num_objects` of type `Count`. It creates an in-database function
that will count the number of results inside the database.
> Note the 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*.
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.
## F-objects
What if we wanted to compare two dynamic 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 (silly example, but ...)? This can be with Django's
[F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions).
So-called F expressions allow you to do a query that looks at a value of each object in the database.
```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'))
)
```
Here we used `.annotate` to create two in-query 'variables' `num_objects` and `num_tags`. We then
directly use these results in the filter. Using `F()` allows for also the right-hand-side of the filter
condition to be calculated on the fly, completely within the database.
## Grouping and returning only certain properties
Suppose you used tags to mark someone belonging to an organization. Now you want to make a list and
need to get the membership count of every organization all at once.
The `.annotate`, `.values_list`, and `.order_by` queryset methods are useful for this. Normally when
you run a `.filter`, what you get back is a bunch of full typeclass instances, like roses or swords.
Using `.values_list` you can instead choose to only get back certain properties on objects.
The `.order_by` method finally allows for sorting the results according to some criterion:
```python
from django.db.models import Count
from typeclasses.rooms import Room
result = (
Character.objects
.filter(db_tags__db_category="organization")
.annotate(tagcount=Count('id'))
.order_by('-tagcount'))
.values_list('db_tags__db_key', "tagcount")
```
Here we fetch all Characters who ...
- ... has a tag of category "organization" on them
- ... along the way we count how many different Characters (each `id` is unique) we find for each organization
and store it in a 'variable' `tagcount` using `.annotate` and `Count`
- ... we use this count to sort the result in descending order of `tagcount` (descending because there is a minus sign,
default is increasing order but we want the most popular organization to be first).
- ... and finally we make sure to only return exactly the properties we want, namely the name of the organization tag
and how many matches we found for that organization.
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's poets society', 3872),
("Chainsol's Ainneve Testers", 2076),
("Blaufeuer's Whitespace Fixers", 1903),
("Volund's Bikeshed Design Crew", 1764),
("Tehom's Glorious Misanthropes", 1763)
]
```
## Conclusions
We have covered a lot of ground in this lesson and covered several more complex topics. Knowing how to
query using Django is a powerful skill to have.
This concludes the first part of the Evennia starting tutorial - "What we have". Now we have a good foundation
to understand how to plan what our tutorial game will be about.

View file

@ -0,0 +1,123 @@
# Overview of the Evennia library
```{sidebar} API
API stands for `Application Programming Interface`, a description for how to access
the resources of a program or library.
```
A good place to start exploring Evennia is the [Evenia-API frontpage](../../../Evennia-API.md).
This page sums up the main components of Evennia with a short description of each. Try clicking through
to a few entries - once you get deep enough you'll see full descriptions
of each component along with their documentation. You can also click `[source]` to see the full Python source
for each thing.
You can also browse [the evennia repository on github](https://github.com/evennia/evennia). This is exactly
what you can download from us. The github repo is also searchable.
Finally, you can clone the evennia repo to your own computer and read the sources locally. This is necessary
if you want to help with Evennia's development itself. See the
[extended install instructions](../../../Setup/Installation-Git.md) if you want to do this.
## Where is it?
If Evennia is installed, you can import from it simply with
import evennia
from evennia import some_module
from evennia.some_module.other_module import SomeClass
and so on.
If you installed Evennia with `pip install`, the library folder will be installed deep inside your Python
installation. If you cloned the repo there will be a folder `evennia` on your hard drive there.
If you cloned the repo or read the code on `github` you'll find this being the outermost structure:
evennia/
bin/
CHANGELOG.md
...
...
docs/
evennia/
This outer layer is for Evennia's installation and package distribution. That internal folder `evennia/evennia/` is
the _actual_ library, the thing covered by the API auto-docs and what you get when you do `import evennia`.
> The `evennia/docs/` folder contains the sources for this documentation. See
> [contributing to the docs](../../../Contributing-Docs.md) if you want to learn more about how this works.
This the the structure of the Evennia library:
- evennia
- [`__init__.py`](../../../Evennia-API.md#shortcuts) - The "flat API" of Evennia resides here.
- [`settings_default.py`](../../../Setup/Settings.md#settings-file) - Root settings of Evennia. Copy settings
from here to `mygame/server/settings.py` file.
- [`commands/`](../../../Components/Commands.md) - The command parser and handler.
- `default/` - The [default commands](../../../Components/Default-Commands.md) and cmdsets.
- [`comms/`](../../../Components/Channels.md) - Systems for communicating in-game.
- `contrib/` - Optional plugins too game-specific for core Evennia.
- `game_template/` - Copied to become the "game directory" when using `evennia --init`.
- [`help/`](../../../Components/Help-System.md) - Handles the storage and creation of help entries.
- `locale/` - Language files ([i18n](../../../Concepts/Internationalization.md)).
- [`locks/`](../../../Components/Locks.md) - Lock system for restricting access to in-game entities.
- [`objects/`](../../../Components/Objects.md) - In-game entities (all types of items and Characters).
- [`prototypes/`](../../../Components/Prototypes.md) - Object Prototype/spawning system and OLC menu
- [`accounts/`](../../../Components/Accounts.md) - Out-of-game Session-controlled entities (accounts, bots etc)
- [`scripts/`](../../../Components/Scripts.md) - Out-of-game entities equivalence to Objects, also with timer support.
- [`server/`](../../../Components/Portal-And-Server.md) - Core server code and Session handling.
- `portal/` - Portal proxy and connection protocols.
- [`typeclasses/`](../../../Components/Typeclasses.md) - Abstract classes for the typeclass storage and database system.
- [`utils/`](../../../Components/Coding-Utils.md) - Various miscellaneous useful coding resources.
- [`web/`](../../../Concepts/Web-Features.md) - Web resources and webserver. Partly copied into game directory on initialization.
```{sidebar} __init__.py
The `__init__.py` file is a special Python filename used to represent a Python 'package'.
When you import `evennia` on its own, you import this file. When you do `evennia.foo` Python will
first look for a property `.foo` in `__init__.py` and then for a module or folder of that name
in the same location.
```
While all the actual Evennia code is found in the various folders, the `__init__.py` represents the entire
package `evennia`. It contains "shortcuts" to code that is actually located elsewhere. Most of these shortcuts
are listed if you [scroll down a bit](../../../Evennia-API.md) on the Evennia-API page.
## An example of exploring the library
In the previous lesson we took a brief look at `mygame/typeclasses/objects` as an example of a Python module. Let's
open it again. Inside is the `Object` class, which inherits from `DefaultObject`.
Near the top of the module is this line:
from evennia import DefaultObject
We want to figure out just what this DefaultObject offers. Since this is imported directly from `evennia`, we
are actually importing from `evennia/__init__.py`.
[Look at Line 159](github:evennia/__init__.py#159) of `evennia/__init__.py` and you'll find this line:
from .objects.objects import DefaultObject
```{sidebar} Relative and absolute imports
The first full-stop in `from .objects.objects ...` means that
we are importing from the current location. This is called a `relative import`.
By comparison, `from evennia.objects.objects` is an `absolute import`. In this particular
case, the two would give the same result.
```
> You can also look at [the right section of the API frontpage](../../../Evennia-API.md#typeclasses) and click through
> to the code that way.
The fact that `DefaultObject` is imported into `__init__.py` here is what makes it possible to also import
it as `from evennia import DefaultObject` even though the code for the class is not actually here.
So to find the code for `DefaultObject` we need to look in `evennia/objects/objects.py`. Here's how
to look it up in the docs:
1. Open the [API frontpage](../../../Evennia-API.md)
2. Locate the link to [evennia.objects.objects](evennia.objects.objects) and click on it.
3 You are now in the python module. Scroll down (or search in your web browser) to find the `DefaultObject` class.
4 You can now read what this does and what methods are on it. If you want to see the full source, click the
\[source\] link next to it.

View file

@ -0,0 +1,208 @@
# Overview of your new Game Dir
Next we will take a little detour to look at the _Tutorial World_. This is a little solo adventure
that comes with Evennia, a showcase for some of the things that are possible.
Now we have 'run the game' a bit and started with our forays into Python from inside Evennia.
It is time to start to look at how things look 'outside of the game'. Let's do a tour of your game-dir
Like everywhere in the docs we'll assume it's called `mygame`.
> When looking through files, ignore files ending with `.pyc` and the
`__pycache__` folder if it exists. This is internal Python compilation files that you should never
> need to touch. Files `__init__.py` is also often empty and can be ignored (they have to do with
> Python package management).
You may have noticed when we were building things in-game that we would often refer to code through
"python paths", such as
```{sidebar} Python-paths
A 'python path' uses '.' instead of '/' or '`\\`' and
skips the `.py` ending of files. It can also point to
the code contents of python files. Since Evennia is already
looking for code in your game dir, your python paths can start
from there.
So a path `/home/foo/devel/mygame/commands/command.py`
would translate to a Python-path `commands.command`.
```
create/drop button:tutorial_examples.red_button.RedButton
This is a fundamental aspect of coding Evennia - _you create code and then you tell Evennia where that
code is and when it should be used_. Above we told it to create a red button by pulling from specific code
in the `contribs/` folder but the same principle is true everywhere. So it's important to know where code is
and how you point to it correctly.
- `mygame/`
- `commands/` - This holds all your custom commands (user-input handlers). You both add your own
and override Evennia's defaults from here.
- `server`/ - The structure of this folder should not change since Evennia expects it.
- `conf/` - All server configuration files sits here. The most important file is `settings.py`.
- `logs/` - Server log files are stored here. When you use `evennia --log` you are actually
tailing the files in this directory.
- `typeclasses/` - this holds empty templates describing all database-bound entities in the
game, like Characters, Scripts, Accounts etc. Adding code here allows to customize and extend
the defaults.
- `web/` - This is where you override and extend the default templates, views and static files used
for Evennia's web-presence, like the website and the HTML5 webclient.
- `world/` - this is a "miscellaneous" folder holding everything related to the world you are
building, such as build scripts and rules modules that don't fit with one of the other folders.
> The `server/` subfolder should remain the way it is - Evennia expects this. But you could in
> principle change the structure of the rest of your game dir as best fits your preference.
> Maybe you don't need a world/ folder but prefer many folders with different aspects of your world?
> Or a new folder 'rules' for your RPG rules? This is fine. If you move things around you just need
> to update Evennia's default settings to point to the right places in the new structure.
## commands/
The `commands/` folder holds Python modules related to creating and extending the [Commands](../../../Components/Commands.md)
of Evennia. These manifest in game like the server understanding input like `look` or `dig`.
```{sidebar} Classes
A `class` is template for creating object-instances of a particular type
in Python. We will explain classes in more detail in the next
`python overview <Python-basic-tutorial-part-two>`_.
```
- [command.py](github:evennia/game_template/commands/command.py) (Python-path: `commands.command`) - this contain the
base _classes_ for designing new input commands, or override the defaults.
- [default_cmdsets.py](github:evennia/game_template/commands/default_cmdsets.py) (Python path: `commands.default_commands`) -
a cmdset (Command-Set) groups Commands together. Command-sets can be added and removed from objects on the fly,
meaning a user could have a different set of commands (or versions of commands) available depending on their circumstance
in the game. In order to add a new command to the game, it's common to import the new command-class
from `command.py` and add it to one of the default cmdsets in this module.
## server/
This folder contains resource necessary for running Evennia. Contrary to the other folders, the structure
of this should be kept the way it is.
- `evennia.db3` - you will only have this file if you are using the default SQLite3 database. This file
contains the entire database. Just copy it to make a backup. For development you could also just
make a copy once you have set up everything you need and just copy that back to 'reset' the state.
If you delete this file you can easily recreate it by running `evennia migrate`.
### server/logs/
This holds the server logs. When you do `evennia --log`, the evennia program is in fact tailing and concatenating
the `server.log` and `portal.log` files in this directory. The logs are rotated every week. Depending on your settings,
other logs, like the webserver HTTP request log can also be found here.
### server/conf/
This contains all configuration files of the Evennia server. These are regular Python modules which
means that they must be extended with valid Python. You can also add logic to them if you wanted to.
Common for the settings is that you generally will never them directly via their python-path; instead Evennia
knows where they are and will read them to configure itself at startup.
- `settings.py` - this is by far the most important file. It's nearly empty by default, rather you
are expected to copy&paste the changes you need from [evennia/default_settings.py](github:evennia/default_settings.py).
The default settings file is extensively documented. Importing/accessing the values in the settings
file is done in a special way, like this:
from django.conf import settings
To get to the setting `TELNET_PORT` in the settings file you'd then do
telnet_port = settings.TELNET_PORT
You cannot assign to the settings file dynamically; you must change the `settings.py` file directly to
change a setting.
- `secret_settings.py` - If you are making your code effort public, you may not want to share all settings online.
There may be server-specific secrets or just fine-tuning for your game systems that you prefer be kept secret
from the players. Put such settings in here, it will override values in `settings.py` and not be included in
version control.
- `at_initial_setup.py` - When Evennia starts up for the very first time, it does some basic tasks, like creating the
superuser and Limbo room. Adding to this file allows to add more actions for it to for first-startup.
- `at_search.py` - When searching for objects and either finding no match or more than one match, it will
respond by giving a warning or offering the user to differentiate between the multiple matches. Modifying
the code here will change this behavior to your liking.
- `at_server_startstop.py` - This allows to inject code to execute every time the server starts, stops or reloads
in different ways.
- `connection_screens.py` - This allows for changing the connection screen you see when you first connect to your
game.
- `inlinefuncs.py` - _Inlinefuncs_ are optional and limited 'functions' that can be embedded in any strings being
sent to a player. They are written as `$funcname(args)` and are used to customize the output
depending on the user receiving it. For example sending people the text `"Let's meet at $realtime(13:00, GMT)!`
would show every player seeing that string the time given in their own time zone. The functions added to this
module will become new inlinefuncs in the game.
- `inputfucs.py` - When a command like `look` is received by the server, it is handled by an _inputfunc_
that redirects it to the cmdhandler system. But there could be other inputs coming from the clients, like
button-presses or the request to update a health-bar. While most common cases are already covered, this is
where one adds new functions to process new types of input.
- `lockfuncs.py` - _Locks_ restrict access to things in-game. Lock funcs are used in a mini-language
to defined more complex locks. For example you could have a lockfunc that checks if the user is carrying
a given item, is bleeding or has a certain skill value. New functions added in this modules will
become available for use in lock definitions.
- `mssp.py` - Mud Server Status Protocol is a way for online MUD archives/listings (which you usually have
to sign up for) to track which MUDs are currently online, how many players they have etc. While Evennia handles
the dynamic information automatically, this is were you set up the meta-info about your game, such as its
theme, if player-killing is allowed and so on. This is a more generic form of the Evennia Game directory.
- `portal_services_plugins.py` - If you want to add new external connection protocols to Evennia, this is the place
to add them.
- `server_services_plugins.py` - This allows to override internal server connection protocols.
- `web_plugins.py` - This allows to add plugins to the Evennia webserver as it starts.
### typeclasses/
The [Typeclasses](../../../Components/Typeclasses.md) of Evennia are Evennia-specific Python classes whose instances save themselves
to the database. This allows a Character to remain in the same place and your updated strength stat to still
be the same after a server reboot.
- [accounts.py](github:evennia/game_template/typeclasses/accounts.py) (Python-path: `typeclasses.accounts`) - An
[Account](../../../Components/Accounts.md) represents the player connecting to the game. It holds information like email,
password and other out-of-character details.
- [channels.py](github:evennia/game_template/typeclasses/channels.py) (Python-path: `typeclasses.channels`) -
[Channels](../../../Components/Channels.md) are used to manage in-game communication between players.
- [objects.py](github:evennia/game_template/typeclasses/objects.py) (Python-path: `typeclasses.objects`) -
[Objects](../../../Components/Objects.md) represent all things having a location within the game world.
- [characters.py](github:evennia/game_template/typeclasses/characters.py) (Python-path: `typeclasses.characters`) -
The [Character](../../../Components/Objects.md#characters) is a subclass of Objects, controlled by Accounts - they are the player's
avatars in the game world.
- [rooms.py](github:evennia/game_template/typeclasses/rooms.py) (Python-path: `typeclasses.rooms`) - A
[Room](../../../Components/Objects.md#rooms) is also a subclass of Object; describing discrete locations. While the traditional
term is 'room', such a location can be anything and on any scale that fits your game, from a forest glade,
an entire planet or an actual dungeon room.
- [exits.py](github:evennia/game_template/typeclasses/exits.py) (Python-path: `typeclasses.exits`) -
[Exits](../../../Components/Objects.md#exits) is another subclass of Object. Exits link one Room to another.
- [scripts.py](github:evennia/game_template/typeclasses/scripts.py) (Python-path: `typeclasses.scripts`) -
[Scripts](../../../Components/Scripts.md) are 'out-of-character' objects. They have no location in-game and can serve as basis for
anything that needs database persistence, such as combat, weather, or economic systems. They also
have the ability to execute code repeatedly, on a timer.
### web/
This folder contains folders for overriding the default web-presence of Evennia with your own designs.
Most of these folders are empty except for a README file or a subset of other empty folders.
- `media/` - this empty folder is where you can place your own images or other media files you want the
web server to serve. If you are releasing your game with a lot of media (especially if you want videos) you
should consider re-pointing Evennia to use some external service to serve your media instead.
- `static_overrides/` - 'static' files include fonts, CSS and JS. Within this folder you'll find sub-folders for
overriding the static files for the `admin` (this is the Django web-admin), the `webclient` (this is thet
HTML5 webclient) and the `website`. Adding files to this folder will replace same-named files in the
default web presence.
- `template_overrides/` - these are HTML files, for the `webclient` and the `website`. HTML files are written
using [Jinja](https://jinja.palletsprojects.com/en/2.11.x/) templating, which means that one can override
only particular parts of a default template without touching others.
- `static/` - this is a work-directory for the web system and should _not_ be manually modified. Basically,
Evennia will copy static data from `static_overrides` here when the server starts.
- `urls.py` - this module links up the Python code to the URLs you go to in the browser.
### world/
This folder only contains some example files. It's meant to hold 'the rest' of your game implementation. Many
people change and re-structure this in various ways to better fit their ideas.
- [batch_cmds.ev](github:evennia/game_template/world/batch_cmds.ev) - This is an `.ev` file, which is essentially
just a list of Evennia commands to execute in sequence. This one is empty and ready to expand on. The
[Tutorial World](./Tutorial-World.md) was built with such a batch-file.
- [prototypes.py](github:evennia/game_template/world/prototypes.py) - A [prototype](../../../Components/Prototypes.md) is a way
to easily vary objects without changing their base typeclass. For example, one could use prototypes to
tell that Two goblins, while both of the class 'Goblin' (so they follow the same code logic), should have different
equipment, stats and looks.

View file

@ -0,0 +1,621 @@
# Making objects persistent
Now that we have learned a little about how to find things in the Evennia library, let's use it.
In the [Python classes and objects](./Python-classes-and-objects.md) lesson we created the dragons Fluffy, Cuddly
and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we `restart`
the server or `quit()` out of python mode they are gone.
This is what you should have in `mygame/typeclasses/monsters.py` so far:
```python
class Monster:
"""
This is a base class for Monsters.
"""
def __init__(self, key):
self.key = key
def move_around(self):
print(f"{self.key} is moving!")
class Dragon(Monster):
"""
This is a dragon-specific monster.
"""
def move_around(self):
super().move_around()
print("The world trembles.")
def firebreath(self):
"""
Let our dragon breathe fire.
"""
print(f"{self.key} breathes fire!")
```
## Our first persistent object
At this point we should know enough to understand what is happening in `mygame/typeclasses/objects.py`. Let's
open it:
```python
"""
module docstring
"""
from evennia import DefaultObject
class Object(DefaultObject):
"""
class docstring
"""
pass
```
So we have a class `Object` that _inherits_ from `DefaultObject`, which we have imported from Evennia.
The class itself doesn't do anything (it just `pass`es) but that doesn't mean it's useless. As we've seen,
it inherits all the functionality of its parent. It's in fact an _exact replica_ of `DefaultObject` right now.
If we knew what kind of methods and resources were available on `DefaultObject` we could add our own and
change the way it works!
> Hint: We will get back to this, but to learn what resources an Evennia parent like `DefaultObject` offers,
> easiest is to peek at its [API documentation](evennia.objects.objects.DefaultObject). The docstring for
> the `Object` class can also help.
One thing that Evennia classes offers and which you don't get with vanilla Python classes is _persistence_. As
you've found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this.
Go back to `mygame/typeclasses/monsters.py`. Change it as follows:
```python
from typeclasses.objects import Object
class Monster(Object):
"""
This is a base class for Monsters.
"""
def move_around(self):
print(f"{self.key} is moving!")
class Dragon(Monster):
"""
This is a dragon-specific Monster.
"""
def move_around(self):
super().move_around()
print("The world trembles.")
def firebreath(self):
"""
Let our dragon breathe fire.
"""
print(f"{self.key} breathes fire!")
```
Don't forget to save. We removed `Monster.__init__` and made `Monster` inherit from Evennia's `Object` (which in turn
inherits from Evennia's `DefaultObject`, as we saw). By extension, this means that `Dragon` also inherits
from `DefaultObject`, just from further away!
### Making a new object by calling the class
First reload the server as usual. We will need to create the dragon a little differently this time:
```{sidebar} Keyword arguments
Keyword arguments (like `db_key="Smaug"`) is a way to
name the input arguments to a function or method. They make
things easier to read but also allows for conveniently setting
defaults for values not given explicitly.
```
> py
> from typeclasses.monsters import Dragon
> smaug = Dragon(db_key="Smaug", db_location=here)
> smaug.save()
> smaug.move_around()
Smaug is moving!
The world trembles.
Smaug works the same as before, but we created him differently: first we used
`Dragon(db_key="Smaug", db_location=here)` to create the object, and then we used `smaug.save()` afterwards.
> quit()
Python Console is closing.
> look
You should now see that Smaug _is in the room with you_. Woah!
> reload
> look
_He's still there_... What we just did was to create a new entry in the database for Smaug. We gave the object
its name (key) and set its location to our current location (remember that `here` is just something available
in the `py` command, you can't use it elsewhere).
To make use of Smaug in code we must first find him in the database. For an object in the current
location we can easily do this in `py` by using `me.search()`:
> py smaug = me.search("Smaug") ; smaug.firebreath()
Smaug breathes fire!
### Creating using create_object
Creating Smaug like we did above is nice because it's similar to how we created non-database
bound Python instances before. But you need to use `db_key` instead of `key` and you also have to
remember to call `.save()` afterwards. Evennia has a helper function that is more common to use,
called `create_object`:
> py fluffy = evennia.create_object('typeclases.monster.Monster', key="Fluffy", location=here)
> look
Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the
python-path to the code you want and then set the key and location. Evennia sets things up and saves for you.
If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper:
> fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around()
Fluffy is moving!
> The `[0]` is because `search_object` always returns a _list_ of zero, one or more found objects. The `[0]`
means that we want the first element of this list (counting in Python always starts from 0). If there were
multiple Fluffies we could get the second one with `[1]`.
### Creating using create-command
Finally, you can also create a new Dragon using the familiar builder-commands we explored a few lessons ago:
> create/drop Cuddly:typeclasses.monsters.Monster
Cuddly is now in the room. After learning about how objects are created you'll realize that all this command really
does is to parse your input, figure out that `/drop` means to "give the object the same location as the caller",
and then do a call akin to
evennia.create_object("typeclasses.monsters.Monster", key="Cuddly", location=here)
That's pretty much all there is to the mighty `create` command! The rest is just parsing for the command
to understand just what the user wants to create.
## Typeclasses
The `Object` (and `DefafultObject` class we inherited from above is what we refer to as a _Typeclass_. This
is an Evennia thing. The instance of a typeclass saves itself to the database when it is created, and after
that you can just search for it to get it back. We use the term _typeclass_ or _typeclassed_ to differentiate
these types of classes and objects from the normal Python classes, whose instances go away on a reload.
The number of typeclasses in Evennia are so few they can be learned by heart:
- `evennia.DefaultObject`: This is the parent of all in-game entities - everything with a location. Evennia makes
a few very useful child classes of this class:
- `evennia.DefaultCharacter`: The default entity represening a player avatar in-game.
- `evennia.DefaultRoom`: A location in the game world.
- `evennia.DefaultExit`: A link between locations.
- `evennia.DefaultAccount`: The OOC representation of a player, holds password and account info.
- `evennia.DefaultChannel`: In-game channels. These could be used for all sorts of in-game communication.
- `evennia.DefaultScript`: Out-of-game objects, with no presence in the game world. Anything you want to create that
needs to be persistent can be stored with these entities, such as combat state, economic systems or what have you.
If you take a look in `mygame/typeclasses/` you'll find modules for each of these. Each contains an empty child
class ready that already inherits from the right parent, ready for you to modify or build from:
- `mygame/typeclasses/objects.py` has `class Object(DefaultObject)`, a class directly inheriting the basic in-game entity, this
works as a base for any object.
- `mygame/typeclasses/characters.py` has `class Character(DefaultCharacter)`
- `mygame/typeclasses/rooms.py` has `class Room(DefaultRoom)`
- `mygame/typeclasses/exits.py` has `class Exit(DefaultExit)`
- `mygame/typeclasses/accounts.py` has `class Account(DefaultAccount)`
- `mygame/typeclasses/channels.py` has `class Channel(DefaultChannel)`
- `mygame/typeclasses/scripts.py` has `class Script(DefaultScript)`
> Notice that the classes in `mygame/typeclasses/` are _not inheriting from each other_. For example,
> `Character` is inheriting from `evennia.DefaultCharacter` and not from `typeclasses.objects.Object`.
> So if you change `Object` you will not cause any change in the `Character` class. If you want that you
> can easily just change the child classes to inherit in that way instead; Evennia doesn't care.
As seen with our `Dragon` example, you don't _have_ to modify these modules directly. You can just make your
own modules and import the base class.
### Examining and defaults
When you do
> create/drop giantess:typeclasses.monsters.Monster
You create a new Monster: giantess.
or
> py evennia.create_object("typeclasses.monsters.Monster", key="Giantess", location=here)
You are specifying exactly which typeclass you want to use to build the Giantess. Let's examine the result:
> examine giantess
-------------------------------------------------------------------------------
Name/key: Giantess (#14)
Typeclass: Monster (typeclasses.monsters.Monster)
Location: Limbo (#2)
Home: Limbo (#2)
Permissions: <None>
Locks: call:true(); control:id(1) or perm(Admin); delete:id(1) or perm(Admin);
drop:holds(); edit:perm(Admin); examine:perm(Builder); get:all();
puppet:pperm(Developer); tell:perm(Admin); view:all()
Persistent attributes:
desc = You see nothing special.
-------------------------------------------------------------------------------
We used the `examine` command briefly in the [lesson about building in-game](./Building-Quickstart.md). Now these lines
may be more useful to us:
- **Name/key** - The name of this thing. The value `(#14)` is probably different for you. This is the
unique 'primary key' or _dbref_ for this entity in the database.
- **Typeclass**: This show the typeclass we specified, and the path to it.
- **Location**: We are in Limbo. If you moved elsewhere you'll see that instead. Also the `#dbref` is shown.
- **Permissions**: _Permissions_ are like the inverse to _Locks_ - they are like keys to unlock access to other things.
The giantess have no such keys (maybe fortunately).
- **Locks**: Locks are the inverse of _Permissions_ - specify what criterion _other_ objects must fulfill in order to
access the `giantess` object. This uses a very flexible mini-language. For examine, the line `examine:perm(Builders)`
is read as "Only those with permission _Builder_ or higher can _examine_ this object". Since we are the superuser
we pass (even bypass) such locks with ease.
- **Persistent attributes**: This allows for storing arbitrary, persistent data on the typeclassed entity. We'll get
to those in the next section.
Note how the **Typeclass** line describes exactly where to find the code of this object? This is very useful for
understanding how any object in Evennia works.
What happens if we _don't_ specify the typeclass though?
> create/drop box
You create a new Object: box.
or
> py create.create_object(None, key="box", location=here)
Now check it out:
> examine box
You will find that the **Typeclass** line now reads
Typeclass: Object (typeclasses.objects.Object)
So when you didn't specify a typeclass, Evennia used a default, more specifically the (so far) empty `Object` class in
`mygame/typeclasses/objects.py`. This is usually what you want, especially since you can tweak that class as much
as you like.
But the reason Evennia knows to fall back to this class is not hard-coded - it's a setting. The default is
in [evennia/settings_default.py](https://github.com/evennia/evennia/blob/master/evennia/settings_default.py#L465),
with the name `BASE_OBJECT_TYPECLASS`, which is set to `typeclasses.objects.Object`.
```{sidebar} Changing things
While it's tempting to change folders around to your liking, this can
make it harder to follow tutorials and may confuse if
you are asking others for help. So don't overdo it unless you really
know what you are doing.
```
So if you wanted the creation commands and methods to default to some other class you could
add your own `BASE_OBJECT_TYPECLASS` line to `mygame/server/conf/settings.py`. The same is true for all the other
typeclasseses, like characters, rooms and accounts. This way you can change the
layout of your game dir considerably if you wanted. You just need to tell Evennia where everything is.
## Modifying ourselves
Let's try to modify ourselves a little. Open up `mygame/typeclasses/characters.py`.
```python
"""
(module docstring)
"""
from evennia import DefaultCharacter
class Character(DefaultCharacter):
"""
(class docstring)
"""
pass
```
This looks quite familiar now - an empty class inheriting from the Evennia base typeclass. As you would expect,
this is also the default typeclass used for creating Characters if you don't specify it. You can verify it:
> examine me
------------------------------------------------------------------------------
Name/key: YourName (#1)
Session id(s): #1
Account: YourName
Account Perms: <Superuser> (quelled)
Typeclass: Character (typeclasses.characters.Character)
Location: Limbo (#2)
Home: Limbo (#2)
Permissions: developer, player
Locks: boot:false(); call:false(); control:perm(Developer); delete:false();
drop:holds(); edit:false(); examine:perm(Developer); get:false();
msg:all(); puppet:false(); tell:perm(Admin); view:all()
Stored Cmdset(s):
commands.default_cmdsets.CharacterCmdSet [DefaultCharacter] (Union, prio 0)
Merged Cmdset(s):
...
Commands available to YourName (result of Merged CmdSets):
...
Persistent attributes:
desc = This is User #1.
prelogout_location = Limbo
Non-Persistent attributes:
last_cmd = None
------------------------------------------------------------------------------
You got a lot longer output this time. You have a lot more going on than a simple Object. Here are some new fields of note:
- **Session id(s)**: This identifies the _Session_ (that is, the individual connection to a player's game client).
- **Account** shows, well the `Account` object associated with this Character and Session.
- **Stored/Merged Cmdsets** and **Commands available** is related to which _Commands_ are stored on you. We will
get to them in the [next lesson](./Adding-Commands.md). For now it's enough to know these consitute all the
commands available to you at a given moment.
- **Non-Persistent attributes** are Attributes that are only stored temporarily and will go away on next reload.
Look at the **Typeclass** field and you'll find that it points to `typeclasses.character.Character` as expected.
So if we modify this class we'll also modify ourselves.
### A method on ourselves
Let's try something simple first. Back in `mygame/typeclasses/characters.py`:
```python
class Character(DefaultCharacter):
"""
(class docstring)
"""
str = 10
dex = 12
int = 15
def get_stats(self):
"""
Get the main stats of this character
"""
return self.str, self.dex, self.int
```
> reload
> py self.get_stats()
(10, 12, 15)
```{sidebar} Tuples and lists
- A `list` is written `[a, b, c, d, ...]`. It can be modified after creation.
- A `tuple` is written `(a, b, c, ...)`. It cannot be modified once created.
```
We made a new method, gave it a docstring and had it `return` the RP-esque values we set. It comes back as a
_tuple_ `(10, 12, 15)`. To get a specific value you could specify the _index_ of the value you want,
starting from zero:
> py stats = self.get_stats() ; print(f"Strength is {stats[0]}.")
Strength is 10.
### Attributes
So what happens when we increase our strength? This would be one way:
> py self.str = self.str + 1
> py self.str
11
Here we set the strength equal to its previous value + 1. A shorter way to write this is to use Python's `+=`
operator:
> py self.str += 1
> py self.str
12
> py self.get_stats()
(12, 12, 15)
This looks correct! Try to change the values for dex and int too; it works fine. However:
> reload
> py self.get_stats()
(10, 12, 15)
After a reload all our changes were forgotten. When we change properties like this, it only changes in memory,
not in the database (nor do we modify the python module's code). So when we reloaded, the 'fresh' `Character`
class was loaded, and it still has the original stats we wrote to it.
In principle we could change the python code. But we don't want to do that manually every time. And more importantly
since we have the stats hardcoded in the class, _every_ character instance in the game will have exactly the
same `str`, `dex` and `int` now! This is clearly not what we want.
Evennia offers a special, persistent type of property for this, called an `Attribute`. Rework your
`mygame/typeclasses/characters.py` like this:
```python
class Character(DefaultCharacter):
"""
(class docstring)
"""
def get_stats(self):
"""
Get the main stats of this character
"""
return self.db.str, self.db.dex, self.db.int
```
```{sidebar} Spaces in Attribute name?
What if you want spaces in your Attribute name? Or you want to assign the
name of the Attribute on-the fly? Then you can use `.attributes.add(name, value)` instead,
for example `self.attributes.add("str", 10)`.
```
We removed the hard-coded stats and added added `.db` for every stat. The `.db` handler makes the stat
into an an Evennia `Attribute`.
> reload
> py self.get_stats()
(None, None, None)
Since we removed the hard-coded values, Evennia don't know what they should be (yet). So all we get back
is `None`, which is a Python reserved word to represent nothing, a no-value. This is different from a normal python
property:
> py self.str
AttributeError: 'Character' object has no attribute 'str'
> py self.db.str
(nothing will be displayed, because it's None)
Trying to get an unknown normal Python property will give an error. Getting an unknown Evennia `Attribute` will
never give an error, but only result in `None` being returned. This is often very practical.
> py self.db.str, self.db.dex, self.db.int = 10, 12, 15
> py self.get_stats()
(10, 12, 15)
> reload
> py self.get_stats()
(10, 12, 15)
Now we set the Attributes to the right values. We can see that things work the same as before, also after a
server reload. Let's modify the strength:
> py self.db.str += 2
> py self.get_stats()
(12, 12, 15)
> reload
> py self.get_stats()
(12, 12, 15)
Our change now survives a reload since Evennia automatically saves the Attribute to the database for us.
### Setting things on new Characters
Things a looking better, but one thing remains strange - the stats start out with a value `None` and we
have to manually set them to something reasonable. In a later lesson we will investigate character-creation
in more detail. For now, let's give every new character some random stats to start with.
We want those stats to be set only once, when the object is first created. For the Character, this method
is called `at_object_creation`.
```{sidebar} __init__ vs at_object_creation
For the `Monster` class we used `__init__` to set up the class. We can't use this
for a typeclass because it will be called more than once, at the very least after
every reload and maybe more depending on caching. Even if you are familiar with Python,
avoid touching `__init__` for typeclasses, the results will not be what you expect.
```
```python
# up by the other imports
import random
class Character(DefaultCharacter):
"""
(class docstring)
"""
def at_object_creation(self):
self.db.str = random.randint(3, 18)
self.db.dex = random.randint(3, 18)
self.db.int = random.randint(3, 18)
def get_stats(self):
"""
Get the main stats of this character
"""
return self.db.str, self.db.dex, self.db.int
```
We imported a new module, `random`. This is part of Python's standard library. We used `random.randint` to
set a random value from 3 to 18 to each stat. Simple, but for some classical RPGs this is all you need!
> reload
> py self.get_stats()
(12, 12, 15)
Hm, this is the same values we set before. They are not random. The reason for this is of course that, as said,
`at_object_creation` only runs _once_, the very first time a character is created. Our character object was already
created long before, so it will not be called again.
It's simple enough to run it manually though:
> self.at_object_creation()
> py self.get_stats()
(5, 4, 8)
Lady luck didn't smile on us for this example; maybe you'll fare better. Evennia has a helper command
`update` that re-runs the creation hook and also cleans up any other Attributes not re-created by `at_object_creation`:
> update self
> py self.get_stats()
(8, 16, 14)
### Updating all Characters in a loop
Needless to say, for your game you are wise to have a feel for what you want to go into the `at_object_creation` hook
before you create a lot of objects (characters in this case). But should it come to that you don't want to have to
go around and re-run the method on everyone manually. For the Python beginner, doing this will also give a chance to
try out Python _loops_. We try them out in multi-line Python mode:
> py
> for a in [1, 2, "foo"]: > print(a)
1
2
foo
A python _for-loop_ allows us to loop over something. Above, we made a _list_ of two numbers and a string. In
every iteration of the loop, the variable `a` becomes one element in turn, and we print that.
For our list, we want to loop over all Characters, and want to call `.at_object_creation` on each. This is how
this is done (still in python multi-line mode):
> from typeclasses.characters import Character
> for char in Character.objects.all()
> char.at_object_creation()
```{sidebar} Database queries
`Character.objects.all()` is an example of a database query expressed in Python. This will be converted
into a database query under the hood. This syntax is part of
`Django's query language <https://docs.djangoproject.com/en/3.0/topics/db/queries/>`_. You don't need to
know Django to use Evennia, but if you ever need more specific database queries, this is always available
when you need it.
```
We import the `Character` class and then we use `.objects.all()` to get all `Character` instances. Simplified,
`.objects` is a resource from which one can _query_ for all `Characters`. Using `.all()` gets us a listing
of all of them that we then immediately loop over. Boom, we just updated all Characters, including ourselves:
> quit()
Closing the Python console.
> self.get_stats()
(3, 18, 10)
## Extra Credits
This principle is the same for other typeclasses. So using the tools explored in this lesson, try to expand
the default room with an `is_dark` flag. It can be either `True` or `False`.
Have all new rooms start with `is_dark = False` and make it so that once you change it, it survives a reload.
Oh, and if you created any other rooms before, make sure they get the new flag too!
## Conclusions
In this lesson we created database-persistent dragons by having their classes inherit from one `Object`, one
of Evennia's _typeclasses_. We explored where Evennia looks for typeclasses if we don't specify the path
explicitly. We then modified ourselves - via the `Character` class - to give us some simple RPG stats. This
led to the need to use Evennia's _Attributes_, settable via `.db` and to use a for-loop to update ourselves.
Typeclasses are a fundamental part of Evennia and we will see a lot of more uses of them in the course of
this tutorial. But that's enough of them for now. It's time to take some action. Let's learn about _Commands_.

View file

@ -0,0 +1,497 @@
# Parsing Command input
In this lesson we learn some basics about parsing the input of Commands. We will
also learn how to add, modify and extend Evennia's default commands.
## More advanced parsing
In the last lesson we made a `hit` Command and hit a dragon with it. You should have the code
from that still around.
Let's expand our simple `hit` command to accept a little more complex input:
hit <target> [[with] <weapon>]
That is, we want to support all of these forms
hit target
hit target weapon
hit target with weapon
If you don't specify a weapon you'll use your fists. It's also nice to be able to skip "with" if
you are in a hurry. Time to modify `mygame/commands/mycommands.py` again. Let us break out the parsing
a little, in a new method `parse`:
```python
#...
class CmdHit(Command):
"""
Hit a target.
Usage:
hit <target>
"""
key = "hit"
def parse(self):
self.args = self.args.strip()
target, *weapon = self.args.split(" with ", 1)
if not weapon:
target, *weapon = target.split(" ", 1)
self.target = target.strip()
if weapon:
self.weapon = weapon.strip()
else:
self.weapon = ""
def func(self):
if not self.args:
self.caller.msg("Who do you want to hit?")
return
# get the target for the hit
target = self.caller.search(self.target)
if not target:
return
# get and handle the weapon
weapon = None
if self.weapon:
weapon = self.caller.search(self.weapon)
if weapon:
weaponstr = f"{weapon.key}"
else:
weaponstr = "bare fists"
self.caller.msg(f"You hit {target.key} with {weaponstr}!")
target.msg(f"You got hit by {self.caller.key} with {weaponstr}!")
# ...
```
The `parse` method is called before `func` and has access to all the same on-command variables as in `func`. Using
`parse` not only makes things a little easier to read, it also means you can easily let other Commands _inherit_
your parsing - if you wanted some other Command to also understand input on the form `<arg> with <arg>` you'd inherit
from this class and just implement the `func` needed for that command without implementing `parse` anew.
```{sidebar} Tuples and Lists
- A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created.
- A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created.
```
- **Line 14** - We do the stripping of `self.args` once and for all here. We also store the stripped version back
into `self.args`, overwriting it. So there is no way to get back the non-stripped version from here on, which is fine
for this command.
- **Line 15** - This makes use of the `.split` method of strings. `.split` will, well, split the string by some criterion.
`.split(" with ", 1)` means "split the string once, around the substring `" with "` if it exists". The result
of this split is a _list_. Just how that list looks depends on the string we are trying to split:
1. If we entered just `hit smaug`, we'd be splitting just `"smaug"` which would give the result `["smaug"]`.
2. `hit smaug sword` gives `["smaug sword"]`
3. `hit smaug with sword` gives `["smaug", "sword"]`
So we get a list of 1 or 2 elements. We assign it to two variables like this, `target, *weapon = `. That
asterisk in `*weapon` is a nifty trick - it will automatically become a list of _0 or more_ values. It sorts of
"soaks" up everything left over.
1. `target` becomes `"smaug"` and `weapon` becomes `[]`
2. `target` becomes `"smaug sword"` and `weapon` becomes `[]`
3. `target` becomes `"smaug"` and `weapon` becomes `sword`
- **Lines 16-17** - In this `if` condition we check if `weapon` is falsy (that is, the empty list). This can happen
under two conditions (from the example above):
1. `target` is simply `smaug`
2. `target` is `smaug sword`
To separate these cases we split `target` once again, this time by empty space `" "`. Again we store the
result back with `target, *weapon =`. The result will be one of the following:
1. `target` remains `smaug` and `weapon` remains `[]`
2. `target` becomes `smaug` and `weapon` becomes `sword`
- **Lines 18-22** - We now store `target` and `weapon` into `self.target` and `self.weapon`. We must do this in order
for these local variables to made available in `func` later. Note how we need to check so `weapon` is not falsy
before running `strip()` on it. This is because we know that if it's falsy, it's an empty list `[]` and lists
don't have the `.strip()` method on them (so if we tried to use it, we'd get an error).
Now onto the `func` method. The main difference is we now have `self.target` and `self.weapon` available for
convenient use.
- **Lines 29 and 35** - We make use of the previously parsed search terms for the target and weapon to find the
respective resource.
- **Lines 34-39** - Since the weapon is optional, we need to supply a default (use our fists!) if it's not set. We
use this to create a `weaponstr` that is different depending on if we have a weapon or not.
- **Lines 41-42** - We merge the `weaponstr` with our attack text.
Let's try it out!
> reload
> hit smaug with sword
Could not find 'sword'.
You hit smaug with bare fists!
Oops, our `self.caller.search(self.weapon)` is telling us that it found no sword. Since we are not `return`ing
in this situation (like we do if failing to find `target`) we still continue fighting with our bare hands.
This won't do. Let's make ourselves a sword.
> create sword
Since we didn't specify `/drop`, the sword will end up in our inventory and can seen with the `i` or
`inventory` command. The `.search` helper will still find it there. There is no need to reload to see this
change (no code changed, only stuff in the database).
> hit smaug with sword
You hit smaug with sword!
## Adding a Command to an object
The commands of a cmdset attached to an object with `obj.cmdset.add()` will by default be made available to that object
but _also to those in the same location as that object_. If you did the [Building introduction](./Building-Quickstart.md)
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Tutorial-World.md)
also has many examples of objects with commands on them.
To show how this could work, let's put our 'hit' Command on our simple `sword` object from the previous section.
> self.search("sword").cmdset.add("commands.mycommands.MyCmdSet", persistent=True)
We find the sword (it's still in our inventory so `self.search` should be able to find it), then
add `MyCmdSet` to it. This actually adds both `hit` and `echo` to the sword, which is fine.
Let's try to swing it!
> hit
More than one match for 'hit' (please narrow target):
hit-1 (sword #11)
hit-2
```{sidebar} Multi-matches
Some game engines will just pick the first hit when finding more than one.
Evennia will always give you a choice. The reason for this is that Evennia
cannot know if `hit` and `hit` are different or the same - maybe it behaves
differently depending on the object it sits on? Besides, imagine if you had
a red and a blue button both with the command `push` on it. Now you just write
`push`. Wouldn't you prefer to be asked `which` button you really wanted to push?
```
Woah, that didn't go as planned. Evennia actually found _two_ `hit` commands to didn't know which one to use
(_we_ know they are the same, but Evennia can't be sure of that). As we can see, `hit-1` is the one found on
the sword. The other one is from adding `MyCmdSet` to ourself earlier. It's easy enough to tell Evennia which
one you meant:
> hit-1
Who do you want to hit?
> hit-2
Who do you want to hit?
In this case we don't need both command-sets, so let's just keep the one on the sword:
> self.cmdset.remove("commands.mycommands.MyCmdSet")
> hit
Who do you want to hit?
Now try this:
> tunnel n = kitchen
> n
> drop sword
> s
> hit
Command 'hit' is not available. Maybe you meant ...
> n
> hit
Who do you want to hit?
The `hit` command is now only available if you hold or are in the same room as the sword.
### You need to hold the sword!
Let's get a little ahead of ourselves and make it so you have to _hold_ the sword for the `hit` command to
be available. This involves a _Lock_. We've cover locks in more detail later, just know that they are useful
for limiting the kind of things you can do with an object, including limiting just when you can call commands on
it.
```{sidebar} Locks
Evennia Locks are defined as a mini-language defined in `lockstrings`. The lockstring
is on a form `<situation>:<lockfuncs>`, where `situation` determines when this
lock applies and the `lockfuncs` (there can be more than one) are run to determine
if the lock-check passes or not depending on circumstance.
```
> py self.search("sword").locks.add("call:holds()")
We added a new lock to the sword. The _lockstring_ `"call:holds()"` means that you can only _call_ commands on
this object if you are _holding_ the object (that is, it's in your inventory).
For locks to work, you cannot be _superuser_, since the superuser passes all locks. You need to `quell` yourself
first:
```{sidebar} quell/unquell
Quelling allows you as a developer to take on the role of players with less
priveleges. This is useful for testing and debugging, in particular since a
superuser has a little `too` much power sometimes.
Use `unquell` to get back to your normal self.
```
> quell
If the sword lies on the ground, try
> hit
Command 'hit' is not available. ..
> get sword
> hit
> Who do you want to hit?
Finally, we get rid of ours sword so we have a clean slate with no more `hit` commands floating around.
We can do that in two ways:
delete sword
or
py self.search("sword").delete()
## Adding the Command to a default Cmdset
As we have seen we can use `obj.cmdset.add()` to add a new cmdset to objects, whether that object
is ourself (`self`) or other objects like the `sword`.
This is how all commands in Evennia work, including default commands like `look`, `dig`, `inventory` and so on.
All these commands are in just loaded on the default objects that Evennia provides out of the box.
- Characters (that is 'you' in the gameworld) has the `CharacterCmdSet`.
- Accounts (the thing that represents your out-of-character existence on the server) has the `AccountCmdSet`
- Sessions (representing one single client connection) has the `SessionCmdSet`
- Before you log in (at the connection screen) you'll have access to the `UnloggedinCmdSet`.
The thing must commonly modified is the `CharacterCmdSet`.
The default cmdset are defined in `mygame/commands/default_cmdsets.py`. Open that file now:
```python
"""
(module docstring)
"""
from evennia import default_cmds
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
class AccountCmdSet(default_cmds.AccountCmdSet):
key = "DefaultAccount"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
key = "DefaultUnloggedin"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
class SessionCmdSet(default_cmds.SessionCmdSet):
key = "DefaultSession"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
```
```{sidebar} super()
The `super()` function refers to the parent of the current class and is commonly
used to call same-named methods on the parent.
```
`evennia.default_cmds` is a container that holds all of Evennia's default commands and cmdsets. In this module
we can see that this was imported and then a new child class was made for each cmdset. Each class looks familiar
(except the `key`, that's mainly used to easily identify the cmdset in listings). In each `at_cmdset_creation` all
we do is call `super().at_cmdset_creation` which means that we call `at_cmdset_creation() on the _parent_ CmdSet.
This is what adds all the default commands to each CmdSet.
To add even more Commands to a default cmdset, we can just add them below the `super()` line. Usefully, if we were to
add a Command with the same `.key` as a default command, it would completely replace that original. So if you were
to add a command with a key `look`, the original `look` command would be replaced by your own version.
For now, let's add our own `hit` and `echo` commands to the `CharacterCmdSet`:
```python
# ...
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
self.add(mycommands.CmdEcho)
self.add(mycommands.CmdHit)
```
> reload
> hit
Who do you want to hit?
Your new commands are now available for all player characters in the game. There is another way to add a bunch
of commands at once, and that is to add a _CmdSet_ to the other cmdset. All commands in that cmdset will then be added:
```python
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
self.add(mycommands.MyCmdSet)
```
Which way you use depends on how much control you want, but if you already have a CmdSet,
this is practical. A Command can be a part of any number of different CmdSets.
### Removing Commands
To remove your custom commands again, you of course just delete the change you did to
`mygame/commands/default_cmdsets.py`. But what if you want to remove a default command?
We already know that we use `cmdset.remove()` to remove a cmdset. It turns out you can
do the same in `at_cmdset_creation`. For example, let's remove the default `get` Command
from Evennia. We happen to know this can be found as `default_cmds.CmdGet`.
```python
# ...
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
self.add(mycommands.MyCmdSet)
self.remove(default_cmds.CmdGet)
# ...
```
> reload
> get
Command 'get' is not available ...
## Replace a default command
At this point you already have all the pieces for how to do this! We just need to add a new
command with the same `key` in the `CharacterCmdSet` to replace the default one.
Let's combine this with what we know about classes and
how to _override_ a parent class. Open `mygame/commands/mycommands.py` and lets override
that `CmdGet` command.
```python
# up top, by the other imports
from evennia import default_cmds
# somewhere below
class MyCmdGet(default_cmds.CmdGet):
def func(self):
super().func()
self.caller.msg(str(self.caller.location.contents))
```
- **Line2**: We import `default_cmds` so we can get the parent class.
We made a new class and we make it _inherit_ `default_cmds.CmdGet`. We don't
need to set `.key` or `.parse`, that's already handled by the parent.
In `func` we call `super().func()` to let the parent do its normal thing,
- **Line 7**: By adding our own `func` we replace the one in the parent.
- **Line 8**: For this simple change we still want the command to work the
same as before, so we use `super()` to call `func` on the parent.
- **Line 9**: `.location` is the place an object is at. `.contents` contains, well, the
contents of an object. If you tried `py self.contents` you'd get a list that equals
your inventory. For a room, the contents is everything in it.
So `self.caller.location.contents` gets the contents of our current location. This is
a _list_. In order send this to us with `.msg` we turn the list into a string. Python
has a special function `str()` to do this.
We now just have to add this so it replaces the default `get` command. Open
`mygame/commands/default_cmdsets.py` again:
```python
# ...
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
self.add(mycommands.MyCmdSet)
self.add(mycommands.MyCmdGet)
# ...
```
```{sidebar} Another way
Instead of adding `MyCmdGet` explicitly in default_cmdset.py,
you could also add it to `mycommands.MyCmdSet` and let it be
added automatically for you.
```
> reload
> get
Get What?
[smaug, fluffy, YourName, ...]
We just made a new `get`-command that tells us everything we could pick up (well, we can't pick up ourselves, so
there's some room for improvement there).
## Summary
In this lesson we got into some more advanced string formatting - many of those tricks will help you a lot in
the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default
command on ourselves.

View file

@ -0,0 +1,653 @@
# Intro to using Python with Evennia
Time to dip our toe into some coding! Evennia is written and extended in [Python](https://python.org),
which is a mature and professional programming language that is very fast to work with.
That said, even though Python is widely considered easy to learn, we can only cover the most immediately
important aspects of Python in this series of starting tutorials. Hopefully we can get you started
but then you'll need to continue learning from there. See our [link section](../../../Links.md) for finding
more reference material and dedicated Python tutorials.
> While this will be quite basic if you are an experienced developer, you may want to at least
> stay around for the first few sections where we cover how to run Python from inside Evennia.
First, if you were quelling yourself to play the tutorial world, make sure to get your
superuser powers back:
unquell
## Evennia Hello world
The `py` Command (or `!`, which is an alias) allows you as a superuser to execute raw Python from in-
game. This is useful for quick testing. From the game's input line, enter the following:
> py print("Hello World!")
```{sidebar} Command input
The line with `>` indicates input to enter in-game, while the lines below are the
expected return from that input.
```
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. We are sending "Hello World" as an _argument_ to this function. The quotes `"..."`
mean that you are inputting a *string* (i.e. text). You could also have used single-quotes `'...'`,
Python accepts both. A third variant is triple-quotes (`"""..."""` or `'''...'''`, which work across multiple
lines and are common for larger text-blocks. The way we use the `py` command right now only supports
single-line input however.
## Making some text 'graphics'
When making a text-game you will, unsurprisingly, be working a lot with text. Even if you have the occational
button or even graphical element, the normal process is for the user to input commands as
text and get text back. As we saw above, a piece of text is called a _string_ in Python and is enclosed in
either single- or double-quotes.
Strings can be added together:
> py print("This is a " + "breaking change.")
This is a breaking change.
A string multiplied with a number will repeat that string as many times:
> py print("|" + "-" * 40 + "|")
|----------------------------------------|
or
> py print("A" + "a" * 5 + "rgh!")
Aaaaaargh!
### .format()
While combining different strings is useful, even more powerful is the ability to modify the contents
of the string in-place. There are several ways to do this in Python and we'll show two of them here. The first
is to use the `.format` _method_ of the string:
> py print("This is a {} idea!".format("good"))
This is a good idea!
```{eval-rst}
.. sidebar:: Functions and Methods
Function:
Something that performs and action when you `call` it with zero or more `arguments`. A function
is stand-alone in a python module, like `print()`
Method:
A function that sits "on" an object, like `<string>.format()`.
```
A method can be thought of as a resource "on" another object. The method knows on which object it
sits and can thus affect it in various ways. You access it with the period `.`. In this case, the
string has a resource `format(...)` that modifies it. More specifically, it replaced the `{}` marker
inside the string with the value passed to the format. You can do so many times:
> py print("This is a {} idea!".format("bad"))
This is a bad idea!
or
> py print("This is the {} and {} {} idea!".format("first", "second", "great"))
This is the first and second great idea!
> Note the double-parenthesis at the end - the first closes the `format(...` method and the outermost
closes the `print(...`. Not closing them will give you a scary `SyntaxError`. We will talk a
little more about errors in the next section, for now just fix until it prints as expected.
Here we passed three comma-separated strings as _arguments_ to the string's `format` method. These
replaced the `{}` markers in the same order as they were given.
The input does not have to be strings either:
> py print("STR: {}, DEX: {}, INT: {}".format(12, 14, 8))
STR: 12, DEX: 14, INT: 8
To separate two Python instructions on the same line, you use the semi-colon, `;`. Try this:
> py a = "awesome sauce" ; print("This is {}!".format(a))
This is awesome sauce!
```{warning} MUD clients and semi-colon
Some MUD clients use the semi-colon `;` to split client-inputs
into separate sends. If so, the above will give an error. Most clients allow you to
run in 'verbatim' mode or to remap to use some other separator than `;`. If you still have
trouble, use the Evennia web client.
```
What happened here was that we _assigned_ the string `"awesome sauce"` to a _variable_ we chose
to name `a`. In the next statement, Python remembered what `a` was and we passed that into `format()`
to get the output. If you replaced the value of `a` with something else in between, _that_ would be printed
instead.
Here's the stat-example again, moving the stats to variables (here we just set them, but in a real
game they may be changed over time, or modified by circumstance):
> py stren, dext, intel = 13, 14, 8 ; print("STR: {}, DEX: {}, INT: {}".format(stren, dext, intel))
STR: 13, DEX: 14, INT: 8
The point is that even if the values of the stats change, the print() statement would not change - it just keeps
pretty-printing whatever is given to it.
### f-strings
Using `.format()` is convenient (and there is a [lot more](https://www.w3schools.com/python/ref_string_format.asp)
you can do with it). But the _f-string_ can be even more convenient. An
f-string looks like a normal string ... except there is an `f` front of it, like this:
f"this is now an f-string."
An f-string on its own is just like any other string. But let's redo the example we did before, using an f-string:
> py a = "awesome sauce" ; print(f"This is {a}!")
This is awesome sauce!
We could just insert that `a` variable directly into the f-string using `{a}`. Fewer parentheses to
remember and arguable easier to read as well.
> py stren, dext, intel = 13, 14, 8 ; print(f"STR: {stren}, DEX: {dext}, INT: {intel}")
STR: 13, DEX: 14, INT: 8
We will be exploring more complex string concepts when we get to creating Commands and need to
parse and understand player input.
### Colored text
Python itself knows nothing about colored text, this is an Evennia thing. Evennia supports the
standard color schemes of traditional MUDs.
> py print("|rThis is red text!|n This is normal color.")
Adding that `|r` at the start will turn our output bright red. `|R` will make it dark red. `|n`
gives the normal text color. You can also use RGB (Red-Green-Blue) values from 0-5 (Xterm256 colors):
> py print("|043This is a blue-green color.|[530|003 Now dark blue text on orange background.")
> If you don't see the expected color, your client or terminal may not support Xterm256 (or
color at all). Use the Evennia webclient.
Use the commands `color ansi` or `color xterm` to see which colors are available. Experiment!
## Importing code from other modules
As we saw in the previous sections, we used `.format` to format strings and `me.msg` to access
the `msg` method on `me`. This use of the full-stop character is used to access all sorts of resources,
including that in other Python 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!")
```
```{sidebar} Python module
This is a text file with the `.py` file ending. A module
contains Python source code and from within Python one can
access its contents by importing it via its python-path.
```
Don't forget to _save_ the file. We just created our first Python _module_!
To use this in-game we have to *import* it. Try this:
> py import world.test
Hello World
If you make some error (we'll cover how to handle errors below), fix the error in the module and
run the `reload` command in-game 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".
Now try to run this a second time:
> 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 of how Python importing works - it stores all imported modules and will
avoid importing them more than once. So your `print` will only run the first time, when the module
is first imported.
Try this:
> reload
And then
> py import world.test
Hello World!
Now we see it again. The `reload` wiped the server's memory of what was imported, so it had to
import it anew. You'd have to do this every time you wanted the print to show though, which is
not very useful.
> We'll get back to more advanced ways to import code in later tutorial sections - this is an
> important topic. But for now, let's press on and resolve this particular problem.
### Our first own function
We want to be able to print our hello-world message at any time, not just once after a server
reload. Change your `mygame/world/test.py` file to look like this:
```python
def hello_world():
print("Hello World!")
```
As we are moving to 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`.
- 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, for your own sanity's sake,
set up your editor to always indent *4 spaces* (**not** a single tab-character) when you press the TAB key.
So about that function. Line 1:
- `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. We recommend you do the same.
- The colon (`:`) at the end of line 1 indicates that the header of the function is complete.
Line 2:
- 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
start at least at this indentation level.
Now let's try this out. First `reload` your game to have it pick up
our updated Python module, then import it.
> reload
> py import world.test
Nothing happened! That is because the function in our module won't do anything just by importing it (this
is what we wanted). It will only act when we *call* it. So we need to first import the module and then access the
function within:
> py import world.test ; world.test.hello_world()
Hello world!
There is our "Hello World"! As mentioned earlier, use use semi-colon to put multiple
Python-statements on one line. Note also the previous warning about mud-clients using the `;` to their
own ends.
So what happened there? First we imported `world.test` as usual. But this time we continued and
accessed the `hello_world` function _inside_ the newly imported module.
By adding `()` to the `hello_world` function we _call_ it, that is we run the body of the function and
print our text. We can now redo this as many times as we want without having to `reload` in between:
> py import world.test ; world.test.hello_world()
Hello world!
> py import world.test ; world.test.hello_world()
Hello world!
## Sending text to others
The `print` command is a standard Python structure. We can use that here in the `py` command since
we can se the output. 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 a shortcut to 'us', the one running the `py` command. It is not some special
Python thing, but something Evennia just makes available in the `py` command for convenience
(`self` is an alias).
The `me` is an example of an *Object instance*. Objects are fundamental in Python and Evennia.
The `me` object also contains a lot of useful resources for doing
things with that object. We access those resources with '`.`'.
One such resource is `msg`, which 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`.
For now, `print` and `me.msg` behaves the same, just remember that `print` is mainly used for
debugging and `.msg()` will be more useful for you in the future.
## Parsing Python errors
Let's try this new text-sending in the function we just created. Go back to
your `test.py` file and Replace the function with this instead:
```python
def hello_world():
me.msg("Hello World!")
```
Save your file and `reload` your server to tell Evennia to re-import new code,
then run it like before:
> py import world.test ; world.test.hello_world()
No go - this time you get an error!
```python
File "./world/test.py", line 2, in hello_world
me.msg("Hello World!")
NameError: name 'me' is not defined
```
```{sidebar} Errors in the logs
In regular use, tracebacks will often appear in the log rather than
in the game. Use `evennia --log` to view the log in the terminal. Make
sure to scroll back if you expect an error and don't see it. Use
`Ctrl-C` (or `Cmd-C` on Mac) to exit the log-view.
```
This is called a *traceback*. Python's errors are very friendly and will most of the time tell you
exactly what and where things go wrong. It's important that you learn to parse tracebacks so you
know how to fix your code.
A traceback is to be read from the _bottom up_:
- (line 3) An error of type `NameError` is the problem ...
- (line 3) ... more specifically it is due to the variable `me` not being defined.
- (line 2) This happened on the line `me.msg("Hello world!")` ...
- (line 1) ... which is on line `2` 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 the program 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 mentioned, it's just something Evennia came up with for convenience in the `py`
command). As far as the module is concerned `me` is an unfamiliar name, appearing out of nowhere.
Hence the `NameError`.
## Passing arguments to functions
We know that `me` exists at the point when we run the `py` command, because we can do `py me.msg("Hello World!")`
with no problem. So let's _pass_ that me along to the function so it knows what it should be.
Go back to your `test.py` and change it to this:
```python
def hello_world(who):
who.msg("Hello World!")
```
We now added an _argument_ to the function. We could have named it anything. Whatever `who` is,
we will call a method `.msg()` on it.
As usual, `reload` the server to make sure the new code is available.
> py import world.test ; world.test.hello_world(me)
Hello World!
Now it worked. We _passed_ `me` to our function. It will appear inside the function renamed as `who` and
now the function works and prints as expected. Note how the `hello_world` function doesn't care _what_ you
pass into it as long as it has a `.msg()` method on it. So you could reuse this function over and over for other
suitable targets.
> **Extra Credit:** As an exercise, try to pass something else into `hello_world`. Try for example
>to pass the number `5` or the string `"foo"`. You'll get errors telling you that they don't have
>the attribute `msg`. They don't care about `me` itself not being a string or a number. If you are
>familiar with other programming languages (especially C/Java) you may be tempted to start *validating*
>`who` to make sure it's of the right type before you send it. This is usually not recommended in Python.
>Python philosophy is to [handle](https://docs.python.org/2/tutorial/errors.html) the error if it happens
>rather than to add a lot of code to prevent it from happening. See [duck typing](https://en.wikipedia.org/wiki/Duck_typing)
>and the concept of _Leap before you Look_.
## Finding others to send to
Let's wrap up this first Python `py` crash-course by finding someone else to send to.
In Evennia's `contrib/` folder (`evennia/contrib/tutorial_examples/mirror.py`) is a handy little
object called the `TutorialMirror`. The mirror will echo whatever is being sent to it to
the room it is in.
On the game command-line, let's create a mirror:
> create/drop mirror:contrib.tutorial_examples.mirror.TutorialMirror
```{sidebar} Creating objects
The `create` command was first used to create boxes in the
`Building Stuff <Building-Quickstart>`_ tutorial. Note how it
uses a "python-path" to describe where to load the mirror's code from.
```
A mirror should appear in your location.
> look mirror
mirror shows your reflection:
This is User #1
What you are seeing is actually your own avatar in the game, the same thing that is available as `me` in the `py`
command.
What we are aiming for now is the equivalent of `mirror.msg("Mirror Mirror on the wall")`. But the first thing that
comes to mind will not work:
> py mirror.msg("Mirror, Mirror on the wall ...")
NameError: name 'mirror' is not defined.
This is not surprising: Python knows nothing about "mirrors" or locations or anything. The `me` we've been using
is, as mentioned, just a convenient thing the Evennia devs makes available to the `py` command. They couldn't possibly
predict that you wanted to talk to mirrors.
Instead we will need to _search_ for that `mirror` object before we can send to it.
Make sure you are in the same location as the mirror and try:
> py me.search("mirror")
mirror
`me.search("name")` will, by default, search and _return_ an object with the given name found in _the same location_
as the `me` object is. If it can't find anything you'll see an error.
```{sidebar} Function returns
Whereas a function like `print` only prints its arguments, it's very common
for functions/methods to `return` a result of some kind. Think of the function
as a machine - you put something in and out comes a result you can use. In the case
of `me.search`, it will perform a database search and spit out the object it finds.
```
> py me.search("dummy")
Could not find 'dummy'.
Wanting to find things in the same location is very common, but as we continue we'll
find that Evennia provides ample tools for tagging, searching and finding things from all over your game.
Now that we know how to find the 'mirror' object, we just need to use that instead of `me`!
> py mirror = self.search("mirror") ; mirror.msg("Mirror, Mirror on the wall ...")
mirror echoes back to you:
"Mirror, Mirror on the wall ..."
The mirror is useful for testing because its `.msg` method just echoes whatever is sent to it back to the room. More common
would be to talk to a player character, in which case the text you sent would have appeared in their game client.
## Multi-line py
So far we have use `py` in single-line mode, using `;` to separate multiple inputs. This is very convenient
when you want to do some quick testing. But you can also start a full multi-line Python interactive interpreter
inside Evennia.
> py
Evennia Interactive Python mode
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
[GCC 8.2.0] on Linux
[py mode - quit() to exit]
(the details of the output will vary with your Python version and OS). You are now in python interpreter mode. It means
that _everything_ you insert from now on will become a line of Python (you can no longer look around or do other
commands).
> print("Hello World")
>>> print("Hello World")
Hello World
[py mode - quit() to exit]
Note that we didn't need to put `py` in front now. The system will also echo your input (that's the bit after
the `>>>`). For brevity in this tutorual we'll turn the echo off. First exit `py` and then start again with the
`/noecho` flag.
> quit()
Closing the Python console.
> py/noecho
Evennia Interactive Python mode (no echoing of prompts)
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
[GCC 8.2.0] on Linux
[py mode - quit() to exit]
```{sidebar} interactive py
- Start with `py`.
- Use `py/noecho` if you don't want your input to be echoed for every line.
- All your inputs will now be interpreted as Python code.
- Exit with `quit()`.
```
We can now enter multi-line Python code:
> a = "Test"
> print(f"This is a {a}."}
This is a Test.
Let's try to define a function:
> def hello_world(who, txt):
...
> who.msg(txt)
...
>
[py mode - quit() to exit]
Some important things above:
- Definining a function with `def` means we are starting a new code block. Python works so that you mark the content
of the block with indention. So the next line must be manually indented (4 spaces is a good standard) in order
for Python to know it's part of the function body.
- We expand the `hello_world` function with another argument `txt`. This allows us to send any text, not just
"Hello World" over and over.
- To tell `py` that no more lines will be added to the function body, we end with an empty input. When
the normal prompt on how to exit returns, we know we are done.
Now we have defined a new function. Let's try it out:
> hello_world(me, "Hello world to me!")
Hello world to me!
The `me` is still available to us, so we pass that as the `who` argument, along with a little longer
string. Let's combine this with searching for the mirror.
> mirror = me.search("mirror")
> hello_world(mirror, "Mirror, Mirror on the wall ...")
mirror echoes back to you:
"Mirror, Mirror on the wall ..."
Exit the `py` mode with
> quit()
Closing the Python console.
## Other ways to test Python code
The `py` command is very powerful for experimenting with Python in-game. It's great for quick testing.
But you are still limited to working over telnet or the webclient, interfaces that doesn't know anything
about Python per-se.
Outside the game, go to the terminal where you ran Evennia (or any terminal where the `evennia` command
is available).
- `cd` to your game dir.
- `evennia shell`
A Python shell opens. This works like `py` did inside the game, with the exception that you don't have
`me` available out of the box. If you want `me`, you need to first find yourself:
> import evennia
> me = evennia.search_object("YourChar")[0]
Here we make use of one of evennia's search functions, available by importing `evennia` directly.
We will cover more advanced searching later, but suffice to say, you put your own character name instead of
"YourChar" above.
> The `[0]` at the end is because `.search_object` returns a list of objects and we want to
get at the first of them (counting starts from 0).
Use `Ctrl-D` (`Cmd-D` on Mac) or `quit()` to exit the Python console.
## ipython
The default Python shell is quite limited and ugly. It's *highly* recommended to install `ipython` instead. This
is a much nicer, third-party Python interpreter with colors and many usability improvements.
pip install ipython
If `ipython` is installed, `evennia shell` will use it automatically.
evennia shell
...
IPython 7.4.0 -- An enhanced Interactive Python. Type '?' for help
In [1]: You now have Tab-completion:
> import evennia
> evennia.<TAB>
That is, enter `evennia.` and then press the TAB key - you will be given a list of all the resources
available on the `evennia` object. This is great for exploring what Evennia has to offer. For example,
use your arrow keys to scroll to `search_object()` to fill it in.
> evennia.search_object?
Adding a `?` and pressing return will give you the full documentation for `.search_object`. Use `??` if you
want to see the entire source code.
As for the normal python interpreter, use `Ctrl-D`/`Cmd-D` or `quit()` to exit ipython.
```{important} Persistent code
Common for both `py` and `python`/`ipython` is that the code you write is not persistent - it will
be gone after you shut down the interpreter (but ipython will remember your input history). For making long-lasting
Python code, we need to save it in a Python module, like we did for `world/test.py`.
```
## Conclusions
This covers quite a lot of basic Python usage. We printed and formatted strings, defined our own
first function, fixed an error and even searched and talked to a mirror! Being able to access
python inside and outside of the game is an important skill for testing and debugging, but in
practice you will be writing most your code in Python modules.
To that end we also created a first new Python module in the `mygame/` game dir, then imported and used it.
Now let's look at the rest of the stuff you've got going on inside that `mygame/` folder ...

View file

@ -0,0 +1,415 @@
# Introduction to Python classes and objects
We have now learned how to run some simple Python code from inside (and outside) your game server.
We have also taken a look at what our game dir looks and what is where. Now we'll start to use it.
## Importing things
No one writes something as big as an online game in one single huge file. Instead one breaks up the
code into separate files (modules). Each module is dedicated to different purposes. Not only does
it make things cleaner, organized and easier to understand. It also makes it easier to re-use code -
you just import the resources you need and know you only get just what you requested. This makes
it much easier to find errors and to know what code is good and which has issues.
> Evennia itself uses your code in the same way - you just tell it where a particular type of code is,
and it will import and use it (often instead of its defaults).
We have already successfully imported things, for example:
> py import world.test ; world.test.hello_world(me)
Hello World!
In this example, on your hard drive, the files looks like this:
```
mygame/
world/
test.py <- inside this file is a function hello_world
```
If you followed earlier tutorial lessons, the `mygame/world/test.py` file should look like this (if
not, make it so):
```python
def hello_world(who):
who.msg("Hello World!")
```
```{eval-rst}
.. sidebar:: Remember:
- Indentation matters in Python
- So does capitalization
- Use 4 `spaces` to indent, not tabs
- Empty lines are fine
- Anything on a line after a `#` is a `comment`, ignored by Python
```
The _python_path_ describes the relation between Python resources, both between and inside
Python _modules_ (that is, files ending with .py). A python-path separates each part of the
path `.` and always skips the `.py` file endings. Also, Evennia already knows to start looking
for python resources inside `mygame/` so this should never be specified. Hence
import world.test
The `import` Python instruction loads `world.test` so you have it available. You can now go "into"
this module to get to the function you want:
world.test.hello_world(me)
Using `import` like this means that you have to specify the full `world.test` every time you want
to get to your function. Here's a more powerful form of import:
from world.test import hello_world
The `from ... import ...` is very, very common as long as you want to get something with a longer
python path. It imports `hello_world` directly, so you can use it right away!
> py from world.test import hello_world ; hello_world(me)
Hello World!
Let's say your `test.py` module had a bunch of interesting functions. You could then import them
all one by one:
from world.test import hello_world, my_func, awesome_func
If there were _a lot_ of functions, you could instead just import `test` and get the function
from there when you need (without having to give the full `world.test` every time):
> from world import test ; test.hello_world(me
Hello World!
You can also _rename_ stuff you import. Say for example that the module you import to already
has a function `hello_world` but we also want to use the one from `world/test.py`:
from world.test import hello_world as test_hello_world
The form `from ... import ... as ...` renames the import.
> from world.test import hello_world as hw ; hw(me)
Hello World!
> Avoid renaming unless it's to avoid a name-collistion like above - you want to make things as
> easy to read as possible, and renaming adds another layer of potential confusion.
In [the basic intro to Python](./Python-basic-introduction.md) we learned how to open the in-game
multi-line interpreter.
> py
Evennia Interactive Python mode
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
[GCC 8.2.0] on Linux
[py mode - quit() to exit]
You now only need to import once to use the imported function over and over.
> from world.test import hello_world
> hello_world()
Hello World!
> hello_world()
Hello World!
> hello_world()
Hello World!
> quit()
Closing the Python console.
The same goes when writing code in a module - in most Python modules you will see a bunch of
imports at the top, resources that are then used by all code in that module.
## On classes and objects
Now that we know about imports, let look at a real Evennia module and try to understand it.
Open `mygame/typeclasses/objects.py` in your text editor of choice.
```python
"""
module docstring
"""
from evennia import DefaultObject
class Object(DefaultObject):
"""
class docstring
"""
pass
```
```{sidebar} Docstrings vs Comments
A docstring is not the same as a comment (created by `#`). A
docstring is not ignored by Python but is an integral part of the thing
it is documenting (the module and the class in this case).
```
The real file is much longer but we can ignore the multi-line strings (`""" ... """`). These serve
as documentation-strings, or _docstrings_ for the module (at the top) and the `class` below.
Below the module doc string we have the import. In this case we are importing a resource
from the core `evennia` library itself. We will dive into this later, for now we just treat this
as a black box.
Next we have a `class` named `Object`, which _inherits_ from `DefaultObject`. This class doesn't
actually do anything on its own, its only code (except the docstring) is `pass` which means,
well, to pass and don't do anything.
We will get back to this module in the [next lesson](./Learning-Typeclasses.md). First we need to do a
little detour to understand what a 'class', an 'object' or 'instance' is. These are fundamental
things to understand before you can use Evennia efficiently.
```{sidebar} OOP
Classes, objects, instances and inheritance are fundamental to Python. This and some
other concepts are often clumped together under the term Object-Oriented-Programming (OOP).
```
### Classes and instances
A 'class' can be seen as a 'template' for a 'type' of object. The class describes the basic functionality
of everyone of that class. For example, we could have a class `Monster` which has resources for moving itself
from room to room.
Open a new file `mygame/typeclasses/monsters.py`. Add the following simple class:
```python
class Monster:
key = "Monster"
def move_around(self):
print(f"{self.key} is moving!")
```
Above we have defined a `Monster` class with one variable `key` (that is, the name) and one
_method_ on it. A method is like a function except it sits "on" the class. It also always has
at least one argument (almost always written as `self` although you could in principle use
another name), which is a reference back to itself. So when we print `self.key` we are referring
back to the `key` on the class.
```{eval-rst}
.. sidebar:: Terms
- A `class` is a code template describing a 'type' of something
- An `object` is an `instance` of a `class`. Like using a mold to cast tin soldiers, one class can be `instantiated` into any number of object-instances.
```
A class is just a template. Before it can be used, we must create an _instance_ of the class. If
`Monster` is a class, then an instance is Fluffy, the individual red dragon. You instantiate
by _calling_ the class, much like you would a function:
fluffy = Monster()
Let's try it in-game (we use multi-line mode, it's easier)
> py
> from typeclasses.monsters import Monster
> fluffy = Monster()
> fluffy.move_around()
Monster is moving!
We created an _instance_ of `Monster`, which we stored in the variable `fluffy`. We then
called the `move_around` method on fluffy to get the printout.
> Note how we _didn't_ call the method as `fluffy.move_around(self)`. While the `self` has to be
> there when defining the method, we _never_ add it explicitly when we call the method (Python
> will add the correct `self` for us automatically behind the scenes).
Let's create the sibling of Fluffy, Cuddly:
> cuddly = Monster()
> cuddly.move_around()
Monster is moving!
We now have two dragons and they'll hang around until with call `quit()` to exit this Python
instance. We can have them move as many times as we want. But no matter how many dragons we
create, they will all show the same printout since `key` is always fixed as "Monster".
Let's make the class a little more flexible:
```python
class Monster:
def __init__(self, key):
self.key = key
def move_around(self):
print(f"{self.key} is moving!")
```
The `__init__` is a special method that Python recognizes. If given, this handles extra arguments
when you instantiate a new Monster. We have it add an argument `key` that we store on `self`.
Now, for Evennia to see this code change, we need to reload the server. You can either do it this
way:
> quit()
Python Console is closing.
> reload
Or you can use a separate terminal and restart from outside the game:
```{sidebar} On reloading
Reloading with the python mode gets a little annoying since you need to redo everything
after every reload. Just keep in mind that during regular development you will not be
working this way. The in-game python mode is practical for quick fixes and experiments like
this, but actual code is normally written externally, in python modules.
```
$ evennia reload (or restart)
Either way you'll need to go into `py` again:
> py
> from typeclasses.monsters import Monster
fluffy = Monster("Fluffy")
fluffy.move_around()
Fluffy is moving!
Now we passed `"Fluffy"` as an argument to the class. This went into `__init__` and set `self.key`, which we
later used to print with the right name! Again, note that we didn't include `self` when calling.
### What's so good about objects?
So far all we've seen a class do is to behave our first `hello_world` function but more complex. We
could just have made a function:
```python
def monster_move_around(key):
print(f"{key} is moving!")
```
The difference between the function and an instance of a class (the object), is that the
object retains _state_. Once you called the function it forgets everything about what you called
it with last time. The object, on the other hand, remembers changes:
> fluffy.key = "Cuddly"
> fluffy.move_around()
Cuddly is moving!
The `fluffy` object's `key` was changed to "Cuddly" for as long as it's around. This makes objects
extremely useful for representing and remembering collections of data - some of which can be other
objects in turn:
- A player character with all its stats
- A monster with HP
- A chest with a number of gold coins in it
- A room with other objects inside it
- The current policy positions of a political party
- A rule with methods for resolving challenges or roll dice
- A multi-dimenstional data-point for a complex economic simulation
- And so much more!
### Classes can have children
Classes can _inherit_ from each other. A "child" class will inherit everything from its "parent" class. But if
the child adds something with the same name as its parent, it will _override_ whatever it got from its parent.
Let's expand `mygame/typeclasses/monsters.py` with another class:
```python
class Monster:
"""
This is a base class for Monster.
"""
def __init__(self, key):
self.key = key
def move_around(self):
print(f"{self.key} is moving!")
class Dragon(Monster):
"""
This is a dragon-specific monster.
"""
def move_around(self):
print(f"{self.key} flies through the air high above!")
def firebreath(self):
"""
Let our dragon breathe fire.
"""
print(f"{self.key} breathes fire!")
```
We added some docstrings for clarity. It's always a good idea to add doc strings; you can do so also for methods,
as exemplified for the new `firebreath` method.
We created the new class `Dragon` but we also specified that `Monster` is the _parent_ of `Dragon` but adding
the parent in parenthesis. `class Classname(Parent)` is the way to do this.
```{sidebar} Multi-inheritance
It's possible to add more comma-separated parents to a class. You should usually avoid
this until you `really` know what you are doing. A single parent will be enough for almost
every case you'll need.
```
Let's try out our new class. First `reload` the server and the do
> py
> from typeclasses.monsters import Dragon
> smaug = Dragon("Smaug")
> smaug.move_around()
Smaug flies through the air high above!
> smaug.firebreath()
Smaug breathes fire!
Because we didn't implement `__init__` in `Dragon`, we got the one from `Monster` instead. But since we
implemented our own `move_around` in `Dragon`, it _overrides_ the one in `Monster`. And `firebreath` is only
available for `Dragon`s of course. Having that on `Monster` would not have made much sense, since not every monster
can breathe fire.
One can also force a class to use resources from the parent even if you are overriding some of it. This is done
with the `super()` method. Modify your `Dragon` class as follows:
```python
# ...
class Dragon(Monster):
def move_around(self):
super().move_around()
print("The world trembles.")
# ...
```
> Keep `Monster` and the `firebreath` method, `# ...` indicates the rest of the code is untouched.
>
The `super().move_around()` line means that we are calling `move_around()` on the parent of the class. So in this
case, we will call `Monster.move_around` first, before doing our own thing.
Now `reload` the server and then:
> py
> from typeclasses.monsters import Dragon
> smaug = Dragon("Smaug")
> smaug.move_around()
Smaug is moving!
The world trembles.
We can see that `Monster.move_around()` is calls first and prints "Smaug is moving!", followed by the extra bit
about the trembling world we added in the `Dragon` class.
Inheritance is very powerful because it allows you to organize and re-use code while only adding the special things
you want to change. Evennia uses this concept a lot.
## Summary
We have created our first dragons from classes. We have learned a little about how you _instantiate_ a class
into an _object_. We have seen some examples of _inheritance_ and we tested to _override_ a method in the parent
with one in the child class. We also used `super()` to good effect.
We have used pretty much raw Python so far. In the coming lessons we'll start to look at the extra bits that Evennia
provides. But first we need to learn just where to find everything.

View file

@ -0,0 +1,261 @@
# Searching for things
We have gone through how to create the various entities in Evennia. But creating something is of little use
if we cannot find and use it afterwards.
## Main search functions
The base tools are the `evennia.search_*` functions, such as `evennia.search_object`.
rose = evennia.search_object(key="rose")
acct = evennia.search_account(key="MyAccountName", email="foo@bar.com")
```{sidebar} Querysets
What is returned from the main search functions is actually a `queryset`. They can be
treated like lists except that they can't modified in-place. We'll discuss querysets in
the `next lesson` <Django-queries>`_.
```
Strings are always case-insensitive, so searching for `"rose"`, `"Rose"` or `"rOsE"` give the same results.
It's important to remember that what is returned from these search methods is a _listing_ of 0, one or more
elements - all the matches to your search. To get the first match:
rose = rose[0]
Often you really want all matches to the search parameters you specify. In other situations, having zero or
more than one match is a sign of a problem and you need to handle this case yourself.
the_one_ring = evennia.search_object(key="The one Ring")
if not the_one_ring:
# handle not finding the ring at all
elif len(the_one_ring) > 1:
# handle finding more than one ring
else:
# ok - exactly one ring found
the_one_ring = the_one_ring[0]
There are equivalent search functions for all the main resources. You can find a listing of them
[in the Search functions section](../../../Evennia-API.md) of the API frontpage.
## Searching using Object.search
On the `DefaultObject` is a `.search` method which we have already tried out when we made Commands. For
this to be used you must already have an object available:
rose = obj.search("rose")
The `.search` method wraps `evennia.search_object` and handles its output in various ways.
- By default it will always search for objects among those in `obj.location.contents` and `obj.contents` (that is,
things in obj's inventory or in the same room).
- It will always return exactly one match. If it found zero or more than one match, the return is `None`.
- On a no-match or multimatch, `.search` will automatically send an error message to `obj`.
So this method handles error messaging for you. A very common way to use it is in commands:
```python
from evennia import Command
class MyCommand(Command):
key = "findfoo"
def func(self):
foo = self.caller.search("foo")
if not foo:
return
```
Remember, `self.caller` is the one calling the command. This is usually a Character, which
inherits from `DefaultObject`! This (rather stupid) Command searches for an object named "foo" in
the same location. If it can't find it, `foo` will be `None`. The error has already been reported
to `self.caller` so we just abort with `return`.
You can use `.search` to find anything, not just stuff in the same room:
volcano = self.caller.search("Volcano", global=True)
If you only want to search for a specific list of things, you can do so too:
stone = self.caller.search("MyStone", candidates=[obj1, obj2, obj3, obj4])
This will only return a match if MyStone is one of the four provided candidate objects. This is quite powerful,
here's how you'd find something only in your inventory:
potion = self.caller.search("Healing potion", candidates=self.caller.contents)
You can also turn off the automatic error handling:
swords = self.caller.search("Sword", quiet=True)
With `quiet=True` the user will not be notified on zero or multi-match errors. Instead you are expected to handle this
yourself and what you get back is now a list of zero, one or more matches!
## What can be searched for
These are the main database entities one can search for:
- [Objects](../../../Components/Objects.md)
- [Accounts](../../../Components/Accounts.md)
- [Scripts](../../../Components/Scripts.md),
- [Channels](../../../Components/Channels.md),
- [Messages](../../../Components/Msg.md)
- [Help Entries](../../../Components/Help-System.md).
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?
### Search by key
The `key` is the name of the entity. Searching for this is always case-insensitive.
### Search by aliases
Objects and Accounts can have any number of aliases. When searching for `key` these will searched too,
you can't easily search only for aliases.
rose.aliases.add("flower")
If the above `rose` has a `key` `"Rose"`, it can now also be found by searching for `flower`. In-game
you can assign new aliases to things with the `alias` command.
### Search by location
Only Objects (things inheriting from `evennia.DefaultObject`) has a location. This is usually a room.
The `Object.search` method will automatically limit it search by location, but it also works for the
general search function. If we assume `room` is a particular Room instance,
chest = evennia.search_object("Treasure chest", location=room)
### Search by Tags
Think of a [Tag](../../../Components/Tags.md) as the label the airport puts on your luggage when flying.
Everyone going on the same plane gets a tag grouping them together so the airport can know what should
go to which plane. Entities in Evennia can be grouped in the same way. Any number of tags can be attached
to each object.
rose.tags.add("flowers")
daffodil.tags.add("flowers")
tulip.tags.add("flowers")
You can now find all flowers using the `search_tag` function:
all_flowers = evennia.search_tag("flowers")
Tags can also have categories. By default this category is `None` which is also considered a category.
silmarillion.tags.add("fantasy", category="books")
ice_and_fire.tags.add("fantasy", category="books")
mona_lisa_overdrive.tags.add("cyberpunk", category="books")
Note that if you specify the tag you _must_ also include its category, otherwise that category
will be `None` and find no matches.
all_fantasy_books = evennia.search_tag("fantasy") # no matches!
all_fantasy_books = evennia.search_tag("fantasy", category="books")
Only the second line above returns the two fantasy books. If we specify a category however,
we can get all tagged entities within that category:
all_books = evennia.search_tag(category="books")
This gets all three books.
### Search by Attribute
We can also search by the [Attributes](../../../Components/Attributes.md) associated with entities.
For example, let's give our rose thorns:
rose.db.has_thorns = True
wines.db.has_thorns = True
daffodil.db.has_thorns = False
Now we can find things attribute and the value we want it to have:
is_ouch = evennia.search_object_attribute("has_thorns", True)
This returns the rose and the wines.
> Searching by Attribute can be very practical. But if you plan to do a search very often, searching
> by-tag is generally faster.
### Search by Typeclass
Sometimes it's useful to find all objects of a specific Typeclass. All of Evennia's search tools support this.
all_roses = evennia.search_object(typeclass="typeclasses.flowers.Rose")
If you have the `Rose` class already imported you can also pass it directly:
all_roses = evennia.search_object(typeclass=Rose)
You can also search using the typeclass itself:
all_roses = Rose.objects.all()
This last way of searching is a simple form of a Django _query_. This is a way to express SQL queries using
Python.
### Search by dbref
The database id or `#dbref` is unique and never-reused within each database table. In search methods you can
replace the search for `key` with the dbref to search for. This must be written as a string `#dbref`:
the_answer = self.caller.search("#42")
eightball = evennia.search_object("#8")
Since `#dbref` is always unique, this search is always global.
```{warning} Relying on #dbrefs
You may be used to using #dbrefs a lot from other codebases. It is however considered
`bad practice` in Evennia to rely on hard-coded #dbrefs. It makes your code hard to maintain
and tied to the exact layout of the database. In 99% of cases you should pass the actual objects
around and search by key/tags/attribute instead.
```
## Finding objects relative each other
Let's consider a `chest` with a `coin` inside it. The chests stand in a room `dungeon`. In the dungeon is also
a `door`. This is an exit leading outside.
- `coin.location` is `chest`.
- `chest.location` is `dungeon`.
- `door.location` is `dungeon`.
- `room.location` is `None` since it's not inside something else.
One can use this to find what is inside what. For example, `coin.location.location` is the `room`.
We can also find what is inside each object. This is a list of things.
- `room.contents` is `[chest, door]`
- `chest.contents` is `[coin]`
- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin.
- `door.contents` is `[]` too.
A convenient helper is `.contents_get` - this allows to restrict what is returned:
- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?)
There is a special property for finding exits:
- `room.exits` is `[door]`
- `coin.exits` is `[]` (same for all the other objects)
There is a property `.destination` which is only used by exits:
- `door.destination` is `outside` (or wherever the door leads)
- `room.destination` is `None` (same for all the other non-exit objects)
## Summary
Knowing how to find things is important and the tools from this section will serve you well. For most of your needs
these tools will be all you need ...
... but not always. In the next lesson we will dive further into more complex searching when we look at
Django queries and querysets in earnest.

View file

@ -0,0 +1,123 @@
# The Tutorial World
The *Tutorial World* is a small and functioning MUD-style game world shipped with Evennia.
It's a small showcase of what is possible. It can also be useful for those who have an easier
time learning by deconstructing existing code.
Stand in the Limbo room and install it with
batchcommand tutorial_world.build
What this does is to run the build script
[evennia/contrib/tutorial_world/build.ev](github:evennia/contrib/tutorial_world/build.ev).
This is pretty much just a list of build-commands executed in sequence by the `batchcommand` command.
Wait for the building to complete and don't run it twice.
> After having run the batchcommand, the `intro` command also becomes available in Limbo. Try it out to
> for in-game help and to get an example of [EvMenu](../../../Components/EvMenu.md), Evennia's in-built
> menu generation system!
The game consists of a single-player quest and has some 20 rooms that you can explore as you seek
to discover the whereabouts of a mythical weapon.
A new exit should have appeared named _Tutorial_. Enter by writing `tutorial`.
You will automatically `quell` when you enter (and `unquell` when you leave), so you can play the way it was intended.
Both if you are triumphant or if you use the `give up` command you will eventually end up back in Limbo.
```{important}
Only LOSERS and QUITTERS use the `give up` command.
```
## Gameplay
![the castle off the moor](https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/22916c25-6299-453d-a221-446ec839f567/da2pmzu-46d63c6d-9cdc-41dd-87d6-1106db5a5e1a.jpg/v1/fill/w_600,h_849,q_75,strp/the_castle_off_the_moor_by_griatch_art_da2pmzu-fullview.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOiIsImlzcyI6InVybjphcHA6Iiwib2JqIjpbW3siaGVpZ2h0IjoiPD04NDkiLCJwYXRoIjoiXC9mXC8yMjkxNmMyNS02Mjk5LTQ1M2QtYTIyMS00NDZlYzgzOWY1NjdcL2RhMnBtenUtNDZkNjNjNmQtOWNkYy00MWRkLTg3ZDYtMTEwNmRiNWE1ZTFhLmpwZyIsIndpZHRoIjoiPD02MDAifV1dLCJhdWQiOlsidXJuOnNlcnZpY2U6aW1hZ2Uub3BlcmF0aW9ucyJdfQ.omuS3D1RmFiZCy9OSXiIita-HxVGrBok3_7asq0rflw)
*To get into the mood of this miniature quest, imagine you are an adventurer out to find fame and
fortune. You have heard rumours of an old castle ruin by the coast. In its depth a warrior princess
was buried together with her powerful magical weapon - a valuable prize, if it's true. Of course
this is a chance to adventure that you cannot turn down!*
*You reach the ocean in the midst of a raging thunderstorm. With wind and rain screaming in your
face you stand where the moor meets the sea along a high, rocky coast ...*
---
### Gameplay hints
- Use the command `tutorial` to get code insight behind the scenes of every room.
- Look at everything. While a demo, the Tutorial World is not necessarily trivial to solve - it depends
on your experience with text-based adventure games. Just remember that everything can be solved or bypassed.
- Some objects are interactive in more than one way. Use the normal `help` command to get a feel for
which commands are available at any given time.
- In order to fight, you need to first find some type of weapon.
- *slash* is a normal attack
- *stab* launches an attack that makes more damage but has a lower chance to hit.
- *defend* will lower the chance to taking damage on your enemy's next attack.
- Some things _cannot_ be hurt by mundane weapons. In that case it's OK to run away. Expect
to be chased though.
- Being defeated is a part of the experience. You can't actually die, but getting knocked out
means being left in the dark ...
## Once you are done (or had enough)
Afterwards you'll either have conquered the old ruin and returned in glory and triumph ... or
you returned limping and whimpering from the challenge by using the `give up` command.
Either way you should now be back in Limbo, able to reflect on the experience.
Some features exemplified by the tutorial world:
- Rooms with custom ability to show details (like looking at the wall in the dark room)
- Hidden or impassable exits until you fulfilled some criterion
- Objects with multiple custom interactions (like swords, the well, the obelisk ...)
- Large-area rooms (that bridge is actually only one room!)
- Outdoor weather rooms with weather (the rain pummeling you)
- Dark room, needing light source to reveal itself (the burning splinter even burns out after a while)
- Puzzle object (the wines in the dark cell; hope you didn't get stuck!)
- Multi-room puzzle (the obelisk and the crypt)
- Aggressive mobile with roam, pursue and battle state-engine AI (quite deadly until you find the right weapon)
- Weapons, also used by mobs (most are admittedly not that useful against the big baddie)
- Simple combat system with attack/defend commands (teleporting on-defeat)
- Object spawning (the weapons in the barrel and the final weapoon is actually randomized)
- Teleporter trap rooms (if you fail the obelisk puzzle)
```{sidebar} Extra Credit
If you have previous programming experience (or after you have gone
through this Starter tutorial) it may be instructive to dig a little deeper into the Tutorial-world
code to learn how it achieves what it does. The code is heavily documented.
You can find all the code in [evennia/contrib/tutorials/tutorial_world](evennia.contrib.tutorials.tutorial_world).
The build-script is [here](github:evennia/contrib/tutorials/tutorial_world/build.ev).
When reading the code, remember that the Tutorial World was designed to install easily and to not permanently modify
the rest of the game. It therefore makes sure to only use temporary solutions and to clean up after itself. This is
not something you will often need to worry about when making your own game.
```
Quite a lot of stuff crammed in such a small area!
## Uninstall the tutorial world
Once are done playing with the tutorial world, let's uninstall it.
Uninstalling the tutorial world basically means deleting all the rooms and objects it consists of.
Make sure you are back in Limbo, then
find tut#01
find tut#16
This should locate the first and last rooms created by `build.ev` - *Intro* and *Outro*. If you
installed normally, everything created between these two numbers should be part of the tutorial.
Note their #dbref numbers, for example 5 and 80. Next we just delete all objects in that range:
del 5-80
You will see some errors since some objects are auto-deleted and so cannot be found when the delete
mechanism gets to them. That's fine. You should have removed the tutorial completely once the
command finishes.
Even if the game-style of the Tutorial-world was not similar to the one you are interested in, it
should hopefully have given you a little taste of some of the possibilities of Evennia. Now we'll
move on with how to access this power through code.

View file

@ -0,0 +1,46 @@
# Part 2: What we want
```{eval-rst}
.. sidebar:: Beginner Tutorial Parts
`Introduction <../Beginner-Tutorial-Intro.html>`_
Getting set up.
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
A tour of Evennia and how to use the tools, including an introduction to Python.
**Part 2: What we want**
Planning our tutorial game and what to think about when planning your own in the future.
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
Getting down to the meat of extending Evennia to make our game
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
Building a tech-demo and world content to go with our code
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
Taking our new game online and let players try it out
```
In Part two of the Beginner Tutorial we'll take a step back and plan out the kind of tutorial
game we want to make. This is a more 'theoretical' part where we won't do any hands-on
programming.
In the process we'll go through the common questions of "where to start"
and "what to think about" when creating a multiplayer online text game.
## Lessons
```{toctree}
:maxdepth: 1
Planning-Where-Do-I-Begin.md
Game-Planning.md
Planning-Some-Useful-Contribs.md
Planning-The-Tutorial-Game.md
```
## Table of Contents
```{toctree}
Planning-Where-Do-I-Begin.md
Game-Planning.md
Planning-Some-Useful-Contribs.md
Planning-The-Tutorial-Game.md
```

View file

@ -0,0 +1,209 @@
# On Planning a Game
Last lesson we asked ourselves some questions about our motivation. In this one we'll present
some more technical questions to consider. In the next lesson we'll answer them for the sake of
our tutorial game.
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.
```{important}
Your first all overshadowing goal is to beat the odds and get **something** out the door!
Even if it's a scaled-down version of your dream game, lacking many "must-have" features!
```
Remember: *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. It's better to get your game out there and expand on it
later than to code in isolation until you burn out, lose interest or your hard drive crashes.
- Keep the scope of your initial release down. Way down.
- Start small, with an eye towards expansions later, after first release.
- If the suggestions here seems boring or a chore to you, do it your way instead. Everyone's different.
- Keep having _fun_. You must keep your motivation up, whichever way works for _you_.
## The steps
Here are the rough steps towards your goal.
1. Planning
2. Coding + Gradually building a tech-demo
3. Building the actual game world
4. Release
5. Celebrate
## Planning
You need to have at least a rough idea about what you want to create. Some like a lot of planning, others
do it more seat-of-the-pants style. Regardless, while _some_ planning is always good to do, it's common
to have your plans change on you as you create your code prototypes. So don't get _too_ bogged down in
the details out of the gate.
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. Such things are very important. But
unfortunately, they are not enough to make your game. You need to figure out how to accomplish your ideas in
Evennia.
Below are some questions to get you going. In the next lesson we will try to answer them for our particular
tutorial game. There are of course many more questions you could be asking yourself.
### Administration
- Should your game rules be enforced by coded systems or by human game masters?
- What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
- Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
### Building
- How will the world be built? Traditionally (from in-game with build-commands) or externally (by batchcmds/code
or directly with custom code)?
- Can only privileged Builders create things or should regular players also have limited build-capability?
### Systems
- Do you base your game off an existing RPG system or make up your own?
- What are the game mechanics? How do you decide if an action succeeds or fails?
- Does the flow of time matter in your game - does night and day change? What about seasons?
- Do you want changing, global weather or should weather just be set manually in roleplay?
- Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
- Do you have concepts like reputation and influence?
- Will your characters be known by their name or only by their physical appearance?
### Rooms
- Is a simple room 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?
### Objects / items
- How numerous are your objects? Do you want large loot-lists or are objects just role playing props
created on demand?
- If you use money, is each coin a separate object or do you just store a bank account value?
- Do multiple similar objects 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? Can they be repaired?
- Can you fight with a chair or a flower or must you use a specific 'weapon' kind of thing?
- Will characters be able to craft new objects?
- Should mobs/NPCs have some sort of AI?
- Are NPCs and mobs different entities? How do they differ?
- Should there be NPCs giving quests? If so, how do you track Quest status?
### Characters
- Can players have more than one Character active at a time or are they allowed to multi-play?
- How does the character-generation work? Walk from room-to-room? A menu?
- 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 does the skill tree look like? Can a Character gain experience to improve? By killing
enemies? Solving quests? By roleplaying?
- May player-characters attack each other (PvP)?
- What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
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 and Tech demo
This is the actual work of creating the "game" part of your game. As you code and test systems you should
build a little "tech demo" along the way.
```{sidebar} Tech demo
With "tech demo" we mean a small example of your code in-action: A room with a mob,
a way to jump into and test character-creation etc. The tech demo need not be pretty, it's
there to test functionality. It's not the beginning of your game world (unless you find that
to be more fun).
```
Try to avoid going wild with building a huge game world before you have a tech-demo showing off all parts
you expect to have in the first version of your game. Otherwise you run the risk of having to redo it all
again.
Evennia tries hard to make the coding easier for you, but there is no way around the fact that if you want
anything but a basic chat room 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 and components of Evennia. It's recommended you look over the rest of this Beginner Tutorial to learn
what tools you have available.
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 using _version control_. Github.com offers free Private repos
these days if you don't want the world to learn your secrets. Not only version control
make it easy for your team to collaborate, it also means
your work is backed up at all times. The page on [Version Control](../../../Coding/Version-Control.md)
will help you to setting up a sane developer environment with proper version control.
## World Building
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. You could be 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*.
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. If Builders and coders are different people 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](../../../Setup/Online-Setup.md) 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](https://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!
## Planning our tutorial game
In the next lesson we'll make use of these general points and try to plan out our tutorial game.

View file

@ -0,0 +1,244 @@
# Planning the use of some useful contribs
Evennia is deliberately bare-bones out of the box. The idea is that you should be as unrestricted as possible
in designing your game. This is why you can easily replace the few defaults we have and why we don't try to
prescribe any major game systems on you.
That said, Evennia _does_ offer some more game-opinionated _optional_ stuff. These are referred to as _Contribs_
and is an ever-growing treasure trove of code snippets, concepts and even full systems you can pick and choose
from to use, tweak or take inspiration from when you make your game.
The [Contrib overview](../../../Contribs/Contribs-Overview.md) page gives the full list of the current roster of contributions. On
this page we will review a few contribs we will make use of for our game. We will do the actual installation
of them when we start coding in the next part of this tutorial series. While we will introduce them here, you
are wise to read their doc-strings yourself for the details.
This is the things we know we need:
- A barter system
- Character generation
- Some concept of wearing armor
- The ability to roll dice
- Rooms with awareness of day, night and season
- Roleplaying with short-descs, poses and emotes
- Quests
- Combat (with players and against monsters)
## Barter contrib
[source](../../../api/evennia.contrib.game_systems.barter.md)
Reviewing this contrib suggests that it allows for safe trading between two parties. The basic principle
is that the parties puts up the stuff they want to sell and the system will guarantee that these systems are
exactly what is being offered. Both sides can modify their offers (bartering) until both mark themselves happy
with the deal. Only then the deal is sealed and the objects are exchanged automatically. Interestingly, this
works just fine for money too - just put coin objects on one side of the transaction.
Sue > trade Tom: Hi, I have a necklace to sell; wanna trade for a healing potion?
Tom > trade Sue: Hm, I could use a necklace ...
<both accepted trade. Start trade>
Sue > offer necklace: This necklace is really worth it.
Tom > evaluate necklace:
<Tom sees necklace stats>
Tom > offer ration: I don't have a healing potion, but I'll trade you an iron ration!
Sue > Hey, this is a nice necklace, I need more than a ration for it...
Tom > offer ration, 10gold: Ok, a ration and 10 gold as well.
Sue > accept: Ok, that sounds fair!
Tom > accept: Good! Nice doing business with you.
<goods change hands automatically. Trade ends>
Arguably, in a small game you are just fine to just talk to people and use `give` to do the exchange. The
barter system guarantees trading safety if you don't trust your counterpart to try to give you the wrong thing or
to run away with your money.
We will use the barter contrib as an optional feature for player-player bartering. More importantly we can
add it for NPC shopkeepers and expand it with a little AI, which allows them to potentially trade in other
things than boring gold coin.
## Clothing contrib
[source](../../../api/evennia.contrib.game_systems.clothing.md)
This contrib provides a full system primarily aimed at wearing clothes, but it could also work for armor. You wear
an object in a particular location and this will then be reflected in your character's description. You can
also add roleplaying flavor:
> wear helmet slightly askew on her head
look self
Username is wearing a helmet slightly askew on her head.
By default there are no 'body locations' in this contrib, we will need to expand on it a little to make it useful
for things like armor. It's a good contrib to build from though, so that's what we'll do.
## Dice contrib
[source](../../../api/evennia.contrib.rpg.dice.md)
The dice contrib presents a general dice roller to use in game.
> roll 2d6
Roll(s): 2 and 5. Total result is 7.
> roll 1d100 + 2
Roll(s): 43. Total result is 47
> roll 1d20 > 12
Roll(s): 7. Total result is 7. This is a failure (by 5)
> roll/hidden 1d20 > 12
Roll(s): 18. Total result is 17. This is a success (by 6). (not echoed)
The contrib also has a python function for producing these results in-code. However, while
we will emulate rolls for our rule system, we'll do this as simply as possible with Python's `random`
module.
So while this contrib is fun to have around for GMs or for players who want to get a random result
or play a game, we will not need it for the core of our game.
## Extended room contrib
[source](../../../api/evennia.contrib.grid.extended_room.md)
This is a custom Room typeclass that changes its description based on time of day and season.
For example, at night, in wintertime you could show the room as being dark and frost-covered while in daylight
at summer it could describe a flowering meadow. The description can also contain special markers, so
`<morning> ... </morning>` would include text only visible at morning.
The extended room also supports _details_, which are things to "look at" in the room without there having
to be a separate database object created for it. For example, a player in a church may do `look window` and
get a description of the windows without there needing to be an actual `window` object in the room.
Adding all those extra descriptions can be a lot of work, so they are optional; if not given the room works
like a normal room.
The contrib is simple to add and provides a lot of optional flexibility, so we'll add it to our
game, why not!
## RP-System contrib
[source](../../../api/evennia.contrib.rpg.rpsystem.md)
This contrib adds a full roleplaying subsystem to your game. It gives every character a "short-description"
(sdesc) that is what people will see when first meeting them. Let's say Tom has an sdesc "A tall man" and
Sue has the sdesc "A muscular, blonde woman"
Tom > look
Tom: <room desc> ... You see: A muscular, blonde woman
Tom > emote /me smiles to /muscular.
Tom: Tom smiles to A muscular, blonde woman.
Sue: A tall man smiles to Sue.
Tom > emote Leaning forward, /me says, "Well hello, what's yer name?"
Tom: Leaning forward, Tom says, "Well hello..."
Sue: Leaning forward, A tall man says, "Well hello, what's yer name?"
Sue > emote /me grins. "I'm Angelica", she says.
Sue: Sue grins. "I'm Angelica", she says.
Tom: A muscular, blonde woman grins. "I'm Angelica", she says.
Tom > recog muscular Angelica
Tom > emote /me nods to /angelica: "I have a message for you ..."
Tom: Tom nods to Angelica: "I have a message for you ..."
Sue: A tall man nods to Sue: "I have a message for you ..."
Above, Sue introduces herself as "Angelica" and Tom uses this info to `recoc` her as "Angelica" hereafter. He
could have `recoc`-ed her with whatever name he liked - it's only for his own benefit. There is no separate
`say`, the spoken words are embedded in the emotes in quotes `"..."`.
The RPSystem module also includes options for `poses`, which help to establish your position in the room
when others look at you.
Tom > pose stands by the bar, looking bored.
Sue > look
Sue: <room desc> ... A tall man stands by the bar, looking bored.
You can also wear a mask to hide your identity; your sdesc will then be changed to the sdesc of the mask,
like `a person with a mask`.
The RPSystem gives a lot of roleplaying power out of the box, so we will add it. There is also a separate
[rplanguage](../../../api/evennia.contrib.rpg.rpsystem.md) module that integrates with the spoken words in your emotes and garbles them if you don't understand
the language spoken. In order to restrict the scope we will not include languages for the tutorial game.
## Talking NPC contrib
[source](../../../api/evennia.contrib.tutorials.talking_npc.md)
This exemplifies an NPC with a menu-driven dialogue tree. We will not use this contrib explicitly, but it's
good as inspiration for how we'll do quest-givers later.
## Traits contrib
[source](../../../api/evennia.contrib.rpg.traits.md)
An issue with dealing with roleplaying attributes like strength, dexterity, or skills like hunting, sword etc
is how to keep track of the values in the moment. Your strength may temporarily be buffed by a strength-potion.
Your swordmanship may be worse because you are encumbered. And when you drink your health potion you must make
sure that those +20 health does not bring your health higher than its maximum. All this adds complexity.
The _Traits_ contrib consists of several types of objects to help track and manage values like this. When
installed, the traits are accessed on a new handler `.traits`, for example
> py self.traits.hp.value
100
> py self.traits.hp -= 20 # getting hurt
> py self.traits.hp.value
80
> py self.traits.hp.reset() # drink a potion
> py self.traits.hp.value
100
A Trait is persistent (it uses an Attribute under the hood) and tracks changes, min/max and other things
automatically. They can also be added together in various mathematical operations.
The contrib introduces three main Trait-classes
- _Static_ traits for single values like str, dex, things that at most gets a modifier.
- _Counters_ is a value that never moves outside a given range, even with modifiers. For example a skill
that can at most get a maximum amount of buff. Counters can also easily be _timed_ so that they decrease
or increase with a certain rate per second. This could be good for a time-limited curse for example.
- _Gauge_ is like a fuel-gauge; it starts at a max value and then empties gradually. This is perfect for
things like health, stamina and the like. Gauges can also change with a rate, which works well for the
effects of slow poisons and healing both.
```
> py self.traits.hp.value
100
> py self.traits.hp.rate = -1 # poisoned!
> py self.traits.hp.ratetarget = 50 # stop at 50 hp
# Wait 30s
> py self.traits.hp.value
70
# Wait another 30s
> py self.traits.hp.value
50 # stopped at 50
> py self.traits.hp.rate = 0 # no more poison
> py self.traits.hp.rate = 5 # healing magic!
# wait 5s
> pyself.traits.hp.value
75
```
Traits will be very practical to use for our character sheets.
## Turnbattle contrib
[source](../../../api/evennia.contrib.game_systems.turnbattle.md)
This contrib consists of several implementations of a turn-based combat system, divivided into complexity:
- basic - initiative and turn order, attacks against defense values, damage.
- equip - considers weapons and armor, wielding and weapon accuracy.
- items - adds usable items with conditions and status effects
- magic - adds spellcasting system using MP.
- range - adds abstract positioning and 1D movement to differentiate between melee and ranged attacks.
The turnbattle system is comprehensive, but it's meant as a base to start from rather than offer
a complete system. It's also not built with _Traits_ in mind, so we will need to adjust it for that.
## Conclusions
With some contribs selected, we have pieces to build from and don't have to write everything from scratch.
We will need Quests and will likely need to do a bunch of work on Combat to adapt the combat contrib
to our needs.
We will now move into actually starting to implement our tutorial game
in the next part of this tutorial series. When doing this for yourself, remember to refer
back to your planning and adjust it as you learn what works and what does not.

View file

@ -0,0 +1,425 @@
# Planning our tutorial game
Using the general plan from last lesson we'll now establish what kind of game we want to create for this tutorial.
Remembering that we need to keep the scope down, let's establish some parameters.
Note that for your own
game you don't _need_ to agree/adopt any of these. Many game-types need more or much less than this.
But this makes for good, instructive examples.
- To have something to refer to rather than just saying "our tutorial game" over and over, we'll
name it ... _EvAdventure_.
- We want EvAdventure be a small game we can play ourselves for fun, but which could in principle be expanded
to something more later.
- Let's go with a fantasy theme, it's well understood.
- We'll use some existing, simple RPG system.
- We want to be able to create and customize a character of our own.
- We want the tools to roleplay with other players.
- We don't want to have to rely on a Game master to resolve things, but will rely on code for skill resolution
and combat.
- We want monsters to fight and NPCs we can talk to. So some sort of AI.
- We want to be able to buy and sell stuff, both with NPCs and other players.
- We want some sort of crafting system.
- We want some sort of quest system.
Let's answer the questions from the previous lesson and discuss some of the possibilities.
## Administration
### Should your game rules be enforced by coded systems by human game masters?
Generally, the more work you expect human staffers/GMs to do, the less your code needs to work. To
support GMs you'd need to design commands to support GM-specific actions and the type of game-mastering
you want them to do. You may need to expand communication channels so you can easily
talk to groups people in private and split off gaming groups from each other. RPG rules could be as simple
as the GM sitting with the rule books and using a dice-roller for visibility.
GM:ing is work-intensive however, and even the most skilled and enthusiastic GM can't be awake all hours
of the day to serve an international player base. The computer never needs sleep, so having the ability for
players to "self-serve" their RP itch when no GMs are around is a good idea even for the most GM-heavy games.
On the other side of the spectrum are games with no GMs at all; all gameplay are driven either by the computer
or by the interactions between players. Such games still need an active staff, but nowhere as much active
involvement. Allowing for solo-play with the computer also allows players to have fun when the number of active
players is low.
We want EvAdventure to work entirely without depending on human GMs. That said, there'd be nothing
stopping a GM from stepping in and run an adventure for some players should they want to.
### What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
The default hierarchy is
- `Player` - regular players
- `Player Helper` - can create/edit help entries
- `Builder` - can use build commands
- `Admin` - can kick and ban accounts
- `Developer` - full access, usually also trusted with server access
There is also the _superuser_, the "owner" of the game you create when you first set up your database. This user
goes outside the regular hierarchy and should usually only.
We are okay with keeping this structure for our game.
### Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
Evennia's _Channels_ are by default only available between _Accounts_. That is, for players to communicate with each
other. By default, the `public` channel is created for general discourse.
Channels are logged to a file and when you are coming back to the game you can view the history of a channel
in case you missed something.
> public Hello world!
[Public] MyName: Hello world!
But Channels can also be set up to work between Characters instead of Accounts. This would mean the channels
would have an in-game meaning:
- Members of a guild could be linked telepathically.
- Survivors of the apocalypse can communicate over walkie-talkies.
- Radio stations you can tune into or have to discover.
_Bulletin boards_ are a sort of in-game forum where posts are made publicly or privately. Contrary to a channel,
the messages are usually stored and are grouped into topics with replies. Evennia has no default bulletin-board
system.
In EvAdventure we will just use the default inter-account channels. We will also not be implementing any
bulletin boards.
## Building
### How will the world be built?
There are two main ways to handle this:
- Traditionally, from in-game with build-commands: This means builders creating content in their game
client. This has the advantage of not requiring Python skills nor server access. This can often be a quite
intuitive way to build since you are sort-of walking around in your creation as you build it. However, the
developer (you) must make sure to provide build-commands that are flexible enough for builders to be able to
create the content you want for your game.
- Externally (by batchcmds): Evennia's `batchcmd` takes a text file with Evennia Commands and executes them
in sequence. This allows the build process to be repeated and applied quickly to a new database during development.
It also allows builders to use proper text-editing tools rather than writing things line-by-line in their clients.
The drawback is that for their changes to go live they either need server access or they need to send their
batchcode to the game administrator so they can apply the changes. Or use version control.
- Externally (with batchcode or custom code): This is the "professional game development" approach. This gives the
builders maximum power by creating the content in Python using Evennia primitives. The `batchcode` processor
allows Evennia to apply and re-apply build-scripts that are raw Python modules. Again, this would require the
builder to have server access or to use version control to share their work with the rest of the development team.
In this tutorial, we will show examples of all these ways, but since we don't have a team of builders we'll
build the brunt of things using Evennia's Batchcode system.
### Can only privileged Builders create things or should regular players also have limited build-capability?
In some game styles, players have the ability to create objects and even script them. While giving regular users
the ability to create objects with in-built commands is easy and safe, actual code-creation (aka _softcode_ ) is
not something Evennia supports natively. Regular, untrusted users should never be allowed to execute raw Python
code (such as what you can do with the `py` command). You can
[read more about Evennia's stance on softcode here](../../../Concepts/Soft-Code.md). If you want users to do limited scripting,
it's suggested that this is accomplished by adding more powerful build-commands for them to use.
For our tutorial-game, we will only allow privileged builders to modify the world. The exception is crafting,
which we will limit to repairing broken items by combining them with other repair-related items.
## Systems
### Do you base your game off an existing RPG system or make up your own?
We will make use of [Open Adventure](http://www.geekguild.com/openadventure/), a simple 'old school' RPG-system
that is available for free under the Creative Commons license. We'll only use a subset of the rules from
the blue "basic" book. For the sake of keeping down the length of this tutorial we will limit what features
we will include:
- Only two 'archetypes' (classes) - Arcanist (wizard) and Warrior, these are examples of two different play
styles.
- Two races only (dwarves and elves), to show off how to implement races and race bonuses.
- No extra features of the races/archetypes such as foci and special feats. While these are good for fleshing
out a character, these will work the same as other bonuses and are thus not that instructive.
- We will add only a small number of items/weapons from the Open Adventure rulebook to show how it's done.
### What are the game mechanics? How do you decide if an action succeeds or fails?
Open Adventure's conflict resolution is based on adding a trait (such as Strength) with a random number in
order to beat a target. We will emulate this in code.
Having a "skill" means getting a bonus to that roll for a more narrow action.
Since the computer will need to know exactly what those skills are, we will add them more explicitly than
in the rules, but we will only add the minimum to show off the functionality we need.
### Does the flow of time matter in your game - does night and day change? What about seasons?
Most commonly, game-time runs faster than real-world time. There are
a few advantages with this:
- Unlike in a single-player game, you can't fast-forward time in a multiplayer game if you are waiting for
something, like NPC shops opening.
- Healing and other things that we know takes time will go faster while still being reasonably 'realistic'.
The main drawback is for games with slower roleplay pace. While you are having a thoughtful roleplaying scene
over dinner, the game world reports that two days have passed. Having a slower game time than real-time is
a less common, but possible solution for such games.
It is however _not_ recommended to let game-time exactly equal the speed of real time. The reason for this
is that people will join your game from all around the world, and they will often only be able to play at
particular times of their day. With a game-time drifting relative real-time, everyone will eventually be
able to experience both day and night in the game.
For this tutorial-game we will go with Evennia's default, which is that the game-time runs two times faster
than real time.
### Do you want changing, global weather or should weather just be set manually in roleplay?
A weather system is a good example of a game-global system that affects a subset of game entities
(outdoor rooms). We will not be doing any advanced weather simulation, but we'll show how to do
random weather changes happening across the game world.
### Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
We will allow for money and barter/trade between NPCs/Players and Player/Player, but will not care about
inflation. A real economic simulation could do things like modify shop prices based on supply and demand.
We will not go down that rabbit hole.
### Do you have concepts like reputation and influence?
These are useful things for a more social-interaction heavy game. We will not include them for this
tutorial however.
### Will your characters be known by their name or only by their physical appearance?
This is a common thing in RP-heavy games. Others will only see you as "The tall woman" until you
introduce yourself and they 'recognize' you with a name. Linked to this is the concept of more complex
emoting and posing.
Adding such a system from scratch is complex and way beyond the scope of this tutorial. However,
there is an existing Evennia contrib that adds all of this functionality and more, so we will
include that and explain briefly how it works.
## Rooms
### Is a simple room description enough or should the description be able to change?
Changing room descriptions for day and night, winder and summer is actually quite easy to do, but looks
very impressive. We happen to know there is also a contrib that helps with this, so we'll show how to
include that.
### Should the room have different statuses?
We will have different weather in outdoor rooms, but this will not have any gameplay effect - bow strings
will not get wet and fireballs will not fizzle if it rains.
### Can objects be hidden in the room? Can a person hide in the room?
We will not model hiding and stealth. This will be a game of honorable face-to-face conflict.
## Objects
### How numerous are your objects? Do you want large loot-lists or are objects just role playing props?
Since we are not going for a pure freeform RPG here, we will want objects with properties, like weapons
and potions and such. Monsters should drop loot even though our list of objects will not be huge.
### Is each coin a separate object or do you just store a bank account value?
Since we will use bartering, placing coin objects on one side of the barter makes for a simple way to
handle payments. So we will use coins as-objects.
### Do multiple similar objects form stacks and how are those stacks handled in that case?
Since we'll use coins, it's practical to have them and other items stack together. While Evennia does not
do this natively, we will make use of a contrib for this.
### Does an object have weight or volume (so you cannot carry an infinite amount of them)?
Limiting carrying weight is one way to stop players from hoarding. It also makes it more important
for players to pick only the equipment they need. Carrying limits can easily come across as
annoying to players though, so one needs to be careful with it.
Open Adventure rules include weight limits, so we will include them.
### Can objects be broken? Can they be repaired?
Item breakage is very useful for a game economy; breaking weapons adds tactical considerations (if it's not
too common, then it becomes annoying) and repairing things gives work for crafting players.
We wanted a crafting system, so this is what we will limit it to - repairing items using some sort
of raw materials.
### Can you fight with a chair or a flower or must you use a special 'weapon' kind of thing?
Traditionally, only 'weapons' could be used to fight with. In the past this was a useful
simplification, but with Python classes and inheritance, it's not actually more work to just
let all items in game work as a weapon in a pinch.
So for our game we will let a character use any item they want as a weapon. The difference will
be that non-weapon items will do less damage and also break and become unusable much quicker.
### Will characters be able to craft new objects?
Crafting is a common feature in multiplayer games. In code it usually means using a skill-check
to combine base ingredients from a fixed recipe in order to create a new item. The classic
example is to combine _leather straps_, a _hilt_, a _pommel_ and a _blade_ to make a new _sword_.
A full-fledged crafting system could require multiple levels of crafting, including having to mine
for ore or cut down trees for wood.
In our case we will limit our crafting to repairing broken items. To show how it's done, we will require
extra items (a recipe) in order to facilitate the repairs.
### Should mobs/NPCs have some sort of AI?
A rule of adding Artificial Intelligence is that with today's technology you should not hope to fool
anyone with it anytime soon. Unless you have a side-gig as an AI researcher, users will likely
not notice any practical difference between a simple state-machine and you spending a lot of time learning
how to train a neural net.
For this tutorial, we will show how to add a simple state-machine for monsters. NPCs will only be
shop-keepers and quest-gives so they won't need any real AI to speak of.
### Are NPCs and mobs different entities? How do they differ?
"Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could
also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally
different these days it's often easier to just make NPCs and mobs essentially the same thing.
In EvAdventure, both Monsters and NPCs will be the same type of thing; A monster could give you a quest
and an NPC might fight you as a mob as well as trade with you.
### _Should there be NPCs giving quests? If so, how do you track Quest status?
We will design a simple quest system to track the status of ongoing quests.
## Characters
### Can players have more than one Character active at a time or are they allowed to multi-play?
Since Evennia differentiates between `Sessions` (the client-connection to the game), `Accounts`
and `Character`s, it natively supports multi-play. This is controlled by the `MULTISESSION_MODE`
setting, which has a value from `0` (default) to `3`.
- `0`- One Character per Account and one Session per Account. This means that if you login to the same
account from another client you'll be disconnected from the first. When creating a new account, a Character
will be auto-created with the same name as your Account. This is default mode and mimics legacy code bases
which had no separation between Account and Character.
- `1` - One Character per Account, multiple Sessions per Account. So you can connect simultaneously from
multiple clients and see the same output in all of them.
- `2` - Multiple Characters per Account, one Session per Character. This will not auto-create a same-named
Character for you, instead you get to create/choose between a number of Characters up to a max limit given by
the `MAX_NR_CHARACTERS` setting (default 1). You can play them all simultaneously if you have multiple clients
open, but only one client per Character.
- `3` - Multiple Characters per Account, Multiple Sessions per Character. This is like mode 2, except players
can control each Character from multiple clients, seeing the same output from each Character.
We will go with a multi-role game, so we will use `MULTISESSION_MODE=3` for this tutorial.
### How does the character-generation work?
There are a few common ways to do character generation:
- Rooms. This is the traditional way. Each room's description tells you what command to use to modify
your character. When you are done you move to the next room. Only use this if you have another reason for
using a room, like having a training dummy to test skills on, for example.
- A Menu. The Evennia _EvMenu_ system allows you to code very flexible in-game menus without needing to walk
between rooms. You can both have a step-by-step menu (a 'wizard') or allow the user to jump between the
steps as they please. This tends to be a lot easier for newcomers to understand since it doesn't require
using custom commands they will likely never use again after this.
- Questions. A fun way to build a character is to answer a series of questions. This is usually implemented
with a sequential menu.
For the tutorial we will use a menu to let the user modify each section of their character sheet in any order
until they are happy.
### How do you implement different "classes" or "races"?
The way classes and races work in most RPGs (as well as in OpenAdventure) is that they act as static 'templates'
that inform which bonuses and special abilities you have. This means that all we need to store on the
Character is _which_ class and _which_ race they have; the actual logic can sit in Python code and just
be looked up when we need it.
### If a Character can hide in a room, what skill will decide if they are detected?
Hiding means a few things.
- The Character should not appear in the room's description / character list
- Others hould not be able to interact with a hidden character. It'd be weird if you could do `attack <name>`
or `look <name>` if the named character is in hiding.
- There must be a way for the person to come out of hiding, and probably for others to search or accidentally
find the person (probably based on skill checks).
- The room will also need to be involved, maybe with some modifier as to how easy it is to hide in the room.
We will _not_ be including a hide-mechanic in EvAdventure though.
### What does the skill tree look like? Can a Character gain experience to improve? By killing enemies? Solving quests? By roleplaying?
Gaining experience points (XP) and improving one's character is a staple of roleplaying games. There are many
ways to implement this:
- Gaining XP from kills is very common; it's easy to let a monster be 'worth' a certain number of XP and it's
easy to tell when you should gain it.
- Gaining XP from quests is the same - each quest is 'worth' XP and you get them when completing the test.
- Gaining XP from roleplay is harder to define. Different games have tried a lot of different ways to do this:
- XP from being online - just being online gains you XP. This inflates player numbers but many players may
just be lurking and not be actually playing the game at any given time.
- XP from roleplaying scenes - you gain XP according to some algorithm analyzing your emotes for 'quality',
how often you post, how long your emotes are etc.
- XP from actions - you gain XP when doing things, anything. Maybe your XP is even specific to each action, so
you gain XP only for running when you run, XP for your axe skill when you fight with an axe etc.
- XP from fails - you only gain XP when failing rolls.
- XP from other players - other players can award you XP for good RP.
For EvAdventure we will use Open Adventure's rules for XP, which will be driven by kills and quest successes.
### May player-characters attack each other (PvP)?
Deciding this affects the style of your entire game. PvP makes for exciting gameplay but it opens a whole new
can of worms when it comes to "fairness". Players will usually accept dying to an overpowered NPC dragon. They
will not be as accepting if they perceive another player is perceived as being overpowered. PvP means that you
have to be very careful to balance the game - all characters does not have to be exactly equal but they should
all be viable to play a fun game with. PvP does not only mean combat though. Players can compete in all sorts of ways, including gaining influence in
a political game or gaining market share when selling their crafted merchandise.
For the EvAdventure we will support both Player-vs-environment combat and turn-based PvP. We will allow players
to barter with each other (so potentially scam others?) but that's the extent of it. We will focus on showing
off techniques and will not focus on making a balanced game.
### What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
This is another big decision that strongly affects the mood and style of your game.
Perma-death means that once your character dies, it's gone and you have to make a new one.
- It allows for true heroism. If you genuinely risk losing your character of two years to fight the dragon,
your triumph is an actual feat.
- It limits the old-timer dominance problem. If long-time players dies occationally, it will open things
up for newcomers.
- It lowers inflation, since the hoarded resources of a dead character can be removed.
- It gives capital punishment genuine discouraging power.
- It's realistic.
Perma-death comes with some severe disadvantages however.
- It's impopular. Many players will just not play a game where they risk losing their beloved character
just like that.
- Many players say they like the _idea_ of permadeath except when it could happen to them.
- It can limit roleplaying freedom and make people refuse to take any risks.
- It may make players even more reluctant to play conflict-driving 'bad guys'.
- Game balance is much, much more important when results are "final". This escalates the severity of 'unfairness'
a hundred-fold. Things like bugs or exploits can also lead to much more server effects.
For these reasons, it's very common to do hybrid systems. Some tried variations:
- NPCs cannot kill you, only other players can.
- Death is permanent, but it's difficult to actually die - you are much more likely to end up being severely
hurt/incapacitated.
- You can pre-pay 'insurance' to magically/technologically avoid actually dying. Only if don't have insurance will
you die permanently.
- Death just means harsh penalties, not actual death.
- When you die you can fight your way back to life from some sort of afterlife.
- You'll only die permanently if you as a player explicitly allows it.
For our tutorial-game we will not be messing with perma-death; instead your defeat will mean you will re-spawn
back at your home location with a fraction of your health.
## Conclusions
Going through the questions has helped us get a little bit more of a feel for the game we want to do. There are
many other things we could ask ourselves, but if we can cover these points we will be a good way towards a complete,
playable game!
Before starting to code in earnest a good coder should always do an inventory of all the stuff they _don't_ need
to code themselves. So in the next lesson we will check out what help we have from Evennia's _contribs_.

View file

@ -0,0 +1,145 @@
# Where do I begin?
The good news is that following this Starting tutorial is a great way to begin making an Evennia game.
The bad news is that everyone's different and when it comes to starting your own game there is no
one-size-fits-all answer. Instead we will ask a series of questions
to help you figure this out for yourself. It will also help you evaluate your own skills and maybe
put some more realistic limits on how fast you can achieve your goals.
> The questions in this lesson do not really apply to our tutorial game since we know we are doing it
> to learn Evennia. If you just want to follow along with the technical bits you can skip this lesson and
> come back later when you feel ready to take on making your own game.
## What is your motivation for doing this?
So you want to make a game. First you need to make a few things clear to yourself.
Making a multiplayer online game is a _big_ undertaking. You will (if you are like most of us) be
doing it as a hobby, without getting paid. And youll be doing it for a long time.
So the very first thing you should ask yourself (and your team, if you have any) is
_why am I doing this_? Do some soul-searching here. Here are some possible answers:
- I want to earn recognition and fame from my online community and/or among my friends.
- I want to build the game so I can play and enjoy it myself.
- I want to build the same game I already play but without the bad people.
- I want to create a game so that I can control it and be the head honcho.
- A friend or online acquaintance talked me into working on it.
- I work on this because Im paid to (wow!)
- I only build this for my own benefit or to see if I can pull it off.
- I want to create something to give back to the community I love.
- I want to use this project as a stepping-stone towards other projects (like a career in game design
or programming).
- I am interested in coding or server and network architectures, making a MUD just seems to be a good
way to teach myself.
- I want to build a commercial game and earn money.
- I want to fulfill a life-long dream of game making.
There are many other possibilities. How “solid” your answer is for a long-term development project
is up to you. The important point is that you ask yourself the question.
**Help someone else instead** - Maybe you should _not_ start a new project - maybe you're better off
helping someone else or improve on something that already exists. Or maybe you find you are more of a
game engine developer than a game designer.
**Driven by emotion** - Some answers may suggest that you are driven by emotions of revenge or disconcert. Be careful with that and
check so that's not your _only_ driving force. Those emotions may have abated later when the project
most needs your enthusiasm and motivation.
**Going commercial** - If your aim is to earn money, your design goals will likely be very different from
those of a person who only creates as a hobby or for their own benefit. You may also have a much stricter
timeline for release.
Whichever your motivation, you should at least have it clear in your own mind. Its worth to make
sure your eventual team is on the same page too.
## What are your skills?
Once you have your motivations straight you need to take a stock of your own skills and the skills
available in your team, if you have any.
Your game will have two principal components and you will need skills to cater for both:
- The game engine / code base - Evennia in this case.
- The assets created for using the game engine (“the game world”)
### The game engine
The game engine is maintained and modified by programmers (coders). It represents the infrastructure
that runs the game - the network code, the protocol support, the handling of commands, scripting and
data storage.
If you are just evaluating Evennia, it's worth to do the following:
- Hang out in the community/forums/chat. Expect to need to ask a lot of “stupid” questions as you start
developing (hint: no question is stupid). Is this a community in which you would feel comfortable doing so?
- Keep tabs on the manual (you're already here).
- How's your Python skills? What are the skills in your team? Do you or your team already know it or are
you willing to learn? Learning the language as you go is not too unusual with Evennia devs, but expect it
to add development time. You will also be worse at predicting how 'hard' something is to do.
- If you dont know Python, you should have gotten a few tastes from the first part of this tutorial. But
expect to have to refer to external online tutorials - there are many details of Python that will not be
covered.
### Asset creation
Compared to the level of work needed to produce professional graphics for an MMORPG, detailed text
assets for a mud are cheap to create. This is one of the many reasons muds are so well suited for a
small team.
This is not to say that making “professional” text content is easy though. Knowing how to write
imaginative and grammatically correct prose is only the minimal starting requirement. A good asset-
creator (traditionally called a “builder”) must also be able to utilize the tools of the game engine
to its fullest in order to script events, make quests, triggers and interactive, interesting
environments.
Assuming you are not coding all alone, your teams in-house builders will be the first ones to actually
“use” your game framework and build tools. They will stumble on all the bugs. This means that you
need people who are just not “artsy” or “good with words”. Assuming coders and builders are not the
same people (common for early testing), builders need to be able to collaborate well and give clear
and concise feedback.
If you know your builders are not tech-savvy, you may need to spend more time making easier
build-tools and commands for them.
## So, where do I begin, then?
Right, after all this soul-searching and skill-inventory-checking, lets go back to the original
question. And maybe youll find that you have a better feeling for the answer yourself already:
- Keep following this tutorial and spend the time
to really understand what is happening in the examples. Not only will this give you a better idea
of how parts hang together, it may also give you ideas for what is possible. Maybe something
is easier than you expected!
- Introduce yourself in the IRC/Discord chat and don't be shy to ask questions as you go through
the tutorial. Don't get hung up on trying to resolve something that a seasoned Evennia dev may
clear up for you in five minutes. Also, not all errors are your faults - it's possible the
tutorial is unclear or has bugs, asking will quickly bring those problems to light, if so.
- If Python is new to you, you should complement the tutorial with third-party Python references
so you can read, understand and replicate example code without being completely in the dark.
Once you are out of the starting tutorial, you'll be off to do your own thing.
- The starting tutorial cannot cover everything. Skim through the [Evennia docs](../../../index.md).
Even if you don't read everything, it gives you a feeling for what's available should you need
to look for something later. Make sure to use the search function.
- You can now start by expanding on the tutorial-game we will have created. In the last part there
there will be a list of possible future projects you could take on. Working on your own, without help
from a tutorial is the next step.
As for your builders, they can start getting familiar with Evennia's default build commands ... but
keep in mind that your game is not yet built! Don't set your builders off on creating large zone projects.
If they build anything at all, it should be small test areas to agree on a homogenous form, mood
and literary style.
## Conclusions
Remember that what kills a hobby game project will usually be your own lack of
motivation. So do whatever you can to keep that motivation burning strong! Even if it means
deviating from what you read in a tutorial like this one. Just get that game out there, whichever way
works best for you.
In the next lesson we'll go through some of the technical questions you need to consider. This should
hopefully help you figure out more about the game you want to make. In the lesson following that we'll
then try to answer those questions for the sake of creating our little tutorial game.

View file

@ -0,0 +1,802 @@
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)
# Making a sittable object
In this lesson we will go through how to make a chair you can sit on. Sounds easy, right?
Well it is. But in the process of making the chair we will need to consider the various ways
to do it depending on how we want our game to work.
The goals of this lesson are as follows:
- We want a new 'sittable' object, a Chair in particular".
- We want to be able to use a command to sit in the chair.
- Once we are sitting in the chair it should affect us somehow. To demonstrate this we'll
set a flag "Resting" on the Character sitting in the Chair.
- When you sit down you should not be able to walk to another room without first standing up.
- A character should be able to stand up and move away from the chair.
There are two main ways to design the commands for sitting and standing up.
- You can store the commands on the chair so they are only available when a chair is in the room
- You can store the commands on the Character so they are always available and you must always specify
which chair to sit on.
Both of these are very useful to know about, so in this lesson we'll try both. But first
we need to handle some basics.
## Don't move us when resting
When you are sitting in a chair you can't just walk off without first standing up.
This requires a change to our Character typeclass. Open `mygame/typeclasses/characters.py`:
```python
# ...
class Character(DefaultCharacter):
# ...
def at_pre_move(self, destination):
"""
Called by self.move_to when trying to move somewhere. If this returns
False, the move is immediately cancelled.
"""
if self.db.is_resting:
self.msg("You can't go anywhere while resting.")
return False
return True
```
When moving somewhere, [character.move_to](evennia.objects.objects.DefaultObject.move_to) is called. This in turn
will call `character.at_pre_move`. Here we look for an Attribute `is_resting` (which we will assign below)
to determine if we are stuck on the chair or not.
## Making the Chair itself
Next we need the Chair itself, or rather a whole family of "things you can sit on" that we will call
_sittables_. We can't just use a default Object since we want a sittable to contain some custom code. We need
a new, custom Typeclass. Create a new module `mygame/typeclasses/sittables.py` with the following content:
```python
from evennia import DefaultObject
class Sittable(DefaultObject):
def at_object_creation(self):
self.db.sitter = None
def do_sit(self, sitter):
"""
Called when trying to sit on/in this object.
Args:
sitter (Object): The one trying to sit down.
"""
current = self.db.sitter
if current:
if current == sitter:
sitter.msg("You are already sitting on {self.key}.")
else:
sitter.msg(f"You can't sit on {self.key} "
f"- {current.key} is already sitting there!")
return
self.db.sitting = sitter
sitter.db.is_resting = True
sitter.msg(f"You sit on {self.key}")
def do_stand(self, stander):
"""
Called when trying to stand from this object.
Args:
stander (Object): The one trying to stand up.
"""
current = self.db.sitter
if not stander == current:
stander.msg(f"You are not sitting on {self.key}.")
else:
self.db.sitting = None
stander.db.is_resting = False
stander.msg(f"You stand up from {self.key}")
```
Here we have a small Typeclass that handles someone trying to sit on it. It has two methods that we can simply
call from a Command later. We set the `is_resting` Attribute on the one sitting down.
One could imagine that one could have the future `sit` command check if someone is already sitting in the
chair instead. This would work too, but letting the `Sittable` class handle the logic around who can sit on it makes
logical sense.
We let the typeclass handle the logic, and also let it do all the return messaging. This makes it easy to churn out
a bunch of chairs for people to sit on. But it's not perfect. The `Sittable` class is general. What if you want to
make an armchair. You sit "in" an armchair rather than "on" it. We _could_ make a child class of `Sittable` named
`SittableIn` that makes this change, but that feels excessive. Instead we will make it so that Sittables can
modify this per-instance:
```python
from evennia import DefaultObject
class Sittable(DefaultObject):
def at_object_creation(self):
self.db.sitter = None
# do you sit "on" or "in" this object?
self.db.adjective = "on"
def do_sit(self, sitter):
"""
Called when trying to sit on/in this object.
Args:
sitter (Object): The one trying to sit down.
"""
adjective = self.db.adjective
current = self.db.sitter
if current:
if current == sitter:
sitter.msg(f"You are already sitting {adjective} {self.key}.")
else:
sitter.msg(
f"You can't sit {adjective} {self.key} "
f"- {current.key} is already sitting there!")
return
self.db.sitting = sitter
sitter.db.is_resting = True
sitter.msg(f"You sit {adjective} {self.key}")
def do_stand(self, stander):
"""
Called when trying to stand from this object.
Args:
stander (Object): The one trying to stand up.
"""
current = self.db.sitter
if not stander == current:
stander.msg(f"You are not sitting {self.db.adjective} {self.key}.")
else:
self.db.sitting = None
stander.db.is_resting = False
stander.msg(f"You stand up from {self.key}")
```
We added a new Attribute `adjective` which will probably usually be `in` or `on` but could also be `at` if you
want to be able to sit _at a desk_ for example. A regular builder would use it like this:
> create/drop armchair : sittables.Sittable
> set armchair/adjective = in
This is probably enough. But all those strings are hard-coded. What if we want some more dramatic flair when you
sit down?
You sit down and a whoopie cushion makes a loud fart noise!
For this we need to allow some further customization. Let's let the current strings be defaults that
we can replace.
```python
from evennia import DefaultObject
class Sittable(DefaultObject):
"""
An object one can sit on
Customizable Attributes:
adjective: How to sit (on, in, at etc)
Return messages (set as Attributes):
msg_already_sitting: Already sitting here
format tokens {adjective} and {key}
msg_other_sitting: Someone else is sitting here.
format tokens {adjective}, {key} and {other}
msg_sitting_down: Successfully sit down
format tokens {adjective}, {key}
msg_standing_fail: Fail to stand because not sitting.
format tokens {adjective}, {key}
msg_standing_up: Successfully stand up
format tokens {adjective}, {key}
"""
def at_object_creation(self):
self.db.sitter = None
# do you sit "on" or "in" this object?
self.db.adjective = "on"
def do_sit(self, sitter):
"""
Called when trying to sit on/in this object.
Args:
sitter (Object): The one trying to sit down.
"""
adjective = self.db.adjective
current = self.db.sitter
if current:
if current == sitter:
if self.db.msg_already_sitting:
sitter.msg(
self.db.msg_already_sitting.format(
adjective=self.db.adjective, key=self.key))
else:
sitter.msg(f"You are already sitting {adjective} {self.key}.")
else:
if self.db.msg_other_sitting:
sitter.msg(self.db.msg_already_sitting.format(
other=current.key, adjective=self.db.adjective, key=self.key))
else:
sitter.msg(f"You can't sit {adjective} {self.key} "
f"- {current.key} is already sitting there!")
return
self.db.sitting = sitter
sitter.db.is_resting = True
if self.db.msg_sitting_down:
sitter.msg(self.db.msg_sitting_down.format(adjective=adjective, key=self.key))
else:
sitter.msg(f"You sit {adjective} {self.key}")
def do_stand(self, stander):
"""
Called when trying to stand from this object.
Args:
stander (Object): The one trying to stand up.
"""
current = self.db.sitter
if not stander == current:
if self.db.msg_standing_fail:
stander.msg(self.db.msg_standing_fail.format(
adjective=self.db.adjective, key=self.key))
else:
stander.msg(f"You are not sitting {self.db.adjective} {self.key}")
else:
self.db.sitting = None
stander.db.is_resting = False
if self.db.msg_standing_up:
stander.msg(self.db.msg_standing_up.format(
adjective=self.db.adjective, key=self.key))
else:
stander.msg(f"You stand up from {self.key}")
```
Here we really went all out with flexibility. If you need this much is up to you.
We added a bunch of optional Attributes to hold alternative versions of all the messages.
There are some things to note:
- We don't actually initiate those Attributes in `at_object_creation`. This is a simple
optimization. The assumption is that _most_ chairs will probably not be this customized.
So initiating a bunch of Attributes to, say, empty strings would be a lot of useless database calls.
The drawback is that the available Attributes become less visible when reading the code. So we add a long
describing docstring to the end to explain all you can use.
- We use `.format` to inject formatting-tokens in the text. The good thing about such formatting
markers is that they are _optional_. They are there if you want them, but Python will not complain
if you don't include some or any of them. Let's see an example:
> reload # if you have new code
> create/drop armchair : sittables.Sittable
> set armchair/adjective = in
> set armchair/msg_sitting_down = As you sit down {adjective} {key}, life feels easier.
> set armchair/msg_standing_up = You stand up from {key}. Life resumes.
The `{key}` and `{adjective}` are examples of optional formatting markers. Whenever the message is
returned, the format-tokens within will be replaced with `armchair` and `in` respectively. Should we
rename the chair later, this will show in the messages automatically (since `{key}` will change).
We have no Command to use this chair yet. But we can try it out with `py`:
> py self.search("armchair").do_sit(self)
As you sit down in armchair, life feels easier.
> self.db.resting
True
> py self.search("armchair").do_stand(self)
You stand up from armchair. Life resumes
> self.db.resting
False
If you follow along and get a result like this, all seems to be working well!
## Command variant 1: Commands on the chair
This way to implement `sit` and `stand` puts new cmdsets on the Sittable itself.
As we've learned before, commands on objects are made available to others in the room.
This makes the command easy but instead adds some complexity in the management of the CmdSet.
This is how it will look if `armchair` is in the room:
> sit
As you sit down in armchair, life feels easier.
What happens if there are sittables `sofa` and `barstool` also in the room? Evennia will automatically
handle this for us and allow us to specify which one we want:
> sit
More than one match for 'sit' (please narrow target):
sit-1 (armchair)
sit-2 (sofa)
sit-3 (barstool)
> sit-1
As you sit down in armchair, life feels easier.
To keep things separate we'll make a new module `mygame/commands/sittables.py`:
```{sidebar} Separate Commands and Typeclasses?
You can organize these things as you like. If you wanted you could put the sit-command + cmdset
together with the `Sittable` typeclass in `mygame/typeclasses/sittables.py`. That has the advantage of
keeping everything related to sitting in one place. But there is also some organizational merit to
keeping all Commands in one place as we do here.
```
```python
from evennia import Command, CmdSet
class CmdSit(Command):
"""
Sit down.
"""
key = "sit"
def func(self):
self.obj.do_sit(self.caller)
class CmdStand(Command):
"""
Stand up.
"""
key = "stand"
def func(self):
self.obj.do_stand(self.caller)
class CmdSetSit(CmdSet):
priority = 1
def at_cmdset_creation(self):
self.add(CmdSit)
self.add(CmdStand)
```
As seen, the commands are nearly trivial. `self.obj` is the object to which we added the cmdset with this
Command (so for example a chair). We just call the `do_sit/stand` on that object and the `Sittable` will
do the rest.
Why that `priority = 1` on `CmdSetSit`? This makes same-named Commands from this cmdset merge with a bit higher
priority than Commands from the Character-cmdset. Why this is a good idea will become clear shortly.
We also need to make a change to our `Sittable` typeclass. Open `mygame/typeclasses/sittables.py`:
```python
from evennia import DefaultObject
from commands.sittables import CmdSetSit # <- new
class Sittable(DefaultObject):
"""
(docstring)
"""
def at_object_creation(self):
self.db.sitter = None
# do you sit "on" or "in" this object?
self.db.adjective = "on"
self.cmdset.add_default(CmdSetSit) # <- new
```
Any _new_ Sittables will now have your `sit` Command. Your existing `armchair` will not,
since `at_object_creation` will not re-run for already existing objects. We can update it manually:
> reload
> update armchair
We could also update all existing sittables (all on one line):
> py from typeclasses.sittables import Sittable ;
[sittable.at_object_creation() for sittable in Sittable.objects.all()]
> The above shows an example of a _list comprehension_. Think of it as an efficient way to construct a new list
all in one line. You can read more about list comprehensions
[here in the Python docs](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions).
We should now be able to use `sit` while in the room with the armchair.
> sit
As you sit down in armchair, life feels easier.
> stand
You stand up from armchair.
One issue with placing the `sit` (or `stand`) Command "on" the chair is that it will not be available when in a
room without a Sittable object:
> sit
Command 'sit' is not available. ...
This is practical but not so good-looking; it makes it harder for the user to know a `sit` action is at all
possible. Here is a trick for fixing this. Let's add _another_ Command to the bottom
of `mygame/commands/sittables.py`:
```python
# ...
class CmdNoSitStand(Command):
"""
Sit down or Stand up
"""
key = "sit"
aliases = ["stand"]
def func(self):
if self.cmdname == "sit":
self.msg("You have nothing to sit on.")
else:
self.msg("You are not sitting down.")
```
Here we have a Command that is actually two - it will answer to both `sit` and `stand` since we
added `stand` to its `aliases`. In the command we look at `self.cmdname`, which is the string
_actually used_ to call this command. We use this to return different messages.
We don't need a separate CmdSet for this, instead we will add this
to the default Character cmdset. Open `mygame/commands/default_cmdsets.py`:
```python
# ...
from commands import sittables
class CharacterCmdSet(CmdSet):
"""
(docstring)
"""
def at_cmdset_creation(self):
# ...
self.add(sittables.CmdNoSitStand)
```
To test we'll build a new location without any comfy armchairs and go there:
> reload
> tunnel n = kitchen
north
> sit
You have nothing to sit on.
> south
sit
As you sit down in armchair, life feels easier.
We now have a fully functioning `sit` action that is contained with the chair itself. When no chair is around, a
default error message is shown.
How does this work? There are two cmdsets at play, both of which have a `sit` Command. As you may remember we
set the chair's cmdset to `priority = 1`. This is where that matters. The default Character cmdset has a
priority of 0. This means that whenever we enter a room with a Sittable thing, the `sit` command
from _its_ cmdset will take _precedence_ over the Character cmdset's version. So we are actually picking
_different_ `sit` commands depending on circumstance! The user will never be the wiser.
So this handles `sit`. What about `stand`? That will work just fine:
> stand
You stand up from armchair.
> north
> stand
You are not sitting down.
We have one remaining problem with `stand` though - what happens when you are sitting down and try to
`stand` in a room with more than one chair:
> stand
More than one match for 'stand' (please narrow target):
stand-1 (armchair)
stand-2 (sofa)
stand-3 (barstool)
Since all the sittables have the `stand` Command on them, you'll get a multi-match error. This _works_ ... but
you could pick _any_ of those sittables to "stand up from". That's really weird and non-intuitive. With `sit` it
was okay to get a choice - Evennia can't know which chair we intended to sit on. But we know which chair we
sit on so we should only get _its_ `stand` command.
We will fix this with a `lock` and a custom `lock function`. We want a lock on the `stand` Command that only
makes it available when the caller is actually sitting on the chair the `stand` command is on.
First let's add the lock so we see what we want. Open `mygame/commands/sittables.py`:
```python
# ...
class CmdStand(Command):
"""
Stand up.
"""
key = "stand"
lock = "cmd:sitsonthis()" # < this is new
def func(self):
self.obj.do_stand(self.caller)
# ...
```
We define a [Lock](../../../Components/Locks.md) on the command. The `cmd:` is in what situation Evennia will check
the lock. The `cmd` means that it will check the lock when determining if a user has access to this command or not.
What will be checked is the `sitsonthis` _lock function_ which doesn't exist yet.
Open `mygame/server/conf/lockfuncs.py` to add it!
```python
"""
(module lockstring)
"""
# ...
def sitsonthis(accessing_obj, accessed_obj, *args, **kwargs):
"""
True if accessing_obj is sitting on/in the accessed_obj.
"""
return accessed_obj.db.sitting == accessing_obj
# ...
```
Evennia knows that all functions in `mygame/server/conf/lockfuncs` should be possible to use in a lock definition.
The arguments are required and Evennia will pass all relevant objects to them:
```{sidebar} Lockfuncs
Evennia provides a large number of default lockfuncs, such as checking permission-levels,
if you are carrying or are inside the accessed object etc. There is no concept of 'sitting'
in default Evennia however, so this we need to specify ourselves.
```
- `accessing_obj` is the one trying to access the lock. So us, in this case.
- `accessed_obj` is the entity we are trying to gain a particular type of access to. So the chair.
- `args` is a tuple holding any arguments passed to the lockfunc. Since we use `sitsondthis()` this will
be empty (and if we add anything, it will be ignored).
- `kwargs` is a tuple of keyword arguments passed to the lockfuncs. This will be empty as well in our example.
If you are superuser, it's important that you `quell` yourself before trying this out. This is because the superuser
bypasses all locks - it can never get locked out, but it means it will also not see the effects of a lock like this.
> reload
> quell
> stand
You stand up from armchair
None of the other sittables' `stand` commands passed the lock and only the one we are actually sitting on did.
Adding a Command to the chair object like this is powerful and a good technique to know. It does come with some
caveats though that one needs to keep in mind.
We'll now try another way to add the `sit/stand` commands.
## Command variant 2: Command on Character
Before we start with this, delete the chairs you've created (`del armchair` etc) and then do the following
changes:
- In `mygame/typeclasses/sittables.py`, comment out the line `self.cmdset.add_default(CmdSetSit)`.
- In `mygame/commands/default_cmdsets.py`, comment out the line `self.add(sittables.CmdNoSitStand)`.
This disables the on-object command solution so we can try an alternative. Make sure to `reload` so the
changes are known to Evennia.
In this variation we will put the `sit` and `stand` commands on the `Character` instead of on the chair. This
makes some things easier, but makes the Commands themselves more complex because they will not know which
chair to sit on. We can't just do `sit` anymore. This is how it will work.
> sit <chair>
You sit on chair.
> stand
You stand up from chair.
Open `mygame/commands.sittables.py` again. We'll add a new sit-command. We name the class `CmdSit2` since
we already have `CmdSit` from the previous example. We put everything at the end of the module to
keep it separate.
```python
from evennia import Command, CmdSet
from evennia import InterruptCommand # <- this is new
class CmdSit(Command):
# ...
# ...
# new from here
class CmdSit2(Command):
"""
Sit down.
Usage:
sit <sittable>
"""
key = "sit"
def parse(self):
self.args = self.args.strip()
if not self.args:
self.caller.msg("Sit on what?")
raise InterruptCommand
def func(self):
# self.search handles all error messages etc.
sittable = self.caller.search(self.args)
if not sittable:
return
try:
sittable.do_sit(self.caller)
except AttributeError:
self.caller.msg("You can't sit on that!")
```
With this Command-variation we need to search for the sittable. A series of methods on the Command
are run in sequence:
1. `Command.at_pre_command` - this is not used by default
2. `Command.parse` - this should parse the input
3. `Command.func` - this should implement the actual Command functionality
4. `Command.at_post_func` - this is not used by default
So if we just `return` in `.parse`, `.func` will still run, which is not what we want. To immediately
abort this sequence we need to `raise InterruptCommand`.
```{sidebar} Raising exceptions
Raising an exception allows for immediately interrupting the current program flow. Python
automatically raises error-exceptions when detecting problems with the code. It will be
raised up through the sequence of called code (the 'stack') until it's either `caught` with
a `try ... except` or reaches the outermost scope where it'll be logged or displayed.
```
`InterruptCommand` is an _exception_ that the Command-system catches with the understanding that we want
to do a clean abort. In the `.parse` method we strip any whitespaces from the argument and
sure there actuall _is_ an argument. We abort immediately if there isn't.
We we get to `.func` at all, we know that we have an argument. We search for this and abort if we there was
a problem finding the target.
> We could have done `raise InterruptCommand` in `.func` as well, but `return` is a little shorter to write
> and there is no harm done if `at_post_func` runs since it's empty.
Next we call the found sittable's `do_sit` method. Note that we wrap this call like this:
```python
try:
# code
except AttributeError:
# stuff to do if AttributeError exception was raised
```
The reason is that `caller.search` has no idea we are looking for a Sittable. The user could have tried
`sit wall` or `sit sword`. These don't have a `do_sit` method _but we call it anyway and handle the error_.
This is a very "Pythonic" thing to do. The concept is often called "leap before you look" or "it's easier to
ask for forgiveness than for permission". If `sittable.do_sit` does not exist, Python will raise an `AttributeError`.
We catch this with `try ... except AttributeError` and convert it to a proper error message.
While it's useful to learn about `try ... except`, there is also a way to leverage Evennia to do this without
`try ... except`:
```python
# ...
def func(self):
# self.search handles all error messages etc.
sittable = self.caller.search(
self.args,
typeclass="typeclasses.sittables.Sittable")
if not sittable:
return
sittable.do_sit(self.caller)
```
```{sidebar} Continuing across multiple lines
Note how the `.search()` method's arguments are spread out over multiple
lines. This works for all lists, tuples and other listings and is
a good way to avoid very long and hard-to-read lines.
```
The `caller.search` method has an keyword argument `typeclass` that can take either a python-path to a
typeclass, the typeclass itself, or a list of either to widen the allowed options. In this case we know
for sure that the `sittable` we get is actually a `Sittable` class and we can call `sittable.do_sit` without
needing to worry about catching errors.
Let's do the `stand` command while we are at it. Again, since the Command is external to the chair we don't
know which object we are sitting in and have to search for it.
```python
class CmdStand2(Command):
"""
Stand up.
Usage:
stand
"""
key = "stand"
def func(self):
caller = self.caller
# find the thing we are sitting on/in, by finding the object
# in the current location that as an Attribute "sitter" set
# to the caller
sittable = caller.search(
caller,
candidates=caller.location.contents,
attribute_name="sitter",
typeclass="typeclasses.sittables.Sittable")
# if this is None, the error was already reported to user
if not sittable:
return
sittable.do_stand(caller)
```
This forced us to to use the full power of the `caller.search` method. If we wanted to search for something
more complex we would likely need to break out a [Django query](../Part1/Django-queries.md) to do it. The key here is that
we know that the object we are looking for is a `Sittable` and that it must have an Attribute named `sitter`
which should be set to us, the one sitting on/in the thing. Once we have that we just call `.do_stand` on it
and let the Typeclass handle the rest.
All that is left now is to make this available to us. This type of Command should be available to us all the time
so we can put it in the default Cmdset` on the Character. Open `mygame/default_cmdsets.py`
```python
# ...
from commands import sittables
class CharacterCmdSet(CmdSet):
"""
(docstring)
"""
def at_cmdset_creation(self):
# ...
self.add(sittables.CmdSit2)
self.add(sittables.CmdStand2)
```
Now let's try it out:
> reload
> create/drop sofa : sittables.Sittable
> sit sofa
You sit down on sofa.
> stand
You stand up from sofa.
## Conclusions
In this lesson we accomplished quite a bit:
- We modified our `Character` class to avoid moving when sitting down.
- We made a new `Sittable` typeclass
- We tried two ways to allow a user to interact with sittables using `sit` and `stand` commands.
Eagle-eyed readers will notice that the `stand` command sitting "on" the chair (variant 1) would work just fine
together with the `sit` command sitting "on" the Character (variant 2). There is nothing stopping you from
mixing them, or even try a third solution that better fits what you have in mind.
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)

View file

@ -0,0 +1,63 @@
# Part 3: How we get there
```{eval-rst}
.. sidebar:: Beginner Tutorial Parts
`Introduction <../Beginner-Tutorial-Intro.html>`_
Getting set up.
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
A tour of Evennia and how to use the tools, including an introduction to Python.
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
Planning our tutorial game and what to think about when planning your own in the future.
**Part 3: How we get there**
Getting down to the meat of extending Evennia to make our game
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
Building a tech-demo and world content to go with our code
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
Taking our new game online and let players try it out
```
In part three of the Evennia Beginner tutorial we will go through the creation of several key parts of our tutorial
game _EvAdventure_. This is a pretty big part with plenty of examples.
If you followed the previous parts of this tutorial you will have some notions about Python and where to find
and make use of things in Evennia. We also have a good idea of the type of game we want.
Even if this is not the game-style you are interested in, following along will give you a lot of experience
with using Evennia. This be of much use when doing your own thing later.
## Lessons
_TODO_
```{toctree}
:maxdepth: 1
Implementing-a-game-rule-system
Turn-based-Combat-System
A-Sittable-Object
```
1. [Changing settings](../../../Unimplemented.md)
1. [Applying contribs](../../../Unimplemented.md)
1. [Creating a rule module](../../../Unimplemented.md)
1. [Tweaking the base Typeclasses](../../../Unimplemented.md)
1. [Character creation menu](../../../Unimplemented.md)
1. [Wearing armor and wielding weapons](../../../Unimplemented.md)
1. [Two types of combat](../../../Unimplemented.md)
1. [Monsters and AI](../../../Unimplemented.md)
1. [Questing and rewards](../../../Unimplemented.md)
1. [Overview of Tech demo](../../../Unimplemented.md)
## Table of Contents
_TODO_
```{toctree}
:maxdepth: 1
Implementing-a-game-rule-system
Turn-Based-Combat-System
A-Sittable-Object
```

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](../../../Components/Attributes.md), 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](../../../Concepts/New-Models.md). Which is the better depends on your game and the
complexity of your system.
- Make a clear [API](https://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(f"You are now level {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_template = "You are hit by {attacker} for {dmg} damage!"
wintext_template = "You hit {target} for {dmg} damage!"
xp_gain = randint(1, 3)
if char1.db.combat >= roll1 > roll2:
# char 1 hits
dmg = roll_dmg() + char1.db.STR
char1.msg(wintext_template.format(target=char2, dmg=dmg))
add_XP(char1, xp_gain)
char2.msg(failtext_template.format(attacker=char1, dmg=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_template.format(attacker=char2, dmg=dmg))
char1.db.HP -= dmg
check_defeat(char1)
char2.msg(wintext_template.format(target=char1, dmg=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(f"Skillname {skillname} not found.")
```
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,521 @@
# 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](../../../Components/Commands.md), possibly ones with a [cooldown](../../Command-Cooldown.md). You also need a [game rule
module](./Implementing-a-game-rule-system.md) 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](../../../Components/Scripts.md) 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](../../../Components/Command-Sets.md) 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.md). We will only sketch such a module here for our end-turn
combat resolution.
- An `attack` [command](../../../Components/Commands.md) 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](../../../Components/Scripts.md). 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](../../../Components/TickerHandler.md). 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 = f"combat_handler_{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:
messages.append(
f"{char} tries to hit {tchar}, but {tchar} parries the attack!"
)
elif taction == "defend" and random.random() < 0.5:
messages.append(
f"{tchar} defends against the attack by {char}."
)
elif taction == "flee":
flee[tchar] = -2
messages.append(
f"{char} stops {tchar} from disengaging, with a hit!"
)
else:
messages.append(
f"{char} hits {tchar}, bypassing their {taction}!"
)
elif action == "parry":
if taction == "hit":
messages.append(f"{char} parries the attack by {tchar}.")
elif taction == "feint":
messages.append(
f"{char} tries to parry, but {tchar} feints and hits!"
)
else:
messages.append(f"{char} parries to no avail.")
elif action == "feint":
if taction == "parry":
messages.append(
f"{char} feints past {tchar}'s parry, landing a hit!"
)
elif taction == "hit":
messages.append(f"{char} feints but is defeated by {tchar}'s hit!")
else:
messages.append(f"{char} feints to no avail.")
elif action == "defend":
messages.append(f"{char} defends.")
elif action == "flee":
if char in flee:
flee[char] += 1
else:
flee[char] = 1
messages.append(
f"{char} tries to disengage (two subsequent turns needed)"
)
# echo results of each subturn
combat_handler.msg_all("\n".join(messages))
# at the end of both sub-turns, test if anyone fled
for (char, fleevalue) in flee.items():
if fleevalue == 2:
combat_handler.msg_all(f"{char} withdraws from combat.")
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(f"{self.caller} joins combat!")
else:
# create a new combat handler
chandler = create_script("combat_handler.CombatHandler")
chandler.add_character(self.caller)
chandler.add_character(target)
self.caller.msg(f"You attack {target}! You are in combat.")
target.msg(f"{self.caller} attacks you! You are in combat.")
```
The `attack` command will not go into the combat cmdset but rather into the default cmdset. See e.g.
the [Adding Command Tutorial](../Part1/Adding-Commands.md) 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,42 @@
# Part 4: Using what we created
```{eval-rst}
..sidebar:: Beginner Tutorial Parts
`Introduction <../Beginner-Tutorial-Intro.html>`_
Getting set up.
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
A tour of Evennia and how to use the tools, including an introduction to Python.
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
Planning our tutorial game and what to think about when planning your own in the future.
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
Getting down to the meat of extending Evennia to make our game to make a tech-demo
**Part 4: Using what we created**
Using the tech-demo and world content to go with our code
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
Taking our new game online and let players try it out
```
We now have the code underpinnings of everything we need. We have also tested the various components
and has a simple tech-demo to show it all works together. But there is no real coherence to it at this
point - we need to actually make a world.
In part four we will expand our tech demo into a more full-fledged (if small) game by use of batchcommand
and batchcode processors.
## Lessons
_TODO_
```{toctree}
:maxdepth: 1
```
## Table of Contents
_TODO_
```{toctree}
```

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,46 @@
# Part 5: Showing the world
```{eval-rst}
.. sidebar:: Beginner Tutorial Parts
`Introduction <../Beginner-Tutorial-Intro.html>`_
Getting set up.
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
A tour of Evennia and how to use the tools, including an introduction to Python.
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
Planning our tutorial game and what to think about when planning your own in the future.
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
Getting down to the meat of extending Evennia to make our game
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
Building a tech-demo and world content to go with our code
**Part 5: Showing the world**
Taking our new game online and let players try it out
```
You have a working game! In part five we will look at the web-components of Evennia and how to modify them
to fit your game. We will also look at hosting your game and if you feel up to it we'll also go through how
to bring your game online so you can invite your first players.
## Lessons
_TODO_
```{toctree}
:maxdepth: 1
Add-a-simple-new-web-page.md
Web-Tutorial.md
```
## Table of Contents
_TODO_
```{toctree}
Add-a-simple-new-web-page.md
Web-Tutorial.md
```

View file

@ -0,0 +1,126 @@
# 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.md) 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](https://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](https://en.wikipedia.org/wiki/Html) for the user, and a `static` folder that holds assets
like [CSS](https://en.wikipedia.org/wiki/CSS), [Javascript](https://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](../../../Concepts/New-Models.md) 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.md). 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.md) 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,237 @@
# 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](../Components/Accounts.md) represents the real person logging in and has no game-world existence.
- Any [Object](../Components/Objects.md) can be puppeted by an Account (with proper permissions).
- [Characters](../Components/Objects.md#characters), [Rooms](../Components/Objects.md#rooms), and [Exits](../Components/Objects.md#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:
location.msg_contents(
f"BOOM! The mech fires its gun at {target.key}"
)
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](../Components/Command-Sets.md) (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](../Components/Typeclasses.md) 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, persistent=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,377 @@
# 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.md#removing-default-commands)
- [Preventing character from moving based on a condition](./Coding-FAQ.md#preventing-character-from-
moving-based-on-a-condition)
- [Reference initiating object in an EvMenu command](./Coding-FAQ.md#reference-initiating-object-in-an-
evmenu-command)
- [Adding color to default Evennia Channels](./Coding-FAQ.md#adding-color-to-default-evennia-channels)
- [Selectively turn off commands in a room](./Coding-FAQ.md#selectively-turn-off-commands-in-a-room)
- [Select Command based on a condition](./Coding-FAQ.md#select-command-based-on-a-condition)
- [Automatically updating code when reloading](./Coding-FAQ.md#automatically-updating-code-when-
reloading)
- [Changing all exit messages](./Coding-FAQ.md#changing-all-exit-messages)
- [Add parsing with the "to" delimiter](./Coding-FAQ.md#add-parsing-with-the-to-delimiter)
- [Store last used session IP address](./Coding-FAQ.md#store-last-used-session-ip-address)
- [Use wide characters with EvTable](./Coding-FAQ.md#non-latin-characters-in-evtable)
## Removing default commands
**Q:** How does one *remove* (not replace) e.g. the default `get` [Command](../Components/Commands.md) from the
Character [Command Set](../Components/Command-Sets.md)?
**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](Beginner-Tutorial/Part1/Adding-Commands.md)
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_pre_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](../Components/Attributes.md) `cantmove`.
Add the following code to the `Character` class:
```python
def at_pre_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](../Components/EvMenu.md) 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):
if self.key in COLORS:
p_str = CHANNEL_COLORS.get(self.key.lower())
else:
p_str = self.key.capitalize()
return f"[{p_str}] "
```
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](../Components/Locks.md). 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, persistent=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](../Components/Locks.md) 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](Beginner-Tutorial/Part1/Adding-Commands.md). The `is_full_moon` [lock
function](../Components/Locks.md#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.md#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](../Components/Attributes.md), 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](../Components/Commands.md) 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](../Concepts/Async-Process.md#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(f"You shout '{self.args}' and wait for an echo ...")
# 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
self.caller.msg(
f"You hear an echo: {shout.upper()} ... {shout.capitalize()} ... {shout.lower()}"
)
```
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](https://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(f"You shout '{self.args}', waiting for an echo ...")
# wait 2 seconds before calling self.echo1
utils.delay(2, self.echo1)
# callback chain, started above
def echo1(self):
"First echo"
self.caller.msg(f"... {self.args.upper()}")
# wait 2 seconds for the next one
utils.delay(2, self.echo2)
def echo2(self):
"Second echo"
self.caller.msg(f"... {self.args.capitalize()}")
# wait another 2 seconds
utils.delay(2, callback=self.echo3)
def echo3(self):
"Last echo"
self.caller.msg(f"... {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.md) 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
caller.msg(
f"You hear an echo: {shout.upper()} ... {shout.capitalize()} ... {shout.lower()}"
)
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(f"You shout '{self.args}' and wait for an echo ...")
# 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,125 @@
# 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](../Concepts/OOB.md)-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 = f"You diagnose {target} as having {hp} health, {mp} mana and {sp} stamina."
prompt = f"{hp} HP, {mp} MP, {sp} 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 = f"{caller.db.hp} HP, {caller.db.mp} MP, {caller.db.sp} 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](../Components/Tags.md).
> 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,130 @@
# 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](../Components/Objects.md) and
an [Exit Command](../Components/Commands.md) stored on said exit object. The command has the same key and aliases as the
exit-object, which is why you can see the exit in the room and just write its name to traverse it.
So if you try to enter the name of a non-existing exit, Evennia treats is the same way as if you were trying to
use a non-existing command:
> 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.
> west
You cannot move west.
## Adding default error commands
The way to do this is to give Evennia an _alternative_ Command to use when no Exit-Command is found
in the room. See [Adding Commands](Beginner-Tutorial/Part1/Adding-Commands.md) for more info about the
process of adding new Commands to Evennia.
In this example all we'll do is echo an error message.
```python
# for example in a file mygame/commands/movecommands.py
from evennia import default_cmds, CmdSet
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 error based on key"""
self.caller.msg(f"You cannot move {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"]
# you could add each command on its own to the default cmdset,
# but putting them all in a cmdset here allows you to
# just add this and makes it easier to expand with more
# exit-errors in the future
class MovementFailCmdSet(CmdSet):
def at_cmdset_creation(self):
self.add(CmdExitErrorNorth())
self.add(CmdExitErrorEast())
self.add(CmdExitErrorWest())
self.add(CmdExitErrorSouth())
```
We pack our commands in a new little cmdset; if we add this to our
`CharacterCmdSet`, we can just add more errors to `MovementFailCmdSet`
later without having to change code in two places.
```python
# in mygame/commands/default_cmdsets.py
from commands import movecommands
# [...]
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# [...]
def at_cmdset_creation(self):
# [...]
# this adds all the commands at once
self.add(movecommands.MovementFailCmdSet)
```
`reload` the server. 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 a direction without
having a matching exit for it, you will fall back 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](../Components/Typeclasses.md) directly.
## Why not a single command?
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 reason is that this would *not* work. Understanding why is important.
Evennia's [command system](../Components/Commands.md) compares commands by key and/or aliases. If _any_ key or alias
match, the two commands are considered _identical_. When the cmdsets merge, priority will then decide which of these
'identical' commandss replace which.
So the above example would work fine as long as there were _no Exits at all_ in the room. But when we enter
a room with an exit "north", its Exit-command (which has a higher priority) will override the single `CmdExitError`
with its alias 'north'. So the `CmdExitError` will be gone and while "north" will work, we'll again get the normal
"Command not recognized" error for the other directions.

View file

@ -0,0 +1,494 @@
# Dynamic In Game Map
## Introduction
An often desired feature in a MUD is to show an in-game map to help navigation. The [Static in-game
map](../Contribs/Contrib-Mapbuilder.md) tutorial solves this by creating a *static* map, meaning the map is pre-
drawn once and for all - the rooms are then created to match that map. When walking around, parts of
the static map is then cut out and displayed next to the room description.
In this tutorial we'll instead do it the other way around; We will dynamically draw the map based on
the relationships we find between already existing rooms.
## The Grid of Rooms
There are at least two requirements needed for this tutorial to work.
1. The structure of your mud has to follow a logical layout. Evennia supports the layout of your
world to be 'logically' impossible with rooms looping to themselves or exits leading to the other
side of the map. Exits can also be named anything, from "jumping out the window" to "into the fifth
dimension". This tutorial assumes you can only move in the cardinal directions (N, E, S and W).
2. Rooms must be connected and linked together for the map to be generated correctly. Vanilla
Evennia comes with a admin command [tunnel](evennia.commands.default.building.CmdTunnel) that allows a
user to create rooms in the cardinal directions, but additional work is needed to assure that rooms
are connected. For example, if you `tunnel east` and then immediately do `tunnel west` you'll find
that you have created two completely stand-alone rooms. So care is needed if you want to create a
"logical" layout. In this tutorial we assume you have such a grid of rooms that we can generate the
map from.
## Concept
Before getting into the code, it is beneficial to understand and conceptualize how this is going to
work. The idea is analogous to a worm that starts at your current position. It chooses a direction
and 'walks' outward from it, mapping its route as it goes. Once it has traveled a pre-set distance
it stops and starts over in another direction. An important note is that we want a system which is
easily callable and not too complicated. Therefore we will wrap this entire code into a custom
Python class (not a typeclass as this doesn't use any core objects from evennia itself).
We are going to create something that displays like this when you type 'look':
```
Hallway
[.] [.]
[@][.][.][.][.]
[.] [.] [.]
The distant echoes of the forgotten
wail throughout the empty halls.
Exits: North, East, South
```
Your current location is defined by `[@]` while the `[.]`s are other rooms that the "worm" has seen
since departing from your location.
## Setting up the Map Display
First we must define the components for displaying the map. For the "worm" to know what symbol to
draw on the map we will have it check an Attribute on the room it visits called `sector_type`. For
this tutorial we understand two symbols - a normal room and the room with us in it. We also define a
fallback symbol for rooms without said Attribute - that way the map will still work even if we
didn't prepare the room correctly. Assuming your game folder is named `mygame`, we create this code
in `mygame/world/map.py.`
```python
# in mygame/world/map.py
# the symbol is identified with a key "sector_type" on the
# Room. Keys None and "you" must always exist.
SYMBOLS = { None : ' . ', # for rooms without sector_type Attribute
'you' : '[@]',
'SECT_INSIDE': '[.]' }
```
Since trying to access an unset Attribute returns `None`, this means rooms without the `sector_type`
Atttribute will show as ` . `. Next we start building the custom class `Map`. It will hold all
methods we need.
```python
# in mygame/world/map.py
class Map(object):
def __init__(self, caller, max_width=9, max_length=9):
self.caller = caller
self.max_width = max_width
self.max_length = max_length
self.worm_has_mapped = {}
self.curX = None
self.curY = None
```
- `self.caller` is normally your Character object, the one using the map.
- `self.max_width/length` determine the max width and length of the map that will be generated. Note
that it's important that these variables are set to *odd* numbers to make sure the display area has
a center point.
- ` self.worm_has_mapped` is building off the worm analogy above. This dictionary will store all
rooms the "worm" has mapped as well as its relative position within the grid. This is the most
important variable as it acts as a 'checker' and 'address book' that is able to tell us where the
worm has been and what it has mapped so far.
- `self.curX/Y` are coordinates representing the worm's current location on the grid.
Before any sort of mapping can actually be done we need to create an empty display area and do some
sanity checks on it by using the following methods.
```python
# in mygame/world/map.py
class Map(object):
# [... continued]
def create_grid(self):
# This method simply creates an empty grid/display area
# with the specified variables from __init__(self):
board = []
for row in range(self.max_width):
board.append([])
for column in range(self.max_length):
board[row].append(' ')
return board
def check_grid(self):
# this method simply checks the grid to make sure
# that both max_l and max_w are odd numbers.
return True if self.max_length % 2 != 0 or self.max_width % 2 != 0\
else False
```
Before we can set our worm on its way, we need to know some of the computer science behind all this
called 'Graph Traversing'. In Pseudo code what we are trying to accomplish is this:
```python
# pseudo code
def draw_room_on_map(room, max_distance):
self.draw(room)
if max_distance == 0:
return
for exit in room.exits:
if self.has_drawn(exit.destination):
# skip drawing if we already visited the destination
continue
else:
# first time here!
self.draw_room_on_map(exit.destination, max_distance - 1)
```
The beauty of Python is that our actual code of doing this doesn't differ much if at all from this
Pseudo code example.
- `max_distance` is a variable indicating to our Worm how many rooms AWAY from your current location
will it map. Obviously the larger the number the more time it will take if your current location has
many many rooms around you.
The first hurdle here is what value to use for 'max_distance'. There is no reason for the worm to
travel further than what is actually displayed to you. For example, if your current location is
placed in the center of a display area of size `max_length = max_width = 9`, then the worm need only
go `4` spaces in either direction:
```
[.][.][.][.][@][.][.][.][.]
4 3 2 1 0 1 2 3 4
```
The `max_distance` can be set dynamically based on the size of the display area. As your width/length
changes it becomes a simple algebraic linear relationship which is simply `max_distance =
(min(max_width, max_length) -1) / 2`.
## Building the Mapper
Now we can start to fill our Map object with some methods. We are still missing a few methods that
are very important:
* `self.draw(self, room)` - responsible for actually drawing room to grid.
* `self.has_drawn(self, room)` - checks to see if the room has been mapped and worm has already been
here.
* `self.median(self, number)` - a simple utility method that finds the median (middle point) from 0,
n
* `self.update_pos(self, room, exit_name)` - updates the worm's physical position by reassigning
self.curX/Y. .accordingly
* `self.start_loc_on_grid(self)` - the very first initial draw on the grid representing your
location in the middle of the grid
* `self.show_map` - after everything is done convert the map into a readable string
* `self.draw_room_on_map(self, room, max_distance)` - the main method that ties it all together.
Now that we know which methods we need, let's refine our initial `__init__(self)` to pass some
conditional statements and set it up to start building the display.
```python
#mygame/world/map.py
class Map(object):
def __init__(self, caller, max_width=9, max_length=9):
self.caller = caller
self.max_width = max_width
self.max_length = max_length
self.worm_has_mapped = {}
self.curX = None
self.curY = None
if self.check_grid():
# we have to store the grid into a variable
self.grid = self.create_grid()
# we use the algebraic relationship
self.draw_room_on_map(caller.location,
((min(max_width, max_length) -1 ) / 2)
```
Here we check to see if the parameters for the grid are okay, then we create an empty canvas and map
our initial location as the first room!
As mentioned above, the code for the `self.draw_room_on_map()` is not much different than the Pseudo
code. The method is shown below:
```python
# in mygame/world/map.py, in the Map class
def draw_room_on_map(self, room, max_distance):
self.draw(room)
if max_distance == 0:
return
for exit in room.exits:
if exit.name not in ("north", "east", "west", "south"):
# we only map in the cardinal directions. Mapping up/down would be
# an interesting learning project for someone who wanted to try it.
continue
if self.has_drawn(exit.destination):
# we've been to the destination already, skip ahead.
continue
self.update_pos(room, exit.name.lower())
self.draw_room_on_map(exit.destination, max_distance - 1)
```
The first thing the "worm" does is to draw your current location in `self.draw`. Lets define that...
```python
#in mygame/word/map.py, in the Map class
def draw(self, room):
# draw initial ch location on map first!
if room == self.caller.location:
self.start_loc_on_grid()
self.worm_has_mapped[room] = [self.curX, self.curY]
else:
# map all other rooms
self.worm_has_mapped[room] = [self.curX, self.curY]
# this will use the sector_type Attribute or None if not set.
self.grid[self.curX][self.curY] = SYMBOLS[room.db.sector_type]
```
In `self.start_loc_on_grid()`:
```python
def median(self, num):
lst = sorted(range(0, num))
n = len(lst)
m = n -1
return (lst[n//2] + lst[m//2]) / 2.0
def start_loc_on_grid(self):
x = self.median(self.max_width)
y = self.median(self.max_length)
# x and y are floats by default, can't index lists with float types
x, y = int(x), int(y)
self.grid[x][y] = SYMBOLS['you']
self.curX, self.curY = x, y # updating worms current location
```
After the system has drawn the current map it checks to see if the `max_distance` is `0` (since this
is the inital start phase it is not). Now we handle the iteration once we have each individual exit
in the room. The first thing it does is check if the room the Worm is in has been mapped already..
lets define that...
```python
def has_drawn(self, room):
return True if room in self.worm_has_mapped.keys() else False
```
If `has_drawn` returns `False` that means the worm has found a room that hasn't been mapped yet. It
will then 'move' there. The self.curX/Y sort of lags behind, so we have to make sure to track the
position of the worm; we do this in `self.update_pos()` below.
```python
def update_pos(self, room, exit_name):
# this ensures the coordinates stays up to date
# to where the worm is currently at.
self.curX, self.curY = \
self.worm_has_mapped[room][0], self.worm_has_mapped[room][1]
# now we have to actually move the pointer
# variables depending on which 'exit' it found
if exit_name == 'east':
self.curY += 1
elif exit_name == 'west':
self.curY -= 1
elif exit_name == 'north':
self.curX -= 1
elif exit_name == 'south':
self.curX += 1
```
Once the system updates the position of the worm it feeds the new room back into the original
`draw_room_on_map()` and starts the process all over again..
That is essentially the entire thing. The final method is to bring it all together and make a nice
presentational string out of it using the `self.show_map()` method.
```python
def show_map(self):
map_string = ""
for row in self.grid:
map_string += " ".join(row)
map_string += "\n"
return map_string
```
## Using the Map
In order for the map to get triggered we store it on the Room typeclass. If we put it in
`return_appearance` we will get the map back every time we look at the room.
> `return_appearance` is a default Evennia hook available on all objects; it is called e.g. by the
`look` command to get the description of something (the room in this case).
```python
# in mygame/typeclasses/rooms.py
from evennia import DefaultRoom
from world.map import Map
class Room(DefaultRoom):
def return_appearance(self, looker):
# [...]
string = f"{Map(looker).show_map()}\n"
# Add all the normal stuff like room description,
# contents, exits etc.
string += "\n" + super().return_appearance(looker)
return string
```
Obviously this method of generating maps doesn't take into account of any doors or exits that are
hidden.. etc.. but hopefully it serves as a good base to start with. Like previously mentioned, it
is very important to have a solid foundation on rooms before implementing this. You can try this on
vanilla evennia by using @tunnel and essentially you can just create a long straight/edgy non-
looping rooms that will show on your in-game map.
The above example will display the map above the room description. You could also use an
[EvTable](github:evennia.utils.evtable) to place description and map next to each other. Some other
things you can do is to have a [Command](../Components/Commands.md) that displays with a larger radius, maybe with a
legend and other features.
Below is the whole `map.py` for your reference. You need to update your `Room` typeclass (see above)
to actually call it. Remember that to see different symbols for a location you also need to set the
`sector_type` Attribute on the room to one of the keys in the `SYMBOLS` dictionary. So in this
example, to make a room be mapped as `[.]` you would set the room's `sector_type` to
`"SECT_INSIDE"`. Try it out with `@set here/sector_type = "SECT_INSIDE"`. If you wanted all new
rooms to have a given sector symbol, you could change the default in the `SYMBOLS` dictionary below,
or you could add the Attribute in the Room's `at_object_creation` method.
```python
# mygame/world/map.py
# These are keys set with the Attribute sector_type on the room.
# The keys None and "you" must always exist.
SYMBOLS = { None : ' . ', # for rooms without a sector_type attr
'you' : '[@]',
'SECT_INSIDE': '[.]' }
class Map(object):
def __init__(self, caller, max_width=9, max_length=9):
self.caller = caller
self.max_width = max_width
self.max_length = max_length
self.worm_has_mapped = {}
self.curX = None
self.curY = None
if self.check_grid():
# we actually have to store the grid into a variable
self.grid = self.create_grid()
self.draw_room_on_map(caller.location,
((min(max_width, max_length) -1 ) / 2))
def update_pos(self, room, exit_name):
# this ensures the pointer variables always
# stays up to date to where the worm is currently at.
self.curX, self.curY = \
self.worm_has_mapped[room][0], self.worm_has_mapped[room][1]
# now we have to actually move the pointer
# variables depending on which 'exit' it found
if exit_name == 'east':
self.curY += 1
elif exit_name == 'west':
self.curY -= 1
elif exit_name == 'north':
self.curX -= 1
elif exit_name == 'south':
self.curX += 1
def draw_room_on_map(self, room, max_distance):
self.draw(room)
if max_distance == 0:
return
for exit in room.exits:
if exit.name not in ("north", "east", "west", "south"):
# we only map in the cardinal directions. Mapping up/down would be
# an interesting learning project for someone who wanted to try it.
continue
if self.has_drawn(exit.destination):
# we've been to the destination already, skip ahead.
continue
self.update_pos(room, exit.name.lower())
self.draw_room_on_map(exit.destination, max_distance - 1)
def draw(self, room):
# draw initial caller location on map first!
if room == self.caller.location:
self.start_loc_on_grid()
self.worm_has_mapped[room] = [self.curX, self.curY]
else:
# map all other rooms
self.worm_has_mapped[room] = [self.curX, self.curY]
# this will use the sector_type Attribute or None if not set.
self.grid[self.curX][self.curY] = SYMBOLS[room.db.sector_type]
def median(self, num):
lst = sorted(range(0, num))
n = len(lst)
m = n -1
return (lst[n//2] + lst[m//2]) / 2.0
def start_loc_on_grid(self):
x = self.median(self.max_width)
y = self.median(self.max_length)
# x and y are floats by default, can't index lists with float types
x, y = int(x), int(y)
self.grid[x][y] = SYMBOLS['you']
self.curX, self.curY = x, y # updating worms current location
def has_drawn(self, room):
return True if room in self.worm_has_mapped.keys() else False
def create_grid(self):
# This method simply creates an empty grid
# with the specified variables from __init__(self):
board = []
for row in range(self.max_width):
board.append([])
for column in range(self.max_length):
board[row].append(' ')
return board
def check_grid(self):
# this method simply checks the grid to make sure
# both max_l and max_w are odd numbers
return True if self.max_length % 2 != 0 or \
self.max_width % 2 != 0 else False
def show_map(self):
map_string = ""
for row in self.grid:
map_string += " ".join(row)
map_string += "\n"
return map_string
```
## Final Comments
The Dynamic map could be expanded with further capabilities. For example, it could mark exits or
allow NE, SE etc directions as well. It could have colors for different terrain types. One could
also look into up/down directions and figure out how to display that in a good way.

View file

@ -0,0 +1,196 @@
# 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 advantages of Python are an extremely fast
development cycle and easy ways to create game systems. Doing the same with C can take many times
more code and be harder to make stable and maintainable.
## 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 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. 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,221 @@
# Evennia for MUSH Users
*This page is adopted from an article originally posted for the MUSH community [here on
musoapbox.net](https://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](../Components/FuncParser.md) 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](../Setup/Installation.md).
## 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](https://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.md).
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](Beginner-Tutorial/Part1/Tutorial-World.md) 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](./Howtos-Overview.md)
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](https://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 [Setup Quickstart](../Setup/Installation.md)
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](Beginner-Tutorial/Part1/Beginner-Tutorial-Part1-Intro.md) 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](../Concepts/Building-Permissions.md#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](../Concepts/Building-Permissions.md#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](../Components/Accounts.md) 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 = f"{name}(GM)"
if lookaccount and \
(lookaccount.permissions.get("Developers") or lookaccount.db.is_gm):
# Developers/GMs see name(#dbref) or name(GM)(#dbref)
name = f"{name}(#{self.id})"
return name
```
Above, we change how the Character's name is displayed: If the account controlling this Character is
a GM, we attach the string `(GM)` to the Character's name so everyone can tell who's the boss. If we
ourselves are Developers or GM's we will see database ids attached to Characters names, which can
help if doing database searches against Characters of exactly the same name. We base the "gm-
ingness" on having an flag (an [Attribute](../Components/Attributes.md)) 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](../Components/Commands.md) 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(f"Could not find account '{self.args}'")
return
elif len(accountlist) > 1:
caller.msg(f"Multiple matches for '{self.args}': {accountlist}")
return
else:
account = accountlist[0]
if self.cmdstring == "gm":
# turn someone into a GM
if account.permissions.get("Admins"):
caller.msg(f"Account {account} is already a GM.")
else:
account.permissions.add("Admins")
caller.msg(f"Account {account} is now a GM.")
account.msg(f"You are now a GM (changed by {caller}).")
account.character.db.is_gm = True
else:
# @ungm was entered - revoke GM status from someone
if not account.permissions.get("Admins"):
caller.msg(f"Account {account} is not a GM.")
else:
account.permissions.remove("Admins")
caller.msg(f"Account {account} is no longer a GM.")
account.msg(f"You are no longer a GM (changed by {caller}).")
del account.character.db.is_gm
```
All the command does is to locate the account target and assign it the `Admins` permission if we
used `@gm` or revoke it if using the `@ungm` alias. We also set/unset the `is_gm` Attribute that is
expected by our new `Character.get_display_name` method from earlier.
> We could have made this into two separate commands or opted for a syntax like `@gm/revoke
<accountname>`. Instead we examine how this command was called (stored 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](../Concepts/TextTags.md) 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:
list_of_fieldnames = ", ".join(ALLOWED_FIELDNAMES)
err = f"Allowed field names: {list_of_fieldnames}"
caller.msg(err)
return False
if fieldname in ALLOWED_ATTRS and not value.isdigit():
caller.msg(f"{fieldname} must receive a number.")
return False
return True
class CmdSheet(MuxCommand):
"""
Edit a field on the character sheet
Usage:
@sheet field value
Examples:
@sheet name Ulrik the Warrior
@sheet dex 12
@sheet advantages Super strength, Night vision
If given without arguments, will view the current character sheet.
Allowed field names are:
name,
str, con, dex, int, wis, cha,
advantages, disadvantages
"""
key = "sheet"
aliases = "editsheet"
locks = "cmd: perm(Players)"
help_category = "RP"
def func(self):
caller = self.caller
if not self.args or len(self.args) < 2:
# not enough arguments. Display the sheet
if sheet:
caller.msg(caller.db.charsheet)
else:
caller.msg("You have no character sheet.")
return
# if caller.db.sheet_locked:
caller.msg("Your character sheet is locked.")
return
# split input by whitespace, once
fieldname, value = self.args.split(None, 1)
fieldname = fieldname.lower() # ignore case
if not _validate_fieldnames(caller, fieldname):
return
if fieldname == "name":
self.key = value
else:
caller.chardata[fieldname] = value
caller.update_charsheet()
caller.msg(f"{fieldname} was set to {value}.")
```
Most of this command is error-checking to make sure the right type of data was input. Note how the
`sheet_locked` Attribute is checked and will return if not set.
This command you import into `mygame/commands/default_cmdsets.py` and add to the `CharacterCmdSet`,
in the same way the `@gm` command was added to the `AccountCmdSet` earlier.
### Commands for GM to change Character sheet
Game masters use basically the same input as Players do to edit a character sheet, except they can
do it on other players than themselves. They are also not stopped by any `sheet_locked` flags.
```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(f"{character.key} can no longer edit their character sheet.")
elif "unlock" in self.switches:
if not character.db.sheet_locked:
caller.msg("The character sheet is already unlocked.")
else:
character.db.sheet_locked = False
caller.msg(f"{character.key} can now edit their character sheet.")
if fieldname:
if fieldname == "name":
character.key = value
else:
character.db.chardata[fieldname] = value
character.update_charsheet()
caller.msg(f"You set {character.key}'s {fieldname} to {value}.")
else:
# just display
caller.msg(character.db.charsheet)
```
The `@gmsheet` command takes an additional argument to specify which Character's character sheet to
edit. It also takes `/lock` and `/unlock` switches to block the Player from tweaking their sheet.
Before this can be used, it should be added to the default `CharacterCmdSet` in the same way as the
normal `@sheet`. Due to the lock set on it, this command will only be available to `Admins` (i.e.
GMs) or higher permission levels.
## Dice roller
Evennia's *contrib* folder already comes with a full dice roller. To add it to the game, simply
import `contrib.dice.CmdDice` into `mygame/commands/default_cmdsets.py` and add `CmdDice` to the
`CharacterCmdset` as done with other commands in this tutorial. After a `@reload` you will be able
to roll dice using normal RPG-style format:
```
roll 2d6 + 3
7
```
Use `help dice` to see what syntax is supported or look at `evennia/contrib/dice.py` to see how it's
implemented.
## Rooms
Evennia comes with rooms out of the box, so no extra work needed. A GM will automatically have all
needed building commands available. A fuller go-through is found in the [Building tutorial](Beginner-Tutorial/Part1/Building-Quickstart.md).
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](../Components/Channels.md) 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,304 @@
# 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](../Components/Scripts.md) 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 1, day 1, and at midnight.
> Year, hour, minute and sec starts from 0, month, week and day starts from 1, this makes them
> behave consistently with the standard time.
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, mins, secs = custom_gametime.custom_gametime(absolute=True)
time_string = f"We are in year {year}, day {day}, month {month}."
time_string += f"\nIt's {hour:02}:{mins:02}:{secs:02}."
self.msg(time_string)
```
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,485 @@
# 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/4.0/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/views.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 make it look like this.
```python
# URL patterns for the help_system app
from django.urls import path
from .views import index
urlpatterns = [
path('', index)
]
```
The `urlpatterns` variable is what Django/Evennia looks for to figure out how to
direct a user entering an URL in their browser to the view-code you have
written.
Last we need to tie this into the main namespace for your game. Edit the file
`mygame/web/urls.py`. In it you will find the `urlpatterns` list again.
Add a new `path` to the end of the list.
```python
# mygame/web/urls.py
# [...]
# add patterns
urlpatterns = [
# website
path("", include("web.website.urls")),
# webclient
path("webclient/", include("web.webclient.urls")),
# web admin
path("admin/", include("web.admin.urls")),
# my help system
path('help/', include('web.help_system.urls')) # <--- NEW
]
# [...]
```
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,91 @@
# Tutorials and Howto's
The documents in this section aims to teach how to use Evennia in a tutorial or
a step-by-step way. They often give hints on about solving a problem or implementing
a particular feature or concept. They will often refer to the
[components](../Components/Components-Overview.md) or [concepts](../Concepts/Concepts-Overview.md)
docs for those that want to dive deeper.
## Beginner Tutorial
Recommended starting point! This will take you from absolute beginner to making
a small, but full, game with Evennia. Even if you have a very different game style
in mind for your own game, this will give you a good start.
```{toctree}
:maxdepth: 1
./Beginner-Tutorial/Beginner-Tutorial-Intro
```
## Howto's
```{toctree}
:maxdepth: 1
Coding-FAQ.md
Command-Prompt.md
Command-Cooldown.md
Command-Duration.md
Default-Exit-Errors.md
Manually-Configuring-Color.md
Tutorial-Tweeting-Game-Stats.md
```
## Mobs and NPCs
```{toctree}
:maxdepth: 1
Tutorial-NPCs-listening.md
Tutorial-Aggressive-NPCs.md
NPC-shop-Tutorial.md
```
## Vehicles
```{toctree}
:maxdepth: 1
Building-a-mech-tutorial.md
Tutorial-Vehicles.md
```
## Systems
```{toctree}
:maxdepth: 1
Gametime-Tutorial.md
Help-System-Tutorial.md
Mass-and-weight-for-objects.md
Weather-Tutorial.md
Coordinates.md
Dynamic-In-Game-Map.md
Static-In-Game-Map.md
Arxcode-Installation.md
```
## Web-related tutorials
```{toctree}
:maxdepth: 1
Add-a-wiki-on-your-website.md
Web-Character-Generation.md
Web-Character-View-Tutorial.md
```
## Deep-dives
```{toctree}
:maxdepth: 1
Parsing-commands-tutorial.md
Understanding-Color-Tags.md
Evennia-for-roleplaying-sessions.md
Evennia-for-Diku-Users.md
Evennia-for-MUSH-Users.md
Tutorial-for-basic-MUSH-like-game.md
```

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](../Concepts/Colors.md) 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](../Components/Typeclasses.md) 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](../Components/Attributes.md).
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](../Concepts/TextTags.md#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,98 @@
# 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([
str(item.get_display_name(self.caller.sessions)),
second and second or "",
])
string = f"|wYou are carrying:\n{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](../Components/EvMenu.md)
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](../Components/EvMenu.md) 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 = f"*** Welcome to {shopname}! ***\n"
if wares:
text += f" Things for sale (choose 1-{len(wares)} to inspect); quit to exit:"
else:
text += " There is nothing for sale; quit to exit."
options = []
for ware in wares:
# add an option for every ware in store
gold_val = ware.db.gold_value or 1
options.append({"desc": f"{ware.key} ({gold_val} gold)",
"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 = f"You inspect {ware.key}:\n\n{ware.db.desc}"
def buy_ware_result(caller):
"This will be executed first when choosing to buy."
if wealth >= value:
rtext = f"You pay {value} gold and purchase {ware.key}!"
caller.db.gold -= value
ware.move_to(caller, quiet=True)
else:
rtext = f"You cannot afford {value} gold for {ware.key}!"
caller.msg(rtext)
gold_val = ware.db.gold_value or 1
options = ({
"desc": f"Buy {ware.key} for {gold_val} gold",
"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](../Components/Commands.md) 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](../Components/Command-Sets.md) 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](../Components/Typeclasses.md) 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, persistent=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=f"{shopname}-storage",
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 = f"{shopname}-storekey"
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(f"traverse:holds({storeroom_key_name})")
# inform the builder about progress
self.caller.msg(f"The shop {shop} was created!")
```
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](Starting/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](../Components/Locks.md) 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](../Components/Scripts.md) 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,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](Beginner-Tutorial/Part1/Adding-Commands.md) 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](Beginner-Tutorial/Part1/Searching-Things.md).
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,416 @@
# Static In Game Map
## Introduction
This tutorial describes the creation of an in-game map display based on a pre-drawn map. It also
details how to use the [Batch code processor](../Components/Batch-Code-Processor.md) for advanced building. There is
also the [Dynamic in-game map tutorial](./Dynamic-In-Game-Map.md) that works in the opposite direction,
by generating a map from an existing grid of rooms.
Evennia does not require its rooms to be positioned in a "logical" way. Your exits could be named
anything. You could make an exit "west" that leads to a room described to be in the far north. You
could have rooms inside one another, exits leading back to the same room or describing spatial
geometries impossible in the real world.
That said, most games *do* organize their rooms in a logical fashion, if nothing else to retain the
sanity of their players. And when they do, the game becomes possible to map. This tutorial will give
an example of a simple but flexible in-game map system to further help player's to navigate. We will
To simplify development and error-checking we'll break down the work into bite-size chunks, each
building on what came before. For this we'll make extensive use of the [Batch code processor](Batch-
Code-Processor), so you may want to familiarize yourself with that.
1. **Planning the map** - Here we'll come up with a small example map to use for the rest of the
tutorial.
2. **Making a map object** - This will showcase how to make a static in-game "map" object a
Character could pick up and look at.
3. **Building the map areas** - Here we'll actually create the small example area according to the
map we designed before.
4. **Map code** - This will link the map to the location so our output looks something like this:
```
crossroads(#3)
↑╚∞╝↑
≈↑│↑∩ The merger of two roads. To the north looms a mighty castle.
O─O─O To the south, the glow of a campfire can be seen. To the east lie
≈↑│↑∩ the vast mountains and to the west is heard the waves of the sea.
↑▲O▲↑
Exits: north(#8), east(#9), south(#10), west(#11)
```
We will henceforth assume your game folder is name named `mygame` and that you haven't modified the
default commands. We will also not be using [Colors](../Concepts/Colors.md) for our map since they
don't show in the documentation wiki.
## Planning the Map
Let's begin with the fun part! Maps in MUDs come in many different [shapes and
sizes](http://journal.imaginary-realities.com/volume-05/issue-01/modern-interface-modern-
mud/index.html). Some appear as just boxes connected by lines. Others have complex graphics that are
external to the game itself.
Our map will be in-game text but that doesn't mean we're restricted to the normal alphabet! If
you've ever selected the [Wingdings font](https://en.wikipedia.org/wiki/Wingdings) in Microsoft Word
you will know there are a multitude of other characters around to use. When creating your game with
Evennia you have access to the [UTF-8 character encoding](https://en.wikipedia.org/wiki/UTF-8) which
put at your disposal [thousands of letters, number and geometric shapes](https://mcdlr.com/utf-8/#1).
For this exercise, we've copy-and-pasted from the pallet of special characters used over at
[Dwarf Fortress](https://dwarffortresswiki.org/index.php/Character_table) to create what is hopefully
a pleasing and easy to understood landscape:
```
≈≈↑↑↑↑↑∩∩
≈≈↑╔═╗↑∩∩ Places the account can visit are indicated by "O".
≈≈↑║O║↑∩∩ Up the top is a castle visitable by the account.
≈≈↑╚∞╝↑∩∩ To the right is a cottage and to the left the beach.
≈≈≈↑│↑∩∩∩ And down the bottom is a camp site with tents.
≈≈O─O─O⌂∩ In the center is the starting location, a crossroads
≈≈≈↑│↑∩∩∩ which connect the four other areas.
≈≈↑▲O▲↑∩∩
≈≈↑↑▲↑↑∩∩
≈≈↑↑↑↑↑∩∩
```
There are many considerations when making a game map depending on the play style and requirements
you intend to implement. Here we will display a 5x5 character map of the area surrounding the
account. This means making sure to account for 2 characters around every visitable location. Good
planning at this stage can solve many problems before they happen.
## Creating a Map Object
In this section we will try to create an actual "map" object that an account can pick up and look
at.
Evennia offers a range of [default commands](../Components/Default-Commands.md) for
[creating objects and rooms in-game](Beginner-Tutorial/Part1/Building-Quickstart.md). While readily accessible, these commands are made to do very
specific, restricted things and will thus not offer as much flexibility to experiment (for an
advanced exception see [the FuncParser](../Components/FuncParser.md)). Additionally, entering long
descriptions and properties over and over in the game client can become tedious; especially when
testing and you may want to delete and recreate things over and over.
To overcome this, Evennia offers [batch processors](../Components/Batch-Processors.md) that work as input-files
created out-of-game. In this tutorial we'll be using the more powerful of the two available batch
processors, the [Batch Code Processor ](../Components/Batch-Code-Processor.md), called with the `@batchcode` command.
This is a very powerful tool. It allows you to craft Python files to act as blueprints of your
entire game world. These files have access to use Evennia's Python API directly. Batchcode allows
for easy editing and creation in whatever text editor you prefer, avoiding having to manually build
the world line-by-line inside the game.
> Important warning: `@batchcode`'s power is only rivaled by the `@py` command. Batchcode is so
powerful it should be reserved only for the [superuser](../Concepts/Building-Permissions.md). Think carefully
before you let others (such as `Developer`- level staff) run `@batchcode` on their own - make sure
you are okay with them running *arbitrary Python code* on your server.
While a simple example, the map object it serves as good way to try out `@batchcode`. Go to
`mygame/world` and create a new file there named `batchcode_map.py`:
```Python
# mygame/world/batchcode_map.py
from evennia import create_object
from evennia import DefaultObject
# We use the create_object function to call into existence a
# DefaultObject named "Map" wherever you are standing.
map = create_object(DefaultObject, key="Map", location=caller.location)
# We then access its description directly to make it our map.
map.db.desc = """
≈≈↑↑↑↑↑∩∩
≈≈↑╔═╗↑∩∩
≈≈↑║O║↑∩∩
≈≈↑╚∞╝↑∩∩
≈≈≈↑│↑∩∩∩
≈≈O─O─O⌂∩
≈≈≈↑│↑∩∩∩
≈≈↑▲O▲↑∩∩
≈≈↑↑▲↑↑∩∩
≈≈↑↑↑↑↑∩∩
"""
# This message lets us know our map was created successfully.
caller.msg("A map appears out of thin air and falls to the ground.")
```
Log into your game project as the superuser and run the command
```
@batchcode batchcode_map
```
This will load your `batchcode_map.py` file and execute the code (Evennia will look in your `world/`
folder automatically so you don't need to specify it).
A new map object should have appeared on the ground. You can view the map by using `look map`. Let's
take it with the `get map` command. We'll need it in case we get lost!
## Building the map areas
We've just used batchcode to create an object useful for our adventures. But the locations on that
map does not actually exist yet - we're all mapped up with nowhere to go! Let's use batchcode to
build a game area based on our map. We have five areas outlined: a castle, a cottage, a campsite, a
coastal beach and the crossroads which connects them. Create a new batchcode file for this in
`mygame/world`, named `batchcode_world.py`.
```Python
# mygame/world/batchcode_world.py
from evennia import create_object, search_object
from typeclasses import rooms, exits
# We begin by creating our rooms so we can detail them later.
centre = create_object(rooms.Room, key="crossroads")
north = create_object(rooms.Room, key="castle")
east = create_object(rooms.Room, key="cottage")
south = create_object(rooms.Room, key="camp")
west = create_object(rooms.Room, key="coast")
# This is where we set up the cross roads.
# The rooms description is what we see with the 'look' command.
centre.db.desc = """
The merger of two roads. A single lamp post dimly illuminates the lonely crossroads.
To the north looms a mighty castle. To the south the glow of a campfire can be seen.
To the east lie a wall of mountains and to the west the dull roar of the open sea.
"""
# Here we are creating exits from the centre "crossroads" location to
# destinations to the north, east, south, and west. We will be able
# to use the exit by typing it's key e.g. "north" or an alias e.g. "n".
centre_north = create_object(exits.Exit, key="north",
aliases=["n"], location=centre, destination=north)
centre_east = create_object(exits.Exit, key="east",
aliases=["e"], location=centre, destination=east)
centre_south = create_object(exits.Exit, key="south",
aliases=["s"], location=centre, destination=south)
centre_west = create_object(exits.Exit, key="west",
aliases=["w"], location=centre, destination=west)
# Now we repeat this for the other rooms we'll be implementing.
# This is where we set up the northern castle.
north.db.desc = "An impressive castle surrounds you. " \
"There might be a princess in one of these towers."
north_south = create_object(exits.Exit, key="south",
aliases=["s"], location=north, destination=centre)
# This is where we set up the eastern cottage.
east.db.desc = "A cosy cottage nestled among mountains " \
"stretching east as far as the eye can see."
east_west = create_object(exits.Exit, key="west",
aliases=["w"], location=east, destination=centre)
# This is where we set up the southern camp.
south.db.desc = "Surrounding a clearing are a number of " \
"tribal tents and at their centre a roaring fire."
south_north = create_object(exits.Exit, key="north",
aliases=["n"], location=south, destination=centre)
# This is where we set up the western coast.
west.db.desc = "The dark forest halts to a sandy beach. " \
"The sound of crashing waves calms the soul."
west_east = create_object(exits.Exit, key="east",
aliases=["e"], location=west, destination=centre)
# Lastly, lets make an entrance to our world from the default Limbo room.
limbo = search_object('Limbo')[0]
limbo_exit = create_object(exits.Exit, key="enter world",
aliases=["enter"], location=limbo, destination=centre)
```
Apply this new batch code with `@batchcode batchcode_world`. If there are no errors in the code we
now have a nice mini-world to explore. Remember that if you get lost you can look at the map we
created!
## In-game minimap
Now we have a landscape and matching map, but what we really want is a mini-map that displays
whenever we move to a room or use the `look` command.
We *could* manually enter a part of the map into the description of every room like we did our map
object description. But some MUDs have tens of thousands of rooms! Besides, if we ever changed our
map we would have to potentially alter a lot of those room descriptions manually to match the
change. So instead we will make one central module to hold our map. Rooms will reference this
central location on creation and the map changes will thus come into effect when next running our
batchcode.
To make our mini-map we need to be able to cut our full map into parts. To do this we need to put it
in a format which allows us to do that easily. Luckily, python allows us to treat strings as lists
of characters allowing us to pick out the characters we need.
`mygame/world/map_module.py`
```Python
# We place our map into a sting here.
world_map = """\
≈≈↑↑↑↑↑∩∩
≈≈↑╔═╗↑∩∩
≈≈↑║O║↑∩∩
≈≈↑╚∞╝↑∩∩
≈≈≈↑│↑∩∩∩
≈≈O─O─O⌂∩
≈≈≈↑│↑∩∩∩
≈≈↑▲O▲↑∩∩
≈≈↑↑▲↑↑∩∩
≈≈↑↑↑↑↑∩∩
"""
# This turns our map string into a list of rows. Because python
# allows us to treat strings as a list of characters, we can access
# those characters with world_map[5][5] where world_map[row][column].
world_map = world_map.split('\n')
def return_map():
"""
This function returns the whole map
"""
map = ""
#For each row in our map, add it to map
for valuey in world_map:
map += valuey
map += "\n"
return map
def return_minimap(x, y, radius = 2):
"""
This function returns only part of the map.
Returning all chars in a 2 char radius from (x,y)
"""
map = ""
#For each row we need, add the characters we need.
for valuey in world_map[y-radius:y+radius+1]: for valuex in valuey[x-radius:x+radius+1]:
map += valuex
map += "\n"
return map
```
With our map_module set up, let's replace our hardcoded map in `mygame/world/batchcode_map.py` with
a reference to our map module. Make sure to import our map_module!
```python
# mygame/world/batchcode_map.py
from evennia import create_object
from evennia import DefaultObject
from world import map_module
map = create_object(DefaultObject, key="Map", location=caller.location)
map.db.desc = map_module.return_map()
caller.msg("A map appears out of thin air and falls to the ground.")
```
Log into Evennia as the superuser and run this batchcode. If everything worked our new map should
look exactly the same as the old map - you can use `@delete` to delete the old one (use a number to
pick which to delete).
Now, lets turn our attention towards our game's rooms. Let's use the `return_minimap` method we
created above in order to include a minimap in our room descriptions. This is a little more
complicated.
By itself we would have to settle for either the map being *above* the description with
`room.db.desc = map_string + description_string`, or the map going *below* by reversing their order.
Both options are rather unsatisfactory - we would like to have the map next to the text! For this
solution we'll explore the utilities that ship with Evennia. Tucked away in `evennia\evennia\utils`
is a little module called [EvTable](github:evennia.utils.evtable) . This is an advanced ASCII table
creator for you to utilize in your game. We'll use it by creating a basic table with 1 row and two
columns (one for our map and one for our text) whilst also hiding the borders. Open the batchfile
again
```python
# mygame\world\batchcode_world.py
# Add to imports
from evennia.utils import evtable
from world import map_module
# [...]
# Replace the descriptions with the below code.
# The cross roads.
# We pass what we want in our table and EvTable does the rest.
# Passing two arguments will create two columns but we could add more.
# We also specify no border.
centre.db.desc = evtable.EvTable(map_module.return_minimap(4,5),
"The merger of two roads. A single lamp post dimly " \
"illuminates the lonely crossroads. To the north " \
"looms a mighty castle. To the south the glow of " \
"a campfire can be seen. To the east lie a wall of " \
"mountains and to the west the dull roar of the open sea.",
border=None)
# EvTable allows formatting individual columns and cells. We use that here
# to set a maximum width for our description, but letting the map fill
# whatever space it needs.
centre.db.desc.reformat_column(1, width=70)
# [...]
# The northern castle.
north.db.desc = evtable.EvTable(map_module.return_minimap(4,2),
"An impressive castle surrounds you. There might be " \
"a princess in one of these towers.",
border=None)
north.db.desc.reformat_column(1, width=70)
# [...]
# The eastern cottage.
east.db.desc = evtable.EvTable(map_module.return_minimap(6,5),
"A cosy cottage nestled among mountains stretching " \
"east as far as the eye can see.",
border=None)
east.db.desc.reformat_column(1, width=70)
# [...]
# The southern camp.
south.db.desc = evtable.EvTable(map_module.return_minimap(4,7),
"Surrounding a clearing are a number of tribal tents " \
"and at their centre a roaring fire.",
border=None)
south.db.desc.reformat_column(1, width=70)
# [...]
# The western coast.
west.db.desc = evtable.EvTable(map_module.return_minimap(2,5),
"The dark forest halts to a sandy beach. The sound of " \
"crashing waves calms the soul.",
border=None)
west.db.desc.reformat_column(1, width=70)
```
Before we run our new batchcode, if you are anything like me you would have something like 100 maps
lying around and 3-4 different versions of our rooms extending from limbo. Let's wipe it all and
start with a clean slate. In Command Prompt you can run `evennia flush` to clear the database and
start anew. It won't reset dbref values however, so if you are at #100 it will start from there.
Alternatively you can navigate to `mygame/server` and delete the `evennia.db3` file. Now in Command
Prompt use `evennia migrate` to have a completely freshly made database.
Log in to evennia and run `@batchcode batchcode_world` and you'll have a little world to explore.
## Conclusions
You should now have a mapped little world and a basic understanding of batchcode, EvTable and how
easily new game defining features can be added to Evennia.
You can easily build from this tutorial by expanding the map and creating more rooms to explore. Why
not add more features to your game by trying other tutorials: [Add weather to your world](Weather-
Tutorial), [fill your world with NPC's](./Tutorial-Aggressive-NPCs.md) or
[implement a combat system](Beginner-Tutorial/Part3/Turn-based-Combat-System.md).

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](../Components/Scripts.md) 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.md) 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](../Components/Objects.md#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_post_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_post_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.md) 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 f"{from_obj} said: '{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(f"say {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](../Setup/How-to-connect-Evennia-to-Twitter.md) 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 = f"Prototype Count: {nprots} Random Keys: "
tweet += f" {keys[randint(0,len(keys)-1)]}"
for x in range(0,2): ##tweet 3
tweet += f", {keys[randint(0,len(keys)-1)]}"
# post the tweet
try:
response = api.PostUpdate(tweet)
except:
logger.log_trace(f"Tweet Error: When attempting to tweet {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](../Components/Scripts.md) 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](../Components/Objects.md#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](../Components/Commands.md): 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](../Components/Command-Sets.md) `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](../Components/Locks.md): 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(f"The train is moving forward to {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](../Components/Scripts.md): 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](../Components/Objects.md#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,665 @@
# Tutorial for basic MUSH like game
This tutorial lets you code a small but complete and functioning MUSH-like game in Evennia. A
[MUSH](https://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](Beginner-Tutorial/Part1/Beginner-Tutorial-Part1-Intro.md) 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](../Components/Attributes.md) `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](../Components/Commands.md) to set the "Power" on the `Character`.
- A chargen [CmdSet](../Components/Command-Sets.md) 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](../Components/Commands.md) 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(f"Your Power was set to {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](../Components/Command-Sets.md) 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, persistent=True)
```
Note how new rooms created with this typeclass will always start with `ChargenCmdset` on themselves.
Don't forget the `persistent=True` keyword or you will lose the cmdset after a server reload. For
more information about [Command Sets](../Components/Command-Sets.md) and [Commands](../Components/Commands.md), 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](../Components/Attributes.md) 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_template = "{attacker} +attack{s} with a combat score of {c_score}!"
caller.msg(message_template.format(
attacker="You",
s="",
c_score=combat_score,
))
caller.location.msg_contents(message_template.format(
attacker=caller.key,
s="s",
c_score=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 = f" (combat score: {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](../Components/Permissions.md).
### 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=f"edit:id({caller.id}) and perm(Builders);call:false()")
# announce
message_template = "{creator} created the NPC '{npc}'."
caller.msg(message_template.format(
creator="You",
npc=name,
))
caller.location.msg_contents(message_template.format(
creator=caller.key,
npc=name,
), exclude=caller)
```
Here we define a `+createnpc` (`+createNPC` works too) that is callable by everyone *not* having the
`nonpcs` "[permission](../Components/Permissions.md)" (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](../Components/Locks.md) 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](../Components/Attributes.md) 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 = f"Properties of {npc.key}:"
for propname in allowed_propnames:
propvalue = npc.attributes.get(propname, default="N/A")
output += f"\n {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(f"You told {npc.key} to do '{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](Beginner-Tutorial/Part1/Tutorial-World.md). For
more specific ideas, see the [other tutorials and hints](./Howtos-Overview.md) as well
as the [Evennia Component overview](../Components/Components-Overview.md).

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](../Concepts/TextTags.md) 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](../Concepts/TextTags.md) 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](../Components/TickerHandler.md) 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,647 @@
# 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](../Concepts/New-Models.md) 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(" or ".join([
f"puppet:id({char.id})",
f"pid({user.id})",
"perm(Developers)",
"pperm(Developers)",
]))
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](../Components/Permissions.md) (copied from the `AccountDB`).
* The right `puppet` [locks](../Components/Locks.md) so the Account can actually play as this Character later.
* The relevant Character [typeclass](../Components/Typeclasses.md)
* 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(" or ".join([
f"puppet:id({char.id})",
f"pid({user.id})",
"perm(Developers)",
"pperm(Developers)",
]))
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/).*