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,436 @@
# A voice operated elevator using events
- Previous tutorial: [Adding dialogues in events](Dialogues-in-events)
This tutorial will walk you through the steps to create a voice-operated elevator, using the [in-
game Python
system](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md).
This tutorial assumes the in-game Python system is installed in your game. If it isn't, you can
follow the installation steps given in [the documentation on in-game
Python](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md), and
come back on this tutorial once the system is installed. **You do not need to read** the entire
documentation, it's a good reference, but not the easiest way to learn about it. Hence these
tutorials.
The in-game Python system allows to run code on individual objects in some situations. You don't
have to modify the source code to add these features, past the installation. The entire system
makes it easy to add specific features to some objects, but not all.
> What will we try to do?
In this tutorial, we are going to create a simple voice-operated elevator. In terms of features, we
will:
- Explore events with parameters.
- Work on more interesting callbacks.
- Learn about chained events.
- Play with variable modification in callbacks.
## Our study case
Let's summarize what we want to achieve first. We would like to create a room that will represent
the inside of our elevator. In this room, a character could just say "1", "2" or "3", and the
elevator will start moving. The doors will close and open on the new floor (the exits leading in
and out of the elevator will be modified).
We will work on basic features first, and then will adjust some, showing you how easy and powerfully
independent actions can be configured through the in-game Python system.
## Creating the rooms and exits we need
We'll create an elevator right in our room (generally called "Limbo", of ID 2). You could easily
adapt the following instructions if you already have some rooms and exits, of course, just remember
to check the IDs.
> Note: the in-game Python system uses IDs for a lot of things. While it is not mandatory, it is
good practice to know the IDs you have for your callbacks, because it will make manipulation much
quicker. There are other ways to identify objects, but as they depend on many factors, IDs are
usually the safest path in our callbacks.
Let's go into limbo (`#2`) to add our elevator. We'll add it to the north. To create this room,
in-game you could type:
tunnel n = Inside of an elevator
The game should respond by telling you:
Created room Inside of an elevator(#3) of type typeclasses.rooms.Room.
Created Exit from Limbo to Inside of an elevator: north(#4) (n).
Created Exit back from Inside of an elevator to Limbo: south(#5) (s).
Note the given IDs:
- `#2` is limbo, the first room the system created.
- `#3` is our room inside of an elevator.
- `#4` is the north exit from Limbo to our elevator.
- `#5` is the south exit from an elevator to Limbo.
Keep these IDs somewhere for the demonstration. You will shortly see why they are important.
> Why have we created exits to our elevator and back to Limbo? Isn't the elevator supposed to move?
It is. But we need to have exits that will represent the way inside the elevator and out. What we
will do, at every floor, will be to change these exits so they become connected to the right room.
You'll see this process a bit later.
We have two more rooms to create: our floor 2 and 3. This time, we'll use `dig`, because we don't
need exits leading there, not yet anyway.
dig The second floor
dig The third floor
Evennia should answer with:
Created room The second floor(#6) of type typeclasses.rooms.Room.
Created room The third floor(#7) of type typeclasses.rooms.Room.
Add these IDs to your list, we will use them too.
## Our first callback in the elevator
Let's go to the elevator (you could use `tel #3` if you have the same IDs I have).
This is our elevator room. It looks a bit empty, feel free to add a prettier description or other
things to decorate it a bit.
But what we want now is to be able to say "1", "2" or "3" and have the elevator move in that
direction.
If you have read [the previous tutorial about adding dialogues in events](Dialogues-in-events), you
may remember what we need to do. If not, here's a summary: we need to run some code when somebody
speaks in the room. So we need to create a callback (the callback will contain our lines of code).
We just need to know on which event this should be set. You can enter `call here` to see the
possible events in this room.
In the table, you should see the "say" event, which is called when somebody says something in the
room. So we'll need to add a callback to this event. Don't worry if you're a bit lost, just follow
the following steps, the way they connect together will become more obvious.
call/add here = say 1, 2, 3
1. We need to add a callback. A callback contains the code that will be executed at a given time.
So we use the `call/add` command and switch.
2. `here` is our object, the room in which we are.
3. An equal sign.
4. The name of the event to which the callback should be connected. Here, the event is "say".
Meaning this callback will be executed every time somebody says something in the room.
5. But we add an event parameter to indicate the keywords said in the room that should execute our
callback. Otherwise, our callback would be called every time somebody speaks, no matter what. Here
we limit, indicating our callback should be executed only if the spoken message contains "1", "2" or
"3".
An editor should open, inviting you to enter the Python code that should be executed. The first
thing to remember is to read the text provided (it can contain important information) and, most of
all, the list of variables that are available in this callback:
```
Variables you can use in this event:
character: the character having spoken in this room.
room: the room connected to this event.
message: the text having been spoken by the character.
----------Line Editor [Callback say of Inside of an elevator]---------------------
01|
----------[l:01 w:000 c:0000]------------(:h for help)----------------------------
```
This is important, in order to know what variables we can use in our callback out-of-the-box. Let's
write a single line to be sure our callback is called when we expect it to:
```python
character.msg("You just said {}.".format(message))
```
You can paste this line in-game, then type the `:wq` command to exit the editor and save your
modifications.
Let's check. Try to say "hello" in the room. You should see the standard message, but nothing
more. Now try to say "1". Below the standard message, you should see:
You just said 1.
You can try it. Our callback is only called when we say "1", "2" or "3". Which is just what we
want.
Let's go back in our code editor and add something more useful.
call/edit here = say
> Notice that we used the "edit" switch this time, since the callback exists, we just want to edit
it.
The editor opens again. Let's empty it first:
:DD
And turn off automatic indentation, which will help us:
:=
> Auto-indentation is an interesting feature of the code editor, but we'd better not use it at this
point, it will make copy/pasting more complicated.
## Our entire callback in the elevator
So here's the time to truly code our callback in-game. Here's a little reminder:
1. We have all the IDs of our three rooms and two exits.
2. When we say "1", "2" or "3", the elevator should move to the right room, that is change the
exits. Remember, we already have the exits, we just need to change their location and destination.
It's a good idea to try to write this callback yourself, but don't feel bad about checking the
solution right now. Here's a possible code that you could paste in the code editor:
```python
# First let's have some constants
ELEVATOR = get(id=3)
FLOORS = {
"1": get(id=2),
"2": get(id=6),
"3": get(id=7),
}
TO_EXIT = get(id=4)
BACK_EXIT = get(id=5)
# Now we check that the elevator isn't already at this floor
floor = FLOORS.get(message)
if floor is None:
character.msg("Which floor do you want?")
elif TO_EXIT.location is floor:
character.msg("The elevator already is at this floor.")
else:
# 'floor' contains the new room where the elevator should be
room.msg_contents("The doors of the elevator close with a clank.")
TO_EXIT.location = floor
BACK_EXIT.destination = floor
room.msg_contents("The doors of the elevator open to {floor}.",
mapping=dict(floor=floor))
```
Let's review this longer callback:
1. We first obtain the objects of both exits and our three floors. We use the `get()` eventfunc,
which is a shortcut to obtaining objects. We usually use it to retrieve specific objects with an
ID. We put the floors in a dictionary. The keys of the dictionary are the floor number (as str),
the values are room objects.
2. Remember, the `message` variable contains the message spoken in the room. So either "1", "2", or
"3". We still need to check it, however, because if the character says something like "1 2" in the
room, our callback will be executed. Let's be sure what she says is a floor number.
3. We then check if the elevator is already at this floor. Notice that we use `TO_EXIT.location`.
`TO_EXIT` contains our "north" exit, leading inside of our elevator. Therefore, its `location` will
be the room where the elevator currently is.
4. If the floor is a different one, have the elevator "move", changing just the location and
destination of both exits.
- The `BACK_EXIT` (that is "north") should change its location. The elevator shouldn't be
accessible through our old floor.
- The `TO_EXIT` (that is "south", the exit leading out of the elevator) should have a different
destination. When we go out of the elevator, we should find ourselves in the new floor, not the old
one.
Feel free to expand on this example, changing messages, making further checks. Usage and practice
are keys.
You can quit the editor as usual with `:wq` and test it out.
## Adding a pause in our callback
Let's improve our callback. One thing that's worth adding would be a pause: for the time being,
when we say the floor number in the elevator, the doors close and open right away. It would be
better to have a pause of several seconds. More logical.
This is a great opportunity to learn about chained events. Chained events are very useful to create
pauses. Contrary to the events we have seen so far, chained events aren't called automatically.
They must be called by you, and can be called after some time.
- Chained events always have the name "chain_X". Usually, X is a number, but you can give the
chained event a more explicit name.
- In our original callback, we will call our chained events in, say, 15 seconds.
- We'll also have to make sure the elevator isn't already moving.
Other than that, a chained event can be connected to a callback as usual. We'll create a chained
event in our elevator, that will only contain the code necessary to open the doors to the new floor.
call/add here = chain_1
The callback is added to the "chain_1" event, an event that will not be automatically called by the
system when something happens. Inside this event, you can paste the code to open the doors at the
new floor. You can notice a few differences:
```python
TO_EXIT.location = floor
TO_EXIT.destination = ELEVATOR
BACK_EXIT.location = ELEVATOR
BACK_EXIT.destination = floor
room.msg_contents("The doors of the elevator open to {floor}.",
mapping=dict(floor=floor))
```
Paste this code into the editor, then use `:wq` to save and quit the editor.
Now let's edit our callback in the "say" event. We'll have to change it a bit:
- The callback will have to check the elevator isn't already moving.
- It must change the exits when the elevator move.
- It has to call the "chain_1" event we have defined. It should call it 15 seconds later.
Let's see the code in our callback.
call/edit here = say
Remove the current code and disable auto-indentation again:
:DD
:=
And you can paste instead the following code. Notice the differences with our first attempt:
```python
# First let's have some constants
ELEVATOR = get(id=3)
FLOORS = {
"1": get(id=2),
"2": get(id=6),
"3": get(id=7),
}
TO_EXIT = get(id=4)
BACK_EXIT = get(id=5)
# Now we check that the elevator isn't already at this floor
floor = FLOORS.get(message)
if floor is None:
character.msg("Which floor do you want?")
elif BACK_EXIT.location is None:
character.msg("The elevator is between floors.")
elif TO_EXIT.location is floor:
character.msg("The elevator already is at this floor.")
else:
# 'floor' contains the new room where the elevator should be
room.msg_contents("The doors of the elevator close with a clank.")
TO_EXIT.location = None
BACK_EXIT.location = None
call_event(room, "chain_1", 15)
```
What changed?
1. We added a little test to make sure the elevator wasn't already moving. If it is, the
`BACK_EXIT.location` (the "south" exit leading out of the elevator) should be `None`. We'll remove
the exit while the elevator is moving.
2. When the doors close, we set both exits' `location` to `None`. Which "removes" them from their
room but doesn't destroy them. The exits still exist but they don't connect anything. If you say
"2" in the elevator and look around while the elevator is moving, you won't see any exits.
3. Instead of opening the doors immediately, we call `call_event`. We give it the object containing
the event to be called (here, our elevator), the name of the event to be called (here, "chain_1")
and the number of seconds from now when the event should be called (here, `15`).
4. The `chain_1` callback we have created contains the code to "re-open" the elevator doors. That
is, besides displaying a message, it reset the exits' `location` and `destination`.
If you try to say "3" in the elevator, you should see the doors closing. Look around you and you
won't see any exit. Then, 15 seconds later, the doors should open, and you can leave the elevator
to go to the third floor. While the elevator is moving, the exit leading to it will be
inaccessible.
> Note: we don't define the variables again in our chained event, we just call them. When we
execute `call_event`, a copy of our current variables is placed in the database. These variables
will be restored and accessible again when the chained event is called.
You can use the `call/tasks` command to see the tasks waiting to be executed. For instance, say "2"
in the room, notice the doors closing, and then type the `call/tasks` command. You will see a task
in the elevator, waiting to call the `chain_1` event.
## Changing exit messages
Here's another nice little feature of events: you can modify the message of a single exit without
altering the others. In this case, when someone goes north into our elevator, we'd like to see
something like: "someone walks into the elevator." Something similar for the back exit would be
great too.
Inside of the elevator, you can look at the available events on the exit leading outside (south).
call south
You should see two interesting rows in this table:
```
| msg_arrive | 0 (0) | Customize the message when a character |
| | | arrives through this exit. |
| msg_leave | 0 (0) | Customize the message when a character leaves |
| | | through this exit. |
```
So we can change the message others see when a character leaves, by editing the "msg_leave" event.
Let's do that:
call/add south = msg_leave
Take the time to read the help. It gives you all the information you should need. We'll need to
change the "message" variable, and use custom mapping (between braces) to alter the message. We're
given an example, let's use it. In the code editor, you can paste the following line:
```python
message = "{character} walks out of the elevator."
```
Again, save and quit the editor by entering `:wq`. You can create a new character to see it leave.
charcreate A beggar
tel #8 = here
(Obviously, adapt the ID if necessary.)
py self.search("beggar").move_to(self.search("south"))
This is a crude way to force our beggar out of the elevator, but it allows us to test. You should
see:
A beggar(#8) walks out of the elevator.
Great! Let's do the same thing for the exit leading inside of the elevator. Follow the beggar,
then edit "msg_leave" of "north":
call/add north = msg_leave
```python
message = "{character} walks into the elevator."
```
Again, you can force our beggar to move and see the message we have just set. This modification
applies to these two exits, obviously: the custom message won't be used for other exits. Since we
use the same exits for every floor, this will be available no matter at what floor the elevator is,
which is pretty neat!
## Tutorial F.A.Q.
- **Q:** what happens if the game reloads or shuts down while a task is waiting to happen?
- **A:** if your game reloads while a task is in pause (like our elevator between floors), when the
game is accessible again, the task will be called (if necessary, with a new time difference to take
into account the reload). If the server shuts down, obviously, the task will not be called, but
will be stored and executed when the server is up again.
- **Q:** can I use all kinds of variables in my callback? Whether chained or not?
- **A:** you can use every variable type you like in your original callback. However, if you
execute `call_event`, since your variables are stored in the database, they will need to respect the
constraints on persistent attributes. A callback will not be stored in this way, for instance.
This variable will not be available in your chained event.
- **Q:** when you say I can call my chained events something else than "chain_1", "chain_2" and
such, what is the naming convention?
- **A:** chained events have names beginning by "chain_". This is useful for you and for the
system. But after the underscore, you can give a more useful name, like "chain_open_doors" in our
case.
- **Q:** do I have to pause several seconds to call a chained event?
- **A:** no, you can call it right away. Just leave the third parameter of `call_event` out (it
will default to 0, meaning the chained event will be called right away). This will not create a
task.
- **Q:** can I have chained events calling themselves?
- **A:** you can. There's no limitation. Just be careful, a callback that calls itself,
particularly without delay, might be a good recipe for an infinite loop. However, in some cases, it
is useful to have chained events calling themselves, to do the same repeated action every X seconds
for instance.
- **Q:** what if I need several elevators, do I need to copy/paste these callbacks each time?
- **A:** not advisable. There are definitely better ways to handle this situation. One of them is
to consider adding the code in the source itself. Another possibility is to call chained events
with the expected behavior, which makes porting code very easy. This side of chained events will be
shown in the next tutorial.
- Previous tutorial: [Adding dialogues in events](Dialogues-in-events)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,249 @@
# Dialogues in events
- Next tutorial: [adding a voice-operated elevator with events](A-voice-operated-elevator-using-
events).
This tutorial will walk you through the steps to create several dialogues with characters, using the
[in-game Python
system](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md).
This tutorial assumes the in-game Python system is installed in your game. If it isn't, you can
follow the installation steps given in [the documentation on in-game
Python](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md), and
come back on this tutorial once the system is installed. **You do not need to read** the entire
documentation, it's a good reference, but not the easiest way to learn about it. Hence these
tutorials.
The in-game Python system allows to run code on individual objects in some situations. You don't
have to modify the source code to add these features, past the installation. The entire system
makes it easy to add specific features to some objects, but not all. This is why it can be very
useful to create a dialogue system taking advantage of the in-game Python system.
> What will we try to do?
In this tutorial, we are going to create a basic dialogue to have several characters automatically
respond to specific messages said by others.
## A first example with a first character
Let's create a character to begin with.
@charcreate a merchant
This will create a merchant in the room where you currently are. It doesn't have anything, like a
description, you can decorate it a bit if you like.
As said above, the in-game Python system consists in linking objects with arbitrary code. This code
will be executed in some circumstances. Here, the circumstance is "when someone says something in
the same room", and might be more specific like "when someone says hello". We'll decide what code
to run (we'll actually type the code in-game). Using the vocabulary of the in-game Python system,
we'll create a callback: a callback is just a set of lines of code that will run under some
conditions.
You can have an overview of every "conditions" in which callbacks can be created using the `@call`
command (short for `@callback`). You need to give it an object as argument. Here for instance, we
could do:
@call a merchant
You should see a table with three columns, showing the list of events existing on our newly-created
merchant. There are quite a lot of them, as it is, althougn no line of code has been set yet. For
our system, you might be more interested by the line describing the `say` event:
| say | 0 (0) | After another character has said something in |
| | | the character's room. |
We'll create a callback on the `say` event, called when we say "hello" in the merchant's room:
@call/add a merchant = say hello
Before seeing what this command displays, let's see the command syntax itself:
- `@call` is the command name, `/add` is a switch. You can read the help of the command to get the
help of available switches and a brief overview of syntax.
- We then enter the object's name, here "a merchant". You can enter the ID too ("#3" in my case),
which is useful to edit the object when you're not in the same room. You can even enter part of the
name, as usual.
- An equal sign, a simple separator.
- The event's name. Here, it's "say". The available events are displayed when you use `@call`
without switch.
- After a space, we enter the conditions in which this callback should be called. Here, the
conditions represent what the other character should say. We enter "hello". Meaning that if
someone says something containing "hello" in the room, the callback we are now creating will be
called.
When you enter this command, you should see something like this:
```
After another character has said something in the character's room.
This event is called right after another character has said
something in the same location. The action cannot be prevented
at this moment. Instead, this event is ideal to create keywords
that would trigger a character (like a NPC) in doing something
if a specific phrase is spoken in the same location.
To use this event, you have to specify a list of keywords as
parameters that should be present, as separate words, in the
spoken phrase. For instance, you can set a callback that would
fire if the phrase spoken by the character contains "menu" or
"dinner" or "lunch":
@call/add ... = say menu, dinner, lunch
Then if one of the words is present in what the character says,
this callback will fire.
Variables you can use in this event:
speaker: the character speaking in this room.
character: the character connected to this event.
message: the text having been spoken by the character.
```
That's some list of information. What's most important to us now is:
- The "say" event is called whenever someone else speaks in the room.
- We can set callbacks to fire when specific keywords are present in the phrase by putting them as
additional parameters. Here we have set this parameter to "hello". We can have several keywords
separated by a comma (we'll see this in more details later).
- We have three default variables we can use in this callback: `speaker` which contains the
character who speaks, `character` which contains the character who's modified by the in-game Python
system (here, or merchant), and `message` which contains the spoken phrase.
This concept of variables is important. If it makes things more simple to you, think of them as
parameters in a function: they can be used inside of the function body because they have been set
when the function was called.
This command has opened an editor where we can type our Python code.
```
----------Line Editor [Callback say of a merchant]--------------------------------
01|
----------[l:01 w:000 c:0000]------------(:h for help)----------------------------
```
For our first test, let's type something like:
```python
character.location.msg_contents("{character} shrugs and says: 'well, yes, hello to you!'",
mapping=dict(character=character))
```
Once you have entered this line, you can type `:wq` to save the editor and quit it.
And now if you use the "say" command with a message containing "hello":
```
You say, "Hello sir merchant!"
a merchant(#3) shrugs and says: 'well, yes, hello to you!'
```
If you say something that doesn't contain "hello", our callback won't execute.
**In summary**:
1. When we say something in the room, using the "say" command, the "say" event of all characters
(except us) is called.
2. The in-game Python system looks at what we have said, and checks whether one of our callbacks in
the "say" event contains a keyword that we have spoken.
3. If so, call it, defining the event variables as we have seen.
4. The callback is then executed as normal Python code. Here we have called the `msg_contents`
method on the character's location (probably a room) to display a message to the entire room. We
have also used mapping to easily display the character's name. This is not specific to the in-game
Python system. If you feel overwhelmed by the code we've used, just shorten it and use something
more simple, for instance:
```python
speaker.msg("You have said something to me.")
```
## The same callback for several keywords
It's easy to create a callback that will be triggered if the sentence contains one of several
keywords.
@call/add merchant = say trade, trader, goods
And in the editor that opens:
```python
character.location.msg_contents("{character} says: 'Ho well, trade's fine as long as roads are
safe.'", mapping=dict(character=character))
```
Then you can say something with either "trade", "trader" or "goods" in your sentence, which should
call the callback:
```
You say, "and how is your trade going?"
a merchant(#3) says: 'Ho well, trade's fine as long as roads are safe.'
```
We can set several keywords when adding the callback. We just need to separate them with commas.
## A longer callback
So far, we have only set one line in our callbacks. Which is useful, but we often need more. For
an entire dialogue, you might want to do a bit more than that.
@call/add merchant = say bandit, bandits
And in the editor you can paste the following lines:
```python
character.location.msg_contents("{character} says: 'Bandits he?'",
mapping=dict(character=character))
character.location.msg_contents("{character} scratches his head, considering.",
mapping=dict(character=character))
character.location.msg_contents("{character} whispers: 'Aye, saw some of them, north from here. No
trouble o' mine, but...'", mapping=dict(character=character))
speaker.msg("{character} looks at you more
closely.".format(character=character.get_display_name(speaker)))
speaker.msg("{character} continues in a low voice: 'Ain't my place to say, but if you need to find
'em, they're encamped some distance away from the road, I guess near a cave or
something.'.".format(character=character.get_display_name(speaker)))
```
Now try to ask the merchant about bandits:
```
You say, "have you seen bandits?"
a merchant(#3) says: 'Bandits he?'
a merchant(#3) scratches his head, considering.
a merchant(#3) whispers: 'Aye, saw some of them, north from here. No trouble o' mine, but...'
a merchant(#3) looks at you more closely.
a merchant(#3) continues in a low voice: 'Ain't my place to say, but if you need to find 'em,
they're encamped some distance away from the road, I guess near a cave or something.'.
```
Notice here that the first lines of dialogue are spoken to the entire room, but then the merchant is
talking directly to the speaker, and only the speaker hears it. There's no real limit to what you
can do with this.
- You can set a mood system, storing attributes in the NPC itself to tell you in what mood he is,
which will influence the information he will give... perhaps the accuracy of it as well.
- You can add random phrases spoken in some context.
- You can use other actions (you're not limited to having the merchant say something, you can ask
him to move, gives you something, attack if you have a combat system, or whatever else).
- The callbacks are in pure Python, so you can write conditions or loops.
- You can add in "pauses" between some instructions using chained events. This tutorial won't
describe how to do that however. You already have a lot to play with.
## Tutorial F.A.Q.
- **Q:** can I create several characters who would answer to specific dialogue?
- **A:** of course. Te in-game Python system is so powerful because you can set unique code for
various objects. You can have several characters answering to different things. You can even have
different characters in the room answering to greetings. All callbacks will be executed one after
another.
- **Q:** can I have two characters answering to the same dialogue in exactly the same way?
- **A:** It's possible but not so easy to do. Usually, event grouping is set in code, and depends
on different games. However, if it is for some infrequent occurrences, it's easy to do using
[chained
events](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md#chained-
events).
- **Q:** is it possible to deploy callbacks on all characters sharing the same prototype?
- **A:** not out of the box. This depends on individual settings in code. One can imagine that all
characters of some type would share some events, but this is game-specific. Rooms of the same zone
could share the same events as well. It is possible to do but requires modification of the source
code.
- Next tutorial: [adding a voice-operated elevator with events](A-voice-operated-elevator-using-
events).

View file

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

View file

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

View file

@ -0,0 +1,106 @@
# Tutorial World Introduction
The *Tutorial World* is a small and functioning MUD-style game world. It is intended to be
deconstructed and used as a way to learn Evennia. The game consists of a single-player quest and
has some 20 rooms that you can explore as you seek to discover the whereabouts of a mythical weapon.
The source code is fully documented. You can find the whole thing in
`evennia/contrib/tutorial_world/`.
Some features exemplified by the tutorial world:
- Tutorial command, giving "behind-the-scenes" help for every room and some of the special objects
- Rooms with custom `return_appearance` to show details.
- Hidden exits
- Objects with multiple custom interactions
- Large-area rooms
- Outdoor weather rooms
- Dark room, needing light source
- Puzzle object
- Multi-room puzzle
- Aggressive mobile with roam, pursue and battle state-engine AI
- Weapons, also used by mobs
- Simple combat system with attack/defend commands
- Object spawning
- Teleporter trap rooms
## Install
The tutorial world consists of a few modules in `evennia/contrib/tutorial_world/` containing custom
[Typeclasses](Typeclasses) for [rooms and objects](Objects) and associated [Commands](Commands).
These reusable bits and pieces are then put together into a functioning game area ("world" is maybe
too big a word for such a small zone) using a [batch script](Batch-Processors) called `build.ev`. To
install, log into the server as the superuser (user #1) and run:
@batchcommand tutorial_world.build
The world will be built (this might take a while, so don't rerun the command even if it seems the
system has frozen). After finishing you will end up back in Limbo with a new exit called `tutorial`.
An alternative is
@batchcommand/interactive tutorial_world.build
with the /interactive switch you are able to step through the building process at your own pace to
see what happens in detail.
To play the tutorial "correctly", you should *not* do so as superuser. The reason for this is that
many game systems ignore the presence of a superuser and will thus not work as normal. Use the
`@quell` command to limit your powers or log out and reconnect as a different user. As superuser you
can of course examine things "under the hood" later if you want.
## Gameplay
![the castle off the moor](https://images-wixmp-
ed30a86b8c4ca887773594c2.wixmp.com/f/22916c25-6299-453d-a221-446ec839f567/da2pmzu-46d63c6d-9cdc-41dd-87d6-1106db5a5e1a.jpg/v1/fill/w_600,h_849,q_75,strp/the_castle_off_the_moor_by_griatch_art_da2pmzu-
fullview.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOiIsImlzcyI6InVybjphcHA6Iiwib2JqIjpbW3siaGVpZ2h0IjoiPD04NDkiLCJwYXRoIjoiXC9mXC8yMjkxNmMyNS02Mjk5LTQ1M2QtYTIyMS00NDZlYzgzOWY1NjdcL2RhMnBtenUtNDZkNjNjNmQtOWNkYy00MWRkLTg3ZDYtMTEwNmRiNWE1ZTFhLmpwZyIsIndpZHRoIjoiPD02MDAifV1dLCJhdWQiOlsidXJuOnNlcnZpY2U6aW1hZ2Uub3BlcmF0aW9ucyJdfQ.omuS3D1RmFiZCy9OSXiIita-
HxVGrBok3_7asq0rflw)
*To get into the mood of this miniature quest, imagine you are an adventurer out to find fame and
fortune. You have heard rumours of an old castle ruin by the coast. In its depth a warrior princess
was buried together with her powerful magical weapon - a valuable prize, if it's true. Of course
this is a chance to adventure that you cannot turn down!*
*You reach the ocean in the midst of a raging thunderstorm. With wind and rain screaming in your
face you stand where the moor meets the sea along a high, rocky coast ...*
- Look at everything.
- Some objects are interactive in more than one way. Use the normal `help` command to get a feel for
which commands are available at any given time. (use the command `tutorial` to get insight behind
the scenes of the tutorial).
- In order to fight, you need to first find some type of weapon.
- *slash* is a normal attack
- *stab* launches an attack that makes more damage but has a lower chance to hit.
- *defend* will lower the chance to taking damage on your enemy's next attack.
- You *can* run from a fight that feels too deadly. Expect to be chased though.
- Being defeated is a part of the experience ...
## Uninstall
Uninstalling the tutorial world basically means deleting all the rooms and objects it consists of.
First, move out of the tutorial area.
@find tut#01
@find tut#16
This should locate the first and last rooms created by `build.ev` - *Intro* and *Outro*. If you
installed normally, everything created between these two numbers should be part of the tutorial.
Note their dbref numbers, for example 5 and 80. Next we just delete all objects in that range:
@del 5-80
You will see some errors since some objects are auto-deleted and so cannot be found when the delete
mechanism gets to them. That's fine. You should have removed the tutorial completely once the
command finishes.
## Notes
When reading and learning from the code, keep in mind that *Tutorial World* was created with a very
specific goal: to install easily and to not permanently modify the rest of the server. It therefore
goes to some length to use only temporary solutions and to clean up after
itself.