How I got into Evennia
It’s a common dream among mudders: a game of your own, just the way you want it, lacking the quirks and annoyances of your favourite game, that has those extra features you cannot get existing admins to implement for you.
I got into MUDs very late for my age. Being a long-time tabletop roleplayer, I was looking for online alternatives and found the mainstream graphical RPGs severely lacking. I didn’t actually test a MUD until a few years back, but I soon found this was the best roleplaying experience to be had online. It also did not take long for the developer in me to find things I wanted to improve on. The alluring thought of my own game snuck up on me.
Having used C, C++, Java, and Fortran at work, I had no interest in also using them in my free time. I had become very fond of Python and thus started to look into Python MUD codebases.
My first stint was with an abandoned Python-MOO project. I got it up and running, fixed some old bugs and toyed with some of the basic systems I pictured for my game. I got a few nice things going before realizing that I spent most of my time working around limitations in the MOO-language interpreter. Following that thought further — I was working in a scripting language implemented in Python. Something is wrong with that concept. Why not use Python itself?
I looked into NakedMud, which is a very nice system using Python for a lot of functionality. But I felt, at least at the time, that I would eventually have to go to C to get the kind of flexibility I wanted. I preferred to be able to stay in Python throughout.
That was when I came upon Evennia. Evennia was a bare-bones Python codebase originally created and developed by Greg Taylor. None of its technical underpinnings meant all that much to me at the time. What intrigued me was that Evennia simply imported and used normal Python modules for all its coding, using neither a custom softcode language for its content nor a compiled language for its core.
Evennia turned out to be just what I had been looking for: a base system I could expand on freely using plain Python. Evennia was in alpha, so I helped as best I could with patches and new features. I knew nothing about the frameworks that Evennia is based on, so this was a great learning experience for me. In 2010, Greg Taylor found himself lacking the time to maintain the project and passed the role of project owner to me.
Evennia has come a long way towards being a general MUD-design system since then. My dream of my own MUD game remains, but is on hold. For now, I’m happy to see others begin to use the system to create worlds of their own.
Evennia technical overview and philosophy
Evennia is written in Python; it is open-source, released under the BSD license. It isn’t really a “MUD codebase” in the usual sense — it’s more of a MUD/MU*-creation system and server in one. Since Evennia is leveraging the Twisted and Django frameworks, one can easily expand one’s game with a dynamic web presence. Evennia is, in fact, its own web server — a default web page and game web client comes with the package.
The idea is to handle all the boring database and networking stuff that all games need while offering the building blocks for building the game and world itself with as much freedom as possible. This is the reason there are no character classes, combat systems or really any game-specific systems hardcoded into Evennia. For convenience, the distribution does come with a number of default commands for administrative and basic game functionality, as well as base objects for rooms, exits, player accounts and characters, but all of these are intended to be modified, extended or discarded as desired. The base Evennia install works fine as a minimal talker-type server.
Contributors are more than welcome to offer up more specialized systems, which are distributed via an optional “contrib” folder. We have stuff from dice rollers to barter systems, character generators and tutorial worlds in there.
Evennia is currently in beta. While we continue to work on it, people have already started to use it, both for new games and to convert old ones. Our IRC channel #evennia on Freenode has a lot of good MUD discussion. The current status and development effort is best summarised in our devblog at http://evennia.blogspot.se/.
Diving into Evennia: Living on the asynchronous edge
Evennia runs on top of the Twisted asynchronous framework. Twisted not only offers asynchronous event execution, it also makes it easy to implement and support various connection protocols (e.g. Telnet, SSH, SSL, Comet, WebSockets). We allow for interconnection between Evennia game channels and IRC, IMC2 and RSS feeds. People who want to add their own protocols (such as for their own custom web client) can plug that in, often just by referring to a Twisted tutorial.
An important point about Twisted is that it is not concurrent in the way a thread or a subprocess is; program tasks do not execute simultaneously. Rather, Twisted works by “chopping up” code into small snippets that relinquish control of the process whenever they would otherwise block, which allows other tasks to run. The result is tasks that rarely have to wait long to execute; in a MUD platform like Evennia, this means that players generally do not have to wait for each other. There is no magic here, though; the application is single-threaded, and each task has 100% of the process’s resources while it is running. So calling sleep(10), which is a blocking operation, is a sure way to freeze the entire server for ten seconds. To function properly in an asynchronous framework, code must be written to avoid blocking operations by using asynchronous calls, which allow the current code snippet to stop running and let other snippets execute; once the asynchronous call is complete, execution continues using a callback to a new snippet, this having been provided by the first snippet when it made the asynchronous call. Though this can add complexity, a lot of simplicity is gained back because snippets always have a completely consistent internal state during their execution, meaning that the concurrency issues multi-threaded environments manage by synchronization techniques like locks, semaphores and mutexes simply do not exist in single-threaded asynchronous environments.
Twisted is used in many professional environments, and a MUD is certainly not the most demanding one. That said, one needs to keep these things in mind to avoid having long-running pieces of code execute too often. Our command execution code is an example where Evennia yields often in order to allow Twisted to do its thing.
While we know Twisted scales very well, our combining it with Django for ORM (object-relational mapping) and for web integration is worth discussing. Firstly, Django is a very solid, professional web framework that adds considerably to the maintainability and web capabilities of Evennia. Tasks like integrating a website or forum directly with the game, adding a web-based character generation system or supporting a custom web client are what Django excels at (as mentioned, we offer a website and web client out of the box).
But Django is not asynchronous, which means that its database access is written as a blocking operation. While modern databases are very fast, this could certainly be a bottleneck for Evennia’s performance. In testing, however, we have found this to less of an issue than one would think, especially once we apply aggressive database query caching to limit the number of lookups.
There is certainly room for improvement, but Evennia’s scaling capabilities are nevertheless promising. In a stress test I had several hundred simulated players online on my laptop: bots that would register, log in and, at random intervals, chat, build, read help entries, pose and move between rooms. On an older version of the server I’ve had a thousand dummy players connected in this way without the game feeling too sluggish. While this was on a vanilla install, and it’s hard to simulate the behaviour of that many actual players, I dare say few modern hobby MUDs can hope to attract even a hundred simultaneous players, so Evennia’s current performance should stand up well to its intended use.
Diving into Evennia: Typeclasses and Attributes
Evennia supports a number of different SQL databases for its persistent storage. But how does one represent game entities in the database? How do you differentiate a chair from a bear or a room from a laser gun?
The naïve solution is to create separate database tables for each type. The bear would have separate fields for health, AI and so on. The chair would have weight and a field for storing who is currently sitting on it, the gun would list its remaining ammo, and so on. Whenever you wanted a new type of object you’d need to create a new table. For very specific, simple games this might work. For complex, open-ended games, it’s hardly a very flexible approach.
Instead, Evennia uses generic database tables, leveraging the Django ORM for abstracted Python access, in which a table is represented by a Django model. On top of this we use what we call a typeclass, which is an almost-normal Python class, a child of Evennia’s Typeclass parent. Typeclasses can be inherited, expanded and used pretty much as you would expect. The difference is that each instance of a typeclass is directly tied to a row in the database, and updating named fields on the typeclass automatically updates the database underneath.
The interesting thing about this is that, to the game developer, it looks like they are just developing their game by creating new classes. To overload base functionality, you just overload methods on the typeclass. Our goal is that you should never need to touch the core library because everything you need to do can be done through polymorphism. For example, you could add new functionality to how text is returned to the player by overloading the msg() method on the parent typeclass and modifying its behavior to your specifications.
However, typeclasses as a mechanism are not sufficient for representing game entities. Consider this case: assuming their only main difference is colour, you generally wouldn’t want to have to create separate classes to represent “red roses” and “white roses”. For this, Evennia uses Attributes, properties that are connected to each typeclassed entity through a foreign key relationship. Using Python’s pickle module for serialization, they allow for storing arbitrary data. So, in our example, the instance of the Rose typeclass would have an Attribute colour with the value “red”.
This example typeclass is for an unliftable “Heavy” object (possibly used as a parent for other objects):
from ev import Object class Heavy(Object): "Heavy object that cannot be picked up" def at_object_creation(self): "Called whenever a new object is created" # lock so only Builders (and above) can pick it up self.locks.add("get:perm(Builders)") # attribute the "get" command looks for self.db.get_err_msg = "This is too heavy for you to pick up." # You’d probably change desc on a per-instance basis self.db.desc = "An immovable and heavy object"
The simplicity of coding offered by typeclasses is paid for by increased complexity under the hood. What is happening is that the Python get/set property methods are overloaded to seamlessly convey data from the typeclass to the database model. This adds some overhead for property access as well as forcing core developers to tread more carefully to avoid accidental lookup loops between the involved model and its typeclass. Careful caching is again useful from an implementation perspective.
Diving into Evennia: The Hierarchy of the User
Many MUDs have a very simple relation between the user (the person playing the game) and the player character (the in-game avatar): they log in and immediately connect to their avatar. If they want to create a new character, they have to register anew. “Character creation” consists of gradually setting properties on the avatar object to customize it.
More sophisticated systems allow for an account mechanic. In its simplest form, this means that multiple game logins are tied together by a global account name. This helps with tracking multi-playing users as well as with distributing account-level perks and bonuses (such as giving a bonus to your next character if your last one died heroically).
Evennia uses this hierarchy:
User ⇔ Sessions(s) ⇔ Player ⇔ Object(s)
Here, “user” is the actual individual playing the game. Session(s) represents the actual connection(s) to the game. Player is our equivalent to an “account”, an OOC object that has no existence in the game world. Finally, the Player is puppeting an Object. This is the in-game avatar; Object is usually of typeclass Character.
Output text sent to the Object/Character is piped upward to the relevant Session. In the other direction, command instructions flow down through the hierarchy to reach the current bottommost level (Session if user is not currently logged in, otherwise Player or Object/Character). This makes it very easy to implement puppeting and multiple-character systems: just disconnect the Player from its current Object/Character and connect it to something else, permissions allowing.
This separation of Player from Object/Character means that you can design your character creation to happen while in “player mode” — for example, by using a menu. The use of generic objects for avatars also means you could just as well puppet a vase on a table as a person.
An interesting feature (originally requested by MUCK players) that this separation enables is connecting multiple Sessions to the same Player, with each Session tracking which Object/Character is puppeted. Through this Evennia allows true multiplaying, where a single account (Player) can control any number of Objects/Characters at the same time from multiple open clients. You can turn this feature off in settings, but it offers a lot of flexibility for creating different game experiences.
Diving into Evennia: Commands
A command in Evennia is defined as everything entered by the user through whatever client and protocol they are using to connect. But how is that represented in code?
Evennia, being a MUD-design system, has some rather stringent requirements for a command implementation. It needs commands to be flexible and easily extensible but also able to represent a wide range of situations. A hard-coded command set is not an option.
In MUDs, a common way to implement a command is by functions, as a function maps naturally to a command: it has a name and it takes arguments. Evennia, instead, uses classes. This design choice was made for a number of reasons: classes can be inherited and are thus easy to modify and extend while avoiding a lot of boilerplate. An instance of a class can also hold properties of its own. A very simplified sketch of the command invocation process looks like:
- Once a command has been identified by Evennia (by getting the beginning of the raw user input), its relevant class is brought up and, if necessary, instanced (existing instances are reused on a per-object level). The instance is then loaded with useful properties — such as who called it, on which object the command is defined, the original raw input string, and so on.
- The parse() method of the command is called. This deals with detailed parsing of the input, splitting eventual arguments into lists, and so on. Results are stored directly on the command object itself. This step is almost always the same, so you usually only implement parse() once, then let other commands inherit it.
- Next the func() method is called. This is always overloaded by each command class, and implements the actual work of the command. It has available all the properties saved and prepared by parse() before it.
A simple Evennia command implementation:
from ev import Command class CmdEcho(Command): """ Simple command example Usage: echo <text> This command simply echoes text back to the caller. """ key = "echo" locks = "cmd:all()" def func(self): "This actually does things" if not self.args: self.caller.msg("You didn't enter anything!") else: self.caller.msg("You gave the string: '%s'." % self.args)
The initial doc string is used to dynamically create and update the command’s help entry. This example does not even worry about implementing parse(), simply using the data already available to it to perform its (in this case minimal) work.
Another feature of this system is that none of the methods require any arguments or return values, as all data is available on the command object. This may seem like a small style issue, but when writing a lot of command classes, this lack of boilerplate makes for very clean code.
Diving into Evennia: Command Sets
So we have individual commands. How do we now group and access them?
A common method, used by older versions of Evennia, is to use a global command list: whenever a user enters a command, the system looks to the global list and gets the matching command object. All users have access to this list, as determined by their permissions. Most codebases use this method, and it clearly works very well for many games. The problem comes when wanting to deal with states. Consider the following examples:
- An in-game menu. Selecting a menu item means an instruction from the player — that is, a command. A menu could have numbered options, but it might also have named options that vary from menu node to menu node.
- You are walking down a dark hallway, torch in hand. Suddenly your light goes out and you are thrown into darkness. You cannot see anything now, not even to look in your own backpack! Next, you knock your head and become dizzy. Suddenly your "navigation" skill is gone and your movement commands may randomly be turned around. Dizziness combined with darkness means your inventory command now returns a strange, confused mess. Next, you get into a fight, and discover that dizzy fight commands in the dark are very different from those available during the day.
- In a hypothetical FishingMUD, you have lots of detailed skills for fishing. However, different types of fishing rods make different types of throws (that is, commands) available. Also, they all work differently if you are on a shore rather than on a boat.
These are all examples of state-dependent commands. In principle, you could implement the features above with a slew of nested if statements in all your commands, but the complexity climbs quickly, and the chance to make errors or miss edge cases climbs even faster. Let’s see what could be done instead.
You could picture handling the menu problem by dynamically adding new commands to the available ones. But now the global list becomes an issue. Why would other users suddenly have access to your menu commands?
You could resolve this by tracking who has which list of commands available to them at any moment. One user would have the extended menu-command list and another would not. This can be thought of as being in a character-specific state. But what then happens when you start combining multiple states, such as being dark and dizzy and in a fight? From there, how do you conveniently get back to a previous interim state, such as light returning while still being dizzy and fighting?
A few iterations of such thinking leads to what Evennia now uses: a non-global command set system. A command set (cmdset) is stored on individual objects. It groups any number of commands inside it and behaves similarly to a mathematical set. Command sets can be merged using various set operations (like union, intersection, and so on) to produce new, temporary sets. The merged set is what is available to the Object (and thus most often to the user) at any moment.
The important thing about cmdsets is that they are merged non-destructively, which means that even though we merged the “dark cmdset” with the “dizzy cmdset” and the “fighting cmdset”, we can remove either of them and produce a new subset to describe the new state. A menu becomes trivial to implement in complete isolation: you create your “menu cmdset” and just have it temporarily replace your normal set. Then, while you are in the menu, you’ll only have the valid options available. The available cmdset can even update as you progress through the menu to give you new menu choices. The fishing poles become items with different cmdsets for the user to access, all of which may be modified by the situation.
We have found that the combination of commands-as-classes and cmdsets allow for great flexibility in command schemes and for implementing interesting game states. No custom code handling or checking for special cases is needed. Plugging in something like a barter system becomes a matter of supplying a cmdset to merge into the default one — the new commands are immediately integrated into the game.
An article like this can necessarily only touch briefly on the various systems going into a game server. Do check into our documentation, IRC chat or mailing list if you are curious.
Some of our users use Evennia as a motivation to learn Python. I don’t know how efficient that is, but I do know I’ve learned a truckload of Python since starting to work in earnest on the server, and my Django experience made me qualified for a project at work. One of our users even got a job based on Django skills they picked up when working with Evennia!
This is a hobby for almost all of us, and as such one of the most fun things is the interaction with our little open-source community. Time will tell where things lead from here.