Reorganize docs into flat folder layout

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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