Command Processing

2025-02-16

Basic Game Loop

When a player types a command in Agony Forge, their browser sends the command to the server over Web Sockets. The command is received by the WebSocketController as a String. The lowest level pattern in the core of the game engine is that every input is sent to a Question, and from there it is processed into a Response. Any Output generated by the Question is sent back to the user, and the current Question appends that output with a prompt. That's it. That's the entire loop.

It's simple, but powerful: a Question can do anything it wants with that input, from the normal in-game prompt to the character menu, to in-game editors or whatever other kind of interactive command line it needs. Part of the Response object has the output and the other part indicates which Question object should be used next.

In a menu with multiple screens, each screen is a separate Question and moving between screens is as simple as passing the next screen's name in the Response. Many Question objects are themselves simple state machines that model a screen in a menu, with a state for handling each of the choices in the menu.

To move between menus or in and out of an editor, the Response object can tell WebSocketController to swap to a different Question to prompt the user and interpret the next input.

In-Game Commands

In-game commands are handled by the CommandQuestion which has a secondary layer of processing for user input. It has a set of CommandReference objects that represent the names and priorities of commands the user could execute. The CommandReference refers to a Command which contains the actual logic for running the command. Most commands have a single CommandReference that points to a Command, but it doesn't have to be one-to-one. For example, I have a CommandReference for each directional command (north, east, south, west, etc.) that all refer to the same MoveCommand, just with different arguments to indicate which direction to move in.

CommandQuestion tries to match the first word of the input to a command, then sends the rest of the input as the command's arguments. The input is "tokenized", or split up into a list of words. The commands are sorted both by priority and alphabetically. That's why if you type "s" you get SOUTH instead of SAY. All the words after the first are sent as arguments to the command. Different commands expect different kinds of arguments, so there are a few typical ways that we process them.

Object and Creature Targets

Most often, commands have an argument that is an object in the game world like GET SHIELD where "SHIELD" is the name of an item on the ground. The command simply assumes the second token in the input will be the name of an item in the same room as the player, and it tries to find something that matches. If it can't, you get an error message like "You don't see anything like that here." If it does find the shield, the command will move it from the room to your inventory. I call this "object binding", because you're taking a word and trying to "bind" it to some kind of actual object (or creature) in the game world.

There are many varieties of object binding because commands need to match all kinds of different targets. An argument could be an item, a creature, or a player. It could be in the player's inventory or in the same room. It could be something in a different room. It could even need to be a specific kind of object like a container or a weapon. That's why it was easiest to write the logic for it individually in each command rather than creating a standard system for it. The code in a lot of the commands is similar, but not quite the same.

Quoted Strings

Other times the argument is a string, such as for communication commands like SAY or GOSSIP. The communication commands are actually a special case for this because we can short circuit the process and assume everything after the command should be taken literally as the message the player wants to communicate. For TELL there's a target and then the string, but the idea is the same. We don't want to process or otherwise change the user's input at all or we'd be changing what they wanted to say. So the command simply takes the command and arguments off the front and treats the rest as the message, keeping it exactly as the user typed it. I call this "implicit quoting" because the player doesn't have to type the quotes for it to work.

There are other cases where a command might have an explicitly quoted string in it, such as the ROLL command that takes an Effort (e.g. "Weapons & Tools") as an argument. You have to quote for it to work: ROLL DEX "Weapons & Tools". If you don't quote it, it splits the name of the Effort into multiple words and the command fails. When we see quoted strings, we treat everything between the quotes as a single "word" instead of splitting on the spaces.

Commands that require quoted strings are a good argument for aliases, a feature Agony Forge still lacks. In the old CircleMUDs I used to play, to cast spells you'd have to type out the name of the spell. In combat that could be quite time consuming and error prone, so you'd create an alias for the command: mm might map to CAST 'magic missile', and then you'd just have to type mm in combat. In Agony Forge, aliases will be a step at the very beginning of the CommandQuestion. If your input matches an alias, it will simply swap the input for the value of the alias before continuing. The rest of the command interpreter never even needs to know the input has been transformed.

ID Numbers

Some other commands, in particular commands for admins, take numbers as arguments. For example, IEDIT can take the ID number of any item that you'd like to edit. This edits the item template, and not a specific instance of the item. GOTO takes a room ID to transport you instantly from where you are to the room you specify.

Future Improvements

While it's good enough for the time being and I've moved on to work on other parts of the game, there are some key improvements that can be made to this system.

Small Words

One easy improvement would be to throw out tokens such as "A", "AN", "AT", "FROM", "MY", and other small words that the software doesn't need in order to understand the command. Then all of the following commands would become identical to one another, and interacting with the game would feel more like "natural language":

  • GET APPLE BAG
  • GET APPLE FROM BAG
  • GET APPLE FROM MY BAG
  • GET THE APPLE FROM BAG
  • GET THE APPLE FROM THE BAG
  • GET AN APPLE FROM THE BAG

Selectors

Imagine you are standing in a room with 50 swords in it. All of them have the "sword" keyword on them, and while some of them have other keywords, the one you want has the same keywords as a dozen of the other swords. Right now the GET command would get the sword it found first. To get the one you actually wanted, you could pick all of them up and then drop them one by one until you got to the one you wanted. Then stash that one somewhere else and drop all the rest. But that's a lot of work.

CircleMUD got around this with a strange "dot" syntax. If you want the 8th sword on the list, type: get 8.sword. It's not intuitive, but it did work.

I would love to try a more natural way with something I call "selectors". In regular English you might say something like: get the eighth sword. Assuming we're filtering out "small words" from the previous section, get eighth sword should work. The command just has to recognize that "eighth" is an optional argument related to the argument after it and that it modifies the search for "sword" to return the 8th result instead of the first. I haven't implemented this yet because it's pretty obscenely complicated to do with the way the code is currently structured, and would have to be duplicated into a dozen different commands.

Object Binding

A significant refactor would be to add another pre-processing step to match tokens to objects before the command ever receives the input. The command could define a syntax, declaring through annotations or method arguments which objects (not Strings) it requires. Our GET example above could declare that it needs first an inventory item inside a container, and next an inventory item that is a container. When the command is invoked, the engine could pass in the actual items already having been looked up. If no such items can be found, an error message can be generated before the command is ever invoked at all. The benefits of this are substantial, because it would standardize the code for looking things up into one place, and the error output for every command would be the same. Overall I think doing this would easily eliminate half of the code for commands as well as a huge number of repetitive unit tests.

Verb Binding

Another good improvement would be to always match the first token to a valid command name. The inputs "S", "SO", "SOU", "SOUT", and "SOUTH" are all going to match to the same command, so the token should always be normalized to "SOUTH". Since there is no binding to commands in the current system, each of those abbreviations of the command name would be different tokens.

References

These ideas are not entirely my own. Parsing is an area of computer science that has been around since the beginning, and there is a great deal of research and learning to lean upon when designing a system that needs to process user input. However there is one obscure source that has influenced me on this subject for a good long time now. Back in 1998 I read a post by Dr. Richard Bartle, the creator of MUD1 and MUD2, that described an improved not-quite-natural-language system for parsing commands on a MUD. I've built a system that was similar for an earlier MUD but it's an easy system for me to get bogged down in. For Agony Forge I have decided to build a good-enough command parser for now and I plan to circle back to it some time down the road after the game is playable.

The way Agony Forge works today is also inspired in some ways by how CircleMUD and Smaug MUDs do it, although more object oriented. In Circle based MUDs there is a list of commands that map to C functions. The list is sorted in priority and alphabetical order, the same as Agony Forge, and the first word in the input is matched to the command name that is the first match. Then the function is invoked with the rest of the arguments in a string. Each function is responsible for processing the arguments and mapping them to objects before doing the actual work of the command. It suffers from many of the same problems I described: error messages are inconsistent, a lot of code is duplicated, and each command has a blend of parsing and game logic.

Go to Comments
[ Copyright 2024 Scion Altera ]