Results of more kids’ play

Well, this has been a long hiatus! I come and go in and out of hobbies. I may or may not be back here to stay for awhile; this may be just a dalliance, not sure.

This post is to point out a few things I had to fix after my kids played with the game a bit. They found the following three things that needed fixing.

Wait! First, let me point out that they were totally thrilled to be able to make their own realms, map them out on paper, and connect them with exits. They would ask one another where they were, and try to find one another. They could very easily get lost in their own, tiny, 6-room realms, because this text-based interface was all new to them. But they thought it was fun anyway.

So, the things I had to fix:

  1. They discovered that my teleport command didn’t work when they typed it with no arguments. Then I discovered, after fixing that, that I had fallen prey to the “variable declarations must go first in a function, always!” gotcha. Ayiyi, LPC.
  2. The telnet app I got for an iPad doesn’t echo the newlines at the end of their commands. So things ran together in an ugly way. I updated my user.c commandHook() to check for vt220 (what the iPad reports as) and add newlines after each command from the MUD’s end. The code looked like this.
        // my iPad connects with vt220 and doesn't use enough \n's, so add some:
        if ( this_user()->query_property( "termtype" ) == "vt220" )
            write( "\n" );
    
  3. They needed an easy way to get back to their home. The teleport command was too lengthy, so I added a home command. It had two uses: “home here” makes your home the room you’re in, and “home” takes you to your home, just like teleport would.

Those were the main things. There were other usability improvements I’d like to make based on my observing the kids’ playing with the MUD, but I haven’t had a chance yet. After I implement them, I’ll come and post here about it.

1 Comment

Exit, stage west

The standard way to connect rooms in a MUD is with exits. Each room has exits, usually in standard compass directions (plus possibly up, down, in, out, things like that). To use an exit, the player types the name of the exit and their character obeys by taking that exit.

In some MUDs, there may be something blocking an exit (a lock, an obstacle, an opponent) but in this post we just get the fundamentals down: Using exits to connect rooms, and allowing players to use them.

Storing the data

Exits are an association of direction names to destinations, which can be stored as an LPC data structure called a mapping. Below is the code I added to my room.c to support exits. The only thing that would be different for you, since your rooms are probably not strange like mine, is that set_exit() will probably just take one parameter, and behave like the first case in my if. The three-argument case is so that someone can call, say, set_exit( "west", "backbone", "The Clouds" ) and not need to know the filename.

// exits
private mapping exits;
// set_exit(direction,filename) or set_exit(direction,domain,shortdesc)
varargs void set_exit ( string direction, string dom, string sdesc )
{
    if ( !sdesc )
        exits[direction] = dom; // treat it like a filename
    else
        exits[direction] = ({ dom, sdesc }); // treat it like a pair
}
void remove_exit ( string direction )
{
    map_delete( exits, direction );
}
mixed query_exit ( string direction )
{
    return exits[direction];
}
string array query_exits ()
{
    return keys( exits );
}

Now your room files (those that inherit ROOM_OB;) can, in their create() functions, set up not only the room’s descriptions, but its exits, with calls to set_exit().

Well, that’s nice, that the data is now available to the MUD, but of course the players still can’t see the exits or use them. Those are the other two pieces of this puzzle.

Seeing exits

This is a simple matter of enhancing your /command/basic/look.c command to print exits after it prints the long description. Mine gives output like the following. I’ll leave this as an exercise, since it’s easy.

> look
The Clouds:
    You are sitting in the clouds, high in the sky above the world.
There is nothing here but you.
Exits from here: east in
> 

Using exits

There are three ways you may choose to expose the exit actions to your players.

  1. Create a single command, such as “go,” that allows players to take exits. You could make /command/basic/go.c that allowed players to type things like “go west” and it would examine their environment, ensure it was an instance of ROOM_OB, and if so, move the player.
  2. Modify the commandHook() function in the user object to do this, so that each exit becomes its own verb. Thus the player just types “west,” and that function does all the work just mentioned.
  3. Modify the room object to add actions to players when they enter it, one action for each exit. I haven’t experimented much with how this would work, so I can’t say much about it. But it occurred to me as a possibility. At least, I think it’s possible. :)

I chose the second option, since it seemed like the most natural thing for the users. However, the first option is reasonable as well, especially if you have aliases that allow players to make, say, “w” short for “go west.” The new code in my commandHook() function looks like this.

    // see if it's a directional command (using a room exit)
    if ( !arg && move_command( verb ) )
        return 1;

Obviously the heavy lifting there is done by the move_command() function, so let’s see the code for that. Several points in it raise questions that I address below.

// try to interpret dir as a move command, return 1 if it worked, 0 otherwise
int move_command ( string dir )
{
    object obj = environment( this_user() );
    mixed dest;
    if ( instance_of( obj, ROOM_OB ) ) { // see comments below
        dest = obj->query_exit( dir );
        if ( dest ) { 
            if ( stringp( dest ) ) { // this is the case most MUDs deal with:
                dest = load_object( dest );
            } if ( arrayp( dest ) && ( sizeof( dest ) == 2 ) ) { // see comments below
                dest = ROOM_OB->find_room( dest[0], dest[1] );
            } else {
                write( "An error occurred with this room's exit data!\n" );
                return 1;
            }   
            if ( !dest ) { 
                write( "An error occurred loading that place!\n" );
                return 1;
            }   
            write( "You go " + dir + ".\n" );
            say( capitalize( this_user()->query_name() )
               + " goes " + dir + ".\n" );
            this_user()->move( dest );
            say( capitalize( this_user()->query_name() )
               + " arrives.\n" );
            return 1;
        }   
    }   
    return 0;
}

The above code has a few points marked as requiring further explanation. First, what’s this instance_of() function? I wanted a function that behaved like the Java(Script) instanceof operator, and LPC had no such thing. It does have inherits(), but that is a non-reflexive operator (i.e., room.c doesn’t inherit room.c, according to it). So I built my own; it’s an interesting exercise for you to do the same!

Second, a typical MUD will not need to handle two cases, as I do–asking if the destination is a string or an array. In my case, the destination may be referred to by filename (a single string, as on most MUDs) or by a pair (domain and short desc together, which I can use to determine a filename). In your situation, which is probably more typical, these two cases will not be necessary, and you can just proceed right to dest = load_object( dest );.

Odds and ends

On my MUD, I also extended my room-creation command to allow creating or changing the exits of a room. If you chose to do things my way, you’ll want to do this as well. But you probably didn’t!

Leave a comment

A unique take on rooms

Well, I’m not sure this is unique, because I haven’t played MUDs in a million years, and obviously never played them all. But I made it up, so it’s new and unique to me, but maybe someone else does something similar.

I wanted to make it easy for my kids to create new rooms. The problem is that the usual way to create rooms is to write code. I.e., you can’t make a room unless you can edit a file and balance quotes, parentheses, and curly brackets. My eight-year-old can’t do that. So I needed a different way.

How rooms load

Fortunately, when you’re making a MUDLib you control…well, everything. So I can control the process of how rooms are loaded from disk. What loads rooms? Well, the teleport command loads them if you try to teleport to a filename. And when we eventually add exits to rooms then rooms themselves will load their neighbors when you take an exit. But as I said, both of those things are under my control. So I do not have to just use load_object() to get the room to load from a file; I could do something else.

The way I chose to do it was to have rooms save their crucial data to .o files, specifically /data/rooms/[domain]/[shortdesc].o files. Each room comes with a short description and a domain. Domains include Backbone, plus a domain for each developer, and maybe others in the future. So for example, here are some rooms in our MUD.

  • /data/rooms/backbone/The_Clouds.o
  • /data/rooms/endad/The_Cave.o

When I issue a command, I can do “teleport The Clouds” and it will find the right room to teleport me into. It loads it by creating a generic instance of room.c and then calling restore_object(). Here is the code, stored in room.c, for doing this.

object find_room ( string dom, string sdesc )
{
    string filename;
    object room;
    // If this room has already been loaded, find the old version
    // and don't create another copy; that's not the intent:
    foreach ( room in children( ROOM_OB ) ) 
        if ( ( room->query_domain() == dom )
          && ( room->query_short_desc() == sdesc ) ) 
            return room;
    room = 0;
    // save_filename() is another method in the room object
    // it does things like escape bad characters, to turn things
    // like "The Clouds" into "The_Clouds", etc.
    filename = save_filename( dom, sdesc );
    // We leave open the possibility that someone may have
    // coded a room in LPC, though this is rarely the case:
    if ( file_size( filename + ".c" ) > 0 ) 
        room = load_object( filename + ".c" );
    // Here's what usually happens:  Make a new room
    // and restore its data from the .o file on disk:
    if ( !room ) { 
        load_object( ROOM_OB );
        room = new( ROOM_OB );
    }   
    if ( room )
        room->restore( dom, sdesc );
    return room;
}

Easy room editing

This enables all sorts of cool tricks. For instance, we can not only load rooms from .o files, but we can save them to .o files. In fact, I can create an easy-to-use developer command that lets you tweak aspects of the room your standing in, and it automatically saves your changes to disk while you’re standing there! For instance, you could type room name My Castle and it would execute some code like this.

    // assume arg is a string, the text following "name" in the above example
    room->set_short_desc( arg );
    write( "You changed the room's name to this: " + arg + "\n" );
    say( capitalize( this_user()->query_name() )
       + " changed this place's name.\n" );
    return 1;

And you can create all manner of other ways to use the room command, such as room addtodesc [lots of text], or room domain [newdomain].

This makes it easy for my children (or those of them who can spell, that is) to edit rooms. You can even create a room create command that clones a new instance of room.c, but doesn’t save it until you’ve specified a domain and short description.

Later we’ll make it so that this command can also connect rooms, but first we have to build exits. That will be the next post!

Leave a comment

Feng shui

I don’t know if that’s really what I’m supposed to call this, but I know it means arranging the stuff in your room in a mystically perfect way, or something like that. And now we’ve built rooms, but they’re really basic, so we need to spiff them up. So that’s my desperate attempt to justify the title. Moving on…

The challenges in this post are as follows! First, those that are direct consequences of having just created rooms:

  • Now that you have rooms, so that people can be in different places, you need a shout command. Because not being able to be annoying will just never do. It’s easy: Just copy the say command and change say() to shout().
  • Add short descriptions to rooms. These will have many uses; I list a few here, the first two of which you can implement now if you like it, and the last of which we’re not yet ready for.
    1. Add to the who list information about where the player is.
    2. Have the look command print the short description as a title above the long description.
    3. When a player enters a room, tell them where they’ve arrived in one sentence (using the short desc).
  • Expand the look command to list the objects in the room, after the room’s long description.
  • Remove unneeded code from say.c and other files like it, where the __NO_ENVIRONMENT__ define is queried. Since we clearly both have and use environments now, that check is unneeded.

And one final challenge that’s completely miscellaneous:

  • Improve the update command, which is not very helpful with its spammy errors.
    1. If the file does not exist, say so and exit without trying to load it.
    2. Wrap a catch() call around the load_object() call, and print the error message returned, if there is one. This makes the loading error clear, rather than reporting a pile of errors and requiring you to sift through them to find the one that was actually about the code in the file being updated.

Leave a comment

Rooms! (for real)

The past few posts were supposed to be all about getting us ready to build rooms. Well, now it’s time to really do so. This post will be a tutorial that gets the basics done, then the following few posts will be challenges to help you add the bells and whistles.

I’m doing rooms in a fairly nonstandard way in my MUD, but we’ll come back to that later. For now, I’m starting with the same basics that I suspect everyone will want. (Unless of course you’re going to do something even more nonstandard than I am, in which case–you’re on your own!)

What is a room?

A room is an environment in which player objects (and other objects) can sit, and which reports facts about itself that give the characters a sense of “being somewhere.” There is no such sense in the MUD I’ve described coding up to this point; the only sense you get is of being on a chat channel. The differences are these:

  1. Right now there is only one location in the game, and everyone is in it. We want multiple locations, so people can be in different places.
  2. Each location should know something about itself, so that people can inspect that location and see what it’s like.
  3. Players should also be able to tell who (or what) else is in the same location as them, and when people come and go.
  4. Finally, there will need to be some means of transporting players from one location to another, so that players feel in some measure in control of their whereabouts.

Let’s take the above list as a vague requirements specification for what kind of support the MUD needs to provide for rooms. Most of this we’ll put in the generic room object, /clone/room.c, but not all of it goes there. Let’s take this one step at a time. (It’s a tutorial, after all.)

Create /clone/room.c

The generic room object can be so simple to start out that I’ll actually post the entirety of its essential code here. After the code block, I explain it.

#include

inherit BASE;

// rooms cannot be moved
int move ( mixed dest )
{
}

// long dsecription
private string longdesc;
void set_long_desc ( string ld )
{
    longdesc = ld;
}
string query_long_desc ()
{
    return longdesc;
}

The first line is self-explanatory, and the second line inherits the routines common to all objects. (See /inherit/base.c for what they include.) The first function overrides the generic move operation to do nothing for rooms. In other words, you’re not allowed to move rooms in my MUD. (No rooms within bags within players within rooms–too confusing, at least for now.)

The last block of code creates a member variable for storing the long description of the room, and functions for setting and getting it. If you’re familiar with MUDs, you know that a long description is usually about a paragraph of text describing the room, which players can read to get a sense of where they are. It is most of what the player expects to see when he or she types “look.”

Once you’ve created even just this simple file, you can place yourself in a room with a few well-done eval statements. Try something like this (in which I assume you’ve already added ROOM_OB to your globals.h file):

> eval this_user()->move( new( ROOM_OB ) )
Result = #0
> eval environment(this_user())->set_long_desc( "This is a tiny castle, with a tiny long description." )
Result = #0
> eval environment(this_user())->query_long_desc()
Result = "This is a tiny castle, with a tiny long description."

Several things may come to your mind. The first is that you’re sick of typing eval environment(this_user())-> already, so you might want to do this.

alias here eval environment(this_user())->

(Remember the idea for aliases, and even the alias for “me,” in this post.)

The second is that no one in their right mind wants to look at their environment using an eval command. Right you are! So we will make a command that does it for them.

Look

A new command that belongs in the basic command set is “look.” It should just ensure that environment(this_user()) exists and returns a value from query_long_desc(). If so, it should print that value, and if not, display some shocking error message. After that you’ll have this extremely satisfying experience:

> look
This is a tiny castle, with a tiny long description.
>

HOLY COW! WE ARE SOMEWHERE! IT’S LIKE A REAL GAME! Sort of.

Leap

Most MUDs store each room’s data in a separate file. Developers create new rooms by writing the code for an object that inherits /clone/room.c, and contains a setup function like this one.

#include
inherit ROOM_OB;
void setup ()
{
    set_long_desc( "This is a tiny castle, with a tiny long description." );
    // and eventually other things will be set here, too, but we haven't built them yet
}

You’ll want to be able to create multiple such rooms and leap about them using something other than the clunky eval calls we used above. I suggest creating a room very much like the one above, saving it somewhere (say, /data/tinycastle.c) and then going to it, like this.

> me move( "/data/tinycastle" )
Result = #0
> look
This is a tiny castle, with a tiny long description.
>

In order to facilitate leaping directly to such rooms in the future, I suggest creating a command (maybe “goto” or “teleport”) that accepts a filename as parameter (and passes it through resolve_path()), then makes the above move() call for you. This way you can just type things like “teleport tinycastle” and be there.

Surely there’s more!

Indeed, what would a long desc be without a short desc? And what about teleporting to other players? And what about mazes? Don’t forget the mazes! Indeed, there is much still to do, but this is a great start. My next few posts will be on this same topic, and will cover these aspects of it:

But for this post, that’s quite enough. Now go make some overly wordy long descriptions!

Leave a comment

A few little upgrades

Here are a few random items I did at this point in my development that you may want to do also. They’re minor, so I’m not showing you how to do them as a tutorial, but just issuing them as challenges.

  1. Wherever you have full path names as string literals, begin to replace them with #defines in globals.h.
    // old way:
    user = new("/clone/user");
    // new way:
    user = new( USER_OB );
    // assuming USER_OB was #defined to be "/clone/user" in globals.h,
    // so you can change it later if you need to in just one place
    
  2. Tweak eval in several ways:
    1. #include <globals.h>, so that if you need to use things like USER_OB and its friends, they’re available.
    2. I don’t know about you, but I find it disconcerting when eval 2+2 gives me zero. This is, of course, because the code eval writes doesn’t have a return statement, so the default return value is zero. I therefore tweaked eval to insert a return statement so I didn’t have to type eval return 2+2 (or whatever) every time.
    3. However, this means your evals have to be single statements, and maybe you don’t want that. So I also tweaked eval to allow code before the return as well.
      Example: eval 2+2 results in evaluating a function containing return 2+2;, while
      Example: eval |int x=2;| x+x results in evaluating a function containing int x=2; return x+x;.
      Sure, it’s a little convoluted, but the first use case is way more common, and I forget the “return” all the time. Pick your poison.
    4. Run the result through the dump_variable() sefun for prettier output formatting.
  3. Create an alias command. If you’re not familiar with aliases in a MUD, they’re customizable typing shortcuts. For example, let’s say you type “look” a lot and you want to just be able to type “l” instead. You issue this command:
    alias l look

    And then “l” is a shortcut for “look” thereafter. Implementing this requires several steps:

    1. Creating a place in the user object to store aliases. Rather than creating this as a one-off, I created a generic property dictionary in which any future permanent values like this could be stored.
    2. Creating a command (in the commands folder available to everyone) that allows the user to manage their aliases (add, remove, see the list of currently defined ones).
    3. Modifying commandHook() in clone/user.c to run every line of user input through the alias expander before processing it.

    Then you can do cool stuff like this:

    alias me eval this_user()->
    me query_name()

Leave a comment

Why, resolve_path(), why?

As I alluded to in other posts, resolve_path() doesn’t behave right. In what ways does it behave wrongly? I’ll list a few.

  1. It can’t handle the empty string as parameter.
  2. It can’t handle the input ~.
  3. The expression newer[<0..<1] = curr + "/"; didn’t seem to work for me, though I get the intent.
  4. The code style is atrocious. Who the heck indented this mess?

Here’s a better version:

string resolve_path(string curr, string newer) {
    int i, j, size;
    string *tmp;
    
    switch(newer) {
    case 0:  
    case "": 
    case ".":
        return curr;

    case "~":
        return user_path((string)this_player()->query_name());
    
#ifndef __NO_ENVIRONMENT__
    case "here":
        return file_name(environment())+".c";
#endif
    
    default:
        if (newer[0..1] == "~/")
            newer = user_path((string)this_player()->query_name())
                  + newer[2..];
        else {
            switch(newer[0]) {
            case '~':
            {   
                i = strsrch(newer, '/');
                if (i < 0)
                    newer = user_path(newer[1..]);
                else
                    newer = user_path(newer[1..i-1]) + newer[i..];
                break;
            }   
            case '/':
                break;
            default:
                newer = curr + "/" + newer;
            }   
        }   
    
        if (newer[<1] != '/')
            newer += "/";
        size = sizeof(tmp = regexp(explode(newer, "/"), "."));
    
        i = j = 0;
    
        while (i < size) {
            switch(tmp[i]) {
            case "..":
                if (j) {
                    while (j-- && !tmp[j]);
                    if (j >= 0)
                        tmp[j] = 0;
                    else
                        j++;
                }
            case ".":
                tmp[i++] = 0;
                break;
            default:
                j = ++i;
                break;
            }
        }
        return "/"+implode(tmp, "/");
    }
}

Leave a comment