Assignment #8

Intermediate C Programming

UW Experimental College


Assignment #8

This handout presents several more potential additions and improvements to the game, not couched so much in terms of an assignment, but more as food for thought.

  1. Last week we added a func field to struct object, so that an object could contain a pointer to a function implementing actions specific to that object. Another ability this gives us is to write new, object-specific code which is intended to be fired up not directly, in response to a user's typed command, but rather, indirectly, at other spots in the game where we're working with objects. We'll do this by letting objects optionally define special ``verbs'' (with names beginning with periods, by convention) which we'll ``call'' when we need to.

    Suppose we wanted the name or description of an object (printed in the listing of a room's contents or the actor's possessions, or in response to the ``examine'' command) to vary, depending on the state of the object. (We've already done that, in a crude way, by having the ``examine'' command look at a few of the object attributes we've defined.) For example, we might want the player to be able to find an object which is described only as
    	a wad of rubberized fabric
    
    until the player also finds the ``air pump'' object, and thinks to type
    	inflate wad with pump
    
    at which point the object will be described as
    	an inflatable boat
    
    To do this, we'll let an object define a pseudoverb ``.list''. Then, any time we would have printed the object's name field in a list of objects, we'll let it print its own name, if it wants to. For example, the hypothetical rubber boat might have object-specific code like this:
    int boatfunc(struct actor *actp, struct object *objp,
    					struct sentence *cmd)
    {
    	if(strcmp(cmd->verb, ".list") == 0)
    		{
    		if(hasattr(objp, "inflated"))
    			printf("an inflatable boat");
    		else	printf("a wad of rubberized fabric");
    		return SUCCESS;
    		}
    
    	return CONTINUE;
    }
    
    When we're printing a list of objects, we'll have to ``call'' the ``.list'' command (more specifically, call the object's func, if it has one, passing a command verb of ``.list''). Setting up one of these special-purpose commands as if it were a ``sentence'' the user typed will be a tiny bit of a nuisance, so we'll write an intermediate function to do it:
    int objcall(struct actor *actp, struct object *objp, char *command)
    {
    	struct sentence cmd;
    	if(objp->func == NULL)
    		return ERROR;
    	cmd.verb = command;
    	cmd.object = objp;
    	cmd.preposition = NULL;
    	cmd.xobject = NULL;
    	return (*objp->func)(actp, objp, &cmd);
    }
    
    Now we can rewrite the object-listing code in object.c. Where it used to say something like
    	printf("%s\n", lp->name);
    
    it can now say
    	/* if obj has a function, try letting it list itself */
    	if(lp->func != NULL && objcall(actp, lp, ".list") == SUCCESS)
    		printf("\n");
    	else	printf("%s\n", lp->name);
    
    If objcall returns SUCCESS, the object has printed itself, and all we have to do is append the trailing newline. Otherwise, we print the object's name, as before.

    (This modification to the object-listing code has one problem. If the listobjects function in object.c is going to call objcall in this way, it needs a pointer to the actor, but it doesn't have it. So we're going to have to rewrite the listobjects function, and each of the places it's called, to pass the actor pointer as an additional argument. As it happens, two of the other improvements suggested in this assignment end up requiring exactly the same change. It turns out that listobjects probably should have been accepting the actor pointer as an extra argument all along, if there are so many good reasons why it ends up needing it.)

    There are several other situations where we can use custom, ``internal'' functions like these. We can arrange that an object also be able to print its long description, by interpreting a ``.describe'' verb. (We'd rewrite the ``examine'' command to print an object's desc field only if the object didn't succeed at performing the ``.describe'' verb.) We could also implement customized ``hooks'' into the action of picking up an object. In last week's assignment (exercise 5), we implemented an object with a custom ``take'' command which printed some text but then returned CONTINUE, so that the rest of the default ``take'' machinery would handle the actual picking up of the object. But we might want the custom ``take'' action (and any messages it prints) to happen after the default machinery has performed most of the picking up of the object. To do this, we could define a new special verb ``.take2'', and rewrite the ``take'' code in commands.c to call .take2 at the very end, after the call to takeobject.
  2. When we started writing object-specific functions last week, we placed them in a file called objfuncs.c. Although (as we've begun to see) it can be a fantastically powerful ability to allow objects to ``point at'' arbitrarily-complicated functions written just for them, in practice, writing these functions will still be a nuisance. Suppose we've just been playing with the data file, and we've come up with a fun new object to put in the dungeon for the player to play with, and the object needs some new, special-purpose code to make it do its stuff. We'd have to write that code (in C, of course), and then recompile the whole game, before we could hook the new function up to the new object. The whole point of the data file is that the information about objects (and the rest of the dungeon) can be read out of it; we stopped editing the source files to change the dungeon way back during the first week, when we introduced the data file in the first place.

    So, another major leap will be to put actual code (not just a pointer to a function) in objects. This code will, however, not be written in C, for a variety of reasons. (A sufficient reason is that there's no easy way to get the C compiler to compile scraps of source code we're reading from a data file and to attach the resultant object code to some data structures in the already-running program.) So, we'll take the (seemingly big, but it's not that bad) step of defining our own little miniature language for describing what objects can do, writing an interpreter (not a compiler) which reads and implements this language, and then writing the code scraps for objects in the little language.

    Therefore, we'll add another field to struct object, right next to the func field we added last week:
    	char *script;
    
    For objects which contain these new, ``scripted'' functions, func will be a pointer to the script-interpreting function (one function, the same function, for all objects which are scripted), and the script field will contain the text of the script to be interpreted.

    First, a description of the little language. It is optimized for the kinds of things that we want our object functions (at least the simple ones) to be able to do: check attributes of the tool or direct object, set attributes of the tool or direct object, and print messages. Here is a sample script:
    	entry "break"
    	chktoolqual "heavy"
    	csetobjstate "broken"
    	message "It is broken."
    	return 1
    
    This is some code which might be suitable for the hammer object. The entry line says that this is code for the verb ``break''. (There can potentially be many entry lines, if a tool can be used in multiple ways.) The chktoolqual line makes sure that the tool has the given attribute, and prints an appropriate message (and fails) if it does not. The csetobjstate conditionally sets the state of the direct object, unless the object already has that state, in which case it prints a message like ``The x is already broken'', and fails. Finally, the message line prints a message, and the return line returns.

    The code for the interpreter which will execute these little scripts is too large to print out, but it's in the week8 subdirectory of the distributed source code. It interfaces with the rest of the game in two places. We must arrange for the interpreter to be called at the right time(s), but that's done already: if an object's func pointer points to the new interpreter function, the code we added to docommand last week will call it. But we must also be able to read in a script (from the data file) along with the rest of a description of an object. Here is a new case for parsedatafile:
    	else if(strcmp(av[0], "script") == 0)
    		{
    		/* XXX cumbersome un-getwords required */
    		int i, l = 0;
    		if(currentobject == NULL)
    			continue;
    		for(i = 1; i < ac; i++)
    			l += strlen(av[i]) + 1;
    		currentobject->script = chkmalloc(l);
    		*currentobject->script = '\0';
    		for(i = 1; i < ac; i++)
    			{
    			if(i > 1)
    				strcat(currentobject->script, " ");
    			strcat(currentobject->script, av[i]);
    			}
    
    		currentobject->func = interp;
    		}
    
    The script is read all from one line (which is a nuisance, but this is a preliminary implementation). The long description reading code faced the same problem; this code uses a different solution, by un-doing the action of getwords and rebuilding a single string (in malloc'ed memory) which the script field can point to.

    Integrating the interpreter code requires paying attention to a few other details. You'll need to add the prototype
    	extern int interp(struct actor *, struct object *, struct sentence *);
    
    to game.h if it's not there already, and the line
    	objp->script = NULL;
    
    to the newobject function in object.c. Also, for the moment, we'll have to pay attention to the numeric values of the status codes (SUCCESS, FAILURE, CONTINUE, ERROR) we defined last week, because the simple interpreter doesn't know how to handle the symbolic names. Instead, we'll have to say return 1 for SUCCESS, etc.
  3. This game is a prime candidate for moving from C to C++. Actors and rooms are really special cases of objects: Rooms contain objects, just like actors and container objects do, and rooms also contain actors. Rather than having separate structures for rooms, actors, and objects, we'd like to say things like ``a room is just like an object, but it also has exits.'' If we rigged it up right (and cleaned up a number of messes which we've perpetrated along the way because of the fact that we hadn't been using this structure) we could have a single piece of code which would transfer objects between rooms and actors (``take''), between actors and rooms (``drop''), between actors and (container) objects (``put in'') and which would even transfer actors between rooms (when the actor moved from one room to another). This function (and much of the rest of the game) could operate on generic ``objects,'' without worrying about whether they were simple objects, actors, or rooms. Only those portions of the game specific to operating on actors or rooms would look at the additional fields differentiating an actor or room from an object.

    The notion that some data structures are extensions of others, that an ``x'' is just like a ``y'' except for some extra stuff, is another cornerstone (perhaps the cornerstone) of Object-Oriented Programming. The formal term for this idea is inheritance, and the data structures are usually spoken of as being objects, which is precisely what the word ``object'' is doing in ``Object-Oriented Programming.'' (It's again more than an coincidence, but rather an indication that our game is a perfect application of C++ or another object-oriented language, that we were already calling our fundamental data structures ``objects.'')

    The changes to the game to let it use C++ are extensive, and I'm not going to try to present them all here. (I've completed many of the changes, however, and you can find them in subdirectories of the week8 directory associated with the on-line web pages for this class.) The basic idea is that our old struct object is no longer just a structure; it is a class:
    class object
    	{
    public:
    	object(const char *);
    	~object(void);
    
    	char name[MAXNAME];
    	struct list *attrs;
    	object *contents;
    	object *container;
    	object *lnext;			/* next in list of contained objects */
    					/* (i.e. in this object's container) */
    	char *desc;			/* long description */
    	};
    
    (Among other things, all objects now contain pointers back to their containers, analogous to the way struct actor used to contain a pointer back to its location.)

    We write a constructor for new instances of this class, replacing our old newobject function in object.c:
    object::object(const char *newname)
    {
    strcpy(name, newname);
    lnext = NULL;
    attrs = NULL;
    contents = NULL;
    container = NULL;
    desc = NULL;
    }
    
    Wherever we used to write something like
    	objp = newobject(name);
    
    we instead use the C++ new operator:
    	objp = new object(name);
    
    (Actually, the only place we called newobject was in readdatafile in io.c, when setting currentobject. It is entirely a coincidence, though not a surprising one, that the use of the C++ new operator here looks so eerily similar to the old newobject call.)

    Going back to game.h, we define the actor and room classes as being derived from class object:
    class actor : public object
    	{
    public:
    	actor();
    	};
    
    class room : public object
    	{
    public:
    	room(const char *);
    
    	room *exits[NEXITS];
    	struct room *next;		/* list of all rooms */
    	};
    
    These say that an actor is just like an object (we don't actually have any actor-specific information at the moment), and that a room is just like an object except that it has an exits array and an extra next pointer so that we can construct a list of all rooms.

    Here is the new, general-purpose object-transferring function, for object.c:
    /* transfer object from one general container to another */
    
    transferobject(object *objp, object *newcontainer)
    {
    object *lp;
    object *prevlp = NULL;
    
    if(objp->container != NULL)
    	{
    	object *oldc = objp->container;
    	for(lp = oldc->contents; lp != NULL; lp = lp->lnext)
    		{
    		if(lp == objp)				/* found it */
    			{
    			/* splice out of old container's list */
    			if(lp == oldc->contents)	/* head of list */
    				oldc->contents = lp->lnext;
    			else	prevlp->lnext = lp->lnext;
    			break;
    			}
    		prevlp = lp;
    		}
    	}
    
    /* splice into new container's list */
    
    if(newcontainer != NULL)
    	{
    	objp->lnext = newcontainer->contents;
    	newcontainer->contents = objp;
    	}
    
    objp->container = newcontainer;
    
    return TRUE;
    }
    
    Notice that the parameters are declared as being of type ``object *''. In C++, every structure and class you declare has its tag name implicitly defined as a typedef, so that the keyword struct or class is no longer needed in later declarations. (Theoretically, we should seek out every instance of struct object in the game and replace them with object or perhaps class object, and similarly for every struct actor and struct room. The compiler I'm using, the C++ version of the GNU C Compiler, namely g++, doesn't seem to be complaining about stray struct keywords referring to what I've actually redefined as classes, but I'm not sure what the formal rules of C++ say.)

    In any case, even though transferobject looks like it's defined only for use on objects, since actors and rooms are now also objects, we can also use transferobject to move objects to and from the actor, and even to move the actor between rooms. That is, we can rewrite the other transfer functions from object.c and rooms.c very simply:
    takeobject(actor *actp, object *objp)
    {
    return transferobject(objp, actp);
    }
    
    dropobject(actor *actp, object *objp)
    {
    return transferobject(objp, actp->container);
    }
    
    putobject(actor *actp, object *objp, object *container)
    {
    return transferobject(objp, container);
    }
    
    int
    gotoroom(actor *actor, room *room)
    {
    return transferobject(actor, room);
    }
    
  4. Another improvement which you might be interested in (and another sweeping one) would be to rewrite the game so that several people could play it at once, over the network. Instead of one instance of struct actor, representing one player sitting at one keyboard and viewing output on one screen, we could have arbitrarily many actor structures (just as we now have arbitrarily many objects and rooms), with each actor representing a player sitting somewhere on the network, typing input and receiving output over a network connection.

    The changes to make a multiplayer version of the game are actually rather straightforward and self-contained, with the glaring exception of the fact that everywhere we used to call printf to print some text to the user's screen, we must instead call some special output function of our own which knows how to send the text to the network connection of the appropriate player.

    To represent the network connection, we'll add a file descriptor to the actor structure. If we're using Unix-style networking, the file descriptor will just be an integer:
    	int remotefd;
    
    If we've gone over to the C++ style of doing things, this means that class actor now does have something to distinguish it from a plain object:
    class actor : public object
    	{
    public:
    	actor();
    	int remotefd;
    	struct actor *next;		/* list of all actors */
    	};
    
    (It turns out we're also going to need a list of all players, just like we need a list of all rooms.)

    Then, using what we learned about variable-length argument lists in chapter 25 of the class notes, we can write an output function which is like printf except that it writes the message to a particular player's network connection:
    void output(struct actor *actp, char *msg, ...)
    {
    char tmpbuf[200];	/* XXX */
    va_list argp;
    va_start(argp, msg);
    vsprintf(tmpbuf, msg, argp);
    va_end(argp);
    write(actp->remotefd, tmpbuf, strlen(tmpbuf));
    }
    
    We call vsprintf to ``print'' the printf-style message to a temporary string buffer, then use the low-level Unix write function to write the string to the network connection. (This function has a significant limitation as written: if a single message is ever more than 200 characters long, the tmpbuf array will overflow, with results which might range from annoying to catastrophic. It's unfortunately tricky to this sort of thing right; the comment /* XXX */ is a reminder that the tmpbuf array with its fixed size of 200 is a weakness in the program.)

    The rest of the code for the multiplayer version of the game is too bulky to include here, but I've placed it in subdirectories of the week8 directory, too. See the README file there for more information.


This page by Steve Summit // Copyright 1995-9 // mail feedback