mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Start refactor howtos
This commit is contained in:
parent
f175609cf3
commit
af5884705e
9 changed files with 102 additions and 96 deletions
|
|
@ -52,7 +52,7 @@ 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)
|
||||
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
|
||||
|
|
|
|||
349
docs/source/Howto/Starting/Coordinates.md
Normal file
349
docs/source/Howto/Starting/Coordinates.md
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
# Coordinates
|
||||
|
||||
# Adding room coordinates in your game
|
||||
|
||||
This tutorial is moderately difficult in content. You might want to be familiar and at ease with
|
||||
some Python concepts (like properties) and possibly Django concepts (like queries), although this
|
||||
tutorial will try to walk you through the process and give enough explanations each time. If you
|
||||
don't feel very confident with math, don't hesitate to pause, go to the example section, which shows
|
||||
a tiny map, and try to walk around the code or read the explanation.
|
||||
|
||||
Evennia doesn't have a coordinate system by default. Rooms and other objects are linked by location
|
||||
and content:
|
||||
|
||||
- An object can be in a location, that is, another object. Like an exit in a room.
|
||||
- An object can access its content. A room can see what objects uses it as location (that would
|
||||
include exits, rooms, characters and so on).
|
||||
|
||||
This system allows for a lot of flexibility and, fortunately, can be extended by other systems.
|
||||
Here, I offer you a way to add coordinates to every room in a way most compliant with Evennia
|
||||
design. This will also show you how to use coordinates, find rooms around a given point for
|
||||
instance.
|
||||
|
||||
## Coordinates as tags
|
||||
|
||||
The first concept might be the most surprising at first glance: we will create coordinates as
|
||||
[tags](../../Component/Tags).
|
||||
|
||||
> Why not attributes, wouldn't that be easier?
|
||||
|
||||
It would. We could just do something like `room.db.x = 3`. The advantage of using tags is that it
|
||||
will be easy and effective to search. Although this might not seem like a huge advantage right now,
|
||||
with a database of thousands of rooms, it might make a difference, particularly if you have a lot of
|
||||
things based on coordinates.
|
||||
|
||||
Rather than giving you a step-by-step process, I'll show you the code. Notice that we use
|
||||
properties to easily access and update coordinates. This is a Pythonic approach. Here's our first
|
||||
`Room` class, that you can modify in `typeclasses/rooms.py`:
|
||||
|
||||
```python
|
||||
# in typeclasses/rooms.py
|
||||
|
||||
from evennia import DefaultRoom
|
||||
|
||||
class Room(DefaultRoom):
|
||||
"""
|
||||
Rooms are like any Object, except their location is None
|
||||
(which is default). They also use basetype_setup() to
|
||||
add locks so they cannot be puppeted or picked up.
|
||||
(to change that, use at_object_creation instead)
|
||||
|
||||
See examples/object.py for a list of
|
||||
properties and methods available on all Objects.
|
||||
"""
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
"""Return the X coordinate or None."""
|
||||
x = self.tags.get(category="coordx")
|
||||
return int(x) if isinstance(x, str) else None
|
||||
|
||||
@x.setter
|
||||
def x(self, x):
|
||||
"""Change the X coordinate."""
|
||||
old = self.tags.get(category="coordx")
|
||||
if old is not None:
|
||||
self.tags.remove(old, category="coordx")
|
||||
if x is not None:
|
||||
self.tags.add(str(x), category="coordx")
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
"""Return the Y coordinate or None."""
|
||||
y = self.tags.get(category="coordy")
|
||||
return int(y) if isinstance(y, str) else None
|
||||
|
||||
@y.setter
|
||||
def y(self, y):
|
||||
"""Change the Y coordinate."""
|
||||
old = self.tags.get(category="coordy")
|
||||
if old is not None:
|
||||
self.tags.remove(old, category="coordy")
|
||||
if y is not None:
|
||||
self.tags.add(str(y), category="coordy")
|
||||
|
||||
@property
|
||||
def z(self):
|
||||
"""Return the Z coordinate or None."""
|
||||
z = self.tags.get(category="coordz")
|
||||
return int(z) if isinstance(z, str) else None
|
||||
|
||||
@z.setter
|
||||
def z(self, z):
|
||||
"""Change the Z coordinate."""
|
||||
old = self.tags.get(category="coordz")
|
||||
if old is not None:
|
||||
self.tags.remove(old, category="coordz")
|
||||
if z is not None:
|
||||
self.tags.add(str(z), category="coordz")
|
||||
```
|
||||
|
||||
If you aren't familiar with the concept of properties in Python, I encourage you to read a good
|
||||
tutorial on the subject. [This article on Python properties](https://www.programiz.com/python-
|
||||
programming/property)
|
||||
is well-explained and should help you understand the idea.
|
||||
|
||||
Let's look at our properties for `x`. First of all is the read property.
|
||||
|
||||
```python
|
||||
@property
|
||||
def x(self):
|
||||
"""Return the X coordinate or None."""
|
||||
x = self.tags.get(category="coordx")
|
||||
return int(x) if isinstance(x, str) else None
|
||||
```
|
||||
|
||||
What it does is pretty simple:
|
||||
|
||||
1. It gets the tag of category `"coordx"`. It's the tag category where we store our X coordinate.
|
||||
The `tags.get` method will return `None` if the tag can't be found.
|
||||
2. We convert the value to an integer, if it's a `str`. Remember that tags can only contain `str`,
|
||||
so we'll need to convert it.
|
||||
|
||||
> I thought tags couldn't contain values?
|
||||
|
||||
Well, technically, they can't: they're either here or not. But using tag categories, as we have
|
||||
done, we get a tag, knowing only its category. That's the basic approach to coordinates in this
|
||||
tutorial.
|
||||
|
||||
Now, let's look at the method that will be called when we wish to set `x` in our room:
|
||||
|
||||
```python
|
||||
@x.setter
|
||||
def x(self, x):
|
||||
"""Change the X coordinate."""
|
||||
old = self.tags.get(category="coordx")
|
||||
if old is not None:
|
||||
self.tags.remove(old, category="coordx")
|
||||
if x is not None:
|
||||
self.tags.add(str(x), category="coordx")
|
||||
```
|
||||
|
||||
1. First, we remove the old X coordinate, if it exists. Otherwise, we'd end up with two tags in our
|
||||
room with "coordx" as their category, which wouldn't do at all.
|
||||
2. Then we add the new tag, giving it the proper category.
|
||||
|
||||
> Now what?
|
||||
|
||||
If you add this code and reload your game, once you're logged in with a character in a room as its
|
||||
location, you can play around:
|
||||
|
||||
```
|
||||
@py here.x
|
||||
@py here.x = 0
|
||||
@py here.y = 3
|
||||
@py here.z = -2
|
||||
@py here.z = None
|
||||
```
|
||||
|
||||
The code might not be that easy to read, but you have to admit it's fairly easy to use.
|
||||
|
||||
## Some additional searches
|
||||
|
||||
Having coordinates is useful for several reasons:
|
||||
|
||||
1. It can help in shaping a truly logical world, in its geography, at least.
|
||||
2. It can allow to look for specific rooms at given coordinates.
|
||||
3. It can be good in order to quickly find the rooms around a location.
|
||||
4. It can even be great in path-finding (finding the shortest path between two rooms).
|
||||
|
||||
So far, our coordinate system can help with 1., but not much else. Here are some methods that we
|
||||
could add to the `Room` typeclass. These methods will just be search methods. Notice that they are
|
||||
class methods, since we want to get rooms.
|
||||
|
||||
### Finding one room
|
||||
|
||||
First, a simple one: how to find a room at a given coordinate? Say, what is the room at X=0, Y=0,
|
||||
Z=0?
|
||||
|
||||
```python
|
||||
class Room(DefaultRoom):
|
||||
# ...
|
||||
@classmethod
|
||||
def get_room_at(cls, x, y, z):
|
||||
"""
|
||||
Return the room at the given location or None if not found.
|
||||
|
||||
Args:
|
||||
x (int): the X coord.
|
||||
y (int): the Y coord.
|
||||
z (int): the Z coord.
|
||||
|
||||
Return:
|
||||
The room at this location (Room) or None if not found.
|
||||
|
||||
"""
|
||||
rooms = cls.objects.filter(
|
||||
db_tags__db_key=str(x), db_tags__db_category="coordx").filter(
|
||||
db_tags__db_key=str(y), db_tags__db_category="coordy").filter(
|
||||
db_tags__db_key=str(z), db_tags__db_category="coordz")
|
||||
if rooms:
|
||||
return rooms[0]
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
This solution includes a bit of [Django
|
||||
queries](https://docs.djangoproject.com/en/1.11/topics/db/queries/).
|
||||
Basically, what we do is reach for the object manager and search for objects with the matching tags.
|
||||
Again, don't spend too much time worrying about the mechanism, the method is quite easy to use:
|
||||
|
||||
```
|
||||
Room.get_room_at(5, 2, -3)
|
||||
```
|
||||
|
||||
Notice that this is a class method: you will call it from `Room` (the class), not an instance.
|
||||
Though you still can:
|
||||
|
||||
@py here.get_room_at(3, 8, 0)
|
||||
|
||||
### Finding several rooms
|
||||
|
||||
Here's another useful method that allows us to look for rooms around a given coordinate. This is
|
||||
more advanced search and doing some calculation, beware! Look at the following section if you're
|
||||
lost.
|
||||
|
||||
```python
|
||||
from math import sqrt
|
||||
|
||||
class Room(DefaultRoom):
|
||||
|
||||
# ...
|
||||
|
||||
@classmethod
|
||||
def get_rooms_around(cls, x, y, z, distance):
|
||||
"""
|
||||
Return the list of rooms around the given coordinates.
|
||||
|
||||
This method returns a list of tuples (distance, room) that
|
||||
can easily be browsed. This list is sorted by distance (the
|
||||
closest room to the specified position is always at the top
|
||||
of the list).
|
||||
|
||||
Args:
|
||||
x (int): the X coord.
|
||||
y (int): the Y coord.
|
||||
z (int): the Z coord.
|
||||
distance (int): the maximum distance to the specified position.
|
||||
|
||||
Returns:
|
||||
A list of tuples containing the distance to the specified
|
||||
position and the room at this distance. Several rooms
|
||||
can be at equal distance from the position.
|
||||
|
||||
"""
|
||||
# Performs a quick search to only get rooms in a square
|
||||
x_r = list(reversed([str(x - i) for i in range(0, distance + 1)]))
|
||||
x_r += [str(x + i) for i in range(1, distance + 1)]
|
||||
y_r = list(reversed([str(y - i) for i in range(0, distance + 1)]))
|
||||
y_r += [str(y + i) for i in range(1, distance + 1)]
|
||||
z_r = list(reversed([str(z - i) for i in range(0, distance + 1)]))
|
||||
z_r += [str(z + i) for i in range(1, distance + 1)]
|
||||
wide = cls.objects.filter(
|
||||
db_tags__db_key__in=x_r, db_tags__db_category="coordx").filter(
|
||||
db_tags__db_key__in=y_r, db_tags__db_category="coordy").filter(
|
||||
db_tags__db_key__in=z_r, db_tags__db_category="coordz")
|
||||
|
||||
# We now need to filter down this list to find out whether
|
||||
# these rooms are really close enough, and at what distance
|
||||
# In short: we change the square to a circle.
|
||||
rooms = []
|
||||
for room in wide:
|
||||
x2 = int(room.tags.get(category="coordx"))
|
||||
y2 = int(room.tags.get(category="coordy"))
|
||||
z2 = int(room.tags.get(category="coordz"))
|
||||
distance_to_room = sqrt(
|
||||
(x2 - x) ** 2 + (y2 - y) ** 2 + (z2 - z) ** 2)
|
||||
if distance_to_room <= distance:
|
||||
rooms.append((distance_to_room, room))
|
||||
|
||||
# Finally sort the rooms by distance
|
||||
rooms.sort(key=lambda tup: tup[0])
|
||||
return rooms
|
||||
```
|
||||
|
||||
This gets more serious.
|
||||
|
||||
1. We have specified coordinates as parameters. We determine a broad range using the distance.
|
||||
That is, for each coordinate, we create a list of possible matches. See the example below.
|
||||
2. We then search for the rooms within this broader range. It gives us a square
|
||||
around our location. Some rooms are definitely outside the range. Again, see the example below
|
||||
to follow the logic.
|
||||
3. We filter down the list and sort it by distance from the specified coordinates.
|
||||
|
||||
Notice that we only search starting at step 2. Thus, the Django search doesn't look and cache all
|
||||
objects, just a wider range than what would be really necessary. This method returns a circle of
|
||||
coordinates around a specified point. Django looks for a square. What wouldn't fit in the circle
|
||||
is removed at step 3, which is the only part that includes systematic calculation. This method is
|
||||
optimized to be quick and efficient.
|
||||
|
||||
### An example
|
||||
|
||||
An example might help. Consider this very simple map (a textual description follows):
|
||||
|
||||
```
|
||||
4 A B C D
|
||||
3 E F G H
|
||||
2 I J K L
|
||||
1 M N O P
|
||||
1 2 3 4
|
||||
```
|
||||
|
||||
The X coordinates are given below. The Y coordinates are given on the left. This is a simple
|
||||
square with 16 rooms: 4 on each line, 4 lines of them. All the rooms are identified by letters in
|
||||
this example: the first line at the top has rooms A to D, the second E to H, the third I to L and
|
||||
the fourth M to P. The bottom-left room, X=1 and Y=1, is M. The upper-right room X=4 and Y=4 is D.
|
||||
|
||||
So let's say we want to find all the neighbors, distance 1, from the room J. J is at X=2, Y=2.
|
||||
|
||||
So we use:
|
||||
|
||||
Room.get_rooms_around(x=2, y=2, z=0, distance=1)
|
||||
# we'll assume a z coordinate of 0 for simplicity
|
||||
|
||||
1. First, this method gets all the rooms in a square around J. So it gets E F G, I J K, M N O. If
|
||||
you want, draw the square around these coordinates to see what's happening.
|
||||
2. Next, we browse over this list and check the real distance between J (X=2, Y=2) and the room.
|
||||
The four corners of the square are not in this circle. For instance, the distance between J and M
|
||||
is not 1. If you draw a circle of center J and radius 1, you'll notice that the four corners of our
|
||||
square (E, G, M and O) are not in this circle. So we remove them.
|
||||
3. We sort by distance from J.
|
||||
|
||||
So in the end we might obtain something like this:
|
||||
|
||||
```
|
||||
[
|
||||
(0, J), # yes, J is part of this circle after all, with a distance of 0
|
||||
(1, F),
|
||||
(1, I),
|
||||
(1, K),
|
||||
(1, N),
|
||||
]
|
||||
```
|
||||
|
||||
You can try with more examples if you want to see this in action.
|
||||
|
||||
### To conclude
|
||||
|
||||
You can definitely use this system to map other objects, not just rooms. You can easily remove the
|
||||
`Z coordinate too, if you simply need X and Y.
|
||||
218
docs/source/Howto/Starting/Game-Planning.md
Normal file
218
docs/source/Howto/Starting/Game-Planning.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# Game Planning
|
||||
|
||||
|
||||
So you have Evennia up and running. You have a great game idea in mind. Now it's time to start
|
||||
cracking! But where to start? Here are some ideas for a workflow. Note that the suggestions on this
|
||||
page are just that - suggestions. Also, they are primarily aimed at a lone hobby designer or a small
|
||||
team developing a game in their free time. There is an article in the Imaginary Realities e-zine
|
||||
which was written by the Evennia lead dev. It focuses more on you finding out your motivations for
|
||||
making a game - you can [read the article here](http://journal.imaginary-
|
||||
realities.com/volume-07/issue-03/where-do-i-begin/index.html).
|
||||
|
||||
|
||||
Below are some minimal steps for getting the first version of a new game world going with players.
|
||||
It's worth to at least make the attempt to do these steps in order even if you are itching to jump
|
||||
ahead in the development cycle. On the other hand, you should also make sure to keep your work fun
|
||||
for you, or motivation will falter. Making a full game is a lot of work as it is, you'll need all
|
||||
your motivation to make it a reality.
|
||||
|
||||
Remember that *99.99999% of all great game ideas never lead to a game*. Especially not to an online
|
||||
game that people can actually play and enjoy. So our first all overshadowing goal is to beat those
|
||||
odds and get *something* out the door! Even if it's a scaled-down version of your dream game,
|
||||
lacking many "must-have" features! It's better to get it out there and expand on it later than to
|
||||
code in isolation forever until you burn out, lose interest or your hard drive crashes.
|
||||
|
||||
Like is common with online games, getting a game out the door does not mean you are going to be
|
||||
"finished" with the game - most MUDs add features gradually over the course of years - it's often
|
||||
part of the fun!
|
||||
|
||||
## Planning (step 1)
|
||||
|
||||
This is what you do before having coded a single line or built a single room. Many prospective game
|
||||
developers are very good at *parts* of this process, namely in defining what their world is "about":
|
||||
The theme, the world concept, cool monsters and so on. It is by all means very important to define
|
||||
what is the unique appeal of your game. But it's unfortunately not enough to make your game a
|
||||
reality. To do that you must also have an idea of how to actually map those great ideas onto
|
||||
Evennia.
|
||||
|
||||
A good start is to begin by planning out the basic primitives of the game and what they need to be
|
||||
able to do. Below are a far-from-complete list of examples (and for your first version you should
|
||||
definitely try for a much shorter list):
|
||||
|
||||
### Systems
|
||||
|
||||
These are the behind-the-scenes features that exist in your game, often without being represented by
|
||||
a specific in-game object.
|
||||
|
||||
- Should your game rules be enforced by coded systems or are you planning for human game masters to
|
||||
run and arbitrate rules?
|
||||
- What are the actual mechanical game rules? How do you decide if an action succeeds or fails? What
|
||||
"rolls" does the game need to be able to do? Do you base your game off an existing system or make up
|
||||
your own?
|
||||
- Does the flow of time matter in your game - does night and day change? What about seasons? Maybe
|
||||
your magic system is affected by the phase of the moon?
|
||||
- Do you want changing, global weather? This might need to operate in tandem over a large number of
|
||||
rooms.
|
||||
- Do you want a game-wide economy or just a simple barter system? Or no formal economy at all?
|
||||
- Should characters be able to send mail to each other in-game?
|
||||
- Should players be able to post on Bulletin boards?
|
||||
- What is the staff hierarchy in your game? What powers do you want your staff to have?
|
||||
- What should a Builder be able to build and what commands do they need in order to do that?
|
||||
- etc.
|
||||
|
||||
### Rooms
|
||||
|
||||
Consider the most basic room in your game.
|
||||
|
||||
- Is a simple description enough or should the description be able to change (such as with time, by
|
||||
light conditions, weather or season)?
|
||||
- Should the room have different statuses? Can it have smells, sounds? Can it be affected by
|
||||
dramatic weather, fire or magical effects? If so, how would this affect things in the room? Or are
|
||||
these things something admins/game masters should handle manually?
|
||||
- Can objects be hidden in the room? Can a person hide in the room? How does the room display this?
|
||||
- etc.
|
||||
|
||||
### Objects
|
||||
|
||||
Consider the most basic (non-player-controlled) object in your game.
|
||||
|
||||
- How numerous are your objects? Do you want large loot-lists or are objects just role playing props
|
||||
created on demand?
|
||||
- Does the game use money? If so, is each coin a separate object or do you just store a bank account
|
||||
value?
|
||||
- What about multiple identical objects? Do they form stacks and how are those stacks handled in
|
||||
that case?
|
||||
- Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
- Can objects be broken? If so, does it have a health value? Is burning it causing the same damage
|
||||
as smashing it? Can it be repaired?
|
||||
- Is a weapon a specific type of object or are you supposed to be able to fight with a chair too?
|
||||
Can you fight with a flower or piece of paper as well?
|
||||
- NPCs/mobs are also objects. Should they just stand around or should they have some sort of AI?
|
||||
- Are NPCs/mobs differet entities? How is an Orc different from a Kobold, in code - are they the
|
||||
same object with different names or completely different types of objects, with custom code?
|
||||
- Should there be NPCs giving quests? If so, how would you track quest status and what happens when
|
||||
multiple players try to do the same quest? Do you use instances or some other mechanism?
|
||||
- etc.
|
||||
|
||||
### Characters
|
||||
|
||||
These are the objects controlled directly by Players.
|
||||
|
||||
- Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
- How does a Player create their Character? A Character-creation screen? Answering questions?
|
||||
Filling in a form?
|
||||
- Do you want to use classes (like "Thief", "Warrior" etc) or some other system, like Skill-based?
|
||||
- How do you implement different "classes" or "races"? Are they separate types of objects or do you
|
||||
simply load different stats on a basic object depending on what the Player wants?
|
||||
- If a Character can hide in a room, what skill will decide if they are detected?
|
||||
- What skill allows a Character to wield a weapon and hit? Do they need a special skill to wield a
|
||||
chair rather than a sword?
|
||||
- Does a Character need a Strength attribute to tell how much they can carry or which objects they
|
||||
can smash?
|
||||
- What does the skill tree look like? Can a Character gain experience to improve? By killing
|
||||
enemies? Solving quests? By roleplaying?
|
||||
- etc.
|
||||
|
||||
A MUD's a lot more involved than you would think and these things hang together in a complex web. It
|
||||
can easily become overwhelming and it's tempting to want *all* functionality right out of the door.
|
||||
Try to identify the basic things that "make" your game and focus *only* on them for your first
|
||||
release. Make a list. Keep future expansions in mind but limit yourself.
|
||||
|
||||
## Coding (step 2)
|
||||
|
||||
This is the actual work of creating the "game" part of your game. Many "game-designer" types tend to
|
||||
gloss over this bit and jump directly to **World Building**. Vice versa, many "game-coder" types
|
||||
tend to jump directly to this part without doing the **Planning** first. Neither way is good and
|
||||
*will* lead to you having to redo all your hard work at least once, probably more.
|
||||
|
||||
Evennia's [Developer Central](Developer-Central) tries to help you with this bit of development. We
|
||||
also have a slew of [Tutorials](Tutorials) with worked examples. Evennia tries hard to make this
|
||||
part easier for you, but there is no way around the fact that if you want anything but a very basic
|
||||
Talker-type game you *will* have to bite the bullet and code your game (or find a coder willing to
|
||||
do it for you).
|
||||
|
||||
Even if you won't code anything yourself, as a designer you need to at least understand the basic
|
||||
paradigms of Evennia, such as [Objects](../../Component/Objects), [Commands](../../Component/Commands) and [Scripts](../../Component/Scripts) and
|
||||
how they hang together. We recommend you go through the [Tutorial World](Tutorial-World-
|
||||
Introduction) in detail (as well as glancing at its code) to get at least a feel for what is
|
||||
involved behind the scenes. You could also look through the tutorial for [building a game from
|
||||
scratch](Tutorial-for-basic-MUSH-like-game).
|
||||
|
||||
During Coding you look back at the things you wanted during the **Planning** phase and try to
|
||||
implement them. Don't be shy to update your plans if you find things easier/harder than you thought.
|
||||
The earlier you revise problems, the easier they will be to fix.
|
||||
|
||||
A good idea is to host your code online (publicly or privately) using version control. Not only will
|
||||
this make it easy for multiple coders to collaborate (and have a bug-tracker etc), it also means
|
||||
your work is backed up at all times. The [Version Control](../../Coding/Version-Control) tutorial has
|
||||
instructions for setting up a sane developer environment with proper version control.
|
||||
|
||||
### "Tech Demo" Building
|
||||
|
||||
This is an integral part of your Coding. It might seem obvious to experienced coders, but it cannot
|
||||
be emphasized enough that you should *test things on a small scale* before putting your untested
|
||||
code into a large game-world. The earlier you test, the easier and cheaper it will be to fix bugs
|
||||
and even rework things that didn't work out the way you thought they would. You might even have to
|
||||
go back to the **Planning** phase if your ideas can't handle their meet with reality.
|
||||
|
||||
This means building singular in-game examples. Make one room and one object of each important type
|
||||
and test so they work correctly in isolation. Then add more if they are supposed to interact with
|
||||
each other in some way. Build a small series of rooms to test how mobs move around ... and so on. In
|
||||
short, a test-bed for your growing code. It should be done gradually until you have a fully
|
||||
functioning (if not guaranteed bug-free) miniature tech demo that shows *all* the features you want
|
||||
in the first release of your game. There does not need to be any game play or even a theme to your
|
||||
tests, this is only for you and your co-coders to see. The more testing you do on this small scale,
|
||||
the less headaches you will have in the next phase.
|
||||
|
||||
## World Building (step 3)
|
||||
|
||||
Up until this point we've only had a few tech-demo objects in the database. This step is the act of
|
||||
populating the database with a larger, thematic world. Too many would-be developers jump to this
|
||||
stage too soon (skipping the **Coding** or even **Planning** stages). What if the rooms you build
|
||||
now doesn't include all the nice weather messages the code grows to support? Or the way you store
|
||||
data changes under the hood? Your building work would at best require some rework and at worst you
|
||||
would have to redo the whole thing. And whereas Evennia's typeclass system does allow you to edit
|
||||
the properties of existing objects, some hooks are only called at object creation ... Suffice to
|
||||
say you are in for a *lot* of unnecessary work if you build stuff en masse without having the
|
||||
underlying code systems in some reasonable shape first.
|
||||
|
||||
So before starting to build, the "game" bit (**Coding** + **Testing**) should be more or less
|
||||
**complete**, *at least to the level of your initial release*.
|
||||
|
||||
Before starting to build, you should also plan ahead again. Make sure it is clear to yourself and
|
||||
your eventual builders just which parts of the world you want for your initial release. Establish
|
||||
for everyone which style, quality and level of detail you expect. Your goal should *not* be to
|
||||
complete your entire world in one go. You want just enough to make the game's "feel" come across.
|
||||
You want a minimal but functioning world where the intended game play can be tested and roughly
|
||||
balanced. You can always add new areas later.
|
||||
|
||||
During building you get free and extensive testing of whatever custom build commands and systems you
|
||||
have made at this point. Since Building often involves different people than those Coding, you also
|
||||
get a chance to hear if some things are hard to understand or non-intuitive. Make sure to respond
|
||||
to this feedback.
|
||||
|
||||
|
||||
## Alpha Release
|
||||
|
||||
As mentioned, don't hold onto your world more than necessary. *Get it out there* with a huge *Alpha*
|
||||
flag and let people try it! Call upon your alpha-players to try everything - they *will* find ways
|
||||
to break your game in ways that you never could have imagined. In Alpha you might be best off to
|
||||
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
|
||||
feedback and bug reports (there *will* be bugs, there is no way around it). Follow the quick
|
||||
instructions for [Online Setup](../../Setup/Online-Setup) to make your game visible online. If you hadn't
|
||||
already, make sure to put up your game on the [Evennia game index](http://games.evennia.com/) so
|
||||
people know it's in the works (actually, even pre-alpha games are allowed in the index so don't be
|
||||
shy)!
|
||||
|
||||
## Beta Release/Perpetual Beta
|
||||
|
||||
Once things stabilize in Alpha you can move to *Beta* and let more people in. Many MUDs are in
|
||||
[perpetual beta](http://en.wikipedia.org/wiki/Perpetual_beta), meaning they are never considered
|
||||
"finished", but just repeat the cycle of Planning, Coding, Testing and Building over and over as new
|
||||
features get implemented or Players come with suggestions. As the game designer it is now up to you
|
||||
to gradually perfect your vision.
|
||||
|
||||
## Congratulate yourself!
|
||||
|
||||
You are worthy of a celebration since at this point you have joined the small, exclusive crowd who
|
||||
have made their dream game a reality!
|
||||
265
docs/source/Howto/Starting/Implementing-a-game-rule-system.md
Normal file
265
docs/source/Howto/Starting/Implementing-a-game-rule-system.md
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
# Implementing a game rule system
|
||||
|
||||
|
||||
The simplest way to create an online roleplaying game (at least from a code perspective) is to
|
||||
simply grab a paperback RPG rule book, get a staff of game masters together and start to run scenes
|
||||
with whomever logs in. Game masters can roll their dice in front of their computers and tell the
|
||||
players the results. This is only one step away from a traditional tabletop game and puts heavy
|
||||
demands on the staff - it is unlikely staff will be able to keep up around the clock even if they
|
||||
are very dedicated.
|
||||
|
||||
Many games, even the most roleplay-dedicated, thus tend to allow for players to mediate themselves
|
||||
to some extent. A common way to do this is to introduce *coded systems* - that is, to let the
|
||||
computer do some of the heavy lifting. A basic thing is to add an online dice-roller so everyone can
|
||||
make rolls and make sure noone is cheating. Somewhere at this level you find the most bare-bones
|
||||
roleplaying MUSHes.
|
||||
|
||||
The advantage of a coded system is that as long as the rules are fair the computer is too - it makes
|
||||
no judgement calls and holds no personal grudges (and cannot be accused of holding any). Also, the
|
||||
computer doesn't need to sleep and can always be online regardless of when a player logs on. The
|
||||
drawback is that a coded system is not flexible and won't adapt to the unprogrammed actions human
|
||||
players may come up with in role play. For this reason many roleplay-heavy MUDs do a hybrid
|
||||
variation - they use coded systems for things like combat and skill progression but leave role play
|
||||
to be mostly freeform, overseen by staff game masters.
|
||||
|
||||
Finally, on the other end of the scale are less- or no-roleplay games, where game mechanics (and
|
||||
thus player fairness) is the most important aspect. In such games the only events with in-game value
|
||||
are those resulting from code. Such games are very common and include everything from hack-and-slash
|
||||
MUDs to various tactical simulations.
|
||||
|
||||
So your first decision needs to be just what type of system you are aiming for. This page will try
|
||||
to give some ideas for how to organize the "coded" part of your system, however big that may be.
|
||||
|
||||
## Overall system infrastructure
|
||||
|
||||
We strongly recommend that you code your rule system as stand-alone as possible. That is, don't
|
||||
spread your skill check code, race bonus calculation, die modifiers or what have you all over your
|
||||
game.
|
||||
|
||||
- Put everything you would need to look up in a rule book into a module in `mygame/world`. Hide away
|
||||
as much as you can. Think of it as a black box (or maybe the code representation of an all-knowing
|
||||
game master). The rest of your game will ask this black box questions and get answers back. Exactly
|
||||
how it arrives at those results should not need to be known outside the box. Doing it this way
|
||||
makes it easier to change and update things in one place later.
|
||||
- Store only the minimum stuff you need with each game object. That is, if your Characters need
|
||||
values for Health, a list of skills etc, store those things on the Character - don't store how to
|
||||
roll or change them.
|
||||
- Next is to determine just how you want to store things on your Objects and Characters. You can
|
||||
choose to either store things as individual [Attributes](../../Component/Attributes), like `character.db.STR=34` and
|
||||
`character.db.Hunting_skill=20`. But you could also use some custom storage method, like a
|
||||
dictionary `character.db.skills = {"Hunting":34, "Fishing":20, ...}`. A much more fancy solution is
|
||||
to look at the Ainneve [Trait
|
||||
handler](https://github.com/evennia/ainneve/blob/master/world/traits.py). Finally you could even go
|
||||
with a [custom django model](../../Concept/New-Models). Which is the better depends on your game and the
|
||||
complexity of your system.
|
||||
- Make a clear [API](http://en.wikipedia.org/wiki/Application_programming_interface) into your
|
||||
rules. That is, make methods/functions that you feed with, say, your Character and which skill you
|
||||
want to check. That is, you want something similar to this:
|
||||
|
||||
```python
|
||||
from world import rules
|
||||
result = rules.roll_skill(character, "hunting")
|
||||
result = rules.roll_challenge(character1, character2, "swords")
|
||||
```
|
||||
|
||||
You might need to make these functions more or less complex depending on your game. For example the
|
||||
properties of the room might matter to the outcome of a roll (if the room is dark, burning etc).
|
||||
Establishing just what you need to send into your game mechanic module is a great way to also get a
|
||||
feel for what you need to add to your engine.
|
||||
|
||||
## Coded systems
|
||||
|
||||
Inspired by tabletop role playing games, most game systems mimic some sort of die mechanic. To this
|
||||
end Evennia offers a full [dice
|
||||
roller](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) in its `contrib`
|
||||
folder. For custom implementations, Python offers many ways to randomize a result using its in-built
|
||||
`random` module. No matter how it's implemented, we will in this text refer to the action of
|
||||
determining an outcome as a "roll".
|
||||
|
||||
In a freeform system, the result of the roll is just compared with values and people (or the game
|
||||
master) just agree on what it means. In a coded system the result now needs to be processed somehow.
|
||||
There are many things that may happen as a result of rule enforcement:
|
||||
|
||||
- Health may be added or deducted. This can effect the character in various ways.
|
||||
- Experience may need to be added, and if a level-based system is used, the player might need to be
|
||||
informed they have increased a level.
|
||||
- Room-wide effects need to be reported to the room, possibly affecting everyone in the room.
|
||||
|
||||
There are also a slew of other things that fall under "Coded systems", including things like
|
||||
weather, NPC artificial intelligence and game economy. Basically everything about the world that a
|
||||
Game master would control in a tabletop role playing game can be mimicked to some level by coded
|
||||
systems.
|
||||
|
||||
|
||||
## Example of Rule module
|
||||
|
||||
Here is a simple example of a rule module. This is what we assume about our simple example game:
|
||||
- Characters have only four numerical values:
|
||||
- Their `level`, which starts at 1.
|
||||
- A skill `combat`, which determines how good they are at hitting things. Starts between 5 and
|
||||
10.
|
||||
- Their Strength, `STR`, which determine how much damage they do. Starts between 1 and 10.
|
||||
- Their Health points, `HP`, which starts at 100.
|
||||
- When a Character reaches `HP = 0`, they are presumed "defeated". Their HP is reset and they get a
|
||||
failure message (as a stand-in for death code).
|
||||
- Abilities are stored as simple Attributes on the Character.
|
||||
- "Rolls" are done by rolling a 100-sided die. If the result is below the `combat` value, it's a
|
||||
success and damage is rolled. Damage is rolled as a six-sided die + the value of `STR` (for this
|
||||
example we ignore weapons and assume `STR` is all that matters).
|
||||
- Every successful `attack` roll gives 1-3 experience points (`XP`). Every time the number of `XP`
|
||||
reaches `(level + 1) ** 2`, the Character levels up. When leveling up, the Character's `combat`
|
||||
value goes up by 2 points and `STR` by one (this is a stand-in for a real progression system).
|
||||
|
||||
### Character
|
||||
|
||||
The Character typeclass is simple. It goes in `mygame/typeclasses/characters.py`. There is already
|
||||
an empty `Character` class there that Evennia will look to and use.
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
Custom rule-restricted character. We randomize
|
||||
the initial skill and ability values bettween 1-10.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called only when first created"
|
||||
self.db.level = 1
|
||||
self.db.HP = 100
|
||||
self.db.XP = 0
|
||||
self.db.STR = randint(1, 10)
|
||||
self.db.combat = randint(5, 10)
|
||||
```
|
||||
|
||||
`@reload` the server to load up the new code. Doing `examine self` will however *not* show the new
|
||||
Attributes on yourself. This is because the `at_object_creation` hook is only called on *new*
|
||||
Characters. Your Character was already created and will thus not have them. To force a reload, use
|
||||
the following command:
|
||||
|
||||
```
|
||||
@typeclass/force/reset self
|
||||
```
|
||||
|
||||
The `examine self` command will now show the new Attributes.
|
||||
|
||||
### Rule module
|
||||
|
||||
This is a module `mygame/world/rules.py`.
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
|
||||
def roll_hit():
|
||||
"Roll 1d100"
|
||||
return randint(1, 100)
|
||||
|
||||
def roll_dmg():
|
||||
"Roll 1d6"
|
||||
return randint(1, 6)
|
||||
|
||||
def check_defeat(character):
|
||||
"Checks if a character is 'defeated'."
|
||||
if character.db.HP <= 0:
|
||||
character.msg("You fall down, defeated!")
|
||||
character.db.HP = 100 # reset
|
||||
|
||||
def add_XP(character, amount):
|
||||
"Add XP to character, tracking level increases."
|
||||
character.db.XP += amount
|
||||
if character.db.XP >= (character.db.level + 1) ** 2:
|
||||
character.db.level += 1
|
||||
character.db.STR += 1
|
||||
character.db.combat += 2
|
||||
character.msg("You are now level %i!" % character.db.level)
|
||||
|
||||
def skill_combat(*args):
|
||||
"""
|
||||
This determines outcome of combat. The one who
|
||||
rolls under their combat skill AND higher than
|
||||
their opponent's roll hits.
|
||||
"""
|
||||
char1, char2 = args
|
||||
roll1, roll2 = roll_hit(), roll_hit()
|
||||
failtext = "You are hit by %s for %i damage!"
|
||||
wintext = "You hit %s for %i damage!"
|
||||
xp_gain = randint(1, 3)
|
||||
if char1.db.combat >= roll1 > roll2:
|
||||
# char 1 hits
|
||||
dmg = roll_dmg() + char1.db.STR
|
||||
char1.msg(wintext % (char2, dmg))
|
||||
add_XP(char1, xp_gain)
|
||||
char2.msg(failtext % (char1, dmg))
|
||||
char2.db.HP -= dmg
|
||||
check_defeat(char2)
|
||||
elif char2.db.combat >= roll2 > roll1:
|
||||
# char 2 hits
|
||||
dmg = roll_dmg() + char2.db.STR
|
||||
char1.msg(failtext % (char2, dmg))
|
||||
char1.db.HP -= dmg
|
||||
check_defeat(char1)
|
||||
char2.msg(wintext % (char1, dmg))
|
||||
add_XP(char2, xp_gain)
|
||||
else:
|
||||
# a draw
|
||||
drawtext = "Neither of you can find an opening."
|
||||
char1.msg(drawtext)
|
||||
char2.msg(drawtext)
|
||||
|
||||
SKILLS = {"combat": skill_combat}
|
||||
|
||||
def roll_challenge(character1, character2, skillname):
|
||||
"""
|
||||
Determine the outcome of a skill challenge between
|
||||
two characters based on the skillname given.
|
||||
"""
|
||||
if skillname in SKILLS:
|
||||
SKILLS[skillname](character1, character2)
|
||||
else:
|
||||
raise RunTimeError("Skillname %s not found." % skillname)
|
||||
```
|
||||
|
||||
These few functions implement the entirety of our simple rule system. We have a function to check
|
||||
the "defeat" condition and reset the `HP` back to 100 again. We define a generic "skill" function.
|
||||
Multiple skills could all be added with the same signature; our `SKILLS` dictionary makes it easy to
|
||||
look up the skills regardless of what their actual functions are called. Finally, the access
|
||||
function `roll_challenge` just picks the skill and gets the result.
|
||||
|
||||
In this example, the skill function actually does a lot - it not only rolls results, it also informs
|
||||
everyone of their results via `character.msg()` calls.
|
||||
|
||||
Here is an example of usage in a game command:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
from world import rules
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
attack an opponent
|
||||
|
||||
Usage:
|
||||
attack <target>
|
||||
|
||||
This will attack a target in the same room, dealing
|
||||
damage with your bare hands.
|
||||
"""
|
||||
def func(self):
|
||||
"Implementing combat"
|
||||
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("You need to pick a target to attack.")
|
||||
return
|
||||
|
||||
target = caller.search(self.args)
|
||||
if target:
|
||||
rules.roll_challenge(caller, target, "combat")
|
||||
```
|
||||
|
||||
Note how simple the command becomes and how generic you can make it. It becomes simple to offer any
|
||||
number of Combat commands by just extending this functionality - you can easily roll challenges and
|
||||
pick different skills to check. And if you ever decided to, say, change how to determine hit chance,
|
||||
you don't have to change every command, but need only change the single `roll_hit` function inside
|
||||
your `rules` module.
|
||||
Loading…
Add table
Add a link
Reference in a new issue