Might-Work MUD
If you've followed my Github profile you'll know I've tried lots of different ways to make lots of different game servers, with the primary focus being on text and audio. I recently started playing A Hero's Call which reminded me of when I used to play Valhalla MUD which was awesome. Anyways, the long and the short of it is that now I have as good as been banned from Valhalla, I'd quite like to make something that works similarly.
There is of course:
And probably others, but I thought maybe I could do something different. For anyone who knows me, that translates loosely to:
I don't understand how they work, and I'd rather write my own than read documentation.
MWM uses Python's argparse module for the majority of it's commands. This gives you all the flexibility that you'd get writing any other console program. As that is basically what a MUD is, I thought why not?
Anyone who complains that my method spoils immersion is completely right... That said, I don't understand that doesn't exactly keep me in the spirit of the thing either, and at least MWM commands have easy to read autogenerated help files which you'd probably end up writing anyway.
MWM has support for command substitutions. By default:
- '
- say
- :
- emote
- ;
- eval
- `
- exec
These are configurable on a per-instance basis with the configuration module (which has yet to gain a front end).
While MWM uses a flat file for persistent storage, an in-memory SQLite database is used while the game is running. This gives us greater control over pretty much everything as complex queries can be formed to ask questions about the state of the server.
Sure: [room for room in rooms.values() if room.light is True]
would work, but Room.query(light=True)
is less typing and more concise I feel.
There are plenty of commands already written which serve to document how the commands system works, but I thought I'd include a step-by-step guide anyway to hopefully outline any pitfalls.
As I said before, commands use the argparse module. In fact, commands.base.Command
is a direct subclass of argparse.ArgumentParser
. For instructions your first port of call should be the argparse documentation.
It is a good idea to put your command either in one of the existing command files in the commands
directory or create a new file there.
from .base import Command
Now you can subclass Command with no worries. Remember to include a docstring as this will be used as the description for the new command.
class Test(Command):
"""This is a test command which can be invoked by typing test when logged into the game."""
By default, self.prog
is set to the name of the class with all underscores replaced with dashes, and converted to lower case.
class Test_Command(Command):
"""You can type test-command when logged into the game to invoke this command."""
Instead of overriding __init__
(which requires more boilerplate than I like), provide an on_init
method.
class Test(Command):
"""This is a test command."""
def on_init(self):
"""Initialisation stuff goes here."""
If you want your command to be accessible by more than one name, you can add as many aliases as you like to the aliases
list.
class Test(Command):
"""This is a test command which can be invoked by typing test when logged into the game."""
def on_init(self):
"""Initialisation stuff goes here."""
# Add one at a time:
self.aliases.append('@test')
self.aliases.append('@test-command')
# Or add multiple aliases:
self.aliases.extend(['@command-test', 'command-test', 'test-command'])
You can add arguments using the standard argparse machinary.
class Open(Command):
"""Open something."""
def on_init(self):
self.add_argument('thing', help='The thing to open')
Now we know how to add arguments, let's write a meaningful command. Let's start like real programmers with a simple hello world
example. We add code with the func
method.
class Hello(Command):
"""Say hello world to the character."""
def func(self, character, args, text):
"""Say hello and be done with it."""
character.notify('Hello world.')
Simple, but effective! Let's talk about the arguments:
- character
- A `character` instance. You can safely assume this is a fully authenticated character, with a valid `connection` property which you can manipulate with [Twisted](https://twistedmatrix.com/)'s normal idioms.
- args
- The result of calling the `parse_args` method of your command. Don't worry, the `exit` method has been overridden to ensure your command will never raise `SystemExit`.
- text
- The text of your command, minus the command portion.
Instances of commands.base.command
may raise commands.base.CommandError
errors. This exception is used in lieu of sys.exit
for commands.base.Command.exit
.
Socials are possible with MWM using the emote-utils package. The package's contents has been wrapped in the following ways:
- An instance of
emote_utils.SocialsFactory
is provided associals.socials
. - All social suffixes should be left in the
socials.py
file. - You can use
db.characters.Character.do_social
to have a givencharacter
instance perform a social.
You can use do_social
as follows:
character.do_social('%1n smile%1s.')
character.do_social('%1n smile%1s at %2n.', _others=[other])
You can pass arbitrary keyword arguments to the underlying get_strings
method as extra keyword arguments to do_social
. For instance:
character.do_social('%1n say%1s: "{text}"', text='This is what I say.')
At the basic level, do_social
gives you a perspectives list which has the character in the first position, extended by _others
if necessary, and all extra arguments passed directly to socials.get_strings
.
Moreover, the perspectives list is iterated over by do_social
to send the correct string to the relevant recipient so you don't have to.
While there is some test coverage using pytest, I haven't written tests for everything... In fairness actually, the test coverage is pretty rubbish; feel free to submit pull requests if this worries you (it does me).
For everything else there is t.py. This little script loads configuration and database and gives you some handy globals. We will use this in the next section.
As previously stated, the database is where the bulk of the magic (no pun intended) happens.
Instead of me writing a list here, execute the file table.py to see what is available.
For programmers (rather than developers), the internal language use by MWM is Lua. Instead of a MOO-style approach I have gone for more of an event-driven feel.
As far as developing the MWM codebase goes, an event is defined as a column in a given table of the database which uses db.base.Code
as its type:
event = Column(Code, nullable=True)
Programs like this can be programmed or cleared with the program
(or @program
) command.
The wonderful thing about lupa which is the pythonic interface to Lua I am using is that the execute function works like Python's execute builtin except that it can return code. As such we can use the return value from a given event going forward.
Most if not all events are / will be called using the programming.as_function
method.
assert = as_function('return 5') == 5
You can pass extra arguments to as_function
which will be added to the lua globals and then removed once the code has finished running.
thing = object()
assert as_function('return thing', thing=thing) is thing
You have to be careful however as some of Python's objects might not work as you expect, particularly those returned by sqlalchemy.
To this end, there is a special lua_query
function which returns a dictionary to replace the query object usually resulting from a call to Session.query
.
Furthermore, to ensure events are called, use the functions outlined below:
Instead of setting character.location
manually, use character.move(destination)
.
What follows is a (hopefully complete) list of events used internally. Of course you could add your own to the database and use them as you like.
All events are called with a character instance available to them. All other locals are shown.
Called before an exit is used. Should return true
if the character is allowed to pass.
Once the return value is checked nothing else happens, so if the character is not allowed to pass the event should react accordingly, telling them why not ETC.
Called before a character has been moved to this room. Return value is ignored.
The room in question.
Called before a character leaves the room.
The room in question.